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.
- checksums.yaml +4 -4
- data/lib/coradoc/markdown/model/admonition.rb +35 -0
- data/lib/coradoc/markdown/model/attribute_list.rb +5 -16
- data/lib/coradoc/markdown/model/base.rb +2 -34
- data/lib/coradoc/markdown/model/comment.rb +14 -0
- data/lib/coradoc/markdown/model/cross_reference.rb +10 -0
- data/lib/coradoc/markdown/model/definition_list.rb +0 -12
- data/lib/coradoc/markdown/model/definition_term.rb +16 -6
- data/lib/coradoc/markdown/model/document.rb +9 -0
- data/lib/coradoc/markdown/model/example_block.rb +26 -0
- data/lib/coradoc/markdown/model/extension.rb +9 -18
- data/lib/coradoc/markdown/model/footnote_reference.rb +0 -5
- data/lib/coradoc/markdown/model/hard_line_break.rb +21 -0
- data/lib/coradoc/markdown/model/highlight.rb +0 -5
- data/lib/coradoc/markdown/model/horizontal_rule.rb +1 -6
- data/lib/coradoc/markdown/model/list_item.rb +1 -1
- data/lib/coradoc/markdown/model/literal.rb +21 -0
- data/lib/coradoc/markdown/model/math.rb +0 -14
- data/lib/coradoc/markdown/model/named_value.rb +18 -0
- data/lib/coradoc/markdown/model/open_block.rb +22 -0
- data/lib/coradoc/markdown/model/paragraph.rb +2 -2
- data/lib/coradoc/markdown/model/pass.rb +21 -0
- data/lib/coradoc/markdown/model/sidebar.rb +22 -0
- data/lib/coradoc/markdown/model/strikethrough.rb +0 -5
- data/lib/coradoc/markdown/model/subscript.rb +9 -0
- data/lib/coradoc/markdown/model/superscript.rb +9 -0
- data/lib/coradoc/markdown/model/underline.rb +9 -0
- data/lib/coradoc/markdown/model/verse.rb +27 -0
- data/lib/coradoc/markdown/parser/block_parser.rb +1 -1
- data/lib/coradoc/markdown/parser/frontmatter_parser.rb +23 -0
- data/lib/coradoc/markdown/serializer/builder.rb +57 -0
- data/lib/coradoc/markdown/serializer/config.rb +77 -0
- data/lib/coradoc/markdown/serializer/context.rb +66 -0
- data/lib/coradoc/markdown/serializer/element_serializer.rb +46 -0
- data/lib/coradoc/markdown/serializer/flavor.rb +82 -0
- data/lib/coradoc/markdown/serializer/registrations.rb +111 -0
- data/lib/coradoc/markdown/serializer/registry.rb +62 -0
- data/lib/coradoc/markdown/serializer/runner.rb +84 -0
- data/lib/coradoc/markdown/serializer/serializers/abbreviation.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/admonition.rb +20 -0
- data/lib/coradoc/markdown/serializer/serializers/attribute_list.rb +25 -0
- data/lib/coradoc/markdown/serializer/serializers/blockquote.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/code.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/code_block.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/comment.rb +31 -0
- data/lib/coradoc/markdown/serializer/serializers/cross_reference.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/definition_list.rb +20 -0
- data/lib/coradoc/markdown/serializer/serializers/document.rb +22 -0
- data/lib/coradoc/markdown/serializer/serializers/emphasis.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/example_block.rb +40 -0
- data/lib/coradoc/markdown/serializer/serializers/extension.rb +30 -0
- data/lib/coradoc/markdown/serializer/serializers/footnote.rb +20 -0
- data/lib/coradoc/markdown/serializer/serializers/footnote_reference.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/hard_line_break.rb +22 -0
- data/lib/coradoc/markdown/serializer/serializers/heading.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/highlight.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/horizontal_rule.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/image.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/link.rb +29 -0
- data/lib/coradoc/markdown/serializer/serializers/list.rb +45 -0
- data/lib/coradoc/markdown/serializer/serializers/literal.rb +21 -0
- data/lib/coradoc/markdown/serializer/serializers/math.rb +23 -0
- data/lib/coradoc/markdown/serializer/serializers/open_block.rb +38 -0
- data/lib/coradoc/markdown/serializer/serializers/paragraph.rb +23 -0
- data/lib/coradoc/markdown/serializer/serializers/pass.rb +21 -0
- data/lib/coradoc/markdown/serializer/serializers/sidebar.rb +20 -0
- data/lib/coradoc/markdown/serializer/serializers/strikethrough.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/strong.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/subscript.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/superscript.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/table.rb +37 -0
- data/lib/coradoc/markdown/serializer/serializers/underline.rb +19 -0
- data/lib/coradoc/markdown/serializer/serializers/verse.rb +31 -0
- data/lib/coradoc/markdown/serializer/strategies/admonition/base.rb +30 -0
- data/lib/coradoc/markdown/serializer/strategies/admonition/container.rb +34 -0
- data/lib/coradoc/markdown/serializer/strategies/admonition/gfm_alert.rb +28 -0
- data/lib/coradoc/markdown/serializer/strategies/admonition/github.rb +29 -0
- data/lib/coradoc/markdown/serializer/strategies/admonition/html.rb +25 -0
- data/lib/coradoc/markdown/serializer/strategies/admonition/registry.rb +50 -0
- data/lib/coradoc/markdown/serializer/strategies/autolink/angle.rb +35 -0
- data/lib/coradoc/markdown/serializer/strategies/autolink/bare.rb +23 -0
- data/lib/coradoc/markdown/serializer/strategies/autolink/base.rb +33 -0
- data/lib/coradoc/markdown/serializer/strategies/autolink/none.rb +27 -0
- data/lib/coradoc/markdown/serializer/strategies/autolink/registry.rb +58 -0
- data/lib/coradoc/markdown/serializer/strategies/definition_list/base.rb +37 -0
- data/lib/coradoc/markdown/serializer/strategies/definition_list/flat.rb +48 -0
- data/lib/coradoc/markdown/serializer/strategies/definition_list/nested_html.rb +54 -0
- data/lib/coradoc/markdown/serializer/strategies/definition_list/registry.rb +37 -0
- data/lib/coradoc/markdown/serializer.rb +30 -181
- data/lib/coradoc/markdown/toc_generator.rb +2 -8
- data/lib/coradoc/markdown/transform/block_transformer.rb +163 -0
- data/lib/coradoc/markdown/transform/from_core_model.rb +205 -40
- data/lib/coradoc/markdown/transform/image_transformer.rb +20 -0
- data/lib/coradoc/markdown/transform/inline_transformer.rb +74 -0
- data/lib/coradoc/markdown/transform/list_transformer.rb +52 -0
- data/lib/coradoc/markdown/transform/structural_transformer.rb +94 -0
- data/lib/coradoc/markdown/transform/table_transformer.rb +40 -0
- data/lib/coradoc/markdown/transform/to_core_model.rb +36 -30
- data/lib/coradoc/markdown/transformer.rb +87 -2
- data/lib/coradoc/markdown/version.rb +1 -1
- data/lib/coradoc/markdown.rb +23 -20
- 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
|