coradoc 2.0.18 → 2.0.19

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: dfb7ecfe3eb593e8a88dc23950b2e75fb5bafd4ea8f31ee7840c44e1cdaadd28
4
+ data.tar.gz: 1835d0ac0defa944dc45d26c967d3e9ddb0d20a599f79c85657531689c67faf0
5
5
  SHA512:
6
- metadata.gz: 55e37e38878cc8728ed4d12cebfac1fd370fe19c1e69339a10a3de9492cde1f096a75ee6e49917869440c8c72b90109281cfd392e19741f56d9bd3cfd968319b
7
- data.tar.gz: 5d8c7ca506943cad23f79c61c511666b2cd466324dd849d2c267a381f8f119f3d956f50c6c26fa879664a3d49e7467c057500ff7d4a8515edb0574929d718815
6
+ metadata.gz: b1a73e74172ae6af0dd2a5426f520b546170c2a4f00910f91dd45211bccf448ca187d45b64ba94030549331008a50adaff283ca1ced17c39bb164d6a6cd9a032
7
+ data.tar.gz: 57efec479b5c118c4c18d99d7e0b0090046ef1c42ed17b265804f044467775fb82781bec4a7c3a119b24a428ff46ec50c642e4557488471447f5e96af48d4572
@@ -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.
@@ -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
@@ -49,6 +49,7 @@ module Coradoc
49
49
  autoload :ElementAttribute, "#{__dir__}/core_model/element_attribute"
50
50
  autoload :Metadata, "#{__dir__}/core_model/metadata"
51
51
  autoload :MetadataEntry, "#{__dir__}/core_model/metadata"
52
+ autoload :FrontmatterBlock, "#{__dir__}/core_model/frontmatter"
52
53
  autoload :Footnote, "#{__dir__}/core_model/footnote"
53
54
  autoload :FootnoteReference, "#{__dir__}/core_model/footnote"
54
55
  autoload :Abbreviation, "#{__dir__}/core_model/footnote"
@@ -71,6 +72,7 @@ module Coradoc
71
72
  autoload :ReviewerBlock, "#{__dir__}/core_model/reviewer_block"
72
73
  autoload :ParagraphBlock, "#{__dir__}/core_model/paragraph_block"
73
74
  autoload :CommentBlock, "#{__dir__}/core_model/comment_block"
75
+ autoload :CommentLine, "#{__dir__}/core_model/comment_line"
74
76
  autoload :HorizontalRuleBlock, "#{__dir__}/core_model/horizontal_rule_block"
75
77
  autoload :IdGenerator, "#{__dir__}/core_model/id_generator"
76
78
  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.19'
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.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -62,11 +62,17 @@ files:
62
62
  - lib/coradoc/core_model/block.rb
63
63
  - lib/coradoc/core_model/children_content.rb
64
64
  - lib/coradoc/core_model/comment_block.rb
65
+ - lib/coradoc/core_model/comment_line.rb
65
66
  - lib/coradoc/core_model/definition_item.rb
66
67
  - lib/coradoc/core_model/definition_list.rb
67
68
  - lib/coradoc/core_model/element_attribute.rb
68
69
  - lib/coradoc/core_model/example_block.rb
69
70
  - lib/coradoc/core_model/footnote.rb
71
+ - lib/coradoc/core_model/frontmatter.rb
72
+ - lib/coradoc/core_model/frontmatter/codec.rb
73
+ - lib/coradoc/core_model/frontmatter/field_transform.rb
74
+ - lib/coradoc/core_model/frontmatter/schema_resolver.rb
75
+ - lib/coradoc/core_model/frontmatter/text_splitter.rb
70
76
  - lib/coradoc/core_model/horizontal_rule_block.rb
71
77
  - lib/coradoc/core_model/id_generator.rb
72
78
  - lib/coradoc/core_model/image.rb