coradoc-mirror 0.1.1

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/lib/coradoc/mirror/core_model_to_mirror.rb +181 -0
  3. data/lib/coradoc/mirror/handler_registry.rb +105 -0
  4. data/lib/coradoc/mirror/handlers/admonition.rb +29 -0
  5. data/lib/coradoc/mirror/handlers/bibliography.rb +43 -0
  6. data/lib/coradoc/mirror/handlers/blockquote.rb +19 -0
  7. data/lib/coradoc/mirror/handlers/code_block.rb +69 -0
  8. data/lib/coradoc/mirror/handlers/comment.rb +14 -0
  9. data/lib/coradoc/mirror/handlers/definition_list.rb +69 -0
  10. data/lib/coradoc/mirror/handlers/example.rb +19 -0
  11. data/lib/coradoc/mirror/handlers/footnote.rb +18 -0
  12. data/lib/coradoc/mirror/handlers/frontmatter.rb +71 -0
  13. data/lib/coradoc/mirror/handlers/generic_block.rb +24 -0
  14. data/lib/coradoc/mirror/handlers/horizontal_rule.rb +14 -0
  15. data/lib/coradoc/mirror/handlers/image.rb +58 -0
  16. data/lib/coradoc/mirror/handlers/inline.rb +213 -0
  17. data/lib/coradoc/mirror/handlers/list.rb +80 -0
  18. data/lib/coradoc/mirror/handlers/open_block.rb +16 -0
  19. data/lib/coradoc/mirror/handlers/paragraph.rb +16 -0
  20. data/lib/coradoc/mirror/handlers/reviewer.rb +14 -0
  21. data/lib/coradoc/mirror/handlers/sidebar.rb +19 -0
  22. data/lib/coradoc/mirror/handlers/structural.rb +84 -0
  23. data/lib/coradoc/mirror/handlers/table.rb +82 -0
  24. data/lib/coradoc/mirror/handlers/toc.rb +48 -0
  25. data/lib/coradoc/mirror/handlers/verse.rb +22 -0
  26. data/lib/coradoc/mirror/handlers.rb +38 -0
  27. data/lib/coradoc/mirror/mark.rb +181 -0
  28. data/lib/coradoc/mirror/mark_reverse_builder.rb +142 -0
  29. data/lib/coradoc/mirror/mirror_json_format.rb +42 -0
  30. data/lib/coradoc/mirror/mirror_to_core_model.rb +73 -0
  31. data/lib/coradoc/mirror/mirror_yaml_format.rb +41 -0
  32. data/lib/coradoc/mirror/node.rb +856 -0
  33. data/lib/coradoc/mirror/output.rb +62 -0
  34. data/lib/coradoc/mirror/partitioner.rb +62 -0
  35. data/lib/coradoc/mirror/reverse_builder.rb +600 -0
  36. data/lib/coradoc/mirror/transformer.rb +41 -0
  37. data/lib/coradoc/mirror/version.rb +7 -0
  38. data/lib/coradoc/mirror.rb +161 -0
  39. data/lib/coradoc-mirror.rb +14 -0
  40. metadata +140 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ # Handler modules for transforming CoreModel types to Mirror nodes.
