coradoc 2.0.18 → 2.0.20

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d959d40057c13f8a634e88fb5f60b9a42c4d80cda2ccf5c701222afd33799017
4
- data.tar.gz: 933c97c0ffc3691683760d256354a5c03148aa929f2c61b51c3a41e859f54c9b
3
+ metadata.gz: 05f1c48e0de6edb3f2473ab0e1c0cdb4747c513bf805820b8456846888b86c37
4
+ data.tar.gz: caaadd87ea878292673070688c08c8a7bfb7c39c58173f928e1960eb5af397ad
5
5
  SHA512:
6
- metadata.gz: 55e37e38878cc8728ed4d12cebfac1fd370fe19c1e69339a10a3de9492cde1f096a75ee6e49917869440c8c72b90109281cfd392e19741f56d9bd3cfd968319b
7
- data.tar.gz: 5d8c7ca506943cad23f79c61c511666b2cd466324dd849d2c267a381f8f119f3d956f50c6c26fa879664a3d49e7467c057500ff7d4a8515edb0574929d718815
6
+ metadata.gz: 86f2108a83a94d30b5660586c5b83085b1b9f1ebd0ab470d21292f258cc0473d4769eb266394082d335a7d39ed2499297f0d4d1f1e2ac517c42f3d5b3bbe3527
7
+ data.tar.gz: aa4241a2a9c0ae8cec854b747b76a709bb8954b9bbbf6a3c15b3ebaad77f7b1dea199fc57129541fadcce94bee957cde48f7c3bda906379a5095a4893efdd0e1
@@ -117,6 +117,23 @@ module Coradoc
117
117
  end
118
118
  end
119
119
 
120
+ def flat_text
121
+ ""
122
+ end
123
+
124
+ # Flatten this element to a plain-text string.
125
+ #
126
+ # Subclasses that include ChildrenContent override this to
127
+ # concatenate their children's text. Block-level elements
128
+ # without textual content (ListBlock, Table, etc.) fall back
129
+ # to the empty string — they are serialized structurally,
130
+ # not flattened into inline text.
131
+ #
132
+ # @return [String]
133
+ def flat_text
134
+ ""
135
+ end
136
+
120
137
  # Accept a visitor to traverse this element
121
138
  #
122
139
  # Implements the visitor pattern for document traversal.
@@ -53,10 +53,16 @@ module Coradoc
53
53
  # @return [String, nil] language identifier for source code blocks
54
54
  attribute :language, :string
55
55
 
56
+ # @!attribute callouts
57
+ # @return [Array<Callout>] callout annotations attached to this
58
+ # block. Empty for most block types; populated by format gems
59
+ # when AsciiDoc-style `<N>` annotations follow a verbatim block.
60
+ attribute :callouts, Callout, collection: true, default: -> { [] }
61
+
56
62
  private
57
63
 
58
64
  def comparable_attributes
59
- super + %i[block_semantic_type content]
65
+ super + %i[block_semantic_type content callouts]
60
66
  end
61
67
  end
