coradoc-markdown 1.0.2 → 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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/markdown/model/admonition.rb +35 -0
  3. data/lib/coradoc/markdown/model/comment.rb +14 -0
  4. data/lib/coradoc/markdown/model/definition_term.rb +16 -6
  5. data/lib/coradoc/markdown/model/document.rb +9 -0
  6. data/lib/coradoc/markdown/model/example_block.rb +26 -0
  7. data/lib/coradoc/markdown/model/hard_line_break.rb +21 -0
  8. data/lib/coradoc/markdown/model/horizontal_rule.rb +1 -6
  9. data/lib/coradoc/markdown/model/list_item.rb +1 -1
  10. data/lib/coradoc/markdown/model/literal.rb +21 -0
  11. data/lib/coradoc/markdown/model/open_block.rb +22 -0
  12. data/lib/coradoc/markdown/model/paragraph.rb +2 -2
  13. data/lib/coradoc/markdown/model/pass.rb +21 -0
  14. data/lib/coradoc/markdown/model/sidebar.rb +22 -0
  15. data/lib/coradoc/markdown/model/verse.rb +27 -0
  16. data/lib/coradoc/markdown/parser/block_parser.rb +1 -1
  17. data/lib/coradoc/markdown/parser/frontmatter_parser.rb +23 -0
  18. data/lib/coradoc/markdown/serializer/builder.rb +57 -0
  19. data/lib/coradoc/markdown/serializer/config.rb +77 -0
  20. data/lib/coradoc/markdown/serializer/context.rb +66 -0
  21. data/lib/coradoc/markdown/serializer/element_serializer.rb +46 -0
  22. data/lib/coradoc/markdown/serializer/flavor.rb +82 -0
  23. data/lib/coradoc/markdown/serializer/registrations.rb +111 -0
  24. data/lib/coradoc/markdown/serializer/registry.rb +62 -0
  25. data/lib/coradoc/markdown/serializer/runner.rb +84 -0
  26. data/lib/coradoc/markdown/serializer/serializers/abbreviation.rb +19 -0
  27. data/lib/coradoc/markdown/serializer/serializers/admonition.rb +20 -0
  28. data/lib/coradoc/markdown/serializer/serializers/attribute_list.rb +25 -0
  29. data/lib/coradoc/markdown/serializer/serializers/blockquote.rb +19 -0
  30. data/lib/coradoc/markdown/serializer/serializers/code.rb +19 -0
  31. data/lib/coradoc/markdown/serializer/serializers/code_block.rb +19 -0
  32. data/lib/coradoc/markdown/serializer/serializers/comment.rb +31 -0
  33. data/lib/coradoc/markdown/serializer/serializers/cross_reference.rb +19 -0
  34. data/lib/coradoc/markdown/serializer/serializers/definition_list.rb +20 -0
  35. data/lib/coradoc/markdown/serializer/serializers/document.rb +22 -0
  36. data/lib/coradoc/markdown/serializer/serializers/emphasis.rb +19 -0
  37. data/lib/coradoc/markdown/serializer/serializers/example_block.rb +40 -0
  38. data/lib/coradoc/markdown/serializer/serializers/extension.rb +30 -0
  39. data/lib/coradoc/markdown/serializer/serializers/footnote.rb +20 -0
  40. data/lib/coradoc/markdown/serializer/serializers/footnote_reference.rb +19 -0
  41. data/lib/coradoc/markdown/serializer/serializers/hard_line_break.rb +22 -0
  42. data/lib/coradoc/markdown/serializer/serializers/heading.rb +19 -0
  43. data/lib/coradoc/markdown/serializer/serializers/highlight.rb +19 -0
  44. data/lib/coradoc/markdown/serializer/serializers/horizontal_rule.rb +19 -0
  45. data/lib/coradoc/markdown/serializer/serializers/image.rb +19 -0
  46. data/lib/coradoc/markdown/serializer/serializers/link.rb +29 -0
  47. data/lib/coradoc/markdown/serializer/serializers/list.rb +45 -0
  48. data/lib/coradoc/markdown/serializer/serializers/literal.rb +21 -0
  49. data/lib/coradoc/markdown/serializer/serializers/math.rb +23 -0
  50. data/lib/coradoc/markdown/serializer/serializers/open_block.rb +38 -0
  51. data/lib/coradoc/markdown/serializer/serializers/paragraph.rb +23 -0
  52. data/lib/coradoc/markdown/serializer/serializers/pass.rb +21 -0
  53. data/lib/coradoc/markdown/serializer/serializers/sidebar.rb +20 -0
  54. data/lib/coradoc/markdown/serializer/serializers/strikethrough.rb +19 -0
  55. data/lib/coradoc/markdown/serializer/serializers/strong.rb +19 -0
  56. data/lib/coradoc/markdown/serializer/serializers/subscript.rb +19 -0
  57. data/lib/coradoc/markdown/serializer/serializers/superscript.rb +19 -0
  58. data/lib/coradoc/markdown/serializer/serializers/table.rb +37 -0
  59. data/lib/coradoc/markdown/serializer/serializers/underline.rb +19 -0
  60. data/lib/coradoc/markdown/serializer/serializers/verse.rb +31 -0
  61. data/lib/coradoc/markdown/serializer/strategies/admonition/base.rb +30 -0
  62. data/lib/coradoc/markdown/serializer/strategies/admonition/container.rb +34 -0
  63. data/lib/coradoc/markdown/serializer/strategies/admonition/gfm_alert.rb +28 -0
  64. data/lib/coradoc/markdown/serializer/strategies/admonition/github.rb +29 -0
  65. data/lib/coradoc/markdown/serializer/strategies/admonition/html.rb +25 -0
  66. data/lib/coradoc/markdown/serializer/strategies/admonition/registry.rb +50 -0
  67. data/lib/coradoc/markdown/serializer/strategies/autolink/angle.rb +35 -0
  68. data/lib/coradoc/markdown/serializer/strategies/autolink/bare.rb +23 -0
  69. data/lib/coradoc/markdown/serializer/strategies/autolink/base.rb +33 -0
  70. data/lib/coradoc/markdown/serializer/strategies/autolink/none.rb +27 -0
  71. data/lib/coradoc/markdown/serializer/strategies/autolink/registry.rb +58 -0
  72. data/lib/coradoc/markdown/serializer/strategies/definition_list/base.rb +37 -0
  73. data/lib/coradoc/markdown/serializer/strategies/definition_list/flat.rb +48 -0
  74. data/lib/coradoc/markdown/serializer/strategies/definition_list/nested_html.rb +54 -0
  75. data/lib/coradoc/markdown/serializer/strategies/definition_list/registry.rb +37 -0
  76. data/lib/coradoc/markdown/serializer.rb +29 -243
  77. data/lib/coradoc/markdown/toc_generator.rb +2 -2
  78. data/lib/coradoc/markdown/transform/block_transformer.rb +163 -0
  79. data/lib/coradoc/markdown/transform/from_core_model.rb +187 -28
  80. data/lib/coradoc/markdown/transform/image_transformer.rb +20 -0
  81. data/lib/coradoc/markdown/transform/inline_transformer.rb +74 -0
  82. data/lib/coradoc/markdown/transform/list_transformer.rb +52 -0
  83. data/lib/coradoc/markdown/transform/structural_transformer.rb +94 -0
  84. data/lib/coradoc/markdown/transform/table_transformer.rb +40 -0
  85. data/lib/coradoc/markdown/transform/to_core_model.rb +24 -3
  86. data/lib/coradoc/markdown/transformer.rb +87 -2
  87. data/lib/coradoc/markdown/version.rb +1 -1
  88. data/lib/coradoc/markdown.rb +16 -2
  89. metadata +75 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f645b393312f1c1dac03f7986da00e8165c28a08fa10e656284dfd0fa8b024e
