Add documentation and support for three.js

pull/2392/head
Chaitanya Rahalkar 1 month ago
parent c14d91e966
commit 2f0ad7c09f

@ -0,0 +1,56 @@
._threejs {
// Code blocks
pre, code {
background-color: #f5f5f5;
border-radius: 3px;
padding: 0.2em 0.4em;
}
pre {
padding: 1em;
margin: 1em 0;
overflow: auto;
code {
background: none;
padding: 0;
}
}
// Links
a {
color: #049EF4;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// Headings
h2 {
margin-top: 2em;
padding-bottom: 0.3em;
border-bottom: 1px solid #eaecef;
}
h3 {
margin-top: 1.5em;
}
// Tables
table {
border-collapse: collapse;
margin: 1em 0;
width: 100%;
}
th, td {
border: 1px solid #dfe2e5;
padding: 6px 13px;
}
tr:nth-child(2n) {
background-color: #f6f8fa;
}
}

@ -289,3 +289,11 @@ it to `docs/sqlite`
```sh ```sh
curl https://sqlite.org/2022/sqlite-doc-3400000.zip | bsdtar --extract --file - --directory=docs/sqlite/ --strip-components=1 curl https://sqlite.org/2022/sqlite-doc-3400000.zip | bsdtar --extract --file - --directory=docs/sqlite/ --strip-components=1
``` ```
## Three.js
```sh
git clone --depth 1 --branch r${VERSION} https://github.com/mrdoob/three.js.git
mv three.js/docs/ docs/threejs~${VERSION}/
rm -rf three.js/
```

@ -0,0 +1,348 @@
module Docs
class Threejs
class CleanHtmlFilter < Filter
def call
# Remove unnecessary elements
css('head, script, style').remove
# Add syntax highlighting CSS
style = doc.document.create_element('style')
style.content = <<-CSS
.highlight { background: #272b30; color: #e9ecef; border-radius: 4px; margin: 1em 0; }
.highlight pre { margin: 0; padding: 10px; }
.highlight .k { color: #cc7832; font-weight: bold; } /* Keyword */
.highlight .kd { color: #cc7832; font-weight: bold; } /* Keyword.Declaration */
.highlight .nb { color: #6897bb; } /* Name.Builtin */
.highlight .nx { color: #ffc66d; } /* Name.Other */
.highlight .nf { color: #ffc66d; } /* Name.Function */
.highlight .mi { color: #6897bb; } /* Literal.Number.Integer */
.highlight .s1 { color: #6a8759; } /* Literal.String.Single */
.highlight .s2 { color: #6a8759; } /* Literal.String.Double */
.highlight .c1 { color: #808080; font-style: italic; } /* Comment.Single */
.highlight .lineno { color: #606366; margin-right: 10px; -webkit-user-select: none; user-select: none; }
.highlight-javascript { padding: 0; }
/* Method signatures */
.sig { padding: 5px 10px; }
.sig-name { color: #ffc66d; }
.sig-param { color: #e9ecef; }
.sig-param .sig-type { color: #6897bb; }
.sig-returns { color: #cc7832; }
.sig-returns .sig-type { color: #6897bb; }
.sig-paren { color: #e9ecef; }
.property .pre { color: #cc7832; }
/* Inline code */
code.literal { background: #2b2b2b; padding: 2px 4px; border-radius: 3px; }
code.literal .pre { color: #e9ecef; }
/* Links */
.reference { color: #6897bb; text-decoration: none; }
.reference:hover { text-decoration: underline; }
.reference.external { color: #6a8759; }
/* Notes */
.admonition.note { background: #2b2b2b; padding: 12px 15px; border-left: 4px solid #6897bb; margin: 1em 0; }
.admonition-title { color: #6897bb; font-weight: bold; margin: 0 0 5px 0; }
CSS
doc.at_css('head') ? doc.at_css('head').add_child(style) : doc.add_child(style)
# Create a wrapper div for better styling
if root = at_css('body')
content = root.inner_html
else
content = doc.inner_html
end
# Create Django-like structure
content = <<-HTML
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body" role="main">
#{content}
</div>
</div>
</div>
</div>
HTML
doc.inner_html = content
# Handle source links
css('h2').each do |node|
if node.content.strip == 'Source'
content = node.next_element&.inner_html
if content
# Clean up any existing formatting
content = content.gsub(/<[^>]+>/, '')
# Extract the path from the content
if content =~ /src\/(.*?)\.js/
path = "/#{$1}.js"
formatted_link = %Q(<a class="reference external" href="https://github.com/mrdoob/three.js/blob/master/src#{path}">src#{path}</a>)
node.next_element.inner_html = formatted_link if node.next_element
end
end
end
end
# Handle method signatures
css('h3').each do |node|
content = node.inner_html
# Handle [method:this methodName]( param1, param2, ... ) format
content = content.gsub(/\[method:this\s+([^\]]+)\]\s*\((.*?)\)/) do |match|
method_name, params_str = $1, $2
# Format parameters
params = params_str.split(',').map do |param|
param = param.strip
if param.include?(' ')
type, name = param.split(' ', 2).map(&:strip)
"<span class='sig-param'><span class='sig-type'>#{type}</span> <span class='sig-name'>#{name}</span></span>"
else
"<span class='sig-param'>#{param}</span>"
end
end.join("<span class='sig-paren'>, </span>")
"<dt class='sig sig-object js' id='#{method_name}'>" \
"<span class='property'><span class='pre'>this</span></span>." \
"<span class='sig-name descname'>#{method_name}</span>" \
"<span class='sig-paren'>(</span>" \
"#{params}" \
"<span class='sig-paren'>)</span></dt>"
end
# Handle [method:returnType methodName]( param1, param2, ... ) format
content = content.gsub(/\[method:([^\s\]]+)\s+([^\]]+)\]\s*\((.*?)\)/) do |match|
return_type, method_name, params_str = $1, $2, $3
next if method_name.start_with?('this') # Skip if already handled above
# Format parameters
params = params_str.split(',').map do |param|
param = param.strip
if param.include?(' ')
type, name = param.split(' ', 2).map(&:strip)
"<span class='sig-param'><span class='sig-type'>#{type}</span> <span class='sig-name'>#{name}</span></span>"
else
"<span class='sig-param'>#{param}</span>"
end
end.join("<span class='sig-paren'>, </span>")
"<dt class='sig sig-object js' id='#{method_name}'>" \
"<span class='sig-name descname'>#{method_name}</span>" \
"<span class='sig-paren'>(</span>" \
"#{params}" \
"<span class='sig-paren'>)</span>" \
"<span class='sig-returns'><span class='sig-colon'>:</span> " \
"<span class='sig-type'>#{return_type}</span></span></dt>"
end
# Handle [method:returnType methodName] format (no parameters)
content = content.gsub(/\[method:([^\s\]]+)\s+([^\]]+)\](?!\()/) do |match|
return_type, method_name = $1, $2
"<dt class='sig sig-object js' id='#{method_name}'>" \
"<span class='sig-name descname'>#{method_name}</span>" \
"<span class='sig-paren'>(</span>" \
"<span class='sig-paren'>)</span>" \
"<span class='sig-returns'><span class='sig-colon'>:</span> " \
"<span class='sig-type'>#{return_type}</span></span></dt>"
end
node.inner_html = content
end
# Handle [name] placeholders in headers and constructor
css('h1, h3').each do |node|
content = node.inner_html
# Replace [name] with class name
content = content.gsub(/\[name\]/) do
name = slug.split('/').last.gsub('.html', '')
"<span class='descname'>#{name}</span>"
end
# Format constructor parameters
content = content.gsub(/\[param:([^\]]+?)\s+([^\]]+?)\]/) do |match|
type, name = $1, $2
"<span class='sig-param'><span class='sig-type'>#{type}</span> <code class='sig-name'>#{name}</code></span>"
end
node.inner_html = content
end
# Clean up property formatting
css('h3').each do |node|
node.inner_html = node.inner_html.gsub(/\[property:([^\]]+?)\s+([^\]]+?)\]/) do |match|
type, name = $1, $2
"<dt class='sig sig-object js'>" \
"<span class='sig-name descname'>#{name}</span>" \
"<span class='sig-colon'>:</span> " \
"<span class='sig-type'>#{type}</span></dt>"
end
end
# Clean up external links
css('*').each do |node|
next if node.text?
# Handle example links [example:tag Title]
node.inner_html = node.inner_html.gsub(/\[example:([^\s\]]+)\s+([^\]]+)\]/) do |match|
tag, title = $1, $2
"<a class='reference external' href='https://threejs.org/examples/##{tag}'>#{title}</a>"
end
# Handle external links with [link:url text] format
node.inner_html = node.inner_html.gsub(/\[link:([^\s\]]+)\s+([^\]]+)\]/) do |match|
url, text = $1, $2
"<a class='reference external' href='#{url}'>#{text}</a>"
end
# Handle external links with [link:url] format
node.inner_html = node.inner_html.gsub(/\[link:([^\]]+)\]/) do |match|
url = $1
"<a class='reference external' href='#{url}'>#{url}</a>"
end
# Handle internal page links with text
node.inner_html = node.inner_html.gsub(/\[page:([^\]]+?)\s+([^\]]+?)\]/) do
path, text = $1, $2
"<a class='reference internal' href='#{path.downcase}'><code class='xref js js-#{path.downcase}'>#{text}</code></a>"
end
# Handle internal page links without text
node.inner_html = node.inner_html.gsub(/\[page:([^\]]+?)\]/) do |match|
path = $1
"<a class='reference internal' href='#{path.downcase}'><code class='xref js js-#{path.downcase}'>#{path}</code></a>"
end
end
# Fix all href attributes to be lowercase and remove .html
css('a[href]').each do |link|
next if link['href'].start_with?('http')
link['href'] = link['href'].remove('../').downcase.sub(/\.html$/, '')
link['class'] = 'reference internal'
end
# Add section classes
css('h2').each do |node|
node['class'] = 'section-title'
section = node.next_element
if section
wrapper = doc.document.create_element('div')
wrapper['class'] = 'section'
node.after(wrapper)
wrapper.add_child(node)
current = section
while current && current.name != 'h2'
next_el = current.next
wrapper.add_child(current)
current = next_el
end
end
end
# Format description paragraphs
css('p.desc').each do |node|
node['class'] = 'section-desc'
end
# Handle inline code/backticks in text
css('p, li, dt, dd').each do |node|
next if node.at_css('pre') # Skip if contains a code block
# Replace backticks with proper code formatting
node.inner_html = node.inner_html.gsub(/`([^`]+)`/) do |match|
code = $1
"<code class='docutils literal notranslate'><span class='pre'>#{code}</span></code>"
end
end
# Handle inline code in property descriptions
css('.property-type').each do |node|
node.inner_html = node.inner_html.gsub(/`([^`]+)`/) do |match|
code = $1
"<code class='docutils literal notranslate'><span class='pre'>#{code}</span></code>"
end
end
# Clean up code blocks
css('pre').each do |node|
wrapper = doc.document.create_element('div')
wrapper['class'] = 'highlight'
node.replace(wrapper)
div = doc.document.create_element('div')
div['class'] = 'highlight-javascript notranslate'
wrapper.add_child(div)
pre = doc.document.create_element('pre')
pre['class'] = ''
div.add_child(pre)
# Format the code content
code = node.content.strip
# Add syntax highlighting spans
highlighted_code = highlight_javascript(code)
pre.inner_html = highlighted_code
end
# Add proper heading IDs and classes
css('h1, h2, h3, h4').each do |node|
node['id'] ||= node.content.strip.downcase.gsub(/[^\w]+/, '-')
existing_class = node['class'].to_s
node['class'] = "#{existing_class} section-header"
end
# Remove interactive examples
css('.threejs_example_container').remove
# Add note styling
css('p').each do |node|
if node.content.start_with?('Note:')
wrapper = doc.document.create_element('div')
wrapper['class'] = 'admonition note'
title = doc.document.create_element('p')
title['class'] = 'first admonition-title'
title.content = 'Note'
content = doc.document.create_element('p')
content['class'] = 'last'
content.inner_html = node.inner_html.sub('Note:', '').strip
wrapper.add_child(title)
wrapper.add_child(content)
node.replace(wrapper)
end
end
doc
end
private
def highlight_javascript(code)
code = code.gsub(/\b(function|return|var|let|const|if|else|for|while|do|switch|case|break|continue|new|try|catch|throw|this|typeof|instanceof|in|of|class|extends|super|import|export|default|null|undefined|true|false)\b/, '<span class="k">\1</span>') # keywords
code = code.gsub(/\b(\d+(\.\d+)?)\b/, '<span class="mi">\1</span>') # numbers
code = code.gsub(/'([^']*)'/, '<span class="s1">\'\1\'</span>') # single quotes
code = code.gsub(/"([^"]*)"/, '<span class="s2">"\1"</span>') # double quotes
code = code.gsub(/`([^`]*)`/, '<span class="s2">`\1`</span>') # template literals
code = code.gsub(/\/\/[^\n]*/, '<span class="c1">\0</span>') # single line comments
code = code.gsub(/\b(console|document|window|Array|Object|String|Number|Boolean|Function|Symbol|Map|Set|Promise|async|await)\b/, '<span class="nb">\1</span>') # built-ins
code = code.gsub(/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/, '<span class="nx">\1</span>(') # function calls
code = code.gsub(/\b(addEventListener|querySelector|getElementById|setTimeout|setInterval)\b/, '<span class="nx">\1</span>') # common methods
# Add line numbers
lines = code.split("\n")
numbered_lines = lines.map.with_index(1) do |line, i|
"<span class=\"lineno\">#{i}</span>#{line}"
end
numbered_lines.join("\n")
end
end
end
end

@ -0,0 +1,54 @@
module Docs
class Threejs
class EntriesFilter < Docs::EntriesFilter
def get_name
# Try to get name from the title first
if title = at_css('.lesson-title h1')&.content
title
else
# Fallback to path-based name for API docs
slug.split('/').last.gsub('.html', '').titleize
end
end
def get_type
if slug.start_with?('api/en/')
# For API documentation, use the section as type
# e.g. "api/en/animation/AnimationAction" -> "Animation"
path_parts = slug.split('/')
if path_parts.length >= 3
path_parts[2].titleize
else
'API'
end
elsif slug.start_with?('manual/en/')
# For manual pages, get the section from the path
# e.g. "manual/en/introduction/Creating-a-scene" -> "Introduction"
path_parts = slug.split('/')
if path_parts.length >= 3
path_parts[2].titleize
else
'Manual'
end
else
'Other'
end
end
def additional_entries
entries = []
# Get all methods and properties from h3 headings
css('h3').each do |node|
name = node.content.strip
# Skip if it's a constructor or doesn't have an ID
next if name == get_name || !node['id']
entries << [name, node['id'], get_type]
end
entries
end
end
end
end

@ -0,0 +1,85 @@
module Docs
class Threejs < FileScraper
self.name = 'Three.js'
self.type = 'threejs'
self.slug = 'threejs'
self.links = {
home: 'https://threejs.org/',
code: 'https://github.com/mrdoob/three.js'
}
html_filters.push 'threejs/entries', 'threejs/clean_html'
# The content is directly in the body
options[:container] = 'body'
options[:skip] = %w(
prettify.js
lesson.js
lang.css
lesson.css
editor.html
list.js
page.js
)
options[:only_patterns] = [
/\Aapi\/en\/.+\.html/, # API documentation
/\Amanual\/en\/.+\.html/ # Manual pages
]
options[:skip_patterns] = [
/examples/,
/\A_/,
/\Aresources\//,
/\Ascenes\//
]
options[:attribution] = <<-HTML
&copy; 2010&ndash;#{Time.current.year} Three.js Authors<br>
Licensed under the MIT License.
HTML
self.class_attribute :release
version '160' do
self.release = '160'
self.base_url = "https://threejs.org/docs"
end
def get_latest_version(opts)
get_latest_github_release('mrdoob', 'three.js', opts)
end
def initial_paths
paths = []
json_path = File.expand_path("docs/threejs~#{self.release}/list.json")
json_content = File.read(json_path)
json_data = JSON.parse(json_content)
# Process both API and manual sections
process_documentation(json_data['en'], paths)
paths
end
private
def process_documentation(data, paths, prefix = '')
data.each do |category, items|
if items.is_a?(Hash)
if items.values.first.is_a?(String)
# This is a leaf node with actual pages
items.each do |name, path|
paths << "#{path}.html"
end
else
# This is a category with subcategories
items.each do |subcategory, subitems|
process_documentation(items, paths, "#{prefix}#{category}/")
end
end
end
end
end
end
end
Loading…
Cancel
Save