coradoc-adoc 2.0.26 → 2.0.28

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: 6d381a7e511028e9caa79826eccd74e14694968a18825cd179d28cbd785928c7
4
- data.tar.gz: 0646f6bf33b86848c345fa772e23583def67169f01ae9b9a905ec0f8fa1922c7
3
+ metadata.gz: 4af9421f3be892d3f963f70789a03201faf12f95ba487207c3affb9f0dd94ba0
4
+ data.tar.gz: d1dfcfe239edf6ad09a67bf1dd9a39ac7992466e9aa979475d376b525e0472b5
5
5
  SHA512:
6
- metadata.gz: 86b395057b440b17616f1743a4d4e72f8c9cffb21b344019b695233c81445450bfd2e17bfc77cb0e706d1284267c4e399680d1174818ebae709570c485c5d435
7
- data.tar.gz: 25d8a142605cd3fe9df0dbd191c1004f4d9450ba79614a7b660cd629ef7c8673045143d4b5ed9fd7c0cdf25385d94ec615c2d2591479f41c228abe1fa16c77aa
6
+ metadata.gz: eb5d3978b3777a0b04fa51cf736fcee2cf4ce6de38bca4353eff5bd4b8cbf7dcce849f84fb60c05c2979f339fa6b182ff74662e491825a939ca29ea5424152c3
7
+ data.tar.gz: 0fc692a6bd815a8ee102f485c2724b1941fe90bffb09cfdcc066ab6ddf90479386f848fe4ad0ea63f66f8d75b12f40a53ed946a4e0343b3f9d201e860f3d042c
@@ -3,19 +3,29 @@
3
3
  module Coradoc
4
4
  module AsciiDoc
5
5
  module Parser
6
- # Single Responsibility: parse the optional header that precedes a block.
6
+ # Single Responsibility: parse the optional header that precedes a
7
+ # structural element (block or section).
7
8
  #
8
- # Encapsulates the canonical AsciiDoc block-header grammar in one place
9
- # (DRY, MECE, single source of truth). Before this module existed, every
10
- # block-like rule (block, table, section, paragraph, block_image) inlined
11
- # its own header rule with subtly different slot orderings — and several
12
- # of them captured the same Parslet key more than once in a single
13
- # sequence, which triggered Parslet's "Duplicate subtrees while merging
14
- # result" warning and silently discarded one of the captured values.
9
+ # Encapsulates the canonical AsciiDoc header grammar in one place
10
+ # (DRY, MECE, single source of truth). Two flavours live here because
11
+ # blocks and sections have different header grammars:
15
12
  #
16
- # Canonical header shape, each component at most once:
13
+ # block_header = block_title? >> element_id? >> attribute_blocks?
14
+ # section_header = element_id? >> attribute_blocks?
17
15
  #
18
- # block_title? >> element_id? >> attribute_blocks?
16
+ # Asciidoctor does NOT permit `.Title` to apply to a section — sections
17
+ # accept `[role]` attribute lists and `[[id]]` anchors but not block
18
+ # titles. The section's heading IS its title. Mixing the two shapes
19
+ # into one rule caused `section_block` to admit `.Foo` lines that
20
+ # collided with `section_title`'s `:title` capture, triggering
21
+ # Parslet's "Duplicate subtrees while merging result … (keys:
22
+ # [:title])" warning and silently dropping the block title.
23
+ #
24
+ # Before this module existed, every block-like rule inlined its own
25
+ # header rule with subtly different slot orderings — and several
26
+ # captured the same Parslet key more than once in a single sequence,
27
+ # which triggered Parslet's "Duplicate subtrees" warning and silently
28
+ # discarded one of the captured values.
19
29
  #
20
30
  # `attribute_blocks` accepts one or more consecutive `[...]` attribute
21
31
  # lists, captured as a Parslet sequence under the :attribute_list key.
@@ -31,8 +41,11 @@ module Coradoc
31
41
  # every attribute and lets the transformer merge them into a single
32
42
  # Coradoc::AsciiDoc::Model::AttributeList downstream.
33
43
  module BlockHeader
