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,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Output
|
|
6
|
+
class Pipeline
|
|
7
|
+
attr_reader :steps, :context
|
|
8
|
+
|
|
9
|
+
def initialize(xml_path:, steps: nil, flavor: nil, title: nil,
|
|
10
|
+
id_strategy: nil)
|
|
11
|
+
@steps = steps || [Steps::ParseXml, Steps::TransformMirror, Steps::AttachMetadata]
|
|
12
|
+
@context = PipelineContext.new(
|
|
13
|
+
xml_path: xml_path,
|
|
14
|
+
flavor: flavor,
|
|
15
|
+
title: title || File.basename(xml_path, ".*"),
|
|
16
|
+
id_strategy: id_strategy,
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def process
|
|
21
|
+
@steps.each { |step_class| step_class.new.call(@context) }
|
|
22
|
+
Model::Guide.new(
|
|
23
|
+
content: @context.content,
|
|
24
|
+
meta: @context.meta,
|
|
25
|
+
title: @context.title,
|
|
26
|
+
document: @context.parsed,
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module Steps
|
|
31
|
+
class ParseXml
|
|
32
|
+
def call(context)
|
|
33
|
+
flavor = context.flavor || infer_flavor(context.xml_path)
|
|
34
|
+
doc_class = flavor_class(flavor)
|
|
35
|
+
xml_content = File.read(context.xml_path)
|
|
36
|
+
context.parsed = doc_class.from_xml(xml_content)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.flavor_map
|
|
40
|
+
@flavor_map ||= Metanorma.constants.each_with_object({}) do |c, map|
|
|
41
|
+
next unless c.to_s.end_with?("Document")
|
|
42
|
+
|
|
43
|
+
flavor = c.to_s.delete_suffix("Document").downcase
|
|
44
|
+
map[flavor] = c.to_s
|
|
45
|
+
end.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def infer_flavor(xml_path)
|
|
49
|
+
basename = File.basename(xml_path, ".*")
|
|
50
|
+
from_name = basename.split("-").first.to_s.downcase
|
|
51
|
+
return from_name if self.class.flavor_map.key?(from_name)
|
|
52
|
+
|
|
53
|
+
File.dirname(xml_path).split("/").reverse_each do |seg|
|
|
54
|
+
prefix = seg.split("-").first.to_s.downcase
|
|
55
|
+
return prefix if self.class.flavor_map.key?(prefix)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def flavor_class(flavor)
|
|
62
|
+
camel = flavor.split("_").map(&:capitalize).join
|
|
63
|
+
new_namespace = "Metanorma::#{camel}::Document"
|
|
64
|
+
begin
|
|
65
|
+
require "metanorma/#{flavor}/document"
|
|
66
|
+
rescue LoadError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
if Object.const_defined?(new_namespace)
|
|
70
|
+
return Object.const_get(new_namespace).const_get(:Root)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
require "metanorma/standoc"
|
|
75
|
+
rescue LoadError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
class_name = self.class.flavor_map[flavor] || "StandardDocument"
|
|
79
|
+
Metanorma.const_get(class_name).const_get(:Root)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class TransformMirror
|
|
84
|
+
def call(context)
|
|
85
|
+
id_strategy = context.id_strategy || Mirror::DEFAULT_ID_STRATEGY
|
|
86
|
+
transformer = Mirror::Transformer.new(id_strategy: id_strategy)
|
|
87
|
+
context.content = transformer.from_metanorma(context.parsed)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class AttachMetadata
|
|
92
|
+
def call(context)
|
|
93
|
+
parsed = context.parsed
|
|
94
|
+
return unless parsed
|
|
95
|
+
|
|
96
|
+
bibdata = SafeAttr.read(parsed, :bibdata)
|
|
97
|
+
return unless bibdata
|
|
98
|
+
|
|
99
|
+
meta = {}
|
|
100
|
+
meta["title"] = Metadata.title_from_bibdata(bibdata)
|
|
101
|
+
meta["flavor"] = context.flavor if context.flavor
|
|
102
|
+
context.meta = meta
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Output
|
|
6
|
+
class PipelineContext
|
|
7
|
+
attr_reader :xml_path, :flavor, :title, :id_strategy
|
|
8
|
+
attr_accessor :parsed, :content, :meta
|
|
9
|
+
|
|
10
|
+
def initialize(xml_path:, flavor: nil, title: nil, id_strategy: nil)
|
|
11
|
+
@xml_path = xml_path
|
|
12
|
+
@flavor = flavor
|
|
13
|
+
@title = title
|
|
14
|
+
@id_strategy = id_strategy
|
|
15
|
+
@parsed = nil
|
|
16
|
+
@content = nil
|
|
17
|
+
@meta = {}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Output
|
|
6
|
+
autoload :Pipeline, "#{__dir__}/output/pipeline"
|
|
7
|
+
autoload :PipelineContext, "#{__dir__}/output/pipeline_context"
|
|
8
|
+
autoload :Builder, "#{__dir__}/output/builder"
|
|
9
|
+
autoload :Formats, "#{__dir__}/output/formats"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
# Rewrites a mirror Model graph with configurable type-skipping and
|
|
6
|
+
# per-type customization. Despite living in the reverse-conversion
|
|
7
|
+
# namespace, this class produces a Model graph (not Metanorma XML) —
|
|
8
|
+
# the output is a fully model-driven representation that can be
|
|
9
|
+
# serialized via Model#to_hash or embedded into HTML output by
|
|
10
|
+
# Output::Formats::InlineFormat (which renders the source document
|
|
11
|
+
# through the classic Metanorma::Html renderer).
|
|
12
|
+
#
|
|
13
|
+
# Instance API:
|
|
14
|
+
# Rewriter.new(skip: Set.new(%w[review footnotes]))
|
|
15
|
+
# rewriter.skip("my_type")
|
|
16
|
+
# rewriter.register("my_type") { |n, r| ... }
|
|
17
|
+
#
|
|
18
|
+
# Class API (seeds defaults copied into each new instance):
|
|
19
|
+
# Rewriter.skip("review") # adds to defaults
|
|
20
|
+
# Rewriter.register("my_type") { |n, r| ... }
|
|
21
|
+
#
|
|
22
|
+
# Two Rewriter instances do not share state. Class-level mutation only
|
|
23
|
+
# affects instances created afterwards (the default snapshot is copied
|
|
24
|
+
# at `new` time).
|
|
25
|
+
class Rewriter
|
|
26
|
+
DEFAULT_SKIPPED_TYPES = %w[review footnotes].freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Default skip set, copied into each new instance.
|
|
30
|
+
def default_skipped_types
|
|
31
|
+
@default_skipped_types ||= Set.new(DEFAULT_SKIPPED_TYPES)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Default builder map, copied into each new instance.
|
|
35
|
+
def default_builders
|
|
36
|
+
@default_builders ||= {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add a type to the default skip set. Affects instances created
|
|
40
|
+
# afterwards; existing instances are unchanged.
|
|
41
|
+
def skip(type)
|
|
42
|
+
default_skipped_types << type
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Register a builder for a type at the default level. Affects
|
|
47
|
+
# instances created afterwards; existing instances are unchanged.
|
|
48
|
+
def register(type, &block)
|
|
49
|
+
default_builders[type] = block
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(skip: nil, builders: nil)
|
|
55
|
+
@skipped_types = skip || self.class.default_skipped_types.dup
|
|
56
|
+
@builders = builders || self.class.default_builders.dup
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attr_reader :skipped_types, :builders
|
|
60
|
+
|
|
61
|
+
def skip(type)
|
|
62
|
+
skipped_types << type
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def skipped?(type)
|
|
67
|
+
skipped_types.include?(type)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def register(type, &block)
|
|
71
|
+
builders[type] = block
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def call(mirror_node)
|
|
76
|
+
build(mirror_node)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build(node)
|
|
80
|
+
return nil unless node
|
|
81
|
+
|
|
82
|
+
return rewrite_string(node) if node.is_a?(String)
|
|
83
|
+
|
|
84
|
+
node.accept_rewriter(self)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def rewrite_container(node)
|
|
88
|
+
return nil if skipped?(node.type)
|
|
89
|
+
|
|
90
|
+
builder = builders[node.type]
|
|
91
|
+
return builder.call(node, self) if builder
|
|
92
|
+
|
|
93
|
+
Model::Container.new(
|
|
94
|
+
type: node.type,
|
|
95
|
+
attrs: node.attrs,
|
|
96
|
+
content: build_children(node.content),
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def rewrite_leaf(node)
|
|
101
|
+
return nil if skipped?(node.type)
|
|
102
|
+
|
|
103
|
+
Model::Leaf.new(type: node.type, attrs: node.attrs)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def rewrite_text(node)
|
|
107
|
+
Model::Text.new(text: node.text, marks: node.marks)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def rewrite_soft_break(_node)
|
|
111
|
+
Model::SoftBreak.new
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def rewrite_string(string)
|
|
115
|
+
Model::Text.new(text: string)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_children(content)
|
|
119
|
+
Array(content).filter_map { |child| build(child) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module SafeAttr
|
|
6
|
+
def self.read(element, attr_name)
|
|
7
|
+
return nil unless element.is_a?(Lutaml::Model::Serializable)
|
|
8
|
+
|
|
9
|
+
klass = element.class
|
|
10
|
+
return nil unless klass.attributes.key?(attr_name)
|
|
11
|
+
|
|
12
|
+
element.public_send(attr_name)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Mirror
|
|
7
|
+
module Serialization
|
|
8
|
+
class JsonSerializer
|
|
9
|
+
# Every Model::Node (Leaf included) has a data to_hash; the former
|
|
10
|
+
# Container-only guard serialized bare leaves as inspect strings.
|
|
11
|
+
def self.serialize(node)
|
|
12
|
+
data = node.is_a?(Model::Node) ? node.to_hash : node
|
|
13
|
+
data.to_json
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.serialize_pretty(node)
|
|
17
|
+
data = node.is_a?(Model::Node) ? node.to_hash : node
|
|
18
|
+
JSON.pretty_generate(data)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.deserialize(json_string)
|
|
22
|
+
Model::Factory.from_hash(JSON.parse(json_string))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Mirror
|
|
7
|
+
module Serialization
|
|
8
|
+
class YamlSerializer
|
|
9
|
+
def self.serialize(node)
|
|
10
|
+
data = node.is_a?(Model::Container) ? node.to_hash : node
|
|
11
|
+
data.to_yaml
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.deserialize(yaml_string)
|
|
15
|
+
Model::Factory.from_hash(YAML.safe_load(yaml_string))
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
class Transformer
|
|
6
|
+
attr_reader :registry, :id_strategy
|
|
7
|
+
|
|
8
|
+
def initialize(registry: Mirror.default_registry,
|
|
9
|
+
id_strategy: Mirror::DEFAULT_ID_STRATEGY)
|
|
10
|
+
@registry = registry
|
|
11
|
+
@id_strategy = id_strategy
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(root)
|
|
15
|
+
attrs = {}
|
|
16
|
+
attrs[:flavor] = root.flavor if root.flavor
|
|
17
|
+
attrs[:type] = root.type if root.type
|
|
18
|
+
attrs[:schema_version] = root.schema_version if root.schema_version
|
|
19
|
+
|
|
20
|
+
title = extract_root_title(root)
|
|
21
|
+
attrs[:title] = title if title
|
|
22
|
+
|
|
23
|
+
content = []
|
|
24
|
+
|
|
25
|
+
preface = root.preface
|
|
26
|
+
if preface
|
|
27
|
+
result = @registry.handle(preface, context: self)
|
|
28
|
+
result.append_to(content)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sections = root.sections
|
|
32
|
+
if sections
|
|
33
|
+
result = @registry.handle(sections, context: self)
|
|
34
|
+
result.append_to(content)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
annex = root.annex
|
|
38
|
+
annex&.each do |a|
|
|
39
|
+
result = @registry.handle(a, context: self)
|
|
40
|
+
result.append_to(content)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
bibliography = root.bibliography
|
|
44
|
+
if bibliography
|
|
45
|
+
result = @registry.handle(bibliography, context: self)
|
|
46
|
+
result.append_to(content)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Handlers.build_node("doc", attrs: attrs, content: content)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def from_metanorma(root)
|
|
53
|
+
document = call(root)
|
|
54
|
+
@id_strategy.finalize!(document)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rewrite(mirror_node)
|
|
58
|
+
Rewriter.new.call(mirror_node)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_blocks(element)
|
|
62
|
+
content = []
|
|
63
|
+
|
|
64
|
+
element.each_mixed_content do |node|
|
|
65
|
+
next if node.is_a?(String)
|
|
66
|
+
|
|
67
|
+
result = @registry.handle(node, context: self)
|
|
68
|
+
result.append_to(content)
|
|
69
|
+
end
|
|
70
|
+
content
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extract_section_children(element)
|
|
74
|
+
content = []
|
|
75
|
+
%i[clause terms definitions references floating_title].each do |attr|
|
|
76
|
+
collection = SafeAttr.read(element, attr)
|
|
77
|
+
next unless collection
|
|
78
|
+
|
|
79
|
+
collection.each do |child|
|
|
80
|
+
result = @registry.handle(child, context: self)
|
|
81
|
+
result.append_to(content)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
content
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_named_collections(element, collection_attrs)
|
|
88
|
+
content = []
|
|
89
|
+
collection_attrs.each do |attr|
|
|
90
|
+
collection = SafeAttr.read(element, attr)
|
|
91
|
+
next unless collection
|
|
92
|
+
|
|
93
|
+
collection.each do |child|
|
|
94
|
+
result = @registry.handle(child, context: self)
|
|
95
|
+
result.append_to(content)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
content
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def text_node(text, marks: [])
|
|
102
|
+
Handlers.build_text(text, marks: marks)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extract_root_title(root)
|
|
106
|
+
Metadata.title_from_bibdata(SafeAttr.read(root, :bibdata))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
# The Mirror format: a lossless JSON projection of a Metanorma
|
|
7
|
+
# document as a typed node tree (blocks, inlines, marks), for
|
|
8
|
+
# renderers and tooling (the SMART document reader, the HTML JS
|
|
9
|
+
# renderer, MKO payload provenance).
|
|
10
|
+
#
|
|
11
|
+
# This gem is format + mechanism only: node models, the handler
|
|
12
|
+
# registry, the transformer walk, serializers, and the output
|
|
13
|
+
# pipeline. Model knowledge (which document classes map to which
|
|
14
|
+
# handlers, marks, renderers, id categories) is REGISTERED by the
|
|
15
|
+
# model gems — metanorma-document and metanorma-standoc seed the
|
|
16
|
+
# defaults at load time via the registration seams below.
|
|
17
|
+
module Mirror
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
autoload :VERSION, "#{__dir__}/mirror/version"
|
|
21
|
+
autoload :SafeAttr, "#{__dir__}/mirror/safe_attr"
|
|
22
|
+
autoload :MathUtil, "#{__dir__}/mirror/math_util"
|
|
23
|
+
autoload :Metadata, "#{__dir__}/mirror/metadata"
|
|
24
|
+
autoload :Model, "#{__dir__}/mirror/model"
|
|
25
|
+
autoload :ClassTable, "#{__dir__}/mirror/class_table"
|
|
26
|
+
autoload :HandlerResult, "#{__dir__}/mirror/handler_result"
|
|
27
|
+
autoload :Transformer, "#{__dir__}/mirror/transformer"
|
|
28
|
+
autoload :Rewriter, "#{__dir__}/mirror/rewriter"
|
|
29
|
+
autoload :HandlerRegistry, "#{__dir__}/mirror/handler_registry"
|
|
30
|
+
autoload :Handlers, "#{__dir__}/mirror/handlers"
|
|
31
|
+
autoload :IdStrategy, "#{__dir__}/mirror/id_strategy"
|
|
32
|
+
autoload :Output, "#{__dir__}/mirror/output"
|
|
33
|
+
autoload :Serialization, "#{__dir__}/mirror/serialization"
|
|
34
|
+
autoload :DefaultRegistry, "#{__dir__}/mirror/default_registry"
|
|
35
|
+
|
|
36
|
+
DEFAULT_ID_STRATEGY = IdStrategy::Preserve.new
|
|
37
|
+
|
|
38
|
+
# Registration seams. Model gems call these at load time so the
|
|
39
|
+
# format layer stays free of model constants:
|
|
40
|
+
|
|
41
|
+
# Handler entries for the default registry, in registration order.
|
|
42
|
+
@default_hooks = []
|
|
43
|
+
|
|
44
|
+
def self.register_default(&block)
|
|
45
|
+
@default_hooks << block
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.default_hooks
|
|
49
|
+
@default_hooks
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Inline element classes -> mark builders (e.g. EmRawElement ->
|
|
53
|
+
# emphasis mark).
|
|
54
|
+
def self.mark_builders
|
|
55
|
+
@mark_builders ||= ClassTable.new
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Inline element classes -> rich-HTML renderer lambdas (stem, xref,
|
|
59
|
+
# link, eref, fn, span, br).
|
|
60
|
+
def self.rich_html_renderers
|
|
61
|
+
@rich_html_renderers ||= ClassTable.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Inline element classes -> plain-text substitutions (TabElement ->
|
|
65
|
+
# " ", BrElement -> "\n").
|
|
66
|
+
def self.inline_text_substitutions
|
|
67
|
+
@inline_text_substitutions ||= ClassTable.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Inline element classes -> SIMPLE_WRAPS mark types (see
|
|
71
|
+
# Handlers::Inline::Catalog).
|
|
72
|
+
def self.simple_inline_elements
|
|
73
|
+
@simple_inline_elements ||= ClassTable.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Element classes recognized as semx wrappers (cross-reference
|
|
77
|
+
# carriers); payload is always true.
|
|
78
|
+
def self.semx_elements
|
|
79
|
+
@semx_elements ||= ClassTable.new
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The mixed-content walker for inline containers: an object
|
|
83
|
+
# responding to each(element) { |node| } yielding semantic children
|
|
84
|
+
# in document order (document seeds register
|
|
85
|
+
# Components::Inline::SemanticContent).
|
|
86
|
+
class << self
|
|
87
|
+
attr_accessor :inline_content_iterator
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.default_registry
|
|
91
|
+
@default_registry ||= build_default_registry
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.build_default_registry
|
|
95
|
+
DefaultRegistry.build
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|