coradoc-markdown 1.0.0 → 1.0.3

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/markdown/model/admonition.rb +35 -0
  3. data/lib/coradoc/markdown/model/attribute_list.rb +5 -16
  4. data/lib/coradoc/markdown/model/base.rb +2 -34
  5. data/lib/coradoc/markdown/model/comment.rb +14 -0
  6. data/lib/coradoc/markdown/model/cross_reference.rb +10 -0
  7. data/lib/coradoc/markdown/model/definition_list.rb +0 -12
  8. data/lib/coradoc/markdown/model/definition_term.rb +16 -6
  9. data/lib/coradoc/markdown/model/document.rb +9 -0
  10. data/lib/coradoc/markdown/model/example_block.rb +26 -0
  11. data/lib/coradoc/markdown/model/extension.rb +9 -18
  12. data/lib/coradoc/markdown/model/footnote_reference.rb +0 -5
  13. data/lib/coradoc/markdown/model/hard_line_break.rb +21 -0
  14. data/lib/coradoc/markdown/model/highlight.rb +0 -5
  15. data/lib/coradoc/markdown/model/horizontal_rule.rb +1 -6
  16. data/lib/coradoc/markdown/model/list_item.rb +1 -1
  17. data/lib/coradoc/markdown/model/literal.rb +21 -0
  18. data/lib/coradoc/markdown/model/math.rb +0 -14
  19. data/lib/coradoc/markdown/model/named_value.rb +18 -0
  20. data/lib/coradoc/markdown/model/open_block.rb +22 -0
  21. data/lib/coradoc/markdown/model/paragraph.rb +2 -2
  22. data/lib/coradoc/markdown/model/pass.rb +21 -0
  23. data/lib/coradoc/markdown/model/sidebar.rb +22 -0
  24. data/lib/coradoc/markdown/model/strikethrough.rb +0 -5
  25. data/lib/coradoc/markdown/model/subscript.rb +9 -0
  26. data/lib/coradoc/markdown/model/superscript.rb +9 -0
  27. data/lib/coradoc/markdown/model/underline.rb +9 -0
  28. data/lib/coradoc/markdown/model/verse.rb +27 -0
  29. data/lib/coradoc/markdown/parser/block_parser.rb +1 -1
  30. data/lib/coradoc/markdown/parser/frontmatter_parser.rb +23 -0
  31. data/lib/coradoc/markdown/serializer/builder.rb +57 -0
  32. data/lib/coradoc/markdown/serializer/config.rb +77 -0
  33. data/lib/coradoc/markdown/serializer/context.rb +66 -0
  34. data/lib/coradoc/markdown/serializer/element_serializer.rb +46 -0
  35. data/lib/coradoc/markdown/serializer/flavor.rb +82 -0
  36. data/lib/coradoc/markdown/serializer/registrations.rb +111 -0
  37. data/lib/coradoc/markdown/serializer/registry.rb +62 -0
  38. data/lib/coradoc/markdown/serializer/runner.rb +84 -0
  39. data/lib/coradoc/markdown/serializer/serializers/abbreviation.rb +19 -0
  40. data/lib/coradoc/markdown/serializer/serializers/admonition.rb +20 -0
  41. data/lib/coradoc/markdown/serializer/serializers/attribute_list.rb +25 -0
  42. data/lib/coradoc/markdown/serializer/serializers/blockquote.rb +19 -0
  43. data/lib/coradoc/markdown/serializer/serializers/code.rb +19 -0
  44. data/lib/coradoc/markdown/serializer/serializers/code_block.rb +19 -0
  45. data/lib/coradoc/markdown/serializer/serializers/comment.rb +31 -0
  46. data/lib/coradoc/markdown/serializer/serializers/cross_reference.rb +19 -0
  47. data/lib/coradoc/markdown/serializer/serializers/definition_list.rb +20 -0
  48. data/lib/coradoc/markdown/serializer/serializers/document.rb +22 -0
  49. data/lib/coradoc/markdown/serializer/serializers/emphasis.rb +19 -0
  50. data/lib/coradoc/markdown/serializer/serializers/example_block.rb +40 -0
  51. data/lib/coradoc/markdown/serializer/serializers/extension.rb +30 -0
  52. data/lib/coradoc/markdown/serializer/serializers/footnote.rb +20 -0
  53. data/lib/coradoc/markdown/serializer/serializers/footnote_reference.rb +19 -0
  54. data/lib/coradoc/markdown/serializer/serializers/hard_line_break.rb +22 -0
  55. data/lib/coradoc/markdown/serializer/serializers/heading.rb +19 -0
  56. data/lib/coradoc/markdown/serializer/serializers/highlight.rb +19 -0
  57. data/lib/coradoc/markdown/serializer/serializers/horizontal_rule.rb +19 -0
  58. data/lib/coradoc/markdown/serializer/serializers/image.rb +19 -0
  59. data/lib/coradoc/markdown/serializer/serializers/link.rb +29 -0
  60. data/lib/coradoc/markdown/serializer/serializers/list.rb +45 -0
  61. data/lib/coradoc/markdown/serializer/serializers/literal.rb +21 -0
  62. data/lib/coradoc/markdown/serializer/serializers/math.rb +23 -0
  63. data/lib/coradoc/markdown/serializer/serializers/open_block.rb +38 -0
  64. data/lib/coradoc/markdown/serializer/serializers/paragraph.rb +23 -0
  65. data/lib/coradoc/markdown/serializer/serializers/pass.rb +21 -0
  66. data/lib/coradoc/markdown/serializer/serializers/sidebar.rb +20 -0
  67. data/lib/coradoc/markdown/serializer/serializers/strikethrough.rb +19 -0
  68. data/lib/coradoc/markdown/serializer/serializers/strong.rb +19 -0
  69. data/lib/coradoc/markdown/serializer/serializers/subscript.rb +19 -0
  70. data/lib/coradoc/markdown/serializer/serializers/superscript.rb +19 -0
  71. data/lib/coradoc/markdown/serializer/serializers/table.rb +37 -0
  72. data/lib/coradoc/markdown/serializer/serializers/underline.rb +19 -0
  73. data/lib/coradoc/markdown/serializer/serializers/verse.rb +31 -0
  74. data/lib/coradoc/markdown/serializer/strategies/admonition/base.rb +30 -0
  75. data/lib/coradoc/markdown/serializer/strategies/admonition/container.rb +34 -0
  76. data/lib/coradoc/markdown/serializer/strategies/admonition/gfm_alert.rb +28 -0
  77. data/lib/coradoc/markdown/serializer/strategies/admonition/github.rb +29 -0
  78. data/lib/coradoc/markdown/serializer/strategies/admonition/html.rb +25 -0
  79. data/lib/coradoc/markdown/serializer/strategies/admonition/registry.rb +50 -0
  80. data/lib/coradoc/markdown/serializer/strategies/autolink/angle.rb +35 -0
  81. data/lib/coradoc/markdown/serializer/strategies/autolink/bare.rb +23 -0
  82. data/lib/coradoc/markdown/serializer/strategies/autolink/base.rb +33 -0
  83. data/lib/coradoc/markdown/serializer/strategies/autolink/none.rb +27 -0
  84. data/lib/coradoc/markdown/serializer/strategies/autolink/registry.rb +58 -0
  85. data/lib/coradoc/markdown/serializer/strategies/definition_list/base.rb +37 -0
  86. data/lib/coradoc/markdown/serializer/strategies/definition_list/flat.rb +48 -0
  87. data/lib/coradoc/markdown/serializer/strategies/definition_list/nested_html.rb +54 -0
  88. data/lib/coradoc/markdown/serializer/strategies/definition_list/registry.rb +37 -0
  89. data/lib/coradoc/markdown/serializer.rb +30 -181
  90. data/lib/coradoc/markdown/toc_generator.rb +2 -8
  91. data/lib/coradoc/markdown/transform/block_transformer.rb +163 -0
  92. data/lib/coradoc/markdown/transform/from_core_model.rb +205 -40
  93. data/lib/coradoc/markdown/transform/image_transformer.rb +20 -0
  94. data/lib/coradoc/markdown/transform/inline_transformer.rb +74 -0
  95. data/lib/coradoc/markdown/transform/list_transformer.rb +52 -0
  96. data/lib/coradoc/markdown/transform/structural_transformer.rb +94 -0
  97. data/lib/coradoc/markdown/transform/table_transformer.rb +40 -0
  98. data/lib/coradoc/markdown/transform/to_core_model.rb +36 -30
  99. data/lib/coradoc/markdown/transformer.rb +87 -2
  100. data/lib/coradoc/markdown/version.rb +1 -1
  101. data/lib/coradoc/markdown.rb +23 -20
  102. metadata +89 -10
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+ require_relative 'context'
5
+ require_relative 'registry'
6
+ require_relative 'registrations'
7
+ require_relative 'runner'
8
+
9
+ module Coradoc
10
+ module Markdown
11
+ class Serializer
12
+ # Fluent builder for a configured serializer runner.
13
+ #
14
+ # Usage:
15
+ #
16
+ # Serializer.build(:gfm) do |config|
17
+ # config.admonition_style = :container
18
+ # config.suppress_comments = false
19
+ # end.call(element)
20
+ #
21
+ # The block yields the Builder itself; assignments accumulate as
22
+ # overrides and are frozen into a Config when `runner` (or `call`)
23
+ # is invoked.
24
+ class Builder
25
+ attr_reader :flavor, :overrides
26
+
27
+ def initialize(flavor = Flavor::DEFAULT_FLAVOR)
28
+ @flavor = flavor
29
+ @overrides = {}
30
+ end
31
+
32
+ Config::ATTRIBUTES.each do |attr|
33
+ define_method("#{attr}=") do |value|
34
+ @overrides[attr] = value
35
+ end
36
+ end
37
+
38
+ def apply(hash)
39
+ hash.each { |k, v| @overrides[k.to_sym] = v }
40
+ self
41
+ end
42
+
43
+ def config
44
+ @config ||= Config.new(flavor: flavor, **overrides)
45
+ end
46
+
47
+ def runner
48
+ @runner ||= Runner.new(config: config, registry: Registrations.default_registry)
49
+ end
50
+
51
+ def call(element)
52
+ runner.call(element)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'flavor'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ class Serializer
8
+ # Immutable serialization configuration.
9
+ #
10
+ # Created via `Serializer.build` with a flavor and overrides. Once
11
+ # built, the Config is frozen and resolves capability strategies
12
+ # (admonitions, autolinks, comments, etc.) by combining flavor
13
+ # defaults with caller overrides.
14
+ #
15
+ # SSOT: the 5 spec-mandated options live here and nowhere else.
16
+ # Strategy classes read their mode via `config.strategy_for(:capability)`.
17
+ class Config
18
+ ATTRIBUTES = %i[
19
+ markdown_flavor
20
+ admonition_style
21
+ definition_list_nested
22
+ suppress_comments
23
+ autolinks
24
+ ].freeze
25
+
26
+ attr_reader(*ATTRIBUTES)
27
+
28
+ def initialize(flavor: :gfm, **overrides)
29
+ unless Flavor.known?(flavor)
30
+ raise ArgumentError, "Unknown markdown_flavor: #{flavor.inspect}. " \
31
+ "Known: #{Flavor.names.inspect}"
32
+ end
33
+
34
+ resolved = Flavor.resolve(flavor).merge(symbolize(overrides))
35
+ validate_options!(resolved)
36
+
37
+ @markdown_flavor = resolved.fetch(:markdown_flavor)
38
+ @admonition_style = resolved.fetch(:admonition_style)
39
+ @definition_list_nested = resolved.fetch(:definition_list_nested)
40
+ @suppress_comments = resolved.fetch(:suppress_comments)
41
+ @autolinks = resolved.fetch(:autolinks)
42
+
43
+ freeze
44
+ end
45
+
46
+ def to_h
47
+ ATTRIBUTES.to_h { |k| [k, public_send(k)] }
48
+ end
49
+
50
+ def with(overrides)
51
+ self.class.new(**to_h.merge(symbolize(overrides)))
52
+ end
53
+
54
+ private
55
+
56
+ def symbolize(hash)
57
+ hash.to_h { |k, v| [k.to_sym, v] }
58
+ end
59
+
60
+ def validate_options!(resolved)
61
+ unless %i[github container html gfm_alert].include?(resolved.fetch(:admonition_style))
62
+ raise ArgumentError, "Unknown admonition_style: #{resolved[:admonition_style].inspect}"
63
+ end
64
+ unless %i[html flatten].include?(resolved.fetch(:definition_list_nested))
65
+ raise ArgumentError, "Unknown definition_list_nested: #{resolved[:definition_list_nested].inspect}"
66
+ end
67
+ unless [true, false].include?(resolved.fetch(:suppress_comments))
68
+ raise ArgumentError, 'suppress_comments must be boolean'
69
+ end
70
+ return if [true, false].include?(resolved.fetch(:autolinks))
71
+
72
+ raise ArgumentError, 'autolinks must be boolean'
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ class Serializer
6
+ # Per-document mutable state threaded through every serializer call,
7
+ # AND the single dispatch interface element serializers use to
8
+ # recurse into child elements.
9
+ #
10
+ # Threading dispatch through the context (not the runner) means
11
+ # serializers stay stateless and don't need a back-reference to
12
+ # the runner — they call `ctx.serialize(child)` and `ctx.serialize_inline(child)`.
13
+ #
14
+ # Holds counters (footnotes, link references) and accumulators
15
+ # (footnote definitions emitted at document end, etc.). Created
16
+ # fresh per top-level `call` — never shared between documents.
17
+ class Context
18
+ attr_reader :config, :registry, :runner,
19
+ :footnote_defs, :link_refs,
20
+ :footnote_counter, :link_counter
21
+
22
+ def initialize(config:, registry:, runner:)
23
+ @config = config
24
+ @registry = registry
25
+ @runner = runner
26
+ @footnote_defs = []
27
+ @link_refs = []
28
+ @footnote_counter = 0
29
+ @link_counter = 0
30
+ end
31
+
32
+ def serialize(element)
33
+ runner.serialize(element, self)
34
+ end
35
+
36
+ def serialize_inline(element)
37
+ runner.serialize_inline(element, self)
38
+ end
39
+
40
+ def serialize_inline_join(elements)
41
+ Array(elements).map { |e| serialize_inline(e) }.join
42
+ end
43
+
44
+ def next_footnote_id
45
+ @footnote_counter += 1
46
+ "fn#{@footnote_counter}"
47
+ end
48
+
49
+ def next_link_ref_id
50
+ @link_counter += 1
51
+ @link_counter.to_s
52
+ end
53
+
54
+ def register_footnote_def(definition_text)
55
+ @footnote_defs << definition_text unless @footnote_defs.include?(definition_text)
56
+ end
57
+
58
+ def register_link_ref(id, url, title: nil)
59
+ @link_refs << LinkRef.new(id: id, url: url, title: title)
60
+ end
61
+
62
+ LinkRef = Struct.new(:id, :url, :title)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ class Serializer
6
+ # Base class for element serializers. Subclasses declare:
7
+ # - `handles_type TypeClass` — the model class (or ancestor) they accept
8
+ # - `call(element, ctx)` — the actual serialization
9
+ #
10
+ # `handles?` defaults to `is_a?(handles_type)`; override for finer
11
+ # conditional dispatch (e.g. "I handle DefinitionList only when nested").
12
+ class ElementSerializer
13
+ class << self
14
+ def handles_type(klass = nil)
15
+ if klass
16
+ @handles_type = klass
17
+ self
18
+ else
19
+ @handles_type || (superclass.respond_to?(:handles_type) ? superclass.handles_type : nil)
20
+ end
21
+ end
22
+
23
+ def handles?(_element)
24
+ true
25
+ end
26
+
27
+ def call(...)
28
+ new.call(...)
29
+ end
30
+ end
31
+
32
+ def call(_element, _ctx)
33
+ raise NotImplementedError, "#{self.class.name}#call not implemented"
34
+ end
35
+
36
+ def handles?(element)
37
+ self.class.handles?(element)
38
+ end
39
+
40
+ def handles_type
41
+ self.class.handles_type
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ class Serializer
6
+ # Named Markdown flavor profiles.
7
+ #
8
+ # Each flavor bundles sensible defaults for the 5 spec options.
9
+ # Callers can override any option via `Serializer.build`.
10
+ #
11
+ # Adding a new flavor = adding one entry here. No serializer code
12
+ # needs to change — Open/Closed.
13
+ module Flavor
14
+ PROFILES = {
15
+ commonmark: {
16
+ markdown_flavor: :commonmark,
17
+ admonition_style: :html,
18
+ definition_list_nested: :html,
19
+ suppress_comments: true,
20
+ autolinks: true
21
+ },
22
+ gfm: {
23
+ markdown_flavor: :gfm,
24
+ admonition_style: :github,
25
+ definition_list_nested: :html,
26
+ suppress_comments: true,
27
+ autolinks: true
28
+ },
29
+ kramdown: {
30
+ markdown_flavor: :kramdown,
31
+ admonition_style: :html,
32
+ definition_list_nested: :html,
33
+ suppress_comments: true,
34
+ autolinks: true
35
+ },
36
+ pandoc: {
37
+ markdown_flavor: :pandoc,
38
+ admonition_style: :html,
39
+ definition_list_nested: :html,
40
+ suppress_comments: true,
41
+ autolinks: true
42
+ },
43
+ vitepress: {
44
+ markdown_flavor: :vitepress,
45
+ admonition_style: :container,
46
+ definition_list_nested: :html,
47
+ suppress_comments: true,
48
+ autolinks: true
49
+ },
50
+ php_markdown_extra: {
51
+ markdown_flavor: :php_markdown_extra,
52
+ admonition_style: :html,
53
+ definition_list_nested: :html,
54
+ suppress_comments: true,
55
+ autolinks: true
56
+ }
57
+ }.freeze
58
+
59
+ DEFAULT_FLAVOR = :gfm
60
+
61
+ class << self
62
+ def names
63
+ PROFILES.keys
64
+ end
65
+
66
+ def known?(name)
67
+ PROFILES.key?(name.to_sym)
68
+ end
69
+
70
+ def resolve(name)
71
+ profile = PROFILES[name.to_sym] || PROFILES[DEFAULT_FLAVOR]
72
+ profile.dup
73
+ end
74
+
75
+ def default
76
+ PROFILES[DEFAULT_FLAVOR]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'registry'
4
+ require_relative 'serializers/document'
5
+ require_relative 'serializers/heading'
6
+ require_relative 'serializers/paragraph'
7
+ require_relative 'serializers/list'
8
+ require_relative 'serializers/code_block'
9
+ require_relative 'serializers/blockquote'
10
+ require_relative 'serializers/link'
11
+ require_relative 'serializers/image'
12
+ require_relative 'serializers/horizontal_rule'
13
+ require_relative 'serializers/table'
14
+ require_relative 'serializers/emphasis'
15
+ require_relative 'serializers/strong'
16
+ require_relative 'serializers/code'
17
+ require_relative 'serializers/strikethrough'
18
+ require_relative 'serializers/highlight'
19
+ require_relative 'serializers/subscript'
20
+ require_relative 'serializers/superscript'
21
+ require_relative 'serializers/underline'
22
+ require_relative 'serializers/cross_reference'
23
+ require_relative 'serializers/attribute_list'
24
+ require_relative 'serializers/math'
25
+ require_relative 'serializers/extension'
26
+ require_relative 'serializers/definition_list'
27
+ require_relative 'serializers/footnote'
28
+ require_relative 'serializers/footnote_reference'
29
+ require_relative 'serializers/abbreviation'
30
+ require_relative 'serializers/comment'
31
+ require_relative 'serializers/admonition'
32
+ require_relative 'serializers/example_block'
33
+ require_relative 'serializers/open_block'
34
+ require_relative 'serializers/sidebar'
35
+ require_relative 'serializers/verse'
36
+ require_relative 'serializers/pass'
37
+ require_relative 'serializers/literal'
38
+ require_relative 'serializers/hard_line_break'
39
+
40
+ module Coradoc
41
+ module Markdown
42
+ class Serializer
43
+ # Auto-registers all built-in element serializers into a fresh
44
+ # Registry. Called once per Runner (cached via `default_registry`).
45
+ #
46
+ # Adding a new element serializer = appending one entry here. No
47
+ # lookup code changes — Open/Closed.
48
+ module Registrations
49
+ SERIALIZEABLE = [
50
+ Serializers::Document,
51
+ Serializers::Heading,
52
+ Serializers::Paragraph,
53
+ Serializers::List,
54
+ Serializers::CodeBlock,
55
+ Serializers::Blockquote,
56
+ Serializers::Link,
57
+ Serializers::Image,
58
+ Serializers::HorizontalRule,
59
+ Serializers::Table,
60
+ Serializers::Emphasis,
61
+ Serializers::Strong,
62
+ Serializers::Code,
63
+ Serializers::Strikethrough,
64
+ Serializers::Highlight,
65
+ Serializers::Subscript,
66
+ Serializers::Superscript,
67
+ Serializers::Underline,
68
+ Serializers::CrossReference,
69
+ Serializers::AttributeList,
70
+ Serializers::Math,
71
+ Serializers::Extension,
72
+ Serializers::DefinitionList,
73
+ Serializers::Footnote,
74
+ Serializers::FootnoteReference,
75
+ Serializers::Abbreviation,
76
+ Serializers::Comment,
77
+ Serializers::Admonition,
78
+ Serializers::ExampleBlock,
79
+ Serializers::OpenBlock,
80
+ Serializers::Sidebar,
81
+ Serializers::Verse,
82
+ Serializers::Pass,
83
+ Serializers::Literal,
84
+ Serializers::HardLineBreak
85
+ ].freeze
86
+
87
+ @mutex = Mutex.new
88
+ @default = nil
89
+
90
+ class << self
91
+ def default_registry
92
+ @mutex.synchronize do
93
+ @default ||= register_all(Registry.new)
94
+ end
95
+ end
96
+
97
+ def fresh_registry
98
+ register_all(Registry.new)
99
+ end
100
+
101
+ private
102
+
103
+ def register_all(registry)
104
+ SERIALIZEABLE.each { |klass| registry.register(klass.new) }
105
+ registry
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ class Serializer
6
+ # Type-keyed dispatch table for element serializers.
7
+ #
8
+ # Each entry is a tuple of (serializer_instance, priority). Dispatch
9
+ # resolves the highest-priority entry whose declared `handles?` predicate
10
+ # accepts the element. This lets specialized serializers (e.g. one that
11
+ # targets a specific CoreModel subclass) override generic ones without
12
+ # modifying the registry lookup logic — Open/Closed.
13
+ class Registry
14
+ class Entry < Struct.new(:serializer, :priority)
15
+ include Comparable
16
+
17
+ def <=>(other)
18
+ priority <=> other.priority
19
+ end
20
+ end
21
+
22
+ def initialize
23
+ @entries = Hash.new { |h, k| h[k] = [] }
24
+ end
25
+
26
+ def register(serializer, priority: 0)
27
+ klass = serializer.handles_type
28
+ raise ArgumentError, "Serializer #{serializer.class} declares no handles_type" unless klass
29
+
30
+ @entries[klass] << Entry.new(serializer, priority)
31
+ @entries[klass].sort!
32
+ serializer
33
+ end
34
+
35
+ def lookup(element)
36
+ each_candidate(element).first&.serializer
37
+ end
38
+
39
+ def lookup!(element)
40
+ lookup(element) || raise(ArgumentError,
41
+ "Unknown element type for serialization: #{element.class}. " \
42
+ 'Expected a known Markdown model type.')
43
+ end
44
+
45
+ private
46
+
47
+ def each_candidate(element)
48
+ return enum_for(:each_candidate, element) unless block_given?
49
+
50
+ walked = element.class.ancestors
51
+ walked.each do |klass|
52
+ @entries[klass].sort_by { |e| -e.priority }.each do |entry|
53
+ next unless entry.serializer.handles?(element)
54
+
55
+ yield entry
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'context'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ class Serializer
8
+ # Stateful runner: holds a frozen Config + Registry, exposes `#call`.
9
+ # Each top-level `call` creates a fresh Context (per-document mutable
10
+ # state) so the same runner can serialize multiple documents without
11
+ # cross-document leakage.
12
+ class Runner
13
+ attr_reader :config, :registry
14
+
15
+ def initialize(config:, registry:)
16
+ @config = config
17
+ @registry = registry
18
+ end
19
+
20
+ def call(element)
21
+ ctx = Context.new(config: config, registry: registry, runner: self)
22
+ result = serialize(element, ctx)
23
+ result = append_link_refs(result, ctx)
24
+ append_footnote_defs(result, ctx).to_s
25
+ end
26
+
27
+ def serialize(element, ctx = nil)
28
+ ctx ||= Context.new(config: config, registry: registry, runner: self)
29
+ case element
30
+ when String
31
+ element
32
+ when nil
33
+ ''
34
+ when Array
35
+ element.map { |e| serialize(e, ctx) }.join
36
+ else
37
+ serializer = registry.lookup(element)
38
+ if serializer
39
+ serializer.call(element, ctx)
40
+ else
41
+ raise ArgumentError,
42
+ "Unknown element type for serialization: #{element.class}. " \
43
+ 'Expected a known Markdown model type.'
44
+ end
45
+ end
46
+ end
47
+
48
+ def serialize_inline(element, ctx)
49
+ case element
50
+ when String
51
+ element
52
+ when nil
53
+ ''
54
+ when ::Coradoc::Markdown::Base, ::Coradoc::Markdown::Document
55
+ serialize(element, ctx)
56
+ when Array
57
+ element.map { |e| serialize_inline(e, ctx) }.join
58
+ else
59
+ raise ArgumentError,
60
+ "Cannot serialize inline content of type #{element.class}. " \
61
+ 'Expected String, known inline model, or Base subclass.'
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def append_link_refs(result, ctx)
68
+ return result if ctx.link_refs.empty?
69
+
70
+ refs = ctx.link_refs.map do |ref|
71
+ "[#{ref.id}]: #{ref.url}#{ref.title ? " \"#{ref.title}\"" : ''}"
72
+ end.join("\n")
73
+ "#{result}\n\n#{refs}"
74
+ end
75
+
76
+ def append_footnote_defs(result, ctx)
77
+ return result if ctx.footnote_defs.empty?
78
+
79
+ "#{result}\n\n#{ctx.footnote_defs.join("\n")}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../element_serializer'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ class Serializer
8
+ module Serializers
9
+ class Abbreviation < ElementSerializer
10
+ handles_type ::Coradoc::Markdown::Abbreviation
11
+
12
+ def call(element, _ctx)
13
+ "*[#{element.term}]: #{element.definition}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../element_serializer'
4
+ require_relative '../strategies/admonition/registry'
5
+
6
+ module Coradoc
7
+ module Markdown
8
+ class Serializer
9
+ module Serializers
10
+ class Admonition < ElementSerializer
11
+ handles_type ::Coradoc::Markdown::Admonition
12
+
13
+ def call(element, ctx)
14
+ Strategies::Admonition::Registry.render(element, ctx: ctx)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../element_serializer'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ class Serializer
8
+ module Serializers
9
+ class AttributeList < ElementSerializer
10
+ handles_type ::Coradoc::Markdown::AttributeList
11
+
12
+ def call(element, _ctx)
13
+ return '' if element.empty?
14
+
15
+ parts = []
16
+ parts << "##{element.id}" if element.id
17
+ parts += element.classes.map { |c| ".#{c}" }
18
+ parts += element.attributes.map { |nv| %(#{nv.name}="#{nv.value}") }
19
+ "{:#{parts.join(' ')}}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../element_serializer'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ class Serializer
8
+ module Serializers
9
+ class Blockquote < ElementSerializer
10
+ handles_type ::Coradoc::Markdown::Blockquote
11
+
12
+ def call(element, _ctx)
13
+ element.content.to_s.lines.map { |line| "> #{line}" }.join
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../element_serializer'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ class Serializer
8
+ module Serializers
9
+ class Code < ElementSerializer
10
+ handles_type ::Coradoc::Markdown::Code
11
+
12
+ def call(element, _ctx)
13
+ "`#{element.text}`"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end