34
- # Canonical block header rule. Single canonical order; each of title,
35
- # id, and attribute_blocks is optional and matched at most once.
44
+ # Canonical block header rule. Includes the block title every
45
+ # real block (delimited block, table, paragraph, image) accepts
46
+ # an optional `.Title` line before its body. Single canonical
47
+ # order; each of title, id, and attribute_blocks is optional and
48
+ # matched at most once.
36
49
  # @return [Parslet::Atoms::Base]
37
50
  def block_header
38
51
  block_title.maybe >>
@@ -40,6 +53,16 @@ module Coradoc
40
53
  attribute_blocks.maybe
41
54
  end
42
55
 
56
+ # Section header rule. Asciidoctor does not permit `.Title` on
57
+ # sections — the section heading itself is the title. Sections
58
+ # still accept the same element_id and attribute_blocks slots
59
+ # that blocks do (`[[anchor]]`, `[appendix]`, `[role=x]`, etc.).
60
+ # @return [Parslet::Atoms::Base]
61
+ def section_header
62
+ element_id.maybe >>
63
+ attribute_blocks.maybe
64
+ end
65
+
43
66
  # One or more consecutive attribute_list + newline sequences, captured
44
67
  # as a Parslet sequence under :attribute_list. When multiple `[...]`
45
68
  # blocks precede a delimiter, all of them reach the transformer; when
@@ -4,6 +4,27 @@ module Coradoc
4
4
  module AsciiDoc
5
5
  module Parser
6
6
  module Inline
7
+ # AsciiDoc typographic quote syntax: a 2-char pattern that
8
+ # Asciidoctor substitutes with the corresponding Unicode curly
9
+ # quote. Single source of truth for the pattern → Unicode char
10
+ # mapping; the transformer reads this table.
11
+ #
12
+ # The patterns MUST be recognised before +monospace_constrained+
13
+ # in the +inline+ alternation, otherwise the lone backtick in
14
+ # +`"`+ or +`"``+ fires monospace and the surrounding quote
15
+ # collapses to straight ASCII quotes wrapped around a spurious
16
+ # code span.
17
+ TYPOGRAPHIC_QUOTE_PATTERNS = {
18
+ '"`' => "“", # U+201C left double
19
+ '`"' => "”", # U+201D right double
20
+ "'`" => "‘", # U+2018 left single
21
+ "`'" => "’" # U+2019 right single
22
+ }.freeze
23
+
24
+ def typographic_quote
25
+ (str('"`') | str('`"') | str("'`") | str("`'")).as(:typographic_quote)
26
+ end
27
+
7
28
  def attribute_reference
8
29
  str('{').present? >> str('{') >>
9
30
  match('[a-zA-Z0-9_-]').repeat(1).as(:attribute_reference) >>
@@ -170,6 +191,7 @@ module Coradoc
170
191
 
171
192
  def inline_chars?
172
193
  match('[\[*#_{<^~`]').present? |
194
+ typographic_quote.present? |
173
195
  str('http').present? |
174
196
  str('https').present? |
175
197
  str('link:').present? |
@@ -198,7 +220,8 @@ module Coradoc
198
220
  end
199
221
 
200
222
  def inline
201
- bold_unconstrained |
223
+ typographic_quote |
224
+ bold_unconstrained |
202
225
  bold_constrained |
203
226
  span_unconstrained |
204
227
  span_constrained |
@@ -20,13 +20,13 @@ module Coradoc
20
20
  def ordered_list(nesting_level = 1)
21
21
  attrs = (attribute_list >> newline).maybe
22
22
  r = olist_item(nesting_level)
23
- attrs >> olist_item(nesting_level).present? >> r.repeat(1).as(:ordered)
23
+ attrs >> (empty_line.repeat(0) >> r).repeat(1).as(:ordered)
24
24
  end
25
25
 
26
26
  def unordered_list(nesting_level = 1)
27
27
  attrs = (attribute_list >> newline).maybe
28
28
  r = ulist_item(nesting_level)
29
- attrs >> r.repeat(1).as(:unordered)
29
+ attrs >> (empty_line.repeat(0) >> r).repeat(1).as(:unordered)
30
30
  end
31
31
 
32
32
  def definition_list(_delimiter = nil)
@@ -53,7 +53,7 @@ module Coradoc
53
53
  def olist_item(nesting_level = 1)
54
54
  item = olist_marker(nesting_level).as(:marker) >>