4
- data.tar.gz: f90f52be5816d785860b14615ed5aff2588f6a061ed8006753adc030111f5c05
3
+ metadata.gz: e84594b2f47a9d07086b781a843c5b1f7b2ddb34303bbe32114ea1c9e7ac1aec
4
+ data.tar.gz: 7fd311efb6eef499247c37bacb8357d3b6349eabc1347c6a875d21c6b315fa18
5
5
  SHA512:
6
- metadata.gz: 29d1fa6b132ff28425d7ccfd7eeacfc782f9d648a07802fdbfb434134e13a2a0ee40489e207ca6ea4d1f92358feb0c94097dc168024339b833cf558e7e6bcab3
7
- data.tar.gz: 92960c95e653cc2c0c16ccfb1e23563db3d1ebe7a931b4c3a28f88c8be87d922e604b6448c53d493661e42c1bbcdeb8b2b08ce0847300cee624948accebacc92
6
+ metadata.gz: ee32797ed75c3659d6e21fd9041d4f9a193497c50417f1f2ca986bd23459bf83b5a3cab47540f417d34014d806b1f20671e132ca4b0c0195b7b23870cb4aff37
7
+ data.tar.gz: 2cfc83d2fa5c1e732283d6efa0d37f05b5f9ea4c8970217f64cbb6771084c4aa2f6fa0aaaba4049aceb9ed9fb611d6395d210a5c8ce8c199a3bbf1768e4d4665
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Admonition block — a callout like NOTE, WARNING, TIP, etc.
8
+ #
9
+ # Markdown has no native admonition syntax (GFM Alerts since Dec 2023
10
+ # notwithstanding — see admonition/gfm_alert strategy). The
11
+ # `admonition_style` config option selects the output form:
12
+ #
13
+ # :github → > **NOTE:** text (broad compat)
14
+ # :gfm_alert → > [!NOTE]\n> text (GFM native since 2024)
15
+ # :container → :::note\n... \n::: (VitePress / markdown-it)
16
+ # :html → <div class="note">...</div>
17
+ #
18
+ # Type is stored lowercase. Content is raw Markdown text (already
19
+ # serialized). Title is optional.
20
+ class Admonition < Base
21
+ ALLOWED_TYPES = %w[note tip warning important caution].freeze
22
+
23
+ attribute :admonition_type, :string
24
+ attribute :content, :string
25
+ attribute :title, :string
26
+
27
+ def initialize(admonition_type:, content:, title: nil, **rest)
28
+ super
29
+ @admonition_type = admonition_type.to_s.downcase
30
+ @content = content
31
+ @title = title
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ # HTML comment in Markdown source (`<!-- text -->`).
6
+ #
7
+ # Markdown has no native comment syntax; HTML comments are the conventional
8
+ # mechanism for non-rendered authoring notes. Both AsciiDoc single-line
9
+ # (`//`) and block (`//// ... ////`) comments collapse to this type.
10
+ class Comment < Base
11
+ attribute :text, :string
12
+ end
13
+ end
14
+ end
@@ -7,15 +7,25 @@ module Coradoc
7
7
  # A term can have multiple definitions and can span multiple lines.
