History

Merge pull request #2392 from chaitanyarahalkar/add/threejs-docs

feat(documentation): Add documentation support for three.js

Simon Legner committed 1 year ago

Commit 1c07f172 parents 8f29e60 6dbe8348 files · +382 −-2Browse files

Unified · Split

8 files
  1. assets/javascripts/news.json +4 −0
  2. docs/file-scrapers.md +15 −0
  3. lib/docs/filters/threejs/clean_html.rb +229 −0
  4. lib/docs/filters/threejs/entries.rb +54 −0
  5. lib/docs/scrapers/threejs.rb +81 −0
  6. public/icons/docs/threejs/16.png +0 −0
  7. public/icons/docs/threejs/16@2x.png +0 −0
  8. public/icons/docs/threejs/SOURCE +1 −0
assets/javascripts/news.json+4−0
@@ -1,4 +1,8 @@
11 [
2+ [
3+ "2025-02-23",
4+ "New documentation: <a href=\"/threejs/\">Three.js</a>"
5+ ],
26 [
37 "2025-02-16",
48 "New documentation: <a href=\"/openlayers/\">OpenLayers</a>"
docs/file-scrapers.md+15−0
@@ -300,3 +300,18 @@ it to `docs/sqlite` · 299 unmodified lines
300300 ```sh
301301 curl https://sqlite.org/2022/sqlite-doc-3400000.zip | bsdtar --extract --file - --directory=docs/sqlite/ --strip-components=1
302302 ```
303+
304+## Three.js
305+Download the docs from https://github.com/mrdoob/three.js/tree/dev/files or run the following commands in your terminal:
306+Make sure to set the version per the release tag (e.g. r160). Note that the r prefix is already included, only the version number is needed.
307+
308+```sh
309+curl https://codeload.github.com/mrdoob/three.js/tar.gz/refs/tags/r${VERSION} > threejs.tar.gz
310+tar -xzf threejs.tar.gz
311+mkdir -p docs/threejs~${VERSION}
312+mv three.js-r${VERSION}/list.json tmp/list.json
313+mv three.js-r${VERSION}/docs/* docs/threejs~${VERSION}/
314+
315+rm -rf three.js-r${VERSION}/
316+rm threejs.tar.gz
317+```
lib/docs/filters/threejs/clean_html.rb+229−0
@@ -0,0 +1,229 @@
1+module Docs
2+ class Threejs
3+ class CleanHtmlFilter < Filter
4+ PATTERNS = {
5+ method_this: /\[method:this\s+([^\]]+)\]\s*\((.*?)\)/,
6+ method_return: /\[method:([^\s\]]+)\s+([^\]]+)\]\s*\((.*?)\)/,
7+ method_no_params: /\[method:([^\s\]]+)\s+([^\]]+)\](?!\()/,
8+ property: /\[property:([^\]]+?)\s+([^\]]+?)\]/,
9+ example_link: /\[example:([^\s\]]+)\s+([^\]]+)\]/,
10+ external_link_text: /\[link:([^\s\]]+)\s+([^\]]+)\]/,
11+ external_link: /\[link:([^\]]+)\]/,
12+ page_link_text: /\[page:([^\]]+?)\s+([^\]]+?)\]/,
13+ page_link: /\[page:([^\]]+?)\]/,
14+ inline_code: /`([^`]+)`/,
15+ name_placeholder: /\[name\]/,
16+ constructor_param: /\[param:([^\]]+?)\s+([^\]]+?)\]/
17+ }.freeze
18+
19+ def call
20+ remove_unnecessary_elements
21+ wrap_code_blocks
22+ process_sections
23+ format_links
24+ add_section_structure
25+ format_notes
26+ add_heading_attributes
27+ doc
28+ end
29+
30+ private
31+
32+ def remove_unnecessary_elements
33+ css('head, script, style').remove
34+ end
35+
36+ def wrap_code_blocks
37+ css('code').each do |node|
38+ next if node.parent.name == 'pre'
39+ node.wrap('<pre>')
40+ node.parent['data-language'] = 'javascript'
41+ end
42+ end
43+
44+ def process_sections
45+ # Handle source links
46+ css('h2').each do |node|
47+ next unless node.content.strip == 'Source'
48+ node.next_element.remove
49+ node.remove
50+ end
51+
52+ # Handle method signatures and properties
53+ css('h3').each do |node|
54+ content = node.inner_html
55+ content = handle_method_signatures(content)
56+ content = handle_properties(content)
57+ node.inner_html = content
58+ end
59+
60+ # Handle name placeholders and constructor params
61+ css('h1, h3').each do |node|
62+ content = node.inner_html
63+ content = handle_name_placeholders(content)
64+ content = format_constructor_params(content)
65+ node.inner_html = content
66+ end
67+ end
68+
69+ def handle_method_signatures(content)
70+ content
71+ .gsub(PATTERNS[:method_this]) { format_method_signature('this', $1, $2) }
72+ .gsub(PATTERNS[:method_return]) do |match|
73+ next if $2.start_with?('this')
74+ format_method_signature($1, $2, $3, true)
75+ end
76+ .gsub(PATTERNS[:method_no_params]) { format_method_signature($1, $2, nil, true) }
77+ end
78+
79+ def format_method_signature(type_or_this, name, params_str, with_return = false)
80+ params = if params_str
81+ params_str.split(',').map { |param| format_parameter(param.strip) }.join("<span class='sig-paren'>, </span>")
82+ end
83+
84+ html = "<dt class='sig sig-object js' id='#{name}'>"
85+ if type_or_this == 'this'
86+ html << "<span class='property'><span class='pre'>this</span></span>."
87+ end
88+ html << "<span class='sig-name descname'>#{name}</span>" \
89+ "<span class='sig-paren'>(</span>" \
90+ "#{params}" \
91+ "<span class='sig-paren'>)</span>"
92+ if with_return
93+ html << "<span class='sig-returns'><span class='sig-colon'>:</span> " \
94+ "<span class='sig-type'>#{type_or_this}</span></span>"
95+ end
96+ html << "</dt>"
97+ end
98+
99+ def format_parameter(param)
100+ if param.include?(' ')
101+ type, name = param.split(' ', 2).map(&:strip)
102+ "<span class='sig-param'><span class='sig-type'>#{type}</span> <span class='sig-name'>#{name}</span></span>"
103+ else
104+ "<span class='sig-param'>#{param}</span>"
105+ end
106+ end
107+
108+ def handle_properties(content)
109+ content.gsub(PATTERNS[:property]) do |match|
110+ type, name = $1, $2
111+ "<dt class='sig sig-object js'>" \
112+ "<span class='sig-name descname'>#{name}</span>" \
113+ "<span class='sig-colon'>:</span> " \
114+ "<span class='sig-type'>#{type}</span></dt>"
115+ end
116+ end
117+
118+ def handle_name_placeholders(content)
119+ content.gsub(PATTERNS[:name_placeholder]) do
120+ name = slug.split('/').last.gsub('.html', '')
121+ "<span class='descname'>#{name}</span>"
122+ end
123+ end
124+
125+ def format_constructor_params(content)
126+ content.gsub(PATTERNS[:constructor_param]) do |match|
127+ type, name = $1, $2
128+ "<span class='sig-param'><span class='sig-type'>#{type}</span> <code class='sig-name'>#{name}</code></span>"
129+ end
130+ end
131+
132+ def format_links
133+ css('*').each do |node|
134+ next if node.text?
135+
136+ content = node.inner_html
137+ .gsub(PATTERNS[:example_link]) { create_external_link("https://threejs.org/examples/##{$1}", $2) }
138+ .gsub(PATTERNS[:external_link_text]) { create_external_link($1, $2) }
139+ .gsub(PATTERNS[:external_link]) { create_external_link($1, $1) }
140+ .gsub(PATTERNS[:page_link_text]) { create_internal_link($1, $2) }
141+ .gsub(PATTERNS[:page_link]) { create_internal_link($1, $1) }
142+
143+ node.inner_html = content
144+ end
145+
146+ normalize_href_attributes
147+ end
148+
149+ def create_external_link(url, text)
150+ %Q(<a class='reference external' href='#{url}'>#{text}</a>)
151+ end
152+
153+ def create_internal_link(path, text)
154+ %Q(<a class='reference internal' href='#{path.downcase}'><code class='xref js js-#{path.downcase}'>#{text}</code></a>)
155+ end
156+
157+ def normalize_href_attributes
158+ css('a[href]').each do |link|
159+ next if link['href'].start_with?('http')
160+ link['href'] = link['href'].remove('../').downcase.sub(/\.html$/, '')
161+ link['class'] = 'reference internal'
162+ end
163+ end
164+
165+ def add_section_structure
166+ css('h2').each do |node|
167+ node['class'] = 'section-title'
168+ section = node.next_element
169+ next unless section
170+
171+ wrapper = doc.document.create_element('div')
172+ wrapper['class'] = 'section'
173+ node.after(wrapper)
174+ wrapper.add_child(node)
175+
176+ current = section
177+ while current && current.name != 'h2'
178+ next_el = current.next
179+ wrapper.add_child(current)
180+ current = next_el
181+ end
182+ end
183+
184+ css('p.desc').each { |node| node['class'] = 'section-desc' }
185+ end
186+
187+ def format_notes
188+ css('p').each do |node|
189+ next unless node.content.start_with?('Note:')
190+
191+ wrapper = doc.document.create_element('div')
192+ wrapper['class'] = 'admonition note'
193+
194+ title = doc.document.create_element('p')
195+ title['class'] = 'first admonition-title'
196+ title.content = 'Note'
197+
198+ content = doc.document.create_element('p')
199+ content['class'] = 'last'
200+ content.inner_html = node.inner_html.sub('Note:', '').strip
201+
202+ wrapper.add_child(title)
203+ wrapper.add_child(content)
204+ node.replace(wrapper)
205+ end
206+ end
207+
208+ def add_heading_attributes
209+ css('h1, h2, h3, h4').each do |node|
210+ node['id'] ||= node.content.strip.downcase.gsub(/[^\w]+/, '-')
211+ existing_class = node['class'].to_s
212+ node['class'] = "#{existing_class} section-header"
213+ end
214+
215+ format_inline_code
216+ end
217+
218+ def format_inline_code
219+ selectors = ['p', 'li', 'dt', 'dd', '.property-type'].join(', ')
220+ css(selectors).each do |node|
221+ next if node.at_css('pre')
222+ node.inner_html = node.inner_html.gsub(PATTERNS[:inline_code]) do |match|
223+ "<code class='docutils literal notranslate'><span class='pre'>#{$1}</span></code>"
224+ end
225+ end
226+ end
227+ end
228+ end
229+end
lib/docs/filters/threejs/entries.rb+54−0
@@ -0,0 +1,54 @@
1+module Docs
2+ class Threejs
3+ class EntriesFilter < Docs::EntriesFilter
4+ def get_name
5+ # Try to get name from the title first
6+ if title = at_css('.lesson-title h1')&.content
7+ title
8+ else
9+ # Fallback to path-based name for API docs
10+ slug.split('/').last.gsub('.html', '').titleize
11+ end
12+ end
13+
14+ def get_type
15+ if slug.start_with?('api/en/')
16+ # For API documentation, use the section as type
17+ # e.g. "api/en/animation/AnimationAction" -> "Animation"
18+ path_parts = slug.split('/')
19+ if path_parts.length >= 3
20+ path_parts[2].titleize
21+ else
22+ 'API'
23+ end
24+ elsif slug.start_with?('manual/en/')
25+ # For manual pages, get the section from the path
26+ # e.g. "manual/en/introduction/Creating-a-scene" -> "Introduction"
27+ path_parts = slug.split('/')
28+ if path_parts.length >= 3
29+ path_parts[2].titleize
30+ else
31+ 'Manual'
32+ end
33+ else
34+ 'Other'
35+ end
36+ end
37+
38+ def additional_entries
39+ entries = []
40+
41+ # Get all methods and properties from h3 headings
42+ css('h3').each do |node|
43+ name = node.content.strip
44+ # Skip if it's a constructor or doesn't have an ID
45+ next if name == get_name || !node['id']
46+
47+ entries << [name, node['id'], get_type]
48+ end
49+
50+ entries
51+ end
52+ end
53+ end
54+end
lib/docs/scrapers/threejs.rb+81−0
@@ -0,0 +1,81 @@
1+module Docs
2+ class Threejs < UrlScraper
3+ self.name = 'Three.js'
4+ self.type = 'simple'
5+ self.slug = 'threejs'
6+ self.links = {
7+ home: 'https://threejs.org/',
8+ code: 'https://github.com/mrdoob/three.js'
9+ }
10+
11+ html_filters.push 'threejs/entries', 'threejs/clean_html'
12+
13+ # The content is directly in the body
14+ options[:container] = 'body'
15+
16+ options[:skip] = %w(
17+ prettify.js
18+ lesson.js
19+ lang.css
20+ lesson.css
21+ editor.html
22+ list.js
23+ page.js
24+ )
25+
26+ options[:only_patterns] = [
27+ /\Aapi\/en\/.+\.html/, # API documentation
28+ /\Amanual\/en\/.+\.html/ # Manual pages
29+ ]
30+
31+ options[:skip_patterns] = [
32+ /examples/,
33+ /\A_/,
34+ /\Aresources\//,
35+ /\Ascenes\//
36+ ]
37+
38+ options[:attribution] = <<-HTML
39+ &copy; 2010&ndash;#{Time.current.year} Three.js Authors<br>
40+ Licensed under the MIT License.
41+ HTML
42+
43+ self.release = '173'
44+ self.base_url = "https://threejs.org/docs"
45+
46+ def get_latest_version(opts)
47+ get_latest_github_release('mrdoob', 'three.js', opts)[1..]
48+ end
49+
50+ def initial_paths
51+ paths = []
52+ url = 'https://threejs.org/docs/list.json'
53+ response = Request.run(url)
54+ json_data = JSON.parse(response.body)
55+
56+ # Process both API and manual sections
57+ process_documentation(json_data['en'], paths)
58+ paths
59+ end
60+
61+ private
62+
63+ def process_documentation(data, paths, prefix = '')
64+ data.each do |category, items|
65+ if items.is_a?(Hash)
66+ if items.values.first.is_a?(String)
67+ # This is a leaf node with actual pages
68+ items.each do |name, path|
69+ paths << "#{path}.html"
70+ end
71+ else
72+ # This is a category with subcategories
73+ items.each do |subcategory, subitems|
74+ process_documentation(items, paths, "#{prefix}#{category}/")
75+ end
76+ end
77+ end
78+ end
79+ end
80+ end
81+end
public/icons/docs/threejs/16.png+0−0

Binary file not shown.

public/icons/docs/threejs/16@2x.png+0−0

Binary file not shown.

public/icons/docs/threejs/SOURCE+1−0
@@ -0,0 +1 @@
1+https://github.com/mrdoob/three.js/tree/dev/files