metanorma-mirror 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +25 -0
- data/README.adoc +41 -0
- data/lib/metanorma/mirror/class_table.rb +50 -0
- data/lib/metanorma/mirror/default_registry.rb +23 -0
- data/lib/metanorma/mirror/handler_registry.rb +60 -0
- data/lib/metanorma/mirror/handler_result.rb +37 -0
- data/lib/metanorma/mirror/handlers/admonition.rb +18 -0
- data/lib/metanorma/mirror/handlers/example.rb +21 -0
- data/lib/metanorma/mirror/handlers/figure.rb +53 -0
- data/lib/metanorma/mirror/handlers/formula.rb +35 -0
- data/lib/metanorma/mirror/handlers/imagemap.rb +43 -0
- data/lib/metanorma/mirror/handlers/inline/catalog.rb +36 -0
- data/lib/metanorma/mirror/handlers/inline/rich_html_renderer.rb +132 -0
- data/lib/metanorma/mirror/handlers/inline/text_extractor.rb +114 -0
- data/lib/metanorma/mirror/handlers/inline.rb +155 -0
- data/lib/metanorma/mirror/handlers/list.rb +110 -0
- data/lib/metanorma/mirror/handlers/note.rb +26 -0
- data/lib/metanorma/mirror/handlers/paragraph.rb +19 -0
- data/lib/metanorma/mirror/handlers/quote.rb +16 -0
- data/lib/metanorma/mirror/handlers/review.rb +19 -0
- data/lib/metanorma/mirror/handlers/section.rb +149 -0
- data/lib/metanorma/mirror/handlers/sourcecode.rb +27 -0
- data/lib/metanorma/mirror/handlers/structural.rb +68 -0
- data/lib/metanorma/mirror/handlers/svgmap.rb +52 -0
- data/lib/metanorma/mirror/handlers/table.rb +81 -0
- data/lib/metanorma/mirror/handlers/term.rb +100 -0
- data/lib/metanorma/mirror/handlers.rb +62 -0
- data/lib/metanorma/mirror/id_strategy/positional.rb +151 -0
- data/lib/metanorma/mirror/id_strategy/preserve.rb +12 -0
- data/lib/metanorma/mirror/id_strategy.rb +34 -0
- data/lib/metanorma/mirror/math_util.rb +53 -0
- data/lib/metanorma/mirror/metadata.rb +37 -0
- data/lib/metanorma/mirror/model/container.rb +50 -0
- data/lib/metanorma/mirror/model/factory.rb +34 -0
- data/lib/metanorma/mirror/model/guide.rb +29 -0
- data/lib/metanorma/mirror/model/leaf.rb +17 -0
- data/lib/metanorma/mirror/model/mark.rb +39 -0
- data/lib/metanorma/mirror/model/node.rb +42 -0
- data/lib/metanorma/mirror/model/soft_break.rb +32 -0
- data/lib/metanorma/mirror/model/text.rb +27 -0
- data/lib/metanorma/mirror/model.rb +16 -0
- data/lib/metanorma/mirror/output/builder.rb +39 -0
- data/lib/metanorma/mirror/output/formats/base_format.rb +90 -0
- data/lib/metanorma/mirror/output/formats/inline_format.rb +142 -0
- data/lib/metanorma/mirror/output/formats.rb +53 -0
- data/lib/metanorma/mirror/output/pipeline.rb +109 -0
- data/lib/metanorma/mirror/output/pipeline_context.rb +22 -0
- data/lib/metanorma/mirror/output.rb +12 -0
- data/lib/metanorma/mirror/rewriter.rb +123 -0
- data/lib/metanorma/mirror/safe_attr.rb +16 -0
- data/lib/metanorma/mirror/serialization/json_serializer.rb +27 -0
- data/lib/metanorma/mirror/serialization/yaml_serializer.rb +20 -0
- data/lib/metanorma/mirror/serialization.rb +10 -0
- data/lib/metanorma/mirror/transformer.rb +110 -0
- data/lib/metanorma/mirror/version.rb +7 -0
- data/lib/metanorma/mirror.rb +98 -0
- metadata +197 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
class Container < Node
|
|
7
|
+
def initialize(type: nil, attrs: {}, content: [], **)
|
|
8
|
+
super(type: type, attrs: attrs, **)
|
|
9
|
+
self.content = Array(content)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def container?
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def text_content
|
|
17
|
+
content.map do |item|
|
|
18
|
+
item.is_a?(String) ? item : item.text_content
|
|
19
|
+
end.join
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def accept_rewriter(rewriter)
|
|
23
|
+
rewriter.rewrite_container(self)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# content is declared after the sibling node classes exist: the
|
|
28
|
+
# union member list references Container itself, and lutaml-model
|
|
29
|
+
# resolves union members eagerly at declaration time. Deserialized
|
|
30
|
+
# children dispatch through Factory (the child's shape — content
|
|
31
|
+
# array vs leaf vs text — picks the class), which is why the union
|
|
32
|
+
# never receives raw hashes on the way in.
|
|
33
|
+
class Container
|
|
34
|
+
attribute :content, [Text, SoftBreak, Leaf, Container, :string],
|
|
35
|
+
collection: true, default: -> { [] }
|
|
36
|
+
|
|
37
|
+
key_value do
|
|
38
|
+
map "content", to: :content, render_empty: false,
|
|
39
|
+
with: { from: :content_from_mapping }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def content_from_mapping(model, value)
|
|
43
|
+
model.content = Array(value).map do |child|
|
|
44
|
+
child.is_a?(Hash) ? Factory.from_hash(child) : child
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
# Semantic dispatcher: the wire's "type" and "content" shape pick
|
|
7
|
+
# the model class; deserialization itself is the framework's
|
|
8
|
+
# from_hash on the chosen class.
|
|
9
|
+
class Factory
|
|
10
|
+
INVALID_INPUT = "Factory.from_hash expects a Hash, got %<class>s"
|
|
11
|
+
MISSING_TYPE = "Factory.from_hash requires a 'type' key, got %<hash>s"
|
|
12
|
+
|
|
13
|
+
def self.from_hash(hash)
|
|
14
|
+
unless hash.is_a?(Hash)
|
|
15
|
+
raise ArgumentError, format(INVALID_INPUT, class: hash.class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
case hash["type"]
|
|
19
|
+
when "text" then Text.from_hash(hash)
|
|
20
|
+
when "soft_break" then SoftBreak.from_hash(hash)
|
|
21
|
+
when nil
|
|
22
|
+
raise ArgumentError, format(MISSING_TYPE, hash: hash.inspect)
|
|
23
|
+
else
|
|
24
|
+
if hash["content"].is_a?(Array)
|
|
25
|
+
Container.from_hash(hash)
|
|
26
|
+
else
|
|
27
|
+
Leaf.from_hash(hash)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
class Guide < Lutaml::Model::Serializable
|
|
7
|
+
attribute :content, [Container, :string]
|
|
8
|
+
attribute :meta, :hash, default: -> { {} }
|
|
9
|
+
attribute :title, :string
|
|
10
|
+
|
|
11
|
+
key_value do
|
|
12
|
+
map "content", to: :content
|
|
13
|
+
map "meta", to: :meta, render_empty: false
|
|
14
|
+
map "title", to: :title, render_nil: false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# the parsed source document rides along for output formats; it
|
|
18
|
+
# is deliberately outside the serialization contract
|
|
19
|
+
attr_accessor :document
|
|
20
|
+
|
|
21
|
+
def initialize(content: nil, meta: {}, title: nil, document: nil,
|
|
22
|
+
**)
|
|
23
|
+
super(content: content, meta: meta, title: title, **)
|
|
24
|
+
self.document = document
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
class Mark < Lutaml::Model::Serializable
|
|
7
|
+
attribute :type, :string
|
|
8
|
+
attribute :attrs, :hash, default: -> { {} }
|
|
9
|
+
|
|
10
|
+
key_value do
|
|
11
|
+
map "type", to: :type
|
|
12
|
+
map "attrs", to: :attrs, render_empty: false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(type: nil, attrs: {}, **)
|
|
16
|
+
# wire keys are strings; handlers build attrs with symbol keys
|
|
17
|
+
super(type: type, attrs: (attrs || {}).transform_keys(&:to_s), **)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def [](key)
|
|
21
|
+
attrs[key.to_s]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def []=(key, value)
|
|
25
|
+
attrs[key.to_s] = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def set_attr(key, value)
|
|
29
|
+
attrs[key.to_s] = value
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(key, default = nil, &)
|
|
34
|
+
attrs.fetch(key.to_s, default, &)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
class Node < Lutaml::Model::Serializable
|
|
7
|
+
attribute :type, :string
|
|
8
|
+
attribute :attrs, :hash, default: -> { {} }
|
|
9
|
+
|
|
10
|
+
key_value do
|
|
11
|
+
map "type", to: :type
|
|
12
|
+
map "attrs", to: :attrs, render_empty: false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# **options carries framework construction kwargs
|
|
16
|
+
# (e.g. lutaml_register) through to the base serializer
|
|
17
|
+
def initialize(type: nil, attrs: {}, **)
|
|
18
|
+
# wire keys are strings; handlers and the transformer build
|
|
19
|
+
# attrs with symbol keys
|
|
20
|
+
super(type: type, attrs: (attrs || {}).transform_keys(&:to_s), **)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def leaf?
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def container?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def text_content
|
|
32
|
+
""
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def accept_rewriter(_rewriter)
|
|
36
|
+
raise NotImplementedError,
|
|
37
|
+
"#{self.class}#accept_rewriter not implemented"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
class SoftBreak < Lutaml::Model::Serializable
|
|
7
|
+
attribute :type, :string, default: -> { "soft_break" }
|
|
8
|
+
|
|
9
|
+
key_value do
|
|
10
|
+
map "type", to: :type, render_default: true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# rewriter contract: a soft break carries no attrs or content
|
|
14
|
+
def attrs
|
|
15
|
+
{}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def content
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def text_content
|
|
23
|
+
""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def accept_rewriter(rewriter)
|
|
27
|
+
rewriter.rewrite_soft_break(self)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
class Text < Lutaml::Model::Serializable
|
|
7
|
+
attribute :type, :string, default: -> { "text" }
|
|
8
|
+
attribute :text, :string, default: -> { "" }
|
|
9
|
+
attribute :marks, Mark, collection: true, default: -> { [] }
|
|
10
|
+
|
|
11
|
+
key_value do
|
|
12
|
+
map "type", to: :type, render_default: true
|
|
13
|
+
map "text", to: :text, render_default: true, render_empty: true
|
|
14
|
+
map "marks", to: :marks, render_empty: false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def text_content
|
|
18
|
+
text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def accept_rewriter(rewriter)
|
|
22
|
+
rewriter.rewrite_text(self)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Model
|
|
6
|
+
autoload :Node, "#{__dir__}/model/node"
|
|
7
|
+
autoload :Container, "#{__dir__}/model/container"
|
|
8
|
+
autoload :Leaf, "#{__dir__}/model/leaf"
|
|
9
|
+
autoload :Text, "#{__dir__}/model/text"
|
|
10
|
+
autoload :SoftBreak, "#{__dir__}/model/soft_break"
|
|
11
|
+
autoload :Mark, "#{__dir__}/model/mark"
|
|
12
|
+
autoload :Guide, "#{__dir__}/model/guide"
|
|
13
|
+
autoload :Factory, "#{__dir__}/model/factory"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Mirror
|
|
7
|
+
module Output
|
|
8
|
+
class Builder
|
|
9
|
+
attr_reader :xml_path, :output_path, :format, :options
|
|
10
|
+
|
|
11
|
+
def initialize(xml_path:, output_path:, format: :inline, **options)
|
|
12
|
+
@xml_path = xml_path
|
|
13
|
+
@output_path = output_path
|
|
14
|
+
@format = format
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build
|
|
19
|
+
pipeline = Pipeline.new(
|
|
20
|
+
xml_path: @xml_path,
|
|
21
|
+
flavor: @options[:flavor],
|
|
22
|
+
title: @options[:title] || File.basename(@xml_path, ".*"),
|
|
23
|
+
id_strategy: @options[:id_strategy],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
guide = pipeline.process
|
|
27
|
+
|
|
28
|
+
format_class = Formats.lookup(@format)
|
|
29
|
+
raise ArgumentError, "Unknown format: #{@format}" unless format_class
|
|
30
|
+
|
|
31
|
+
formatter = format_class.new(dist_dir: @options[:dist_dir])
|
|
32
|
+
formatter.write(@output_path, guide, title: @options[:title])
|
|
33
|
+
|
|
34
|
+
@output_path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Mirror
|
|
7
|
+
module Output
|
|
8
|
+
module Formats
|
|
9
|
+
class BaseFormat
|
|
10
|
+
attr_reader :dist_dir
|
|
11
|
+
|
|
12
|
+
def initialize(dist_dir: nil)
|
|
13
|
+
@dist_dir = dist_dir || self.class.default_dist_dir
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write(output_path, guide, title: "Metanorma")
|
|
17
|
+
raise NotImplementedError, "#{self.class}#write not implemented"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
attr_accessor :configured_dist_dir
|
|
22
|
+
|
|
23
|
+
def default_dist_dir
|
|
24
|
+
@configured_dist_dir || File.expand_path(
|
|
25
|
+
"../../../../../frontend/dist", __dir__
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
def safe_json(data)
|
|
33
|
+
JSON.generate(data).gsub("</script", '<\\/script')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def html_boilerplate(title:, body_content:, head_extra: "",
|
|
37
|
+
script_data: nil, app_mount_id: nil)
|
|
38
|
+
Nokogiri::HTML5::Builder.new do |doc|
|
|
39
|
+
doc.html(lang: "en") do
|
|
40
|
+
build_head(doc, title:, head_extra:, script_data:)
|
|
41
|
+
build_body(doc, body_content:, app_mount_id:)
|
|
42
|
+
end
|
|
43
|
+
end.doc.to_html
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def iife_bundle_path
|
|
47
|
+
File.join(@dist_dir, "app.iife.js")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def iife_css_path
|
|
51
|
+
File.join(@dist_dir, "app.css")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def iife_bundle_exists?
|
|
55
|
+
File.exist?(iife_bundle_path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def build_head(doc, title:, head_extra:, script_data:)
|
|
61
|
+
doc.head do
|
|
62
|
+
doc.meta(charset: "UTF-8")
|
|
63
|
+
doc.meta(name: "viewport",
|
|
64
|
+
content: "width=device-width, initial-scale=1.0")
|
|
65
|
+
doc.title { doc.text title.to_s }
|
|
66
|
+
if script_data
|
|
67
|
+
doc.script { doc.text script_data }
|
|
68
|
+
end
|
|
69
|
+
next unless head_extra && !head_extra.empty?
|
|
70
|
+
|
|
71
|
+
doc << Nokogiri::HTML5::DocumentFragment.parse(head_extra)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_body(doc, body_content:, app_mount_id:)
|
|
76
|
+
doc.body do
|
|
77
|
+
if app_mount_id
|
|
78
|
+
doc.div(id: app_mount_id) do
|
|
79
|
+
doc << Nokogiri::HTML5::DocumentFragment.parse(body_content.to_s)
|
|
80
|
+
end
|
|
81
|
+
else
|
|
82
|
+
doc << Nokogiri::HTML5::DocumentFragment.parse(body_content.to_s)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
module Metanorma
|
|
7
|
+
module Mirror
|
|
8
|
+
module Output
|
|
9
|
+
module Formats
|
|
10
|
+
class InlineFormat < BaseFormat
|
|
11
|
+
# Classic content styles inlined for the no-JS SSR body: reset +
|
|
12
|
+
# typography + block/inline component styles from the classic
|
|
13
|
+
# asset pipeline. Page chrome (layout, print, transitions, dark,
|
|
14
|
+
# cover, header, footer, toc, search, progress, shortcuts,
|
|
15
|
+
# glossary panel, flavor modules) is owned by the SPA and
|
|
16
|
+
# excluded.
|
|
17
|
+
CONTENT_CSS_MODULES = %w[
|
|
18
|
+
base/_reset
|
|
19
|
+
base/_typography
|
|
20
|
+
components/section
|
|
21
|
+
components/note
|
|
22
|
+
components/example
|
|
23
|
+
components/sourcecode
|
|
24
|
+
components/formula
|
|
25
|
+
components/admonition
|
|
26
|
+
components/table
|
|
27
|
+
components/footnote
|
|
28
|
+
components/figure
|
|
29
|
+
components/term
|
|
30
|
+
components/bibliography
|
|
31
|
+
components/inline
|
|
32
|
+
components/index
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
attr_accessor :missing_bundle_warned
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write(output_path, guide, title: "Metanorma")
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
41
|
+
|
|
42
|
+
warn_missing_bundle unless iife_bundle_exists?
|
|
43
|
+
|
|
44
|
+
data_script = "window.METANORMA_DATA = #{safe_json(guide.to_hash)};"
|
|
45
|
+
ssr_body = render_ssr_body(guide)
|
|
46
|
+
head_parts = [build_style(classic_content_css)]
|
|
47
|
+
css_inline = read_css_inline
|
|
48
|
+
if css_inline && !css_inline.empty?
|
|
49
|
+
head_parts << build_style(css_inline)
|
|
50
|
+
end
|
|
51
|
+
head_parts << build_script_src("app.iife.js") if iife_bundle_exists?
|
|
52
|
+
head_extra = head_parts.join("\n")
|
|
53
|
+
|
|
54
|
+
html = html_boilerplate(
|
|
55
|
+
title: title,
|
|
56
|
+
body_content: ssr_body,
|
|
57
|
+
head_extra: head_extra,
|
|
58
|
+
script_data: data_script,
|
|
59
|
+
app_mount_id: iife_bundle_exists? ? "metanorma-app" : nil,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
File.write(output_path, html)
|
|
63
|
+
|
|
64
|
+
if iife_bundle_exists?
|
|
65
|
+
copy_if_exists(iife_bundle_path,
|
|
66
|
+
File.join(File.dirname(output_path),
|
|
67
|
+
"app.iife.js"))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
output_path
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# SSR body is the classic renderer's document body: identical to
|
|
76
|
+
# the static HTML output, embedded as the no-JS / first-paint
|
|
77
|
+
# placeholder the SPA replaces on mount.
|
|
78
|
+
def render_ssr_body(guide)
|
|
79
|
+
document = guide.document
|
|
80
|
+
unless document
|
|
81
|
+
raise ArgumentError,
|
|
82
|
+
"InlineFormat requires a Guide carrying its source document"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
renderer = Metanorma::Html::Generator.renderer_for(document).new
|
|
86
|
+
renderer.generate_body(document)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def classic_content_css
|
|
90
|
+
css_dir = Metanorma::Html::AssetPipeline::CSS_DIR
|
|
91
|
+
CONTENT_CSS_MODULES.filter_map do |mod|
|
|
92
|
+
path = File.join(css_dir, "#{mod}.css")
|
|
93
|
+
File.read(path) if File.exist?(path)
|
|
94
|
+
end.join("\n")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def warn_missing_bundle
|
|
98
|
+
return if self.class.missing_bundle_warned
|
|
99
|
+
|
|
100
|
+
warn "metanorma-document: #{iife_bundle_path} not found — " \
|
|
101
|
+
"writing static HTML without the interactive SPA. " \
|
|
102
|
+
"Run `rake build_frontend` to build it."
|
|
103
|
+
self.class.missing_bundle_warned = true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_style(css)
|
|
107
|
+
Nokogiri::HTML5::Builder.new do |doc|
|
|
108
|
+
doc.style { doc.text css }
|
|
109
|
+
end.doc.root.to_html
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_script_src(src)
|
|
113
|
+
Nokogiri::HTML5::Builder.new do |doc|
|
|
114
|
+
doc.script(src: src)
|
|
115
|
+
end.doc.root.to_html
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def read_css_inline
|
|
119
|
+
path = iife_css_path
|
|
120
|
+
File.exist?(path) ? File.read(path) : ""
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def copy_if_exists(src, dst)
|
|
124
|
+
return unless File.exist?(src)
|
|
125
|
+
|
|
126
|
+
unless File.exist?(dst) && FileUtils.identical?(
|
|
127
|
+
src, dst
|
|
128
|
+
)
|
|
129
|
+
FileUtils.cp(src,
|
|
130
|
+
dst)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
Metanorma::Mirror::Output::Formats.register(
|
|
140
|
+
:inline,
|
|
141
|
+
Metanorma::Mirror::Output::Formats::InlineFormat,
|
|
142
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Output
|
|
6
|
+
module Formats
|
|
7
|
+
autoload :BaseFormat, "#{__dir__}/formats/base_format"
|
|
8
|
+
autoload :InlineFormat, "#{__dir__}/formats/inline_format"
|
|
9
|
+
|
|
10
|
+
# All format modules that should be auto-registered. Adding a new
|
|
11
|
+
# format = adding one entry here. No edits to lookup logic.
|
|
12
|
+
REGISTERED = %i[InlineFormat].freeze
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
# Returns the format class registered under `name`, or nil.
|
|
16
|
+
# Triggers autoload of all known format modules on first call.
|
|
17
|
+
def lookup(name)
|
|
18
|
+
ensure_loaded
|
|
19
|
+
format_map[name]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def registered?(name)
|
|
23
|
+
ensure_loaded
|
|
24
|
+
format_map.key?(name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def register(name, format_class)
|
|
28
|
+
format_map[name] = format_class
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unregister(name)
|
|
33
|
+
format_map.delete(name)
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def format_map
|
|
40
|
+
@format_map ||= {}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ensure_loaded
|
|
44
|
+
return if @loaded
|
|
45
|
+
|
|
46
|
+
REGISTERED.each { |mod_name| const_get(mod_name) }
|
|
47
|
+
@loaded = true
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|