6
+ #
7
+ # Each handler is a module/class that responds to +call(element, context:)+,
8
+ # where +element+ is a CoreModel instance and +context+ is the
9
+ # CoreModelToMirror transformer providing shared helpers.
10
+ #
11
+ # New handlers are added by creating a new module file and registering
12
+ # it in +Coradoc::Mirror.default_registry+ — no existing code changes (OCP).
13
+ module Handlers
14
+ autoload :Structural, "#{__dir__}/handlers/structural"
15
+ autoload :Paragraph, "#{__dir__}/handlers/paragraph"
16
+ autoload :CodeBlock, "#{__dir__}/handlers/code_block"
17
+ autoload :Blockquote, "#{__dir__}/handlers/blockquote"
18
+ autoload :Example, "#{__dir__}/handlers/example"
19
+ autoload :Sidebar, "#{__dir__}/handlers/sidebar"
20
+ autoload :OpenBlock, "#{__dir__}/handlers/open_block"
21
+ autoload :Verse, "#{__dir__}/handlers/verse"
22
+ autoload :Comment, "#{__dir__}/handlers/comment"
23
+ autoload :HorizontalRule, "#{__dir__}/handlers/horizontal_rule"
24
+ autoload :Reviewer, "#{__dir__}/handlers/reviewer"
25
+ autoload :Admonition, "#{__dir__}/handlers/admonition"
26
+ autoload :List, "#{__dir__}/handlers/list"
27
+ autoload :DefinitionList, "#{__dir__}/handlers/definition_list"
28
+ autoload :Table, "#{__dir__}/handlers/table"
29
+ autoload :Image, "#{__dir__}/handlers/image"
30
+ autoload :Inline, "#{__dir__}/handlers/inline"
31
+ autoload :Bibliography, "#{__dir__}/handlers/bibliography"
32
+ autoload :Footnote, "#{__dir__}/handlers/footnote"
33
+ autoload :Toc, "#{__dir__}/handlers/toc"
34
+ autoload :Frontmatter, "#{__dir__}/handlers/frontmatter"
35
+ autoload :GenericBlock, "#{__dir__}/handlers/generic_block"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ # ProseMirror-compatible inline mark (formatting annotation).
8
+ #
9
+ # Marks decorate inline text nodes with formatting semantics like
10
+ # bold, italic, link, etc. Wire format:
11
+ #
12
+ # { "type": "strong" }
13
+ # { "type": "link", "attrs": { "href": "..." } }
14
+ #
15
+ # All built-in Mark subclasses live below in this file so the
16
+ # TYPE_TO_CLASS registry at the bottom can see every PM_TYPE at
17
+ # load time. Adding a new mark type = adding one subclass + letting
18
+ # the registry walker pick it up (OCP).
19
+ class Mark < Lutaml::Model::Serializable
20
+ PM_TYPE = 'mark'
21
+
22
+ attribute :type, :string, default: -> { self.class::PM_TYPE }
23
+
24
+ key_value do
25
+ map 'type', to: :type, render_default: true
26
+ end
27
+
28
+ def text_content
29
+ ''
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ module Coradoc
36
+ module Mirror
37
+ class Mark
38
+ # ── Marks without attrs ──
39
+
40
+ class Bold < Mark
41
+ PM_TYPE = 'strong'
42
+ end
43
+
44
+ class Italic < Mark
45
+ PM_TYPE = 'emphasis'
46
+ end
47
+
48
+ class Monospace < Mark
49
+ PM_TYPE = 'code'
50
+ end
51
+
52
+ class Underline < Mark
53
+ PM_TYPE = 'underline'
54
+ end
55
+
56
+ class Strikethrough < Mark
57
+ PM_TYPE = 'strike'
58
+ end
59
+
60
+ class Subscript < Mark
61
+ PM_TYPE = 'subscript'
62
+ end
63
+
64
+ class Superscript < Mark
65
+ PM_TYPE = 'superscript'
66
+ end
67
+
68
+ class Highlight < Mark
69
+ PM_TYPE = 'highlight'
70
+ end
71
+
72
+ # ── Marks with attrs ──
73
+
74
+ class Link < Mark
75
+ PM_TYPE = 'link'
76
+
77
+ class Attrs < Lutaml::Model::Serializable
78
+ attribute :href, :string
79
+
80
+ key_value do
81
+ map 'href', to: :href
82
+ end
83
+ end
84
+
85
+ attribute :attrs, Attrs
86
+
87
+ key_value do
88
+ map 'type', to: :type, render_default: true
89
+ map 'attrs', to: :attrs
90
+ end
91
+ end
92
+
93
+ class CrossReference < Mark
94
+ PM_TYPE = 'xref'
95
+
96
+ class Attrs < Lutaml::Model::Serializable
97
+ attribute :target, :string
98
+ attribute :resolved, :string
99
+
100
+ key_value do
101
+ map 'target', to: :target
102
+ map 'resolved', to: :resolved
103
+ end
104
+ end
105
+
106
+ attribute :attrs, Attrs
107
+
108
+ key_value do
109
+ map 'type', to: :type, render_default: true
110
+ map 'attrs', to: :attrs
111
+ end
112
+ end
113
+
114
+ class Stem < Mark
115
+ PM_TYPE = 'stem'
116
+
117
+ class Attrs < Lutaml::Model::Serializable
118
+ attribute :stem_type, :string
119
+
120
+ key_value do
121
+ map 'stem_type', to: :stem_type
122
+ end
123
+ end
124
+
125
+ attribute :attrs, Attrs
126
+
127
+ key_value do
128
+ map 'type', to: :type, render_default: true
129
+ map 'attrs', to: :attrs
130
+ end
131
+ end
132
+
133
+ class Span < Mark
134
+ PM_TYPE = 'span'
135
+
136
+ class Attrs < Lutaml::Model::Serializable
137
+ attribute :role, :string
138
+
139
+ key_value do
140
+ map 'role', to: :role
141
+ end
142
+ end
143
+
144
+ attribute :attrs, Attrs
145
+
146
+ key_value do
147
+ map 'type', to: :type, render_default: true
148
+ map 'attrs', to: :attrs
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ module Coradoc
156
+ module Mirror
157
+ class Mark
158
+ # Polymorphic class map — flat hash from PM_TYPE wire string to
159
+ # fully-qualified Ruby class name. Used by Node and Mark mappings
160
+ # to dispatch polymorphic deserialization. Populated once after
161
+ # all subclasses are defined above.
162
+ TYPE_TO_CLASS = begin
163
+ result = {}
164
+ Mark.constants.each do |name|
165
+ k = Mark.const_get(name)
166
+ next unless k.is_a?(Class) && k < Mark && k::PM_TYPE != 'mark'
167
+
168
+ result[k::PM_TYPE] = k.name
169
+ end
170
+ result.freeze
171
+ end
172
+
173
+ # Frozen polymorphic option block. Referenced verbatim by every
174
+ # Node mapping that has a `marks` collection.
175
+ POLYMORPHIC = {
176
+ attribute: 'type',
177
+ class_map: TYPE_TO_CLASS
178
+ }.freeze
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ # OCP-compliant registry for Mirror mark -> CoreModel transformation.
6
+ #
7
+ # This is the mark-level counterpart to ReverseBuilder (which handles
8
+ # node-level dispatch). Adding support for a new mark type is purely
9
+ # additive:
10
+ #
11
+ # module MarkReverseBuilder
12
+ # class Concept < Base
13
+ # registers 'concept'
14
+ #
15
+ # def build(inner, _mark)
16
+ # CoreModel::TermElement.new(children: Array(inner))
17
+ # end
18
+ # end
19
+ # end
20
+ #
21
+ # No edits to MirrorToCoreModel#apply_mark or any other existing class.
22
+ module MarkReverseBuilder
23
+ REGISTRY = {}
24
+
25
+ module_function
26
+
27
+ def register(type, builder_class)
28
+ REGISTRY[type] = builder_class
29
+ end
30
+
31
+ def lookup(type)
32
+ REGISTRY[type]
33
+ end
34
+
35
+ def registered_types
36
+ REGISTRY.keys
37
+ end
38
+
39
+ # Base class for all mark reverse builders. Subclasses register one
40
+ # mark type string via `registers` and implement `#build(inner, mark)`.
41
+ # `inner` is the already-built CoreModel inline content this mark
42
+ # wraps; `mark` is the source Mirror::Mark (for marks that carry
43
+ # attrs, like `link` reading `mark.href`).
44
+ class Base
45
+ def self.registers(*types)
46
+ types.each { |t| MarkReverseBuilder.register(t, self) }
47
+ end
48
+
49
+ def build(_inner, _mark)
50
+ raise NotImplementedError,
51
+ "#{self.class} must implement #build(inner, mark)"
52
+ end
53
+ end
54
+
55
+ # ── Simple wraps: typed InlineElement subclass, no attrs ──
56
+
57
+ class Bold < Base
58
+ registers 'strong'
59
+
60
+ def build(inner, _mark)
61
+ CoreModel::BoldElement.new(children: Array(inner))
62
+ end
63
+ end
64
+
65
+ class Italic < Base
66
+ registers 'emphasis'
67
+
68
+ def build(inner, _mark)
69
+ CoreModel::ItalicElement.new(children: Array(inner))
70
+ end
71
+ end
72
+
73
+ class Monospace < Base
74
+ registers 'code'
75
+
76
+ def build(inner, _mark)
77
+ CoreModel::MonospaceElement.new(children: Array(inner))
78
+ end
79
+ end
80
+
81
+ class Underline < Base
82
+ registers 'underline'
83
+
84
+ def build(inner, _mark)
85
+ CoreModel::UnderlineElement.new(children: Array(inner))
86
+ end
87
+ end
88
+
89
+ class Strikethrough < Base
90
+ registers 'strike'
91
+
92
+ def build(inner, _mark)
93
+ CoreModel::StrikethroughElement.new(children: Array(inner))
94
+ end
95
+ end
96
+
97
+ class Subscript < Base
98
+ registers 'subscript'
99
+
100
+ def build(inner, _mark)
101
+ CoreModel::SubscriptElement.new(children: Array(inner))
102
+ end
103
+ end
104
+
105
+ class Superscript < Base
106
+ registers 'superscript'
107
+
108
+ def build(inner, _mark)
109
+ CoreModel::SuperscriptElement.new(children: Array(inner))
110
+ end
111
+ end
112
+
113
+ class Highlight < Base
114
+ registers 'highlight'
115
+
116
+ def build(inner, _mark)
117
+ CoreModel::HighlightElement.new(children: Array(inner))
118
+ end
119
+ end
120
+
121
+ # ── Marks with attrs ──
122
+
123
+ class Link < Base
124
+ registers 'link'
125
+
126
+ def build(inner, mark)
127
+ CoreModel::LinkElement.new(target: mark.attrs&.href, children: Array(inner))
128
+ end
129
+ end
130
+
131
+ class CrossReference < Base
132
+ registers 'xref'
133
+
134
+ def build(inner, mark)
135
+ CoreModel::CrossReferenceElement.new(
136
+ target: mark.attrs&.target, children: Array(inner)
137
+ )
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ # Format module for mirror JSON output.
8
+ #
9
+ # Registers with Coradoc so the CLI can discover it:
10
+ # Coradoc.convert(text, from: :asciidoc, to: :mirror_json)
11
+ # coradoc convert doc.adoc -t mirror_json
12
+ module MirrorJsonFormat
13
+ class << self
14
+ # Output-only format — parsing from mirror JSON is not supported via
15
+ # the format registry. Use Mirror::Node.from_hash directly.
16
+ def parse_to_core(_input, _options = {})
17
+ raise Coradoc::UnsupportedFormatError,
18
+ 'Parsing from mirror JSON is not supported via the format registry. ' \
19
+ 'Use Coradoc::Mirror::Node.from_hash(JSON.parse(input)) directly.'
20
+ end
21
+
22
+ # Accept CoreModel, serialize to Mirror JSON.
23
+ def serialize(document, options = {})
24
+ pretty = options[:pretty] != false
25
+ node = Coradoc::Mirror.transform(document)
26
+ pretty ? JSON.pretty_generate(node.to_hash) : JSON.generate(node.to_hash)
27
+ end
28
+
29
+ def serialize?
30
+ true
31
+ end
32
+
33
+ def handles_model?(_model)
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ Coradoc.register_format(:mirror_json, Coradoc::Mirror::MirrorJsonFormat,
42
+ extensions: %w[.mirror.json])
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Mirror
5
+ # Transforms ProseMirror-compatible Mirror nodes back into CoreModel.
6
+ #
7
+ # Dispatch is delegated to ReverseBuilder (node-level) and
8
+ # MarkReverseBuilder (mark-level) — adding a new node or mark type is
9
+ # done by registering a new Builder class, with no edit to this file
10
+ # (OCP). ReverseBuilder and MarkReverseBuilder are autoloaded from
11
+ # coradoc/mirror.rb; referencing them here triggers load of the
12
+ # registry files, which is where the built-in builders self-register.
13
+ class MirrorToCoreModel
14
+ def call(mirror_node)
15
+ build_node(mirror_node)
16
+ end
17
+
18
+ def build_node(node)
19
+ builder_class = ReverseBuilder.lookup(node.type)
20
+ raise Error, "Unknown mirror node type: #{node.type}" unless builder_class
21
+
22
+ builder_class.new(self).build(node)
23
+ end
24
+
25
+ # ── Shared helpers (single source of truth — used by every
26
+ # ReverseBuilder::Base subclass via delegation) ──
27
+
28
+ def build_content(node)
29
+ return [] unless node.content
30
+
31
+ node.content.flat_map do |child|
32
+ result = build_node(child)
33
+ next [] if result.nil?
34
+
35
+ result.is_a?(Array) ? result : [result]
36
+ end
37
+ end
38
+
39
+ # Mark dispatch goes through MarkReverseBuilder so adding a new
40
+ # mark type is purely additive (OCP parity with node dispatch).
41
+ # Unknown marks pass `inner` through unchanged.
42
+ def apply_mark(inner, mark)
43
+ builder_class = MarkReverseBuilder.lookup(mark.type)
44
+ return inner unless builder_class
45
+
46
+ builder_class.new.build(inner, mark)
47
+ end
48
+
49
+ def build_inline_children(node)
50
+ return [] unless node.content
51
+
52
+ node.content.filter_map do |child|
53
+ next unless child.is_a?(Node)
54
+
55
+ build_node(child)
56
+ end
57
+ end
58
+
59
+ def extract_text(node)
60
+ return node.text.to_s if node.is_a?(Node::Text)
61
+ return '' unless node.content
62
+
63
+ node.content.filter_map do |child|
64
+ child.is_a?(Node) ? extract_text(child) : ''
65
+ end.join
66
+ end
67
+
68
+ def inline_content(element)
69
+ element.is_a?(CoreModel::InlineElement) ? element.content : element.to_s
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Coradoc
6
+ module Mirror
7
+ # Format module for mirror YAML output.
8
+ #
9
+ # Registers with Coradoc so the CLI can discover it:
10
+ # Coradoc.convert(text, from: :asciidoc, to: :mirror_yaml)
11
+ # coradoc convert doc.adoc -t mirror_yaml
12
+ module MirrorYamlFormat
13
+ class << self
14
+ # Output-only format — parsing from mirror YAML is not supported via
15
+ # the format registry. Use Mirror::Node.from_hash directly.
16
+ def parse_to_core(_input, _options = {})
17
+ raise Coradoc::UnsupportedFormatError,
18
+ 'Parsing from mirror YAML is not supported via the format registry. ' \
19
+ 'Use Coradoc::Mirror::Node.from_hash(YAML.safe_load(input)) directly.'
20
+ end
21
+
22
+ # Accept CoreModel, serialize to Mirror YAML.
23
+ def serialize(document, _options = {})
24
+ node = Coradoc::Mirror.transform(document)
25
+ YAML.dump(node.to_hash)
26
+ end
27
+
28
+ def serialize?
29
+ true
30
+ end
31
+
32
+ def handles_model?(_model)
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ Coradoc.register_format(:mirror_yaml, Coradoc::Mirror::MirrorYamlFormat,
41
+ extensions: %w[.mirror.yaml .mirror.yml])