8
8
  # Terms can also have IAL attributes attached.
9
9
  #
10
- # @example Simple term
11
- # term = Coradoc::Markdown::DefinitionTerm.new(text: "kramdown")
12
- #
10
+ # A term can carry a nested DefinitionList when the original document
11
+ # had structured sub-entries (e.g. glossary sub-terms). When `nested`
12
+ # is present, the serializer's nested_html strategy kicks in to emit
13
+ # HTML `<dl>` structure that Markdown syntax cannot express.
13
14
  class DefinitionTerm < Base
14
- # The term text content
15
15
  attribute :text, :string
16
-
17
- # Definitions for this term
18
16
  attribute :definitions, Coradoc::Markdown::DefinitionItem, collection: true, default: []
17
+
18
+ # Optional nested definition list under this term.
19
+ # When present, the flat-PHP-Markdown-Extra syntax is no longer
20
+ # sufficient and the serializer falls back to HTML <dl>/<dt>/<dd>.
21
+ attribute :nested, Coradoc::Markdown::DefinitionList
22
+
23
+ def initialize(text: '', definitions: [], nested: nil, **rest)
24
+ super
25
+ @text = text
26
+ @definitions = definitions
27
+ @nested = nested
28
+ end
19
29
  end
20
30
  end
21
31
  end
