coradoc-adoc 2.0.34 → 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/document_attributes.rb +6 -1
- data/lib/coradoc/asciidoc/parser/inline.rb +26 -19
- data/lib/coradoc/asciidoc/parser/list.rb +25 -8
- data/lib/coradoc/asciidoc/parser/paragraph.rb +4 -2
- 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
|
|
@@ -19,7 +19,12 @@ module Coradoc
|
|
|
19
19
|
|
|
20
20
|
def document_attribute
|
|
21
21
|
str(':') >> attribute_name.as(:key) >> str(':') >>
|
|
22
|
-
|
|
22
|
+
# attribute_value itself already matches empty (its
|
|
23
|
+
# zero-width branch), so the former `| str('')` fallback was
|
|
24
|
+
# dead — ordered choice commits to attribute_value's empty
|
|
25
|
+
# match first — and the VM's shadowed-alternative lint
|
|
26
|
+
# (parsanol 1.3.55+) correctly rejects it.
|
|
27
|
+
space? >> attribute_value.as(:value) >> (line_ending | eof?)
|
|
23
28
|
end
|
|
24
29
|
end
|
|
25
30
|
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
|
|
@@ -138,7 +142,7 @@ module Coradoc
|
|
|
138
142
|
end
|
|
139
143
|
|
|
140
144
|
def link
|
|
141
|
-
((str('http')
|
|
145
|
+
((str('http') | str('https') | str('ftp')).present? >>
|
|
142
146
|
match('[A-Za-z0-9_.\\-:/&?=+,%#~;]+').repeat(1).as(:path) >>
|
|
143
147
|
(str('[') >> match('[^\\]]').repeat(1).as(:text) >> str(']')).maybe
|
|
144
148
|
).as(:link) |
|
|
@@ -201,19 +205,22 @@ module Coradoc
|
|
|
201
205
|
end
|
|
202
206
|
|
|
203
207
|
def inline_chars?
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
208
|
+
# Single lookahead over the whole dispatch alternation; the former
|
|
209
|
+
# per-branch .present? chain put zero-width branches in every
|
|
210
|
+
# non-last slot (shadowed-alternative lint, parsanol 1.3.55+).
|
|
211
|
+
(match('[\\\\\[*#_{<^~`]') |
|
|
212
|
+
typographic_quote |
|
|
213
|
+
str('http') |
|
|
214
|
+
str('https') |
|
|
215
|
+
str('link:') |
|
|
216
|
+
str('image:') |
|
|
217
|
+
str('+++') |
|
|
218
|
+
str('pass:') |
|
|
219
|
+
term_type |
|
|
220
|
+
str('footnote') |
|
|
221
|
+
stem_type |
|
|
222
|
+
str('\\<<') |
|
|
223
|
+
hard_line_break_marker?).present?
|
|
217
224
|
end
|
|
218
225
|
|
|
219
226
|
# AsciiDoc hard line break: a space followed by `+` at end of line,
|
|
@@ -221,8 +228,8 @@ module Coradoc
|
|
|
221
228
|
# the enclosing paragraph/verse. Recognised ahead of `text_unformatted`
|
|
222
229
|
# so the marker isn't swallowed as plain text.
|
|
223
230
|
def hard_line_break_marker?
|
|
224
|
-
(str(' +') >> str("\n"))
|
|
225
|
-
|
|
231
|
+
((str(' +') >> str("\n")) |
|
|
232
|
+
(str('\\') >> str("\n"))).present?
|
|
226
233
|
end
|
|
227
234
|
|
|
228
235
|
def hard_line_break
|
|
@@ -244,7 +251,7 @@ module Coradoc
|
|
|
244
251
|
monospace_unconstrained monospace_constrained
|
|
245
252
|
superscript subscript
|
|
246
253
|
attribute_reference
|
|
247
|
-
escaped_xref cross_reference
|
|
254
|
+
escaped_xref escape_char cross_reference
|
|
248
255
|
term_inline term_inline2
|
|
249
256
|
footnote stem
|
|
250
257
|
link inline_image
|
|
@@ -7,10 +7,20 @@ module Coradoc
|
|
|
7
7
|
module List
|
|
8
8
|
def list(nesting_level = 1)
|
|
9
9
|
(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
unordered_list(nesting_level) |
|
|
11
|
+
ordered_list(nesting_level) |
|
|
12
|
+
definition_list
|
|
13
|
+
).as(:list)
|
|
14
|
+
end
|
|
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(:|)
|
|
14
24
|
end
|
|
15
25
|
|
|
16
26
|
def list_continuation
|
|
@@ -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
|
|
@@ -259,8 +276,8 @@ module Coradoc
|
|
|
259
276
|
end
|
|
260
277
|
|
|
261
278
|
def list_body_hard_break_marker?
|
|
262
|
-
(str(' +') >> str("\n"))
|
|
263
|
-
(str('\\') >> str("\n")).present?
|
|
279
|
+
((str(' +') >> str("\n")) |
|
|
280
|
+
(str('\\') >> str("\n"))).present?
|
|
264
281
|
end
|
|
265
282
|
|
|
266
283
|
# Recognizes line endings for a list body source line, including
|
|
@@ -9,7 +9,7 @@ module Coradoc
|
|
|
9
9
|
(attribute_list >> newline).absent? >>
|
|
10
10
|
block_delimiter.absent? >>
|
|
11
11
|
(str('|===') >> newline).absent? >>
|
|
12
|
-
|
|
12
|
+
list_start.absent? >>
|
|
13
13
|
list_prefix.absent? >>
|
|
14
14
|
list_continuation.absent? >>
|
|
15
15
|
element_id.absent? >>
|
|
@@ -49,7 +49,9 @@ module Coradoc
|
|
|
49
49
|
(paragraph_text_line(true).repeat(1, 1) >>
|
|
50
50
|
(newline.repeat(1).as(:line_break) | eof?)).repeat(0, 1))
|
|
51
51
|
).as(:lines) >>
|
|
52
|
-
|
|
52
|
+
# the former "| eof?" was dead: newline.repeat(0) already
|
|
53
|
+
# matches empty (shadowed-alternative lint, parsanol 1.3.55+)
|
|
54
|
+
newline.repeat(0)
|
|
53
55
|
).as(:paragraph)
|
|
54
56
|
end
|
|
55
57
|
|
|
@@ -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
|