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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/grammar/coradoc-adoc.artifact.json +1 -0
  3. data/grammar/coradoc-adoc.parg +396 -0
  4. data/lib/coradoc/asciidoc/formatter.rb +75 -0
  5. data/lib/coradoc/asciidoc/lint/document_title.rb +26 -0
  6. data/lib/coradoc/asciidoc/lint/duplicate_ids.rb +36 -0
  7. data/lib/coradoc/asciidoc/lint/empty_section.rb +45 -0
  8. data/lib/coradoc/asciidoc/lint/heading_levels.rb +44 -0
  9. data/lib/coradoc/asciidoc/lint.rb +27 -0
  10. data/lib/coradoc/asciidoc/model/text_element.rb +18 -5
  11. data/lib/coradoc/asciidoc/parser/base.rb +28 -11
  12. data/lib/coradoc/asciidoc/parser/citation.rb +10 -0
  13. data/lib/coradoc/asciidoc/parser/inline.rb +8 -4
  14. data/lib/coradoc/asciidoc/parser/list.rb +19 -2
  15. data/lib/coradoc/asciidoc/parser/paragraph.rb +1 -1
  16. data/lib/coradoc/asciidoc/parser/positioned_string.rb +40 -0
  17. data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +3 -2
  18. data/lib/coradoc/asciidoc/parser/tree_materializer.rb +28 -0
  19. data/lib/coradoc/asciidoc/parser.rb +2 -0
  20. data/lib/coradoc/asciidoc/serializer/serializers/block/source_code.rb +3 -1
  21. data/lib/coradoc/asciidoc/serializer/serializers/list/item.rb +4 -0
  22. data/lib/coradoc/asciidoc/serializer/serializers/table.rb +8 -2
  23. data/lib/coradoc/asciidoc/serializer/serializers/text_element.rb +3 -1
  24. data/lib/coradoc/asciidoc/transform/from_core_model/list_building.rb +86 -0
  25. data/lib/coradoc/asciidoc/transform/from_core_model.rb +100 -29
  26. data/lib/coradoc/asciidoc/transform/frontmatter_attribute_map.rb +3 -2
  27. data/lib/coradoc/asciidoc/transform/text_extract_visitor.rb +5 -2
  28. data/lib/coradoc/asciidoc/transform/to_core_model.rb +12 -2
  29. data/lib/coradoc/asciidoc/transform.rb +1 -0
  30. data/lib/coradoc/asciidoc/transformer/source_line_extractor.rb +1 -1
  31. data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +18 -7
  32. data/lib/coradoc/asciidoc/version.rb +1 -1
  33. data/lib/coradoc/asciidoc.rb +12 -0
  34. data/lib/coradoc/util/asciidoc.rb +56 -0
  35. metadata +16 -5