55
55
  match("\n").absent? >> space >>
56
- (text_line(true, unguarded: true) >>
56
+ (text_line(false, unguarded: true) >>
57
57
  list_item_continuation_lines).as(:lines)
58
58
 
59
59
  att = (list_continuation.present? >>
@@ -105,7 +105,7 @@ module Coradoc
105
105
  item = ulist_marker(nesting_level).as(:marker) >>
106
106
  str(' [[[').absent? >>
107
107
  match("\n").absent? >> space >>
108
- (text_line(true, unguarded: true) >>
108
+ (text_line(false, unguarded: true) >>
109
109
  list_item_continuation_lines).as(:lines)
110
110
 
111
111
  att = (list_continuation.present? >>
@@ -127,8 +127,15 @@ module Coradoc
127
127
  # list marker, block delimiter, attribute list, section, element
128
128
  # id, table boundary, list continuation, or list prefix).
129
129
  # `line_not_text?` (from Paragraph) is that exact lookahead.
130
+ #
131
+ # Each iteration matches exactly one source line via
132
+ # `text_line(false, ...)` (single-newline termination). The
133
+ # `.repeat(0)` walks multiple continuation lines one at a time.
134
+ # Using `text_line(true, ...)` here would let `line_ending.repeat(1)`
135
+ # greedy-match across blank lines, silently absorbing the
136
+ # follow-up paragraph into the last list item's content.
130
137
  def list_item_continuation_lines
131
- (line_not_text? >> text_line(true, unguarded: true)).repeat(0)
138
+ (line_not_text? >> text_line(false, unguarded: true)).repeat(0)
132
139
  end
133
140
 
134
141
  def dlist_delimiter
@@ -27,7 +27,7 @@ module Coradoc
27
27
  def section_block(level = 2)
28
28
  return nil if level > 8
29
29
 
30
- block_header >>
30
+ section_header >>
31
31
  section_title(level).as(:title) >>
32
32
  contents.as(:contents).maybe
33
33
  end
@@ -12,6 +12,7 @@ module Coradoc
12
12
  children = ToCoreModel.transform(doc.sections || doc.contents || [])
13
13
  children = CalloutMerger.call(children)
14
14
  children = prepend_frontmatter(children, doc.frontmatter)
15
+ children = insert_title_heading_after_frontmatter(children, title_text, doc.id)
15
16
  children = resolve_attribute_references(children, attributes)
16
17
 
17
18
  Coradoc::CoreModel::DocumentElement.new(
@@ -33,6 +34,41 @@ module Coradoc
33
34
  [block, *children]
34
35
  end
35
36
 
37
+ # Per the AsciiDoc spec, the document title (`= Title`) is the
38
+ # document's level-0 heading. Asciidoctor renders it as an
39
+ # <h1> at the top of the body by default. Emit a HeaderElement
40
+ # at level 0 as the first body child (after any FrontmatterBlock)
41
+ # so consumers that walk the body's children see the title —
42
+ # the standard ProseMirror/HTML rendering pattern — instead of
43
+ # having to read DocumentElement.title separately.
44
+ #
45
+ # The title attribute on DocumentElement is preserved for
46
+ # consumers that read it directly. The FromCoreModel Document
47
+ # transformer consumes the HeaderElement when constructing
48
+ # Model::Header so AsciiDoc round-trip does not double-render.
49
+ def insert_title_heading_after_frontmatter(children, title_text, doc_id)
50
+ return children if title_text.nil? || title_text.strip.empty?
51
+ return children if children.any? { |c| title_heading?(c) }
52
+
53
+ title_id = doc_id || Coradoc::CoreModel::IdGenerator
54
+ .generate_from_title(title_text)
55
+ title_heading = Coradoc::CoreModel::HeaderElement.new(
56
+ level: 0,
57
+ title: title_text,
58
+ content: title_text,
59
+ id: title_id
60
+ )
61
+ frontmatter_count = children.count do |child|
62
+ child.is_a?(Coradoc::CoreModel::FrontmatterBlock)
63
+ end
64
+ children.insert(frontmatter_count, title_heading)
65
+ end
66
+
67
+ def title_heading?(child)
68
+ child.is_a?(Coradoc::CoreModel::HeaderElement) &&
69
+ child.level.to_i.zero?
70
+ end
71
+
36
72
  # Resolve `{name}` attribute references in the body against the
37
73
  # document's own declared attributes. Single source of truth:
38
74
  # the resolver lives in core so other format gems can reuse it
@@ -27,23 +27,24 @@ module Coradoc
27
27
  def transform_structural_element(element)
28
28
  case element
29
29
  when CoreModel::DocumentElement
30
- header = if element.title
31
- Coradoc::AsciiDoc::Model::Header.new(
32
- title: Coradoc::AsciiDoc::Model::Title.new(
33
- content: element.title,
34
- level_int: 0
35
- )
36
- )
37
- else
38
- Coradoc::AsciiDoc::Model::Header.new(title: '')
39
- end
40
-
41
30
  without_frontmatter, frontmatter = extract_frontmatter(Array(element.children))
31
+ without_title_heading, title_text = extract_title_heading(without_frontmatter, element.title)
32
+
33
+ header = if title_text
34
+ Coradoc::AsciiDoc::Model::Header.new(
35
+ title: Coradoc::AsciiDoc::Model::Title.new(
36
+ content: title_text,
37
+ level_int: 0
38
+ )
39
+ )
40
+ else
41
+ Coradoc::AsciiDoc::Model::Header.new(title: '')
42
+ end
42
43
 
43
44
  Coradoc::AsciiDoc::Model::Document.new(
44
45
  id: element.id,
45
46
  header: header,
46
- sections: flatten_children(without_frontmatter),
47
+ sections: flatten_children(without_title_heading),
47
48
  frontmatter: frontmatter
48
49
  )
49
50
  when CoreModel::SectionElement
@@ -456,6 +457,26 @@ module Coradoc
456
457
  [children.drop(1), yaml.nil? || yaml.empty? ? nil : yaml]
457
458
  end
458
459
 
460
+ # Pull the level-0 HeaderElement (the document title) out of the
461
+ # body so the AsciiDoc serializer can render it as `= Title`
462
+ # via Model::Header instead of double-rendering it as a body
463
+ # section. Returns [remaining_children, title_text]. Falls
464
+ # back to +fallback_title+ when no HeaderElement is present
465
+ # (preserves backward compatibility with callers that build
466
+ # DocumentElement programmatically and put the title only on
467
+ # DocumentElement#title rather than in the children list).
468
+ def extract_title_heading(children, fallback_title)
469
+ title_idx = children.index do |c|
470
+ c.is_a?(CoreModel::HeaderElement) && c.level.to_i.zero?
471
+ end
472
+ return [children, fallback_title] unless title_idx
473
+
474
+ heading = children[title_idx]
475
+ title_text = heading.title || fallback_title
476
+ remaining = children[0...title_idx] + children[(title_idx + 1)..]
477
+ [remaining, title_text]
478
+ end
479
+
459
480
  def resolve_semantic_type(block)
460
481
  semantic = block.resolve_semantic_type
461
482
  return semantic if semantic
@@ -67,6 +67,19 @@ module Coradoc
67
67
  )
68
68
  end
69
69
 
70
+ # Typographic quotes — Asciidoctor's curly-quote substitution.
71
+ # Each 2-char pattern collapses to a single Unicode character
72
+ # at parse time so downstream consumers (HTML, Markdown) see
73
+ # the literal typographic char without any post-processing.
74
+ # The mapping is the single source of truth declared on the
75
+ # parser side (Parser::Inline::TYPOGRAPHIC_QUOTE_PATTERNS).
76
+ rule(typographic_quote: simple(:pattern)) do
77
+ char = Coradoc::AsciiDoc::Parser::Inline::TYPOGRAPHIC_QUOTE_PATTERNS.fetch(
78
+ pattern.to_s, pattern.to_s
79
+ )
80
+ Model::TextElement.new(content: char)
81
+ end
82
+
70
83
  # Hard line break (` +\n` or `\\n`). Emitted as a dedicated
71
84
  # AsciiDoc model (Inline::HardLineBreak) distinct from
72
85
  # Model::LineBreak, which only represents paragraph-separator
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module AsciiDoc
5
- VERSION = '2.0.26'
5
+ VERSION = '2.0.28'
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.26
4
+ version: 2.0.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.