diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-01 11:35:02 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-01 11:37:12 +0900 |
| commit | 376781b3592203de7d3ceb01b1d7a07dc5bd87a7 (patch) | |
| tree | 0b859587d5f766ff805ecb440a194ce1dcc8ba79 | |
| parent | 6aea40c400108a2a6191fc10cbd81abda975c027 (diff) | |
| download | nsfisis.dev-376781b3592203de7d3ceb01b1d7a07dc5bd87a7.tar.gz nsfisis.dev-376781b3592203de7d3ceb01b1d7a07dc5bd87a7.tar.zst nsfisis.dev-376781b3592203de7d3ceb01b1d7a07dc5bd87a7.zip | |
refactor(nuldoc): DOM builder
42 files changed, 1439 insertions, 1311 deletions
diff --git a/services/nuldoc/lib/nuldoc.rb b/services/nuldoc/lib/nuldoc.rb index 83df42d6..cb94051d 100644 --- a/services/nuldoc/lib/nuldoc.rb +++ b/services/nuldoc/lib/nuldoc.rb @@ -11,8 +11,9 @@ require 'webrick' require_relative 'nuldoc/pipeline' require_relative 'nuldoc/dom' -require_relative 'nuldoc/dom/atom_xml' -require_relative 'nuldoc/dom/html' +require_relative 'nuldoc/dom/builder' +require_relative 'nuldoc/dom/html_builder' +require_relative 'nuldoc/dom/atom_xml_builder' require_relative 'nuldoc/revision' require_relative 'nuldoc/config' require_relative 'nuldoc/page' diff --git a/services/nuldoc/lib/nuldoc/components/global_footer.rb b/services/nuldoc/lib/nuldoc/components/global_footer.rb index 879aac55..8440d7d8 100644 --- a/services/nuldoc/lib/nuldoc/components/global_footer.rb +++ b/services/nuldoc/lib/nuldoc/components/global_footer.rb @@ -1,10 +1,13 @@ module Nuldoc module Components - class GlobalFooter - extend DOM::HTML + class GlobalFooter < DOM::HTMLBuilder + def initialize(config:) + super() + @config = config + end - def self.render(config:) - footer(class: 'footer') { text "© #{config.site.copyright_year} #{config.site.author}" } + def build + footer(class: 'footer') { text "© #{@config.site.copyright_year} #{@config.site.author}" } end end end diff --git a/services/nuldoc/lib/nuldoc/components/global_headers.rb b/services/nuldoc/lib/nuldoc/components/global_headers.rb index fb19096a..ab62a5f0 100644 --- a/services/nuldoc/lib/nuldoc/components/global_headers.rb +++ b/services/nuldoc/lib/nuldoc/components/global_headers.rb @@ -1,41 +1,50 @@ module Nuldoc module Components - class DefaultGlobalHeader - extend DOM::HTML + class DefaultGlobalHeader < DOM::HTMLBuilder + def initialize(config:) + super() + @config = config + end - def self.render(config:) + def build header(class: 'header') do div(class: 'site-logo') do - a(href: "https://#{config.sites.default.fqdn}/") { text 'nsfisis.dev' } + a(href: "https://#{@config.sites.default.fqdn}/") { text 'nsfisis.dev' } end end end end - class AboutGlobalHeader - extend DOM::HTML + class AboutGlobalHeader < DOM::HTMLBuilder + def initialize(config:) + super() + @config = config + end - def self.render(config:) + def build header(class: 'header') do div(class: 'site-logo') do - a(href: "https://#{config.sites.default.fqdn}/") { text 'nsfisis.dev' } + a(href: "https://#{@config.sites.default.fqdn}/") { text 'nsfisis.dev' } end end end end - class BlogGlobalHeader - extend DOM::HTML + class BlogGlobalHeader < DOM::HTMLBuilder + def initialize(config:) + super() + @config = config + end - def self.render(config:) + def build header(class: 'header') do div(class: 'site-logo') do - a(href: "https://#{config.sites.default.fqdn}/") { text 'nsfisis.dev' } + a(href: "https://#{@config.sites.default.fqdn}/") { text 'nsfisis.dev' } end - div(class: 'site-name') { text config.sites.blog.site_name } + div(class: 'site-name') { text @config.sites.blog.site_name } nav(class: 'nav') do ul do - li { a(href: "https://#{config.sites.about.fqdn}/") { text 'About' } } + li { a(href: "https://#{@config.sites.about.fqdn}/") { text 'About' } } li { a(href: '/posts/') { text 'Posts' } } li { a(href: '/tags/') { text 'Tags' } } end @@ -44,17 +53,20 @@ module Nuldoc end end - class SlidesGlobalHeader - extend DOM::HTML + class SlidesGlobalHeader < DOM::HTMLBuilder + def initialize(config:) + super() + @config = config + end - def self.render(config:) + def build header(class: 'header') do div(class: 'site-logo') do - a(href: "https://#{config.sites.default.fqdn}/") { text 'nsfisis.dev' } + a(href: "https://#{@config.sites.default.fqdn}/") { text 'nsfisis.dev' } end nav(class: 'nav') do ul do - li { a(href: "https://#{config.sites.about.fqdn}/") { text 'About' } } + li { a(href: "https://#{@config.sites.about.fqdn}/") { text 'About' } } li { a(href: '/slides/') { text 'Slides' } } li { a(href: '/tags/') { text 'Tags' } } end diff --git a/services/nuldoc/lib/nuldoc/components/page_layout.rb b/services/nuldoc/lib/nuldoc/components/page_layout.rb index 4ddd0968..d1b31744 100644 --- a/services/nuldoc/lib/nuldoc/components/page_layout.rb +++ b/services/nuldoc/lib/nuldoc/components/page_layout.rb @@ -1,32 +1,45 @@ module Nuldoc module Components - class PageLayout - extend DOM::HTML + class PageLayout < DOM::HTMLBuilder + def initialize(meta_copyright_year:, meta_description:, meta_title:, site:, config:, children:, + meta_keywords: nil, meta_atom_feed_href: nil) + super() + @meta_copyright_year = meta_copyright_year + @meta_description = meta_description + @meta_title = meta_title + @site = site + @config = config + @children = children + @meta_keywords = meta_keywords + @meta_atom_feed_href = meta_atom_feed_href + end - def self.render(meta_copyright_year:, meta_description:, meta_title:, site:, config:, children:, - meta_keywords: nil, meta_atom_feed_href: nil) - site_entry = config.site_entry(site) + def build + site_entry = @config.site_entry(@site) html(lang: 'ja-JP') do head do meta(charset: 'UTF-8') meta(name: 'viewport', content: 'width=device-width, initial-scale=1.0') - meta(name: 'author', content: config.site.author) - meta(name: 'copyright', content: "© #{meta_copyright_year} #{config.site.author}") - meta(name: 'description', content: meta_description) - meta(name: 'keywords', content: meta_keywords.join(',')) if meta_keywords && !meta_keywords.empty? + meta(name: 'author', content: @config.site.author) + meta(name: 'copyright', content: "© #{@meta_copyright_year} #{@config.site.author}") + meta(name: 'description', content: @meta_description) + if @meta_keywords && !@meta_keywords.empty? + meta(name: 'keywords', + content: @meta_keywords.join(',')) + end meta(property: 'og:type', content: 'article') - meta(property: 'og:title', content: meta_title) - meta(property: 'og:description', content: meta_description) + meta(property: 'og:title', content: @meta_title) + meta(property: 'og:description', content: @meta_description) meta(property: 'og:site_name', content: site_entry.site_name) meta(property: 'og:locale', content: 'ja_JP') meta(name: 'Hatena::Bookmark', content: 'nocomment') - link(rel: 'alternate', href: meta_atom_feed_href, type: 'application/atom+xml') if meta_atom_feed_href + link(rel: 'alternate', href: @meta_atom_feed_href, type: 'application/atom+xml') if @meta_atom_feed_href link(rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml') - title { text meta_title } - StaticStylesheet.render(file_name: '/style.css', config: config) + title { text @meta_title } + render(StaticStylesheet, file_name: '/style.css', config: @config) end - child children + child @children end end end diff --git a/services/nuldoc/lib/nuldoc/components/pagination.rb b/services/nuldoc/lib/nuldoc/components/pagination.rb index 61978cb1..3a6a5dc6 100644 --- a/services/nuldoc/lib/nuldoc/components/pagination.rb +++ b/services/nuldoc/lib/nuldoc/components/pagination.rb @@ -1,37 +1,46 @@ module Nuldoc module Components - class Pagination - extend DOM::HTML - - def self.render(current_page:, total_pages:, base_path:) - return div if total_pages <= 1 + class Pagination < DOM::HTMLBuilder + def initialize(current_page:, total_pages:, base_path:) + super() + @current_page = current_page + @total_pages = total_pages + @base_path = base_path + end - pages = generate_page_numbers(current_page, total_pages) + def build + if @total_pages <= 1 + div + else + pages = generate_page_numbers(@current_page, @total_pages) - nav(class: 'pagination') do - div(class: 'pagination-prev') do - a(href: page_url_at(base_path, current_page - 1)) { text '前へ' } if current_page > 1 - end - pages.each do |page| - if page == '...' - div(class: 'pagination-elipsis') { text "\u2026" } - elsif page == current_page - div(class: 'pagination-page pagination-page-current') do - span { text page.to_s } - end - else - div(class: 'pagination-page') do - a(href: page_url_at(base_path, page)) { text page.to_s } + nav(class: 'pagination') do + div(class: 'pagination-prev') do + a(href: page_url_at(@base_path, @current_page - 1)) { text '前へ' } if @current_page > 1 + end + pages.each do |page| + if page == '...' + div(class: 'pagination-elipsis') { text "\u2026" } + elsif page == @current_page + div(class: 'pagination-page pagination-page-current') do + span { text page.to_s } + end + else + div(class: 'pagination-page') do + a(href: page_url_at(@base_path, page)) { text page.to_s } + end end end - end - div(class: 'pagination-next') do - a(href: page_url_at(base_path, current_page + 1)) { text '次へ' } if current_page < total_pages + div(class: 'pagination-next') do + a(href: page_url_at(@base_path, @current_page + 1)) { text '次へ' } if @current_page < @total_pages + end end end end - def self.generate_page_numbers(current_page, total_pages) + private + + def generate_page_numbers(current_page, total_pages) pages = Set.new pages.add(1) pages.add([1, current_page - 1].max) @@ -57,11 +66,9 @@ module Nuldoc result end - def self.page_url_at(base_path, page) + def page_url_at(base_path, page) page == 1 ? base_path : "#{base_path}#{page}/" end - - private_class_method :generate_page_numbers, :page_url_at end end end diff --git a/services/nuldoc/lib/nuldoc/components/post_page_entry.rb b/services/nuldoc/lib/nuldoc/components/post_page_entry.rb index 623c2be6..4a9ab6c6 100644 --- a/services/nuldoc/lib/nuldoc/components/post_page_entry.rb +++ b/services/nuldoc/lib/nuldoc/components/post_page_entry.rb @@ -1,17 +1,21 @@ module Nuldoc module Components - class PostPageEntry - extend DOM::HTML + class PostPageEntry < DOM::HTMLBuilder + def initialize(post:, config:) + super() + @post = post + @config = config + end - def self.render(post:, config:) - published = Revision.date_to_string(GeneratorUtils.published_date(post)) - updated = Revision.date_to_string(GeneratorUtils.updated_date(post)) - has_updates = GeneratorUtils.any_updates?(post) + def build + published = Revision.date_to_string(GeneratorUtils.published_date(@post)) + updated = Revision.date_to_string(GeneratorUtils.updated_date(@post)) + has_updates = GeneratorUtils.any_updates?(@post) article(class: 'post-entry') do - a(href: post.href) do - header(class: 'entry-header') { h2 { text post.title } } - section(class: 'entry-content') { p { text post.description } } + a(href: @post.href) do + header(class: 'entry-header') { h2 { text @post.title } } + section(class: 'entry-content') { p { text @post.description } } footer(class: 'entry-footer') do time(datetime: published) { text published } text ' 投稿' @@ -20,7 +24,7 @@ module Nuldoc time(datetime: updated) { text updated } text ' 更新' end - TagList.render(tags: post.tags, config: config) if post.tags.length.positive? + render(TagList, tags: @post.tags, config: @config) if @post.tags.length.positive? end end end diff --git a/services/nuldoc/lib/nuldoc/components/slide_page_entry.rb b/services/nuldoc/lib/nuldoc/components/slide_page_entry.rb index c78d3d23..4aabc425 100644 --- a/services/nuldoc/lib/nuldoc/components/slide_page_entry.rb +++ b/services/nuldoc/lib/nuldoc/components/slide_page_entry.rb @@ -1,17 +1,21 @@ module Nuldoc module Components - class SlidePageEntry - extend DOM::HTML + class SlidePageEntry < DOM::HTMLBuilder + def initialize(slide:, config:) + super() + @slide = slide + @config = config + end - def self.render(slide:, config:) - published = Revision.date_to_string(GeneratorUtils.published_date(slide)) - updated = Revision.date_to_string(GeneratorUtils.updated_date(slide)) - has_updates = GeneratorUtils.any_updates?(slide) + def build + published = Revision.date_to_string(GeneratorUtils.published_date(@slide)) + updated = Revision.date_to_string(GeneratorUtils.updated_date(@slide)) + has_updates = GeneratorUtils.any_updates?(@slide) article(class: 'post-entry') do - a(href: slide.href) do - header(class: 'entry-header') { h2 { text slide.title } } - section(class: 'entry-content') { p { text slide.description } } + a(href: @slide.href) do + header(class: 'entry-header') { h2 { text @slide.title } } + section(class: 'entry-content') { p { text @slide.description } } footer(class: 'entry-footer') do time(datetime: published) { text published } text ' 登壇' @@ -20,7 +24,7 @@ module Nuldoc time(datetime: updated) { text updated } text ' 更新' end - TagList.render(tags: slide.tags, config: config) if slide.tags.length.positive? + render(TagList, tags: @slide.tags, config: @config) if @slide.tags.length.positive? end end end diff --git a/services/nuldoc/lib/nuldoc/components/static_script.rb b/services/nuldoc/lib/nuldoc/components/static_script.rb index 5c8af53d..fd6e1c3d 100644 --- a/services/nuldoc/lib/nuldoc/components/static_script.rb +++ b/services/nuldoc/lib/nuldoc/components/static_script.rb @@ -1,14 +1,21 @@ module Nuldoc module Components - class StaticScript - extend DOM::HTML + class StaticScript < DOM::HTMLBuilder + def initialize(file_name:, config:, site: nil, type: nil, defer: nil) + super() + @file_name = file_name + @config = config + @site = site + @type = type + @defer = defer + end - def self.render(file_name:, config:, site: nil, type: nil, defer: nil) - file_path = File.join(Dir.pwd, config.locations.static_dir, site || '_all', file_name) + def build + file_path = File.join(Dir.pwd, @config.locations.static_dir, @site || '_all', @file_name) hash = ComponentUtils.calculate_file_hash(file_path) - attrs = { src: "#{file_name}?h=#{hash}" } - attrs[:type] = type if type - attrs[:defer] = defer if defer + attrs = { src: "#{@file_name}?h=#{hash}" } + attrs[:type] = @type if @type + attrs[:defer] = @defer if @defer script(**attrs) end end diff --git a/services/nuldoc/lib/nuldoc/components/static_stylesheet.rb b/services/nuldoc/lib/nuldoc/components/static_stylesheet.rb index 5246ee1c..f0925e18 100644 --- a/services/nuldoc/lib/nuldoc/components/static_stylesheet.rb +++ b/services/nuldoc/lib/nuldoc/components/static_stylesheet.rb @@ -1,12 +1,17 @@ module Nuldoc module Components - class StaticStylesheet - extend DOM::HTML + class StaticStylesheet < DOM::HTMLBuilder + def initialize(file_name:, config:, site: nil) + super() + @file_name = file_name + @config = config + @site = site + end - def self.render(file_name:, config:, site: nil) - file_path = File.join(Dir.pwd, config.locations.static_dir, site || '_all', file_name) + def build + file_path = File.join(Dir.pwd, @config.locations.static_dir, @site || '_all', @file_name) hash = ComponentUtils.calculate_file_hash(file_path) - link(rel: 'stylesheet', href: "#{file_name}?h=#{hash}") + link(rel: 'stylesheet', href: "#{@file_name}?h=#{hash}") end end end diff --git a/services/nuldoc/lib/nuldoc/components/table_of_contents.rb b/services/nuldoc/lib/nuldoc/components/table_of_contents.rb index 0be95706..499ca681 100644 --- a/services/nuldoc/lib/nuldoc/components/table_of_contents.rb +++ b/services/nuldoc/lib/nuldoc/components/table_of_contents.rb @@ -1,23 +1,26 @@ module Nuldoc module Components - class TableOfContents - extend DOM::HTML + class TableOfContents < DOM::HTMLBuilder + def initialize(toc:) + super() + @toc = toc + end - def self.render(toc:) + def build nav(class: 'toc') do h2 { text '目次' } - ul { toc.items.each { |entry| toc_entry_component(entry) } } + ul { @toc.items.each { |entry| toc_entry_component(entry) } } end end - def self.toc_entry_component(entry) + private + + def toc_entry_component(entry) li do a(href: "##{entry.id}") { text entry.text } ul { entry.children.each { |c| toc_entry_component(c) } } if entry.children.length.positive? end end - - private_class_method :toc_entry_component end end end diff --git a/services/nuldoc/lib/nuldoc/components/tag_list.rb b/services/nuldoc/lib/nuldoc/components/tag_list.rb index 57d11ab0..acbcaaf3 100644 --- a/services/nuldoc/lib/nuldoc/components/tag_list.rb +++ b/services/nuldoc/lib/nuldoc/components/tag_list.rb @@ -1,13 +1,17 @@ module Nuldoc module Components - class TagList - extend DOM::HTML + class TagList < DOM::HTMLBuilder + def initialize(tags:, config:) + super() + @tags = tags + @config = config + end - def self.render(tags:, config:) + def build ul(class: 'entry-tags') do - tags.each do |slug| + @tags.each do |slug| li(class: 'tag') do - span(class: 'tag-inner') { text config.tag_label(slug) } + span(class: 'tag-inner') { text @config.tag_label(slug) } end end end diff --git a/services/nuldoc/lib/nuldoc/dom.rb b/services/nuldoc/lib/nuldoc/dom.rb index ec802fb6..abe625fe 100644 --- a/services/nuldoc/lib/nuldoc/dom.rb +++ b/services/nuldoc/lib/nuldoc/dom.rb @@ -1,63 +1,19 @@ module Nuldoc - Text = Struct.new(:content, keyword_init: true) do + Text = Data.define(:content) do def kind = :text end - RawHTML = Struct.new(:html, keyword_init: true) do + RawNode = Data.define(:content) do def kind = :raw end - Element = Struct.new(:name, :attributes, :children, keyword_init: true) do + Element = Data.define(:name, :attributes, :children) do def kind = :element end module DOM - CHILDREN_STACK_KEY = :__nuldoc_dom_children_stack - private_constant :CHILDREN_STACK_KEY - module_function - def text(content) - node = Text.new(content: content) - _auto_append(node) - node - end - - def raw_html(html) - node = RawHTML.new(html: html) - _auto_append(node) - node - end - - def child(*nodes) - stack = Thread.current[CHILDREN_STACK_KEY] - return unless stack && !stack.empty? - - nodes.each do |node| - case node - when nil, false - next - when String - stack.last.push(Text.new(content: node)) - when Array - node.each { |n| child(n) } - else - stack.last.push(node) - end - end - end - - def elem(name, **attrs, &) - children = _collect_children(&) - node = Element.new( - name: name, - attributes: attrs.transform_keys(&:to_s), - children: children - ) - _auto_append(node) - node - end - def add_class(element, klass) classes = element.attributes['class'] if classes.nil? @@ -104,6 +60,24 @@ module Nuldoc end end + def map_children_recursively(element, &) + element.children.map! do |c| + c = yield(c) + map_children_recursively(c, &) if c.kind == :element + c + end + end + + def map_element_of_type(root, element_name, &) + map_children_recursively(root) do |n| + if n.kind == :element && n.name == element_name + yield(n) + else + n + end + end + end + def process_text_nodes_in_element(element) new_children = [] element.children.each do |child| @@ -115,29 +89,5 @@ module Nuldoc end element.children.replace(new_children) end - - private - - def _collect_children(&block) - return [] unless block - - stack = Thread.current[CHILDREN_STACK_KEY] ||= [] - stack.push([]) - begin - yield - stack.last - ensure - stack.pop - end - end - - def _auto_append(node) - stack = Thread.current[CHILDREN_STACK_KEY] - return unless stack && !stack.empty? - - stack.last.push(node) - end - - module_function :_collect_children, :_auto_append end end diff --git a/services/nuldoc/lib/nuldoc/dom/atom_xml.rb b/services/nuldoc/lib/nuldoc/dom/atom_xml.rb deleted file mode 100644 index 9ab10822..00000000 --- a/services/nuldoc/lib/nuldoc/dom/atom_xml.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Nuldoc - module DOM - module AtomXML - def self.extended(base) - base.extend(DOM) - end - - def self.included(base) - base.include(DOM) - end - - module_function - - def author(**attrs, &) = DOM.elem('author', **attrs, &) - def entry(**attrs, &) = DOM.elem('entry', **attrs, &) - def feed(**attrs, &) = DOM.elem('feed', **attrs, &) - def id(**attrs, &) = DOM.elem('id', **attrs, &) - def link(**attrs) = DOM.elem('link', **attrs) - def name(**attrs, &) = DOM.elem('name', **attrs, &) - def published(**attrs, &) = DOM.elem('published', **attrs, &) - def summary(**attrs, &) = DOM.elem('summary', **attrs, &) - def title(**attrs, &) = DOM.elem('title', **attrs, &) - def updated(**attrs, &) = DOM.elem('updated', **attrs, &) - end - end -end diff --git a/services/nuldoc/lib/nuldoc/dom/atom_xml_builder.rb b/services/nuldoc/lib/nuldoc/dom/atom_xml_builder.rb new file mode 100644 index 00000000..2d865b6e --- /dev/null +++ b/services/nuldoc/lib/nuldoc/dom/atom_xml_builder.rb @@ -0,0 +1,16 @@ +module Nuldoc + module DOM + class AtomXMLBuilder < Builder + def author(**attrs, &) = elem('author', **attrs, &) + def entry(**attrs, &) = elem('entry', **attrs, &) + def feed(**attrs, &) = elem('feed', **attrs, &) + def id(**attrs, &) = elem('id', **attrs, &) + def link(**attrs) = elem('link', **attrs) + def name(**attrs, &) = elem('name', **attrs, &) + def published(**attrs, &) = elem('published', **attrs, &) + def summary(**attrs, &) = elem('summary', **attrs, &) + def title(**attrs, &) = elem('title', **attrs, &) + def updated(**attrs, &) = elem('updated', **attrs, &) + end + end +end diff --git a/services/nuldoc/lib/nuldoc/dom/builder.rb b/services/nuldoc/lib/nuldoc/dom/builder.rb new file mode 100644 index 00000000..2d8751c5 --- /dev/null +++ b/services/nuldoc/lib/nuldoc/dom/builder.rb @@ -0,0 +1,67 @@ +module Nuldoc + module DOM + class Builder + def initialize + @stack = [] + end + + def build(&) + instance_eval(&) + end + + def text(content) + try_append(Text.new(content: content)) + end + + def raw(content) + try_append(RawNode.new(content: content)) + end + + def render(component, **) + try_append(component.new(**).build) + end + + def child(*nodes) + return if @stack.empty? + + nodes.each do |node| + case node + when nil, false + next + when String + try_append(Text.new(content: node)) + when Array + node.each { |n| child(n) } + else + try_append(node) + end + end + end + + def elem(name, **attrs, &) + try_append( + Element.new( + name: name, + attributes: attrs.transform_keys(&:to_s), + children: collect_children(&) + ) + ) + end + + private + + def collect_children + return [] unless block_given? + + @stack.push([]) + yield + @stack.pop + end + + def try_append(node) + @stack.last.push(node) unless @stack.empty? + node + end + end + end +end diff --git a/services/nuldoc/lib/nuldoc/dom/html.rb b/services/nuldoc/lib/nuldoc/dom/html.rb deleted file mode 100644 index 1d9b1cab..00000000 --- a/services/nuldoc/lib/nuldoc/dom/html.rb +++ /dev/null @@ -1,56 +0,0 @@ -module Nuldoc - module DOM - module HTML - def self.extended(base) - base.extend(DOM) - end - - def self.included(base) - base.include(DOM) - end - - module_function - - def a(**attrs, &) = DOM.elem('a', **attrs, &) - def article(**attrs, &) = DOM.elem('article', **attrs, &) - def blockquote(**attrs, &) = DOM.elem('blockquote', **attrs, &) - def body(**attrs, &) = DOM.elem('body', **attrs, &) - def button(**attrs, &) = DOM.elem('button', **attrs, &) - def canvas(**attrs, &) = DOM.elem('canvas', **attrs, &) - def code(**attrs, &) = DOM.elem('code', **attrs, &) - def del(**attrs, &) = DOM.elem('del', **attrs, &) - def div(**attrs, &) = DOM.elem('div', **attrs, &) - def em(**attrs, &) = DOM.elem('em', **attrs, &) - def footer(**attrs, &) = DOM.elem('footer', **attrs, &) - def h1(**attrs, &) = DOM.elem('h1', **attrs, &) - def h2(**attrs, &) = DOM.elem('h2', **attrs, &) - def h3(**attrs, &) = DOM.elem('h3', **attrs, &) - def h4(**attrs, &) = DOM.elem('h4', **attrs, &) - def h5(**attrs, &) = DOM.elem('h5', **attrs, &) - def h6(**attrs, &) = DOM.elem('h6', **attrs, &) - def head(**attrs, &) = DOM.elem('head', **attrs, &) - def header(**attrs, &) = DOM.elem('header', **attrs, &) - def hr(**attrs) = DOM.elem('hr', **attrs) - def html(**attrs, &) = DOM.elem('html', **attrs, &) - def img(**attrs) = DOM.elem('img', **attrs) - def li(**attrs, &) = DOM.elem('li', **attrs, &) - def link(**attrs) = DOM.elem('link', **attrs) - def main(**attrs, &) = DOM.elem('main', **attrs, &) - def meta(**attrs) = DOM.elem('meta', **attrs) - def nav(**attrs, &) = DOM.elem('nav', **attrs, &) - def ol(**attrs, &) = DOM.elem('ol', **attrs, &) - def p(**attrs, &) = DOM.elem('p', **attrs, &) - def script(**attrs, &) = DOM.elem('script', **attrs, &) - def section(**attrs, &) = DOM.elem('section', **attrs, &) - def span(**attrs, &) = DOM.elem('span', **attrs, &) - def strong(**attrs, &) = DOM.elem('strong', **attrs, &) - def table(**attrs, &) = DOM.elem('table', **attrs, &) - def tbody(**attrs, &) = DOM.elem('tbody', **attrs, &) - def thead(**attrs, &) = DOM.elem('thead', **attrs, &) - def time(**attrs, &) = DOM.elem('time', **attrs, &) - def title(**attrs, &) = DOM.elem('title', **attrs, &) - def tr(**attrs, &) = DOM.elem('tr', **attrs, &) - def ul(**attrs, &) = DOM.elem('ul', **attrs, &) - end - end -end diff --git a/services/nuldoc/lib/nuldoc/dom/html_builder.rb b/services/nuldoc/lib/nuldoc/dom/html_builder.rb new file mode 100644 index 00000000..2a506640 --- /dev/null +++ b/services/nuldoc/lib/nuldoc/dom/html_builder.rb @@ -0,0 +1,46 @@ +module Nuldoc + module DOM + class HTMLBuilder < Builder + def a(**attrs, &) = elem('a', **attrs, &) + def article(**attrs, &) = elem('article', **attrs, &) + def blockquote(**attrs, &) = elem('blockquote', **attrs, &) + def body(**attrs, &) = elem('body', **attrs, &) + def button(**attrs, &) = elem('button', **attrs, &) + def canvas(**attrs, &) = elem('canvas', **attrs, &) + def code(**attrs, &) = elem('code', **attrs, &) + def del(**attrs, &) = elem('del', **attrs, &) + def div(**attrs, &) = elem('div', **attrs, &) + def em(**attrs, &) = elem('em', **attrs, &) + def footer(**attrs, &) = elem('footer', **attrs, &) + def h1(**attrs, &) = elem('h1', **attrs, &) + def h2(**attrs, &) = elem('h2', **attrs, &) + def h3(**attrs, &) = elem('h3', **attrs, &) + def h4(**attrs, &) = elem('h4', **attrs, &) + def h5(**attrs, &) = elem('h5', **attrs, &) + def h6(**attrs, &) = elem('h6', **attrs, &) + def head(**attrs, &) = elem('head', **attrs, &) + def header(**attrs, &) = elem('header', **attrs, &) + def hr(**attrs) = elem('hr', **attrs) + def html(**attrs, &) = elem('html', **attrs, &) + def img(**attrs) = elem('img', **attrs) + def li(**attrs, &) = elem('li', **attrs, &) + def link(**attrs) = elem('link', **attrs) + def main(**attrs, &) = elem('main', **attrs, &) + def meta(**attrs) = elem('meta', **attrs) + def nav(**attrs, &) = elem('nav', **attrs, &) + def ol(**attrs, &) = elem('ol', **attrs, &) + def p(**attrs, &) = elem('p', **attrs, &) + def script(**attrs, &) = elem('script', **attrs, &) + def section(**attrs, &) = elem('section', **attrs, &) + def span(**attrs, &) = elem('span', **attrs, &) + def strong(**attrs, &) = elem('strong', **attrs, &) + def table(**attrs, &) = elem('table', **attrs, &) + def tbody(**attrs, &) = elem('tbody', **attrs, &) + def thead(**attrs, &) = elem('thead', **attrs, &) + def time(**attrs, &) = elem('time', **attrs, &) + def title(**attrs, &) = elem('title', **attrs, &) + def tr(**attrs, &) = elem('tr', **attrs, &) + def ul(**attrs, &) = elem('ul', **attrs, &) + end + end +end diff --git a/services/nuldoc/lib/nuldoc/generators/about.rb b/services/nuldoc/lib/nuldoc/generators/about.rb index e64f0d1d..d5b9dec4 100644 --- a/services/nuldoc/lib/nuldoc/generators/about.rb +++ b/services/nuldoc/lib/nuldoc/generators/about.rb @@ -7,7 +7,7 @@ module Nuldoc end def generate - html = Pages::AboutPage.render(slides: @slides, config: @config) + html = Pages::AboutPage.new(slides: @slides, config: @config).render Page.new( root: html, diff --git a/services/nuldoc/lib/nuldoc/generators/atom.rb b/services/nuldoc/lib/nuldoc/generators/atom.rb index 74750eb7..4b24bb2f 100644 --- a/services/nuldoc/lib/nuldoc/generators/atom.rb +++ b/services/nuldoc/lib/nuldoc/generators/atom.rb @@ -46,7 +46,7 @@ module Nuldoc ) Page.new( - root: Pages::AtomPage.render(feed: feed), + root: Pages::AtomPage.new(feed: feed).render, renderer: :xml, site: @site, dest_file_path: feed_path, diff --git a/services/nuldoc/lib/nuldoc/generators/home.rb b/services/nuldoc/lib/nuldoc/generators/home.rb index 54f8753f..1e9b36a3 100644 --- a/services/nuldoc/lib/nuldoc/generators/home.rb +++ b/services/nuldoc/lib/nuldoc/generators/home.rb @@ -6,7 +6,7 @@ module Nuldoc end def generate - html = Pages::HomePage.render(config: @config) + html = Pages::HomePage.new(config: @config).render Page.new( root: html, diff --git a/services/nuldoc/lib/nuldoc/generators/not_found.rb b/services/nuldoc/lib/nuldoc/generators/not_found.rb index cffe1df8..bd139d72 100644 --- a/services/nuldoc/lib/nuldoc/generators/not_found.rb +++ b/services/nuldoc/lib/nuldoc/generators/not_found.rb @@ -7,7 +7,7 @@ module Nuldoc end def generate - html = Pages::NotFoundPage.render(site: @site, config: @config) + html = Pages::NotFoundPage.new(site: @site, config: @config).render Page.new( root: html, diff --git a/services/nuldoc/lib/nuldoc/generators/post.rb b/services/nuldoc/lib/nuldoc/generators/post.rb index 0d5a3afc..ca46a460 100644 --- a/services/nuldoc/lib/nuldoc/generators/post.rb +++ b/services/nuldoc/lib/nuldoc/generators/post.rb @@ -28,7 +28,7 @@ module Nuldoc end def generate - html = Pages::PostPage.render(doc: @doc, config: @config) + html = Pages::PostPage.new(doc: @doc, config: @config).render content_dir = File.join(Dir.pwd, @config.locations.content_dir) dest_file_path = File.join( diff --git a/services/nuldoc/lib/nuldoc/generators/post_list.rb b/services/nuldoc/lib/nuldoc/generators/post_list.rb index 680a0c32..344e9b01 100644 --- a/services/nuldoc/lib/nuldoc/generators/post_list.rb +++ b/services/nuldoc/lib/nuldoc/generators/post_list.rb @@ -15,12 +15,12 @@ module Nuldoc page_posts = @posts[page_index * posts_per_page, posts_per_page] current_page = page_index + 1 - html = Pages::PostListPage.render( + html = Pages::PostListPage.new( posts: page_posts, config: @config, current_page: current_page, total_pages: total_pages - ) + ).render dest_file_path = current_page == 1 ? '/posts/index.html' : "/posts/#{current_page}/index.html" href = current_page == 1 ? '/posts/' : "/posts/#{current_page}/" diff --git a/services/nuldoc/lib/nuldoc/generators/slide.rb b/services/nuldoc/lib/nuldoc/generators/slide.rb index 58fde56e..334fd55e 100644 --- a/services/nuldoc/lib/nuldoc/generators/slide.rb +++ b/services/nuldoc/lib/nuldoc/generators/slide.rb @@ -11,7 +11,7 @@ module Nuldoc end def generate - html = Pages::SlidePage.render(slide: @slide, config: @config) + html = Pages::SlidePage.new(slide: @slide, config: @config).render content_dir = File.join(Dir.pwd, @config.locations.content_dir) dest_file_path = File.join( diff --git a/services/nuldoc/lib/nuldoc/generators/slide_list.rb b/services/nuldoc/lib/nuldoc/generators/slide_list.rb index 8d23e4b4..c3a42e27 100644 --- a/services/nuldoc/lib/nuldoc/generators/slide_list.rb +++ b/services/nuldoc/lib/nuldoc/generators/slide_list.rb @@ -7,7 +7,7 @@ module Nuldoc end def generate - html = Pages::SlideListPage.render(slides: @slides, config: @config) + html = Pages::SlideListPage.new(slides: @slides, config: @config).render Page.new( root: html, diff --git a/services/nuldoc/lib/nuldoc/generators/tag.rb b/services/nuldoc/lib/nuldoc/generators/tag.rb index 7a5f7a7b..f114c0b5 100644 --- a/services/nuldoc/lib/nuldoc/generators/tag.rb +++ b/services/nuldoc/lib/nuldoc/generators/tag.rb @@ -12,7 +12,7 @@ module Nuldoc end def generate - html = Pages::TagPage.render(tag_slug: @tag_slug, pages: @pages, site: @site, config: @config) + html = Pages::TagPage.new(tag_slug: @tag_slug, pages: @pages, site: @site, config: @config).render TagPageData.new( root: html, diff --git a/services/nuldoc/lib/nuldoc/generators/tag_list.rb b/services/nuldoc/lib/nuldoc/generators/tag_list.rb index 089b6f0c..106687c8 100644 --- a/services/nuldoc/lib/nuldoc/generators/tag_list.rb +++ b/services/nuldoc/lib/nuldoc/generators/tag_list.rb @@ -8,7 +8,7 @@ module Nuldoc end def generate - html = Pages::TagListPage.render(tags: @tags, site: @site, config: @config) + html = Pages::TagListPage.new(tags: @tags, site: @site, config: @config).render Page.new( root: html, diff --git a/services/nuldoc/lib/nuldoc/markdown/parser/block_parser.rb b/services/nuldoc/lib/nuldoc/markdown/parser/block_parser.rb index 583d7201..b914882f 100644 --- a/services/nuldoc/lib/nuldoc/markdown/parser/block_parser.rb +++ b/services/nuldoc/lib/nuldoc/markdown/parser/block_parser.rb @@ -1,607 +1,607 @@ module Nuldoc module Parser - class BlockParser - extend DOM::HTML - + class BlockParser < DOM::HTMLBuilder HeaderBlock = Struct.new(:level, :id, :attributes, :heading_element, keyword_init: true) FootnoteBlock = Struct.new(:id, :children, keyword_init: true) - class << self - def parse(text) - scanner = LineScanner.new(text) - blocks = parse_blocks(scanner) - build_document(blocks) - end + def self.parse(text) + new.parse(text) + end - private + def parse(text) + scanner = LineScanner.new(text) + blocks = parse_blocks(scanner) + build_document(blocks) + end - # --- Block parsing --- + private - def parse_blocks(scanner) - blocks = [] - until scanner.eof? - block = parse_block(scanner) - blocks << block if block - end - blocks - end + # --- Block parsing --- - def parse_block(scanner) - return nil if scanner.eof? + def parse_blocks(scanner) + blocks = [] + until scanner.eof? + block = parse_block(scanner) + blocks << block if block + end + blocks + end - line = scanner.peek + def parse_block(scanner) + return nil if scanner.eof? - # 1. Blank line - if line.strip.empty? - scanner.advance - return nil - end + line = scanner.peek - # 2. HTML comment - if (result = try_html_comment(scanner)) - return result - end + # 1. Blank line + if line.strip.empty? + scanner.advance + return nil + end - # 3. Fenced code block - if (result = try_fenced_code(scanner)) - return result - end + # 2. HTML comment + if (result = try_html_comment(scanner)) + return result + end - # 4. Note/Edit block - if (result = try_note_block(scanner)) - return result - end + # 3. Fenced code block + if (result = try_fenced_code(scanner)) + return result + end - # 5. Heading - if (result = try_heading(scanner)) - return result - end + # 4. Note/Edit block + if (result = try_note_block(scanner)) + return result + end - # 6. Horizontal rule - if (result = try_hr(scanner)) - return result - end + # 5. Heading + if (result = try_heading(scanner)) + return result + end - # 7. Footnote definition - if (result = try_footnote_def(scanner)) - return result - end + # 6. Horizontal rule + if (result = try_hr(scanner)) + return result + end - # 8. Table - if (result = try_table(scanner)) - return result - end + # 7. Footnote definition + if (result = try_footnote_def(scanner)) + return result + end - # 9. Blockquote - if (result = try_blockquote(scanner)) - return result - end + # 8. Table + if (result = try_table(scanner)) + return result + end - # 10. Ordered list - if (result = try_ordered_list(scanner)) - return result - end + # 9. Blockquote + if (result = try_blockquote(scanner)) + return result + end - # 11. Unordered list - if (result = try_unordered_list(scanner)) - return result - end + # 10. Ordered list + if (result = try_ordered_list(scanner)) + return result + end - # 12. HTML block - if (result = try_html_block(scanner)) - return result - end + # 11. Unordered list + if (result = try_unordered_list(scanner)) + return result + end - # 13. Paragraph - parse_paragraph(scanner) + # 12. HTML block + if (result = try_html_block(scanner)) + return result end - def try_html_comment(scanner) - line = scanner.peek - return nil unless line.strip.start_with?('<!--') + # 13. Paragraph + parse_paragraph(scanner) + end - content = +'' - until scanner.eof? - l = scanner.advance - content << l << "\n" - break if l.include?('-->') - end - nil # skip comments + def try_html_comment(scanner) + line = scanner.peek + return nil unless line.strip.start_with?('<!--') + + content = +'' + until scanner.eof? + l = scanner.advance + content << l << "\n" + break if l.include?('-->') end + nil # skip comments + end - def try_fenced_code(scanner) - match = scanner.match(/^```(\S*)(.*)$/) - return nil unless match + def try_fenced_code(scanner) + match = scanner.match(/^```(\S*)(.*)$/) + return nil unless match - scanner.advance - language = match[1].empty? ? nil : match[1] - meta_string = match[2].strip + scanner.advance + language = match[1].empty? ? nil : match[1] + meta_string = match[2].strip - attrs = {} - attrs[:language] = language if language + attrs = {} + attrs[:language] = language if language - if meta_string && !meta_string.empty? - filename_match = meta_string.match(/filename="([^"]+)"/) - attrs[:filename] = filename_match[1] if filename_match - attrs[:numbered] = 'true' if meta_string.include?('numbered') - end + if meta_string && !meta_string.empty? + filename_match = meta_string.match(/filename="([^"]+)"/) + attrs[:filename] = filename_match[1] if filename_match + attrs[:numbered] = 'true' if meta_string.include?('numbered') + end - code_lines = [] - until scanner.eof? - l = scanner.peek - if l.start_with?('```') - scanner.advance - break - end - code_lines << scanner.advance + code_lines = [] + until scanner.eof? + l = scanner.peek + if l.start_with?('```') + scanner.advance + break end - - code = code_lines.join("\n") - elem('codeblock', **attrs) { text code } + code_lines << scanner.advance end - def try_note_block(scanner) - match = scanner.match(/^:::(note|edit)(.*)$/) - return nil unless match - - scanner.advance - block_type = match[1] - attr_string = match[2].strip + code = code_lines.join("\n") + elem('codeblock', **attrs) { text code } + end - attrs = {} - if block_type == 'edit' - # Parse {editat="..." operation="..."} - editat_match = attr_string.match(/editat="([^"]+)"/) - operation_match = attr_string.match(/operation="([^"]+)"/) - attrs[:editat] = editat_match[1] if editat_match - attrs[:operation] = operation_match[1] if operation_match - end + def try_note_block(scanner) + match = scanner.match(/^:::(note|edit)(.*)$/) + return nil unless match - # Collect content until ::: - content_lines = [] - until scanner.eof? - l = scanner.peek - if l.strip == ':::' - scanner.advance - break - end - content_lines << scanner.advance - end + scanner.advance + block_type = match[1] + attr_string = match[2].strip - inner_text = content_lines.join("\n") - inner_scanner = LineScanner.new(inner_text) - children = parse_blocks(inner_scanner) + attrs = {} + if block_type == 'edit' + # Parse {editat="..." operation="..."} + editat_match = attr_string.match(/editat="([^"]+)"/) + operation_match = attr_string.match(/operation="([^"]+)"/) + attrs[:editat] = editat_match[1] if editat_match + attrs[:operation] = operation_match[1] if operation_match + end - # Convert children - they are block elements already - child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawHTML) } - elem('note', **attrs) { child(*child_elements) } + # Collect content until ::: + content_lines = [] + until scanner.eof? + l = scanner.peek + if l.strip == ':::' + scanner.advance + break + end + content_lines << scanner.advance end - def try_heading(scanner) - match = scanner.match(/^(\#{1,5})\s+(.+)$/) - return nil unless match + inner_text = content_lines.join("\n") + inner_scanner = LineScanner.new(inner_text) + children = parse_blocks(inner_scanner) - scanner.advance - level = match[1].length - raw_text = match[2] + # Convert children - they are block elements already + child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawNode) } + elem('note', **attrs) { child(*child_elements) } + end - text_before, id, attributes = Attributes.parse_trailing_attributes(raw_text) + def try_heading(scanner) + match = scanner.match(/^(\#{1,5})\s+(.+)$/) + return nil unless match - inline_nodes = InlineParser.parse(text_before.strip) - heading_element = elem('h') { child(*inline_nodes) } + scanner.advance + level = match[1].length + raw_text = match[2] - HeaderBlock.new(level: level, id: id, attributes: attributes, heading_element: heading_element) - end + text_before, id, attributes = Attributes.parse_trailing_attributes(raw_text) - def try_hr(scanner) - match = scanner.match(/^---+\s*$/) - return nil unless match + inline_nodes = InlineParser.parse(text_before.strip) + heading_element = elem('h') { child(*inline_nodes) } - scanner.advance - hr - end + HeaderBlock.new(level: level, id: id, attributes: attributes, heading_element: heading_element) + end - def try_footnote_def(scanner) - match = scanner.match(/^\[\^([^\]]+)\]:\s*(.*)$/) - return nil unless match + def try_hr(scanner) + match = scanner.match(/^---+\s*$/) + return nil unless match - scanner.advance - id = match[1] - first_line = match[2] + scanner.advance + hr + end - content_lines = [first_line] - # Continuation lines: 4-space indent - until scanner.eof? - l = scanner.peek - break unless l.start_with?(' ') + def try_footnote_def(scanner) + match = scanner.match(/^\[\^([^\]]+)\]:\s*(.*)$/) + return nil unless match - content_lines << scanner.advance[4..] + scanner.advance + id = match[1] + first_line = match[2] - end + content_lines = [first_line] + # Continuation lines: 4-space indent + until scanner.eof? + l = scanner.peek + break unless l.start_with?(' ') - inner_text = content_lines.join("\n").strip - inner_scanner = LineScanner.new(inner_text) - children = parse_blocks(inner_scanner) + content_lines << scanner.advance[4..] - child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawHTML) } - FootnoteBlock.new(id: id, children: child_elements) end - def try_table(scanner) - # Check if this looks like a table - return nil unless scanner.peek.start_with?('|') + inner_text = content_lines.join("\n").strip + inner_scanner = LineScanner.new(inner_text) + children = parse_blocks(inner_scanner) - # Quick lookahead: second line must be a separator - return nil if scanner.pos + 1 >= scanner.lines.length - return nil unless scanner.lines[scanner.pos + 1].match?(/^\|[\s:|-]+\|$/) + child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawNode) } + FootnoteBlock.new(id: id, children: child_elements) + end - # Collect table lines - lines = [] - while !scanner.eof? && scanner.peek.start_with?('|') - lines << scanner.peek - scanner.advance - end + def try_table(scanner) + # Check if this looks like a table + return nil unless scanner.peek.start_with?('|') - header_line = lines[0] - separator_line = lines[1] - body_lines = lines[2..] || [] + # Quick lookahead: second line must be a separator + return nil if scanner.pos + 1 >= scanner.lines.length + return nil unless scanner.lines[scanner.pos + 1].match?(/^\|[\s:|-]+\|$/) - alignment = parse_table_alignment(separator_line) - header_cells = parse_table_row_cells(header_line) - header_row = build_table_row(header_cells, true, alignment) + # Collect table lines + lines = [] + while !scanner.eof? && scanner.peek.start_with?('|') + lines << scanner.peek + scanner.advance + end - body_rows = body_lines.map do |bl| - cells = parse_table_row_cells(bl) - build_table_row(cells, false, alignment) - end + header_line = lines[0] + separator_line = lines[1] + body_lines = lines[2..] || [] - table do - thead { child header_row } - tbody { child(*body_rows) } unless body_rows.empty? - end - end + alignment = parse_table_alignment(separator_line) + header_cells = parse_table_row_cells(header_line) + header_row = build_table_row(header_cells, true, alignment) - def parse_table_alignment(separator_line) - cells = separator_line.split('|').map(&:strip).reject(&:empty?) - cells.map do |cell| - left = cell.start_with?(':') - right = cell.end_with?(':') - if left && right - 'center' - elsif right - 'right' - elsif left - 'left' - end - end + body_rows = body_lines.map do |bl| + cells = parse_table_row_cells(bl) + build_table_row(cells, false, alignment) end - def parse_table_row_cells(line) - # Strip leading and trailing |, then split by | - stripped = line.strip - stripped = stripped[1..] if stripped.start_with?('|') - stripped = stripped[0...-1] if stripped.end_with?('|') - stripped.split('|').map(&:strip) + table do + thead { child header_row } + tbody { child(*body_rows) } unless body_rows.empty? end + end - def build_table_row(cells, is_header, alignment) - cell_elements = cells.each_with_index.map do |cell_text, i| - attrs = {} - align = alignment[i] - attrs[:align] = align if align && align != 'default' - - tag = is_header ? 'th' : 'td' - inline_nodes = InlineParser.parse(cell_text) - elem(tag, **attrs) { child(*inline_nodes) } + def parse_table_alignment(separator_line) + cells = separator_line.split('|').map(&:strip).reject(&:empty?) + cells.map do |cell| + left = cell.start_with?(':') + right = cell.end_with?(':') + if left && right + 'center' + elsif right + 'right' + elsif left + 'left' end - tr { child(*cell_elements) } end + end - def try_blockquote(scanner) - return nil unless scanner.peek.start_with?('> ') || scanner.peek == '>' + def parse_table_row_cells(line) + # Strip leading and trailing |, then split by | + stripped = line.strip + stripped = stripped[1..] if stripped.start_with?('|') + stripped = stripped[0...-1] if stripped.end_with?('|') + stripped.split('|').map(&:strip) + end - lines = [] - while !scanner.eof? && (scanner.peek.start_with?('> ') || scanner.peek == '>') - line = scanner.advance - lines << (line == '>' ? '' : line[2..]) - end + def build_table_row(cells, is_header, alignment) + cell_elements = cells.each_with_index.map do |cell_text, i| + attrs = {} + align = alignment[i] + attrs[:align] = align if align && align != 'default' - inner_text = lines.join("\n") - inner_scanner = LineScanner.new(inner_text) - children = parse_blocks(inner_scanner) - child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawHTML) } + tag = is_header ? 'th' : 'td' + inline_nodes = InlineParser.parse(cell_text) + elem(tag, **attrs) { child(*inline_nodes) } + end + tr { child(*cell_elements) } + end + + def try_blockquote(scanner) + return nil unless scanner.peek.start_with?('> ') || scanner.peek == '>' - blockquote { child(*child_elements) } + lines = [] + while !scanner.eof? && (scanner.peek.start_with?('> ') || scanner.peek == '>') + line = scanner.advance + lines << (line == '>' ? '' : line[2..]) end - def try_ordered_list(scanner) - match = scanner.match(/^(\d+)\.\s+(.*)$/) - return nil unless match + inner_text = lines.join("\n") + inner_scanner = LineScanner.new(inner_text) + children = parse_blocks(inner_scanner) + child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawNode) } - items = parse_list_items(scanner, :ordered) - return nil if items.empty? + blockquote { child(*child_elements) } + end - build_list(:ordered, items) - end + def try_ordered_list(scanner) + match = scanner.match(/^(\d+)\.\s+(.*)$/) + return nil unless match - def try_unordered_list(scanner) - match = scanner.match(/^\*\s+(.*)$/) - return nil unless match + items = parse_list_items(scanner, :ordered) + return nil if items.empty? - items = parse_list_items(scanner, :unordered) - return nil if items.empty? + build_list(:ordered, items) + end - build_list(:unordered, items) - end + def try_unordered_list(scanner) + match = scanner.match(/^\*\s+(.*)$/) + return nil unless match - def parse_list_items(scanner, type) - items = [] - marker_re = type == :ordered ? /^(\d+)\.\s+(.*)$/ : /^\*\s+(.*)$/ - indent_size = 4 + items = parse_list_items(scanner, :unordered) + return nil if items.empty? - while !scanner.eof? && (m = scanner.match(marker_re)) - scanner.advance - first_line = type == :ordered ? m[2] : m[1] + build_list(:unordered, items) + end - content_lines = [first_line] - has_blank = false + def parse_list_items(scanner, type) + items = [] + marker_re = type == :ordered ? /^(\d+)\.\s+(.*)$/ : /^\*\s+(.*)$/ + indent_size = 4 - # Collect continuation lines and sub-items - until scanner.eof? - l = scanner.peek + while !scanner.eof? && (m = scanner.match(marker_re)) + scanner.advance + first_line = type == :ordered ? m[2] : m[1] - # Blank line might be part of loose list - if l.strip.empty? - # Check if next non-blank line is still part of the list - next_pos = scanner.pos + 1 - next_pos += 1 while next_pos < scanner.lines.length && scanner.lines[next_pos].strip.empty? + content_lines = [first_line] + has_blank = false - if next_pos < scanner.lines.length - next_line = scanner.lines[next_pos] - if next_line.start_with?(' ' * indent_size) || next_line.match?(marker_re) - has_blank = true - content_lines << '' - scanner.advance - next - end - end - break - end + # Collect continuation lines and sub-items + until scanner.eof? + l = scanner.peek + + # Blank line might be part of loose list + if l.strip.empty? + # Check if next non-blank line is still part of the list + next_pos = scanner.pos + 1 + next_pos += 1 while next_pos < scanner.lines.length && scanner.lines[next_pos].strip.empty? - # Indented continuation (sub-items or content) - if l.start_with?(' ' * indent_size) - content_lines << scanner.advance[indent_size..] - next + if next_pos < scanner.lines.length + next_line = scanner.lines[next_pos] + if next_line.start_with?(' ' * indent_size) || next_line.match?(marker_re) + has_blank = true + content_lines << '' + scanner.advance + next + end end + break + end - # New list item at same level - break if l.match?(marker_re) + # Indented continuation (sub-items or content) + if l.start_with?(' ' * indent_size) + content_lines << scanner.advance[indent_size..] + next + end - # Non-indented, non-marker line - might be continuation of tight paragraph - break if l.strip.empty? - break if l.match?(/^[#>*\d]/) && !l.match?(/^\d+\.\s/) # another block element + # New list item at same level + break if l.match?(marker_re) - # Paragraph continuation - content_lines << scanner.advance - end + # Non-indented, non-marker line - might be continuation of tight paragraph + break if l.strip.empty? + break if l.match?(/^[#>*\d]/) && !l.match?(/^\d+\.\s/) # another block element - items << { lines: content_lines, has_blank: has_blank } + # Paragraph continuation + content_lines << scanner.advance end - items + items << { lines: content_lines, has_blank: has_blank } end - def build_list(type, items) - # Determine tight/loose - is_tight = items.none? { |item| item[:has_blank] } + items + end - attrs = {} - attrs[:__tight] = is_tight ? 'true' : 'false' + def build_list(type, items) + # Determine tight/loose + is_tight = items.none? { |item| item[:has_blank] } - # Check for task list items - is_task_list = false - if type == :unordered - is_task_list = items.any? { |item| item[:lines].first&.match?(/^\[[ xX]\]\s/) } - attrs[:type] = 'task' if is_task_list - end + attrs = {} + attrs[:__tight] = is_tight ? 'true' : 'false' - list_items = items.map do |item| - build_list_item(item, is_task_list) - end - - if type == :ordered - ol(**attrs) { child(*list_items) } - else - ul(**attrs) { child(*list_items) } - end + # Check for task list items + is_task_list = false + if type == :unordered + is_task_list = items.any? { |item| item[:lines].first&.match?(/^\[[ xX]\]\s/) } + attrs[:type] = 'task' if is_task_list end - def build_list_item(item, is_task_list) - attrs = {} - content = item[:lines].join("\n") + list_items = items.map do |item| + build_list_item(item, is_task_list) + end - if is_task_list - task_match = content.match(/^\[( |[xX])\]\s(.*)$/m) - if task_match - attrs[:checked] = task_match[1] == ' ' ? 'false' : 'true' - content = task_match[2] - end - end + if type == :ordered + ol(**attrs) { child(*list_items) } + else + ul(**attrs) { child(*list_items) } + end + end - # Parse inner content as blocks - inner_scanner = LineScanner.new(content) - children = parse_blocks(inner_scanner) + def build_list_item(item, is_task_list) + attrs = {} + content = item[:lines].join("\n") - # If no block-level elements were created, wrap in paragraph - child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawHTML) } - if child_elements.empty? - inline_nodes = InlineParser.parse(content) - child_elements = [p { child(*inline_nodes) }] + if is_task_list + task_match = content.match(/^\[( |[xX])\]\s(.*)$/m) + if task_match + attrs[:checked] = task_match[1] == ' ' ? 'false' : 'true' + content = task_match[2] end + end - li(**attrs) { child(*child_elements) } + # Parse inner content as blocks + inner_scanner = LineScanner.new(content) + children = parse_blocks(inner_scanner) + + # If no block-level elements were created, wrap in paragraph + child_elements = children.compact.select { |c| c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawNode) } + if child_elements.empty? + inline_nodes = InlineParser.parse(content) + child_elements = [p { child(*inline_nodes) }] end - def try_html_block(scanner) - line = scanner.peek - match = line.match(/^<(div|details|summary)(\s[^>]*)?>/) - return nil unless match + li(**attrs) { child(*child_elements) } + end - tag = match[1] - lines = [] - close_tag = "</#{tag}>" + def try_html_block(scanner) + line = scanner.peek + match = line.match(/^<(div|details|summary)(\s[^>]*)?>/) + return nil unless match - until scanner.eof? - l = scanner.advance - lines << l - break if l.include?(close_tag) - end + tag = match[1] + lines = [] + close_tag = "</#{tag}>" - html_content = lines.join("\n") + until scanner.eof? + l = scanner.advance + lines << l + break if l.include?(close_tag) + end - if tag == 'div' - # Parse inner content for div blocks - inner_match = html_content.match(%r{<div([^>]*)>(.*)</div>}m) - if inner_match - attr_str = inner_match[1] - inner_content = inner_match[2].strip + html_content = lines.join("\n") - attrs = {} - attr_str.scan(/([\w-]+)="([^"]*)"/) do |key, value| - attrs[key.to_sym] = value - end + if tag == 'div' + # Parse inner content for div blocks + inner_match = html_content.match(%r{<div([^>]*)>(.*)</div>}m) + if inner_match + attr_str = inner_match[1] + inner_content = inner_match[2].strip - if inner_content.empty? - div(**attrs) - else - inner_scanner = LineScanner.new(inner_content) - children = parse_blocks(inner_scanner) - child_elements = children.compact.select do |c| - c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawHTML) - end - div(**attrs) { child(*child_elements) } - end + attrs = {} + attr_str.scan(/([\w-]+)="([^"]*)"/) do |key, value| + attrs[key.to_sym] = value + end + + if inner_content.empty? + div(**attrs) else - div(class: 'raw-html') { raw_html html_content } + inner_scanner = LineScanner.new(inner_content) + children = parse_blocks(inner_scanner) + child_elements = children.compact.select do |c| + c.is_a?(Element) || c.is_a?(Text) || c.is_a?(RawNode) + end + div(**attrs) { child(*child_elements) } end else - div(class: 'raw-html') { raw_html html_content } + div(class: 'raw-html') { raw html_content } end + else + div(class: 'raw-html') { raw html_content } end + end - def parse_paragraph(scanner) - lines = [] - until scanner.eof? - l = scanner.peek - break if l.strip.empty? - break if l.match?(/^```/) - break if l.match?(/^\#{1,5}\s/) - break if l.match?(/^---+\s*$/) - break if l.match?(/^>\s/) - break if l.match?(/^\*\s/) - break if l.match?(/^\d+\.\s/) - if l.match?(/^\|/) && - scanner.pos + 1 < scanner.lines.length && - scanner.lines[scanner.pos + 1].match?(/^\|[\s:|-]+\|$/) - break - end - break if l.match?(/^:::/) - break if l.match?(/^<!--/) - break if l.match?(/^<(div|details|summary)/) - break if l.match?(/^\[\^[^\]]+\]:/) - - lines << scanner.advance + def parse_paragraph(scanner) + lines = [] + until scanner.eof? + l = scanner.peek + break if l.strip.empty? + break if l.match?(/^```/) + break if l.match?(/^\#{1,5}\s/) + break if l.match?(/^---+\s*$/) + break if l.match?(/^>\s/) + break if l.match?(/^\*\s/) + break if l.match?(/^\d+\.\s/) + if l.match?(/^\|/) && + scanner.pos + 1 < scanner.lines.length && + scanner.lines[scanner.pos + 1].match?(/^\|[\s:|-]+\|$/) + break end + break if l.match?(/^:::/) + break if l.match?(/^<!--/) + break if l.match?(/^<(div|details|summary)/) + break if l.match?(/^\[\^[^\]]+\]:/) - return nil if lines.empty? - - text_content = lines.join("\n") - inline_nodes = InlineParser.parse(text_content) - p { child(*inline_nodes) } + lines << scanner.advance end - # --- Section hierarchy --- + return nil if lines.empty? - def build_document(blocks) - footnote_blocks = blocks.select { |b| b.is_a?(FootnoteBlock) } - non_footnote_blocks = blocks.reject { |b| b.is_a?(FootnoteBlock) } - - article_content = build_section_hierarchy(non_footnote_blocks) + text_content = lines.join("\n") + inline_nodes = InlineParser.parse(text_content) + p { child(*inline_nodes) } + end - unless footnote_blocks.empty? - footnote_elements = footnote_blocks.map do |fb| - elem('footnote', id: fb.id) { child(*fb.children) } - end - footnote_section = section(class: 'footnotes') { child(*footnote_elements) } - article_content.push(footnote_section) - end + # --- Section hierarchy --- - elem('__root__') { article { child(*article_content) } } - end + def build_document(blocks) + footnote_blocks = blocks.select { |b| b.is_a?(FootnoteBlock) } + non_footnote_blocks = blocks.reject { |b| b.is_a?(FootnoteBlock) } - def build_section_hierarchy(blocks) - result = [] - section_stack = [] + article_content = build_section_hierarchy(non_footnote_blocks) - blocks.each do |block| - if block.is_a?(HeaderBlock) - level = block.level + unless footnote_blocks.empty? + footnote_elements = footnote_blocks.map do |fb| + elem('footnote', id: fb.id) { child(*fb.children) } + end + footnote_section = section(class: 'footnotes') { child(*footnote_elements) } + article_content.push(footnote_section) + end - while !section_stack.empty? && section_stack.last[:level] >= level - closed = section_stack.pop - section_el = create_section_element(closed) - if section_stack.empty? - result.push(section_el) - else - section_stack.last[:children].push(section_el) - end - end + elem('__root__') { article { child(*article_content) } } + end - section_stack.push({ - id: block.id, - attributes: block.attributes, - level: level, - heading: block.heading_element, - children: [] - }) - else - next if block.nil? + def build_section_hierarchy(blocks) + result = [] + section_stack = [] - targets = if section_stack.empty? - result - else - section_stack.last[:children] - end + blocks.each do |block| + if block.is_a?(HeaderBlock) + level = block.level - if block.is_a?(Array) - targets.concat(block) + while !section_stack.empty? && section_stack.last[:level] >= level + closed = section_stack.pop + section_el = create_section_element(closed) + if section_stack.empty? + result.push(section_el) else - targets.push(block) + section_stack.last[:children].push(section_el) end end - end - until section_stack.empty? - closed = section_stack.pop - section_el = create_section_element(closed) - if section_stack.empty? - result.push(section_el) + section_stack.push({ + id: block.id, + attributes: block.attributes, + level: level, + heading: block.heading_element, + children: [] + }) + else + next if block.nil? + + targets = if section_stack.empty? + result + else + section_stack.last[:children] + end + + if block.is_a?(Array) + targets.concat(block) else - section_stack.last[:children].push(section_el) + targets.push(block) end end + end - result + until section_stack.empty? + closed = section_stack.pop + section_el = create_section_element(closed) + if section_stack.empty? + result.push(section_el) + else + section_stack.last[:children].push(section_el) + end end - def create_section_element(section_info) - attributes = section_info[:attributes].dup - attributes['id'] = section_info[:id] if section_info[:id] + result + end - section(**attributes.transform_keys(&:to_sym)) do - child section_info[:heading] - child(*section_info[:children]) - end + def create_section_element(section_info) + attributes = section_info[:attributes].dup + attributes['id'] = section_info[:id] if section_info[:id] + + section(**attributes.transform_keys(&:to_sym)) do + child section_info[:heading] + child(*section_info[:children]) end end end diff --git a/services/nuldoc/lib/nuldoc/markdown/parser/inline_parser.rb b/services/nuldoc/lib/nuldoc/markdown/parser/inline_parser.rb index c1715904..79ad45f8 100644 --- a/services/nuldoc/lib/nuldoc/markdown/parser/inline_parser.rb +++ b/services/nuldoc/lib/nuldoc/markdown/parser/inline_parser.rb @@ -1,382 +1,382 @@ module Nuldoc module Parser - class InlineParser - extend DOM::HTML + class InlineParser < DOM::HTMLBuilder + INLINE_HTML_TAGS = %w[del mark sub sup ins br].freeze + SELF_CLOSING_TAGS = %w[br].freeze - class << self - INLINE_HTML_TAGS = %w[del mark sub sup ins br].freeze - SELF_CLOSING_TAGS = %w[br].freeze + def self.parse(text) + new.parse(text) + end - def parse(text) - parse_inline(text, 0, text.length) - end + def parse(text) + parse_inline(text, 0, text.length) + end - private + private - def parse_inline(text, start, stop) - nodes = [] - pos = start + def parse_inline(text, start, stop) + nodes = [] + pos = start - while pos < stop - # Try each inline pattern - matched = try_escape(text, pos, stop, nodes) || - try_code_span(text, pos, stop, nodes) || - try_autolink(text, pos, stop, nodes) || - try_inline_html(text, pos, stop, nodes) || - try_image(text, pos, stop, nodes) || - try_link(text, pos, stop, nodes) || - try_footnote_ref(text, pos, stop, nodes) || - try_bold(text, pos, stop, nodes) || - try_italic(text, pos, stop, nodes) || - try_strikethrough(text, pos, stop, nodes) || - try_typographic(text, pos, stop, nodes) + while pos < stop + # Try each inline pattern + matched = try_escape(text, pos, stop, nodes) || + try_code_span(text, pos, stop, nodes) || + try_autolink(text, pos, stop, nodes) || + try_inline_html(text, pos, stop, nodes) || + try_image(text, pos, stop, nodes) || + try_link(text, pos, stop, nodes) || + try_footnote_ref(text, pos, stop, nodes) || + try_bold(text, pos, stop, nodes) || + try_italic(text, pos, stop, nodes) || + try_strikethrough(text, pos, stop, nodes) || + try_typographic(text, pos, stop, nodes) - if matched - pos = matched - else - # Plain character - char_end = pos + 1 - # Collect consecutive plain characters - while char_end < stop - break if special_char?(text[char_end]) + if matched + pos = matched + else + # Plain character + char_end = pos + 1 + # Collect consecutive plain characters + while char_end < stop + break if special_char?(text[char_end]) - char_end += 1 - end - nodes << text(text[pos...char_end]) - pos = char_end + char_end += 1 end - end - - merge_text_nodes(nodes) - end - - def special_char?(ch) - case ch - when '\\', '`', '<', '!', '[', '*', '~', '.', '-', "'", '"' - true - else - false + nodes << text(text[pos...char_end]) + pos = char_end end end - def try_escape(text, pos, _stop, nodes) - return nil unless text[pos] == '\\' - return nil if pos + 1 >= text.length - - next_char = text[pos + 1] - return unless '\\`*_{}[]()#+-.!~|>:'.include?(next_char) + merge_text_nodes(nodes) + end - nodes << text(next_char) - pos + 2 + def special_char?(ch) + case ch + when '\\', '`', '<', '!', '[', '*', '~', '.', '-', "'", '"' + true + else + false end + end - def try_autolink(text, pos, stop, nodes) - return nil unless text[pos] == '<' + def try_escape(text, pos, _stop, nodes) + return nil unless text[pos] == '\\' + return nil if pos + 1 >= text.length - # Match <URL> where URL starts with http:// or https:// - close = text.index('>', pos + 1) - return nil unless close && close < stop + next_char = text[pos + 1] + return unless '\\`*_{}[]()#+-.!~|>:'.include?(next_char) - url = text[(pos + 1)...close] - return nil unless url.match?(%r{^https?://\S+$}) + nodes << text(next_char) + pos + 2 + end - nodes << a(href: url, class: 'url') { text url } - close + 1 - end + def try_autolink(text, pos, stop, nodes) + return nil unless text[pos] == '<' - def try_code_span(text, pos, stop, nodes) - return nil unless text[pos] == '`' + # Match <URL> where URL starts with http:// or https:// + close = text.index('>', pos + 1) + return nil unless close && close < stop - # Count opening backticks - tick_count = 0 - i = pos - while i < stop && text[i] == '`' - tick_count += 1 - i += 1 - end + url = text[(pos + 1)...close] + return nil unless url.match?(%r{^https?://\S+$}) - # Find matching closing backticks - close_pos = text.index('`' * tick_count, i) - return nil unless close_pos && close_pos < stop + nodes << a(href: url, class: 'url') { text url } + close + 1 + end - content = text[i...close_pos] - # Strip one leading and one trailing space if both present - content = content[1...-1] if content.length >= 2 && content[0] == ' ' && content[-1] == ' ' + def try_code_span(text, pos, stop, nodes) + return nil unless text[pos] == '`' - nodes << code { text content } - close_pos + tick_count + # Count opening backticks + tick_count = 0 + i = pos + while i < stop && text[i] == '`' + tick_count += 1 + i += 1 end - def try_inline_html(text, pos, stop, nodes) - return nil unless text[pos] == '<' + # Find matching closing backticks + close_pos = text.index('`' * tick_count, i) + return nil unless close_pos && close_pos < stop - # Self-closing tags - SELF_CLOSING_TAGS.each do |tag| - pattern = "<#{tag}>" - len = pattern.length - if pos + len <= stop && text[pos, len].downcase == pattern - nodes << elem(tag) - return pos + len - end - pattern_sc = "<#{tag} />" - len_sc = pattern_sc.length - if pos + len_sc <= stop && text[pos, len_sc].downcase == pattern_sc - nodes << elem(tag) - return pos + len_sc - end - pattern_sc2 = "<#{tag}/>" - len_sc2 = pattern_sc2.length - if pos + len_sc2 <= stop && text[pos, len_sc2].downcase == pattern_sc2 - nodes << elem(tag) - return pos + len_sc2 - end - end + content = text[i...close_pos] + # Strip one leading and one trailing space if both present + content = content[1...-1] if content.length >= 2 && content[0] == ' ' && content[-1] == ' ' - # Opening tags with content - (INLINE_HTML_TAGS - SELF_CLOSING_TAGS).each do |tag| - open_tag = "<#{tag}>" - close_tag = "</#{tag}>" - next unless pos + open_tag.length <= stop && text[pos, open_tag.length].downcase == open_tag + nodes << code { text content } + close_pos + tick_count + end - close_pos = text.index(close_tag, pos + open_tag.length) - next unless close_pos && close_pos + close_tag.length <= stop + def try_inline_html(text, pos, stop, nodes) + return nil unless text[pos] == '<' - inner = text[(pos + open_tag.length)...close_pos] - children = parse_inline(inner, 0, inner.length) - nodes << elem(tag) { child(*children) } - return close_pos + close_tag.length + # Self-closing tags + SELF_CLOSING_TAGS.each do |tag| + pattern = "<#{tag}>" + len = pattern.length + if pos + len <= stop && text[pos, len].downcase == pattern + nodes << elem(tag) + return pos + len end + pattern_sc = "<#{tag} />" + len_sc = pattern_sc.length + if pos + len_sc <= stop && text[pos, len_sc].downcase == pattern_sc + nodes << elem(tag) + return pos + len_sc + end + pattern_sc2 = "<#{tag}/>" + len_sc2 = pattern_sc2.length + if pos + len_sc2 <= stop && text[pos, len_sc2].downcase == pattern_sc2 + nodes << elem(tag) + return pos + len_sc2 + end + end + + # Opening tags with content + (INLINE_HTML_TAGS - SELF_CLOSING_TAGS).each do |tag| + open_tag = "<#{tag}>" + close_tag = "</#{tag}>" + next unless pos + open_tag.length <= stop && text[pos, open_tag.length].downcase == open_tag - nil + close_pos = text.index(close_tag, pos + open_tag.length) + next unless close_pos && close_pos + close_tag.length <= stop + + inner = text[(pos + open_tag.length)...close_pos] + children = parse_inline(inner, 0, inner.length) + nodes << elem(tag) { child(*children) } + return close_pos + close_tag.length end - def try_image(text, pos, stop, nodes) - return nil unless text[pos] == '!' && pos + 1 < stop && text[pos + 1] == '[' + nil + end - # Find ] - bracket_close = find_matching_bracket(text, pos + 1, stop) - return nil unless bracket_close + def try_image(text, pos, stop, nodes) + return nil unless text[pos] == '!' && pos + 1 < stop && text[pos + 1] == '[' - alt = text[(pos + 2)...bracket_close] + # Find ] + bracket_close = find_matching_bracket(text, pos + 1, stop) + return nil unless bracket_close - # Expect ( - return nil unless bracket_close + 1 < stop && text[bracket_close + 1] == '(' + alt = text[(pos + 2)...bracket_close] - paren_close = find_matching_paren(text, bracket_close + 1, stop) - return nil unless paren_close + # Expect ( + return nil unless bracket_close + 1 < stop && text[bracket_close + 1] == '(' - inner = text[(bracket_close + 2)...paren_close].strip - url, title = parse_url_title(inner) + paren_close = find_matching_paren(text, bracket_close + 1, stop) + return nil unless paren_close - attrs = {} - attrs[:src] = url if url - attrs[:alt] = alt unless alt.empty? - attrs[:title] = title if title + inner = text[(bracket_close + 2)...paren_close].strip + url, title = parse_url_title(inner) - nodes << img(**attrs) - paren_close + 1 - end + attrs = {} + attrs[:src] = url if url + attrs[:alt] = alt unless alt.empty? + attrs[:title] = title if title - def try_link(text, pos, stop, nodes) - return nil unless text[pos] == '[' + nodes << img(**attrs) + paren_close + 1 + end - bracket_close = find_matching_bracket(text, pos, stop) - return nil unless bracket_close + def try_link(text, pos, stop, nodes) + return nil unless text[pos] == '[' - link_text = text[(pos + 1)...bracket_close] + bracket_close = find_matching_bracket(text, pos, stop) + return nil unless bracket_close - # Expect ( - return nil unless bracket_close + 1 < stop && text[bracket_close + 1] == '(' + link_text = text[(pos + 1)...bracket_close] - paren_close = find_matching_paren(text, bracket_close + 1, stop) - return nil unless paren_close + # Expect ( + return nil unless bracket_close + 1 < stop && text[bracket_close + 1] == '(' - inner = text[(bracket_close + 2)...paren_close].strip - url, title = parse_url_title(inner) + paren_close = find_matching_paren(text, bracket_close + 1, stop) + return nil unless paren_close - attrs = {} - attrs[:href] = url if url - attrs[:title] = title if title + inner = text[(bracket_close + 2)...paren_close].strip + url, title = parse_url_title(inner) - children = parse_inline(link_text, 0, link_text.length) + attrs = {} + attrs[:href] = url if url + attrs[:title] = title if title - # Check if autolink - is_autolink = children.length == 1 && - children[0].kind == :text && - children[0].content == url - attrs[:class] = 'url' if is_autolink + children = parse_inline(link_text, 0, link_text.length) - nodes << a(**attrs) { child(*children) } - paren_close + 1 - end + # Check if autolink + is_autolink = children.length == 1 && + children[0].kind == :text && + children[0].content == url + attrs[:class] = 'url' if is_autolink - def try_footnote_ref(text, pos, stop, nodes) - return nil unless text[pos] == '[' && pos + 1 < stop && text[pos + 1] == '^' + nodes << a(**attrs) { child(*children) } + paren_close + 1 + end - close = text.index(']', pos + 2) - return nil unless close && close < stop + def try_footnote_ref(text, pos, stop, nodes) + return nil unless text[pos] == '[' && pos + 1 < stop && text[pos + 1] == '^' - # Make sure there's no nested [ or ( between - inner = text[(pos + 2)...close] - return nil if inner.include?('[') || inner.include?(']') - return nil if inner.empty? + close = text.index(']', pos + 2) + return nil unless close && close < stop - nodes << elem('footnoteref', reference: inner) - close + 1 - end + # Make sure there's no nested [ or ( between + inner = text[(pos + 2)...close] + return nil if inner.include?('[') || inner.include?(']') + return nil if inner.empty? - def try_bold(text, pos, stop, nodes) - return nil unless text[pos] == '*' && pos + 1 < stop && text[pos + 1] == '*' + nodes << elem('footnoteref', reference: inner) + close + 1 + end - close = text.index('**', pos + 2) - return nil unless close && close + 2 <= stop + def try_bold(text, pos, stop, nodes) + return nil unless text[pos] == '*' && pos + 1 < stop && text[pos + 1] == '*' - inner = text[(pos + 2)...close] - children = parse_inline(inner, 0, inner.length) - nodes << strong { child(*children) } - close + 2 - end + close = text.index('**', pos + 2) + return nil unless close && close + 2 <= stop - def try_italic(text, pos, stop, nodes) - return nil unless text[pos] == '*' - return nil if pos + 1 < stop && text[pos + 1] == '*' + inner = text[(pos + 2)...close] + children = parse_inline(inner, 0, inner.length) + nodes << strong { child(*children) } + close + 2 + end - # Find closing * that is not ** - i = pos + 1 - while i < stop - if text[i] == '*' - # Make sure it's not ** - if i + 1 < stop && text[i + 1] == '*' - i += 2 - else - inner = text[(pos + 1)...i] - return nil if inner.empty? + def try_italic(text, pos, stop, nodes) + return nil unless text[pos] == '*' + return nil if pos + 1 < stop && text[pos + 1] == '*' - children = parse_inline(inner, 0, inner.length) - nodes << em { child(*children) } - return i + 1 - end + # Find closing * that is not ** + i = pos + 1 + while i < stop + if text[i] == '*' + # Make sure it's not ** + if i + 1 < stop && text[i + 1] == '*' + i += 2 else - i += 1 + inner = text[(pos + 1)...i] + return nil if inner.empty? + + children = parse_inline(inner, 0, inner.length) + nodes << em { child(*children) } + return i + 1 end + else + i += 1 end - nil end + nil + end - def try_strikethrough(text, pos, stop, nodes) - return nil unless text[pos] == '~' && pos + 1 < stop && text[pos + 1] == '~' - - close = text.index('~~', pos + 2) - return nil unless close && close + 2 <= stop + def try_strikethrough(text, pos, stop, nodes) + return nil unless text[pos] == '~' && pos + 1 < stop && text[pos + 1] == '~' - inner = text[(pos + 2)...close] - children = parse_inline(inner, 0, inner.length) - nodes << del { child(*children) } - close + 2 - end + close = text.index('~~', pos + 2) + return nil unless close && close + 2 <= stop - def try_typographic(text, pos, stop, nodes) - # Ellipsis - if text[pos] == '.' && pos + 2 < stop && text[pos + 1] == '.' && text[pos + 2] == '.' - nodes << text("\u2026") - return pos + 3 - end + inner = text[(pos + 2)...close] + children = parse_inline(inner, 0, inner.length) + nodes << del { child(*children) } + close + 2 + end - # Em dash (must check before en dash) - if text[pos] == '-' && pos + 2 < stop && text[pos + 1] == '-' && text[pos + 2] == '-' - nodes << text("\u2014") - return pos + 3 - end + def try_typographic(text, pos, stop, nodes) + # Ellipsis + if text[pos] == '.' && pos + 2 < stop && text[pos + 1] == '.' && text[pos + 2] == '.' + nodes << text("\u2026") + return pos + 3 + end - # En dash - # Make sure it's not --- - if text[pos] == '-' && pos + 1 < stop && text[pos + 1] == '-' && (pos + 2 >= stop || text[pos + 2] != '-') - nodes << text("\u2013") - return pos + 2 - end + # Em dash (must check before en dash) + if text[pos] == '-' && pos + 2 < stop && text[pos + 1] == '-' && text[pos + 2] == '-' + nodes << text("\u2014") + return pos + 3 + end - # Smart quotes - try_smart_quotes(text, pos, stop, nodes) + # En dash + # Make sure it's not --- + if text[pos] == '-' && pos + 1 < stop && text[pos + 1] == '-' && (pos + 2 >= stop || text[pos + 2] != '-') + nodes << text("\u2013") + return pos + 2 end - def try_smart_quotes(text, pos, _stop, nodes) - ch = text[pos] - return nil unless ["'", '"'].include?(ch) + # Smart quotes + try_smart_quotes(text, pos, stop, nodes) + end - prev_char = pos.positive? ? text[pos - 1] : nil - is_opening = prev_char.nil? || prev_char == ' ' || prev_char == "\n" || prev_char == '(' || prev_char == '[' + def try_smart_quotes(text, pos, _stop, nodes) + ch = text[pos] + return nil unless ["'", '"'].include?(ch) - nodes << if ch == "'" - text(is_opening ? "\u2018" : "\u2019") - else - text(is_opening ? "\u201C" : "\u201D") - end - pos + 1 - end + prev_char = pos.positive? ? text[pos - 1] : nil + is_opening = prev_char.nil? || prev_char == ' ' || prev_char == "\n" || prev_char == '(' || prev_char == '[' - def find_matching_bracket(text, pos, stop) - return nil unless text[pos] == '[' + nodes << if ch == "'" + text(is_opening ? "\u2018" : "\u2019") + else + text(is_opening ? "\u201C" : "\u201D") + end + pos + 1 + end - depth = 0 - i = pos - while i < stop - case text[i] - when '\\' - i += 2 - next - when '[' - depth += 1 - when ']' - depth -= 1 - return i if depth.zero? - end - i += 1 + def find_matching_bracket(text, pos, stop) + return nil unless text[pos] == '[' + + depth = 0 + i = pos + while i < stop + case text[i] + when '\\' + i += 2 + next + when '[' + depth += 1 + when ']' + depth -= 1 + return i if depth.zero? end - nil + i += 1 end + nil + end - def find_matching_paren(text, pos, stop) - return nil unless text[pos] == '(' + def find_matching_paren(text, pos, stop) + return nil unless text[pos] == '(' - depth = 0 - i = pos - while i < stop - case text[i] - when '\\' - i += 2 - next - when '(' - depth += 1 - when ')' - depth -= 1 - return i if depth.zero? - end - i += 1 + depth = 0 + i = pos + while i < stop + case text[i] + when '\\' + i += 2 + next + when '(' + depth += 1 + when ')' + depth -= 1 + return i if depth.zero? end - nil + i += 1 end + nil + end - def parse_url_title(inner) - # URL might be followed by "title" - match = inner.match(/^(\S+)\s+"([^"]*)"$/) - if match - [match[1], match[2]] - else - [inner, nil] - end + def parse_url_title(inner) + # URL might be followed by "title" + match = inner.match(/^(\S+)\s+"([^"]*)"$/) + if match + [match[1], match[2]] + else + [inner, nil] end + end - def merge_text_nodes(nodes) - result = [] - nodes.each do |node| - if node.kind == :text && !result.empty? && result.last.kind == :text - result[-1] = text(result.last.content + node.content) - else - result << node - end + def merge_text_nodes(nodes) + result = [] + nodes.each do |node| + if node.kind == :text && !result.empty? && result.last.kind == :text + result[-1] = text(result.last.content + node.content) + else + result << node end - result end + result end end end diff --git a/services/nuldoc/lib/nuldoc/markdown/transform.rb b/services/nuldoc/lib/nuldoc/markdown/transform.rb index 76968fb8..0e20493e 100644 --- a/services/nuldoc/lib/nuldoc/markdown/transform.rb +++ b/services/nuldoc/lib/nuldoc/markdown/transform.rb @@ -1,12 +1,13 @@ module Nuldoc - class Transform - include DOM::HTML + class Transform < DOM::HTMLBuilder + include DOM def self.to_html(doc) new(doc).to_html end def initialize(doc) + super() @doc = doc end @@ -37,15 +38,15 @@ module Nuldoc new_children = [] current_text = +'' - n.children.each do |child| - if child.kind == :text - current_text << child.content + n.children.each do |c| + if c.kind == :text + current_text << c.content else unless current_text.empty? new_children.push(text(current_text)) current_text = +'' end - new_children.push(child) + new_children.push(c) end end @@ -160,23 +161,25 @@ module Nuldoc def transform_section_title_element section_level = 1 - g = proc do |c| - next unless c.kind == :element + g = proc do |parent| + parent.children.each_with_index do |c, i| + next unless c.kind == :element - if c.name == 'section' - section_level += 1 - c.attributes['__sectionLevel'] = section_level.to_s + if c.name == 'section' + section_level += 1 + c.attributes['__sectionLevel'] = section_level.to_s + end + g.call(c) + section_level -= 1 if c.name == 'section' + parent.children[i] = c.with(name: "h#{section_level}") if c.name == 'h' end - for_each_child(c, &g) - section_level -= 1 if c.name == 'section' - c.name = "h#{section_level}" if c.name == 'h' end - for_each_child(@doc.root, &g) + g.call(@doc.root) end def transform_note_element - for_each_element_of_type(@doc.root, 'note') do |n| + map_element_of_type(@doc.root, 'note') do |n| editat_attr = n.attributes['editat'] operation_attr = n.attributes['operation'] is_edit_block = editat_attr && operation_attr @@ -185,9 +188,9 @@ module Nuldoc text(is_edit_block ? "#{editat_attr} #{operation_attr}" : 'NOTE') end content_element = div(class: 'admonition-content') { child(*n.children.dup) } - n.name = 'div' add_class(n, 'admonition') n.children.replace([label_element, content_element]) + n.with(name: 'div') end end @@ -205,9 +208,9 @@ module Nuldoc footnote_counter = 0 footnote_map = {} - for_each_element_of_type(@doc.root, 'footnoteref') do |n| + map_element_of_type(@doc.root, 'footnoteref') do |n| reference = n.attributes['reference'] - next unless reference + next n unless reference unless footnote_map.key?(reference) footnote_counter += 1 @@ -215,7 +218,6 @@ module Nuldoc end footnote_number = footnote_map[reference] - n.name = 'sup' n.attributes.delete('reference') n.attributes['class'] = 'footnote' n.children.replace([ @@ -224,19 +226,18 @@ module Nuldoc text "[#{footnote_number}]" end ]) + n.with(name: 'sup') end - for_each_element_of_type(@doc.root, 'footnote') do |n| + map_element_of_type(@doc.root, 'footnote') do |n| id = n.attributes['id'] unless id && footnote_map.key?(id) - n.name = 'span' n.children.replace([]) - next + next n.with(name: 'span') end footnote_number = footnote_map[id] - n.name = 'div' n.attributes.delete('id') n.attributes['class'] = 'footnote' n.attributes['id'] = "footnote--#{id}" @@ -246,6 +247,7 @@ module Nuldoc a(href: "#footnoteref--#{id}") { text "#{footnote_number}. " }, *old_children ]) + n.with(name: 'div') end end @@ -257,39 +259,34 @@ module Nuldoc is_tight = n.attributes['__tight'] == 'true' next unless is_tight - n.children.each do |child| - next unless child.kind == :element && child.name == 'li' + n.children.each do |c| + next unless c.kind == :element && c.name == 'li' new_grand_children = [] - child.children.each do |grand_child| + c.children.each do |grand_child| if grand_child.kind == :element && grand_child.name == 'p' new_grand_children.concat(grand_child.children) else new_grand_children.push(grand_child) end end - child.children.replace(new_grand_children) + c.children.replace(new_grand_children) end end end def transform_and_highlight_code_block_element - for_each_child_recursively(@doc.root) do |n| - next unless n.kind == :element && n.name == 'codeblock' + map_children_recursively(@doc.root) do |n| + next n unless n.kind == :element && n.name == 'codeblock' language = n.attributes['language'] || 'text' filename = n.attributes['filename'] numbered = n.attributes['numbered'] source_code_node = n.children[0] - source_code = if source_code_node.kind == :text - source_code_node.content.rstrip - else - source_code_node.html.rstrip - end + source_code = source_code_node.content.rstrip highlighted = highlight_code(source_code, language) - n.name = 'div' n.attributes['class'] = 'codeblock' n.attributes.delete('language') @@ -302,11 +299,12 @@ module Nuldoc n.attributes.delete('filename') n.children.replace([ div(class: 'filename') { text filename }, - raw_html(highlighted) + raw(highlighted) ]) else - n.children.replace([raw_html(highlighted)]) + n.children.replace([raw(highlighted)]) end + n.with(name: 'div') end end @@ -351,8 +349,8 @@ module Nuldoc next unless section_id heading_text = '' - node.children.each do |child| - heading_text = inner_text(child) if child.kind == :element && child.name == 'a' + node.children.each do |c| + heading_text = inner_text(c) if c.kind == :element && c.name == 'a' end entry = { id: section_id, text: heading_text, level: level, children: [] } @@ -394,12 +392,12 @@ module Nuldoc return root if root.kind == :element && root.name == 'section' && root.children.include?(target) if root.kind == :element - root.children.each do |child| - next unless child.kind == :element + root.children.each do |c| + next unless c.kind == :element - return child if child.name == 'section' && child.children.include?(target) + return c if c.name == 'section' && c.children.include?(target) - result = find_parent_section(child, target) + result = find_parent_section(c, target) return result if result end end diff --git a/services/nuldoc/lib/nuldoc/pages/about_page.rb b/services/nuldoc/lib/nuldoc/pages/about_page.rb index 755ec233..07391f9a 100644 --- a/services/nuldoc/lib/nuldoc/pages/about_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/about_page.rb @@ -1,73 +1,78 @@ module Nuldoc module Pages class AboutPage - extend DOM::HTML + def initialize(slides:, config:) + @slides = slides + @config = config + end - def self.render(slides:, config:) - sorted_slides = slides.sort_by { |s| GeneratorUtils.published_date(s) }.reverse + def render + config = @config + sorted_slides = @slides.sort_by { |s| GeneratorUtils.published_date(s) }.reverse - Components::PageLayout.render( + Components::PageLayout.new( meta_copyright_year: config.site.copyright_year, meta_description: 'このサイトの著者について', meta_title: "About|#{config.sites.about.site_name}", site: 'about', config: config, - children: body(class: 'single') do - Components::AboutGlobalHeader.render(config: config) - main(class: 'main') do - article(class: 'post-single') do - header(class: 'post-header') do - h1(class: 'post-title') { text 'nsfisis' } - div(class: 'my-icon') do - div(id: 'myIcon') { img(src: '/favicon.svg') } - Components::StaticScript.render( - site: 'about', - file_name: '/my-icon.js', - defer: 'true', - config: config - ) - end - end - div(class: 'post-content') do - section do - h2 { text '読み方' } - p { text '読み方は決めていません。音にする必要があるときは本名である「いまむら」をお使いください。' } + children: DOM::HTMLBuilder.new.build do + body class: 'single' do + render(Components::AboutGlobalHeader, config: config) + main class: 'main' do + article class: 'post-single' do + header class: 'post-header' do + h1(class: 'post-title') { text 'nsfisis' } + div class: 'my-icon' do + div(id: 'myIcon') { img(src: '/favicon.svg') } + render(Components::StaticScript, + site: 'about', + file_name: '/my-icon.js', + defer: 'true', + config: config) + end end - section do - h2 { text 'アカウント' } - ul do - li do - a(href: 'https://twitter.com/nsfisis', target: '_blank', rel: 'noreferrer') do - text 'Twitter (現 𝕏): @nsfisis' + div class: 'post-content' do + section do + h2 { text '読み方' } + p { text '読み方は決めていません。音にする必要があるときは本名である「いまむら」をお使いください。' } + end + section do + h2 { text 'アカウント' } + ul do + li do + a href: 'https://twitter.com/nsfisis', target: '_blank', rel: 'noreferrer' do + text 'Twitter (現 𝕏): @nsfisis' + end end - end - li do - a(href: 'https://github.com/nsfisis', target: '_blank', rel: 'noreferrer') do - text 'GitHub: @nsfisis' + li do + a href: 'https://github.com/nsfisis', target: '_blank', rel: 'noreferrer' do + text 'GitHub: @nsfisis' + end end end end - end - section do - h2 { text '仕事' } - ul do - li do - text '2021-01~現在: ' - a(href: 'https://www.dgcircus.com/', target: '_blank', rel: 'noreferrer') do - text 'デジタルサーカス株式会社' + section do + h2 { text '仕事' } + ul do + li do + text '2021-01~現在: ' + a href: 'https://www.dgcircus.com/', target: '_blank', rel: 'noreferrer' do + text 'デジタルサーカス株式会社' + end end end end - end - section do - h2 { text '登壇' } - ul do - sorted_slides.each do |slide| - slide_url = "https://#{config.sites.slides.fqdn}#{slide.href}" - slide_date = Revision.date_to_string(GeneratorUtils.published_date(slide)) - li do - a(href: slide_url) do - text "#{slide_date}: #{slide.event} (#{slide.talk_type})" + section do + h2 { text '登壇' } + ul do + sorted_slides.each do |slide| + slide_url = "https://#{config.sites.slides.fqdn}#{slide.href}" + slide_date = Revision.date_to_string(GeneratorUtils.published_date(slide)) + li do + a href: slide_url do + text "#{slide_date}: #{slide.event} (#{slide.talk_type})" + end end end end @@ -75,10 +80,10 @@ module Nuldoc end end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/atom_page.rb b/services/nuldoc/lib/nuldoc/pages/atom_page.rb index e9f9541c..bf49f548 100644 --- a/services/nuldoc/lib/nuldoc/pages/atom_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/atom_page.rb @@ -1,24 +1,29 @@ module Nuldoc module Pages class AtomPage - extend DOM::AtomXML + def initialize(feed:) + @feed = feed + end - def self.render(feed:) - feed(xmlns: 'http://www.w3.org/2005/Atom') do - id { text feed.id } - title { text feed.title } - link(rel: 'alternate', href: feed.link_to_alternate) - link(rel: 'self', href: feed.link_to_self) - author { name { text feed.author } } - updated { text feed.updated } - feed.entries.each do |entry| - entry do - id { text entry.id } - link(rel: 'alternate', href: entry.link_to_alternate) - title { text entry.title } - summary { text entry.summary } - published { text entry.published } - updated { text entry.updated } + def render + feed = @feed + DOM::AtomXMLBuilder.new.build do + feed xmlns: 'http://www.w3.org/2005/Atom' do + id { text feed.id } + title { text feed.title } + link(rel: 'alternate', href: feed.link_to_alternate) + link(rel: 'self', href: feed.link_to_self) + author { name { text feed.author } } + updated { text feed.updated } + feed.entries.each do |entry| + entry do + id { text entry.id } + link(rel: 'alternate', href: entry.link_to_alternate) + title { text entry.title } + summary { text entry.summary } + published { text entry.published } + updated { text entry.updated } + end end end end diff --git a/services/nuldoc/lib/nuldoc/pages/home_page.rb b/services/nuldoc/lib/nuldoc/pages/home_page.rb index 405197d1..22c33c08 100644 --- a/services/nuldoc/lib/nuldoc/pages/home_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/home_page.rb @@ -1,45 +1,50 @@ module Nuldoc module Pages class HomePage - extend DOM::HTML + def initialize(config:) + @config = config + end - def self.render(config:) - Components::PageLayout.render( + def render + config = @config + Components::PageLayout.new( meta_copyright_year: config.site.copyright_year, meta_description: 'nsfisis のサイト', meta_title: config.sites.default.site_name, meta_atom_feed_href: "https://#{config.sites.default.fqdn}/atom.xml", site: 'default', config: config, - children: body(class: 'single') do - Components::DefaultGlobalHeader.render(config: config) - main(class: 'main') do - article(class: 'post-single') do - article(class: 'post-entry') do - a(href: "https://#{config.sites.about.fqdn}/") do - header(class: 'entry-header') { h2 { text 'About' } } + children: DOM::HTMLBuilder.new.build do + body class: 'single' do + render(Components::DefaultGlobalHeader, config: config) + main class: 'main' do + article class: 'post-single' do + article class: 'post-entry' do + a href: "https://#{config.sites.about.fqdn}/" do + header(class: 'entry-header') { h2 { text 'About' } } + end end - end - article(class: 'post-entry') do - a(href: "https://#{config.sites.blog.fqdn}/posts/") do - header(class: 'entry-header') { h2 { text 'Blog' } } + article class: 'post-entry' do + a href: "https://#{config.sites.blog.fqdn}/posts/" do + header(class: 'entry-header') { h2 { text 'Blog' } } + end end - end - article(class: 'post-entry') do - a(href: "https://#{config.sites.slides.fqdn}/slides/") do - header(class: 'entry-header') { h2 { text 'Slides' } } + article class: 'post-entry' do + a href: "https://#{config.sites.slides.fqdn}/slides/" do + header(class: 'entry-header') { h2 { text 'Slides' } } + end end - end - article(class: 'post-entry') do - a(href: "https://repos.#{config.sites.default.fqdn}/") do - header(class: 'entry-header') { h2 { text 'Repositories' } } + article class: 'post-entry' do + a href: "https://repos.#{config.sites.default.fqdn}/" do + header(class: 'entry-header') { h2 { text 'Repositories' } } + end end end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/not_found_page.rb b/services/nuldoc/lib/nuldoc/pages/not_found_page.rb index 08a9f2f5..b7b80285 100644 --- a/services/nuldoc/lib/nuldoc/pages/not_found_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/not_found_page.rb @@ -1,9 +1,14 @@ module Nuldoc module Pages class NotFoundPage - extend DOM::HTML + def initialize(site:, config:) + @site = site + @config = config + end - def self.render(site:, config:) + def render + site = @site + config = @config global_header = case site when 'about' then Components::AboutGlobalHeader when 'blog' then Components::BlogGlobalHeader @@ -13,20 +18,22 @@ module Nuldoc site_entry = config.site_entry(site) - Components::PageLayout.render( + Components::PageLayout.new( meta_copyright_year: config.site.copyright_year, meta_description: 'リクエストされたページが見つかりません', meta_title: "Page Not Found|#{site_entry.site_name}", site: site, config: config, - children: body(class: 'single') do - global_header.render(config: config) - main(class: 'main') do - article { div(class: 'not-found') { text '404' } } + children: DOM::HTMLBuilder.new.build do + body class: 'single' do + render(global_header, config: config) + main class: 'main' do + article { div(class: 'not-found') { text '404' } } + end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/post_list_page.rb b/services/nuldoc/lib/nuldoc/pages/post_list_page.rb index dfa77b6f..a7de1f31 100644 --- a/services/nuldoc/lib/nuldoc/pages/post_list_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/post_list_page.rb @@ -1,34 +1,45 @@ module Nuldoc module Pages class PostListPage - extend DOM::HTML + def initialize(posts:, config:, current_page:, total_pages:) + @posts = posts + @config = config + @current_page = current_page + @total_pages = total_pages + end - def self.render(posts:, config:, current_page:, total_pages:) + def render + posts = @posts + config = @config + current_page = @current_page + total_pages = @total_pages page_title = '投稿一覧' page_info_suffix = " (#{current_page}ページ目)" meta_title = "#{page_title}#{page_info_suffix}|#{config.sites.blog.site_name}" meta_description = "投稿した記事の一覧#{page_info_suffix}" - Components::PageLayout.render( + Components::PageLayout.new( meta_copyright_year: config.site.copyright_year, meta_description: meta_description, meta_title: meta_title, meta_atom_feed_href: "https://#{config.sites.blog.fqdn}/posts/atom.xml", site: 'blog', config: config, - children: body(class: 'list') do - Components::BlogGlobalHeader.render(config: config) - main(class: 'main') do - header(class: 'page-header') { h1 { text "#{page_title}#{page_info_suffix}" } } - Components::Pagination.render(current_page: current_page, total_pages: total_pages, - base_path: '/posts/') - posts.each { |post| Components::PostPageEntry.render(post: post, config: config) } - Components::Pagination.render(current_page: current_page, total_pages: total_pages, - base_path: '/posts/') + children: DOM::HTMLBuilder.new.build do + body class: 'list' do + render(Components::BlogGlobalHeader, config: config) + main class: 'main' do + header(class: 'page-header') { h1 { text "#{page_title}#{page_info_suffix}" } } + render(Components::Pagination, current_page: current_page, total_pages: total_pages, + base_path: '/posts/') + posts.each { |post| render(Components::PostPageEntry, post: post, config: config) } + render(Components::Pagination, current_page: current_page, total_pages: total_pages, + base_path: '/posts/') + end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/post_page.rb b/services/nuldoc/lib/nuldoc/pages/post_page.rb index d98dcd5d..bcd5a644 100644 --- a/services/nuldoc/lib/nuldoc/pages/post_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/post_page.rb @@ -1,53 +1,60 @@ module Nuldoc module Pages class PostPage - extend DOM::HTML + def initialize(doc:, config:) + @doc = doc + @config = config + end - def self.render(doc:, config:) - Components::PageLayout.render( + def render + doc = @doc + config = @config + Components::PageLayout.new( meta_copyright_year: GeneratorUtils.published_date(doc).year, meta_description: doc.description, meta_keywords: doc.tags.map { |slug| config.tag_label(slug) }, meta_title: "#{doc.title}|#{config.sites.blog.site_name}", site: 'blog', config: config, - children: body(class: 'single') do - Components::BlogGlobalHeader.render(config: config) - main(class: 'main') do - article(class: 'post-single') do - header(class: 'post-header') do - h1(class: 'post-title') { text doc.title } - if doc.tags.length.positive? - ul(class: 'post-tags') do - doc.tags.each do |slug| - li(class: 'tag') do - a(class: 'tag-inner', href: "/tags/#{slug}/") { text config.tag_label(slug) } + children: DOM::HTMLBuilder.new.build do + body class: 'single' do + render(Components::BlogGlobalHeader, config: config) + main class: 'main' do + article class: 'post-single' do + header class: 'post-header' do + h1(class: 'post-title') { text doc.title } + if doc.tags.length.positive? + ul class: 'post-tags' do + doc.tags.each do |slug| + li class: 'tag' do + a(class: 'tag-inner', href: "/tags/#{slug}/") { text config.tag_label(slug) } + end end end end end - end - Components::TableOfContents.render(toc: doc.toc) if doc.toc && doc.toc.items.length.positive? - div(class: 'post-content') do - section(id: 'changelog') do - h2 { a(href: '#changelog') { text '更新履歴' } } - ol do - doc.revisions.each do |rev| - ds = Revision.date_to_string(rev.date) - li(class: 'revision') do - time(datetime: ds) { text ds } - text ": #{rev.remark}" + render(Components::TableOfContents, toc: doc.toc) if doc.toc && doc.toc.items.length.positive? + div class: 'post-content' do + section id: 'changelog' do + h2 { a(href: '#changelog') { text '更新履歴' } } + ol do + doc.revisions.each do |rev| + ds = Revision.date_to_string(rev.date) + li class: 'revision' do + time(datetime: ds) { text ds } + text ": #{rev.remark}" + end end end end + child(*doc.root.children[0].children) end - child(*doc.root.children[0].children) end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/slide_list_page.rb b/services/nuldoc/lib/nuldoc/pages/slide_list_page.rb index 86a5fb40..62ca6552 100644 --- a/services/nuldoc/lib/nuldoc/pages/slide_list_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/slide_list_page.rb @@ -1,30 +1,36 @@ module Nuldoc module Pages class SlideListPage - extend DOM::HTML + def initialize(slides:, config:) + @slides = slides + @config = config + end - def self.render(slides:, config:) + def render + config = @config page_title = 'スライド一覧' - sorted = slides.sort_by { |s| GeneratorUtils.published_date(s) }.reverse + sorted = @slides.sort_by { |s| GeneratorUtils.published_date(s) }.reverse - Components::PageLayout.render( + Components::PageLayout.new( meta_copyright_year: config.site.copyright_year, meta_description: '登壇したイベントで使用したスライドの一覧', meta_title: "#{page_title}|#{config.sites.slides.site_name}", meta_atom_feed_href: "https://#{config.sites.slides.fqdn}/slides/atom.xml", site: 'slides', config: config, - children: body(class: 'list') do - Components::SlidesGlobalHeader.render(config: config) - main(class: 'main') do - header(class: 'page-header') { h1 { text page_title } } - sorted.each do |slide| - Components::SlidePageEntry.render(slide: slide, config: config) + children: DOM::HTMLBuilder.new.build do + body class: 'list' do + render(Components::SlidesGlobalHeader, config: config) + main class: 'main' do + header(class: 'page-header') { h1 { text page_title } } + sorted.each do |slide| + render(Components::SlidePageEntry, slide: slide, config: config) + end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/slide_page.rb b/services/nuldoc/lib/nuldoc/pages/slide_page.rb index 0259c1e4..0e76599d 100644 --- a/services/nuldoc/lib/nuldoc/pages/slide_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/slide_page.rb @@ -1,75 +1,81 @@ module Nuldoc module Pages class SlidePage - extend DOM::HTML + def initialize(slide:, config:) + @slide = slide + @config = config + end - def self.render(slide:, config:) - Components::PageLayout.render( + def render + slide = @slide + config = @config + Components::PageLayout.new( meta_copyright_year: GeneratorUtils.published_date(slide).year, meta_description: "「#{slide.title}」(#{slide.event} で登壇)", meta_keywords: slide.tags.map { |slug| config.tag_label(slug) }, meta_title: "#{slide.title} (#{slide.event})|#{config.sites.slides.site_name}", site: 'slides', config: config, - children: body(class: 'single') do - Components::StaticStylesheet.render(site: 'slides', file_name: '/slides.css', config: config) - Components::SlidesGlobalHeader.render(config: config) - main(class: 'main') do - article(class: 'post-single') do - header(class: 'post-header') do - h1(class: 'post-title') { text slide.title } - if slide.tags.length.positive? - ul(class: 'post-tags') do - slide.tags.each do |slug| - li(class: 'tag') do - a(class: 'tag-inner', href: "/tags/#{slug}/") { text config.tag_label(slug) } + children: DOM::HTMLBuilder.new.build do + body class: 'single' do + render(Components::StaticStylesheet, site: 'slides', file_name: '/slides.css', config: config) + render(Components::SlidesGlobalHeader, config: config) + main class: 'main' do + article class: 'post-single' do + header class: 'post-header' do + h1(class: 'post-title') { text slide.title } + if slide.tags.length.positive? + ul class: 'post-tags' do + slide.tags.each do |slug| + li class: 'tag' do + a(class: 'tag-inner', href: "/tags/#{slug}/") { text config.tag_label(slug) } + end end end end end - end - div(class: 'post-content') do - section(id: 'changelog') do - h2 { a(href: '#changelog') { text '更新履歴' } } - ol do - slide.revisions.each do |rev| - ds = Revision.date_to_string(rev.date) - li(class: 'revision') do - time(datetime: ds) { text ds } - text ": #{rev.remark}" + div class: 'post-content' do + section id: 'changelog' do + h2 { a(href: '#changelog') { text '更新履歴' } } + ol do + slide.revisions.each do |rev| + ds = Revision.date_to_string(rev.date) + li class: 'revision' do + time(datetime: ds) { text ds } + text ": #{rev.remark}" + end end end end - end - canvas(id: 'slide', 'data-slide-link': slide.slide_link) - div(class: 'controllers') do - div(class: 'controllers-buttons') do - button(id: 'prev', type: 'button') do - elem('svg', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', - stroke: 'currentColor', 'stroke-width': '2') do - elem('path', d: 'M15 18l-6-6 6-6') + canvas(id: 'slide', 'data-slide-link': slide.slide_link) + div class: 'controllers' do + div class: 'controllers-buttons' do + button id: 'prev', type: 'button' do + elem 'svg', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', + stroke: 'currentColor', 'stroke-width': '2' do + elem('path', d: 'M15 18l-6-6 6-6') + end end - end - button(id: 'next', type: 'button') do - elem('svg', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', - stroke: 'currentColor', 'stroke-width': '2') do - elem('path', d: 'M9 18l6-6-6-6') + button id: 'next', type: 'button' do + elem 'svg', width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', + stroke: 'currentColor', 'stroke-width': '2' do + elem('path', d: 'M9 18l6-6-6-6') + end end end end + render(Components::StaticScript, + site: 'slides', + file_name: '/slide.js', + type: 'module', + config: config) end - Components::StaticScript.render( - site: 'slides', - file_name: '/slide.js', - type: 'module', - config: config - ) end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/tag_list_page.rb b/services/nuldoc/lib/nuldoc/pages/tag_list_page.rb index 16b3df05..0d8dab2a 100644 --- a/services/nuldoc/lib/nuldoc/pages/tag_list_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/tag_list_page.rb @@ -1,42 +1,50 @@ module Nuldoc module Pages class TagListPage - extend DOM::HTML + def initialize(tags:, site:, config:) + @tags = tags + @site = site + @config = config + end - def self.render(tags:, site:, config:) + def render + site = @site + config = @config page_title = 'タグ一覧' global_header = site == 'blog' ? Components::BlogGlobalHeader : Components::SlidesGlobalHeader site_entry = config.site_entry(site) - sorted_tags = tags.sort_by(&:tag_slug) + sorted_tags = @tags.sort_by(&:tag_slug) - Components::PageLayout.render( + Components::PageLayout.new( meta_copyright_year: config.site.copyright_year, meta_description: 'タグの一覧', meta_title: "#{page_title}|#{site_entry.site_name}", site: site, config: config, - children: body(class: 'list') do - global_header.render(config: config) - main(class: 'main') do - header(class: 'page-header') { h1 { text page_title } } - sorted_tags.each do |tag| - posts_text = tag.num_of_posts.zero? ? '' : "#{tag.num_of_posts}件の記事" - slides_text = tag.num_of_slides.zero? ? '' : "#{tag.num_of_slides}件のスライド" - separator = !posts_text.empty? && !slides_text.empty? ? '、' : '' - footer_text = "#{posts_text}#{separator}#{slides_text}" + children: DOM::HTMLBuilder.new.build do + body class: 'list' do + render(global_header, config: config) + main class: 'main' do + header(class: 'page-header') { h1 { text page_title } } + sorted_tags.each do |tag| + posts_text = tag.num_of_posts.zero? ? '' : "#{tag.num_of_posts}件の記事" + slides_text = tag.num_of_slides.zero? ? '' : "#{tag.num_of_slides}件のスライド" + separator = !posts_text.empty? && !slides_text.empty? ? '、' : '' + footer_text = "#{posts_text}#{separator}#{slides_text}" - article(class: 'post-entry') do - a(href: tag.href) do - header(class: 'entry-header') { h2 { text tag.tag_label } } - footer(class: 'entry-footer') { text footer_text } + article class: 'post-entry' do + a href: tag.href do + header(class: 'entry-header') { h2 { text tag.tag_label } } + footer(class: 'entry-footer') { text footer_text } + end end end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/pages/tag_page.rb b/services/nuldoc/lib/nuldoc/pages/tag_page.rb index 4bc08a8c..f0946abd 100644 --- a/services/nuldoc/lib/nuldoc/pages/tag_page.rb +++ b/services/nuldoc/lib/nuldoc/pages/tag_page.rb @@ -1,38 +1,48 @@ module Nuldoc module Pages class TagPage - extend DOM::HTML + def initialize(tag_slug:, pages:, site:, config:) + @tag_slug = tag_slug + @pages = pages + @site = site + @config = config + end - def self.render(tag_slug:, pages:, site:, config:) - tag_label = config.tag_label(tag_slug) + def render + config = @config + site = @site + pages = @pages + tag_label = config.tag_label(@tag_slug) page_title = "タグ「#{tag_label}」一覧" global_header = site == 'blog' ? Components::BlogGlobalHeader : Components::SlidesGlobalHeader site_entry = config.site_entry(site) - Components::PageLayout.render( + Components::PageLayout.new( meta_copyright_year: GeneratorUtils.published_date(pages.last).year, meta_description: "タグ「#{tag_label}」のついた記事またはスライドの一覧", meta_keywords: [tag_label], meta_title: "#{page_title}|#{site_entry.site_name}", - meta_atom_feed_href: "https://#{site_entry.fqdn}/tags/#{tag_slug}/atom.xml", + meta_atom_feed_href: "https://#{site_entry.fqdn}/tags/#{@tag_slug}/atom.xml", site: site, config: config, - children: body(class: 'list') do - global_header.render(config: config) - main(class: 'main') do - header(class: 'page-header') { h1 { text page_title } } - pages.each do |page| - if page.respond_to?(:event) - Components::SlidePageEntry.render(slide: page, config: config) - else - Components::PostPageEntry.render(post: page, config: config) + children: DOM::HTMLBuilder.new.build do + body class: 'list' do + render(global_header, config: config) + main class: 'main' do + header(class: 'page-header') { h1 { text page_title } } + pages.each do |page| + if page.respond_to?(:event) + render(Components::SlidePageEntry, slide: page, config: config) + else + render(Components::PostPageEntry, post: page, config: config) + end end end + render(Components::GlobalFooter, config: config) end - Components::GlobalFooter.render(config: config) end - ) + ).build end end end diff --git a/services/nuldoc/lib/nuldoc/renderers/html.rb b/services/nuldoc/lib/nuldoc/renderers/html.rb index 89cd0ede..d71692e5 100644 --- a/services/nuldoc/lib/nuldoc/renderers/html.rb +++ b/services/nuldoc/lib/nuldoc/renderers/html.rb @@ -87,7 +87,7 @@ module Nuldoc when :text text_node_to_html(node, _indent_level: indent_level, is_in_pre: is_in_pre) when :raw - node.html + node.content when :element element_node_to_html(node, indent_level: indent_level, is_in_pre: is_in_pre) end diff --git a/services/nuldoc/lib/nuldoc/renderers/xml.rb b/services/nuldoc/lib/nuldoc/renderers/xml.rb index a1494003..7eaac5c4 100644 --- a/services/nuldoc/lib/nuldoc/renderers/xml.rb +++ b/services/nuldoc/lib/nuldoc/renderers/xml.rb @@ -23,7 +23,7 @@ module Nuldoc when :text text_node_to_xml(node) when :raw - node.html + node.content when :element element_node_to_xml(node, indent_level: indent_level) end |
