coradoc-adoc 2.0.27 → 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: ac328212768fb69c29726e260db2af252ebb60fff81682bb7eed8ad9984bee1a
4
- data.tar.gz: 4b5af72762fcbf96fe40594cd4b849e328b2fe03cc027aa37e08435869656c1c
3
+ metadata.gz: 4af9421f3be892d3f963f70789a03201faf12f95ba487207c3affb9f0dd94ba0
4
+ data.tar.gz: d1dfcfe239edf6ad09a67bf1dd9a39ac7992466e9aa979475d376b525e0472b5
5
5
  SHA512:
6
- metadata.gz: 26ad434a7f144350750011ccca8a932c193ed9687c001080ce6549edcbdb1c16f2d072eb3d62dd0fa825eb744707beae7cb24be4876e03ac1054b2dffa2eaecf
7
- data.tar.gz: bf54821072c38fc983b68be86fca0d0cd46b6358b6b2239d84697cff44aadf3ab4cc5cc88a03760b29d78929568c874367a90cb9ad9c0d3c6405fa512795ea05
6
+ metadata.gz: eb5d3978b3777a0b04fa51cf736fcee2cf4ce6de38bca4353eff5bd4b8cbf7dcce849f84fb60c05c2979f339fa6b182ff74662e491825a939ca29ea5424152c3
7
+ data.tar.gz: 0fc692a6bd815a8ee102f485c2724b1941fe90bffb09cfdcc066ab6ddf90479386f848fe4ad0ea63f66f8d75b12f40a53ed946a4e0343b3f9d201e860f3d042c
@@ -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
@@ -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.27'
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.27
4
+ version: 2.0.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.