coradoc-adoc 2.0.10 → 2.0.12

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: 78eaf011433933b47142179dd46e5e1176fe7f0686b2e803b1c35a0ed8f0fc93
4
- data.tar.gz: 39f23d77a5df2807f35e895e751bbfa0e4030e34a6d1c6372007061fa211f07d
3
+ metadata.gz: 8e7f028d984f0019578e97882169ce2be502e17fcfdeb376766f088d1af23958
4
+ data.tar.gz: 68533e4e500d008b4fc37f1beecc1fd160318adb6f9a6ed43fdf87a97ffedb31
5
5
  SHA512:
6
- metadata.gz: 358a5c0d2562840abc484421446110c917112f68515eb22c9bd1d69c6cafe18832a1ffb3a71c37df8640b309e4aa9ec7b7e544393ed795ee63b894c7a1bd504d
7
- data.tar.gz: aed852abae11e22ef32fbb764f9c09d2334c24ef3f1e0c9bd23fe4c5295ddf2b15adeca603713f732a3ffdb2250bf3a9def286fd6ecf563c9dc5f840cc251f23
6
+ metadata.gz: 6c386bcee2e9a2bc1f3e47f2fca189b92bb7ef568fdb5105c2b59da916a43cc27bf912699c92e850c4f782e3e7575c5472a42262cb57a8da63ba14a5d1de88a3
7
+ data.tar.gz: 45e2116e5532d41b0f76a855c12e51066d26b648b947f3b6a95f817c7772794d0af57ee1b57fb402e7579d9b620e2eb61d44c99962b1030d6de74319f27920ff
@@ -28,7 +28,13 @@ module Coradoc
28
28
  # admonition.content = "Be careful!"
29
29
  #
30
30
  class Admonition < Attached
31
- attribute :content, :string
31
+ attribute :content,
32
+ Lutaml::Model::Serializable,
33
+ collection: true,
34
+ polymorphic: [
35
+ Lutaml::Model::Type::String,
36
+ Coradoc::AsciiDoc::Model::TextElement
37
+ ]
32
38
  attribute :type, :string
33
39
  attribute :line_break, :string, default: -> { '' }
34
40
  end
@@ -36,9 +36,12 @@ module Coradoc
36
36
  # Coerce a raw parser AST value into the canonical ref_text string.
37
37
  # Accepts the shapes produced by Parser::Bibliography for `:ref_text`:
38
38
  # nil, Parslet::Slice, plain String, single Model::Base, or an Array
39
- # of any of these. Keeping this coercion on the model that owns
40
- # ref_text (rather than in a transformer rule) keeps the transformer
41
- # declarative and lets callers build entries from any source shape.
39
+ # of any of these. Model objects (TextElement, Inline::Italic, etc.)
40
+ # are flattened via TextExtractVisitor so their text content is
41
+ # preserved instead of leaking `#<Class:0x...>` inspect strings.
42
+ # Keeping this coercion on the model that owns ref_text (rather than
43
+ # in a transformer rule) keeps the transformer declarative and lets
44
+ # callers build entries from any source shape.
42
45
  # @param raw [Object, nil]
43
46
  # @return [String]
44
47
  def self.coerce_ref_text(raw)
@@ -47,6 +50,8 @@ module Coradoc
47
50
  case raw
48
51
  when Array then raw.map { |e| coerce_ref_text(e) }.join
49
52
  when String then raw
53
+ when Coradoc::AsciiDoc::Model::Base
54
+ Coradoc::AsciiDoc::Transform::TextExtractVisitor.new.extract(raw).to_s
50
55
  else raw.to_s
51
56
  end
52
57
  end
@@ -15,7 +15,7 @@ module Coradoc
15
15
 
16
16
  def admonition_line
17
17
  admonition_type.as(:admonition_type) >> str(': ') >>
