aboutsummaryrefslogtreecommitdiffhomepage
path: root/lib
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2022-11-19 14:23:32 +0900
committernsfisis <nsfisis@gmail.com>2022-11-19 14:25:59 +0900
commit6209453817da9922f28bac1bb1522c6d380630ab (patch)
tree19e0699e751af387d549d6720ca215c8065b3c0c /lib
parent0cafa073914b5e0b162b735a7f8445fb2aa8a604 (diff)
downloadblog.nsfisis.dev-6209453817da9922f28bac1bb1522c6d380630ab.tar.gz
blog.nsfisis.dev-6209453817da9922f28bac1bb1522c6d380630ab.tar.zst
blog.nsfisis.dev-6209453817da9922f28bac1bb1522c6d380630ab.zip
Hugo to Asciidoctor
Diffstat (limited to 'lib')
-rw-r--r--lib/command.rb152
-rw-r--r--lib/extensions/document_title_processor.rb15
-rw-r--r--lib/extensions/lang_attribute_processor.rb9
-rw-r--r--lib/extensions/revision_history_processor.rb27
-rw-r--r--lib/extensions/section_id_validator.rb29
-rw-r--r--lib/extensions/source_id_processor.rb12
-rw-r--r--lib/extensions/source_id_validator.rb32
-rw-r--r--lib/extensions/tags_processor.rb20
-rw-r--r--lib/parser.rb54
-rw-r--r--lib/revision.rb3
-rw-r--r--lib/tag.rb26
11 files changed, 379 insertions, 0 deletions
diff --git a/lib/command.rb b/lib/command.rb
new file mode 100644
index 0000000..81692b3
--- /dev/null
+++ b/lib/command.rb
@@ -0,0 +1,152 @@
+module NulDoc
+ class Command
+ def initialize(config)
+ @config = config
+ @content_dir = @config[:content_dir]
+ @dest_dir = @config[:dest_dir]
+ @template_dir = @config[:template_dir]
+ @parser = NulDoc::Parser.new(
+ {
+ 'author' => @config[:author],
+ 'site-copyright-year' => @config[:site_copyright_year],
+ 'site-name' => @config[:site_name],
+ 'source-highlighter' => 'rouge',
+ 'reproducible' => true,
+ 'sectids' => false,
+ },
+ @content_dir,
+ @template_dir,
+ )
+ end
+
+ def run
+ posts = generate_posts(@content_dir + '/posts')
+ generate_tags(posts)
+ generate_posts_list(posts)
+ end
+
+ private
+
+ def generate_posts(source_dir)
+ post_files = collect_post_files(source_dir)
+ posts = parse_posts(post_files)
+ output_posts(posts)
+ posts
+ end
+
+ def collect_post_files(source_dir)
+ file_paths = []
+ Dir.glob('**/*.adoc', base: source_dir, sort: true) do |path|
+ file_paths << "#{source_dir}/#{path}"
+ end
+ file_paths
+ end
+
+ def parse_posts(post_file_paths)
+ post_file_paths.map { @parser.parse_file(_1) }
+ end
+
+ def output_posts(posts)
+ posts.each do |post|
+ destination_file_path = post.attributes['source-file-path']
+ .sub(@content_dir, @dest_dir)
+ .sub('.adoc', '/index.html')
+ destination_dir = File.dirname(destination_file_path)
+ unless Dir.exist?(destination_dir)
+ FileUtils.makedirs(destination_dir)
+ end
+ open(destination_file_path, 'w') do |f|
+ f.puts(post.convert)
+ end
+ end
+ end
+
+ def generate_tags(posts)
+ tags_and_posts = collect_tags(posts)
+ tag_docs = build_tag_docs(tags_and_posts)
+ output_tags(tag_docs)
+ end
+
+ def collect_tags(posts)
+ tags_and_posts = {}
+ posts.each do |post|
+ post.attributes['tags'].each do |tag|
+ tags_and_posts[tag] ||= []
+ tags_and_posts[tag] << post
+ end
+ end
+ tags_and_posts
+ .transform_values {|posts|
+ posts.sort_by {|post|
+ post.attributes['revision-history'].first.date # created_at
+ }.reverse
+ }
+ .sort_by {|tag, _| tag.slug }
+ .to_h
+ end
+
+ def build_tag_docs(tags_and_posts)
+ tags_and_posts.map do |tag, posts|
+ [tag, build_tag_doc(tag, posts)]
+ end
+ end
+
+ def build_tag_doc(tag, posts)
+ erb = ERB.new(File.read(@template_dir + '/tag.html.erb'))
+ erb.result_with_hash({
+ tag: tag,
+ posts: posts,
+ author: @config[:author],
+ site_copyright_year: @config[:site_copyright_year],
+ site_name: @config[:site_name],
+ lang: 'ja-JP', # TODO
+ copyright_year: posts.last.attributes['revision-history'].first.date.year,
+ description: "タグ「#{tag.label}」のついた記事一覧",
+ })
+ end
+
+ def output_tags(tag_docs)
+ tag_docs.each do |tag, html|
+ destination_file_path = "#{@dest_dir}/tags/#{tag.slug}/index.html"
+ destination_dir = File.dirname(destination_file_path)
+ unless Dir.exist?(destination_dir)
+ FileUtils.makedirs(destination_dir)
+ end
+ open(destination_file_path, 'w') do |f|
+ f.puts(html)
+ end
+ end
+ end
+
+ def generate_posts_list(posts)
+ html = build_posts_list_doc(posts)
+ output_posts_list(html)
+ end
+
+ def build_posts_list_doc(posts)
+ erb = ERB.new(File.read(@template_dir + '/posts_list.html.erb'))
+ erb.result_with_hash({
+ posts: posts.reverse,
+ author: @config[:author],
+ site_copyright_year: @config[:site_copyright_year],
+ site_name: @config[:site_name],
+ lang: 'ja-JP', # TODO
+ copyright_year: @config[:site_copyright_year],
+ description: "記事一覧",
+ doctitle: "Posts",
+ header_title: "Posts",
+ })
+ end
+
+ def output_posts_list(html)
+ destination_file_path = "#{@dest_dir}/posts/index.html"
+ destination_dir = File.dirname(destination_file_path)
+ unless Dir.exist?(destination_dir)
+ FileUtils.makedirs(destination_dir)
+ end
+ open(destination_file_path, 'w') do |f|
+ f.puts(html)
+ end
+ end
+ end
+end
diff --git a/lib/extensions/document_title_processor.rb b/lib/extensions/document_title_processor.rb
new file mode 100644
index 0000000..fd25844
--- /dev/null
+++ b/lib/extensions/document_title_processor.rb
@@ -0,0 +1,15 @@
+module Nuldoc
+ module Extensions
+ class DocumentTitleProcessor < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ doc.title = substitute_document_title(doc.title)
+ end
+
+ private
+
+ def substitute_document_title(title)
+ title.sub(/\A\[(.+?)\] /, '【\1】')
+ end
+ end
+ end
+end
diff --git a/lib/extensions/lang_attribute_processor.rb b/lib/extensions/lang_attribute_processor.rb
new file mode 100644
index 0000000..65511bc
--- /dev/null
+++ b/lib/extensions/lang_attribute_processor.rb
@@ -0,0 +1,9 @@
+module Nuldoc
+ module Extensions
+ class LangAttributeProcessor < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ doc.attributes['lang'] ||= 'ja-JP'
+ end
+ end
+ end
+end
diff --git a/lib/extensions/revision_history_processor.rb b/lib/extensions/revision_history_processor.rb
new file mode 100644
index 0000000..f416de0
--- /dev/null
+++ b/lib/extensions/revision_history_processor.rb
@@ -0,0 +1,27 @@
+module Nuldoc
+ module Extensions
+ class RevisionHistoryProcessor < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ revisions = []
+ i = 1
+ loop do
+ break unless (rev = doc.attributes["revision-#{i}"])
+ revisions << parse_revision(rev)
+ i += 1
+ end
+ doc.attributes['revision-history'] = revisions
+ end
+
+ private
+
+ def parse_revision(rev)
+ m = rev.match(/\A(\d\d\d\d-\d\d-\d\d) (.*)\z/)
+ raise unless m
+ Revision.new(
+ date: Date.parse(m[1], '%Y-%m-%d'),
+ remark: m[2],
+ )
+ end
+ end
+ end
+end
diff --git a/lib/extensions/section_id_validator.rb b/lib/extensions/section_id_validator.rb
new file mode 100644
index 0000000..2ad496c
--- /dev/null
+++ b/lib/extensions/section_id_validator.rb
@@ -0,0 +1,29 @@
+module Nuldoc
+ module Extensions
+ class SectionIdValidator < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ errors = []
+ (doc.find_by(context: :section) {_1.level > 0}).each do |section|
+ errors << validate_section(section)
+ end
+ error_message = errors.compact.join("\n")
+ unless error_message.empty?
+ raise "SectionIdValidator (#{doc.attributes['source-file-path']}):\n#{error_message}"
+ end
+ end
+
+ private
+
+ def validate_section(section)
+ id = section.id
+ unless id
+ return "Section '#{section.title}': each section MUST have an id."
+ end
+ unless id.match?(/\A[-0-9a-z]+\z/)
+ return "Section '#{section.title}' (##{id}): section id MUST consist of either hyphen, digits or lowercases."
+ end
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/extensions/source_id_processor.rb b/lib/extensions/source_id_processor.rb
new file mode 100644
index 0000000..13813e0
--- /dev/null
+++ b/lib/extensions/source_id_processor.rb
@@ -0,0 +1,12 @@
+module Nuldoc
+ module Extensions
+ class SourceIdProcessor < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ errors = []
+ (doc.find_by(context: :listing) {_1.style == 'source'}).each do |source|
+ source.id = "source.#{source.id}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/extensions/source_id_validator.rb b/lib/extensions/source_id_validator.rb
new file mode 100644
index 0000000..6e04deb
--- /dev/null
+++ b/lib/extensions/source_id_validator.rb
@@ -0,0 +1,32 @@
+module Nuldoc
+ module Extensions
+ class SourceIdValidator < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ errors = []
+ (doc.find_by(context: :listing) {_1.style == 'source'}).each do |source|
+ errors << validate_section(source)
+ end
+ error_message = errors.compact.join("\n")
+ unless error_message.empty?
+ raise "SourceIdValidator (#{doc.attributes['source-file-path']}):\n#{error_message}"
+ end
+ end
+
+ private
+
+ def validate_section(source)
+ id = source.id
+ unless id
+ return "Each source MUST have an id."
+ end
+ if id.start_with?('source.')
+ return "Source id (##{id}) MUST NOT start with 'source.', which is appended by `nul`."
+ end
+ unless id.match?(/\A[-0-9a-z]+\z/)
+ return "Source id (##{id}) MUST consist of either hypen, digits or lowercases."
+ end
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/extensions/tags_processor.rb b/lib/extensions/tags_processor.rb
new file mode 100644
index 0000000..efbd2a8
--- /dev/null
+++ b/lib/extensions/tags_processor.rb
@@ -0,0 +1,20 @@
+module Nuldoc
+ module Extensions
+ class TagsProcessor < Asciidoctor::Extensions::TreeProcessor
+ def process(doc)
+ doc.attributes['tags'] = convert_tags(doc.attributes['tags'])
+ end
+
+ private
+
+ def convert_tags(tags)
+ return [] unless tags
+
+ tags
+ .split(',')
+ .map(&:strip)
+ .map { Tag.from_slug(_1) }
+ end
+ end
+ end
+end
diff --git a/lib/parser.rb b/lib/parser.rb
new file mode 100644
index 0000000..8ae3303
--- /dev/null
+++ b/lib/parser.rb
@@ -0,0 +1,54 @@
+module NulDoc
+ class Parser
+ def initialize(common_attributes, content_dir, template_dir)
+ @common_attributes = common_attributes
+ @content_dir = content_dir
+ @template_dir = template_dir
+ end
+
+ def parse_file(file_path)
+ Asciidoctor.load_file(
+ file_path,
+ backend: :html5,
+ doctype: :article,
+ standalone: true,
+ safe: :unsafe,
+ template_dirs: [@template_dir],
+ template_engine: 'erb',
+ attributes: @common_attributes.merge({
+ 'source-file-path' => file_path,
+ 'href' => file_path.sub(@content_dir, '').sub('.adoc', '/'),
+ }),
+ extension_registry: Asciidoctor::Extensions.create do
+ tree_processor Nuldoc::Extensions::RevisionHistoryProcessor
+ tree_processor Nuldoc::Extensions::DocumentTitleProcessor
+ tree_processor Nuldoc::Extensions::LangAttributeProcessor
+ # tree_processor Nuldoc::Extensions::SectionIdValidator
+ # tree_processor Nuldoc::Extensions::SourceIdValidator
+ tree_processor Nuldoc::Extensions::TagsProcessor
+
+ # MUST BE AT THE END
+ tree_processor Nuldoc::Extensions::SourceIdProcessor
+ end,
+ )
+ end
+
+ def parse_string(s, copyright_year)
+ Asciidoctor.convert(
+ s,
+ backend: :html5,
+ doctype: :article,
+ safe: :unsafe,
+ template_dirs: [@template_dir],
+ template_engine: 'erb',
+ attributes: @common_attributes.merge({
+ 'copyright-year' => copyright_year,
+ }),
+ extension_registry: Asciidoctor::Extensions.create do
+ tree_processor Nuldoc::Extensions::LangAttributeProcessor
+ tree_processor Nuldoc::Extensions::TagsProcessor
+ end,
+ )
+ end
+ end
+end
diff --git a/lib/revision.rb b/lib/revision.rb
new file mode 100644
index 0000000..b986a14
--- /dev/null
+++ b/lib/revision.rb
@@ -0,0 +1,3 @@
+module Nuldoc
+ Revision = Struct.new(:date, :remark, keyword_init: true)
+end
diff --git a/lib/tag.rb b/lib/tag.rb
new file mode 100644
index 0000000..a7c13b2
--- /dev/null
+++ b/lib/tag.rb
@@ -0,0 +1,26 @@
+module Nuldoc
+ Tag = Struct.new(:slug, :label, keyword_init: true) do
+ LABELS = {
+ 'conference' => 'カンファレンス',
+ 'cpp' => 'C++',
+ 'cpp17' => 'C++ 17',
+ 'note-to-self' => '備忘録',
+ 'php' => 'PHP',
+ 'phpcon' => 'PHP カンファレンス',
+ 'phperkaigi' => 'PHPerKaigi',
+ 'python' => 'Python',
+ 'python3' => 'Python 3',
+ 'ruby' => 'Ruby',
+ 'ruby3' => 'Ruby 3',
+ 'rust' => 'Rust',
+ 'vim' => 'Vim',
+ }
+
+ def self.from_slug(slug)
+ Tag.new(
+ slug: slug,
+ label: (LABELS[slug] || raise("No label for tag '#{slug}'")),
+ )
+ end
+ end
+end