Unified · Split
| @@ -1,243 +1,238 @@ | |||
| 1 | module Docs | 1 | module Docs |
| 2 | class Threejs | 2 | class Threejs |
| 3 | class CleanHtmlFilter < Filter | 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 | |||
| 4 | def call | 19 | def call |
| 5 | # Remove unnecessary elements | 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 | ||
| 6 | css('head, script, style').remove | 33 | css('head, script, style').remove |
| 7 | 34 | end | |
| 8 | # Wrap code blocks with pre tags and add syntax highlighting | 35 | |
| 36 | def wrap_code_blocks | ||
| 9 | css('code').each do |node| | 37 | css('code').each do |node| |
| 10 | unless node.parent.name == 'pre' | 38 | next if node.parent.name == 'pre' |
| 11 | pre = node.wrap('<pre>') | 39 | pre = node.wrap('<pre>') |
| 12 | pre['data-language'] = 'javascript' | 40 | pre['data-language'] = pre['class'] = 'language-javascript' |
| 13 | pre['class'] = 'language-javascript' | ||
| 14 | end | ||
| 15 | end | 41 | end |
| 16 | 42 | end | |
| 43 | |||
| 44 | def process_sections | ||
| 17 | # Handle source links | 45 | # Handle source links |
| 18 | css('h2').each do |node| | 46 | css('h2').each do |node| |
| 19 | if node.content.strip == 'Source' | 47 | next unless node.content.strip == 'Source' |
| 20 | content = node.next_element&.inner_html | 48 | handle_source_link(node) |
| 21 | if content | ||
| 22 | # Clean up any existing formatting | ||
| 23 | content = content.gsub(/<[^>]+>/, '') | ||
| 24 | # Extract the path from the content | ||
| 25 | if content =~ /src\/(.*?)\.js/ | ||
| 26 | path = "/#{$1}.js" | ||
| 27 | formatted_link = %Q(<a class="reference external" href="https://github.com/mrdoob/three.js/blob/master/src#{path}">src#{path}</a>) | ||
| 28 | node.next_element.inner_html = formatted_link if node.next_element | ||
| 29 | end | ||
| 30 | end | ||
| 31 | end | ||
| 32 | end | 49 | end |
| 33 | 50 | ||
| 34 | # Handle method signatures | 51 | # Handle method signatures and properties |
| 35 | css('h3').each do |node| | 52 | css('h3').each do |node| |
| 36 | content = node.inner_html | 53 | content = node.inner_html |
| 37 | 54 | content = handle_method_signatures(content) | |
| 38 | # Handle [method:this methodName]( param1, param2, ... ) format | 55 | content = handle_properties(content) |
| 39 | content = content.gsub(/\[method:this\s+([^\]]+)\]\s*\((.*?)\)/) do |match| | ||
| 40 | method_name, params_str = $1, $2 | ||
| 41 | |||
| 42 | # Format parameters | ||
| 43 | params = params_str.split(',').map do |param| | ||
| 44 | param = param.strip | ||
| 45 | if param.include?(' ') | ||
| 46 | type, name = param.split(' ', 2).map(&:strip) | ||
| 47 | "<span class='sig-param'><span class='sig-type'>#{type}</span> <span class='sig-name'>#{name}</span></span>" | ||
| 48 | else | ||
| 49 | "<span class='sig-param'>#{param}</span>" | ||
| 50 | end | ||
| 51 | end.join("<span class='sig-paren'>, </span>") | ||
| 52 | |||
| 53 | "<dt class='sig sig-object js' id='#{method_name}'>" \ | ||
| 54 | "<span class='property'><span class='pre'>this</span></span>." \ | ||
| 55 | "<span class='sig-name descname'>#{method_name}</span>" \ | ||
| 56 | "<span class='sig-paren'>(</span>" \ | ||
| 57 | "#{params}" \ | ||
| 58 | "<span class='sig-paren'>)</span></dt>" | ||
| 59 | end | ||
| 60 | |||
| 61 | # Handle [method:returnType methodName]( param1, param2, ... ) format | ||
| 62 | content = content.gsub(/\[method:([^\s\]]+)\s+([^\]]+)\]\s*\((.*?)\)/) do |match| | ||
| 63 | return_type, method_name, params_str = $1, $2, $3 | ||
| 64 | next if method_name.start_with?('this') # Skip if already handled above | ||
| 65 | |||
| 66 | # Format parameters | ||
| 67 | params = params_str.split(',').map do |param| | ||
| 68 | param = param.strip | ||
| 69 | if param.include?(' ') | ||
| 70 | type, name = param.split(' ', 2).map(&:strip) | ||
| 71 | "<span class='sig-param'><span class='sig-type'>#{type}</span> <span class='sig-name'>#{name}</span></span>" | ||
| 72 | else | ||
| 73 | "<span class='sig-param'>#{param}</span>" | ||
| 74 | end | ||
| 75 | end.join("<span class='sig-paren'>, </span>") | ||
| 76 | |||
| 77 | "<dt class='sig sig-object js' id='#{method_name}'>" \ | ||
| 78 | "<span class='sig-name descname'>#{method_name}</span>" \ | ||
| 79 | "<span class='sig-paren'>(</span>" \ | ||
| 80 | "#{params}" \ | ||
| 81 | "<span class='sig-paren'>)</span>" \ | ||
| 82 | "<span class='sig-returns'><span class='sig-colon'>:</span> " \ | ||
| 83 | "<span class='sig-type'>#{return_type}</span></span></dt>" | ||
| 84 | end | ||
| 85 | |||
| 86 | # Handle [method:returnType methodName] format (no parameters) | ||
| 87 | content = content.gsub(/\[method:([^\s\]]+)\s+([^\]]+)\](?!\()/) do |match| | ||
| 88 | return_type, method_name = $1, $2 | ||
| 89 | "<dt class='sig sig-object js' id='#{method_name}'>" \ | ||
| 90 | "<span class='sig-name descname'>#{method_name}</span>" \ | ||
| 91 | "<span class='sig-paren'>(</span>" \ | ||
| 92 | "<span class='sig-paren'>)</span>" \ | ||
| 93 | "<span class='sig-returns'><span class='sig-colon'>:</span> " \ | ||
| 94 | "<span class='sig-type'>#{return_type}</span></span></dt>" | ||
| 95 | end | ||
| 96 | |||
| 97 | node.inner_html = content | 56 | node.inner_html = content |
| 98 | end | 57 | end |
| 99 | 58 | ||
| 100 | # Handle [name] placeholders in headers and constructor | 59 | # Handle name placeholders and constructor params |
| 101 | css('h1, h3').each do |node| | 60 | css('h1, h3').each do |node| |
| 102 | content = node.inner_html | 61 | content = node.inner_html |
| 103 | 62 | content = handle_name_placeholders(content) | |
| 104 | # Replace [name] with class name | 63 | content = format_constructor_params(content) |
| 105 | content = content.gsub(/\[name\]/) do | ||
| 106 | name = slug.split('/').last.gsub('.html', '') | ||
| 107 | "<span class='descname'>#{name}</span>" | ||
| 108 | end | ||
| 109 | |||
| 110 | # Format constructor parameters | ||
| 111 | content = content.gsub(/\[param:([^\]]+?)\s+([^\]]+?)\]/) do |match| | ||
| 112 | type, name = $1, $2 | ||
| 113 | "<span class='sig-param'><span class='sig-type'>#{type}</span> <code class='sig-name'>#{name}</code></span>" | ||
| 114 | end | ||
| 115 | |||
| 116 | node.inner_html = content | 64 | node.inner_html = content |
| 117 | end | 65 | end |
| 66 | end | ||
| 118 | 67 | ||
| 119 | # Clean up property formatting | 68 | def handle_source_link(node) |
| 120 | css('h3').each do |node| | 69 | content = node.next_element&.inner_html |
| 121 | node.inner_html = node.inner_html.gsub(/\[property:([^\]]+?)\s+([^\]]+?)\]/) do |match| | 70 | return unless content |
| 122 | type, name = $1, $2 | 71 | content = content.gsub(/<[^>]+>/, '') |
| 123 | "<dt class='sig sig-object js'>" \ | 72 | if content =~ /src\/(.*?)\.js/ |
| 124 | "<span class='sig-name descname'>#{name}</span>" \ | 73 | path = "/#{$1}.js" |
| 125 | "<span class='sig-colon'>:</span> " \ | 74 | formatted_link = %Q(<a class="reference external" href="https://github.com/mrdoob/three.js/blob/master/src#{path}">src#{path}</a>) |
| 126 | "<span class='sig-type'>#{type}</span></dt>" | 75 | node.next_element.inner_html = formatted_link if node.next_element |
| 76 | end | ||
| 77 | end | ||
| 78 | |||
| 79 | def handle_method_signatures(content) | ||
| 80 | content | ||
| 81 | .gsub(PATTERNS[:method_this]) { format_method_signature('this', $1, $2) } | ||
| 82 | .gsub(PATTERNS[:method_return]) do |match| | ||
| 83 | next if $2.start_with?('this') | ||
| 84 | format_method_signature($1, $2, $3, true) | ||
| 127 | end | 85 | end |
| 86 | .gsub(PATTERNS[:method_no_params]) { format_method_signature($1, $2, nil, true) } | ||
| 87 | end | ||
| 88 | |||
| 89 | def format_method_signature(type_or_this, name, params_str, with_return = false) | ||
| 90 | params = if params_str | ||
| 91 | params_str.split(',').map { |param| format_parameter(param.strip) }.join("<span class='sig-paren'>, </span>") | ||
| 128 | end | 92 | end |
| 129 | 93 | ||
| 130 | # Clean up external links | 94 | html = "<dt class='sig sig-object js' id='#{name}'>" |
| 95 | if type_or_this == 'this' | ||
| 96 | html << "<span class='property'><span class='pre'>this</span></span>." | ||
| 97 | end | ||
| 98 | html << "<span class='sig-name descname'>#{name}</span>" \ | ||
| 99 | "<span class='sig-paren'>(</span>" \ | ||
| 100 | "#{params}" \ | ||
| 101 | "<span class='sig-paren'>)</span>" | ||
| 102 | if with_return | ||
| 103 | html << "<span class='sig-returns'><span class='sig-colon'>:</span> " \ | ||
| 104 | "<span class='sig-type'>#{type_or_this}</span></span>" | ||
| 105 | end | ||
| 106 | html << "</dt>" | ||
| 107 | end | ||
| 108 | |||
| 109 | def format_parameter(param) | ||
| 110 | if param.include?(' ') | ||
| 111 | type, name = param.split(' ', 2).map(&:strip) | ||
| 112 | "<span class='sig-param'><span class='sig-type'>#{type}</span> <span class='sig-name'>#{name}</span></span>" | ||
| 113 | else | ||
| 114 | "<span class='sig-param'>#{param}</span>" | ||
| 115 | end | ||
| 116 | end | ||
| 117 | |||
| 118 | def handle_properties(content) | ||
| 119 | content.gsub(PATTERNS[:property]) do |match| | ||
| 120 | type, name = $1, $2 | ||
| 121 | "<dt class='sig sig-object js'>" \ | ||
| 122 | "<span class='sig-name descname'>#{name}</span>" \ | ||
| 123 | "<span class='sig-colon'>:</span> " \ | ||
| 124 | "<span class='sig-type'>#{type}</span></dt>" | ||
| 125 | end | ||
| 126 | end | ||
| 127 | |||
| 128 | def handle_name_placeholders(content) | ||
| 129 | content.gsub(PATTERNS[:name_placeholder]) do | ||
| 130 | name = slug.split('/').last.gsub('.html', '') | ||
| 131 | "<span class='descname'>#{name}</span>" | ||
| 132 | end | ||
| 133 | end | ||
| 134 | |||
| 135 | def format_constructor_params(content) | ||
| 136 | content.gsub(PATTERNS[:constructor_param]) do |match| | ||
| 137 | type, name = $1, $2 | ||
| 138 | "<span class='sig-param'><span class='sig-type'>#{type}</span> <code class='sig-name'>#{name}</code></span>" | ||
| 139 | end | ||
| 140 | end | ||
| 141 | |||
| 142 | def format_links | ||
| 131 | css('*').each do |node| | 143 | css('*').each do |node| |
| 132 | next if node.text? | 144 | next if node.text? |
| 133 | 145 | ||
| 134 | # Handle example links [example:tag Title] | 146 | content = node.inner_html |
| 135 | node.inner_html = node.inner_html.gsub(/\[example:([^\s\]]+)\s+([^\]]+)\]/) do |match| | 147 | .gsub(PATTERNS[:example_link]) { create_external_link("https://threejs.org/examples/##{$1}", $2) } |
| 136 | tag, title = $1, $2 | 148 | .gsub(PATTERNS[:external_link_text]) { create_external_link($1, $2) } |
| 137 | "<a class='reference external' href='https://threejs.org/examples/##{tag}'>#{title}</a>" | 149 | .gsub(PATTERNS[:external_link]) { create_external_link($1, $1) } |
| 138 | end | 150 | .gsub(PATTERNS[:page_link_text]) { create_internal_link($1, $2) } |
| 139 | 151 | .gsub(PATTERNS[:page_link]) { create_internal_link($1, $1) } | |
| 140 | # Handle external links with [link:url text] format | 152 | |
| 141 | node.inner_html = node.inner_html.gsub(/\[link:([^\s\]]+)\s+([^\]]+)\]/) do |match| | 153 | node.inner_html = content |
| 142 | url, text = $1, $2 | 154 | end |
| 143 | "<a class='reference external' href='#{url}'>#{text}</a>" | ||
| 144 | end | ||
| 145 | 155 | ||
| 146 | # Handle external links with [link:url] format | 156 | normalize_href_attributes |
| 147 | node.inner_html = node.inner_html.gsub(/\[link:([^\]]+)\]/) do |match| | 157 | end |
| 148 | url = $1 | ||
| 149 | "<a class='reference external' href='#{url}'>#{url}</a>" | ||
| 150 | end | ||
| 151 | 158 | ||
| 152 | # Handle internal page links with text | 159 | def create_external_link(url, text) |
| 153 | node.inner_html = node.inner_html.gsub(/\[page:([^\]]+?)\s+([^\]]+?)\]/) do | 160 | %Q(<a class='reference external' href='#{url}'>#{text}</a>) |
| 154 | path, text = $1, $2 | 161 | end |
| 155 | "<a class='reference internal' href='#{path.downcase}'><code class='xref js js-#{path.downcase}'>#{text}</code></a>" | ||
| 156 | end | ||
| 157 | 162 | ||
| 158 | # Handle internal page links without text | 163 | def create_internal_link(path, text) |
| 159 | node.inner_html = node.inner_html.gsub(/\[page:([^\]]+?)\]/) do |match| | 164 | %Q(<a class='reference internal' href='#{path.downcase}'><code class='xref js js-#{path.downcase}'>#{text}</code></a>) |
| 160 | path = $1 | 165 | end |
| 161 | "<a class='reference internal' href='#{path.downcase}'><code class='xref js js-#{path.downcase}'>#{path}</code></a>" | ||
| 162 | end | ||
| 163 | end | ||
| 164 | 166 | ||
| 165 | # Fix all href attributes to be lowercase and remove .html | 167 | def normalize_href_attributes |
| 166 | css('a[href]').each do |link| | 168 | css('a[href]').each do |link| |
| 167 | next if link['href'].start_with?('http') | 169 | next if link['href'].start_with?('http') |
| 168 | link['href'] = link['href'].remove('../').downcase.sub(/\.html$/, '') | 170 | link['href'] = link['href'].remove('../').downcase.sub(/\.html$/, '') |
| 169 | link['class'] = 'reference internal' | 171 | link['class'] = 'reference internal' |
| 170 | end | 172 | end |
| 173 | end | ||
| 171 | 174 | ||
| 172 | # Add section classes | 175 | def add_section_structure |
| 173 | css('h2').each do |node| | 176 | css('h2').each do |node| |
| 174 | node['class'] = 'section-title' | 177 | node['class'] = 'section-title' |
| 175 | section = node.next_element | 178 | section = node.next_element |
| 176 | if section | 179 | next unless section |
| 177 | wrapper = doc.document.create_element('div') | 180 | |
| 178 | wrapper['class'] = 'section' | 181 | wrapper = doc.document.create_element('div') |
| 179 | node.after(wrapper) | 182 | wrapper['class'] = 'section' |
| 180 | wrapper.add_child(node) | 183 | node.after(wrapper) |
| 181 | current = section | 184 | wrapper.add_child(node) |
| 182 | while current && current.name != 'h2' | 185 | |
| 183 | next_el = current.next | 186 | current = section |
| 184 | wrapper.add_child(current) | 187 | while current && current.name != 'h2' |
| 185 | current = next_el | 188 | next_el = current.next |
| 186 | end | 189 | wrapper.add_child(current) |
| 190 | current = next_el | ||
| 187 | end | 191 | end |
| 188 | end | 192 | end |
| 189 | 193 | ||
| 190 | # Format description paragraphs | 194 | css('p.desc').each { |node| node['class'] = 'section-desc' } |
| 191 | css('p.desc').each do |node| | 195 | end |
| 192 | node['class'] = 'section-desc' | ||
| 193 | end | ||
| 194 | 196 | ||
| 195 | # Handle inline code/backticks in text | 197 | def format_notes |
| 196 | css('p, li, dt, dd').each do |node| | 198 | css('p').each do |node| |
| 197 | next if node.at_css('pre') # Skip if contains a code block | 199 | next unless node.content.start_with?('Note:') |
| 198 | 200 | ||
| 199 | # Replace backticks with proper code formatting | 201 | wrapper = doc.document.create_element('div') |
| 200 | node.inner_html = node.inner_html.gsub(/`([^`]+)`/) do |match| | 202 | wrapper['class'] = 'admonition note' |
| 201 | code = $1 | 203 | |
| 202 | "<code class='docutils literal notranslate'><span class='pre'>#{code}</span></code>" | 204 | title = doc.document.create_element('p') |
| 203 | end | 205 | title['class'] = 'first admonition-title' |
| 206 | title.content = 'Note' | ||
| 207 | |||
| 208 | content = doc.document.create_element('p') | ||
| 209 | content['class'] = 'last' | ||
| 210 | content.inner_html = node.inner_html.sub('Note:', '').strip | ||
| 211 | |||
| 212 | wrapper.add_child(title) | ||
| 213 | wrapper.add_child(content) | ||
| 214 | node.replace(wrapper) | ||
| 204 | end | 215 | end |
| 216 | end | ||
| 205 | 217 | ||
| 206 | # Handle inline code in property descriptions | 218 | def add_heading_attributes |
| 207 | css('.property-type').each do |node| | ||
| 208 | node.inner_html = node.inner_html.gsub(/`([^`]+)`/) do |match| | ||
| 209 | code = $1 | ||
| 210 | "<code class='docutils literal notranslate'><span class='pre'>#{code}</span></code>" | ||
| 211 | end | ||
| 212 | end | ||
| 213 | |||
| 214 | # Add proper heading IDs and classes | ||
| 215 | css('h1, h2, h3, h4').each do |node| | 219 | css('h1, h2, h3, h4').each do |node| |
| 216 | node['id'] ||= node.content.strip.downcase.gsub(/[^\w]+/, '-') | 220 | node['id'] ||= node.content.strip.downcase.gsub(/[^\w]+/, '-') |
| 217 | existing_class = node['class'].to_s | 221 | existing_class = node['class'].to_s |
| 218 | node['class'] = "#{existing_class} section-header" | 222 | node['class'] = "#{existing_class} section-header" |
| 219 | end | 223 | end |
| 220 | 224 | ||
| 221 | # Add note styling | 225 | format_inline_code |
| 222 | css('p').each do |node| | 226 | end |
| 223 | if node.content.start_with?('Note:') | 227 | |
| 224 | wrapper = doc.document.create_element('div') | 228 | def format_inline_code |
| 225 | wrapper['class'] = 'admonition note' | 229 | selectors = ['p', 'li', 'dt', 'dd', '.property-type'].join(', ') |
| 226 | 230 | css(selectors).each do |node| | |
| 227 | title = doc.document.create_element('p') | 231 | next if node.at_css('pre') |
| 228 | title['class'] = 'first admonition-title' | 232 | node.inner_html = node.inner_html.gsub(PATTERNS[:inline_code]) do |match| |
| 229 | title.content = 'Note' | 233 | "<code class='docutils literal notranslate'><span class='pre'>#{$1}</span></code>" |
| 230 | |||
| 231 | content = doc.document.create_element('p') | ||
| 232 | content['class'] = 'last' | ||
| 233 | content.inner_html = node.inner_html.sub('Note:', '').strip | ||
| 234 | |||
| 235 | wrapper.add_child(title) | ||
| 236 | wrapper.add_child(content) | ||
| 237 | node.replace(wrapper) | ||
| 238 | end | 234 | end |
| 239 | end | 235 | end |
| 240 | doc | ||
| 241 | end | 236 | end |
| 242 | end | 237 | end |
| 243 | end | 238 | end |