rouge-carve 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +1 -0
- data/lib/rouge/lexers/carve.rb +419 -13
- data/lib/rouge/lexers/carve_continuation.rb +357 -0
- data/lib/rouge-carve/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 300be9217acf879b361b092d5bd9f814858ba169272f98f730cdbfa86a2e75bf
|
|
4
|
+
data.tar.gz: 1b4e02989bd311b9d0423e1021250d5ddac1982420c15016d80600ddb82386b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63d049afb99f11ac50d7b64a2829c8fec7bc220b1501b621cbbe2e6a87160e75cc326bc94f3724f3280650e12e9be2ce0a00bb160f1c9012e717fc2d538916b9
|
|
7
|
+
data.tar.gz: d3dce00b0304ec63f85dc9e258f78e7c516d7a9048e1ac9e76e8745140c2931a8ee3e0de70abc7a7a804203b511707d901f7ecd44c152ad08bdaeaa3e22dd35d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.4] - 2026-10-23
|
|
6
|
+
|
|
7
|
+
- `csv` and `tsv` fence bodies are highlighted: every field has its own token type and the separator is an operator, colored apart from the fields in the default styles (#25, #26, #27, #28).
|
|
8
|
+
- A lone `+` is a continuation marker, tokenized as an operator, only where the engine reads one: at an open container's marker column with a non-list block below, or as the first block after an opener (#29, #30, #32). A prose line ending in `+` is not scoped (#31).
|
|
9
|
+
- A quote-prefixed `+` needs one space after each `>`: `>+`, `>>+` and `>> +` are paragraph text (#33).
|
|
10
|
+
- A quote opened on a list item line (`- > q`) scopes its `>`, and `- > +` is a marker when the next line starts a non-list block at column 0, at the line's column or at the owning quote's column (markup-carve/carve#3421). A stacked `> > +` takes its follower only at column 0 or the first quote's column (#34).
|
|
11
|
+
- A `:` left of the `::` column of the definition list it would continue is paragraph text (markup-carve/carve#3458) (#34).
|
|
12
|
+
|
|
5
13
|
## [0.1.3] - 2026-10-14
|
|
6
14
|
|
|
7
15
|
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.
|
data/README.md
CHANGED
|
@@ -49,6 +49,7 @@ markup, symbol shortcodes, mentions, tags and the typographic runs.
|
|
|
49
49
|
as Ruby, because that is what a reader expects. A ` ```=html ` raw block is not:
|
|
50
50
|
its info string names an output format, not a language, so the payload stays
|
|
51
51
|
opaque.
|
|
52
|
+
CSV and TSV fences highlight separators, quoted fields, escaped quotes, and unquoted fields (the header record apart from the data rows).
|
|
52
53
|
|
|
53
54
|
**Fence widths are respected.** A three-backtick line inside a four-backtick
|
|
54
55
|
fence is content, not a closer. The width is closed over per block rather than
|
data/lib/rouge/lexers/carve.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*- #
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative 'carve_continuation'
|
|
5
|
+
|
|
4
6
|
module Rouge
|
|
5
7
|
module Lexers
|
|
6
8
|
# Carve is a lightweight markup language for documents and the web. Its
|
|
@@ -48,6 +50,74 @@ module Rouge
|
|
|
48
50
|
#
|
|
49
51
|
# Spec: https://markup-carve.github.io/carve/
|
|
50
52
|
class Carve < RegexLexer
|
|
53
|
+
class DelimitedTable < RegexLexer
|
|
54
|
+
# Unquoted fields are Name::Variable (colored in the default Rouge
|
|
55
|
+
# styles, unlike a bare Name); the header record's unquoted fields are
|
|
56
|
+
# Name::Attribute.
|
|
57
|
+
start { @header = true }
|
|
58
|
+
|
|
59
|
+
def field_token
|
|
60
|
+
@header ? Name::Attribute : Name::Variable
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
state :root do
|
|
64
|
+
rule /"/ do
|
|
65
|
+
token Str
|
|
66
|
+
push :unquoted
|
|
67
|
+
push :quoted
|
|
68
|
+
end
|
|
69
|
+
rule /[,\t]/ do |m|
|
|
70
|
+
if m[0] == self.class::SEPARATOR
|
|
71
|
+
token Keyword
|
|
72
|
+
else
|
|
73
|
+
token field_token
|
|
74
|
+
push :unquoted
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
rule /[^,\t\n]+/ do
|
|
78
|
+
token field_token
|
|
79
|
+
push :unquoted
|
|
80
|
+
end
|
|
81
|
+
rule /\n/ do
|
|
82
|
+
token Text
|
|
83
|
+
@header = false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
state :unquoted do
|
|
88
|
+
rule /[,\t]/ do |m|
|
|
89
|
+
if m[0] == self.class::SEPARATOR
|
|
90
|
+
token Keyword
|
|
91
|
+
pop!
|
|
92
|
+
else
|
|
93
|
+
token field_token
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
rule /[^,\t\n]+/ do
|
|
97
|
+
token field_token
|
|
98
|
+
end
|
|
99
|
+
rule /\n/ do
|
|
100
|
+
token Text
|
|
101
|
+
@header = false
|
|
102
|
+
pop!
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
state :quoted do
|
|
107
|
+
rule /""/, Str::Escape
|
|
108
|
+
rule /"/, Str, :pop!
|
|
109
|
+
rule /[^"]+/, Str
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class CsvTable < DelimitedTable
|
|
114
|
+
SEPARATOR = ','.freeze
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class TsvTable < DelimitedTable
|
|
118
|
+
SEPARATOR = "\t".freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
51
121
|
title 'Carve'
|
|
52
122
|
desc 'Carve, a lightweight markup language for documents and the web'
|
|
53
123
|
|
|
@@ -110,8 +180,13 @@ module Rouge
|
|
|
110
180
|
# is a word, not a marker.
|
|
111
181
|
ORDERED = '(?:\\d+|[A-Za-z]|[ivxlcdm]+|[IVXLCDM]+)[.)]'
|
|
112
182
|
|
|
183
|
+
# The states read a column-0 `+` with a flush-left block below as an
|
|
184
|
+
# attachment. Whether a `+` is the marker is settled by CarveContinuation.
|
|
185
|
+
PLUS_NEXT = /(?=\n[^ \t\n])/.freeze
|
|
186
|
+
|
|
113
187
|
# A list marker at the start of a line, as the list rules below read it.
|
|
114
188
|
LIST_MARKER = "(?:[-*]|#{ORDERED}|\\.)(?:#{ATTRS})?(?:[ \\t]|$)"
|
|
189
|
+
OPENER_RUN = "(?:(?:[-*]|#{ORDERED}|\\.)(?:#{ATTRS})? +[ \\t]*|> )+"
|
|
115
190
|
|
|
116
191
|
# A table row needs its closing pipe: `| not a table` is a paragraph.
|
|
117
192
|
ROW_END = /(?=(?:\\.|[^\\\n])*\|[ \t]*(?:#{ATTRS})?[ \t]*$)/.freeze
|
|
@@ -199,6 +274,7 @@ module Rouge
|
|
|
199
274
|
# enters its state through #open_block, which knows which of the two it
|
|
200
275
|
# is running in.
|
|
201
276
|
def self.block_openers(bol)
|
|
277
|
+
root_margin = bol == '^' ? '()' : '( )'
|
|
202
278
|
proc do
|
|
203
279
|
# A raw block: the `=FORMAT` info string routes the payload to that
|
|
204
280
|
# output format verbatim. Emitted as one token including the `=`,
|
|
@@ -233,14 +309,14 @@ module Rouge
|
|
|
233
309
|
end
|
|
234
310
|
|
|
235
311
|
# A caption line attaches to the block above or below it.
|
|
236
|
-
rule %r/#{bol}(#{MARGIN})(\^)([ \t]
|
|
312
|
+
rule %r/#{bol}(#{MARGIN})(\^)( +)(?=[ \t]*[^ \t\n])/ do
|
|
237
313
|
groups Text, Punctuation, Text
|
|
238
314
|
open_block :caption
|
|
239
315
|
end
|
|
240
316
|
|
|
241
317
|
# Headings. Carve has no setext form, so a `#` run is the only
|
|
242
318
|
# spelling and a trailing `{...}` is NOT an attribute block here.
|
|
243
|
-
rule %r/#{bol}(#{MARGIN})(\#{1,6})([ \t]
|
|
319
|
+
rule %r/#{bol}(#{MARGIN})(\#{1,6})( +)(?=[ \t]*[^ \t\n])/ do
|
|
244
320
|
groups Text, Punctuation, Text
|
|
245
321
|
open_block :heading
|
|
246
322
|
end
|
|
@@ -257,7 +333,7 @@ module Rouge
|
|
|
257
333
|
# definition ends with its line.
|
|
258
334
|
rule %r/#{bol}(#{MARGIN})(\[\^)([^\]\n]+)(\]:)( )/ do
|
|
259
335
|
groups Text, Punctuation, Name::Label, Punctuation, Text
|
|
260
|
-
open_block :defline
|
|
336
|
+
open_block(:defline, :defline, host: :footnote)
|
|
261
337
|
end
|
|
262
338
|
rule %r/#{bol}(#{MARGIN})(\*\[)([^\]\n]+)(\]:)( )/ do
|
|
263
339
|
groups Text, Punctuation, Name::Entity, Punctuation, Text
|
|
@@ -270,47 +346,125 @@ module Rouge
|
|
|
270
346
|
|
|
271
347
|
# A definition-list term (`::`) and its definition (`:`). A term
|
|
272
348
|
# may wrap onto lazy lines, and a definition's text is an item's.
|
|
273
|
-
rule %r/#{bol}(#{MARGIN})(::)([ \t]
|
|
349
|
+
rule %r/#{bol}(#{MARGIN})(::)( +)(?=[ \t]*[^ \t\n])/ do
|
|
274
350
|
groups Text, Punctuation, Text
|
|
351
|
+
remember_host(:term)
|
|
275
352
|
open_block :dlterm, :heading
|
|
276
353
|
end
|
|
277
|
-
rule %r/#{bol}
|
|
278
|
-
|
|
354
|
+
rule %r/#{bol}#{root_margin}(:)( +[ \t]*)(\+)()$#{PLUS_NEXT}/ do
|
|
355
|
+
if definition_term_before?
|
|
356
|
+
groups Text, Punctuation, Text, Operator, Text
|
|
357
|
+
else
|
|
358
|
+
groups Text, Punctuation, Text, Text, Text
|
|
359
|
+
end
|
|
279
360
|
open_block :listitem, :quoteline
|
|
280
361
|
end
|
|
362
|
+
# Without a term above, `: b` is paragraph text (a lazy line too).
|
|
363
|
+
rule %r/#{bol}(#{MARGIN})(:)(?= +[ \t]*[^ \t\n])/ do
|
|
364
|
+
# the scan from the last definition is short; the term scan runs once
|
|
365
|
+
if (definition_before? || definition_term_before?) && !separator_below_term?
|
|
366
|
+
groups Text, Punctuation
|
|
367
|
+
term = [@term_stream, @term_pos, @term_quote_depth]
|
|
368
|
+
open_block :listitem, :quoteline
|
|
369
|
+
# the next `: d` may still belong to the same term
|
|
370
|
+
@term_stream, @term_pos, @term_quote_depth = term
|
|
371
|
+
@def_stream = @current_stream
|
|
372
|
+
@def_pos = @last_host_pos
|
|
373
|
+
@def_quote_depth = @term_quote_depth
|
|
374
|
+
else
|
|
375
|
+
groups Text, Text
|
|
376
|
+
open_block :paragraph
|
|
377
|
+
end
|
|
378
|
+
end
|
|
281
379
|
|
|
282
380
|
# A blockquote marker must be followed by a space or end the line;
|
|
283
381
|
# `>foo` and `>> x` are paragraphs, and `> > x` nests. What follows
|
|
284
382
|
# a marker may open a block of its own, so the openers are tried
|
|
285
383
|
# again there.
|
|
286
|
-
rule %r/#{bol}(#{MARGIN})(>)(?=
|
|
384
|
+
rule %r/#{bol}(#{MARGIN})(>)(?= |$)/ do |m|
|
|
287
385
|
groups Text, Punctuation
|
|
288
|
-
|
|
386
|
+
record_quote_depth(quote_depth_on_line + 1)
|
|
387
|
+
remember_host(:quote)
|
|
388
|
+
rooted = state?(:quotestart) ? m[1] == ' ' : m[1].empty?
|
|
389
|
+
if rooted
|
|
390
|
+
state?(:quotestart) ? goto(:quotestart) : push(:quotestart)
|
|
391
|
+
else
|
|
392
|
+
state?(:quotestart) ? goto(:quoteline) : push(:quoteline)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# A task can attach its first block on the marker line.
|
|
397
|
+
rule %r/#{bol}#{root_margin}([-*](?:#{ATTRS})?)( +[ \t]*)(\[[^\]\n]\])( +[ \t]*)(\+)()$#{PLUS_NEXT}/ do
|
|
398
|
+
groups Text, Punctuation, Text, Name::Constant, Text, Operator, Text
|
|
399
|
+
open_block :listitem, :quoteline
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
rule %r/#{bol}#{root_margin}(#{OPENER_RUN})(\+)()$#{PLUS_NEXT}/ do |m|
|
|
403
|
+
token Text, m[1]
|
|
404
|
+
scanner = StringScanner.new(m[2])
|
|
405
|
+
until scanner.eos?
|
|
406
|
+
if (marker = scanner.scan(/(?:#{ORDERED}|\.)(?:#{ATTRS})?/))
|
|
407
|
+
token Num::Integer, marker
|
|
408
|
+
elsif (marker = scanner.scan(/[-*](?:#{ATTRS})?|>/))
|
|
409
|
+
token Punctuation, marker
|
|
410
|
+
else
|
|
411
|
+
token Text, scanner.getch
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
depth = quote_depth_on_line
|
|
415
|
+
token(state?(:quotestart) && !quote_level_follows?(depth) ? Text : Operator, m[3])
|
|
416
|
+
token Text, m[4]
|
|
417
|
+
m[2].rstrip.end_with?('>') ? open_block(:quoteline) : open_block(:listitem, :quoteline)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# A quote opened on a list item's marker line (`- > q`): its `>` is a
|
|
421
|
+
# marker, and a `+` behind it may be the quote's first block
|
|
422
|
+
# (markup-carve/carve#3421).
|
|
423
|
+
rule %r/#{bol}(#{MARGIN})((?:(?:[-*](?:#{ATTRS})?(?: +\[[^\]\n]\])?|#{ORDERED}(?:#{ATTRS})?|\.(?:#{ATTRS})?) +)+)((?:> +)*>)(?:( )(\+)()$|(?= |$))/ do |m|
|
|
424
|
+
token Text, m[1]
|
|
425
|
+
scanner = StringScanner.new(m[2] + m[3])
|
|
426
|
+
until scanner.eos?
|
|
427
|
+
if (marker = scanner.scan(/(?:#{ORDERED}|\.)(?:#{ATTRS})?/))
|
|
428
|
+
token Num::Integer, marker
|
|
429
|
+
elsif (marker = scanner.scan(/\[[^\]\n]\]/))
|
|
430
|
+
token Name::Constant, marker
|
|
431
|
+
elsif (marker = scanner.scan(/[-*](?:#{ATTRS})?|>/))
|
|
432
|
+
token Punctuation, marker
|
|
433
|
+
else
|
|
434
|
+
token Text, scanner.scan(/ +/)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
if m[5]
|
|
438
|
+
token Text, m[4]
|
|
439
|
+
# CarveContinuation settles whether this `+` is the marker.
|
|
440
|
+
token Operator, m[5]
|
|
441
|
+
end
|
|
442
|
+
open_block(:quoteline)
|
|
289
443
|
end
|
|
290
444
|
|
|
291
445
|
# Task items before plain bullets, so the state marker is its own
|
|
292
446
|
# token. The state is any single character, not only a space or an
|
|
293
447
|
# x: `[>]` is deferred and `[-]` is dropped. A `+` is not a bullet in
|
|
294
448
|
# Carve; it continues a container.
|
|
295
|
-
rule %r/#{bol}(#{MARGIN})((?:[-*]|#{ORDERED})(?:#{ATTRS})?)(
|
|
449
|
+
rule %r/#{bol}(#{MARGIN})((?:[-*]|#{ORDERED})(?:#{ATTRS})?)( +)(\[[^\]\n]\])(?= +[ \t]*[^ \t\n])/ do
|
|
296
450
|
groups Text, Punctuation, Text, Name::Constant
|
|
297
451
|
open_block :listitem, :quoteline
|
|
298
452
|
end
|
|
299
453
|
|
|
300
454
|
# Bullets. A run of markers on one line opens nested lists at once
|
|
301
455
|
# (`- - A`), and attributes may be glued straight onto the marker.
|
|
302
|
-
rule %r/#{bol}(#{MARGIN})((?:[-*]
|
|
456
|
+
rule %r/#{bol}(#{MARGIN})((?:[-*] +)*[-*](?:#{ATTRS})?)(?= +[ \t]*[^ \t\n])/ do
|
|
303
457
|
groups Text, Punctuation
|
|
304
458
|
open_block :listitem, :quoteline
|
|
305
459
|
end
|
|
306
460
|
|
|
307
461
|
# Ordered markers: numeric, alphabetic, roman, and the bare `.` that
|
|
308
462
|
# continues the enclosing sequence.
|
|
309
|
-
rule %r/#{bol}(#{MARGIN})(#{ORDERED}(?:#{ATTRS})?)(?=[ \t]
|
|
463
|
+
rule %r/#{bol}(#{MARGIN})(#{ORDERED}(?:#{ATTRS})?)(?= +[ \t]*[^ \t\n])/ do
|
|
310
464
|
groups Text, Num::Integer
|
|
311
465
|
open_block :listitem, :quoteline
|
|
312
466
|
end
|
|
313
|
-
rule %r/#{bol}(#{MARGIN})(\.(?:#{ATTRS})?)(?=[ \t]
|
|
467
|
+
rule %r/#{bol}(#{MARGIN})(\.(?:#{ATTRS})?)(?= +[ \t]*[^ \t\n])/ do
|
|
314
468
|
groups Text, Num::Integer
|
|
315
469
|
open_block :listitem, :quoteline
|
|
316
470
|
end
|
|
@@ -371,6 +525,15 @@ module Rouge
|
|
|
371
525
|
groups Text, Comment::Preproc, Comment
|
|
372
526
|
end
|
|
373
527
|
|
|
528
|
+
rule %r/^(\+)()$#{PLUS_NEXT}/ do |m|
|
|
529
|
+
if continuation_host_before?
|
|
530
|
+
groups Operator, Text
|
|
531
|
+
else
|
|
532
|
+
token Text, m[0]
|
|
533
|
+
push :paragraph
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
374
537
|
mixin :blockopeners
|
|
375
538
|
|
|
376
539
|
# A standalone attribute block, which may span lines.
|
|
@@ -400,6 +563,9 @@ module Rouge
|
|
|
400
563
|
|
|
401
564
|
state :paraline do
|
|
402
565
|
rule(%r/^(?=[ \t]*(?:\n|\z))/) { pop! }
|
|
566
|
+
rule(%r/^(?=\+$#{PLUS_NEXT})/) do
|
|
567
|
+
continuation_host_before? ? pop! : continue_paragraph(:paragraph)
|
|
568
|
+
end
|
|
403
569
|
rule(%r/^(?=#{MARGIN}(?:#{INTERRUPT}))/) { pop! }
|
|
404
570
|
rule(%r/^(?=#{MARGIN}(`{3,}|~{3,}))/) { |m| fence_closed_below?(m[1]) ? pop! : continue_paragraph(:paragraph) }
|
|
405
571
|
rule(%r/^(?=#{MARGIN}\^[ \t])/) { @lone_image ? pop! : continue_paragraph(:paragraph) }
|
|
@@ -422,6 +588,12 @@ module Rouge
|
|
|
422
588
|
|
|
423
589
|
state line do
|
|
424
590
|
rule(%r/^(?=[ \t]*(?:\n|\z))/) { pop! }
|
|
591
|
+
if name == :listitem
|
|
592
|
+
rule %r/^(\+)()$#{PLUS_NEXT}/ do
|
|
593
|
+
groups Operator, Text
|
|
594
|
+
goto name
|
|
595
|
+
end
|
|
596
|
+
end
|
|
425
597
|
rule(%r/^(?=#{MARGIN}(?:#{INTERRUPT}|#{LIST_MARKER}|:[ \t]))/) { pop! }
|
|
426
598
|
rule(%r/^(?=#{MARGIN}(`{3,}|~{3,}))/) { |m| fence_closed_below?(m[1]) ? pop! : goto(name) }
|
|
427
599
|
rule(//) { goto name }
|
|
@@ -849,8 +1021,50 @@ module Rouge
|
|
|
849
1021
|
end
|
|
850
1022
|
end
|
|
851
1023
|
|
|
1024
|
+
# The lexer states cannot carry the open containers' marker columns, so
|
|
1025
|
+
# the `+` scope is settled afterwards from a document-level pass: a `+`
|
|
1026
|
+
# it names is the continuation operator, every other `+` the states
|
|
1027
|
+
# gave Operator is text.
|
|
1028
|
+
def stream_tokens(str, &block)
|
|
1029
|
+
markers = CarveContinuation.markers(str).to_h { |offset| [offset, true] }
|
|
1030
|
+
candidates = CarveContinuation.candidates(str).to_h { |offset| [offset, true] }
|
|
1031
|
+
pos = 0
|
|
1032
|
+
super(str) do |tok, val|
|
|
1033
|
+
if val.include?('+') && candidates.any?
|
|
1034
|
+
retag_plus(tok, val, pos, markers, candidates, &block)
|
|
1035
|
+
else
|
|
1036
|
+
yield tok, val
|
|
1037
|
+
end
|
|
1038
|
+
pos += val.length
|
|
1039
|
+
end
|
|
1040
|
+
end
|
|
1041
|
+
|
|
852
1042
|
private
|
|
853
1043
|
|
|
1044
|
+
def retag_plus(tok, val, pos, markers, candidates)
|
|
1045
|
+
run = +''
|
|
1046
|
+
val.each_char.with_index do |ch, i|
|
|
1047
|
+
scope = if ch != '+' || !candidates[pos + i] || opaque?(tok) then tok
|
|
1048
|
+
elsif markers[pos + i] then Operator
|
|
1049
|
+
elsif tok == Operator then Text
|
|
1050
|
+
else tok
|
|
1051
|
+
end
|
|
1052
|
+
if scope != tok
|
|
1053
|
+
yield tok, run unless run.empty?
|
|
1054
|
+
run = +''
|
|
1055
|
+
yield scope, ch
|
|
1056
|
+
else
|
|
1057
|
+
run << ch
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
yield tok, run unless run.empty?
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Comment and literal payloads keep their scope.
|
|
1064
|
+
def opaque?(tok)
|
|
1065
|
+
tok <= Comment || tok <= Literal
|
|
1066
|
+
end
|
|
1067
|
+
|
|
854
1068
|
# The token a state gives a character that opens nothing.
|
|
855
1069
|
PLAIN_TOKENS = {
|
|
856
1070
|
heading: Generic::Heading, quoteline: Generic::Emph,
|
|
@@ -1083,7 +1297,9 @@ module Rouge
|
|
|
1083
1297
|
# opener that is a single token. After a blockquote marker the opener
|
|
1084
1298
|
# replaces the quote's start state, and a single-token opener hands the
|
|
1085
1299
|
# rest of the line to quoted text.
|
|
1086
|
-
def open_block(name = nil, quoted = name || :quoteline)
|
|
1300
|
+
def open_block(name = nil, quoted = name || :quoteline, host: nil)
|
|
1301
|
+
kind = host || (name == :listitem ? :list : name == :quoteline ? :quote : nil)
|
|
1302
|
+
remember_host(kind) if kind
|
|
1087
1303
|
if state?(:quotestart)
|
|
1088
1304
|
goto quoted
|
|
1089
1305
|
elsif name
|
|
@@ -1091,6 +1307,193 @@ module Rouge
|
|
|
1091
1307
|
end
|
|
1092
1308
|
end
|
|
1093
1309
|
|
|
1310
|
+
def remember_host(kind)
|
|
1311
|
+
@host_stream = @current_stream
|
|
1312
|
+
@last_host_pos = @current_stream.pos
|
|
1313
|
+
@last_host_kind = kind
|
|
1314
|
+
if kind == :term
|
|
1315
|
+
@term_stream = @current_stream
|
|
1316
|
+
@term_pos = @last_host_pos
|
|
1317
|
+
@term_quote_depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1318
|
+
elsif kind != :quote
|
|
1319
|
+
@term_pos = nil
|
|
1320
|
+
end
|
|
1321
|
+
@host_scan = StringScanner.new(@current_stream.string)
|
|
1322
|
+
@host_scan.pos = @last_host_pos
|
|
1323
|
+
@host_scan.scan(/[^\n]*\n/)
|
|
1324
|
+
@host_scan_valid = true
|
|
1325
|
+
@host_scan_fence = nil
|
|
1326
|
+
@host_scan_previous = nil
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
def quote_depth_on_line
|
|
1330
|
+
line_start = line_start_byte
|
|
1331
|
+
@quote_line_start == line_start ? @quote_depth_current : 0
|
|
1332
|
+
end
|
|
1333
|
+
|
|
1334
|
+
def record_quote_depth(depth)
|
|
1335
|
+
@quote_line_start = line_start_byte
|
|
1336
|
+
@quote_depth_current = depth
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
def line_start_byte
|
|
1340
|
+
text = @current_stream.string
|
|
1341
|
+
pos = @current_stream.pos
|
|
1342
|
+
pos -= 1 while pos.positive? && text.getbyte(pos - 1) != 10
|
|
1343
|
+
pos
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
def next_line_after_current
|
|
1347
|
+
scan = StringScanner.new(@current_stream.string)
|
|
1348
|
+
scan.pos = @current_stream.pos
|
|
1349
|
+
return nil unless scan.scan(/[^\n]*\n/)
|
|
1350
|
+
|
|
1351
|
+
scan.scan(/[^\n]*/)
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
def quote_level_follows?(depth)
|
|
1355
|
+
line = next_line_after_current
|
|
1356
|
+
return false unless line
|
|
1357
|
+
depth.times do
|
|
1358
|
+
return false unless line.start_with?('> ')
|
|
1359
|
+
|
|
1360
|
+
line = line[2..]
|
|
1361
|
+
end
|
|
1362
|
+
!line.empty? && !line.match?(/\A[ \t]/)
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
def continuation_host_before?
|
|
1366
|
+
return false unless @host_stream.equal?(@current_stream) && @last_host_pos
|
|
1367
|
+
return false if @last_host_kind == :term
|
|
1368
|
+
return false if @last_host_kind == :quote && !quote_block_follows?
|
|
1369
|
+
|
|
1370
|
+
return false unless @host_scan_valid
|
|
1371
|
+
|
|
1372
|
+
limit = @current_stream.pos
|
|
1373
|
+
while (line = @host_scan.check(/[^\n]*\n/))
|
|
1374
|
+
break if @host_scan.pos + line.bytesize > limit
|
|
1375
|
+
|
|
1376
|
+
@host_scan.scan(/[^\n]*\n/)
|
|
1377
|
+
line = line.chomp("\n")
|
|
1378
|
+
if @host_scan_fence
|
|
1379
|
+
fence = @host_scan_fence
|
|
1380
|
+
@host_scan_fence = nil if line.match?(/\A#{Regexp.escape(fence)}[#{Regexp.escape(fence[0])}]*[ \t]*\z/)
|
|
1381
|
+
elsif line.match?(/\A[ \t]*\z/)
|
|
1382
|
+
@host_scan_valid = false
|
|
1383
|
+
elsif (opening = line.match(/\A(`{3,}|~{3,})/))
|
|
1384
|
+
@host_scan_valid = false unless @host_scan_previous&.match?(/\A\+[ \t]*\z/)
|
|
1385
|
+
@host_scan_fence = opening[1]
|
|
1386
|
+
elsif line.match?(/\A(?:\#{1,6} +|::{1,}|\^ +|\||(?:[-*_][ \t]*){3,}|\[[^\]]+\]: )/)
|
|
1387
|
+
@host_scan_valid = false unless @host_scan_previous&.match?(/\A\+[ \t]*\z/)
|
|
1388
|
+
end
|
|
1389
|
+
@host_scan_previous = line
|
|
1390
|
+
return false unless @host_scan_valid
|
|
1391
|
+
end
|
|
1392
|
+
true
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1395
|
+
def definition_term_before?
|
|
1396
|
+
return false unless @term_stream.equal?(@current_stream) && @term_pos
|
|
1397
|
+
depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1398
|
+
return false unless depth == @term_quote_depth
|
|
1399
|
+
|
|
1400
|
+
between = @current_stream.string.byteslice(@term_pos...@current_stream.pos)
|
|
1401
|
+
lines = between.split("\n", -1)
|
|
1402
|
+
lines.shift
|
|
1403
|
+
lines.pop
|
|
1404
|
+
if depth.positive?
|
|
1405
|
+
prefix = '> ' * depth
|
|
1406
|
+
return false unless lines.all? { |line| line.empty? || line.start_with?(prefix) }
|
|
1407
|
+
|
|
1408
|
+
lines.map! { |line| line.empty? ? line : line[prefix.length..] }
|
|
1409
|
+
end
|
|
1410
|
+
return false if lines.count { |line| line.match?(/\A[ \t]*\z/) } > 1
|
|
1411
|
+
|
|
1412
|
+
lines.none? { |line| line.match?(/\A(?:\#{1,6} +|:{3,}|[-*] +|> |\||`{3,}|~{3,}|(?:[-*_][ \t]*){3,}\z)/) }
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1415
|
+
# A `:` left of the `::` column of every open term above it ends the
|
|
1416
|
+
# definition list and is paragraph text (markup-carve/carve#3458).
|
|
1417
|
+
def separator_below_term?
|
|
1418
|
+
text = @current_stream.string
|
|
1419
|
+
unless @separator_source.equal?(text)
|
|
1420
|
+
@separator_source = text
|
|
1421
|
+
@separator_bytes = text.b
|
|
1422
|
+
@separator_below = {}
|
|
1423
|
+
end
|
|
1424
|
+
start = line_start_byte
|
|
1425
|
+
depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1426
|
+
strip = lambda do |line|
|
|
1427
|
+
depth.times { line = line.start_with?('> ') ? line[2..] : line.sub(/\A>\z/, '') }
|
|
1428
|
+
line
|
|
1429
|
+
end
|
|
1430
|
+
indent = ->(line) { line[/\A[ \t]*/].gsub("\t", ' ').length }
|
|
1431
|
+
column = indent.call(strip.call(text.byteslice(start...(@separator_bytes.index("\n", start) || text.bytesize))))
|
|
1432
|
+
term = nil
|
|
1433
|
+
below = nil
|
|
1434
|
+
finish = start - 1
|
|
1435
|
+
while finish.positive?
|
|
1436
|
+
from = (@separator_bytes.rindex("\n", finish - 1) || -1) + 1
|
|
1437
|
+
line = strip.call(text.byteslice(from...finish))
|
|
1438
|
+
finish = from - 1
|
|
1439
|
+
next if line.strip.empty?
|
|
1440
|
+
|
|
1441
|
+
if line.match?(/\A[ \t]*::[ \t]/)
|
|
1442
|
+
term = [term, indent.call(line)].compact.min
|
|
1443
|
+
break if term <= column
|
|
1444
|
+
elsif line.match?(/\A[ \t]*:[ \t]/)
|
|
1445
|
+
# An earlier separator at this column with no term between reads the same.
|
|
1446
|
+
if term.nil? && indent.call(line) == column && @separator_below.key?(from)
|
|
1447
|
+
below = @separator_below[from]
|
|
1448
|
+
break
|
|
1449
|
+
end
|
|
1450
|
+
elsif term && indent.call(line) < term
|
|
1451
|
+
break
|
|
1452
|
+
end
|
|
1453
|
+
end
|
|
1454
|
+
below = !term.nil? && column < term if below.nil?
|
|
1455
|
+
@separator_below[start] = below
|
|
1456
|
+
end
|
|
1457
|
+
|
|
1458
|
+
# After a definition, only a heading, quote, table or div ends the list.
|
|
1459
|
+
def definition_before?
|
|
1460
|
+
return false unless @def_stream.equal?(@current_stream) && @def_pos
|
|
1461
|
+
depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1462
|
+
return false unless depth == @def_quote_depth
|
|
1463
|
+
|
|
1464
|
+
lines = @current_stream.string.byteslice(@def_pos...@current_stream.pos).split("\n", -1)
|
|
1465
|
+
lines.shift
|
|
1466
|
+
lines.pop
|
|
1467
|
+
prefix = '> ' * depth
|
|
1468
|
+
blank = false
|
|
1469
|
+
lines.all? do |line|
|
|
1470
|
+
if line.empty? || line == prefix.rstrip
|
|
1471
|
+
blank = true
|
|
1472
|
+
next true
|
|
1473
|
+
end
|
|
1474
|
+
next false unless line.start_with?(prefix)
|
|
1475
|
+
|
|
1476
|
+
rest = line[prefix.length..]
|
|
1477
|
+
# a flush-left block after a blank line, or one that interrupts, ends it
|
|
1478
|
+
next false if blank && rest.match?(/\A[^ \t]/)
|
|
1479
|
+
|
|
1480
|
+
blank = false
|
|
1481
|
+
!rest.match?(/\A(?:\#{1,6} +|:{3,}|> |\||::|`{3,}|~{3,}|(?:[-*_][ \t]*){3,}\z)/)
|
|
1482
|
+
end
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
def quote_block_follows?
|
|
1486
|
+
line = next_line_after_current
|
|
1487
|
+
return false unless line
|
|
1488
|
+
return false if line.empty? || line.match?(/\A[ \t]/)
|
|
1489
|
+
|
|
1490
|
+
while line.start_with?('> ')
|
|
1491
|
+
line = line[2..]
|
|
1492
|
+
return false if line.empty? || line.match?(/\A[ \t]/)
|
|
1493
|
+
end
|
|
1494
|
+
line != '>'
|
|
1495
|
+
end
|
|
1496
|
+
|
|
1094
1497
|
def continue_paragraph(name)
|
|
1095
1498
|
@lone_image = false
|
|
1096
1499
|
goto name
|
|
@@ -1199,6 +1602,9 @@ module Rouge
|
|
|
1199
1602
|
def fence_lexer(language)
|
|
1200
1603
|
return nil if language.nil? || language.empty?
|
|
1201
1604
|
|
|
1605
|
+
return CsvTable.new if language == 'csv'
|
|
1606
|
+
return TsvTable.new if language == 'tsv'
|
|
1607
|
+
|
|
1202
1608
|
Lexer.find_fancy(language)
|
|
1203
1609
|
rescue StandardError
|
|
1204
1610
|
nil
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*- #
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Rouge
|
|
5
|
+
module Lexers
|
|
6
|
+
# Decides which `+` characters of a Carve document are continuation
|
|
7
|
+
# markers (Part 9 §17 L3, CARVE-P9-031). The marker is relative: it sits at
|
|
8
|
+
# an open container's marker column, so the answer needs the container
|
|
9
|
+
# stack the line-based lexer states do not carry. This pass walks the
|
|
10
|
+
# document once and returns the character offsets of the markers.
|
|
11
|
+
#
|
|
12
|
+
# It is a reduced block model: list items, quotes, footnotes, definitions
|
|
13
|
+
# and fences, with lazy paragraph lines. Shapes it cannot place (tables,
|
|
14
|
+
# divs, attribute blocks) read as text.
|
|
15
|
+
class CarveContinuation
|
|
16
|
+
TAB = 4
|
|
17
|
+
ORDERED = /\A(?:\d+|[A-Za-z]|[ivxlcdm]+|[IVXLCDM]+)[.)]/.freeze
|
|
18
|
+
BULLET = /\A[-*](?=[ \t]|\z)/.freeze
|
|
19
|
+
FENCE = /\A(`{3,}|~{3,})/.freeze
|
|
20
|
+
|
|
21
|
+
Container = Struct.new(:kind, :col, :content, :quotes, keyword_init: true)
|
|
22
|
+
|
|
23
|
+
# A line whose only content after quote prefixes and openers is `+`:
|
|
24
|
+
# the shapes where the `+` is a marker or a refused marker.
|
|
25
|
+
CANDIDATE = /
|
|
26
|
+
\A(?:[ \t]|>(?=[ \t])|(?:[-*]|(?:\d+|[A-Za-z]|[ivxlcdm]+|[IVXLCDM]+)[.)])(?:\{[^}\n]*\})?(?=[ \t])
|
|
27
|
+
|\[[^\]\n]\](?=[ \t])|::?(?=[ \t])|\[\^[^\]\n]+\]:(?=[ ]))*\+[ \t]*\r?\z
|
|
28
|
+
/x.freeze
|
|
29
|
+
|
|
30
|
+
def self.markers(source)
|
|
31
|
+
new(source).markers
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Offsets of every `+` that stands where a marker could.
|
|
35
|
+
def self.candidates(source)
|
|
36
|
+
offset = 0
|
|
37
|
+
source.split("\n", -1).each_with_object([]) do |line, found|
|
|
38
|
+
found << offset + line.rindex('+') if line.match?(CANDIDATE)
|
|
39
|
+
offset += line.length + 1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(source)
|
|
44
|
+
@lines = source.split("\n", -1)
|
|
45
|
+
@markers = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def markers
|
|
49
|
+
stack = []
|
|
50
|
+
fence = nil
|
|
51
|
+
para = nil
|
|
52
|
+
force = 0
|
|
53
|
+
offset = 0
|
|
54
|
+
blank = false
|
|
55
|
+
@lines.each_with_index do |raw, index|
|
|
56
|
+
line = raw.chomp("\r")
|
|
57
|
+
cols = columns(line)
|
|
58
|
+
matched, pos, quotes = match(stack, line, cols, force)
|
|
59
|
+
rest = line[pos..] || ''
|
|
60
|
+
|
|
61
|
+
if fence && (fence[:forced] || matched >= fence[:depth])
|
|
62
|
+
fence = nil if rest.match?(/\A[ \t]*#{Regexp.escape(fence[:char])}{#{fence[:width]},}[ \t]*\z/)
|
|
63
|
+
offset += raw.length + 1
|
|
64
|
+
force = 0
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
fence = nil
|
|
68
|
+
|
|
69
|
+
lone = rest.match(/\A([ \t]*)\+[ \t]*\z/)
|
|
70
|
+
attached = 0
|
|
71
|
+
if rest.match?(/\A[ \t]*\z/)
|
|
72
|
+
# a blank line ends every quote it does not prefix
|
|
73
|
+
stack.slice!(matched..)
|
|
74
|
+
para = nil
|
|
75
|
+
elsif lone
|
|
76
|
+
col = cols[pos + lone[1].length]
|
|
77
|
+
# A blank line above closes a description or a footnote for a `+`.
|
|
78
|
+
target = stack.rindex do |c|
|
|
79
|
+
c.col == col && c.quotes == quotes && c.kind != :term &&
|
|
80
|
+
!(blank && %i[definition note].include?(c.kind))
|
|
81
|
+
end
|
|
82
|
+
# Only an item refuses a list below it (the sibling rule).
|
|
83
|
+
if target && follower?(index + 1, quotes, col, quote: quotes.zero?,
|
|
84
|
+
list: stack[target].kind != :item)
|
|
85
|
+
@markers << offset + pos + lone[1].length
|
|
86
|
+
stack.slice!((target + 1)..)
|
|
87
|
+
attached = stack.length
|
|
88
|
+
para = nil
|
|
89
|
+
else
|
|
90
|
+
stack.slice!(matched..) unless para
|
|
91
|
+
para ||= stack.length
|
|
92
|
+
end
|
|
93
|
+
elsif para && (!opener?(rest) || lazy_list?(stack, rest, quotes) || unclosed_fence?(rest, index))
|
|
94
|
+
# a lazy line keeps every container open
|
|
95
|
+
else
|
|
96
|
+
stack.slice!(matched..)
|
|
97
|
+
para, fence, plus = open_blocks(stack, line, cols, pos, quotes, para, index)
|
|
98
|
+
fence[:forced] = true if fence && force.positive?
|
|
99
|
+
if plus
|
|
100
|
+
@markers << offset + plus
|
|
101
|
+
attached = stack.length
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
force = attached
|
|
105
|
+
blank = rest.match?(/\A[ \t]*\z/)
|
|
106
|
+
offset += raw.length + 1
|
|
107
|
+
end
|
|
108
|
+
@markers
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# The visual column of every character position, tabs expanded.
|
|
114
|
+
def columns(line)
|
|
115
|
+
cols = []
|
|
116
|
+
col = 0
|
|
117
|
+
line.each_char do |ch|
|
|
118
|
+
cols << col
|
|
119
|
+
col = ch == "\t" ? (col / TAB + 1) * TAB : col + 1
|
|
120
|
+
end
|
|
121
|
+
cols << col
|
|
122
|
+
cols
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# How many containers the line continues, where its content starts, and
|
|
126
|
+
# how many quote prefixes it passed.
|
|
127
|
+
# The containers up to +force+ are the ones an attached line enters.
|
|
128
|
+
def match(stack, line, cols, force)
|
|
129
|
+
pos = 0
|
|
130
|
+
quotes = 0
|
|
131
|
+
matched = 0
|
|
132
|
+
stack.each do |c|
|
|
133
|
+
blank = line[pos..].to_s.match?(/\A[ \t]*\z/)
|
|
134
|
+
if c.kind == :quote
|
|
135
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
136
|
+
at = pos + ws.length
|
|
137
|
+
unless line[at] == '>' && (line[at + 1].nil? || line[at + 1].match?(/[ \t]/))
|
|
138
|
+
break unless matched < force
|
|
139
|
+
|
|
140
|
+
matched += 1
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
pos = at + 1
|
|
145
|
+
pos += 1 if line[pos]&.match?(/[ \t]/)
|
|
146
|
+
quotes += 1
|
|
147
|
+
elsif blank
|
|
148
|
+
# a blank line keeps an item open
|
|
149
|
+
else
|
|
150
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
151
|
+
if cols[pos + ws.length] >= c.content || matched < force
|
|
152
|
+
pos = advance(line, cols, pos, c.content)
|
|
153
|
+
else
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
matched += 1
|
|
158
|
+
end
|
|
159
|
+
[matched, pos, quotes]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Move +pos+ over whitespace up to +col+.
|
|
163
|
+
def advance(line, cols, pos, col)
|
|
164
|
+
pos += 1 while line[pos]&.match?(/[ \t]/) && cols[pos + 1] <= col
|
|
165
|
+
pos
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def opener?(rest)
|
|
169
|
+
text = rest.lstrip
|
|
170
|
+
text.match?(BULLET) || text.match?(ORDERED) && text.match?(/\A\S+[ \t]/) ||
|
|
171
|
+
text.start_with?('>', '|') || text.match?(FENCE) ||
|
|
172
|
+
text.match?(/\A(?:\#{1,6}[ \t]|:::|::?[ \t]|\[\^[^\]]+\]:[ ])/)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A list marker cannot interrupt a paragraph unless it continues a list
|
|
176
|
+
# already open in the line's own quote context.
|
|
177
|
+
def lazy_list?(stack, rest, quotes)
|
|
178
|
+
text = rest.lstrip
|
|
179
|
+
return false unless list_marker?(text)
|
|
180
|
+
|
|
181
|
+
stack.none? { |c| c.kind == :item && c.quotes == quotes }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Only a closed fence interrupts a paragraph.
|
|
185
|
+
def unclosed_fence?(rest, index)
|
|
186
|
+
f = rest.lstrip.match(FENCE)
|
|
187
|
+
return false unless f
|
|
188
|
+
|
|
189
|
+
@last_closer ||= {}
|
|
190
|
+
last = @last_closer[f[1]] ||= begin
|
|
191
|
+
closer = /\A[ \t>]*#{Regexp.escape(f[1][0])}{#{f[1].length},}[ \t]*\r?\z/
|
|
192
|
+
@lines.rindex { |l| l.match?(closer) } || -1
|
|
193
|
+
end
|
|
194
|
+
last <= index
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def list_marker?(text)
|
|
198
|
+
text.match?(BULLET) || text.match?(/#{ORDERED.source}(?=[ \t]|\z)/)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The block a line at +col+ opens is the one the marker attaches.
|
|
202
|
+
# Inside a quote, `> +` does not attach a quote right after the prefix.
|
|
203
|
+
def follower?(index, quotes, col, quote: true, list: false)
|
|
204
|
+
line = @lines[index]
|
|
205
|
+
return false if line.nil?
|
|
206
|
+
|
|
207
|
+
line = line.chomp("\r")
|
|
208
|
+
cols = columns(line)
|
|
209
|
+
pos = 0
|
|
210
|
+
quotes.times do
|
|
211
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
212
|
+
return false unless line[pos + ws.length] == '>'
|
|
213
|
+
|
|
214
|
+
pos += ws.length + 1
|
|
215
|
+
pos += 1 if line[pos]&.match?(/[ \t]/)
|
|
216
|
+
end
|
|
217
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
218
|
+
text = line[(pos + ws.length)..]
|
|
219
|
+
return false if text.nil? || text.empty?
|
|
220
|
+
return false unless cols[pos + ws.length] == col
|
|
221
|
+
return false if !quote && ws.empty? && text.start_with?('>')
|
|
222
|
+
|
|
223
|
+
list || !list_marker?(text)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Push the containers a line opens. Returns the paragraph depth, an open
|
|
227
|
+
# fence and the offset of a first-block marker.
|
|
228
|
+
def open_blocks(stack, line, cols, pos, quotes, para, index)
|
|
229
|
+
base = pos
|
|
230
|
+
start = stack.length
|
|
231
|
+
first = true
|
|
232
|
+
last = nil
|
|
233
|
+
loop do
|
|
234
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
235
|
+
at = pos + ws.length
|
|
236
|
+
text = line[at..]
|
|
237
|
+
col = cols[at]
|
|
238
|
+
# `>+`, `>>+` and `>> +` open no quote: each `>` needs a space after it.
|
|
239
|
+
if text.start_with?('>') && (text[1].nil? || text[1] == ' ')
|
|
240
|
+
break if first && at != base
|
|
241
|
+
|
|
242
|
+
stack << Container.new(kind: :quote, col: col, content: col + 2, quotes: quotes)
|
|
243
|
+
quotes += 1
|
|
244
|
+
pos = at + 1
|
|
245
|
+
pos += 1 if line[pos] == ' '
|
|
246
|
+
last = :quote
|
|
247
|
+
elsif (m = text.match(/\A(?:[-*]|#{ORDERED.source})(?:\{[^}\n]*\})?(?= |\z)/))
|
|
248
|
+
|
|
249
|
+
after = at + m[0].length
|
|
250
|
+
gap = line[after..][/\A[ \t]*/]
|
|
251
|
+
content = gap.empty? || cols[after + gap.length] - cols[after] > 4 ? cols[after] + 1 : cols[after + gap.length]
|
|
252
|
+
# attributes glued to the marker take no columns
|
|
253
|
+
content -= cols[after] - cols[at + m[0].index('{').to_i] if m[0].include?('{')
|
|
254
|
+
stack << Container.new(kind: :item, col: col, content: content, quotes: quotes)
|
|
255
|
+
pos = after + gap.length
|
|
256
|
+
last = m[0].start_with?('-', '*') ? :bullet : :ordered
|
|
257
|
+
elsif last == :bullet && (m = text.match(/\A\[[^\]\n]\](?= )/))
|
|
258
|
+
pos = at + m[0].length
|
|
259
|
+
pos += line[pos..][/\A[ \t]*/].length
|
|
260
|
+
last = :task
|
|
261
|
+
elsif (first || last == :quote) && text.match?(/\A\[\^[^\]\n]+\]: /)
|
|
262
|
+
m = text.match(/\A\[\^[^\]\n]+\]: /)
|
|
263
|
+
stack << Container.new(kind: :note, col: col, content: col + 2, quotes: quotes)
|
|
264
|
+
pos = at + m[0].length
|
|
265
|
+
last = :note
|
|
266
|
+
elsif (first || last == :quote) && text.match?(/\A:: /)
|
|
267
|
+
@term_open = stack.length
|
|
268
|
+
stack << Container.new(kind: :term, col: col, content: col + 2, quotes: quotes)
|
|
269
|
+
return [nil, nil, nil]
|
|
270
|
+
elsif (first || last == :quote) && @term_open == stack.length && text.match?(/\A: /)
|
|
271
|
+
stack << Container.new(kind: :definition, col: col, content: col + 2, quotes: quotes)
|
|
272
|
+
pos = at + 2
|
|
273
|
+
last = :definition
|
|
274
|
+
else
|
|
275
|
+
break
|
|
276
|
+
end
|
|
277
|
+
first = false
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
281
|
+
text = line[(pos + ws.length)..] || ''
|
|
282
|
+
@term_open = last == :definition ? stack.length - 1 : nil
|
|
283
|
+
if last && text.match?(/\A\+[ \t]*\z/)
|
|
284
|
+
case last
|
|
285
|
+
when :quote
|
|
286
|
+
hosted = (start...stack.length).find { |i| stack[i].kind == :item }
|
|
287
|
+
spaced = !hosted || !line[base...pos].match?(/\S[ \t]{2,}/)
|
|
288
|
+
return [nil, nil, pos] if spaced && ws.empty? && first_block_follower?(index + 1, stack, cols[base], hosted, start)
|
|
289
|
+
when :note
|
|
290
|
+
return [nil, nil, pos + ws.length] if first_block_follower?(index + 1, stack, cols[base])
|
|
291
|
+
else
|
|
292
|
+
return [nil, nil, pos + ws.length]
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
return [nil, nil, nil] if text.empty?
|
|
296
|
+
|
|
297
|
+
# at document level a fence opens at column 0 only
|
|
298
|
+
if (f = text.match(FENCE)) && (stack.any? || ws.empty? || pos != base)
|
|
299
|
+
return [nil, { char: f[1][0], width: f[1].length, depth: stack.length }, nil]
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
[stack.length, nil, nil]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# A first-block marker after a quote or a footnote attaches a non-list
|
|
306
|
+
# block at document column 0 or at the column where the line's openers
|
|
307
|
+
# began; right after an enclosing quote's prefix counts as column 0 of
|
|
308
|
+
# that quote. A line that re-enters the opened container is its text.
|
|
309
|
+
# A quote opened behind a list item on the same line (+hosted+, the
|
|
310
|
+
# stack index of the first item the line opens) reads the follower at
|
|
311
|
+
# its raw column: the containers from that item on are not entered, and
|
|
312
|
+
# the item's and the owning quote's columns are eligible too
|
|
313
|
+
# (markup-carve/carve#3421, corpus 649 to 656). Its markers are one space
|
|
314
|
+
# apart: `- > +` is text.
|
|
315
|
+
def first_block_follower?(index, stack, base_col, hosted = nil, start = nil)
|
|
316
|
+
line = @lines[index]
|
|
317
|
+
return false if line.nil?
|
|
318
|
+
|
|
319
|
+
line = line.chomp("\r")
|
|
320
|
+
cols = columns(line)
|
|
321
|
+
pos = 0
|
|
322
|
+
reached = 0
|
|
323
|
+
after_quote = true
|
|
324
|
+
(hosted ? stack[0...hosted] : stack).each do |c|
|
|
325
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
326
|
+
at = pos + ws.length
|
|
327
|
+
if c.kind == :quote
|
|
328
|
+
break unless line[at] == '>' && (line[at + 1].nil? || line[at + 1] == ' ')
|
|
329
|
+
|
|
330
|
+
pos = at + 1
|
|
331
|
+
pos += 1 if line[pos] == ' '
|
|
332
|
+
after_quote = true
|
|
333
|
+
else
|
|
334
|
+
break unless line[at] && cols[at] >= c.content
|
|
335
|
+
|
|
336
|
+
pos = advance(line, cols, pos, c.content)
|
|
337
|
+
after_quote = false
|
|
338
|
+
end
|
|
339
|
+
reached += 1
|
|
340
|
+
end
|
|
341
|
+
return false if !hosted && reached == stack.length
|
|
342
|
+
|
|
343
|
+
ws = line[pos..][/\A[ \t]*/]
|
|
344
|
+
at = pos + ws.length
|
|
345
|
+
text = line[at..] || ''
|
|
346
|
+
return false if text.empty? || list_marker?(text)
|
|
347
|
+
return false if hosted && text.match?(/\A\+[ \t]*\z/)
|
|
348
|
+
|
|
349
|
+
# Stacked quotes opened on one line (`> > +`) take the first one's column.
|
|
350
|
+
own = hosted || start.nil? ? stack.last.col : stack[start].col
|
|
351
|
+
return cols[at].zero? || cols[at] == own if reached.zero?
|
|
352
|
+
|
|
353
|
+
cols[at] == base_col || (after_quote && ws.empty?) || (hosted && [stack[hosted].col, stack.last.col].include?(cols[at]))
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
data/lib/rouge-carve/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.1.4
|
|
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-10-
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rouge
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/rouge-carve/version.rb
|
|
48
48
|
- lib/rouge/demos/carve
|
|
49
49
|
- lib/rouge/lexers/carve.rb
|
|
50
|
+
- lib/rouge/lexers/carve_continuation.rb
|
|
50
51
|
homepage: https://github.com/markup-carve/rouge-carve
|
|
51
52
|
licenses:
|
|
52
53
|
- MIT
|