coradoc-adoc 2.0.35 → 2.0.36
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 +4 -4
- data/grammar/coradoc-adoc.artifact.json +1 -0
- data/grammar/coradoc-adoc.parg +396 -0
- data/lib/coradoc/asciidoc/formatter.rb +75 -0
- data/lib/coradoc/asciidoc/lint/document_title.rb +26 -0
- data/lib/coradoc/asciidoc/lint/duplicate_ids.rb +36 -0
- data/lib/coradoc/asciidoc/lint/empty_section.rb +45 -0
- data/lib/coradoc/asciidoc/lint/heading_levels.rb +44 -0
- data/lib/coradoc/asciidoc/lint.rb +27 -0
- data/lib/coradoc/asciidoc/model/text_element.rb +18 -5
- data/lib/coradoc/asciidoc/parser/base.rb +28 -11
- data/lib/coradoc/asciidoc/parser/citation.rb +10 -0
- data/lib/coradoc/asciidoc/parser/inline.rb +8 -4
- data/lib/coradoc/asciidoc/parser/list.rb +19 -2
- data/lib/coradoc/asciidoc/parser/paragraph.rb +1 -1
- data/lib/coradoc/asciidoc/parser/positioned_string.rb +40 -0
- data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +3 -2
- data/lib/coradoc/asciidoc/parser/tree_materializer.rb +28 -0
- data/lib/coradoc/asciidoc/parser.rb +2 -0
- data/lib/coradoc/asciidoc/serializer/serializers/block/source_code.rb +3 -1
- data/lib/coradoc/asciidoc/serializer/serializers/list/item.rb +4 -0
- data/lib/coradoc/asciidoc/serializer/serializers/table.rb +8 -2
- data/lib/coradoc/asciidoc/serializer/serializers/text_element.rb +3 -1
- data/lib/coradoc/asciidoc/transform/from_core_model/list_building.rb +86 -0
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +100 -29
- data/lib/coradoc/asciidoc/transform/frontmatter_attribute_map.rb +3 -2
- data/lib/coradoc/asciidoc/transform/text_extract_visitor.rb +5 -2
- data/lib/coradoc/asciidoc/transform/to_core_model.rb +12 -2
- data/lib/coradoc/asciidoc/transform.rb +1 -0
- data/lib/coradoc/asciidoc/transformer/source_line_extractor.rb +1 -1
- data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +18 -7
- data/lib/coradoc/asciidoc/version.rb +1 -1
- data/lib/coradoc/asciidoc.rb +12 -0
- data/lib/coradoc/util/asciidoc.rb +56 -0
- metadata +16 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
# AsciiDoc lint rules (#165). Each rule checks the AsciiDoc model
|
|
6
|
+
# tree in a model-driven way — no text scanning. Rules self-register
|
|
7
|
+
# into Coradoc::Lint.registry when this file is required; adding a
|
|
8
|
+
# rule is adding one file under lint/ plus its require here.
|
|
9
|
+
#
|
|
10
|
+
# NOTE: a "list starts deeper than level 1" rule is structurally
|
|
11
|
+
# unobservable here — the parser canonicalizes depth markers
|
|
12
|
+
# (#285), so a lone '**' list already reads as level 1 on the
|
|
13
|
+
# model tree. That concern lives in the parser/serializer, not
|
|
14
|
+
# in a lint.
|
|
15
|
+
module Lint
|
|
16
|
+
require_relative 'lint/heading_levels'
|
|
17
|
+
require_relative 'lint/document_title'
|
|
18
|
+
require_relative 'lint/empty_section'
|
|
19
|
+
require_relative 'lint/duplicate_ids'
|
|
20
|
+
|
|
21
|
+
RULES = [HeadingLevels, DocumentTitle, EmptySection,
|
|
22
|
+
DuplicateIds].freeze
|
|
23
|
+
|
|
24
|
+
RULES.each { |rule| Coradoc::Lint.registry.register(rule) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -40,14 +40,27 @@ module Coradoc
|
|
|
40
40
|
line_break.to_s.empty?
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Escape whole runs of '*'/'_' that touch whitespace on either
|
|
44
|
+
# side. Run-aware two-pass scan — the lookaround + alternation
|
|
45
|
+
# + quantifier regex is modeled as polynomial-redos by CodeQL.
|
|
43
46
|
def self.escape_keychars(string)
|
|
44
47
|
subs = { '*' => '\*', '_' => '\_' }
|
|
45
|
-
string
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
chars = string.chars
|
|
49
|
+
escaped = Array.new(chars.length, false)
|
|
50
|
+
i = 0
|
|
51
|
+
while i < chars.length
|
|
52
|
+
unless subs.key?(chars[i])
|
|
53
|
+
i += 1
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
j = i
|
|
57
|
+
j += 1 while j < chars.length && subs.key?(chars[j])
|
|
58
|
+
touches = (i.positive? && chars[i - 1].match?(/\s/)) ||
|
|
59
|
+
(j < chars.length && chars[j].match?(/\s/))
|
|
60
|
+
(i...j).each { |k| escaped[k] = true } if touches
|
|
61
|
+
i = j
|
|
50
62
|
end
|
|
63
|
+
chars.each_with_index.map { |ch, k| escaped[k] ? subs[ch] : ch }.join
|
|
51
64
|
end
|
|
52
65
|
|
|
53
66
|
# Get text content as a string. Handles the polymorphic shape of
|
|
@@ -85,17 +85,16 @@ module Coradoc
|
|
|
85
85
|
warn e.parse_failure_cause.ascii_tree
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
# The
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
super(string, **options, mode: options.fetch(:mode, :ruby))
|
|
88
|
+
# The compiled PARG artifact (grammar/coradoc-adoc.parg, the
|
|
89
|
+
# single source of truth) is the parse backend; the Ruby-DSL
|
|
90
|
+
# grammar defined by this class remains the parity reference
|
|
91
|
+
# (parg_parity_spec + engine_differential_spec assert the two
|
|
92
|
+
# produce identical trees). The artifact routes through
|
|
93
|
+
# parsanol's compiled-program fast lane (parsanol-ruby#162,
|
|
94
|
+
# 1.3.81) and parses native by default; the historical
|
|
95
|
+
# ruby-mode artifact divergence is fixed (#185, 1.3.82).
|
|
96
|
+
def parse(string, **)
|
|
97
|
+
GrammarBackend.parse(string, **)
|
|
99
98
|
end
|
|
100
99
|
|
|
101
100
|
def rule_dispatch(rule_name, *, **)
|
|
@@ -111,6 +110,24 @@ module Coradoc
|
|
|
111
110
|
end
|
|
112
111
|
end
|
|
113
112
|
|
|
113
|
+
# Lazily-loaded compiled-grammar backend (the shipped artifact).
|
|
114
|
+
module GrammarBackend
|
|
115
|
+
class << self
|
|
116
|
+
def parse(string, **)
|
|
117
|
+
TreeMaterializer.convert(artifact.parse('document', string, **))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def artifact
|
|
123
|
+
require 'parsanol/parg'
|
|
124
|
+
@artifact ||= Parsanol::PARG::Artifact.load(
|
|
125
|
+
File.expand_path('../../../../grammar/coradoc-adoc.artifact.json', __dir__)
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
114
131
|
# Wrap every parser rule for Parsanol memoization. Must run after all
|
|
115
132
|
# parser modules are included in Base so that instance_method(rule_name)
|
|
116
133
|
# finds the methods defined by every module.
|
|
@@ -22,6 +22,16 @@ module Coradoc
|
|
|
22
22
|
def escaped_xref
|
|
23
23
|
str('\\') >> str('<<').as(:text)
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# General AsciiDoc escape: \X (X punctuation, minus < for
|
|
27
|
+
# escaped_xref and \ itself) yields the literal character X
|
|
28
|
+
# without firing any inline rule (#281).
|
|
29
|
+
def escape_char
|
|
30
|
+
# Explicit class (all ASCII punctuation except < for
|
|
31
|
+
# escaped_xref and \ itself): the native engine does not
|
|
32
|
+
# compile && class intersections.
|
|
33
|
+
str('\\') >> match("[!\"#$%&'()*+,\\-./:;=?@\\[\\]^_`{|}~]").as(:text)
|
|
34
|
+
end
|
|
25
35
|
end
|
|
26
36
|
end
|
|
27
37
|
end
|
|
@@ -46,10 +46,14 @@ module Coradoc
|
|
|
46
46
|
# highlight_unconstrained was previously inconsistent (excluded
|
|
47
47
|
# newlines). All four are now uniform.
|
|
48
48
|
|
|
49
|
+
# Constrained markers delimit at boundaries only (#281): the
|
|
50
|
+
# opening marker must not follow a word char, the closing one
|
|
51
|
+
# must not precede one — `foo_bar_baz` stays plain text.
|
|
49
52
|
def constrained_mark(marker, reject_paragraph_break: false, content: nil)
|
|
50
|
-
open_guard =
|
|
53
|
+
open_guard = Parsanol::Atoms::Lookbehind.regex('[[:word:]]', positive: false) >>
|
|
54
|
+
str(marker) >> str(marker).absent?
|
|
51
55
|
content_rule = content || default_constrained_content(marker)
|
|
52
|
-
close_guard = str(marker) >> str(marker).absent?
|
|
56
|
+
close_guard = str(marker) >> str(marker).absent? >> match('[[:word:]]').absent?
|
|
53
57
|
sequence = open_guard >> content_rule >> close_guard
|
|
54
58
|
sequence >>= str("\n\n").absent? if reject_paragraph_break
|
|
55
59
|
sequence
|
|
@@ -204,7 +208,7 @@ module Coradoc
|
|
|
204
208
|
# Single lookahead over the whole dispatch alternation; the former
|
|
205
209
|
# per-branch .present? chain put zero-width branches in every
|
|
206
210
|
# non-last slot (shadowed-alternative lint, parsanol 1.3.55+).
|
|
207
|
-
(match('[
|
|
211
|
+
(match('[\\\\\[*#_{<^~`]') |
|
|
208
212
|
typographic_quote |
|
|
209
213
|
str('http') |
|
|
210
214
|
str('https') |
|
|
@@ -247,7 +251,7 @@ module Coradoc
|
|
|
247
251
|
monospace_unconstrained monospace_constrained
|
|
248
252
|
superscript subscript
|
|
249
253
|
attribute_reference
|
|
250
|
-
escaped_xref cross_reference
|
|
254
|
+
escaped_xref escape_char cross_reference
|
|
251
255
|
term_inline term_inline2
|
|
252
256
|
footnote stem
|
|
253
257
|
link inline_image
|
|
@@ -13,6 +13,16 @@ module Coradoc
|
|
|
13
13
|
).as(:list)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# A list item at ANY depth starts a list construct: a nested
|
|
17
|
+
# list (`* a` followed by `** b`) must stop the parent item's
|
|
18
|
+
# continuation text and attach as `nested` instead. Full items
|
|
19
|
+
# (not bare markers) so `**un**constrained` — no space after
|
|
20
|
+
# the marker — stays ordinary text.
|
|
21
|
+
def list_start
|
|
22
|
+
(1..5).map { |level| unordered_list(level) | ordered_list(level) }
|
|
23
|
+
.reduce(:|)
|
|
24
|
+
end
|
|
25
|
+
|
|
16
26
|
def list_continuation
|
|
17
27
|
line_start? >> str("+\n")
|
|
18
28
|
end
|
|
@@ -133,6 +143,13 @@ module Coradoc
|
|
|
133
143
|
end
|
|
134
144
|
|
|
135
145
|
def ulist_item(nesting_level = 1)
|
|
146
|
+
# A bare marker line (`*` then newline) is an empty list
|
|
147
|
+
# item — without this arm it fell through to a continuation
|
|
148
|
+
# text line and corrupted the preceding item (#139).
|
|
149
|
+
empty_item = ulist_marker(nesting_level).as(:marker) >>
|
|
150
|
+
str(' [[[').absent? >>
|
|
151
|
+
line_ending.as(:lines)
|
|
152
|
+
|
|
136
153
|
item = ulist_marker(nesting_level).as(:marker) >>
|
|
137
154
|
str(' [[[').absent? >>
|
|
138
155
|
match("\n").absent? >> space >>
|
|
@@ -145,7 +162,7 @@ module Coradoc
|
|
|
145
162
|
item >>= (list_marker(nesting_level + 1).present? >>
|
|
146
163
|
list(nesting_level + 1)).repeat(0).as(:nested)
|
|
147
164
|
end
|
|
148
|
-
|
|
165
|
+
(empty_item | item).as(:list_item)
|
|
149
166
|
end
|
|
150
167
|
|
|
151
168
|
# Continuation lines of a list item's first paragraph. AsciiDoc
|
|
@@ -242,7 +259,7 @@ module Coradoc
|
|
|
242
259
|
monospace_unconstrained monospace_constrained
|
|
243
260
|
superscript subscript
|
|
244
261
|
attribute_reference
|
|
245
|
-
escaped_xref cross_reference
|
|
262
|
+
escaped_xref escape_char cross_reference
|
|
246
263
|
term_inline term_inline2
|
|
247
264
|
footnote stem
|
|
248
265
|
link inline_image
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parsanol'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module AsciiDoc
|
|
7
|
+
module Parser
|
|
8
|
+
# An immutable String carrying its byte offset and source input.
|
|
9
|
+
#
|
|
10
|
+
# The native engine hands pooled, mutable Parsanol::Slice objects
|
|
11
|
+
# back in the parse tree; storing those in document models is
|
|
12
|
+
# unsafe (the pool resets them) and they fail serializer type
|
|
13
|
+
# checks. TreeMaterializer converts every Slice into a
|
|
14
|
+
# PositionedString at the GrammarBackend boundary, so models only
|
|
15
|
+
# ever see Strings — while SourceLineExtractor can still recover
|
|
16
|
+
# positions (line_and_column delegates the computation to a
|
|
17
|
+
# throwaway Slice).
|
|
18
|
+
class PositionedString < String
|
|
19
|
+
attr_reader :byte_position, :source_input
|
|
20
|
+
|
|
21
|
+
def initialize(content, byte_position: 0, source_input: nil)
|
|
22
|
+
super(content)
|
|
23
|
+
@byte_position = byte_position
|
|
24
|
+
@source_input = source_input
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_slice(slice)
|
|
28
|
+
new(slice.to_s, byte_position: slice.offset, source_input: slice.input)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def line_and_column
|
|
32
|
+
raise ArgumentError, 'Line/column requires input' unless @source_input
|
|
33
|
+
|
|
34
|
+
@line_and_column ||= Parsanol::Slice.new(@byte_position, to_s,
|
|
35
|
+
@source_input).line_and_column
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -47,7 +47,7 @@ module Coradoc
|
|
|
47
47
|
end
|
|
48
48
|
cache[key] = rule_name
|
|
49
49
|
end
|
|
50
|
-
parser_instance.public_send(cache[key])
|
|
50
|
+
TreeMaterializer.convert(parser_instance.public_send(cache[key]))
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
private
|
|
@@ -59,7 +59,8 @@ module Coradoc
|
|
|
59
59
|
# Wrapping `__send__` in particular causes infinite recursion.
|
|
60
60
|
# @return [Hash{Symbol => Array<Module>}]
|
|
61
61
|
def collect_rule_names
|
|
62
|
-
parser_constants = Parser.constants - %i[Base Cache FixFiles RuleDispatcher
|
|
62
|
+
parser_constants = Parser.constants - %i[Base Cache FixFiles RuleDispatcher
|
|
63
|
+
PositionedString TreeMaterializer]
|
|
63
64
|
parser_constants.each_with_object({}) do |const, acc|
|
|
64
65
|
Parser.const_get(const).instance_methods(false).each do |name|
|
|
65
66
|
acc[name] ||= []
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parsanol'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module AsciiDoc
|
|
7
|
+
module Parser
|
|
8
|
+
# Converts a native-engine parse tree: every
|
|
9
|
+
# Parsanol::Slice becomes an immutable PositionedString. Runs at
|
|
10
|
+
# the GrammarBackend boundary so the transformer and document
|
|
11
|
+
# models never see pooled slices, without losing source
|
|
12
|
+
# positions.
|
|
13
|
+
module TreeMaterializer
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def convert(node)
|
|
17
|
+
case node
|
|
18
|
+
when Parsanol::Slice then PositionedString.from_slice(node)
|
|
19
|
+
when Hash then node.transform_values { |value| convert(value) }
|
|
20
|
+
when Array then node.map { |value| convert(value) }
|
|
21
|
+
else
|
|
22
|
+
node
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -7,6 +7,8 @@ module Coradoc
|
|
|
7
7
|
autoload :Cache, "#{__dir__}/parser/cache"
|
|
8
8
|
autoload :FrontmatterParser, "#{__dir__}/parser/frontmatter_parser"
|
|
9
9
|
autoload :Inline, "#{__dir__}/parser/inline"
|
|
10
|
+
autoload :PositionedString, "#{__dir__}/parser/positioned_string"
|
|
11
|
+
autoload :TreeMaterializer, "#{__dir__}/parser/tree_materializer"
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -8,7 +8,9 @@ module Coradoc
|
|
|
8
8
|
class SourceCode < Core
|
|
9
9
|
def to_adoc(model, _options = {})
|
|
10
10
|
@model = model
|
|
11
|
-
|
|
11
|
+
lang = model.lang.to_s.strip
|
|
12
|
+
header = lang.empty? ? '[source]' : "[source,#{lang}]"
|
|
13
|
+
"\n\n#{gen_anchor}#{header}\n#{gen_delimiter}\n" <<
|
|
12
14
|
gen_lines << "\n#{gen_delimiter}\n\n"
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -87,6 +87,10 @@ module Coradoc
|
|
|
87
87
|
"+\n#{elem.to_adoc}"
|
|
88
88
|
end.join
|
|
89
89
|
nest = model.nested.nil? || (model.nested.is_a?(Array) && model.nested.empty?) ? '' : model.nested.to_adoc
|
|
90
|
+
# The nested list serializer opens with a block separator;
|
|
91
|
+
# adjacent to the item line it would read as a detached
|
|
92
|
+
# sibling list on re-parse.
|
|
93
|
+
nest = nest.sub(/\A\n+/, '')
|
|
90
94
|
out = " #{_anchor}#{out}#{model.line_break}"
|
|
91
95
|
out + attach + nest
|
|
92
96
|
end
|
|
@@ -5,14 +5,20 @@ module Coradoc
|
|
|
5
5
|
module Serializer
|
|
6
6
|
module Serializers
|
|
7
7
|
class Table < Base
|
|
8
|
-
def to_adoc(model,
|
|
8
|
+
def to_adoc(model, options_or_context = {})
|
|
9
9
|
@model = model
|
|
10
|
+
context = normalize_context(options_or_context)
|
|
10
11
|
_anchor = @model.anchor.nil? ? '' : "#{serialize_child(@model.anchor)}\n"
|
|
11
12
|
_attrs = @model.attrs.to_s.empty? ? '' : "#{serialize_child(@model.attrs)}\n"
|
|
12
13
|
_title = @model.title.nil? || @model.title.empty? ? '' : ".#{serialize_child(@model.title)}\n"
|
|
13
14
|
_content = @model.rows.map { |row| serialize_child(row) }.join
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
# Blank-line separation after the table so a following
|
|
17
|
+
# block never glues onto the closer (#151) — same
|
|
18
|
+
# convention as the paragraph serializer, suppressed for
|
|
19
|
+
# the document's last element.
|
|
20
|
+
separator = context.last_element ? '' : "\n"
|
|
21
|
+
"\n\n#{_anchor}#{_attrs}#{_title}|===\n#{_content}|===\n#{separator}"
|
|
16
22
|
end
|
|
17
23
|
end
|
|
18
24
|
end
|
|
@@ -9,7 +9,9 @@ module Coradoc
|
|
|
9
9
|
def to_adoc(model, _options = {})
|
|
10
10
|
result = ''
|
|
11
11
|
result += "[[#{model.id}]] " if model.id
|
|
12
|
-
|
|
12
|
+
content = serialize_content(model.content)
|
|
13
|
+
content = Coradoc::Util::AsciiDoc.escape_text_delimiters(content) if content.is_a?(String)
|
|
14
|
+
result += content
|
|
13
15
|
result += model.line_break.to_s
|
|
14
16
|
result
|
|
15
17
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
module Transform
|
|
6
|
+
# List construction for the AsciiDoc view: nested lists flatten
|
|
7
|
+
# into the item stream with depth markers so AsciiDoc reads them
|
|
8
|
+
# as nested rather than sibling lists (#68) — a marker-type
|
|
9
|
+
# change alone signals nesting; same-type nesting goes one level
|
|
10
|
+
# deeper.
|
|
11
|
+
module ListBuilding
|
|
12
|
+
def transform_list(list, depth = 1)
|
|
13
|
+
items = []
|
|
14
|
+
Array(list.items).each do |item|
|
|
15
|
+
nested_blocks = [item.nested_list,
|
|
16
|
+
*Array(item.children).grep(CoreModel::ListBlock)].compact
|
|
17
|
+
text = inline_text(item)
|
|
18
|
+
attached = attached_blocks(item)
|
|
19
|
+
if text.empty? && attached.empty? && nested_blocks.any?
|
|
20
|
+
# A wrapper item whose only content is its nested list
|
|
21
|
+
# cannot render as a deeper list in AsciiDoc (lists start
|
|
22
|
+
# at level 1, #68), so splice the nested list at this depth.
|
|
23
|
+
nested_blocks.each do |child|
|
|
24
|
+
child_depth = child.marker_type == list.marker_type ? depth : 1
|
|
25
|
+
items.concat(transform_list(child, child_depth).items)
|
|
26
|
+
end
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
items << Coradoc::AsciiDoc::Model::List::Item.new(
|
|
30
|
+
content: text,
|
|
31
|
+
marker: item.marker || (default_marker(list.marker_type) * depth),
|
|
32
|
+
attached: attached
|
|
33
|
+
)
|
|
34
|
+
nested_blocks.each do |child|
|
|
35
|
+
child_depth = child.marker_type == list.marker_type ? depth + 1 : 1
|
|
36
|
+
items.concat(transform_list(child, child_depth).items)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
case list.marker_type
|
|
41
|
+
when 'ordered'
|
|
42
|
+
Coradoc::AsciiDoc::Model::List::Ordered.new(items: items)
|
|
43
|
+
when 'definition'
|
|
44
|
+
Coradoc::AsciiDoc::Model::List::Definition.new(items: items)
|
|
45
|
+
else
|
|
46
|
+
Coradoc::AsciiDoc::Model::List::Unordered.new(items: items)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Inline children render on the item line; CoreModel::Block
|
|
51
|
+
# children become attached blocks (`+` continuations) instead of
|
|
52
|
+
# being glued into the item text.
|
|
53
|
+
def inline_text(item)
|
|
54
|
+
case (rc = item.renderable_content)
|
|
55
|
+
when String then rc
|
|
56
|
+
when Array
|
|
57
|
+
rc.each_with_object(+'') do |child, out|
|
|
58
|
+
next if child.is_a?(CoreModel::Block) || child.is_a?(CoreModel::ListBlock)
|
|
59
|
+
|
|
60
|
+
out << case child
|
|
61
|
+
when CoreModel::TextContent then child.text.to_s
|
|
62
|
+
when String then child
|
|
63
|
+
when CoreModel::Base then child.flat_text.to_s
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
''
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def attached_blocks(item)
|
|
72
|
+
Array(item.children)
|
|
73
|
+
.grep(CoreModel::Block)
|
|
74
|
+
.flat_map { |block| FromCoreModel.flatten_children([block]) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def default_marker(marker_type)
|
|
78
|
+
case marker_type
|
|
79
|
+
when 'ordered' then '.'
|
|
80
|
+
else '*'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -5,6 +5,8 @@ module Coradoc
|
|
|
5
5
|
module Transform
|
|
6
6
|
# Transforms CoreModel to AsciiDoc models
|
|
7
7
|
class FromCoreModel
|
|
8
|
+
extend Transform::ListBuilding
|
|
9
|
+
|
|
8
10
|
@registered = false
|
|
9
11
|
|
|
10
12
|
class << self
|
|
@@ -216,24 +218,11 @@ module Coradoc
|
|
|
216
218
|
)
|
|
217
219
|
end
|
|
218
220
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
)
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
case list.marker_type
|
|
228
|
-
when 'ordered'
|
|
229
|
-
Coradoc::AsciiDoc::Model::List::Ordered.new(items: items)
|
|
230
|
-
when 'definition'
|
|
231
|
-
Coradoc::AsciiDoc::Model::List::Definition.new(items: items)
|
|
232
|
-
else
|
|
233
|
-
Coradoc::AsciiDoc::Model::List::Unordered.new(items: items)
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
221
|
+
# Lists nested inside list items carry depth markers so
|
|
222
|
+
# AsciiDoc reads them as nested rather than sibling lists —
|
|
223
|
+
# without this, item children other than text were dropped
|
|
224
|
+
# entirely (#68 territory). Nested lists keep their own
|
|
225
|
+
# marker type one level deeper.
|
|
237
226
|
def transform_list_item(item)
|
|
238
227
|
Coradoc::AsciiDoc::Model::List::Item.new(
|
|
239
228
|
content: item.flat_text,
|
|
@@ -256,12 +245,95 @@ module Coradoc
|
|
|
256
245
|
)
|
|
257
246
|
end
|
|
258
247
|
|
|
259
|
-
|
|
248
|
+
# Nested inline structure lives in +children+ (e.g. an
|
|
249
|
+
# ItalicElement whose single child is a BoldElement);
|
|
250
|
+
# +content+ is only the flattened display string. Build the
|
|
251
|
+
# adoc model from children so nesting survives, and default
|
|
252
|
+
# to the constrained marker — the constrained/unstrained
|
|
253
|
+
# distinction is not carried in CoreModel, and constrained
|
|
254
|
+
# is the correct rendering for span-boundary emphasis (#159).
|
|
255
|
+
def inline_model(klass, inline, unconstrained: false)
|
|
256
|
+
content = if inline.children.nil? || inline.children.empty?
|
|
257
|
+
inline.content.to_s
|
|
258
|
+
else
|
|
259
|
+
create_text_elements(inline.children)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
klass.new(content: content, unconstrained: unconstrained)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# `_*text*_` parses to Italic{Bold{text}}; Metanorma renders
|
|
266
|
+
# same-span emphasis correctly only with italic innermost
|
|
267
|
+
# (`*_text_*`, #159). When an italic's entire content is one
|
|
268
|
+
# bold (or a bold's one italic), rebuild as Bold outer /
|
|
269
|
+
# Italic inner, both constrained.
|
|
270
|
+
def swap_emphasis_nesting(inline, unconstrained: false)
|
|
271
|
+
children = Array(inline.children)
|
|
272
|
+
return nil unless children.size == 1
|
|
273
|
+
|
|
274
|
+
only = children.first
|
|
275
|
+
return nil unless only.is_a?(CoreModel::InlineElement)
|
|
276
|
+
|
|
277
|
+
inner_kids = Array(only.children)
|
|
278
|
+
return nil unless inner_kids.empty? || inner_kids.all?(CoreModel::TextContent)
|
|
279
|
+
|
|
280
|
+
outer_type = inline.resolve_format_type
|
|
281
|
+
inner_type = only.resolve_format_type
|
|
282
|
+
return nil unless %w[bold italic].include?(outer_type) && outer_type != inner_type &&
|
|
283
|
+
%w[bold italic].include?(inner_type)
|
|
284
|
+
|
|
285
|
+
classes = {
|
|
286
|
+
'bold' => Coradoc::AsciiDoc::Model::Inline::Bold,
|
|
287
|
+
'italic' => Coradoc::AsciiDoc::Model::Inline::Italic
|
|
288
|
+
}
|
|
289
|
+
inner = classes[outer_type].new(
|
|
290
|
+
content: only.children.nil? || only.children.empty? ? only.content.to_s : create_text_elements(only.children),
|
|
291
|
+
unconstrained: false
|
|
292
|
+
)
|
|
293
|
+
classes[inner_type].new(content: inner, unconstrained: unconstrained)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# AsciiDoc constrained markers only reparse as emphasis when
|
|
297
|
+
# the opening delimiter follows a boundary and the closing
|
|
298
|
+
# one precedes one. CoreModel does not carry the constraint,
|
|
299
|
+
# so derive it from the adjacent sibling text: an inline
|
|
300
|
+
# touching a word character on either side must serialize
|
|
301
|
+
# with the unconstrained form or the emphasis is lost on
|
|
302
|
+
# reparse (mid-word bolding is common in Word documents).
|
|
303
|
+
def unconstrained_flanks?(left, right)
|
|
304
|
+
left_ok = left.nil? || left.match?(/\s/)
|
|
305
|
+
right_ok = right.nil? || right.match?(/\s|[[:punct:]]/)
|
|
306
|
+
!(left_ok && right_ok)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def sibling_edge(items, index, direction)
|
|
310
|
+
item = items[index]
|
|
311
|
+
text = case item
|
|
312
|
+
when Coradoc::CoreModel::TextContent then item.text.to_s
|
|
313
|
+
when Coradoc::CoreModel::InlineElement then item.content.to_s
|
|
314
|
+
when String then item
|
|
315
|
+
else return nil
|
|
316
|
+
end
|
|
317
|
+
return nil if text.empty?
|
|
318
|
+
|
|
319
|
+
direction == :last ? text[-1] : text[0]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def transform_inline(inline, siblings: nil, index: 0)
|
|
323
|
+
left = siblings && index.positive? ? sibling_edge(siblings, index - 1, :last) : nil
|
|
324
|
+
right = siblings ? sibling_edge(siblings, index + 1, :first) : nil
|
|
325
|
+
unconstrained = unconstrained_flanks?(left, right)
|
|
260
326
|
case inline.resolve_format_type
|
|
261
327
|
when 'bold'
|
|
262
|
-
Coradoc::AsciiDoc::Model::Inline::Bold
|
|
328
|
+
inline_model(Coradoc::AsciiDoc::Model::Inline::Bold, inline,
|
|
329
|
+
unconstrained: unconstrained)
|
|
263
330
|
when 'italic'
|
|
264
|
-
|
|
331
|
+
# #159: italic directly wrapping only bold (`_*text*_`)
|
|
332
|
+
# re-serializes as Bold outer / Italic inner (`*_text_*`)
|
|
333
|
+
# for correct Metanorma rendering.
|
|
334
|
+
swap_emphasis_nesting(inline, unconstrained: unconstrained) ||
|
|
335
|
+
inline_model(Coradoc::AsciiDoc::Model::Inline::Italic, inline,
|
|
336
|
+
unconstrained: unconstrained)
|
|
265
337
|
when 'monospace'
|
|
266
338
|
Coradoc::AsciiDoc::Model::Inline::Monospace.new(content: inline.content)
|
|
267
339
|
when 'highlight'
|
|
@@ -546,7 +618,13 @@ module Coradoc
|
|
|
546
618
|
def create_text_elements(content)
|
|
547
619
|
case content
|
|
548
620
|
when Array
|
|
549
|
-
content.map
|
|
621
|
+
content.each_with_index.map do |item, i|
|
|
622
|
+
if item.is_a?(Coradoc::CoreModel::InlineElement)
|
|
623
|
+
transform_inline(item, siblings: content, index: i)
|
|
624
|
+
else
|
|
625
|
+
create_text_elements(item)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
550
628
|
when Coradoc::CoreModel::InlineElement
|
|
551
629
|
transform_inline(content)
|
|
552
630
|
when Coradoc::CoreModel::TextContent
|
|
@@ -577,13 +655,6 @@ module Coradoc
|
|
|
577
655
|
attrs['language'] = block.language if block.language
|
|
578
656
|
attrs
|
|
579
657
|
end
|
|
580
|
-
|
|
581
|
-
def default_marker(marker_type)
|
|
582
|
-
case marker_type
|
|
583
|
-
when 'ordered' then '.'
|
|
584
|
-
else '*'
|
|
585
|
-
end
|
|
586
|
-
end
|
|
587
658
|
end
|
|
588
659
|
|
|
589
660
|
def transform(model) = self.class.transform(model)
|
|
@@ -59,10 +59,11 @@ module Coradoc
|
|
|
59
59
|
# @return [Hash{String=>String}]
|
|
60
60
|
def attributes_from_block(block)
|
|
61
61
|
result = {}
|
|
62
|
-
|
|
62
|
+
data = CoreModel::FrontmatterBlock::Codec.to_hash(block)
|
|
63
|
+
return result if data.empty?
|
|
63
64
|
|
|
64
65
|
MAPPINGS.each do |(_, front_key, _)|
|
|
65
|
-
value =
|
|
66
|
+
value = data[front_key]
|
|
66
67
|
next if value.nil?
|
|
67
68
|
|
|
68
69
|
attr_name = front_key_to_attribute(front_key)
|