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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5461b164add6bdfedaeef8996c381ce470a4be96d23d289282ede46e46be49f2
|
|
4
|
+
data.tar.gz: 70fa254f365a47204598f6905e19d9a304690c911eb914767cfbbb3e163a4b2e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 259835bf04a8dca813ad3f9d15fedd088a9493fc0897ffde4ccb66e33ee9bae4c76b2d3fd84cf4e945ffc7aa4d59b8b5f2d65c35ee4704447ce74864fe75eba1
|
|
7
|
+
data.tar.gz: 42b0b8fe0f6678a2f3258cba62ccd8e65e8098a5911390612a1a11fda8a10a6db3207d3fd18335b2191d03e555a7b472840a83eb8e483b1574111418127182f8
|
data/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Ribose Inc.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
17
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
18
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
20
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
21
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
22
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
23
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
24
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
25
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.adoc
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
= metanorma-mirror
|
|
2
|
+
|
|
3
|
+
The Mirror format: a lossless JSON projection of a Metanorma document
|
|
4
|
+
as a typed node tree — blocks, inline runs, marks — preserving document
|
|
5
|
+
order, attributes, and provenance. It is the wire contract consumed by
|
|
6
|
+
the SMART document reader and the HTML JS renderer, and it provides the
|
|
7
|
+
`mirror` provenance object embedded in MKO bundles.
|
|
8
|
+
|
|
9
|
+
== What lives here
|
|
10
|
+
|
|
11
|
+
* Node models (`Metanorma::Mirror::Model`) — lutaml-model Serializables
|
|
12
|
+
with key_value mappings; the JSON wire is the contract.
|
|
13
|
+
* The transformer walk, handler registry, and rewriter.
|
|
14
|
+
* Serializers (JSON, YAML) and the output pipeline.
|
|
15
|
+
|
|
16
|
+
== What does NOT live here
|
|
17
|
+
|
|
18
|
+
Model knowledge. Which document classes map to which handlers, mark
|
|
19
|
+
types, rich-HTML renderers, and id categories is *registered* by the
|
|
20
|
+
model gems: metanorma-document and metanorma-standoc seed the defaults
|
|
21
|
+
through the registration seams (`Mirror.register_default`,
|
|
22
|
+
`Mirror.mark_builders`, `Mirror.rich_html_renderers`,
|
|
23
|
+
`Mirror.simple_inline_elements`, `Mirror.inline_text_substitutions`,
|
|
24
|
+
`Mirror.semx_elements`, `Mirror.inline_content_iterator`,
|
|
25
|
+
`Mirror::IdStrategy::Positional.register_category`). A new flavor or
|
|
26
|
+
model registers; this gem never changes (OCP).
|
|
27
|
+
|
|
28
|
+
== Usage
|
|
29
|
+
|
|
30
|
+
[source,ruby]
|
|
31
|
+
----
|
|
32
|
+
require "metanorma/mirror"
|
|
33
|
+
require "metanorma/document" # seeds the default registrations
|
|
34
|
+
|
|
35
|
+
tree = Metanorma::Mirror::Transformer.new.call(model)
|
|
36
|
+
json = Metanorma::Mirror::Serialization::JsonSerializer.serialize(tree)
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
== License
|
|
40
|
+
|
|
41
|
+
BSD-2-Clause.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
# A class-keyed lookup with ancestor fallback, in registration
|
|
6
|
+
# order. Consumers register their own model classes to payloads
|
|
7
|
+
# (lambdas, strings, prefixes); the table — and therefore this gem —
|
|
8
|
+
# carries no model knowledge. The metanorma-document and
|
|
9
|
+
# metanorma-standoc seeds register their classes at load time.
|
|
10
|
+
class ClassTable
|
|
11
|
+
def initialize
|
|
12
|
+
@entries = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register(klass, payload)
|
|
16
|
+
@entries[klass] = payload
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def lookup(instance)
|
|
21
|
+
entry_for(instance.class) || ancestor_entry(instance.class)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def registered?(klass)
|
|
25
|
+
@entries.key?(klass)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def empty?
|
|
29
|
+
@entries.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def entry_for(klass)
|
|
35
|
+
@entries[klass]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ancestor_entry(klass)
|
|
39
|
+
klass.ancestors.each do |ancestor|
|
|
40
|
+
next if ancestor == klass
|
|
41
|
+
break if ancestor == Object
|
|
42
|
+
|
|
43
|
+
entry = @entries[ancestor]
|
|
44
|
+
return entry if entry
|
|
45
|
+
end
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
# Builds the default handler registry from the registration hooks.
|
|
6
|
+
# Model gems (metanorma-document, metanorma-standoc, flavors) add
|
|
7
|
+
# their class-to-handler entries via Mirror.register_default blocks,
|
|
8
|
+
# run in registration order. The format layer carries no model
|
|
9
|
+
# knowledge (OCP: new model classes register, this file never
|
|
10
|
+
# changes).
|
|
11
|
+
module DefaultRegistry
|
|
12
|
+
class << self
|
|
13
|
+
def build
|
|
14
|
+
registry = HandlerRegistry.new
|
|
15
|
+
Mirror.default_hooks.each do |hook|
|
|
16
|
+
hook.call(registry)
|
|
17
|
+
end
|
|
18
|
+
registry
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
class HandlerRegistry
|
|
6
|
+
Entry = Struct.new(:callable, :concat, :extra_kwargs, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@handlers = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register(model_class, handler, method_name: :call, concat: false,
|
|
13
|
+
extra_kwargs: {})
|
|
14
|
+
callable = resolve_callable(handler, method_name)
|
|
15
|
+
@handlers[model_class] = Entry.new(
|
|
16
|
+
callable: callable,
|
|
17
|
+
concat: concat,
|
|
18
|
+
extra_kwargs: extra_kwargs,
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def registered?(model_class)
|
|
23
|
+
@handlers.key?(model_class)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entry_for(model_element)
|
|
27
|
+
@handlers[model_element.class] || ancestor_entry(model_element)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def handle(model_element, context:)
|
|
31
|
+
entry = entry_for(model_element)
|
|
32
|
+
return HandlerResult.none unless entry
|
|
33
|
+
|
|
34
|
+
kwargs = { context: context }.merge(entry.extra_kwargs || {})
|
|
35
|
+
result = entry.callable.call(model_element, **kwargs)
|
|
36
|
+
HandlerResult.new(result, concat: entry.concat)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def resolve_callable(handler, method_name)
|
|
42
|
+
return handler if handler.is_a?(Proc)
|
|
43
|
+
return handler.method(:call) if method_name == :call
|
|
44
|
+
|
|
45
|
+
handler.method(method_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ancestor_entry(model_element)
|
|
49
|
+
model_element.class.ancestors.each do |ancestor|
|
|
50
|
+
next if ancestor == model_element.class
|
|
51
|
+
break if ancestor == Object
|
|
52
|
+
|
|
53
|
+
entry = @handlers[ancestor]
|
|
54
|
+
return entry if entry
|
|
55
|
+
end
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
class HandlerResult
|
|
6
|
+
attr_reader :nodes
|
|
7
|
+
|
|
8
|
+
def self.none
|
|
9
|
+
new(nil, concat: false)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(nodes, concat: false)
|
|
13
|
+
@nodes = nodes
|
|
14
|
+
@concat = concat
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def none?
|
|
18
|
+
@nodes.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def concat?
|
|
22
|
+
@concat
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def append_to(content)
|
|
26
|
+
return content if none?
|
|
27
|
+
|
|
28
|
+
if concat?
|
|
29
|
+
content.concat(Array(@nodes))
|
|
30
|
+
else
|
|
31
|
+
content << @nodes
|
|
32
|
+
end
|
|
33
|
+
content
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Handlers
|
|
6
|
+
module Admonition
|
|
7
|
+
EXTRA = { type: nil, target: nil, unnumbered: nil }.freeze
|
|
8
|
+
|
|
9
|
+
def self.call(element, context:)
|
|
10
|
+
attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
|
|
11
|
+
content = context.extract_blocks(element)
|
|
12
|
+
|
|
13
|
+
Handlers.build_node("admonition", attrs: attrs, content: content)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Handlers
|
|
6
|
+
module Example
|
|
7
|
+
EXTRA = { unnumbered: nil, subsequence: nil }.freeze
|
|
8
|
+
|
|
9
|
+
def self.call(element, context:)
|
|
10
|
+
attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
|
|
11
|
+
content = context.extract_named_collections(element,
|
|
12
|
+
%i[paragraphs formula
|
|
13
|
+
ul ol quote sourcecode
|
|
14
|
+
table figure dl])
|
|
15
|
+
|
|
16
|
+
Handlers.build_node("example", attrs: attrs, content: content)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Handlers
|
|
6
|
+
module Figure
|
|
7
|
+
def self.call(element, context:)
|
|
8
|
+
attrs = figure_attrs(element)
|
|
9
|
+
content = []
|
|
10
|
+
|
|
11
|
+
img = SafeAttr.read(element, :image)
|
|
12
|
+
if img
|
|
13
|
+
img_attrs = {}
|
|
14
|
+
src = SafeAttr.read(img, :source)
|
|
15
|
+
img_attrs[:src] =
|
|
16
|
+
src && !src.strip.empty? ? src : SafeAttr.read(img, :filename)
|
|
17
|
+
img_attrs[:alt] = SafeAttr.read(img, :alt)
|
|
18
|
+
img_attrs[:height] = SafeAttr.read(img, :height)
|
|
19
|
+
img_attrs[:width] = SafeAttr.read(img, :width)
|
|
20
|
+
content << Handlers.build_node("image", attrs: img_attrs.compact)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subfigures = SafeAttr.read(element, :figure)
|
|
24
|
+
subfigures&.each do |sub|
|
|
25
|
+
result = context.registry.handle(sub, context: context)
|
|
26
|
+
result.append_to(content)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
notes = SafeAttr.read(element, :note)
|
|
30
|
+
notes&.each do |n|
|
|
31
|
+
result = context.registry.handle(n, context: context)
|
|
32
|
+
result.append_to(content)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Handlers.build_node("figure", attrs: attrs, content: content)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.figure_attrs(element)
|
|
39
|
+
attrs = {}
|
|
40
|
+
attrs[:id] = SafeAttr.read(element, :id)
|
|
41
|
+
attrs[:unnumbered] = SafeAttr.read(element, :unnumbered)
|
|
42
|
+
attrs[:width] = SafeAttr.read(element, :width)
|
|
43
|
+
attrs[:align] = SafeAttr.read(element, :align)
|
|
44
|
+
attrs[:semx_id] = SafeAttr.read(element, :semx_id)
|
|
45
|
+
|
|
46
|
+
name = SafeAttr.read(element, :name)
|
|
47
|
+
attrs[:title] = Handlers.extract_name_text(name) if name
|
|
48
|
+
attrs.compact
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Handlers
|
|
6
|
+
module Formula
|
|
7
|
+
EXTRA = { unnumbered: nil, inequality: nil }.freeze
|
|
8
|
+
|
|
9
|
+
def self.call(element, context:)
|
|
10
|
+
attrs = Handlers.extract_attrs(element, extra_attrs: EXTRA)
|
|
11
|
+
|
|
12
|
+
stem = SafeAttr.read(element, :stem)
|
|
13
|
+
if stem
|
|
14
|
+
attrs[:stem_type] = SafeAttr.read(stem, :stem_type)
|
|
15
|
+
|
|
16
|
+
asciimath = SafeAttr.read(stem, :asciimath)
|
|
17
|
+
if asciimath&.value && !asciimath.value.strip.empty?
|
|
18
|
+
attrs[:asciimath] =
|
|
19
|
+
asciimath.value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
math = SafeAttr.read(stem, :math)
|
|
23
|
+
attrs[:mathml] = MathUtil.mathml_from_math(math) if math
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Legacy fallback: some older documents store text directly
|
|
27
|
+
text = SafeAttr.read(element, :text)
|
|
28
|
+
attrs[:math_text] = Array(text).join if text && !attrs[:asciimath]
|
|
29
|
+
|
|
30
|
+
Handlers.build_node("formula", attrs: attrs.compact)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Handlers
|
|
6
|
+
# Image map: a wrapped figure plus hyperlinked areas (shape,
|
|
7
|
+
# link, geometry). Duck-typed over SafeAttr reads.
|
|
8
|
+
module Imagemap
|
|
9
|
+
def self.call(element, context:)
|
|
10
|
+
attrs = {
|
|
11
|
+
id: SafeAttr.read(element, :id),
|
|
12
|
+
semx_id: SafeAttr.read(element, :semx_id),
|
|
13
|
+
areas: area_list(element),
|
|
14
|
+
}.compact
|
|
15
|
+
content = Svgmap.figure_content(element, context)
|
|
16
|
+
Handlers.build_node("imagemap", attrs: attrs, content: content)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.area_list(element)
|
|
20
|
+
areas = Array(SafeAttr.read(element, :area)).map { |a| area_attrs(a) }
|
|
21
|
+
areas.empty? ? nil : areas
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.area_attrs(area)
|
|
25
|
+
ref = Svgmap.reference_of(area)
|
|
26
|
+
radius = SafeAttr.read(area, :radius)
|
|
27
|
+
{
|
|
28
|
+
type: SafeAttr.read(area, :area_type) || SafeAttr.read(area, :type),
|
|
29
|
+
target: ref,
|
|
30
|
+
coords: Array(SafeAttr.read(area, :coords)).map do |c|
|
|
31
|
+
[SafeAttr.read(c, :x), SafeAttr.read(c, :y)]
|
|
32
|
+
end,
|
|
33
|
+
radius: radius && radius_attrs(radius),
|
|
34
|
+
}.compact
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.radius_attrs(radius)
|
|
38
|
+
[SafeAttr.read(radius, :x), SafeAttr.read(radius, :y)]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Metanorma
|
|
4
|
+
module Mirror
|
|
5
|
+
module Handlers
|
|
6
|
+
module Inline
|
|
7
|
+
# Shared declarative table of inline marks whose HTML rendering is
|
|
8
|
+
# a fixed tag with optional static attrs. It is consumed by
|
|
9
|
+
# RichHtmlRenderer, which renders Metanorma XML elements to HTML
|
|
10
|
+
# directly (the fallback / attribute-value path) during the
|
|
11
|
+
# forward transform into the mirror IR.
|
|
12
|
+
#
|
|
13
|
+
# Marks whose attrs vary per instance (link, xref, eref, span)
|
|
14
|
+
# carry data in mark.attrs and are not in this table; each
|
|
15
|
+
# consumer keeps custom handlers for those.
|
|
16
|
+
module Catalog
|
|
17
|
+
SIMPLE_WRAPS = {
|
|
18
|
+
"emphasis" => { tag: :em },
|
|
19
|
+
"strong" => { tag: :strong },
|
|
20
|
+
"subscript" => { tag: :sub },
|
|
21
|
+
"superscript" => { tag: :sup },
|
|
22
|
+
"code" => { tag: :code },
|
|
23
|
+
"underline" => { tag: :u },
|
|
24
|
+
"strike" => { tag: :s },
|
|
25
|
+
"smallcap" => { tag: :span, style: "font-variant: small-caps" },
|
|
26
|
+
"concept" => { tag: :span, class: "concept" },
|
|
27
|
+
"bcp14" => { tag: :span, class: "bcp14" },
|
|
28
|
+
"footnote" => { tag: :sup, class: "footnote-inline" },
|
|
29
|
+
"stem" => { tag: :span, class: "stem" },
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Metanorma
|
|
6
|
+
module Mirror
|
|
7
|
+
module Handlers
|
|
8
|
+
module Inline
|
|
9
|
+
# Renders inline Metanorma elements as HTML strings. Used when an HTML
|
|
10
|
+
# representation of inline content is required (e.g., attribute values,
|
|
11
|
+
# fallback rendering). All HTML construction goes through
|
|
12
|
+
# Nokogiri::HTML5::Builder to guarantee well-formed, escaped output.
|
|
13
|
+
#
|
|
14
|
+
# Simple-wrap elements (em, strong, sub, sup, code, u, s, smallcap,
|
|
15
|
+
# bcp14, concept) take their tag-and-attrs mapping from the shared
|
|
16
|
+
# Inline::Catalog table.
|
|
17
|
+
module RichHtmlRenderer
|
|
18
|
+
# Complex element handlers — these have per-instance attrs or
|
|
19
|
+
# multi-step rendering and bypass the shared Catalog.
|
|
20
|
+
|
|
21
|
+
def self.extract(element)
|
|
22
|
+
return "" unless element
|
|
23
|
+
|
|
24
|
+
unless element.is_a?(Lutaml::Model::Serializable)
|
|
25
|
+
return CGI.escapeHTML(element.to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parts = []
|
|
29
|
+
element.each_mixed_content { |node| parts << render_node(node) }
|
|
30
|
+
result = parts.join.strip
|
|
31
|
+
result.empty? ? TextExtractor.extract_element_text(element) : result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.render_node(node)
|
|
35
|
+
case node
|
|
36
|
+
when String
|
|
37
|
+
CGI.escapeHTML(node)
|
|
38
|
+
when Lutaml::Model::Serializable
|
|
39
|
+
render_element(node)
|
|
40
|
+
else
|
|
41
|
+
""
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.render_element(element)
|
|
46
|
+
complex = Mirror.rich_html_renderers.lookup(element)
|
|
47
|
+
return complex.call(element) if complex
|
|
48
|
+
|
|
49
|
+
mark_type = Mirror.simple_inline_elements.lookup(element)
|
|
50
|
+
return render_simple(mark_type, element) if mark_type
|
|
51
|
+
|
|
52
|
+
extract(element)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.render_simple(mark_type, element)
|
|
56
|
+
spec = Catalog::SIMPLE_WRAPS[mark_type]
|
|
57
|
+
attrs = spec.except(:tag)
|
|
58
|
+
wrap do |d|
|
|
59
|
+
if attrs.empty?
|
|
60
|
+
d.public_send(spec[:tag]) { d << raw(extract(element)) }
|
|
61
|
+
else
|
|
62
|
+
d.public_send(spec[:tag], attrs) { d << raw(extract(element)) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.for_stem(element)
|
|
68
|
+
math = SafeAttr.read(element, :math)
|
|
69
|
+
return TextExtractor.extract_element_text(element) unless math
|
|
70
|
+
|
|
71
|
+
mathml = MathUtil.mathml_from_math(math)
|
|
72
|
+
.sub(/\s*xmlns(:\w+)?="[^"]*"\s*/, " ")
|
|
73
|
+
.gsub(/\s+/, " ")
|
|
74
|
+
.strip
|
|
75
|
+
spec = Catalog::SIMPLE_WRAPS["stem"]
|
|
76
|
+
wrap do |d|
|
|
77
|
+
d.public_send(spec[:tag], spec.except(:tag)) { d << raw(mathml) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.for_xref(element)
|
|
82
|
+
target = SafeAttr.read(element, :target) || ""
|
|
83
|
+
label = TextExtractor.extract_formatted_text(element)
|
|
84
|
+
label = target if label.strip.empty?
|
|
85
|
+
wrap { |d| d.a(href: "##{target}") { d.text label } }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.for_link(element)
|
|
89
|
+
href = SafeAttr.read(element,
|
|
90
|
+
:target) || SafeAttr.read(element, :href) || ""
|
|
91
|
+
label = TextExtractor.extract_formatted_text(element)
|
|
92
|
+
wrap { |d| d.a(href: href) { d.text label } }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.for_eref(element)
|
|
96
|
+
citeas = SafeAttr.read(element, :citeas) || ""
|
|
97
|
+
label = TextExtractor.extract_formatted_text(element)
|
|
98
|
+
wrap { |d| d.a(class: "eref", cite: citeas) { d.text label } }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.for_fn(element)
|
|
102
|
+
reference = SafeAttr.read(element, :reference) || ""
|
|
103
|
+
spec = Catalog::SIMPLE_WRAPS["footnote"]
|
|
104
|
+
wrap do |d|
|
|
105
|
+
d.public_send(spec[:tag], spec.except(:tag)) { d.text reference }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.for_span(element)
|
|
110
|
+
cls = SafeAttr.read(element, :class_attr)
|
|
111
|
+
inner = extract(element)
|
|
112
|
+
wrap do |d|
|
|
113
|
+
if cls
|
|
114
|
+
d.span(class: cls) { d << raw(inner) }
|
|
115
|
+
else
|
|
116
|
+
d.span { d << raw(inner) }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.wrap(&)
|
|
122
|
+
Nokogiri::HTML5::Builder.new(&).doc.root.to_html
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.raw(html_string)
|
|
126
|
+
Nokogiri::HTML5::DocumentFragment.parse(html_string.to_s)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|