rouge-carve 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4006805a5023dc8b016745f1ab3dc73bf29fa24de27acc46c19696658a9ee36
4
- data.tar.gz: 85965e83d476add300683d7e5ce7464d6affb784b067b3797318ef45575d5c78
3
+ metadata.gz: 4b5dd23d4f12415b4c19ed7333a5bd1555ae21e4f547b4f0a8f52b7829f8ad94
4
+ data.tar.gz: 1f9a6066c9c55b5457eda908909ff6b865d84fedaebbe18a37fdbe4f9e62a76d
5
5
  SHA512:
6
- metadata.gz: eb1fb40ca27583ebb19f1af32bf208efcdd5d21c2f880f5e400f4c06562d4a79a977988008ec054d448d1e7ac9c0020acdce83db8b2cd19d0bcb6912878896dd
7
- data.tar.gz: 7bf48fae5c12930737941c834ec4347dcd5f1a30a77e4cbcbdf99e8fa20c4d90c39d9f45bfaa9d1be3a2edea3d1025ef1f31f5ffa3ecbfade9212e0a4a4811ff
6
+ metadata.gz: bdc875e2a50141600798ff0efcd8741e751516dbb3de632f55039a62e881f4edbe4561942e8b39228e6603c1e6cef428d5320323b2617213d5f8ceed1053e550
7
+ data.tar.gz: 8d1983500c85ecef17ac25bac9aad4ebf76b477bf75ee721d1343ef09a82cf424c86c4bf9cb65b948361183bf97cce2bffe90630f20fa519368d5253fed1f13c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.3] - 2026-10-14
6
+
7
+ 0.1.2 was tagged without its `RougeCarve::VERSION` bump, so it never reached RubyGems. 0.1.3 is the first published release with its change.
8
+
9
+ - An include selector whose id starts with a digit is lexed as a selector. `{{ ch.crv #2024-plan }}` used to fall through to plain text (#15, #16).
10
+ - A footnote or abbreviation definition ends with its line. It used to turn the rest of the file into plain text, so no later heading, list or fence was recognized (#22).
11
+ - An inline footnote `^[` or extension `:name[` with no closing `]` in its block stays text instead of swallowing the blocks after it. A `]` inside a code span does not count, and a heading, caption or table cell keeps the body on its line (#22, #24).
12
+ - A blockquote line can open a block: `> # Heading`, `> - item`, and a fence whose lines carry the `>` prefix (#22).
13
+ - A `>` marks a quote only when a space or the line end follows it, so `>> x` is paragraph text and `> > x` nests, as in the engine (#22).
14
+ - A comment fence closes only on a `%` run of its own length, which may be followed by text. An opener with no such closer is a one-line comment, as in the engine (#22).
15
+ - A standalone attribute block no longer runs across a blank line (#22).
16
+ - Bare delimiters follow Carve's flanking rules, so `snake_case_name`, `/usr/local/bin`, `x=1 and y=2`, `2*3*4` and the text between `==>` arrows stay text (#23).
17
+ - Emphasis content is lexed inline: an escape, a code span, a link or another kind of emphasis inside a run keeps its own token, and `*a \* b*` closes where the engine closes it (#23). A run opened inside another must close before it, so `*a /b* c/` is strong `a /b` (#24).
18
+ - A list marker or a caption under a paragraph line stays text, as in the engine; headings, quotes, tables, containers and closed fences still interrupt it. A wrapped definition term keeps its definition (#23).
19
+ - A table row needs its closing pipe, `+` is no longer read as a bullet, and an alphabetic ordered marker is one letter or a roman numeral, so `Body.` is a word (#23).
20
+ - Lexing a long line of unclosed openers (`*a *a *a ...`, `{{ a {{ a ...`) is linear in its length; it used to be quadratic (#23).
21
+ - The gem now requires Rouge 3.30 or later. Rouge 3.0, which the old constraint admitted, cannot load the lexer (#23).
22
+
23
+ ## [0.1.2] - 2026-10-08
24
+
25
+ - An include selector whose id starts with a digit is lexed as a selector. `{{ ch.crv #2024-plan }}` used to fall through to plain text, because the lexer required a letter or underscore first while the spec's `explicit_identifier` has admitted a leading digit since carve 0.1.8 (#15).
26
+
3
27
  ## [0.1.1] - 2026-09-21
4
28
 
5
29
  - The reserved include directive `{{ path #section @key:value }}` (PART 9
data/README.md CHANGED
@@ -14,6 +14,8 @@ Rouge::Lexer.find('carve') # => Rouge::Lexers::Carve
14
14
  Rouge::Lexer.guess(filename: 'notes.crv') # => Rouge::Lexers::Carve
15
15
  ```
16
16
 
17
+ It needs Rouge 3.30 or later and is tested against Rouge 3, 4 and 5.
18
+
17
19
  Requiring the gem registers the lexer. Anything that already highlights with
18
20
  Rouge - GitLab, Jekyll, Redcarpet, most Ruby static site generators - then
19
21
  handles `.crv` and `.carve` files, and ` ```carve ` fenced blocks, without
@@ -3,7 +3,8 @@
3
3
 
4
4
  module Rouge
5
5
  module Lexers
6
- # Carve is a post-Markdown lightweight markup language. Its inline
6
+ # Carve is a lightweight markup language for documents and the web. Its
7
+ # inline
7
8
  # delimiters deliberately differ from Markdown's, which is why lexing a
8
9
  # Carve document as Markdown produces actively WRONG output rather than
9
10
  # merely plain text:
@@ -48,7 +49,7 @@ module Rouge
48
49
  # Spec: https://markup-carve.github.io/carve/
49
50
  class Carve < RegexLexer
50
51
  title 'Carve'
51
- desc 'Carve, a post-Markdown lightweight markup language'
52
+ desc 'Carve, a lightweight markup language for documents and the web'
52
53
 
53
54
  tag 'carve'
54
55
  aliases 'crv'
@@ -62,28 +63,37 @@ module Rouge
62
63
  # One attribute block, brace to brace. Quoted values may contain a brace
63
64
  # and an escaped quote, so the value alternatives come before the
64
65
  # bare-character one; a single nested brace pair is allowed for a braced
65
- # span written inside.
66
- ATTRS = %r/
67
- \{(?=[.:}'"]|\#[\w-]+[\s}]|[A-Za-z][\w-]*(?:[=\s}]|$))(?:
66
+ # span written inside. The tail is the block after its `{`.
67
+ ATTRS_TAIL = %r/
68
+ (?=[.:}'"]|\#[\w-]+[\s}]|[A-Za-z][\w-]*(?:[=\s}]|$))(?:
68
69
  "(?:[^"\\\n]|\\.)*"
69
70
  |'(?:[^'\\\n]|\\.)*'
70
71
  |\{[^{}\n]*\}
71
72
  |[^{}\n]
72
73
  )*\}
73
74
  /x.freeze
75
+ ATTRS = /\{#{ATTRS_TAIL}/.freeze
74
76
 
75
- # The same block as a STANDALONE ATTRIBUTE LINE, which may span lines.
76
- # The inline form deliberately cannot, because an unclosed inline `{`
77
- # would otherwise swallow the rest of the document.
77
+ # The same block as a STANDALONE ATTRIBUTE LINE, which may span lines
78
+ # but not a blank one: a blank line ends the block it would attach to.
79
+ # The inline form cannot span lines at all, because an unclosed inline
80
+ # `{` would otherwise swallow the rest of the document.
81
+ LINE_BREAK = '\n(?![ \t]*(?:\n|\z))'
78
82
  ATTRS_LINE = %r/
79
83
  \{(?=[.:}'"]|\#[\w-]+[\s}]|[A-Za-z][\w-]*(?:[=\s}]|$))(?:
80
- "(?:[^"\\]|\\.)*"
81
- |'(?:[^'\\]|\\.)*'
82
- |\{[^{}]*\}
83
- |[^{}]
84
+ "(?:[^"\\\n]|\\.|#{LINE_BREAK})*"
85
+ |'(?:[^'\\\n]|\\.|#{LINE_BREAK})*'
86
+ |\{(?:[^{}\n]|#{LINE_BREAK})*\}
87
+ |[^{}\n]
88
+ |#{LINE_BREAK}
84
89
  )*\}
85
90
  /x.freeze
86
91
 
92
+ # The prefix of a line inside a blockquote, nested markers included.
93
+ # Each `>` needs a space or the line end after it: `>foo` and `>> x`
94
+ # are paragraph text.
95
+ QUOTE_PREFIX = /(?:[ \t]*>(?=[ \t]|$))+[ \t]?/.freeze
96
+
87
97
  # A bracketed label that may itself contain three levels of brackets. A
88
98
  # regex cannot match arbitrarily balanced brackets, and a link label in
89
99
  # practice nests a level or two (`[t[z]](/u)`). Bounding the nesting keeps
@@ -96,6 +106,69 @@ module Rouge
96
106
  Regexp.new("\\[#{pattern}\\]").freeze
97
107
  end
98
108
 
109
+ # An ordered marker: a number, one letter, or a roman numeral. `Body.`
110
+ # is a word, not a marker.
111
+ ORDERED = '(?:\\d+|[A-Za-z]|[ivxlcdm]+|[IVXLCDM]+)[.)]'
112
+
113
+ # A list marker at the start of a line, as the list rules below read it.
114
+ LIST_MARKER = "(?:[-*]|#{ORDERED}|\\.)(?:#{ATTRS})?(?:[ \\t]|$)"
115
+
116
+ # A table row needs its closing pipe: `| not a table` is a paragraph.
117
+ ROW_END = /(?=(?:\\.|[^\\\n])*\|[ \t]*(?:#{ATTRS})?[ \t]*$)/.freeze
118
+
119
+ # The block openers that interrupt a paragraph. A list marker and a
120
+ # caption do not, and a fence does only when it is closed - that one is
121
+ # checked in code, see the :paraline state.
122
+ INTERRUPT = %r/
123
+ \#{1,6}[ \t] | >(?:[ \t]|$) | ::: | ::[ \t] | %%
124
+ | (?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$
125
+ | \|=?[<>^v~]*#{ROW_END}
126
+ | (?:\[\^|\*\[|\[)[^\]\n]+\]:[ ]
127
+ | #{ATTRS_LINE}[ \t]*$
128
+ /x.freeze
129
+
130
+ # Where a bracketed inline body must close, by the block it sits in: a
131
+ # paragraph ends at a blank line or an interrupting block, a list item
132
+ # also at the next marker, and a heading or table row at its line end.
133
+ # A fence ends either only when it is closed, which is checked in code.
134
+ BODY_ENDS = {
135
+ para: /\n(?=[ \t]*(?:\n|\z)|#{MARGIN}(?:#{INTERRUPT}))/,
136
+ item: /\n(?=[ \t]*(?:\n|\z)|#{MARGIN}(?:#{INTERRUPT}|#{LIST_MARKER}|:[ \t]))/,
137
+ line: /\n/
138
+ }.freeze
139
+ FENCE_NEXT = /\n(?=#{MARGIN}(`{3,}|~{3,}))/.freeze
140
+
141
+ # The block states that decide which of those ends applies.
142
+ BODY_SCOPE = {
143
+ paragraph: :para, listitem: :item, dlterm: :item, heading: :line,
144
+ caption: :line, quoteline: :line, tablerow: :line, defline: :line
145
+ }.freeze
146
+
147
+ # A paragraph that is a lone image or display math: a caption under it
148
+ # is that block's.
149
+ IMAGE_LINE = %r/
150
+ #{MARGIN}(?:!#{LABEL}(?:\([^)\n]*\)|#{LABEL})?|\$\$(`+)[^\n]*?\1)(?:#{ATTRS})?[ \t]*$
151
+ /x.freeze
152
+
153
+ # The braced inline families: delimiter, content token, state.
154
+ BRACED = {
155
+ '*' => [Generic::Strong, :forcedstrong], '/' => [Generic::Emph, :forcedemph],
156
+ '_' => [Generic::Emph, :forcedunderline], '^' => [Generic::Emph, :superscript],
157
+ ',' => [Generic::Emph, :subscript], '=' => [Generic::Inserted, :bracedmark],
158
+ '+' => [Generic::Inserted, :insertion], '-' => [Generic::Deleted, :deletion]
159
+ }.freeze
160
+
161
+ # The bare emphasis delimiters: state, content token, and the characters
162
+ # that may not stand before an opener or after it. A closer needs a
163
+ # non-space before it and no letter or digit after it.
164
+ BARE = {
165
+ '*' => [:strong, Generic::Strong, /[\w*]/, /[\s*]/],
166
+ '/' => [:emph, Generic::Emph, /[\w\/]/, /[\s\/]/],
167
+ '_' => [:underline, Generic::Emph, /[\w_\/]/, /[\s_]/],
168
+ '~' => [:strike, Generic::Deleted, /[\w~]/, /[\s~]/],
169
+ '=' => [:mark, Generic::Inserted, /[\w=<>!]/, /[\s>=]/]
170
+ }.freeze
171
+
99
172
  # Characters that can begin an inline construct. A run of anything else is
100
173
  # ordinary content and is emitted as ONE token - without this every
101
174
  # content character becomes its own token, which is both noisy and
@@ -118,6 +191,147 @@ module Rouge
118
191
  # ------------------------------------------------------------------
119
192
  # Block level
120
193
  # ------------------------------------------------------------------
194
+
195
+ # The openers a blockquote marker can introduce as well as a line start.
196
+ # They are written once and instantiated twice: anchored at a line start
197
+ # for the document, and unanchored for the rest of a line after `>`,
198
+ # where the scanner already sits where the block would begin. Each one
199
+ # enters its state through #open_block, which knows which of the two it
200
+ # is running in.
201
+ def self.block_openers(bol)
202
+ proc do
203
+ # A raw block: the `=FORMAT` info string routes the payload to that
204
+ # output format verbatim. Emitted as one token including the `=`,
205
+ # because the format word without its sigil is not the construct.
206
+ # The payload is never a language, so it is never delegated.
207
+ rule %r/#{bol}(#{MARGIN})(`{3,}|~{3,})([ \t]*)(=[a-zA-Z][\w+.-]*)([^\n]*)/ do |m|
208
+ groups Text, Punctuation, Text, Keyword::Type
209
+ token Text, m[5]
210
+ push_fence(m[2], nil)
211
+ end
212
+
213
+ # A code fence. The first word of the info string names a language,
214
+ # and a lexer for it highlights the payload - the one place this
215
+ # lexer looks past Carve, because a reader of a `.crv` file expects
216
+ # an embedded PHP sample to look like PHP.
217
+ rule %r/#{bol}(#{MARGIN})(`{3,}|~{3,})([ \t]*)([a-zA-Z][\w+#.-]*)?([^\n]*)/ do |m|
218
+ groups Text, Punctuation, Text, Name::Builtin
219
+ lex_info_string(m[5])
220
+ push_fence(m[2], m[4])
221
+ end
222
+
223
+ # Container divs. A reserved kind word (note, tip, figure, ...)
224
+ # names a known container; `:::` followed by `|` is the layout form.
225
+ rule %r/#{bol}(#{MARGIN})(:{3,})([ \t]*)(\|)/ do
226
+ groups Text, Punctuation, Text, Operator
227
+ open_block
228
+ end
229
+ rule %r/#{bol}(#{MARGIN})(:{3,})([ \t]*)([a-zA-Z][\w-]*)?([^\n]*)/ do |m|
230
+ groups Text, Punctuation, Text, Keyword::Namespace
231
+ lex_info_string(m[5])
232
+ open_block
233
+ end
234
+
235
+ # A caption line attaches to the block above or below it.
236
+ rule %r/#{bol}(#{MARGIN})(\^)([ \t]+)/ do
237
+ groups Text, Punctuation, Text
238
+ open_block :caption
239
+ end
240
+
241
+ # Headings. Carve has no setext form, so a `#` run is the only
242
+ # spelling and a trailing `{...}` is NOT an attribute block here.
243
+ rule %r/#{bol}(#{MARGIN})(\#{1,6})([ \t]+)/ do
244
+ groups Text, Punctuation, Text
245
+ open_block :heading
246
+ end
247
+
248
+ # Thematic breaks, before the list rules so `---` is not a bullet.
249
+ rule %r/#{bol}#{MARGIN}(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/ do
250
+ token Punctuation
251
+ open_block
252
+ end
253
+
254
+ # Definition markers: footnote, link reference, abbreviation. The
255
+ # separator after the colon must START WITH A LITERAL SPACE - a
256
+ # tab-first separator makes the line an ordinary paragraph. Each
257
+ # definition ends with its line.
258
+ rule %r/#{bol}(#{MARGIN})(\[\^)([^\]\n]+)(\]:)( )/ do
259
+ groups Text, Punctuation, Name::Label, Punctuation, Text
260
+ open_block :defline
261
+ end
262
+ rule %r/#{bol}(#{MARGIN})(\*\[)([^\]\n]+)(\]:)( )/ do
263
+ groups Text, Punctuation, Name::Entity, Punctuation, Text
264
+ open_block :defline
265
+ end
266
+ rule %r/#{bol}(#{MARGIN})(\[)([^\]\n]+)(\]:)( )/ do
267
+ groups Text, Punctuation, Name::Label, Punctuation, Text
268
+ open_block :linkdest
269
+ end
270
+
271
+ # A definition-list term (`::`) and its definition (`:`). A term
272
+ # may wrap onto lazy lines, and a definition's text is an item's.
273
+ rule %r/#{bol}(#{MARGIN})(::)([ \t]+)/ do
274
+ groups Text, Punctuation, Text
275
+ open_block :dlterm, :heading
276
+ end
277
+ rule %r/#{bol}(#{MARGIN})(:)(?=[ \t])/ do
278
+ groups Text, Punctuation
279
+ open_block :listitem, :quoteline
280
+ end
281
+
282
+ # A blockquote marker must be followed by a space or end the line;
283
+ # `>foo` and `>> x` are paragraphs, and `> > x` nests. What follows
284
+ # a marker may open a block of its own, so the openers are tried
285
+ # again there.
286
+ rule %r/#{bol}(#{MARGIN})(>)(?=[ \t]|$)/ do
287
+ groups Text, Punctuation
288
+ state?(:quotestart) ? goto(:quotestart) : push(:quotestart)
289
+ end
290
+
291
+ # Task items before plain bullets, so the state marker is its own
292
+ # token. The state is any single character, not only a space or an
293
+ # x: `[>]` is deferred and `[-]` is dropped. A `+` is not a bullet in
294
+ # Carve; it continues a container.
295
+ rule %r/#{bol}(#{MARGIN})((?:[-*]|#{ORDERED})(?:#{ATTRS})?)([ \t]+)(\[[^\]\n]\])/ do
296
+ groups Text, Punctuation, Text, Name::Constant
297
+ open_block :listitem, :quoteline
298
+ end
299
+
300
+ # Bullets. A run of markers on one line opens nested lists at once
301
+ # (`- - A`), and attributes may be glued straight onto the marker.
302
+ rule %r/#{bol}(#{MARGIN})((?:[-*][ \t]+)*[-*](?:#{ATTRS})?)(?=[ \t]|$)/ do
303
+ groups Text, Punctuation
304
+ open_block :listitem, :quoteline
305
+ end
306
+
307
+ # Ordered markers: numeric, alphabetic, roman, and the bare `.` that
308
+ # continues the enclosing sequence.
309
+ rule %r/#{bol}(#{MARGIN})(#{ORDERED}(?:#{ATTRS})?)(?=[ \t]|$)/ do
310
+ groups Text, Num::Integer
311
+ open_block :listitem, :quoteline
312
+ end
313
+ rule %r/#{bol}(#{MARGIN})(\.(?:#{ATTRS})?)(?=[ \t]|$)/ do
314
+ groups Text, Num::Integer
315
+ open_block :listitem, :quoteline
316
+ end
317
+
318
+ # Tables. The header marker, the alignment run and the separator row
319
+ # are their own tokens; cell content is lexed inline. A row needs its
320
+ # closing pipe.
321
+ rule %r/#{bol}(#{MARGIN})(\|=[<>^v~]*)#{ROW_END}/ do
322
+ groups Text, Operator
323
+ open_block :tablerow
324
+ end
325
+ rule %r/#{bol}(#{MARGIN})(\|)#{ROW_END}/ do
326
+ groups Text, Punctuation
327
+ open_block :tablerow
328
+ end
329
+ end
330
+ end
331
+
332
+ state(:blockopeners, &block_openers('^'))
333
+ state(:quoteopeners, &block_openers(''))
334
+
121
335
  state :block do
122
336
  # Front matter, only at the very start of the document. \A is what
123
337
  # keeps a `---yaml` line mid-document from opening one.
@@ -127,134 +341,97 @@ module Rouge
127
341
  end
128
342
 
129
343
  # A comment fence (%%% or longer) versus a one-line comment. The fence
130
- # has to be tried first: %%% also matches the one-line form.
131
- rule %r/^(#{MARGIN})(%%%+)([^\n]*)(\n)/ do
344
+ # has to be tried first: %%% also matches the one-line form. Only a
345
+ # run of the SAME length closes it, so the width is closed over the
346
+ # way #push_fence does it, and an opener with no such closer below it
347
+ # is the one-line form. Text after either run is part of the comment.
348
+ rule %r/^(#{MARGIN})(%%%+)([^\n]*)(\n)/ do |m|
349
+ closer = %r/^([ \t]*)(%{#{m[2].length}})(?!%)([^\n]*)$/
350
+ unless closer_below?(closer, m[2].length)
351
+ # The tokens of the one-line rule below. Rouge 3 has no
352
+ # fallthrough! to hand the line on to it.
353
+ token Text, m[1]
354
+ token Comment::Preproc, '%%'
355
+ token Comment, m[2][2..] + m[3]
356
+ token Text, m[4]
357
+ next
358
+ end
359
+
132
360
  groups Text, Comment::Preproc, Comment, Text
133
- push :commentfence
361
+ push do
362
+ rule closer do
363
+ groups Text, Comment::Preproc, Comment
364
+ pop!
365
+ end
366
+ rule %r/\n/, Comment
367
+ rule %r/[^\n]+\n?/, Comment
368
+ end
134
369
  end
135
370
  rule %r/^(#{MARGIN})(%%)([^\n]*)$/ do
136
371
  groups Text, Comment::Preproc, Comment
137
372
  end
138
373
 
139
- # A raw block: the `=FORMAT` info string routes the payload to that
140
- # output format verbatim. Emitted as one token including the `=`,
141
- # because the format word without its sigil is not the construct. The
142
- # payload is never a language, so it is never delegated.
143
- rule %r/^(#{MARGIN})(`{3,}|~{3,})([ \t]*)(=[a-zA-Z][\w+.-]*)([^\n]*)/ do |m|
144
- groups Text, Punctuation, Text, Keyword::Type
145
- token Text, m[5]
146
- push_fence(m[2], nil)
147
- end
148
-
149
- # A code fence. The first word of the info string names a language, and
150
- # a lexer for it highlights the payload - the one place this lexer
151
- # looks past Carve, because a reader of a `.crv` file expects an
152
- # embedded PHP sample to look like PHP.
153
- rule %r/^(#{MARGIN})(`{3,}|~{3,})([ \t]*)([a-zA-Z][\w+#.-]*)?([^\n]*)/ do |m|
154
- groups Text, Punctuation, Text, Name::Builtin
155
- lex_info_string(m[5])
156
- push_fence(m[2], m[4])
157
- end
158
-
159
- # Container divs. A reserved kind word (note, tip, figure, ...) names a
160
- # known container; `:::` followed by `|` is the layout form.
161
- rule %r/^(#{MARGIN})(:{3,})([ \t]*)(\|)/ do
162
- groups Text, Punctuation, Text, Operator
163
- end
164
- rule %r/^(#{MARGIN})(:{3,})([ \t]*)([a-zA-Z][\w-]*)?([^\n]*)/ do |m|
165
- groups Text, Punctuation, Text, Keyword::Namespace
166
- lex_info_string(m[5])
167
- end
168
-
169
- # A caption line attaches to the block above or below it.
170
- rule %r/^(#{MARGIN})(\^)([ \t]+)/ do
171
- groups Text, Punctuation, Text
172
- push :caption
173
- end
174
-
175
- # Headings. Carve has no setext form, so a `#` run is the only spelling
176
- # and a trailing `{...}` is NOT an attribute block here.
177
- rule %r/^(#{MARGIN})(\#{1,6})([ \t]+)/ do
178
- groups Text, Punctuation, Text
179
- push :heading
180
- end
181
-
182
- # Thematic breaks, before the list rules so `---` is not a bullet.
183
- rule %r/^#{MARGIN}(?:\*[ \t]*){3,}$/, Punctuation
184
- rule %r/^#{MARGIN}(?:-[ \t]*){3,}$/, Punctuation
185
- rule %r/^#{MARGIN}(?:_[ \t]*){3,}$/, Punctuation
374
+ mixin :blockopeners
186
375
 
187
- # Definition markers: footnote, link reference, abbreviation. The
188
- # separator after the colon must START WITH A LITERAL SPACE - a
189
- # tab-first separator makes the line an ordinary paragraph.
190
- rule %r/^(#{MARGIN})(\[\^)([^\]\n]+)(\]:)( )/ do
191
- groups Text, Punctuation, Name::Label, Punctuation, Text
192
- push :inline
193
- end
194
- rule %r/^(#{MARGIN})(\*\[)([^\]\n]+)(\]:)( )/ do
195
- groups Text, Punctuation, Name::Entity, Punctuation, Text
196
- push :inline
197
- end
198
- rule %r/^(#{MARGIN})(\[)([^\]\n]+)(\]:)( )/ do
199
- groups Text, Punctuation, Name::Label, Punctuation, Text
200
- push :linkdest
201
- end
202
-
203
- # A definition-list term (`::`) and its definition (`:`).
204
- rule %r/^(#{MARGIN})(::)([ \t]+)/ do
205
- groups Text, Punctuation, Text
206
- push :heading
207
- end
208
- rule %r/^(#{MARGIN})(:)(?=[ \t])/ do
209
- groups Text, Punctuation
376
+ # A standalone attribute block, which may span lines.
377
+ rule %r/^(#{MARGIN})(#{ATTRS_LINE})/ do
378
+ groups Text, Name::Attribute
210
379
  end
211
380
 
212
- # A blockquote marker must be followed by a space or end the line;
213
- # `>foo` is a paragraph.
214
- rule %r/^(#{MARGIN})(>+)(?=[ \t]|$)/ do
215
- groups Text, Punctuation
216
- push :quoteline
381
+ # Any other line with content starts a paragraph.
382
+ rule %r/^(?=#{MARGIN}\S)/ do
383
+ @lone_image = !@current_stream.check(IMAGE_LINE).nil?
384
+ @indented = @current_stream.check(/[ \t]/) ? true : false
385
+ push :paragraph
217
386
  end
218
387
 
219
- # Task items before plain bullets, so the state marker is its own
220
- # token. The state is any single character, not only a space or an x:
221
- # `[>]` is deferred and `[-]` is dropped.
222
- rule %r/^(#{MARGIN})((?:[-*+]|\d+[.)]|[A-Za-z]+[.)])(?:#{ATTRS})?)([ \t]+)(\[[^\]\n]\])/ do
223
- groups Text, Punctuation, Text, Name::Constant
224
- end
388
+ mixin :inline
389
+ end
225
390
 
226
- # Bullets. A run of markers on one line opens nested lists at once
227
- # (`- - A`), and attributes may be glued straight onto the marker.
228
- rule %r/^(#{MARGIN})((?:[-*+][ \t]+)*[-*+](?:#{ATTRS})?)(?=[ \t]|$)/ do
229
- groups Text, Punctuation
230
- end
391
+ # A paragraph, and the start of each line after its first: a line that
392
+ # opens a block interrupting it ends it, anything else continues it, so
393
+ # a list marker or a caption under prose is prose.
394
+ state :paragraph do
395
+ rule(/\n/) { token Text; goto :paraline }
396
+ mixin :inlinecontent
397
+ rule PLAIN, Text
398
+ rule %r/./, Text
399
+ end
231
400
 
232
- # Ordered markers: numeric, alphabetic, roman, and the bare `.` that
233
- # continues the enclosing sequence.
234
- rule %r/^(#{MARGIN})((?:\d+|[A-Za-z]+)[.)](?:#{ATTRS})?)(?=[ \t]|$)/ do
235
- groups Text, Num::Integer
236
- end
237
- rule %r/^(#{MARGIN})(\.(?:#{ATTRS})?)(?=[ \t]|$)/ do
238
- groups Text, Num::Integer
239
- end
401
+ state :paraline do
402
+ rule(%r/^(?=[ \t]*(?:\n|\z))/) { pop! }
403
+ rule(%r/^(?=#{MARGIN}(?:#{INTERRUPT}))/) { pop! }
404
+ rule(%r/^(?=#{MARGIN}(`{3,}|~{3,}))/) { |m| fence_closed_below?(m[1]) ? pop! : continue_paragraph(:paragraph) }
405
+ rule(%r/^(?=#{MARGIN}\^[ \t])/) { @lone_image ? pop! : continue_paragraph(:paragraph) }
406
+ # An indented paragraph sits in a container, so a list marker below it
407
+ # opens a sublist or the next item rather than continuing it.
408
+ rule(%r/^(?=#{MARGIN}#{LIST_MARKER})/) { @indented ? pop! : continue_paragraph(:paragraph) }
409
+ rule(//) { continue_paragraph(:paragraph) }
410
+ end
240
411
 
241
- # Tables. The header marker, the alignment run and the separator row
242
- # are their own tokens; cell content is lexed inline.
243
- rule %r/^(#{MARGIN})(\|=[<>^v~]*)/ do
244
- groups Text, Operator
245
- push :tablerow
246
- end
247
- rule %r/^(#{MARGIN})(\|)/ do
248
- groups Text, Punctuation
249
- push :tablerow
412
+ # The text of a list item, a definition or a definition term. The line
413
+ # after it may also start the next item or definition.
414
+ { listitem: Text, dlterm: Generic::Heading }.each do |name, tok|
415
+ line = :"#{name}line"
416
+ state name do
417
+ rule(/\n/) { token Text; goto line }
418
+ mixin :inlinecontent
419
+ rule PLAIN, tok
420
+ rule(/./) { token plain_token }
250
421
  end
251
422
 
252
- # A standalone attribute block, which may span lines.
253
- rule %r/^(#{MARGIN})(#{ATTRS_LINE})/ do
254
- groups Text, Name::Attribute
423
+ state line do
424
+ rule(%r/^(?=[ \t]*(?:\n|\z))/) { pop! }
425
+ rule(%r/^(?=#{MARGIN}(?:#{INTERRUPT}|#{LIST_MARKER}|:[ \t]))/) { pop! }
426
+ rule(%r/^(?=#{MARGIN}(`{3,}|~{3,}))/) { |m| fence_closed_below?(m[1]) ? pop! : goto(name) }
427
+ rule(//) { goto name }
255
428
  end
429
+ end
256
430
 
257
- mixin :inline
431
+ # Right after a blockquote marker: a block opener, or quoted text.
432
+ state :quotestart do
433
+ mixin :quoteopeners
434
+ rule(//) { goto :quoteline }
258
435
  end
259
436
 
260
437
  state :frontmatter do
@@ -266,17 +443,16 @@ module Rouge
266
443
  rule %r/[^\n]+\n?/, Comment::Special
267
444
  end
268
445
 
269
- state :commentfence do
270
- rule %r/^([ \t]*)(%%%+)([ \t]*)$/ do
271
- groups Text, Comment::Preproc, Text
272
- pop!
273
- end
274
- rule %r/\n/, Comment
275
- rule %r/[^\n]+\n?/, Comment
446
+ state :defline do
447
+ rule %r/$/, Text, :pop!
448
+ mixin :inlinecontent
449
+ rule PLAIN, Text
450
+ rule %r/[^\n]/, Text
276
451
  end
277
452
 
278
453
  state :heading do
279
454
  rule %r/$/, Text, :pop!
455
+ mixin :lineverbatim
280
456
  mixin :inlinecontent
281
457
  rule PLAIN, Generic::Heading
282
458
  rule %r/[^\n]/, Generic::Heading
@@ -284,6 +460,7 @@ module Rouge
284
460
 
285
461
  state :quoteline do
286
462
  rule %r/$/, Text, :pop!
463
+ mixin :lineverbatim
287
464
  mixin :inlinecontent
288
465
  rule PLAIN, Generic::Emph
289
466
  rule %r/[^\n]/, Generic::Emph
@@ -291,6 +468,7 @@ module Rouge
291
468
 
292
469
  state :caption do
293
470
  rule %r/$/, Text, :pop!
471
+ mixin :lineverbatim
294
472
  mixin :inlinecontent
295
473
  rule PLAIN, Generic::Subheading
296
474
  rule %r/[^\n]/, Generic::Subheading
@@ -330,6 +508,18 @@ module Rouge
330
508
  rule %r/./, Text
331
509
  end
332
510
 
511
+ state :lineverbatim do
512
+ rule %r/(!)(`+)(?!`)((?:(?!(?<!`)\2(?!`))[^\n])*)((?<!`)\2(?!`)|$)/ do
513
+ groups Operator, Punctuation, Literal, Punctuation
514
+ end
515
+ rule %r/(\$\$?)(`+)(?!`)((?:(?!(?<!`)\2(?!`))[^\n])*)((?<!`)\2(?!`)|$)/ do
516
+ groups Operator, Punctuation, Str::Other, Punctuation
517
+ end
518
+ rule %r/(`+)(?!`)((?:(?!(?<!`)\1(?!`))[^\n])*)((?<!`)\1(?!`)|$)/ do
519
+ groups Punctuation, Str::Backtick, Punctuation
520
+ end
521
+ end
522
+
333
523
  state :inlinecontent do
334
524
  # An escape wins over every delimiter that follows it. A backslash at
335
525
  # END OF LINE escapes nothing - it is a hard break, and needs its own
@@ -370,74 +560,95 @@ module Rouge
370
560
  # as a hashtag inside a path. After the verbatim family, so a directive
371
561
  # in a code span stays literal; before every other brace rule, none of
372
562
  # which can spell `{{`.
373
- # THE CLOSER IS IN THE LOOKAHEAD, so an unterminated `{{` never opens
374
- # the state at all and stays the ordinary text the processor treats it
375
- # as. Without it the opener scoped and only the newline rule below
376
- # stopped the damage.
377
- rule %r/(\{\{)(?=[ \t]+[^\n]*?[ \t]+\}\})([ \t]+)((?:"(?:\\.|[^"\\])*"|[^#@}\s"][^#@}\s]*))/ do
378
- groups Punctuation, Text, Name::Namespace
379
- push :includeparts
563
+ # An unterminated `{{` never opens the state and stays the ordinary
564
+ # text the processor treats it as.
565
+ rule %r/(\{\{)([ \t]+)((?:"(?:\\.|[^"\\])*"|[^#@}\s"][^#@}\s]*))/ do |m|
566
+ if closer_ahead?(:include, /[ \t]\}\}/)
567
+ groups Punctuation, Text, Name::Namespace
568
+ push :includeparts
569
+ else
570
+ token plain_token, m[0]
571
+ end
380
572
  end
381
573
 
382
- # CriticMarkup substitution and comment, before the forced family:
383
- # `{~old~>new~}` also matches the forced-strike shape.
384
- rule %r/(\{~)([^\n]*?)(~>)([^\n]*?)(~\})/ do
385
- groups Punctuation, Generic::Deleted, Operator, Generic::Inserted, Punctuation
386
- end
387
- rule %r/(\{\#)([^\n]*?)(\#\})/ do
388
- groups Punctuation, Comment, Punctuation
389
- end
574
+ # The braced families. Each opener consumes only its `{` here, so an
575
+ # unclosed one leaves the character after it to the rules below, and
576
+ # its closer must be on the line - looked up in a per-line cache, so
577
+ # a run of unclosed openers stays linear.
578
+ #
579
+ # CriticMarkup substitution and forced strike share `{~`; the first of
580
+ # `~>` and `~}` decides which one it is.
581
+ rule %r/\{(?=~(?!~\}))/ do
582
+ next token(plain_token) unless closer_ahead?(:tilde, /~\}/)
390
583
 
391
- # The braced FORCED emphasis family. Braces make a delimiter apply
392
- # where the bare form would not - `my{_path_}name` is underline inside
393
- # a word - so the content is emphasis and the braces are its
394
- # delimiters, not an attribute block.
395
- rule %r/(\{\*)([^\n]+?)(\*\})/ do
396
- groups Punctuation, Generic::Strong, Punctuation
397
- end
398
- rule %r/(\{\/)([^\n]+?)(\/\})/ do
399
- groups Punctuation, Generic::Emph, Punctuation
400
- end
401
- rule %r/(\{_)([^\n]+?)(_\})/ do
402
- groups Punctuation, Generic::Emph, Punctuation
403
- end
404
- rule %r/(\{~)([^\n]+?)(~\})/ do
405
- groups Punctuation, Generic::Deleted, Punctuation
584
+ enter_braced(:tildebrace)
406
585
  end
407
586
 
408
- # Braced inline families. Sup and sub are braced-only in Carve: a bare
409
- # `^x^` or `,x,` is literal text.
410
- rule %r/(\{\^)([^\n]+?)(\^\})/ do
411
- groups Punctuation, Generic::Emph, Punctuation
412
- end
413
- rule %r/(\{,)([^\n]+?)(,\})/ do
414
- groups Punctuation, Generic::Emph, Punctuation
415
- end
416
- rule %r/(\{=)([^\n]+?)(=\})/ do
417
- groups Punctuation, Generic::Inserted, Punctuation
418
- end
419
- rule %r/(\{\+)([^\n]+?)(\+\})/ do
420
- groups Punctuation, Generic::Inserted, Punctuation
587
+ # A CriticMarkup comment. Without its closer, `{#id}` is an attribute
588
+ # block.
589
+ # The scanner's match is read before #check replaces it.
590
+ rule %r/\{(?=\#)/ do |m|
591
+ brace = m[0]
592
+ if closer_ahead?(:criticcomment, /\#\}/, @current_stream.charpos + 1)
593
+ enter_braced(:criticcomment)
594
+ elsif (tail = @current_stream.check(ATTRS_TAIL))
595
+ token Name::Attribute, brace
596
+ consume_as(Name::Attribute, tail)
597
+ else
598
+ token plain_token, brace
599
+ end
421
600
  end
422
- rule %r/(\{-)([^\n]+?)(-\})/ do
423
- groups Punctuation, Generic::Deleted, Punctuation
601
+
602
+ # The FORCED emphasis family and the other braced inlines. Braces make
603
+ # a delimiter apply where the bare form would not - `my{_path_}name`
604
+ # is underline inside a word - so the content is emphasis and the
605
+ # braces are its delimiters, not an attribute block. Sup and sub are
606
+ # braced-only in Carve: a bare `^x^` or `,x,` is literal text.
607
+ BRACED.each do |char, (_, name)|
608
+ delimiter = Regexp.escape(char)
609
+ rule %r/\{(?=#{delimiter}(?!#{delimiter}\}))/ do
610
+ if closer_ahead?(name, /#{delimiter}\}/, @current_stream.charpos + 2)
611
+ enter_braced(name)
612
+ else
613
+ token plain_token
614
+ end
615
+ end
424
616
  end
425
- rule %r/(\{>>)([^\n]+?)(<<\})/ do
426
- groups Punctuation, Comment, Punctuation
617
+ rule %r/\{(?=>>)/ do
618
+ if closer_ahead?(:criticnote, /<<\}/, @current_stream.charpos + 3)
619
+ enter_braced(:criticnote, 2)
620
+ else
621
+ token plain_token
622
+ end
427
623
  end
428
- rule %r/(\{%)(.*?)(%\})/ do
429
- groups Comment::Preproc, Comment, Comment::Preproc
624
+ rule %r/\{(?=%)/ do
625
+ if closer_ahead?(:bracedcomment, /%\}/, @current_stream.charpos + 1)
626
+ token Comment::Preproc
627
+ push :bracedcomment
628
+ push :preprocchar
629
+ else
630
+ token plain_token
631
+ end
430
632
  end
431
633
 
432
634
  # An inline footnote carries content; a reference carries a label.
433
- rule %r/\^\[/, Punctuation, :inlinefootnote
434
- rule %r/(\[\^)([^\]\n]+)(\])/ do
635
+ # Both bracketed bodies need a closing `]` before the paragraph ends;
636
+ # an opener without one is text, as it is in the engine.
637
+ rule %r/\^\[/ do
638
+ if (scope = bracket_ahead?)
639
+ token Punctuation
640
+ push :"inlinefootnote_#{scope}"
641
+ else
642
+ token plain_token
643
+ end
644
+ end
645
+ rule %r/(\[\^)([^\[\]\n]+)(\])/ do
435
646
  groups Punctuation, Name::Label, Punctuation
436
647
  end
437
648
 
438
649
  # A citation before a link: both open with `[`, and `[@key]` or
439
650
  # `[+@key]` would otherwise be read as a link label.
440
- rule %r/(\[)(\+?@[^\]\n]+)(\])/ do
651
+ rule %r/(\[)(\+?@[^\[\]\n]+)(\])/ do
441
652
  groups Punctuation, Name::Variable, Punctuation
442
653
  end
443
654
 
@@ -468,8 +679,12 @@ module Rouge
468
679
 
469
680
  # The `:name[...]` extension form.
470
681
  rule %r/(:)([a-zA-Z][\w-]*)(\[)/ do
471
- groups Punctuation, Name::Function, Punctuation
472
- push :rolebody
682
+ if (scope = bracket_ahead?)
683
+ groups Punctuation, Name::Function, Punctuation
684
+ push :"rolebody_#{scope}"
685
+ else
686
+ token plain_token
687
+ end
473
688
  end
474
689
 
475
690
  # A code callout marker, and a symbol shortcode - whose name may start
@@ -480,28 +695,12 @@ module Rouge
480
695
  # An attribute block attached to the construct before it.
481
696
  rule ATTRS, Name::Attribute
482
697
 
483
- # Bare emphasis delimiters. Carve's bare set is / * _ ~ = and each
484
- # needs a non-space inner boundary so `a / b` stays literal.
485
- rule %r/(\/\*)([^\n]+?)(\*\/)/ do
486
- groups Punctuation, Generic::Strong, Punctuation
487
- end
488
- rule %r/(\*\/)([^\n]+?)(\/\*)/ do
489
- groups Punctuation, Generic::Strong, Punctuation
490
- end
491
- rule %r/(\*)(\S(?:[^\n]*?\S)?)(\*)/ do
492
- groups Punctuation, Generic::Strong, Punctuation
493
- end
494
- rule %r/(\/)(\S(?:[^\n]*?\S)?)(\/)/ do
495
- groups Punctuation, Generic::Emph, Punctuation
496
- end
497
- rule %r/(_)(\S(?:[^\n]*?\S)?)(_)/ do
498
- groups Punctuation, Generic::Emph, Punctuation
499
- end
500
- rule %r/(~)(\S(?:[^\n]*?\S)?)(~)/ do
501
- groups Punctuation, Generic::Deleted, Punctuation
502
- end
503
- rule %r/(=)(\S(?:[^\n]*?\S)?)(=)/ do
504
- groups Punctuation, Generic::Inserted, Punctuation
698
+ # Bare emphasis delimiters, Carve's / * _ ~ = set. Whether one opens is
699
+ # decided in #open_bare, from the characters on both sides and from
700
+ # whether a closer follows on the line; what it encloses is lexed as
701
+ # inline content. `==>` is left whole for the typographic rule.
702
+ rule %r/[*\/_~]|=(?!=>)/ do |m|
703
+ open_bare(m[0])
505
704
  end
506
705
 
507
706
  # A mention and a tag, each one token: the sigil is part of the name
@@ -528,7 +727,7 @@ module Rouge
528
727
  state :includeparts do
529
728
  rule %r/[ \t]+/, Text
530
729
  rule %r/\}\}/, Punctuation, :pop!
531
- rule %r/#[A-Za-z_][\w-]*/, Name::Label
730
+ rule %r/#[A-Za-z0-9_][\w-]*/, Name::Label
532
731
  rule %r/(@[A-Za-z_][\w-]*)(:)([^\s}]+)/ do
533
732
  groups Name::Attribute, Punctuation, Literal
534
733
  end
@@ -537,24 +736,375 @@ module Rouge
537
736
  rule %r/\n/, Text, :pop!
538
737
  end
539
738
 
540
- state :inlinefootnote do
541
- rule %r/\]/, Punctuation, :pop!
542
- mixin :inlinecontent
543
- rule PLAIN_NO_BRACKET, Generic::Emph
544
- rule %r/[^\]\n]/, Generic::Emph
545
- rule %r/\n/, Text
739
+ # The body of a bare emphasis run. A same-kind delimiter that cannot
740
+ # close is content (`*a *b* c*` closes at `b*`), and the run ends with
741
+ # its line in case a construct inside took the closer the opener saw.
742
+ #
743
+ # NO LOOKBEHIND IN A RULE. Rouge before 5 scans without a fixed anchor,
744
+ # so a lookbehind cannot see what precedes the scan position; the
745
+ # character before a closer is read in #close_bare instead.
746
+ BARE.each do |char, (name, _, _, _)|
747
+ state name do
748
+ rule(%r/#{Regexp.escape(char)}(?![\p{L}\p{N}])/) { close_bare(/\s/) }
749
+ rule(/$/) { pop! }
750
+ rule(/#{Regexp.escape(char)}/) { token plain_token }
751
+ mixin :inlinecontent
752
+ rule(PLAIN) { token plain_token }
753
+ rule(/./) { token plain_token }
754
+ end
546
755
  end
547
756
 
548
- state :rolebody do
549
- rule %r/\]/, Punctuation, :pop!
550
- mixin :inlinecontent
551
- rule PLAIN_NO_BRACKET, Name::Function
552
- rule %r/[^\]\n]/, Name::Function
553
- rule %r/\n/, Text
757
+ # Bold italic in one delimiter pair, in both nesting orders. Its own
758
+ # delimiters inside it are content unless they close it.
759
+ # A comment inside it ends at its closer.
760
+ {
761
+ bolditalic: ['\*\/', /\s/, '(?<=\S)\*\/'],
762
+ italicbold: ['\/\*(?!\w)', /[\s*]/, '(?<![\s*])\/\*(?!\w)']
763
+ }.each do |name, (closer, refused_before, closer_in_line)|
764
+ state name do
765
+ rule(/#{closer}/) { close_bare(refused_before) }
766
+ rule(/$/) { pop! }
767
+ rule %r/(%%)((?:(?!#{closer_in_line})[^\n])*)/ do
768
+ groups Comment::Preproc, Comment
769
+ end
770
+ rule(%r/[*\/]/) { token plain_token }
771
+ mixin :inlinecontent
772
+ rule(PLAIN) { token plain_token }
773
+ rule(/./) { token plain_token }
774
+ end
775
+ end
776
+
777
+ # The rest of an opener whose `{` was already emitted.
778
+ state :punctchar do
779
+ rule(/[^\n]/) { token Punctuation; pop! }
780
+ end
781
+ state :preprocchar do
782
+ rule(/[^\n]/) { token Comment::Preproc; pop! }
783
+ end
784
+
785
+ # Braced bodies, flat up to their closer.
786
+ BRACED.each do |char, (tok, name)|
787
+ closer = /#{Regexp.escape(char)}\}/
788
+ state name do
789
+ rule closer, Punctuation, :pop!
790
+ rule(/$/) { pop! }
791
+ rule %r/(?:(?!#{closer})[^\n])+/, tok
792
+ end
793
+ end
794
+
795
+ state :tildebrace do
796
+ rule %r/~>/, Operator, :tildenew
797
+ rule %r/~\}/, Punctuation, :pop!
798
+ rule(/$/) { pop! }
799
+ rule %r/(?:(?!~[>}])[^\n])+/, Generic::Deleted
800
+ end
801
+
802
+ # The new side of a substitution. Its closer ends the whole construct.
803
+ state :tildenew do
804
+ rule(/~\}/) { token Punctuation; pop!(2) }
805
+ rule(/$/) { pop!(2) }
806
+ rule %r/(?:(?!~\})[^\n])+/, Generic::Inserted
807
+ end
808
+
809
+ state :criticcomment do
810
+ rule %r/\#\}/, Punctuation, :pop!
811
+ rule(/$/) { pop! }
812
+ rule %r/(?:(?!\#\})[^\n])+/, Comment
813
+ end
814
+
815
+ state :criticnote do
816
+ rule %r/<<\}/, Punctuation, :pop!
817
+ rule(/$/) { pop! }
818
+ rule %r/(?:(?!<<\})[^\n])+/, Comment
819
+ end
820
+
821
+ state :bracedcomment do
822
+ rule %r/%\}/, Comment::Preproc, :pop!
823
+ rule(/$/) { pop! }
824
+ rule %r/(?:(?!%\})[^\n])+/, Comment
825
+ end
826
+
827
+ # Both bodies end at their bracket, or where their block ends when a
828
+ # construct inside consumed the bracket the opener saw. The newline is
829
+ # left to the block, which decides what the next line opens.
830
+ { inlinefootnote: Generic::Emph, rolebody: Name::Function }.each do |body, tok|
831
+ BODY_ENDS.each do |scope, ends|
832
+ state :"#{body}_#{scope}" do
833
+ rule %r/\]/, Punctuation, :pop!
834
+ rule(%r/(?=#{ends.source}|\n?\z)/) { pop! }
835
+ # The newline is handed back so the block reads the fence line.
836
+ rule(FENCE_NEXT) do |m|
837
+ if fence_closed_below?(m[1])
838
+ m.pos -= 1
839
+ pop!
840
+ else
841
+ token Text
842
+ end
843
+ end
844
+ mixin :inlinecontent
845
+ rule PLAIN_NO_BRACKET, tok
846
+ rule %r/[^\]\n]/, tok
847
+ rule %r/\n/, Text
848
+ end
849
+ end
554
850
  end
555
851
 
556
852
  private
557
853
 
854
+ # The token a state gives a character that opens nothing.
855
+ PLAIN_TOKENS = {
856
+ heading: Generic::Heading, quoteline: Generic::Emph,
857
+ caption: Generic::Subheading, dlterm: Generic::Heading, bolditalic: Generic::Strong,
858
+ italicbold: Generic::Strong
859
+ }.merge(BARE.values.to_h { |name, tok, _, _| [name, tok] })
860
+ .merge(BODY_ENDS.keys.flat_map do |scope|
861
+ [[:"inlinefootnote_#{scope}", Generic::Emph], [:"rolebody_#{scope}", Name::Function]]
862
+ end.to_h).freeze
863
+
864
+ # Italic or underline inside strong, or the reverse, reads as strong,
865
+ # the token the combined delimiters have always had.
866
+ def plain_token
867
+ name = state.name
868
+ if %i[emph underline].include?(name) && in_state?(:strong)
869
+ Generic::Strong
870
+ else
871
+ PLAIN_TOKENS.fetch(name, Text)
872
+ end
873
+ end
874
+
875
+ # Decide whether the bare delimiter +char+, just consumed, opens a run.
876
+ # It opens when the characters around it allow it and a closer follows
877
+ # on its line; otherwise it is plain text. `/*` and `*/` open bold
878
+ # italic first, with the engine's guards for each order.
879
+ def open_bare(char)
880
+ text = @current_stream.string
881
+ pos = @current_stream.charpos
882
+ before = pos > 1 ? text[pos - 2] : nil
883
+ before = nil if before == "\n"
884
+ after = text[pos]
885
+ name, _, refused_before, refused_after = BARE[char]
886
+
887
+ limit = enclosing_closer(pos)
888
+
889
+ if char == '/' && after == '*' && text[pos + 1].to_s.match?(/\S/) &&
890
+ closer_ahead?(:bolditalic, COMBINED_CLOSERS[:bolditalic], pos + 2, masked: :combined, before: limit)
891
+ token Punctuation
892
+ push :bolditalic
893
+ return push(:punctchar)
894
+ end
895
+
896
+ if char == '*' && after == '/' && !before.to_s.match?(/[\w*\/]/) &&
897
+ text[pos + 1].to_s.match?(/\S/) && !(text[pos + 1] == '*' && text[pos + 2].to_s.match?(/[\s*]/)) &&
898
+ closer_ahead?(:italicbold, COMBINED_CLOSERS[:italicbold], pos + 2, masked: :combined, before: limit)
899
+ token Punctuation
900
+ push :italicbold
901
+ return push(:punctchar)
902
+ end
903
+
904
+ if after && !after.match?(refused_after) && !(before && before.match?(refused_before)) &&
905
+ closer_ahead?(name, BARE_CLOSERS[name], pos, masked: :bare, before: limit)
906
+ token Punctuation
907
+ return push(name)
908
+ end
909
+
910
+ token plain_token
911
+ end
912
+
913
+ BARE_CLOSERS = BARE.to_h do |char, (name, _, _, _)|
914
+ [name, %r/(?<=\S)#{Regexp.escape(char)}(?![\p{L}\p{N}])/]
915
+ end.freeze
916
+ COMBINED_CLOSERS = {
917
+ bolditalic: %r/(?<=\S)\*\//, italicbold: %r/(?<![\s*])\/\*(?!\w)/
918
+ }.freeze
919
+
920
+ # Where the run the scanner is in closes, if it is in one: a run opened
921
+ # inside it must close first, so `*a /b* c/` is strong `a /b`.
922
+ def enclosing_closer(pos)
923
+ name = state.name
924
+ if BARE_CLOSERS.key?(name)
925
+ closer_at(name, BARE_CLOSERS[name], pos, masked: :bare)
926
+ elsif COMBINED_CLOSERS.key?(name)
927
+ closer_at(name, COMBINED_CLOSERS[name], pos, masked: :combined)
928
+ end
929
+ end
930
+
931
+ # A closer candidate inside a bare run, just consumed: it closes unless
932
+ # the character before it is one +refused_before+ matches.
933
+ def close_bare(refused_before)
934
+ text = @current_stream.string
935
+ at = @current_stream.charpos - @current_stream[0].length
936
+ if at.positive? && !text[at - 1].match?(refused_before)
937
+ token Punctuation
938
+ pop!
939
+ else
940
+ token plain_token
941
+ end
942
+ end
943
+
944
+ # Emit the `{` of a braced opener and enter +name+, whose remaining
945
+ # opener characters come next.
946
+ def enter_braced(name, rest = 1)
947
+ token Punctuation
948
+ push name
949
+ rest.times { push :punctchar }
950
+ end
951
+
952
+ # Emit the next +text+ of the input as one +tok+ token.
953
+ def consume_as(tok, text)
954
+ literal = Regexp.new(Regexp.escape(text))
955
+ push do
956
+ rule(literal) { token tok; pop! }
957
+ end
958
+ end
959
+
960
+ # Whether +pattern+ matches on the current line at or after +from+.
961
+ # The matches of one line are found once per pattern and reused, which
962
+ # keeps a line full of unclosed openers linear instead of quadratic.
963
+ # A masked line hides what a bare closer cannot reach into: escapes,
964
+ # code spans, braced inlines, link destinations and autolinks. A bare
965
+ # run's line also ends at a trailing comment, which takes its closer.
966
+ def closer_ahead?(key, pattern, from = @current_stream.charpos, masked: nil, before: nil)
967
+ at = closer_at(key, pattern, from, masked: masked)
968
+ !at.nil? && (before.nil? || at < before)
969
+ end
970
+
971
+ # The position of the first match of +pattern+ on the current line at or
972
+ # after +from+, or nil.
973
+ def closer_at(key, pattern, from = @current_stream.charpos, masked: nil)
974
+ stream = @current_stream
975
+ unless @closers_for.equal?(stream)
976
+ @closers_for = stream
977
+ @closers = {}
978
+ end
979
+ pos = stream.charpos
980
+ entry = @closers[key]
981
+ unless entry && pos >= entry[0] && pos <= entry[1]
982
+ text = stream.string
983
+ first = pos.zero? ? 0 : (text.rindex("\n", pos - 1) || -1) + 1
984
+ last = text.index("\n", pos) || text.length
985
+ line = text[first...last]
986
+ line = mask_opaque(line) if masked
987
+ line = line[0...line.index('%%')] if masked == :bare && line.include?('%%')
988
+ hits = []
989
+ line.scan(pattern) { hits << first + Regexp.last_match.begin(0) }
990
+ entry = @closers[key] = [first, last, hits]
991
+ end
992
+ entry[2].bsearch { |at| at >= from }
993
+ end
994
+
995
+ OPAQUE = /\\.|(?<!`)(`+)(?!`).*?(?<!`)\1(?!`)|\]\([^)\n]*\)|<[^<>\s]+>/.freeze
996
+
997
+ def mask_opaque(line)
998
+ line = line.gsub(OPAQUE) { |span| "\u0001" * span.length }
999
+ # Braced inlines pair up in code: a regex scanning from every unclosed
1000
+ # `{*` to the end of the line is quadratic. Once a kind has no closer
1001
+ # left, no later opener of that kind can have one either.
1002
+ '*/_^,=+~#%-'.each_char do |kind|
1003
+ at = 0
1004
+ while (open = line.index("{#{kind}", at))
1005
+ close = line.index("#{kind}}", open + 3)
1006
+ break unless close
1007
+
1008
+ line[open...close + 2] = "\u0001" * (close + 2 - open)
1009
+ at = close + 2
1010
+ end
1011
+ end
1012
+ line
1013
+ end
1014
+
1015
+ # The scope of the block a bracketed body opens in, when a `]` outside
1016
+ # escapes and code spans follows before that block ends; nil otherwise.
1017
+ # The brackets of one block are found in one pass and reused, so a run
1018
+ # of unclosed openers does not rescan it once each.
1019
+ def bracket_ahead?
1020
+ stream = @current_stream
1021
+ pos = stream.charpos
1022
+ scope = body_scope
1023
+ @brackets = {} unless @brackets_for.equal?(stream)
1024
+ @brackets_for = stream
1025
+ entry = @brackets[scope]
1026
+ unless entry && pos < entry[0]
1027
+ text = stream.string
1028
+ last = body_end(text, pos, scope)
1029
+ hits = []
1030
+ text[pos...last].scan(BRACKET_SCAN) { hits << pos + $~.begin(0) if $~[0] == ']' }
1031
+ entry = @brackets[scope] = [last, hits]
1032
+ end
1033
+ entry[1].bsearch { |at| at >= pos } ? scope : nil
1034
+ end
1035
+
1036
+ # Where a body opened at +pos+ must end: the first block end, or the
1037
+ # first closed fence, after it.
1038
+ def body_end(text, pos, scope)
1039
+ ends = BODY_ENDS[scope]
1040
+ last = text.index(ends, pos) || text.length
1041
+ return last if scope == :line
1042
+
1043
+ at = pos
1044
+ while (fence = text.index(FENCE_NEXT, at)) && fence < last
1045
+ return fence if fence_closed_below?($~[1], fence + 1)
1046
+
1047
+ at = fence + 1
1048
+ end
1049
+ last
1050
+ end
1051
+
1052
+ BRACKET_SCAN = /\\.|(?<!`)(`+)(?!`).*?(?<!`)\1(?!`)|\]/.freeze
1053
+
1054
+ # The scope of the nearest enclosing block state. An indented paragraph
1055
+ # sits in a container, so the next list marker ends it as it ends an item.
1056
+ def body_scope
1057
+ stack.reverse_each do |s|
1058
+ scope = BODY_SCOPE[s.name]
1059
+ next unless scope
1060
+ return :item if scope == :para && @indented
1061
+
1062
+ return scope
1063
+ end
1064
+ :para
1065
+ end
1066
+
1067
+ # Whether +closer+ matches a line below the current position. A miss
1068
+ # is remembered per +key+, because every later opener of the same kind
1069
+ # would miss too - without that, a run of unclosed openers rescans the
1070
+ # rest of the document once each.
1071
+ def closer_below?(closer, key, from = @current_stream.charpos)
1072
+ stream = @current_stream
1073
+ @closer_misses = {} unless @closer_misses_for.equal?(stream)
1074
+ @closer_misses_for = stream
1075
+ return false if @closer_misses[key]
1076
+
1077
+ found = stream.string.index(closer, from)
1078
+ @closer_misses[key] = true unless found
1079
+ !found.nil?
1080
+ end
1081
+
1082
+ # Enter the state of a block whose opener just matched, or none for an
1083
+ # opener that is a single token. After a blockquote marker the opener
1084
+ # replaces the quote's start state, and a single-token opener hands the
1085
+ # rest of the line to quoted text.
1086
+ def open_block(name = nil, quoted = name || :quoteline)
1087
+ if state?(:quotestart)
1088
+ goto quoted
1089
+ elsif name
1090
+ push name
1091
+ end
1092
+ end
1093
+
1094
+ def continue_paragraph(name)
1095
+ @lone_image = false
1096
+ goto name
1097
+ end
1098
+
1099
+ # Whether a fence opened by +run+ on the current line is closed further
1100
+ # down. Only a closed fence interrupts a paragraph.
1101
+ def fence_closed_below?(run, line_start = @current_stream.charpos)
1102
+ text = @current_stream.string
1103
+ line_end = text.index("\n", line_start) || text.length
1104
+ closer = %r/^[ \t]*#{Regexp.escape(run[0])}{#{run.length},}[ \t]*$/
1105
+ closer_below?(closer, run, line_end)
1106
+ end
1107
+
558
1108
  # Lex the tail of a fence or container opener line without leaving the
559
1109
  # current state: a quoted title, a bracketed label and an attribute
560
1110
  # block, each as its own token.
@@ -590,6 +1140,8 @@ module Rouge
590
1140
  sublexer = fence_lexer(language)
591
1141
  sublexer&.reset!
592
1142
 
1143
+ return push_quoted_fence(char, width, sublexer) if state?(:quotestart)
1144
+
593
1145
  push do
594
1146
  rule closer, Punctuation, :pop!
595
1147
  if sublexer
@@ -611,6 +1163,35 @@ module Rouge
611
1163
  end
612
1164
  end
613
1165
 
1166
+ # A fence opened after a blockquote marker. Every payload line carries
1167
+ # the quote prefix at the opener's depth, which is markup rather than
1168
+ # payload; a line with fewer markers leaves that quote and so ends the
1169
+ # fence too, while a deeper marker is payload.
1170
+ def push_quoted_fence(char, width, sublexer)
1171
+ pop!
1172
+ text = @current_stream.string
1173
+ pos = @current_stream.charpos
1174
+ line = text[((text.rindex("\n", pos - 1) || -1) + 1)...pos]
1175
+ depth = line[/\A#{QUOTE_PREFIX}/].to_s.count('>')
1176
+ prefix = %r/(?:[ \t]*>(?=[ \t]|$)){#{depth}}[ \t]?/
1177
+
1178
+ push do
1179
+ rule %r/^(#{prefix})([ \t]*#{char}{#{width},})([ \t]*)$/ do
1180
+ groups Punctuation, Punctuation, Text
1181
+ pop!
1182
+ end
1183
+ rule(%r/^(?!#{prefix})/) { pop! }
1184
+ rule %r/^#{prefix}/, Punctuation
1185
+ if sublexer
1186
+ rule %r/\n/, Text
1187
+ rule(%r/[^\n]+\n?/) { |m| delegate sublexer, m[0] }
1188
+ else
1189
+ rule %r/<\d+>/, Name::Constant
1190
+ rule %r/[^\n<]+|<|\n/, Str::Backtick
1191
+ end
1192
+ end
1193
+ end
1194
+
614
1195
  # A lexer for the fence's declared language, or nil when there is no
615
1196
  # language, none is known, or the name is ambiguous. Guessing is
616
1197
  # deliberately not attempted: a wrong guess colours a sample as the wrong
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Kept apart from the entry file so the gemspec can read it without loading
4
+ # Rouge, which is not installed yet when Bundler evaluates the gemspec.
5
+ module RougeCarve
6
+ VERSION = '0.1.3'
7
+ end
data/lib/rouge-carve.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rouge'
4
+ require_relative 'rouge-carve/version'
4
5
  require_relative 'rouge/lexers/carve'
5
6
 
6
7
  # A Rouge lexer for the Carve markup language.
@@ -9,5 +10,4 @@ require_relative 'rouge/lexers/carve'
9
10
  # registers it, so `Rouge::Lexer.find('carve')` and
10
11
  # `Rouge::Lexer.guess(filename: 'x.crv')` work from that point on.
11
12
  module RougeCarve
12
- VERSION = '0.1.1'
13
13
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rouge-carve
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Scherer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-22 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rouge
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.0'
19
+ version: '3.30'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '6.0'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '3.0'
29
+ version: '3.30'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '6.0'
@@ -44,6 +44,7 @@ files:
44
44
  - LICENSE
45
45
  - README.md
46
46
  - lib/rouge-carve.rb
47
+ - lib/rouge-carve/version.rb
47
48
  - lib/rouge/demos/carve
48
49
  - lib/rouge/lexers/carve.rb
49
50
  homepage: https://github.com/markup-carve/rouge-carve