62
68
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ # A single callout annotation attached to a verbatim block.
6
+ #
7
+ # Callouts are the AsciiDoc convention for annotating individual lines
8
+ # of a source/listing block: `<1>` markers appear inside the code and
9
+ # matching `<1> explanation` lines follow the block. Markdown has no
10
+ # native equivalent, so each format gem decides how to render them.
11
+ #
12
+ # The CoreModel stores each annotation as a typed Callout on its parent
13
+ # block, with the in-code marker `<index>` preserved in the block's
14
+ # `content` for verbatim round-trip.
15
+ class Callout < Base
16
+ # @!attribute index
17
+ # @return [Integer, nil] 1-based callout number matching the
18
+ # `<N>` marker embedded in the parent block's content.
19
+ attribute :index, :integer
20
+
21
+ # @!attribute content
22
+ # @return [String, nil] human-readable annotation text.
23
+ attribute :content, :string
24
+
25
+ private
26
+
27
+ def comparable_attributes
28
+ super + %i[index content]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ # Shared helpers for rendering Callout-annotated verbatim blocks.
6
+ #
7
+ # Both the Markdown and HTML spokes need to (a) order callouts by their
8
+ # numeric index and (b) strip AsciiDoc-style `<N>` markers from the
9
+ # raw code so they don't leak as literal text in the output format.
10
+ # Centralizing these operations here keeps the behavior consistent
11
+ # across spokes and avoids copy-paste drift.
12
+ module CalloutText
13
+ module_function
14
+
15
+ def ordered(callouts)
16
+ Array(callouts).sort_by { |c| c.index || Float::INFINITY }
17
+ end
18
+
19
+ # Removes callout markers (`<N>`) from `code` for the indices
20
+ # referenced by `callouts`. Returns `code` unchanged when no
21
+ # callouts are provided or none carry a usable index, so literal
22
+ # `<N>` sequences in code without callouts are preserved.
23
+ def strip_markers(code, callouts)
24
+ list = Array(callouts)
25
+ return code if list.empty?
26
+
27
+ indices = list.filter_map(&:index).uniq
28
+ return code if indices.empty?
29
+
30
+ pattern = /<\s*(?:#{indices.join('|')})\s*>/
31
+ code.to_s.lines(chomp: true).map { |line| line.gsub(pattern, '').rstrip }.join("\n")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -25,6 +25,9 @@ module Coradoc
25
25
 
26
26
  CoreModel::TextContent.new(text: item.to_s)
27
27
  end.compact
28
+ # Lutaml defines the setter directly on the class, so we overwrite it.
29
+ # We cannot use `super` because the original setter is lost.
30
+ # `instance_variable_set` is required here to actually store the wrapped value.
28
31
  instance_variable_set(:@children, wrapped)
29
32
  end
30
33
  end
@@ -44,10 +47,21 @@ module Coradoc
44
47
  rc = renderable_content
45
48
  case rc
46
49
  when String then rc
47
- when Array then rc.map { |c| c.is_a?(TextContent) ? c.text : c.content.to_s }.join
50
+ when Array then rc.map { |c| extract_child_text(c) }.join
48
51
  else rc.to_s
49
52
  end
50
53
  end
54
+
55
+ private
56
+
57
+ def extract_child_text(child)
58
+ case child
59
+ when TextContent then child.text
60
+ when String then child
61
+ when Base then child.flat_text
62
+ else child.to_s
63
+ end
64
+ end
51
65
  end
52
66
  end
53
67
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ # Single-line comment — editorial or hidden notes that do not render.
6
+ #
7
+ # Distinct from {CommentBlock} (multi-line). Round-trip fidelity for the
8
+ # single-line vs. block distinction is preserved across formats that have
9
+ # a single-line comment syntax (AsciiDoc `//`); formats without one (e.g.
10
+ # Markdown) collapse both to `<!-- ... -->`.
11
+ class CommentLine < Base
12
+ def self.semantic_type
13
+ :comment_line
14
+ end
15
+
16
+ attribute :text, :string
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Coradoc
6
+ module CoreModel
7
+ class FrontmatterBlock
8
+ # Single source of truth for YAML ↔ FrontmatterBlock translation.
9
+ #
10
+ # No other code in any gem may call YAML directly for frontmatter.
11
+ # This isolates permitted-classes configuration and error handling
12
+ # in one MECE location (DRY).
13
+ module Codec
14
+ PERMITTED_CLASSES = [Date, Time, DateTime, Symbol].freeze
15
+
16
+ class << self
17
+ # Parse a YAML string into a FrontmatterBlock.
18
+ # Returns an empty FrontmatterBlock on malformed YAML (graceful
19
+ # degradation — body parsing continues).
20
+ def from_yaml(yaml_text)
21
+ return FrontmatterBlock.new if yaml_text.nil? || yaml_text.strip.empty?
22
+
23
+ parsed = YAML.safe_load(
24
+ yaml_text,
25
+ permitted_classes: PERMITTED_CLASSES,
26
+ aliases: true
27
+ )
28
+ return FrontmatterBlock.new unless parsed.is_a?(Hash)
29
+
30
+ schema = parsed['$schema']
31
+ data = parsed.except('$schema')
32
+ FrontmatterBlock.new(schema: schema&.to_s, data: data)
33
+ rescue YAML::SyntaxError, Psych::DisallowedClass
34
+ FrontmatterBlock.new
35
+ end
36
+
37
+ # Serialize a FrontmatterBlock to canonical YAML text.
38
+ # Does NOT include leading/trailing `---` delimiters; the caller
39
+ # wraps the output.
40
+ def to_yaml(block)
41
+ return '' unless block.is_a?(FrontmatterBlock)
42
+
43
+ tree = {}
44
+ tree['$schema'] = block.schema if block.schema
45
+ tree.merge!(block.data || {})
46
+ return '' if tree.empty?
47
+
48
+ YAML.dump(tree).delete_prefix("---\n").delete_suffix("\n...")
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ class FrontmatterBlock
6
+ # OCP registry for semantic field transforms applied during
7
+ # format conversion (e.g., `authors` array → `author` string when
8
+ # emitting Markdown for Jekyll).
9
+ #
10
+ # Transforms are directional + format-specific. Each transform
11
+ # declares when it applies and how it rewrites the block's data
12
+ # hash. Never mutates the input — always returns a new block.
13
+ module FieldTransform
14
+ # Base class. Override #applies? and #apply in subclasses.
15
+ class Base
16
+ # Override: return true if this transform should fire for the
17
+ # given direction (:to_format or :from_format) and format
18
+ # (:markdown, :asciidoc, etc.).
19
+ def applies?(direction:, format:) # rubocop:disable Lint/UnusedMethodArgument
20
+ false
21
+ end
22
+
23
+ # Override: receive a FrontmatterBlock, return a (possibly new)
24
+ # FrontmatterBlock. Never mutate the input.
25
+ def apply(block)
26
+ block
27
+ end
28
+
29
+ protected
30
+
31
+ # Helper: produce a new FrontmatterBlock with transformed data.
32
+ def rebuild(block, data:)
33
+ FrontmatterBlock.new(schema: block.schema, data: data)
34
+ end
35
+ end
36
+
37
+ class Registry
38
+ DEFAULT = new
39
+
40
+ def initialize
41
+ @transforms = []
42
+ end
43
+
44
+ def register(transform_class)
45
+ @transforms << transform_class unless @transforms.include?(transform_class)
46
+ end
47
+
48
+ def count
49
+ @transforms.size
50
+ end
51
+
52
+ # Apply all registered transforms whose #applies? returns true.
53
+ # Returns a FrontmatterBlock (possibly the same one).
54
+ def apply_all(block, direction:, format:)
55
+ return block unless block.is_a?(FrontmatterBlock)
56
+
57
+ @transforms.reduce(block) do |current, klass|
58
+ transform = klass.new
59
+ if transform.applies?(direction: direction, format: format)
60
+ transform.apply(current)
61
+ else
62
+ current
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ class FrontmatterBlock
6
+ # OCP registry mapping `$schema` URLs to validator classes.
7
+ #
8
+ # Core ships with NO built-in validators. Downstream gems (e.g.,
9
+ # a future `coradoc-jsonschema`) register resolvers without
10
+ # modifying core code.
11
+ module SchemaResolver
12
+ # Structured validation error. Typed — never a hash bag.
13
+ ValidationError = Struct.new(:field, :message, keyword_init: true)
14
+
15
+ # Base class for schema resolvers. Override #validate in subclasses.
16
+ class Base
17
+ def validate(_block)
18
+ []
19
+ end
20
+ end
21
+
22
+ # Registry of URL → resolver class.
23
+ class Registry
24
+ DEFAULT = new
25
+
26
+ def initialize
27
+ @resolvers = {}
28
+ end
29
+
30
+ def register(schema_url, resolver_class)
31
+ @resolvers[schema_url.to_s] = resolver_class
32
+ end
33
+
34
+ def lookup(schema_url)
35
+ @resolvers[schema_url.to_s]
36
+ end
37
+
38
+ def registered?(schema_url)
39
+ @resolvers.key?(schema_url.to_s)
40
+ end
41
+
42
+ # Returns array of ValidationError structs. Empty if no schema,
43
+ # no resolver, or validation passes.
44
+ def validate(block)
45
+ return [] unless block.is_a?(FrontmatterBlock)
46
+ return [] if block.schema.nil?
47
+
48
+ resolver_class = lookup(block.schema)
49
+ return [] unless resolver_class
50
+
51
+ resolver_class.new.validate(block)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ class FrontmatterBlock
6
+ # Format-agnostic text splitter for the YAML frontmatter block
7
+ # convention (`---\n...\n---\n` at the very start of a document).
8
+ #
9
+ # Lives under FrontmatterBlock alongside Codec, SchemaResolver,
10
+ # and FieldTransform — together they form the complete frontmatter
11
+ # machinery (MECE):
12
+ #
13
+ # TextSplitter — text → (frontmatter_text, body_text)
14
+ # Codec — frontmatter_text ↔ typed FrontmatterBlock
15
+ # SchemaResolver — typed FrontmatterBlock → validation errors
16
+ # FieldTransform — typed FrontmatterBlock → transformed block
17
+ #
18
+ # Format gems (Markdown, AsciiDoc, ...) call this splitter at the
19
+ # top of their parse pipeline so frontmatter never reaches the
20
+ # format's block parser. Single source of truth (DRY).
21
+ module TextSplitter
22
+ OPEN_DELIMITER = '---'
23
+ CLOSE_DELIMITERS = %w[--- ...].freeze
24
+
25
+ # Result of splitting source text. +frontmatter+ is the raw YAML
26
+ # body (without delimiters), nil if no frontmatter was present.
27
+ # +body+ is the remaining document text.
28
+ Result = Struct.new(:frontmatter, :body, keyword_init: true) do
29
+ def frontmatter?
30
+ !frontmatter.nil? && !frontmatter.empty?
31
+ end
32
+ end
33
+
34
+ class << self
35
+ # @param text [String, nil] Source document text.
36
+ # @return [Result]
37
+ def call(text)
38
+ return Result.new(frontmatter: nil, body: '') if text.nil? || text.empty?
39
+
40
+ lines = text.lines
41
+ return empty_with(text) unless opens_frontmatter?(lines.first)
42
+
43
+ close_index = find_close_line(lines, 1)
44
+ return empty_with(text) if close_index.nil?
45
+
46
+ frontmatter = lines[1...close_index].join
47
+ body = lines[(close_index + 1)..].join
48
+ body = body.sub(/\A\n+/, '') if body.start_with?("\n")
49
+
50
+ Result.new(frontmatter: frontmatter, body: body)
51
+ end
52
+
53
+ private
54
+
55
+ def opens_frontmatter?(first_line)
56
+ return false unless first_line
57
+
58
+ first_line.strip == OPEN_DELIMITER
59
+ end
60
+
61
+ def find_close_line(lines, start_at)
62
+ start_at.upto(lines.size - 1) do |i|
63
+ return i if CLOSE_DELIMITERS.include?(lines[i].strip)
64
+ end
65
+ nil
66
+ end
67
+
68
+ def empty_with(text)
69
+ Result.new(frontmatter: nil, body: text)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module CoreModel
5
+ # First-class block representing YAML frontmatter attached to a
6
+ # document.
7
+ #
8
+ # Frontmatter is modeled as a Block (not a side-attribute on
9
+ # DocumentElement) so it flows through the standard block pipeline:
10
+ # parsers produce it, transformers dispatch on its class, serializers
11
+ # emit it. No special-casing anywhere.
12
+ #
13
+ # The +data+ hash stores the entire parsed YAML frontmatter (minus
14
+ # +$schema+, which is promoted to the +schema+ attribute). Using a
15
+ # hash — rather than a typed value tree — lets coradoc accept any
16
+ # frontmatter shape without code changes. Type handling is delegated
17
+ # to Ruby's native YAML/JSON, which already preserve Date, Integer,
18
+ # Float, Boolean, nil, Array, and Hash correctly for YAML round-trips.
19
+ #
20
+ # The +$schema+ key, if present in source YAML, is promoted to the
21
+ # +schema+ attribute (single source of truth — DRY); SchemaResolver
22
+ # reads it to find validators.
23
+ class FrontmatterBlock < Block
24
+ def self.semantic_type
25
+ :frontmatter
26
+ end
27
+
28
+ def self.element_type_name
29
+ 'frontmatter'
30
+ end
31
+
32
+ # `$schema` URL, nil-safe. Consumed by SchemaResolver registry.
33
+ attribute :schema, :string
34
+
35
+ # Entire parsed YAML frontmatter (minus `$schema`). Values are
36
+ # native Ruby types from YAML.safe_load (String, Integer, Date,
37
+ # Array, Hash, etc.). Order is preserved for round-trip fidelity.
38
+ attribute :data, :hash, default: {}
39
+
40
+ # Convenience accessor — read a single entry by key.
41
+ def entry(key)
42
+ data[key.to_s]
43
+ end
44
+
45
+ def has_entry?(key)
46
+ data.key?(key.to_s)
47
+ end
48
+
49
+ def empty?
50
+ schema.nil? && (data.nil? || data.empty?)
51
+ end
52
+
53
+ # Sub-namespaces (Codec, SchemaResolver, FieldTransform, TextSplitter)
54
+ # live under FrontmatterBlock and autoload lazily.
55
+ autoload :Codec, "#{__dir__}/frontmatter/codec"
56
+ autoload :SchemaResolver, "#{__dir__}/frontmatter/schema_resolver"
57
+ autoload :FieldTransform, "#{__dir__}/frontmatter/field_transform"
58
+ autoload :TextSplitter, "#{__dir__}/frontmatter/text_splitter"
59
+ end
60
+ end
61
+ end
@@ -12,6 +12,8 @@ module Coradoc
12
12
  # Autoload submodules lazily using relative paths
13
13
  autoload :Base, "#{__dir__}/core_model/base"
14
14
  autoload :ChildrenContent, "#{__dir__}/core_model/children_content"
15
+ autoload :Callout, "#{__dir__}/core_model/callout"
16
+ autoload :CalloutText, "#{__dir__}/core_model/callout_text"
15
17
  autoload :Block, "#{__dir__}/core_model/block"
16
18
  autoload :AnnotationBlock, "#{__dir__}/core_model/annotation_block"
17
19
  autoload :ListBlock, "#{__dir__}/core_model/list_block"
@@ -49,6 +51,7 @@ module Coradoc
49
51
  autoload :ElementAttribute, "#{__dir__}/core_model/element_attribute"
50
52
  autoload :Metadata, "#{__dir__}/core_model/metadata"
51
53
  autoload :MetadataEntry, "#{__dir__}/core_model/metadata"
54
+ autoload :FrontmatterBlock, "#{__dir__}/core_model/frontmatter"
52
55
  autoload :Footnote, "#{__dir__}/core_model/footnote"
53
56
  autoload :FootnoteReference, "#{__dir__}/core_model/footnote"
54
57
  autoload :Abbreviation, "#{__dir__}/core_model/footnote"
@@ -71,6 +74,7 @@ module Coradoc
71
74
  autoload :ReviewerBlock, "#{__dir__}/core_model/reviewer_block"
72
75
  autoload :ParagraphBlock, "#{__dir__}/core_model/paragraph_block"
73
76
  autoload :CommentBlock, "#{__dir__}/core_model/comment_block"
77
+ autoload :CommentLine, "#{__dir__}/core_model/comment_line"
74
78
  autoload :HorizontalRuleBlock, "#{__dir__}/core_model/horizontal_rule_block"
75
79
  autoload :IdGenerator, "#{__dir__}/core_model/id_generator"
76
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coradoc
4
- VERSION = '2.0.18'
4
+ VERSION = '2.0.20'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coradoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.18
4
+ version: 2.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -60,13 +60,21 @@ files:
60
60
  - lib/coradoc/core_model/bibliography.rb
61
61
  - lib/coradoc/core_model/bibliography_entry.rb
62
62
  - lib/coradoc/core_model/block.rb
63
+ - lib/coradoc/core_model/callout.rb
64
+ - lib/coradoc/core_model/callout_text.rb
63
65
  - lib/coradoc/core_model/children_content.rb
64
66
  - lib/coradoc/core_model/comment_block.rb
67
+ - lib/coradoc/core_model/comment_line.rb
65
68
  - lib/coradoc/core_model/definition_item.rb
66
69
  - lib/coradoc/core_model/definition_list.rb
67
70
  - lib/coradoc/core_model/element_attribute.rb
68
71
  - lib/coradoc/core_model/example_block.rb
69
72
  - lib/coradoc/core_model/footnote.rb
73
+ - lib/coradoc/core_model/frontmatter.rb
74
+ - lib/coradoc/core_model/frontmatter/codec.rb
75
+ - lib/coradoc/core_model/frontmatter/field_transform.rb
76
+ - lib/coradoc/core_model/frontmatter/schema_resolver.rb
77
+ - lib/coradoc/core_model/frontmatter/text_splitter.rb
70
78
  - lib/coradoc/core_model/horizontal_rule_block.rb
71
79
  - lib/coradoc/core_model/id_generator.rb
72
80
  - lib/coradoc/core_model/image.rb