@@ -18,6 +18,15 @@ module Coradoc
18
18
  class Document < Base
19
19
  attribute :blocks, Coradoc::Markdown::Base, collection: true
20
20
 
21
+ # Raw YAML frontmatter text, or nil if absent.
22
+ #
23
+ # Markdown treats frontmatter as opaque text — the YAML is only
24
+ # parsed/emitted at the CoreModel boundary by
25
+ # CoreModel::FrontmatterBlock::Codec (single source of truth).
26
+ # Storing raw text keeps Markdown's parser/serializer symmetric
27
+ # without dragging YAML semantics into the Markdown model (MECE).
28
+ attribute :frontmatter, :string
29
+
21
30
  # @param [Integer] index The index of the block to retrieve
22
31
  # @return [Coradoc::Markdown::Base] The block at the specified index
23
32
  def [](index)
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Example block — an illustrative container, optionally with a caption.
8
+ #
9
+ # Markdown has no native example syntax. Default rendering is an
10
+ # HTML fallback that preserves the caption as an `<h4>` heading
11
+ # inside a `<div class="example">` wrapper.
12
+ #
13
+ # When `admonition_style == :container` (VitePress), the serializer
14
+ # emits `:::details Caption\n...\n:::`.
15
+ class ExampleBlock < Base
16
+ attribute :content, :string
17
+ attribute :caption, :string
18
+
19
+ def initialize(content:, caption: nil, **rest)
20
+ super
21
+ @content = content
22
+ @caption = caption
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Hard line break — forces a line break within a paragraph, distinct
8
+ # from a new paragraph.
9
+ #
10
+ # Two strategy modes (configurable via the serializer config):
11
+ # - `:trailing_space` (CommonMark default): two trailing spaces
12
+ # - `:backslash` (GFM alternative): backslash before newline
13
+ #
14
+ # AsciiDoc `+` line-continuation maps to this element.
15
+ class HardLineBreak < Base
16
+ def initialize(**_rest)
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -5,12 +5,7 @@ module Coradoc
5
5
  # HorizontalRule model representing a Markdown horizontal rule (---, ***, ___).
6
6
  #
7
7
  class HorizontalRule < Base
8
- attribute :style, :string, default: '---' # ---, ***, or ___
9
-
10
- def initialize(style: '---')
11
- super()
12
- @style = style
13
- end
8
+ attribute :style, :string, default: '---'
14
9
  end
15
10
  end
16
11
  end
@@ -18,7 +18,7 @@ module Coradoc
18
18
  @text = args[:text] || ''
19
19
  @checked = args[:checked]
20
20
  @sublist = args[:sublist]
21
- @children = args[:children] || []
21
+ @children = args.fetch(:children, [])
22
22
  end
23
23
 
24
24
  def children=(value)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Literal block — preformatted text with no syntax highlighting and