18
- (text.as(:text) >>
18
+ (text_any.as(:text) >>
19
19
  line_ending.as(:line_break)
20
20
  ).repeat(1)
21
21
  .as(:content)
@@ -104,11 +104,22 @@ module Coradoc
104
104
  delim_str = c.captures[capture_key].to_s.strip
105
105
  closing_pattern = str(delim_str) >> newline
106
106
 
107
- content = block_image
108
- content |= block(n_deep - 1) if n_deep.positive?
109
- content |= list
110
- content |= text_line(false, unguarded: true, verbatim: verbatim)
111
- content |= empty_line.as(:line_break)
107
+ # Verbatim blocks (source/listing) treat their body as literal
108
+ # text per the AsciiDoc spec — no substitutions, no nested
109
+ # blocks. Allowing nested block parsing here would consume
110
+ # shorter inner delimiters (e.g. `----` inside `------`) and
111
+ # strip the original structure when serializing back.
112
+ content = if verbatim
113
+ text_line(false, unguarded: true, verbatim: true) |
114
+ empty_line.as(:line_break)
115
+ else
116
+ c = block_image
117
+ c |= block(n_deep - 1) if n_deep.positive?
118
+ c |= list
119
+ c |= text_line(false, unguarded: true)
120
+ c |= empty_line.as(:line_break)
121
+ c
122
+ end
112
123
 
113
124
  (closing_pattern.absent? >> content).repeat(1)
114
125
  end
@@ -24,7 +24,7 @@ module Coradoc
24
24
  # :zero :one :many
25
25
  def text_line(many_breaks = false, unguarded: false, verbatim: false)
26
26
  tl = if verbatim
27
- text_any.as(:text)
27
+ raw_verbatim_line.as(:text)
28
28
  elsif unguarded
29
29
  literal_space? >> text_any.as(:text)
30
30
  else
@@ -38,6 +38,15 @@ module Coradoc
38
38
  end
39
39
  end
40
40
 
41
+ # A verbatim source/listing line: capture every character up to the
42
+ # next newline without applying any inline substitutions. Source and
43
+ # listing blocks must round-trip their text untouched — otherwise
44
+ # sequences like Liquid `{{ var }}` collide with AsciiDoc attribute
45
+ # references and get rewritten.
46
+ def raw_verbatim_line
47
+ (line_ending.absent? >> any).repeat(1)
48
+ end
49
+
41
50
  def asciidoc_char
42
51
  line_start? >> match['*_:+=\-']
43
52
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Transform
6
+ # Post-processing pass that merges AsciiDoc callout annotation
7
+ # paragraphs into the verbatim block they annotate.
8
+ #
9
+ # AsciiDoc callouts look like:
10
+ #
11
+ # [source,ruby]
12
+ # ----
13
+ # get '/hi' do <1>
14
+ # ----
15
+ # <1> Returns hello world
16
+ #
17
+ # The parser emits the source block and the annotation as two
18
+ # independent children. The CoreModel representation should attach
19
+ # the annotation to the block as a typed Callout, so downstream
20
+ # serializers can render them appropriately for each format.
21
+ #
22
+ # Single responsibility: take a flat list of transformed CoreModel
23
+ # children, return a flat list with `<N>` paragraphs adjacent to a
24
+ # SourceBlock / ListingBlock folded into that block's `callouts`.
25
+ # Anything else is passed through untouched.
26
+ class CalloutMerger
27
+ ANNOTATION_LINE = /<(\d+)>\s*(.*?)\s*\z/
28
+ ANNOTATION_SPLIT = /(?=<\d+>)/
29
+
30
+ class << self
31
+ def call(children)
32
+ new.merge(Array(children))
33
+ end
34
+ end
35
+
36
+ # Walks the input children left-to-right. When a paragraph whose
37
+ # content is composed entirely of `<N> text` lines follows a
38
+ # verbatim block (SourceBlock or ListingBlock), each `<N>` line
39
+ # becomes a Callout attached to that block instead of a separate
40
+ # paragraph.
41
+ #
42
+ # Annotations that do not follow a verbatim block are preserved
43
+ # verbatim — they may be legitimate prose.
44
+ def merge(children)
45
+ children.each.with_object([]) do |child, result|
46
+ annotations = extract_annotations(child)
47
+ target = annotations && preceding_verbatim_block(result)
48
+ if target
49
+ target.callouts.concat(annotations)
50
+ else
51
+ result << child
52
+ end
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Returns an Array of Callout if the paragraph is entirely
59
+ # callout annotations, otherwise nil.
60
+ def extract_annotations(child)
61
+ return nil unless child.is_a?(Coradoc::CoreModel::ParagraphBlock)
62
+
63
+ lines = split_annotation_lines(child.flat_text)
64
+ return nil if lines.empty?
65
+
66
+ callouts = lines.map do |line|
67
+ match = line.match(ANNOTATION_LINE)
68
+ match ? Coradoc::CoreModel::Callout.new(index: match[1].to_i, content: match[2]) : nil
69
+ end
70
+ return nil if callouts.any?(&:nil?)
71
+
72
+ callouts
73
+ end
74
+
75
+ # Splits a paragraph into candidate annotation lines. Returns []
76
+ # if any non-blank segment fails to look like an annotation.
77
+ def split_annotation_lines(text)
78
+ return [] if text.nil? || text.strip.empty?
79
+
80
+ chunks = text.strip.split(ANNOTATION_SPLIT)
81
+ chunks.map(&:strip).reject(&:empty?)
82
+ end
83
+
84
+ def preceding_verbatim_block(result)
85
+ last = result.last
86
+ return nil unless last.is_a?(Coradoc::CoreModel::SourceBlock) ||
87
+ last.is_a?(Coradoc::CoreModel::ListingBlock)
88
+
89
+ last
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -10,6 +10,7 @@ module Coradoc
10
10
  title_text = ToCoreModel.extract_title_text(doc.header&.title)
11
11
  attributes = ToCoreModel.extract_document_attributes(doc)
12
12
  children = ToCoreModel.transform(doc.sections || doc.contents || [])
13
+ children = CalloutMerger.call(children)
13
14
  children = prepend_frontmatter(children, doc.frontmatter)
14
15
 
15
16
  Coradoc::CoreModel::DocumentElement.new(
@@ -38,6 +39,7 @@ module Coradoc
38
39
  )
39
40
 
40
41
  content_children = ToCoreModel.transform(section.contents || [])
42
+ content_children = CalloutMerger.call(content_children)
41
43
  nested_sections = (section.sections || []).map do |child|
42
44
  transform_section(child, parent_id: section_id)
43
45
  end
@@ -45,7 +45,7 @@ module Coradoc
45
45
  Coradoc::AsciiDoc::Model::Document.new(
46
46
  id: element.id,
47
47
  header: header,
48
- sections: transform(sections),
48
+ sections: flatten_children(sections),
49
49
  frontmatter: frontmatter
50
50
  )
51
51
  when CoreModel::SectionElement
@@ -53,17 +53,29 @@ module Coradoc
53
53
  id: element.id,
54
54
  level: element.level,
55
55
  title: create_title(element.title, element.level),
56
- contents: transform(element.children)
56
+ contents: flatten_children(element.children)
57
57
  )