@@ -0,0 +1,396 @@
1
+ # Coradoc AsciiDoc grammar — PARG port (parsanol-ruby#129 acceptance).
2
+ #
3
+ # Translation of coradoc-adoc/lib/coradoc/asciidoc/parser (Ruby Parsanol
4
+ # DSL) to .parg. Ported incrementally; each rule family cites its Ruby
5
+ # source. The motivating runtime-state patterns (fence capture + dynamic
6
+ # close, from Parser::Block#block_style / #markdown_code_block) map onto
7
+ # set/state exactly as designed in parsanol-ruby#129.
8
+ #
9
+ # Increment 1 scope: terminals, attribute lists (faithful capture
10
+ # shapes), element ids, block titles, block headers, all delimited
11
+ # block styles (verbatim content), comments/tags/includes, document
12
+ # dispatch subset with the unparsed catch-all.
13
+
14
+ grammar CoradocAdoc version "0.2.0" {
15
+
16
+ # ---- text.rb terminals ----
17
+ any_char = %x01-10FFFF
18
+ space = 1*" "
19
+ sp = [1*" "]
20
+ text = 1*(!"\n" any_char)
21
+ line_ending = ("\n" / "\r\n")
22
+ newline = 1*("\n" / "\r\n")
23
+ newline_single = ("\n" / "\r\n")
24
+ empty_line = "\n"
25
+ keyword = 1*(%x41-5A / %x61-7A / %x30-39 / "_" / "-" / "." / ",")
26
+ file_path = 1*(!"[" any_char)
27
+
28
+ # ---- attribute_list.rb (faithful capture shapes) ----
29
+ named_key = (1*(%x41-5A / %x61-7A / %x30-39 / "_" / "-")) as named_key
30
+
31
+ named_value_sq = %x27 (1*(!(%x27) any_char)) %x27
32
+ named_value_dq = %x22 (1*(!(%x22) any_char)) %x22
33
+ named_value_noquote = 1*(!(%x5D / %x2C) any_char)
34
+ named_value = ((named_value_sq / named_value_dq / named_value_noquote)) as named_value
35
+
36
+ named_attribute = ((named_key [space] "=" [space] named_value [space])) as named
37
+
38
+ positional_value_sq = %x27 (1*(!(%x27) any_char)) %x27
39
+ positional_value_dq = %x22 (1*(!(%x22) any_char)) %x22
40
+ positional_value_unquoted = 1*(!(%x5D / %x2C / %x3D) any_char)
41
+ positional_attribute = (((positional_value_sq / positional_value_dq / positional_value_unquoted) !(%x3D))) as positional
42
+
43
+ named_many = ((1*1(named_attribute)) [*(%x2C [space] named_attribute)])
44
+ positional_one_named_many = ((1*1(positional_attribute)) 1*(%x2C [space] named_attribute))
45
+ positional_many_named_many = ((1*1(positional_attribute)) 1*(%x2C [space] positional_attribute) 1*(%x2C [space] named_attribute))
46
+ positional_many = ((1*1(positional_attribute)) [*(%x2C [space] positional_attribute)])
47
+ positional_zero_or_one = 0*1(positional_attribute)
48
+
49
+ attribute_list = (&(%x5B) %x5B !(%x5B) (((((named_many / positional_one_named_many / positional_many_named_many / positional_many / positional_zero_or_one))) as attribute_array)) as attribute_list %x5D)
50
+
51
+ # ---- content.rb: element_id / block_header.rb ----
52
+ element_id = ((("[[" (keyword) as id "]]") / ("[#" (keyword) as id "]")) newline)
53
+
54
+ attribute_blocks = (1*((attribute_list newline))) as attribute_list
55
+
56
+ # ---- block.rb: delimiters + title ----
57
+ block_delim_run = 4*((%x2A / %x3D / %x5F / %x2B / %x2E / %x2D))
58
+ block_delimiter = ((block_delim_run / (2%x2D) / (3*%x60)) newline)
59
+ block_title = (!(block_delimiter) %x2E !(space) (text) as title newline)
60
+
61
+ block_header = [(block_title)] [(element_id)] [(attribute_blocks)]
62
+
63
+ # ---- text.rb: comment_line / tag / comment_block / include ----
64
+ tag_shape = ("//" !( "/") [1*" "] ("tag" / "end") "::" !( ":") 1*(!"[" any_char))
65
+
66
+ comment_line_content = (!(tag_shape) "//" !("/") [1*" "] *(!"\n" any_char) line_ending)
67
+
68
+ comment_line = (!(tag_shape) ("//" !("/") [1*" "] [(text) as comment_text line_ending as line_break])) as comment_line
69
+
70
+ tag = (("//" !("/") [1*" "] ("tag" / "end") as prefix "::" !(":") ((1*(!"[" any_char))) as name attribute_list ([newline_single] as line_break))) as tag
71
+
72
+ comment_block = (("////" line_ending (*(!((line_ending "////")) any_char)) as comment_text line_ending "////")) as comment_block
73
+
74
+ page_break = ("<<<" line_ending)
75
+
76
+ include_directive = (("include::" file_path as path attribute_list [newline] as line_break)) as include
77
+
78
+ # ---- content.rb text_line(verbatim:) : text + own line ending ----
79
+ # tl >> (line_ending.as(:line_break) | eof?) — the newline rides INSIDE
80
+ # the same iteration hash (parslet sequence merge), matching the DSL.
81
+ verbatim_line = (((1*(!"\n" any_char)) as text) ((line_ending) as line_break / (!(any_char))))
82
+
83
+ # ---- block.rb: delimited blocks (the #129 state patterns) ----
84
+ # markdown_code_block: 3+ backticks, optional language, closes with the
85
+ # SAME fence run (Parser::Block#markdown_code_block).
86
+ state md_fence: string
87
+ md_lang = [*" " (1*(%x41-5A / %x61-7A / %x30-39 / "_" / "+" / "-" / ".")) as language]
88
+ md_close = (state md_fence [space] newline)
89
+ md_content = (!(md_close) verbatim_line)
90
+ md_break = (empty_line as line_break)
91
+ markdown_code_block = (block_header ((set md_fence = (3*%x60)) as delimiter md_lang newline (1*(md_content / md_break)) as lines [space] (state md_fence) [space] newline))
92
+
93
+ # block_style: 4+ identical fence chars, closes with the exact same run.
94
+ state eq_fence: string
95
+ state dash_fence: string
96
+ state us_fence: string
97
+ state ast_fence: string
98
+ state plus_fence: string
99
+ state dot_fence: string
100
+
101
+ eq_content = (!(((state eq_fence) newline)) verbatim_line)
102
+ dash_content = (!(((state dash_fence) newline)) verbatim_line)
103
+ us_content = (!(((state us_fence) newline)) verbatim_line)
104
+ ast_content = (!(((state ast_fence) newline)) verbatim_line)
105
+ plus_content = (!(((state plus_fence) newline)) verbatim_line)
106
+ dot_content = (!(((state dot_fence) newline)) verbatim_line)
107
+ vbreak = (empty_line as line_break)
108
+
109
+ # superseded by depth-unrolled forms: example_block = (block_header ((set eq_fence = (4*%x3D)) as delimiter newline (1*(eq_content / vbreak)) as lines (state eq_fence) newline))
110
+ source_block = (block_header ((set dash_fence = (4*%x2D)) as delimiter newline (1*(dash_content / vbreak)) as lines (state dash_fence) newline))
111
+ # superseded by depth-unrolled forms: quote_block = (block_header ((set us_fence = (4*%x5F)) as delimiter newline (1*(us_content / vbreak)) as lines (state us_fence) newline))
112
+ # superseded by depth-unrolled forms: sidebar_block = (block_header ((set ast_fence = (4*%x2A)) as delimiter newline (1*(ast_content / vbreak)) as lines (state ast_fence) newline))
113
+ pass_block = (block_header ((set plus_fence = (4*%x2B)) as delimiter newline (1*(plus_content / vbreak)) as lines (state plus_fence) newline))
114
+ literal_block = (block_header ((set dot_fence = (4*%x2E)) as delimiter newline (1*(dot_content / vbreak)) as lines (state dot_fence) newline))
115
+
116
+ # block_style_exact: open block — EXACTLY 2 dashes
117
+ state open_fence: string
118
+ open_content = (!(((state open_fence) newline)) verbatim_line)
119
+ # superseded by depth-unrolled forms: open_block = (block_header ((set open_fence = (2%x2D)) as delimiter newline (1*(open_content / vbreak)) as lines (state open_fence) newline))
120
+
121
+ # superseded by depth-unrolled block_3/block_2/block_1
122
+
123
+ # ---- inline.rb (INLINE_RULE_ORDER is load-bearing) ----
124
+ typographic_quote = ((%x22 %x60) / (%x60 %x22) / (%x27 %x60) / (%x60 %x27)) as typographic_quote
125
+
126
+ attribute_reference = (&(%x7B) %x7B ((1*(%x41-5A / %x61-7A / %x30-39 / "_" / "-"))) as attribute_reference %x7D)
127
+
128
+ uri_char = (%x41-5A / %x61-7A / %x30-39 / "_" / "." / "-" / ":" / "/" / "&" / "?" / "=" / "+" / "," / "%" / "#" / "~" / ";")
129
+
130
+ link_tail = (((1*uri_char)) as path [(%x5B ((1*(!(%x5D) any_char))) as text %x5D)])
131
+ link = ((&(("http" / "https" / "ftp")) link_tail) as link / (&("link:") "link:" link_tail) as link)
132
+
133
+ inline_image = (&("image:") "image:" !(":") ((1*uri_char)) as path [attribute_list]) as inline_image
134
+
135
+ inline_passthrough = ((("+++" ((1*(!("+++") !(%x0A) any_char))) as raw "+++")) as inline_passthrough / (&("pass:") "pass:" *(%x41-5A / %x61-7A / %x2B / %x2C) %x5B ((1*(!("]]") !(%x5D / %x0A) any_char))) as raw %x5D) as inline_passthrough)
136
+
137
+ underline = (attribute_list %x5B ".underline" %x5D as role %x23 ((1*(!(%x23 / %x0A) any_char))) as text %x23) as underline
138
+ small = (attribute_list %x5B ".small" %x5D as role %x23 ((1*(!(%x23 / %x0A) any_char))) as text %x23) as small
139
+
140
+ hard_line_break = (((" +" %x0A) / (%x5C %x0A))) as hard_line_break
141
+
142
+ term_type = ("term" / "alt" / "deprecated" / "domain") as term_type
143
+ stem_type = ("stem" / "latexmath" / "asciimath") as stem_type
144
+
145
+ escaped_xref = (%x5C (%x3C %x3C) as text)
146
+ # \X (X punctuation, minus < for escaped_xref and \ itself)
147
+ # yields the literal X without firing any inline rule (#281)
148
+ escape_char = (%x5C ((%x21-2f / %x3a-3b / %x3d-40 / %x5b / %x5d-60 / %x7b-7e)) as text)
149
+ cross_reference = (((%x3C %x3C) ((1*(!((%x2C / %x3E)) any_char)) as href) [((%x2C) ((*(!((%x3E)) any_char)) as text))] (%x3E %x3E)) as cross_reference)
150
+
151
+ term_inline = (term_type ":[" ((1*(!(%x5D) any_char))) as term %x5D)
152
+ term_inline2 = (%x5B term_type %x5D %x23 ((1*(!(%x23) any_char))) as term2 %x23)
153
+
154
+ macro_kw = ("term" / "alt" / "deprecated" / "domain" / "stem" / "latexmath" / "asciimath")
155
+ macro_content = 1*((!(macro_kw) !(%x5D) any_char) / (macro_kw %x3A %x5B macro_content %x5D))
156
+ footnote = ("footnote:" [(keyword) as id] %x5B ((macro_content)) as footnote %x5D)
157
+
158
+ stem = (stem_type ":[" ((1*(!(%x5D) any_char))) as content %x5D) as stem
159
+
160
+ # constrained/unstrained marks — content keeps Ruby's .as(:text).repeat(1,1)
161
+ # array wrap (1*1) except where Ruby uses a bare .as
162
+ bold_constrained = ((!< WORD %x2A !(%x2A) (1*1(((1*(!(%x2A / %x0A) any_char))) as text)) %x2A !(%x2A) !(WORD)) !(%x0A %x0A)) as bold_constrained
163
+ bold_unconstrained = ((%x2A %x2A (1*1(((1*(!(%x2A) any_char))) as text)) %x2A %x2A)) as bold_unconstrained
164
+ span_constrained = (attribute_list %x23 ((1*(!(%x23 / %x0A) any_char))) as text %x23 !(%x23) !(WORD)) as span_constrained
165
+ span_unconstrained = (attribute_list %x23 %x23 ((1*(!(%x23 / %x0A) any_char))) as text %x23 %x23) as span_unconstrained
166
+ italic_constrained = ((!< WORD %x5F !(%x5F) (1*1(((1*(!(%x5F / %x0A) any_char))) as text)) %x5F !(%x5F) !(WORD))) as italic_constrained
167
+ italic_unconstrained = ((%x5F %x5F (1*1(((1*(!(%x5F) any_char))) as text)) %x5F %x5F)) as italic_unconstrained
168
+ highlight_constrained = ((!< WORD %x23 !(%x23) (1*1(((1*(!(%x23 / %x0A) any_char))) as text)) %x23 !(%x23) !(WORD))) as highlight_constrained
169
+ highlight_unconstrained = ((%x23 %x23 (1*1(((1*(!(%x23) any_char))) as text)) %x23 %x23)) as highlight_unconstrained
170
+ monospace_constrained = ((!< WORD %x60 !(%x60) (1*1((1*((!(%x60 / %x0A) any_char) / (%x60 %x60))) as text)) %x60 !(%x60) !(WORD))) as monospace_constrained
171
+ monospace_unconstrained = ((%x60 %x60 (1*1(((1*(!(%x60) any_char))) as text)) %x60 %x60)) as monospace_unconstrained
172
+ superscript = (%x5E (1*1(((1*(!(%x5E / %x0A) any_char))) as text)) %x5E) as superscript
173
+ subscript = (%x7E (1*1(((1*(!(%x7E / %x0A) any_char))) as text)) %x7E) as subscript
174
+
175
+ inline = (typographic_quote / bold_unconstrained / bold_constrained / span_unconstrained / span_constrained / italic_unconstrained / italic_constrained / highlight_unconstrained / highlight_constrained / monospace_unconstrained / monospace_constrained / superscript / subscript / attribute_reference / escaped_xref / escape_char / cross_reference / term_inline / term_inline2 / footnote / stem / link / inline_image / inline_passthrough / underline / small / hard_line_break)
176
+
177
+ # text.rb/inline.rb text layers
178
+ inline_chars = ((%x5B / %x2A / %x23 / %x5F / %x7B / %x3C / %x5E / %x7E / %x60 / %x5C) / typographic_quote / "https" / "http" / "link:" / "image:" / "+++" / "pass:" / term_type / "footnote" / stem_type / (%x5C %x3C %x3C) / hard_line_break)
179
+ text_formatted = (&(inline_chars) inline)
180
+ text_unformatted = 1*(!("\\" %x3C %x3C) !(inline) !(%x0A) any_char)
181
+ text_any = ((2*((text_formatted / (text_unformatted) as text))) / (1*1(text_formatted)) / text_unformatted)
182
+
183
+ # ---- paragraph.rb ----
184
+ # list.absent? from the DSL's line_not_text? — ported with the
185
+ # empty-item arm (#139): a bare marker line must not be eaten as a
186
+ # continuation line of the previous item.
187
+ literal_space = 1*(%x20 / %x09)
188
+ section_prefix = (%x3D *(%x3D) (!(%x0A) any_char))
189
+ list_prefix = (((%x2A 1*5(%x2A)) / (%x2E 1*5(%x2E))) " ")
190
+ # Any-depth list markers stop continuation text: a nested list
191
+ # (`** b` under `* a`) attaches as `nested`, not continuation.
192
+ line_not_text = (!(list_1) !(ulist_item_1) !(list_2) !(list_3) !(list_4) !(list_5) !(list_prefix) !(list_continuation) !((attribute_list newline)) !(block_delimiter) !((%x7C %x3D %x3D %x3D newline)) !(element_id) !(comment_line) !(tag) !(section_prefix))
193
+ asciidoc_char = (%x2A / %x5F / %x3A / %x2B / %x3D / %x2D)
194
+ asciidoc_char_with_id = (asciidoc_char / (%x5B %x23) / (%x5B %x5B))
195
+ element_id_inline = ((%x5B %x5B (keyword) as id %x5D %x5D) / (%x5B %x23 (keyword) as id %x5D))
196
+ # paragraph_text_line(0|false|true): guard, text_any, then the
197
+ # three-state newline tail (0 = eof only; false = single; true = many)
198
+ # Ruby's (a.absent? | element_id_inline | line_not_text?) reduces under
199
+ # the outer line_not_text guard: arms 1/3 always hold there, and
200
+ # element_id_inline implies asciidoc_char_with_id — so it's just an
201
+ # optional inline id (keeps zero-width branches out of the alternation)
202
+ ptl_guard = (line_not_text [(element_id_inline) [*literal_space]])
203
+ paragraph_text_line_0 = (ptl_guard ((text_any)) as text (!(any_char)))
204
+ paragraph_text_line_s = (ptl_guard ((text_any)) as text ((line_ending) as line_break / (!(any_char))))
205
+ paragraph_text_line_m = (ptl_guard ((text_any)) as text ((newline) as line_break / (!(any_char))))
206
+ paragraph = (block_header ((((1*1(paragraph_text_line_0)) ((newline) as line_break / (!(any_char)))) / ((1*(paragraph_text_line_s)) [(1*1(paragraph_text_line_m) ((newline) as line_break / (!(any_char))))]))) as lines *(newline)) as paragraph
207
+
208
+ # ---- admonition.rb ----
209
+ admonition_type = (%i"note" / %i"tip" / %i"warning" / %i"caution" / %i"important" / %i"editor" / %i"todo" / %i"admonition")
210
+ admonition_line = ((admonition_type as admonition_type) ": " ((1*((text_any as text) (line_ending as line_break))) as content))
211
+
212
+ # ---- list.rb (nesting levels unrolled 1..5) ----
213
+ list_continuation = ("+" %x0A)
214
+ list_item_separator = (*(comment_line_content / empty_line))
215
+ ulist_marker_1 = ( ((1%x2A !(%x2A) !("===")) / (%x2D !(%x2D) !(%x30-39))))
216
+ olist_marker_1 = ( 1%x2E !(%x2E) !((*((%x30-39 / %x2E / %x3C / %x3E / %x5E) / %x2B)) "|"))
217
+ list_marker_1 = (olist_marker_1 / ulist_marker_1)
218
+ ulist_marker_2 = ([*literal_space] (2%x2A !(%x2A) !("===")))
219
+ olist_marker_2 = ([*literal_space] 2%x2E !(%x2E) !((*((%x30-39 / %x2E / %x3C / %x3E / %x5E) / %x2B)) "|"))
220
+ list_marker_2 = (olist_marker_2 / ulist_marker_2)
221
+ ulist_marker_3 = ([*literal_space] (3%x2A !(%x2A) !("===")))
222
+ olist_marker_3 = ([*literal_space] 3%x2E !(%x2E) !((*((%x30-39 / %x2E / %x3C / %x3E / %x5E) / %x2B)) "|"))
223
+ list_marker_3 = (olist_marker_3 / ulist_marker_3)
224
+ ulist_marker_4 = ([*literal_space] (4%x2A !(%x2A) !("===")))
225
+ olist_marker_4 = ([*literal_space] 4%x2E !(%x2E) !((*((%x30-39 / %x2E / %x3C / %x3E / %x5E) / %x2B)) "|"))
226
+ list_marker_4 = (olist_marker_4 / ulist_marker_4)
227
+ ulist_marker_5 = ([*literal_space] (5%x2A !(%x2A) !("===")))
228
+ olist_marker_5 = ([*literal_space] 5%x2E !(%x2E) !((*((%x30-39 / %x2E / %x3C / %x3E / %x5E) / %x2B)) "|"))
229
+ list_marker_5 = (olist_marker_5 / ulist_marker_5)
230
+ inline_lb = (typographic_quote / bold_unconstrained / bold_constrained / span_unconstrained / span_constrained / italic_unconstrained / italic_constrained / highlight_unconstrained / highlight_constrained / monospace_unconstrained / monospace_constrained / superscript / subscript / attribute_reference / escaped_xref / escape_char / cross_reference / term_inline / term_inline2 / footnote / stem / link / inline_image / inline_passthrough / underline / small)
231
+ list_body_hard_break = ((" +" %x0A) / (%x5C %x0A))
232
+ list_body_text_unformatted = (1*(!("\\<<") !(inline_lb) !(list_body_hard_break) !(%x0A) any_char))
233
+ list_body_text_any = (1*(inline_lb / (list_body_text_unformatted as text)))
234
+ list_body_line_ending = (((" +" as hard_line_break) (line_ending as line_break)) / (line_ending as line_break) / !(any_char))
235
+ list_body_text_line = ([*literal_space] (list_body_text_any as text) list_body_line_ending)
236
+ list_item_continuation_lines = (*(line_not_text list_body_text_line))
237
+ list_attached_block = (admonition_line / unordered_list_1 / ordered_list_1 / paragraph / block_3)
238
+ list_item_attached = ((*(&(list_continuation) list_continuation list_attached_block)) as attached)
239
+ # a bare marker line is an empty list item — without this
240
+ # arm it falls through to a continuation text line and corrupts
241
+ # the preceding item.
242
+ ulist_item_1 = (empty_ulist_item_1 / ulist_item_1_nonempty)
243
+
244
+ empty_ulist_item_1 = (&(ulist_marker_1) (((ulist_marker_1 as marker) !(" [[[") (%x0A as lines)) as list_item))
245
+
246
+ ulist_item_1_nonempty = (&(ulist_marker_1) (((ulist_marker_1 as marker) !(" [[[") !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_2) list_2)) as nested)) as list_item))
247
+ olist_item_1 = (&(olist_marker_1) (((olist_marker_1 as marker) !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_2) list_2)) as nested)) as list_item))
248
+ ulist_item_2 = (&(ulist_marker_2) (((ulist_marker_2 as marker) !(" [[[") !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_3) list_3)) as nested)) as list_item))
249
+ olist_item_2 = (&(olist_marker_2) (((olist_marker_2 as marker) !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_3) list_3)) as nested)) as list_item))
250
+ ulist_item_3 = (&(ulist_marker_3) (((ulist_marker_3 as marker) !(" [[[") !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_4) list_4)) as nested)) as list_item))
251
+ olist_item_3 = (&(olist_marker_3) (((olist_marker_3 as marker) !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_4) list_4)) as nested)) as list_item))
252
+ ulist_item_4 = (&(ulist_marker_4) (((ulist_marker_4 as marker) !(" [[[") !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_5) list_5)) as nested)) as list_item))
253
+ olist_item_4 = (&(olist_marker_4) (((olist_marker_4 as marker) !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ((*(&(list_marker_5) list_5)) as nested)) as list_item))
254
+ ulist_item_5 = (&(ulist_marker_5) (((ulist_marker_5 as marker) !(" [[[") !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ) as list_item))
255
+ olist_item_5 = (&(olist_marker_5) (((olist_marker_5 as marker) !(%x0A) space ((list_body_text_line list_item_continuation_lines) as lines) [list_item_attached] ) as list_item))
256
+ unordered_list_1 = ([(attribute_list line_ending)] ((1*(list_item_separator ulist_item_1)) as unordered))
257
+ ordered_list_1 = ([(attribute_list line_ending)] ((1*(list_item_separator olist_item_1)) as ordered))
258
+ list_1 = ((unordered_list_1 / ordered_list_1 / definition_list)) as list
259
+ unordered_list_2 = ([(attribute_list line_ending)] ((1*(list_item_separator ulist_item_2)) as unordered))
260
+ ordered_list_2 = ([(attribute_list line_ending)] ((1*(list_item_separator olist_item_2)) as ordered))
261
+ list_2 = ((unordered_list_2 / ordered_list_2 / definition_list)) as list
262
+ unordered_list_3 = ([(attribute_list line_ending)] ((1*(list_item_separator ulist_item_3)) as unordered))
263
+ ordered_list_3 = ([(attribute_list line_ending)] ((1*(list_item_separator olist_item_3)) as ordered))
264
+ list_3 = ((unordered_list_3 / ordered_list_3 / definition_list)) as list
265
+ unordered_list_4 = ([(attribute_list line_ending)] ((1*(list_item_separator ulist_item_4)) as unordered))
266
+ ordered_list_4 = ([(attribute_list line_ending)] ((1*(list_item_separator olist_item_4)) as ordered))
267
+ list_4 = ((unordered_list_4 / ordered_list_4 / definition_list)) as list
268
+ unordered_list_5 = ([(attribute_list line_ending)] ((1*(list_item_separator ulist_item_5)) as unordered))
269
+ ordered_list_5 = ([(attribute_list line_ending)] ((1*(list_item_separator olist_item_5)) as ordered))
270
+ list_5 = ((unordered_list_5 / ordered_list_5 / definition_list)) as list
271
+ dlist_delimiter = (((":::::" !(%x3A)) / ("::::" !(%x3A)) / (":::" !(%x3A)) / ("::" !(%x3A)) / ";;") as delimiter)
272
+ dlist_term = ((([element_id_inline] ((1*(!(dlist_delimiter) !(%x0A) any_char)) as text)) as dlist_term) dlist_delimiter)
273
+ dlist_definition = ((line_not_text *((%x20 / %x09)) ((list_body_text_line list_item_continuation_lines) as lines)) (*empty_line))
274
+ dlist_item = ((((((1*dlist_term) as terms) line_ending (*empty_line) [dlist_definition]) / (((1*dlist_term) as terms) space dlist_definition)) [list_item_attached] (*(list_continuation &(dlist_item)))) as definition_list_item)
275
+ definition_list = ([(attribute_list line_ending)] ((1*(list_item_separator dlist_item)) as definition_list) !(dlist_item))
276
+
277
+ # ---- header.rb ----
278
+ word = (1*(%x41-5A / %x61-7A / %x30-39 / "_" / "-"))
279
+ words = (word (*([*" "] word)))
280
+ email_r = (word %x40 word %x2E word)
281
+ date_r = (2*4(%x30-39) %x2D 1*2(%x30-39) %x2D 1*2(%x30-39))
282
+ header_title = (%x3D !(%x3D) [*" "] (text as title) newline)
283
+ author_r = ((words as first_name) %x2C [*" "] (words as last_name) [*" "] %x3C (email_r as email) %x3E newline)
284
+ revision_r = (((word [(%x2E word)]) as number) %x2C [*" "] (date_r as date) %x3A [*" "] (words as remark) newline)
285
+ header = (header_title ([author_r] as author) ([revision_r] as revision) *(newline))
286
+
287
+ # ---- document_attributes.rb ----
288
+ doc_attr_name = (1*(%x41-5A / %x61-7A / %x30-39 / "_" / "-"))
289
+ doc_attr_value = (text / !(newline_single))
290
+ document_attribute = (%x3A (doc_attr_name as key) %x3A [*" "] (doc_attr_value as value) (newline_single / !(any_char)))
291
+ document_attributes = ((1*document_attribute) as document_attributes)
292
+
293
+ # ---- bibliography.rb ----
294
+ text_line_g = ([element_id_inline] [*literal_space] (text_any as text) ((newline_single as line_break) / !(any_char)))
295
+ bib_entry = (((!(%x2A) any_char) " [[[" ((1*(! (%x2C / %x5B / %x5D / %x0A) any_char)) as anchor_name) [(%x2C ((1*(! (%x5D / %x0A) any_char)) as document_id))] "]]]" [(((0*1text_line_g) (*text_line_g)) as ref_text)] [((1*newline_single) as line_break)]) as bibliography_entry)
296
+
297
+ # ---- text.rb block_image ----
298
+ attribute_list_macro = (&(%x5B) %x5B !(%x5B) (((named_many / positional_one_named_many / positional_many_named_many / positional_many / positional_zero_or_one) as attribute_array) as attribute_list_macro) %x5D)
299
+ block_image = ((block_header "image::" (file_path as path) attribute_list_macro (newline as line_break)) as block_image)
300
+
301
+ # ---- table.rb (state-driven delimiter) ----
302
+ state tdc: string
303
+ state tdf: string
304
+ dchar = (%x7C / %x21 / %x2C / %x3A / %x3B)
305
+ fspec_char = (%x30-39 / %x2E / %x3C / %x3E / "d" / "s" / "e" / "m" / "a" / "l" / "h" / "v" / %x2B / %x5E)
306
+ cell_format_spec = (1*fspec_char)
307
+ table_start = ((set tdc = (dchar as delim_char)) (set tdf = (2*%x3D)))
308
+ table_close = ((state tdc) (state tdf))
309
+ new_row_signal = (newline_single ((state tdc) / (fspec_char (*fspec_char) (state tdc))))
310
+ # \ + a delim char: state-matched chars drop out of the enclosing
311
+ # capture span (#129 nuance), so match the delim class instead —
312
+ # for the active delim it consumes the same pair, and a \ + other-delim
313
+ # pair consumes the same bytes as two single-char iterations
314
+ escaped_delim = (%x5C dchar)
315
+ cell_content = (*(!table_close !new_row_signal (escaped_delim / (!((state tdc)) any_char))))
316
+ table_cell = ([*literal_space] ([cell_format_spec] as cell_format) (state tdc) (cell_content as text))
317
+ table_row = (((1*(!table_close !new_row_signal (table_cell as cell))) as cells) newline_single)
318
+ table_rows = (1*(!table_close (table_row as row)))
319
+ table = (block_header table_start newline_single (table_rows as rows) table_close (newline_single / !(any_char)))
320
+
321
+ # ---- section.rb (levels 2..8) ----
322
+ endline_r = ((newline as line_break) / ((!(any_char)) as line_break))
323
+ section_header = ([(element_id)] [(attribute_blocks)])
324
+
325
+ section_title_2 = ((((2*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
326
+ section_title_3 = ((((3*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
327
+ section_title_4 = ((((4*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
328
+ section_title_5 = ((((5*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
329
+ section_title_6 = ((((6*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
330
+ section_title_7 = ((((7*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
331
+ section_title_8 = ((((8*8(%x3D))) as level) !(%x3D) [*" "] (text as text) endline_r)
332
+ contents_r = ((1*(bib_entry / block_image / tag / comment_block / comment_line / include_directive / admonition_line / block_3 / (table as table) / (page_break as page_break) / paragraph / list_1 / (empty_line as line_break))) as contents)
333
+ section_block_2 = (section_header (section_title_2 as title) [(contents_r)])
334
+ section_block_3 = (section_header (section_title_3 as title) [(contents_r)])
335
+ section_block_4 = (section_header (section_title_4 as title) [(contents_r)])
336
+ section_block_5 = (section_header (section_title_5 as title) [(contents_r)])
337
+ section_block_6 = (section_header (section_title_6 as title) [(contents_r)])
338
+ section_block_7 = (section_header (section_title_7 as title) [(contents_r)])
339
+ section_block_8 = (section_header (section_title_8 as title) [(contents_r)])
340
+ section_8 = section_block_8
341
+ section_7 = (section_block_7 ((*(section_8 as section)) as sections))
342
+ section_6 = (section_block_6 ((*(section_7 as section)) as sections))
343
+ section_5 = (section_block_5 ((*(section_6 as section)) as sections))
344
+ section_4 = (section_block_4 ((*(section_5 as section)) as sections))
345
+ section_3 = (section_block_3 ((*(section_4 as section)) as sections))
346
+ section_2 = ((section_block_2 ((*(section_3 as section)) as sections)) as section)
347
+
348
+ # ---- block.rb block_container_content (non-verbatim block bodies) ----
349
+ # text_line(false, unguarded: true): no guards, text_any body
350
+ text_line_u = ([*literal_space] (text_any as text) ((newline_single as line_break) / !(any_char)))
351
+ container_3 = ((table as table) / block_image / block_2 / list_1 / text_line_u / (empty_line as line_break))
352
+ container_2 = ((table as table) / block_image / block_1 / list_1 / text_line_u / (empty_line as line_break))
353
+ container_1 = ((table as table) / block_image / list_1 / text_line_u / (empty_line as line_break))
354
+ example_block_3 = (block_header ((set eq_fence = (4*%x3D)) as delimiter) newline ((1*(!((state eq_fence) newline) (container_3 / (empty_line as line_break)))) as lines) (state eq_fence) newline)
355
+ quote_block_3 = (block_header ((set us_fence = (4*%x5F)) as delimiter) newline ((1*(!((state us_fence) newline) (container_3 / (empty_line as line_break)))) as lines) (state us_fence) newline)
356
+ sidebar_block_3 = (block_header ((set ast_fence = (4*%x2A)) as delimiter) newline ((1*(!((state ast_fence) newline) (container_3 / (empty_line as line_break)))) as lines) (state ast_fence) newline)
357
+ open_block_3 = (block_header ((set open_fence = (2%x2D)) as delimiter) newline ((1*(!((state open_fence) newline) (container_3 / (empty_line as line_break)))) as lines) (state open_fence) newline)
358
+ block_3 = ((markdown_code_block / example_block_3 / sidebar_block_3 / source_block / quote_block_3 / pass_block / literal_block / open_cast_3 / open_block_3) as block)
359
+ example_block_2 = (block_header ((set eq_fence = (4*%x3D)) as delimiter) newline ((1*(!((state eq_fence) newline) (container_2 / (empty_line as line_break)))) as lines) (state eq_fence) newline)
360
+ quote_block_2 = (block_header ((set us_fence = (4*%x5F)) as delimiter) newline ((1*(!((state us_fence) newline) (container_2 / (empty_line as line_break)))) as lines) (state us_fence) newline)
361
+ sidebar_block_2 = (block_header ((set ast_fence = (4*%x2A)) as delimiter) newline ((1*(!((state ast_fence) newline) (container_2 / (empty_line as line_break)))) as lines) (state ast_fence) newline)
362
+ open_block_2 = (block_header ((set open_fence = (2%x2D)) as delimiter) newline ((1*(!((state open_fence) newline) (container_2 / (empty_line as line_break)))) as lines) (state open_fence) newline)
363
+ block_2 = ((markdown_code_block / example_block_2 / sidebar_block_2 / source_block / quote_block_2 / pass_block / literal_block / open_cast_2 / open_block_2) as block)
364
+ example_block_1 = (block_header ((set eq_fence = (4*%x3D)) as delimiter) newline ((1*(!((state eq_fence) newline) (container_1 / (empty_line as line_break)))) as lines) (state eq_fence) newline)
365
+ quote_block_1 = (block_header ((set us_fence = (4*%x5F)) as delimiter) newline ((1*(!((state us_fence) newline) (container_1 / (empty_line as line_break)))) as lines) (state us_fence) newline)
366
+ sidebar_block_1 = (block_header ((set ast_fence = (4*%x2A)) as delimiter) newline ((1*(!((state ast_fence) newline) (container_1 / (empty_line as line_break)))) as lines) (state ast_fence) newline)
367
+ open_block_1 = (block_header ((set open_fence = (2%x2D)) as delimiter) newline ((1*(!((state open_fence) newline) (container_1 / (empty_line as line_break)))) as lines) (state open_fence) newline)
368
+ block_1 = ((markdown_code_block / example_block_1 / sidebar_block_1 / source_block / quote_block_1 / pass_block / literal_block / open_cast_1 / open_block_1) as block)
369
+
370
+ # ---- block_style_exact's verbatim cast: a [source]/[listing]/
371
+ # ---- [literal] attribute list turns the open block body verbatim
372
+ source_cast_attr = (&(("[" ("source" / "listing" / "literal"))) attribute_blocks)
373
+ open_cast_3 = ((source_cast_attr) ((set open_fence = (2%x2D)) as delimiter) newline ((1*(!((state open_fence) newline) verbatim_line / (empty_line as line_break))) as lines) (state open_fence) newline)
374
+ open_cast_2 = ((source_cast_attr) ((set open_fence = (2%x2D)) as delimiter) newline ((1*(!((state open_fence) newline) verbatim_line / (empty_line as line_break))) as lines) (state open_fence) newline)
375
+ open_cast_1 = ((source_cast_attr) ((set open_fence = (2%x2D)) as delimiter) newline ((1*(!((state open_fence) newline) verbatim_line / (empty_line as line_break))) as lines) (state open_fence) newline)
376
+
377
+ # ---- base.rb rule(:document) — ported members in Ruby order ----
378
+ # the repetition itself is the capture: parenthesize so `as` binds to
379
+ # the star, not the body (per-iteration capture would fragment the tree)
380
+ document = (*(((header) as header) / document_attributes / section_2 / admonition_line / bib_entry / block_3 / block_image / comment_block / comment_line / include_directive / list_1 / (table) as table / (page_break) as page_break / paragraph / tag / (empty_line as line_break) / (any_char as unparsed))) as document
381
+
382
+ entry document: document
383
+
384
+ test {
385
+ accept "----\ncode\n----"
386
+ accept "----\ncode\n----\n----\nmore\n----"
387
+ accept "------\ncode\n------"
388
+ accept "```ruby\nputs 1\n```"
389
+ accept "````\n```\n````"
390
+ accept ".Title\n----\ncode\n----"
391
+ accept "[source, ruby]\n----\nx = 1\n----"
392
+ accept "////\nblock comment\n////"
393
+ accept "// tag::section-name[]"
394
+ accept "include::file.adoc[leveloffset=1]"
395
+ }
396
+ }
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ # Canonical AsciiDoc formatter (#110): parse to the format model
6
+ # tree, serialize, then normalize inter-block whitespace. Blank
7
+ # lines inside verbatim/example fences are preserved — the pass
8
+ # tracks delimiter pairs and only squeezes outside them.
9
+ module Formatter
10
+ # Delimiter-line fence characters: listings, examples, sidebars,
11
+ # quotes, passthroughs, thematic breaks. A fence is 4+ repetitions
12
+ # of ONE character (CodeQL rb/polynomial-redos: the earlier regex
13
+ # form flagged as polynomial; a same-char scan is linear and also
14
+ # stricter — mixed runs are not fences).
15
+ FENCE_CHARACTERS = %w[- = * _ ' +].freeze
16
+
17
+ module_function
18
+
19
+ def call(text)
20
+ canonical_whitespace(Coradoc::AsciiDoc.parse(text).to_adoc)
21
+ end
22
+
23
+ def canonical_whitespace(adoc)
24
+ out = []
25
+ blanks = 0
26
+ fence = nil
27
+
28
+ adoc.lines.each do |line|
29
+ stripped = line.strip
30
+
31
+ if fence
32
+ out << line
33
+ fence = nil if stripped == fence && fence_delimiter(stripped)
34
+ elsif fence_delimiter(stripped)
35
+ emit_blanks(out, blanks)
36
+ blanks = 0
37
+ out << line
38
+ fence = stripped
39
+ elsif stripped.empty?
40
+ blanks += 1
41
+ else
42
+ emit_blanks(out, blanks)
43
+ blanks = 0
44
+ out << line
45
+ end
46
+ end
47
+ joined = out.join
48
+ return joined if joined.empty?
49
+
50
+ # Exactly one trailing newline — a reverse scan, not a regex
51
+ # (CodeQL rb/polynomial-redos models /\n+\z/ as polynomial).
52
+ end_index = joined.length
53
+ end_index -= 1 while end_index.positive? && joined[end_index - 1] == "\n"
54
+ "#{joined[0, end_index]}\n"
55
+ end
56
+
57
+ # The delimiter character when +stripped+ is a fence line
58
+ # (4+ repetitions of one fence character); nil otherwise.
59
+ def fence_delimiter(stripped)
60
+ return nil unless stripped.length >= 4
61
+
62
+ first = stripped[0]
63
+ return nil unless FENCE_CHARACTERS.include?(first)
64
+
65
+ stripped.each_char.all?(first) ? first : nil
66
+ end
67
+
68
+ # Exactly one blank line between blocks; none before the first
69
+ # content line.
70
+ def emit_blanks(out, blanks)
71
+ out << "\n" if blanks.positive? && !out.empty?
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Lint
6
+ # ADOC002 — a document should have exactly one title: the
7
+ # level-0 `=` heading carried by the document header.
8
+ class DocumentTitle < Coradoc::Lint::Rule
9
+ rule_id 'ADOC002'
10
+ applies_to :asciidoc
11
+
12
+ def check(document, path)
13
+ title = document.header&.title
14
+ text = case title
15
+ when Coradoc::AsciiDoc::Model::Title then title.content
16
+ when String then title
17
+ end
18
+ return [] if text && !text.strip.empty?
19
+
20
+ [build_violation(path: path,
21
+ message: 'document has no level-0 title (= ...)')]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Lint
6
+ # ADOC005 — section ids must be unique: duplicated anchors
7
+ # break cross-references.
8
+ class DuplicateIds < Coradoc::Lint::Rule
9
+ rule_id 'ADOC005'
10
+ applies_to :asciidoc
11
+
12
+ def check(document, path)
13
+ ids = Hash.new(0)
14
+ collect(document, ids)
15
+ ids.select { |_id, count| count > 1 }
16
+ .map do |id, count|
17
+ build_violation(path: path,
18
+ message: "duplicate section id #{id.inspect} used #{count} times")
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def collect(node, ids)
25
+ Array(node.sections).each do |child|
26
+ case child
27
+ when Coradoc::AsciiDoc::Model::Section
28
+ ids[child.id] += 1 if child.id
29
+ collect(child, ids)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Lint
6
+ # ADOC003 — sections should carry content: neither block
7
+ # content nor subsections means an empty heading.
8
+ class EmptySection < Coradoc::Lint::Rule
9
+ rule_id 'ADOC003'
10
+ applies_to :asciidoc
11
+
12
+ def check(document, path)
13
+ violations = []
14
+ walk(document, path, violations)
15
+ violations
16
+ end
17
+
18
+ private
19
+
20
+ def walk(node, path, violations)
21
+ Array(node.sections).each do |child|
22
+ case child
23
+ when Coradoc::AsciiDoc::Model::Section
24
+ if empty?(child)
25
+ violations << build_violation(
26
+ path: path,
27
+ message: "empty section: #{child.title&.content}"
28
+ )
29
+ end
30
+ walk(child, path, violations)
31
+ end
32
+ end
33
+ end
34
+
35
+ def empty?(section)
36
+ Array(section.contents).empty? && sections_of(section).empty?
37
+ end
38
+
39
+ def sections_of(section)
40
+ Array(section.sections).grep(Coradoc::AsciiDoc::Model::Section)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Lint
6
+ # ADOC001 — heading levels must not skip: a section may be at
7
+ # most one level deeper than its parent. Skipped levels produce
8
+ # broken document outlines.
9
+ class HeadingLevels < Coradoc::Lint::Rule
10
+ rule_id 'ADOC001'
11
+ applies_to :asciidoc
12
+
13
+ def check(document, path)
14
+ violations = []
15
+ sections(document).each { |s| walk(s, 0, path, violations) }
16
+ violations
17
+ end
18
+
19
+ private
20
+
21
+ def walk(section, parent_level, path, violations)
22
+ level = section.title&.level_int
23
+ if level && level > parent_level + 1
24
+ violations << build_violation(
25
+ path: path,
26
+ message: "heading level skips from #{parent_level} to #{level}: " \
27
+ "#{title_text(section)}"
28
+ )
29
+ end
30
+ sections(section).each { |child| walk(child, level || parent_level, path, violations) }
31
+ end
32
+
33
+ def sections(node)
34
+ Array(node.sections).grep(Coradoc::AsciiDoc::Model::Section)
35
+ end
36
+
37
+ def title_text(section)
38
+ title = section.title
39
+ title.respond_to?(:content) ? title.content : title.to_s
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end