8
+ # no inline formatting. Distinct from a code block (which carries a
9
+ # language for highlighting).
10
+ #
11
+ # Markdown: indented code block (4 leading spaces per line).
12
+ class Literal < Base
13
+ attribute :content, :string
14
+
15
+ def initialize(content:, **rest)
16
+ super
17
+ @content = content
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Open block — generic container that groups content without semantic
8
+ # formatting. Used in AsciiDoc to apply IDs/attributes to a group.
9
+ #
10
+ # Markdown has no native container. The serializer's OpenBlock strategy:
11
+ # - Without id/classes → emit children as siblings (no wrapper)
12
+ # - With id/classes → emit `<div id="...">...</div>` wrapper
13
+ class OpenBlock < Base
14
+ attribute :children, Coradoc::Markdown::Base, collection: true, default: []
15
+
16
+ def initialize(children: [], **rest)
17
+ super
18
+ @children = Array(children)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -14,10 +14,10 @@ module Coradoc
14
14
  # @return [Array] mixed content array
15
15
  attr_reader :children
16
16
 
17
- def initialize(text: '', children: nil)
17
+ def initialize(text: '', children: [])
18
18
  super()
19
19
  @text = text
20
- @children = children || []
20
+ @children = children
21
21
  end
22
22
 
23
23
  def children=(value)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Pass block — raw content passed through verbatim, never rendered as
8
+ # Markdown. Used for embedding HTML or other markup directly.
9
+ #
10
+ # Markdown has no native pass concept; the content is emitted as-is
11
+ # inside a `nomarkdown` kramdown extension or raw HTML passthrough.
12
+ class Pass < Base
13
+ attribute :content, :string
14
+
15
+ def initialize(content:, **rest)
16
+ super
17
+ @content = content
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Sidebar block — a tangential aside, visually distinct from main flow.
8
+ #
9
+ # Markdown has no native sidebar. Serialized as an HTML fallback:
10
+ # <div class="sidebar">...</div>
11
+ class Sidebar < Base
12
+ attribute :content, :string
13
+ attribute :title, :string
14
+
15
+ def initialize(content:, title: nil, **rest)
16
+ super
17
+ @content = content
18
+ @title = title
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Coradoc
6
+ module Markdown
7
+ # Verse block — preformatted text preserving line breaks but allowing
8
+ # inline formatting. Distinct from a literal block (no formatting)
9
+ # or a code block (no formatting + language hint).
10
+ #
11
+ # Markdown has no native verse. Serialized as a blockquote with the
12
+ # understanding that verse semantics are lost but line breaks are
13
+ # preserved via hard line breaks.
14
+ class Verse < Base
15
+ attribute :content, :string
16
+ attribute :attribution, :string
17
+ attribute :citetitle, :string
18
+
19
+ def initialize(content:, attribution: nil, citetitle: nil, **rest)
20
+ super
21
+ @content = content
22
+ @attribution = attribution
23
+ @citetitle = citetitle
24
+ end
25
+ end
26
+ end
27
+ end
@@ -424,7 +424,7 @@ module Coradoc
424
424
  table_separator_row.as(:table_separator) >>
425
425
  line_ending >>
426
426
  (
427
- table_row.as(:table_body_row) >> line_ending
427
+ table_row.as(:table_body_row) >> (line_ending | any.absent?)
428
428
  ).repeat(1).as(:table_body)
429
429
  end
430
430
 
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Markdown
5
+ module Parser
6
+ # Markdown frontmatter extractor.
7
+ #
8
+ # Delegates to the shared +Coradoc::CoreModel::FrontmatterBlock::TextSplitter+
9
+ # — the single source of truth for the `---\n...\n---\n` convention
10
+ # (DRY). Format gems retain a local parser module for discoverability
11
+ # and as the seam for any format-specific extensions should they arise.
12
+ module FrontmatterParser
13
+ class << self
14
+ def call(text)
15
+ Coradoc::CoreModel::FrontmatterBlock::TextSplitter.call(text)
16
+ end
17
+ end
18
+
19
+ Result = Coradoc::CoreModel::FrontmatterBlock::TextSplitter::Result
20
+ end
21
+ end
22
+ end
23
+ end
@@ -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