58
58
  else
59
59
  Coradoc::AsciiDoc::Model::Section.new(
60
60
  id: element.id,
61
61
  title: create_title(element.title, 1),
62
- contents: transform(element.children)
62
+ contents: flatten_children(element.children)
63
63
  )
64
64
  end
65
65
  end
66
66
 
67
+ # Transforms each CoreModel child and flattens one level so a
68
+ # transform that returns multiple siblings (e.g. a source block
69
+ # followed by its re-expanded callout paragraphs) stays in
70
+ # document order.
71
+ def flatten_children(children)
72
+ Array(children).flat_map { |child| flatten_one(transform(child)) }
73
+ end
74
+
75
+ def flatten_one(result)
76
+ result.is_a?(Array) ? result : [result]
77
+ end
78
+
67
79
  def transform_block(block)
68
80
  content = block.renderable_content
69
81
 
@@ -82,7 +94,13 @@ module Coradoc
82
94
  end
83
95
 
84
96
  content_text = safe_content_to_string(content)
97
+ result = build_verbatim_block(semantic, block, content_text)
98
+ return result unless verbatim_with_callouts?(semantic, block)
85
99
 
100
+ [result, *build_callout_paragraphs(block.callouts)]
101
+ end
102
+
103
+ def build_verbatim_block(semantic, block, content_text)
86
104
  case semantic
87
105
  when :source_code
88
106
  Coradoc::AsciiDoc::Model::Block::SourceCode.new(
@@ -163,6 +181,23 @@ module Coradoc
163
181
  end
164
182
  end
165
183
 
184
+ def verbatim_with_callouts?(semantic, block)
185
+ return false unless %i[source_code listing].include?(semantic)
186
+ return false if block.callouts.nil? || block.callouts.empty?
187
+
188
+ true
189
+ end
190
+
191
+ # Re-expands typed Callouts back into the AsciiDoc `<N> text`
192
+ # paragraph form so the round-trip is faithful.
193
+ def build_callout_paragraphs(callouts)
194
+ callouts.sort_by { |c| c.index || Float::INFINITY }.map do |callout|
195
+ Coradoc::AsciiDoc::Model::Paragraph.new(
196
+ content: create_text_elements("<#{callout.index}> #{callout.content}")
197
+ )
198
+ end
199
+ end
200
+
166
201
  def transform_table(table)
167
202
  rows = Array(table.rows).map do |row|
168
203
  columns = Array(row.cells).map do |cell|
@@ -13,6 +13,7 @@ module Coradoc
13
13
  autoload :InlineTransformVisitor, "#{__dir__}/transform/inline_transform_visitor"
14
14
  autoload :ElementTransformers, "#{__dir__}/transform/element_transformers"
15
15
  autoload :FrontmatterAttributeMap, "#{__dir__}/transform/frontmatter_attribute_map"
16
+ autoload :CalloutMerger, "#{__dir__}/transform/callout_merger"
16
17
  end
17
18
  end
18
19
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module AsciiDoc
5
- VERSION = '2.0.10'
5
+ VERSION = '2.0.12'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coradoc-adoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.10
4
+ version: 2.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -313,6 +313,7 @@ files:
313
313
  - lib/coradoc/asciidoc/serializer/serializers/video.rb
314
314
  - lib/coradoc/asciidoc/serializer/spacing_strategy.rb
315
315
  - lib/coradoc/asciidoc/transform.rb
316
+ - lib/coradoc/asciidoc/transform/callout_merger.rb
316
317
  - lib/coradoc/asciidoc/transform/element_transformers.rb
317
318
  - lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb
318
319
  - lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb