rouge-carve 0.1.3 → 0.1.5
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 +17 -0
- data/README.md +13 -70
- data/lib/rouge/demos/carve +4 -4
- data/lib/rouge/lexers/carve.rb +494 -83
- 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: 56fbae7b1785bf82049bdeb87ee240391843fd025ae86d383ad7b48b3b3e9562
|
|
4
|
+
data.tar.gz: bc9a2e6b121c743dec62e18c059105fd2b3959a0d4fc09684849475c071a7e76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be5892326629840d6697bb02c14875b1b075b5220d393e88fd5f85af894af8ada36911dd81c90eb3bb8b4d2cb2c7bc182731704a18c6c2d0ef7f4ef459e360a5
|
|
7
|
+
data.tar.gz: c14089a1b08264317473a18c84fdad82a946499096f5ef0143952b073a74a4d37da745a53d469a635bae2a752c15a94076c16a2abb4429f85c15504581cce2f5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.5] - 2026-11-12
|
|
6
|
+
|
|
7
|
+
- Braced cell alignment (`|={>} Price |`, `|={<^ .x} Name |`, `|{~} 1 |`) lexes as an attribute with its alignment run as an operator, like the deprecated `|=>` prefix; cell pipes and escaped pipes stay outside it (#37).
|
|
8
|
+
- `->`, `<-`, `<->` and `=>` are text, and `-->` and `<--` need their exact hyphen run (#38).
|
|
9
|
+
- A mention or tag name needs an ASCII letter (#38).
|
|
10
|
+
- A first-line `---` opens frontmatter through the end of the document when no closer follows; a mid-document `---` never does (#38).
|
|
11
|
+
- A letter or roman marker with a dot needs two spaces, and a definition term is one line (#38).
|
|
12
|
+
- A code fence opener and a `%%%` comment span open without a closer lookahead, and a multiline attribute block continues only on lines indented past its `{` (#38).
|
|
13
|
+
|
|
14
|
+
## [0.1.4] - 2026-10-23
|
|
15
|
+
|
|
16
|
+
- `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).
|
|
17
|
+
- 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).
|
|
18
|
+
- A quote-prefixed `+` needs one space after each `>`: `>+`, `>>+` and `>> +` are paragraph text (#33).
|
|
19
|
+
- 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).
|
|
20
|
+
- A `:` left of the `::` column of the definition list it would continue is paragraph text (markup-carve/carve#3458) (#34).
|
|
21
|
+
|
|
5
22
|
## [0.1.3] - 2026-10-14
|
|
6
23
|
|
|
7
24
|
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
|
@@ -1,83 +1,26 @@
|
|
|
1
1
|
# rouge-carve
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[Carve](https://markup-carve.github.io/carve/) markup language.
|
|
3
|
+
Rouge lexer for Carve 0.1 syntax. It registers `.crv`, `.carve`, and Carve fenced code for Rouge 3.30 or later. Read the [Carve documentation](https://markup-carve.github.io/carve/) for the language syntax.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
7
8
|
gem install rouge-carve
|
|
8
9
|
```
|
|
9
10
|
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
10
13
|
```ruby
|
|
11
14
|
require 'rouge-carve'
|
|
12
|
-
|
|
13
|
-
Rouge::Lexer.find('carve') # => Rouge::Lexers::Carve
|
|
14
|
-
Rouge::Lexer.guess(filename: 'notes.crv') # => Rouge::Lexers::Carve
|
|
15
|
+
Rouge::Lexer.find('carve')
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Requiring the gem registers the lexer. Anything that already highlights with
|
|
20
|
-
Rouge - GitLab, Jekyll, Redcarpet, most Ruby static site generators - then
|
|
21
|
-
handles `.crv` and `.carve` files, and ` ```carve ` fenced blocks, without
|
|
22
|
-
further configuration.
|
|
23
|
-
|
|
24
|
-
## Why not just use the Markdown lexer
|
|
25
|
-
|
|
26
|
-
Because it is not close enough to be wrong quietly. Carve deliberately changed
|
|
27
|
-
the delimiters Markdown got backwards, and three of them **invert**:
|
|
28
|
-
|
|
29
|
-
| Carve | means | Markdown reads it as |
|
|
30
|
-
| --- | --- | --- |
|
|
31
|
-
| `*bold*` | strong | emphasis |
|
|
32
|
-
| `/italic/` | emphasis | literal slashes |
|
|
33
|
-
| `_under_` | underline | emphasis |
|
|
34
|
-
| `~strike~` | strikethrough | subscript, or literal |
|
|
35
|
-
| `{=mark=}` | highlight | literal braces |
|
|
36
|
-
| `{^sup^}` | superscript | literal braces |
|
|
37
|
-
|
|
38
|
-
Highlighting a Carve document as Markdown does not degrade to plain text. It
|
|
39
|
-
shows bold where the author wrote emphasis.
|
|
40
|
-
|
|
41
|
-
## What it covers
|
|
42
|
-
|
|
43
|
-
Every construct in the language: headings, containers (`::: note`), fenced and
|
|
44
|
-
raw blocks, tables with alignment and per-row attributes, definition lists,
|
|
45
|
-
footnotes, citations, cross-references, attribute blocks, task items, critic
|
|
46
|
-
markup, symbol shortcodes, mentions, tags and the typographic runs.
|
|
47
|
-
|
|
48
|
-
**Fenced code is delegated.** A ` ```ruby ` block inside a `.crv` file is lexed
|
|
49
|
-
as Ruby, because that is what a reader expects. A ` ```=html ` raw block is not:
|
|
50
|
-
its info string names an output format, not a language, so the payload stays
|
|
51
|
-
opaque.
|
|
52
|
-
|
|
53
|
-
**Fence widths are respected.** A three-backtick line inside a four-backtick
|
|
54
|
-
fence is content, not a closer. The width is closed over per block rather than
|
|
55
|
-
approximated, which the sibling grammars in other ecosystems cannot do.
|
|
56
|
-
|
|
57
|
-
## What it cannot say
|
|
58
|
-
|
|
59
|
-
Rouge has no underline token - the vocabulary stops at `Generic::Emph`,
|
|
60
|
-
`Generic::Strong` and `Generic::EmphStrong`. Carve's `_x_` is underline, so its
|
|
61
|
-
content shares `Generic::Emph` with italic. The delimiters stay `Punctuation`,
|
|
62
|
-
so a consumer can still tell the two apart by the delimiter each carries.
|
|
63
|
-
|
|
64
|
-
Block openers are matched at any indent. Carve opens a block at column 0 or at
|
|
65
|
-
an enclosing container's content column and nowhere in between, which needs a
|
|
66
|
-
container model this lexer does not carry. The trade-off is deliberate and
|
|
67
|
-
shared with the Prism, highlight.js and Pygments grammars, so the four agree:
|
|
68
|
-
it over-colours an indented-at-document-level opener rather than
|
|
69
|
-
under-colouring the far more common indented construct inside a list item.
|
|
70
|
-
|
|
71
|
-
## Related
|
|
18
|
+
## Documentation
|
|
72
19
|
|
|
73
|
-
- [
|
|
74
|
-
- [
|
|
75
|
-
|
|
76
|
-
- [carve-grammars](https://github.com/markup-carve/carve-grammars) - TextMate,
|
|
77
|
-
Prism and highlight.js
|
|
78
|
-
- [carve-css](https://github.com/markup-carve/carve-css) - styles for the HTML
|
|
79
|
-
Carve renders
|
|
20
|
+
- [Carve documentation](https://markup-carve.github.io/carve/)
|
|
21
|
+
- [Documentation index](docs/README.md)
|
|
22
|
+
- [Development](docs/development.md)
|
|
80
23
|
|
|
81
|
-
##
|
|
24
|
+
## License
|
|
82
25
|
|
|
83
|
-
|
|
26
|
+
[MIT](LICENSE)
|
data/lib/rouge/demos/carve
CHANGED
|
@@ -21,13 +21,13 @@ A word with no registered handler renders as a generic div, which is what
|
|
|
21
21
|
makes a whole family of constructs possible without touching the parser.
|
|
22
22
|
:::
|
|
23
23
|
|
|
24
|
-
```
|
|
24
|
+
```ruby "example.rb"
|
|
25
25
|
def greet(name) = "Hello, #{name}!"
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|= Engine |= Status |
|
|
29
|
-
| carve-js | shipped
|
|
30
|
-
| carve-rs | shipped
|
|
28
|
+
|= Engine |={~} Status |
|
|
29
|
+
| carve-js | shipped |
|
|
30
|
+
| carve-rs | shipped |
|
|
31
31
|
|
|
32
32
|
^ Table 1: the reference implementations.
|
|
33
33
|
|
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
|
|
|
@@ -88,6 +158,10 @@ module Rouge
|
|
|
88
158
|
|#{LINE_BREAK}
|
|
89
159
|
)*\}
|
|
90
160
|
/x.freeze
|
|
161
|
+
# The same block as an interrupting line, whose continuations must be
|
|
162
|
+
# indented (carve#5080). Exact at column 0; an indented block is checked
|
|
163
|
+
# against its own `{` column in the :block rule.
|
|
164
|
+
ATTRS_INTERRUPT = Regexp.new(ATTRS_LINE.source.gsub(LINE_BREAK, '\n(?=[ \t]+[^ \t\n])'), Regexp::EXTENDED)
|
|
91
165
|
|
|
92
166
|
# The prefix of a line inside a blockquote, nested markers included.
|
|
93
167
|
# Each `>` needs a space or the line end after it: `>foo` and `>> x`
|
|
@@ -107,40 +181,50 @@ module Rouge
|
|
|
107
181
|
end
|
|
108
182
|
|
|
109
183
|
# An ordered marker: a number, one letter, or a roman numeral. `Body.`
|
|
110
|
-
# is a word, not a marker
|
|
111
|
-
|
|
184
|
+
# is a word, not a marker, and a letter with `.` needs two spaces, so
|
|
185
|
+
# `J. R. R. Tolkien` is prose (carve#4811).
|
|
186
|
+
ORDERED = "(?:\\d+[.)]|(?:[A-Za-z]|[ivxlcdm]+|[IVXLCDM]+)(?:\\)|\\.(?=(?:#{ATTRS})? )))"
|
|
187
|
+
|
|
188
|
+
# The states read a column-0 `+` with a flush-left block below as an
|
|
189
|
+
# attachment. Whether a `+` is the marker is settled by CarveContinuation.
|
|
190
|
+
PLUS_NEXT = /(?=\n[^ \t\n])/.freeze
|
|
112
191
|
|
|
113
192
|
# A list marker at the start of a line, as the list rules below read it.
|
|
114
193
|
LIST_MARKER = "(?:[-*]|#{ORDERED}|\\.)(?:#{ATTRS})?(?:[ \\t]|$)"
|
|
194
|
+
OPENER_RUN = "(?:(?:[-*]|#{ORDERED}|\\.)(?:#{ATTRS})? +[ \\t]*|> )+"
|
|
195
|
+
|
|
196
|
+
# A cell's alignment symbols: horizontal, then an optional vertical one.
|
|
197
|
+
CELL_ALIGN = '(?:[<>~][\\^~v]?|\\?[\\^~v])'
|
|
198
|
+
# A cell's attribute block when it opens with alignment (carve#5091 D3),
|
|
199
|
+
# glued to its `|` or `|=` and followed by the cell's space.
|
|
200
|
+
CELL_BRACE = %(({)(#{CELL_ALIGN})((?:[ ]+(?:"(?:[^"\\\\\n]|\\\\.)*"|'(?:[^'\\\\\n]|\\\\.)*'|[^{}"'|\n])*)?)(})(?= ))
|
|
115
201
|
|
|
116
202
|
# A table row needs its closing pipe: `| not a table` is a paragraph.
|
|
117
203
|
ROW_END = /(?=(?:\\.|[^\\\n])*\|[ \t]*(?:#{ATTRS})?[ \t]*$)/.freeze
|
|
118
204
|
|
|
119
205
|
# The block openers that interrupt a paragraph. A list marker and a
|
|
120
|
-
# caption do not
|
|
121
|
-
#
|
|
206
|
+
# caption do not. A fence opener does without looking for its closer
|
|
207
|
+
# (carve#4018); a backtick in a backtick info string makes it a code span.
|
|
122
208
|
INTERRUPT = %r/
|
|
123
209
|
\#{1,6}[ \t] | >(?:[ \t]|$) | ::: | ::[ \t] | %%
|
|
210
|
+
| `{3,}[^`\n]*$ | ~{3,}
|
|
124
211
|
| (?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$
|
|
125
212
|
| \|=?[<>^v~]*#{ROW_END}
|
|
126
213
|
| (?:\[\^|\*\[|\[)[^\]\n]+\]:[ ]
|
|
127
|
-
| #{
|
|
214
|
+
| #{ATTRS_INTERRUPT}[ \t]*$
|
|
128
215
|
/x.freeze
|
|
129
216
|
|
|
130
217
|
# Where a bracketed inline body must close, by the block it sits in: a
|
|
131
218
|
# paragraph ends at a blank line or an interrupting block, a list item
|
|
132
219
|
# 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
220
|
BODY_ENDS = {
|
|
135
221
|
para: /\n(?=[ \t]*(?:\n|\z)|#{MARGIN}(?:#{INTERRUPT}))/,
|
|
136
222
|
item: /\n(?=[ \t]*(?:\n|\z)|#{MARGIN}(?:#{INTERRUPT}|#{LIST_MARKER}|:[ \t]))/,
|
|
137
223
|
line: /\n/
|
|
138
224
|
}.freeze
|
|
139
|
-
FENCE_NEXT = /\n(?=#{MARGIN}(`{3,}|~{3,}))/.freeze
|
|
140
|
-
|
|
141
225
|
# The block states that decide which of those ends applies.
|
|
142
226
|
BODY_SCOPE = {
|
|
143
|
-
paragraph: :para, listitem: :item, dlterm: :
|
|
227
|
+
paragraph: :para, listitem: :item, dlterm: :line, heading: :line,
|
|
144
228
|
caption: :line, quoteline: :line, tablerow: :line, defline: :line
|
|
145
229
|
}.freeze
|
|
146
230
|
|
|
@@ -184,8 +268,15 @@ module Rouge
|
|
|
184
268
|
PLAIN_NO_PIPE = plain_run('|').freeze
|
|
185
269
|
PLAIN_NO_BRACKET = plain_run('\\]').freeze
|
|
186
270
|
|
|
271
|
+
# The document start. Front matter opens only here: Rouge 3 and 4 scan
|
|
272
|
+
# without a fixed anchor, so \A alone would also match mid-document.
|
|
187
273
|
state :root do
|
|
188
|
-
|
|
274
|
+
rule %r/\A(?)(---)(?:( ?)([A-Za-z0-9]+))?([ \t]*(?:\n|\z))/ do
|
|
275
|
+
groups Text, Punctuation, Text, Keyword::Type, Text
|
|
276
|
+
goto :block
|
|
277
|
+
push :frontmatter
|
|
278
|
+
end
|
|
279
|
+
rule(//) { goto :block }
|
|
189
280
|
end
|
|
190
281
|
|
|
191
282
|
# ------------------------------------------------------------------
|
|
@@ -199,6 +290,7 @@ module Rouge
|
|
|
199
290
|
# enters its state through #open_block, which knows which of the two it
|
|
200
291
|
# is running in.
|
|
201
292
|
def self.block_openers(bol)
|
|
293
|
+
root_margin = bol == '^' ? '()' : '( )'
|
|
202
294
|
proc do
|
|
203
295
|
# A raw block: the `=FORMAT` info string routes the payload to that
|
|
204
296
|
# output format verbatim. Emitted as one token including the `=`,
|
|
@@ -233,14 +325,14 @@ module Rouge
|
|
|
233
325
|
end
|
|
234
326
|
|
|
235
327
|
# A caption line attaches to the block above or below it.
|
|
236
|
-
rule %r/#{bol}(#{MARGIN})(\^)([ \t]
|
|
328
|
+
rule %r/#{bol}(#{MARGIN})(\^)( +)(?=[ \t]*[^ \t\n])/ do
|
|
237
329
|
groups Text, Punctuation, Text
|
|
238
330
|
open_block :caption
|
|
239
331
|
end
|
|
240
332
|
|
|
241
333
|
# Headings. Carve has no setext form, so a `#` run is the only
|
|
242
334
|
# spelling and a trailing `{...}` is NOT an attribute block here.
|
|
243
|
-
rule %r/#{bol}(#{MARGIN})(\#{1,6})([ \t]
|
|
335
|
+
rule %r/#{bol}(#{MARGIN})(\#{1,6})( +)(?=[ \t]*[^ \t\n])/ do
|
|
244
336
|
groups Text, Punctuation, Text
|
|
245
337
|
open_block :heading
|
|
246
338
|
end
|
|
@@ -257,7 +349,7 @@ module Rouge
|
|
|
257
349
|
# definition ends with its line.
|
|
258
350
|
rule %r/#{bol}(#{MARGIN})(\[\^)([^\]\n]+)(\]:)( )/ do
|
|
259
351
|
groups Text, Punctuation, Name::Label, Punctuation, Text
|
|
260
|
-
open_block :defline
|
|
352
|
+
open_block(:defline, :defline, host: :footnote)
|
|
261
353
|
end
|
|
262
354
|
rule %r/#{bol}(#{MARGIN})(\*\[)([^\]\n]+)(\]:)( )/ do
|
|
263
355
|
groups Text, Punctuation, Name::Entity, Punctuation, Text
|
|
@@ -270,47 +362,125 @@ module Rouge
|
|
|
270
362
|
|
|
271
363
|
# A definition-list term (`::`) and its definition (`:`). A term
|
|
272
364
|
# may wrap onto lazy lines, and a definition's text is an item's.
|
|
273
|
-
rule %r/#{bol}(#{MARGIN})(::)([ \t]
|
|
365
|
+
rule %r/#{bol}(#{MARGIN})(::)( +)(?=[ \t]*[^ \t\n])/ do
|
|
274
366
|
groups Text, Punctuation, Text
|
|
367
|
+
remember_host(:term)
|
|
275
368
|
open_block :dlterm, :heading
|
|
276
369
|
end
|
|
277
|
-
rule %r/#{bol}
|
|
278
|
-
|
|
370
|
+
rule %r/#{bol}#{root_margin}(:)( +[ \t]*)(\+)()$#{PLUS_NEXT}/ do
|
|
371
|
+
if definition_term_before?
|
|
372
|
+
groups Text, Punctuation, Text, Operator, Text
|
|
373
|
+
else
|
|
374
|
+
groups Text, Punctuation, Text, Text, Text
|
|
375
|
+
end
|
|
279
376
|
open_block :listitem, :quoteline
|
|
280
377
|
end
|
|
378
|
+
# Without a term above, `: b` is paragraph text (a lazy line too).
|
|
379
|
+
rule %r/#{bol}(#{MARGIN})(:)(?= +[ \t]*[^ \t\n])/ do
|
|
380
|
+
# the scan from the last definition is short; the term scan runs once
|
|
381
|
+
if (definition_before? || definition_term_before?) && !separator_below_term?
|
|
382
|
+
groups Text, Punctuation
|
|
383
|
+
term = [@term_stream, @term_pos, @term_quote_depth]
|
|
384
|
+
open_block :listitem, :quoteline
|
|
385
|
+
# the next `: d` may still belong to the same term
|
|
386
|
+
@term_stream, @term_pos, @term_quote_depth = term
|
|
387
|
+
@def_stream = @current_stream
|
|
388
|
+
@def_pos = @last_host_pos
|
|
389
|
+
@def_quote_depth = @term_quote_depth
|
|
390
|
+
else
|
|
391
|
+
groups Text, Text
|
|
392
|
+
open_block :paragraph
|
|
393
|
+
end
|
|
394
|
+
end
|
|
281
395
|
|
|
282
396
|
# A blockquote marker must be followed by a space or end the line;
|
|
283
397
|
# `>foo` and `>> x` are paragraphs, and `> > x` nests. What follows
|
|
284
398
|
# a marker may open a block of its own, so the openers are tried
|
|
285
399
|
# again there.
|
|
286
|
-
rule %r/#{bol}(#{MARGIN})(>)(?=
|
|
400
|
+
rule %r/#{bol}(#{MARGIN})(>)(?= |$)/ do |m|
|
|
287
401
|
groups Text, Punctuation
|
|
288
|
-
|
|
402
|
+
record_quote_depth(quote_depth_on_line + 1)
|
|
403
|
+
remember_host(:quote)
|
|
404
|
+
rooted = state?(:quotestart) ? m[1] == ' ' : m[1].empty?
|
|
405
|
+
if rooted
|
|
406
|
+
state?(:quotestart) ? goto(:quotestart) : push(:quotestart)
|
|
407
|
+
else
|
|
408
|
+
state?(:quotestart) ? goto(:quoteline) : push(:quoteline)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# A task can attach its first block on the marker line.
|
|
413
|
+
rule %r/#{bol}#{root_margin}([-*](?:#{ATTRS})?)( +[ \t]*)(\[[^\]\n]\])( +[ \t]*)(\+)()$#{PLUS_NEXT}/ do
|
|
414
|
+
groups Text, Punctuation, Text, Name::Constant, Text, Operator, Text
|
|
415
|
+
open_block :listitem, :quoteline
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
rule %r/#{bol}#{root_margin}(#{OPENER_RUN})(\+)()$#{PLUS_NEXT}/ do |m|
|
|
419
|
+
token Text, m[1]
|
|
420
|
+
scanner = StringScanner.new(m[2])
|
|
421
|
+
until scanner.eos?
|
|
422
|
+
if (marker = scanner.scan(/(?:#{ORDERED}|\.)(?:#{ATTRS})?/))
|
|
423
|
+
token Num::Integer, marker
|
|
424
|
+
elsif (marker = scanner.scan(/[-*](?:#{ATTRS})?|>/))
|
|
425
|
+
token Punctuation, marker
|
|
426
|
+
else
|
|
427
|
+
token Text, scanner.getch
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
depth = quote_depth_on_line
|
|
431
|
+
token(state?(:quotestart) && !quote_level_follows?(depth) ? Text : Operator, m[3])
|
|
432
|
+
token Text, m[4]
|
|
433
|
+
m[2].rstrip.end_with?('>') ? open_block(:quoteline) : open_block(:listitem, :quoteline)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# A quote opened on a list item's marker line (`- > q`): its `>` is a
|
|
437
|
+
# marker, and a `+` behind it may be the quote's first block
|
|
438
|
+
# (markup-carve/carve#3421).
|
|
439
|
+
rule %r/#{bol}(#{MARGIN})((?:(?:[-*](?:#{ATTRS})?(?: +\[[^\]\n]\])?|#{ORDERED}(?:#{ATTRS})?|\.(?:#{ATTRS})?) +)+)((?:> +)*>)(?:( )(\+)()$|(?= |$))/ do |m|
|
|
440
|
+
token Text, m[1]
|
|
441
|
+
scanner = StringScanner.new(m[2] + m[3])
|
|
442
|
+
until scanner.eos?
|
|
443
|
+
if (marker = scanner.scan(/(?:#{ORDERED}|\.)(?:#{ATTRS})?/))
|
|
444
|
+
token Num::Integer, marker
|
|
445
|
+
elsif (marker = scanner.scan(/\[[^\]\n]\]/))
|
|
446
|
+
token Name::Constant, marker
|
|
447
|
+
elsif (marker = scanner.scan(/[-*](?:#{ATTRS})?|>/))
|
|
448
|
+
token Punctuation, marker
|
|
449
|
+
else
|
|
450
|
+
token Text, scanner.scan(/ +/)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
if m[5]
|
|
454
|
+
token Text, m[4]
|
|
455
|
+
# CarveContinuation settles whether this `+` is the marker.
|
|
456
|
+
token Operator, m[5]
|
|
457
|
+
end
|
|
458
|
+
open_block(:quoteline)
|
|
289
459
|
end
|
|
290
460
|
|
|
291
461
|
# Task items before plain bullets, so the state marker is its own
|
|
292
462
|
# token. The state is any single character, not only a space or an
|
|
293
463
|
# x: `[>]` is deferred and `[-]` is dropped. A `+` is not a bullet in
|
|
294
464
|
# Carve; it continues a container.
|
|
295
|
-
rule %r/#{bol}(#{MARGIN})((?:[-*]|#{ORDERED})(?:#{ATTRS})?)(
|
|
465
|
+
rule %r/#{bol}(#{MARGIN})((?:[-*]|#{ORDERED})(?:#{ATTRS})?)( +)(\[[^\]\n]\])(?= +[ \t]*[^ \t\n])/ do
|
|
296
466
|
groups Text, Punctuation, Text, Name::Constant
|
|
297
467
|
open_block :listitem, :quoteline
|
|
298
468
|
end
|
|
299
469
|
|
|
300
470
|
# Bullets. A run of markers on one line opens nested lists at once
|
|
301
471
|
# (`- - A`), and attributes may be glued straight onto the marker.
|
|
302
|
-
rule %r/#{bol}(#{MARGIN})((?:[-*]
|
|
472
|
+
rule %r/#{bol}(#{MARGIN})((?:[-*] +)*[-*](?:#{ATTRS})?)(?= +[ \t]*[^ \t\n])/ do
|
|
303
473
|
groups Text, Punctuation
|
|
304
474
|
open_block :listitem, :quoteline
|
|
305
475
|
end
|
|
306
476
|
|
|
307
477
|
# Ordered markers: numeric, alphabetic, roman, and the bare `.` that
|
|
308
478
|
# continues the enclosing sequence.
|
|
309
|
-
rule %r/#{bol}(#{MARGIN})(#{ORDERED}(?:#{ATTRS})?)(?=[ \t]
|
|
479
|
+
rule %r/#{bol}(#{MARGIN})(#{ORDERED}(?:#{ATTRS})?)(?= +[ \t]*[^ \t\n])/ do
|
|
310
480
|
groups Text, Num::Integer
|
|
311
481
|
open_block :listitem, :quoteline
|
|
312
482
|
end
|
|
313
|
-
rule %r/#{bol}(#{MARGIN})(\.(?:#{ATTRS})?)(?=[ \t]
|
|
483
|
+
rule %r/#{bol}(#{MARGIN})(\.(?:#{ATTRS})?)(?= +[ \t]*[^ \t\n])/ do
|
|
314
484
|
groups Text, Num::Integer
|
|
315
485
|
open_block :listitem, :quoteline
|
|
316
486
|
end
|
|
@@ -318,6 +488,11 @@ module Rouge
|
|
|
318
488
|
# Tables. The header marker, the alignment run and the separator row
|
|
319
489
|
# are their own tokens; cell content is lexed inline. A row needs its
|
|
320
490
|
# closing pipe.
|
|
491
|
+
rule %r/#{bol}(#{MARGIN})(\|=?)#{CELL_BRACE}#{ROW_END}/ do |m|
|
|
492
|
+
token Text, m[1]
|
|
493
|
+
braced_cell(m)
|
|
494
|
+
open_block :tablerow
|
|
495
|
+
end
|
|
321
496
|
rule %r/#{bol}(#{MARGIN})(\|=[<>^v~]*)#{ROW_END}/ do
|
|
322
497
|
groups Text, Operator
|
|
323
498
|
open_block :tablerow
|
|
@@ -333,32 +508,19 @@ module Rouge
|
|
|
333
508
|
state(:quoteopeners, &block_openers(''))
|
|
334
509
|
|
|
335
510
|
state :block do
|
|
336
|
-
# Front matter, only at the very start of the document. \A is what
|
|
337
|
-
# keeps a `---yaml` line mid-document from opening one.
|
|
338
|
-
rule %r/\A(?)(---)([a-zA-Z][\w-]*)?([ \t]*\n)(?=[\s\S]*?^---[ \t]*$)/ do
|
|
339
|
-
groups Text, Punctuation, Keyword::Type, Text
|
|
340
|
-
push :frontmatter
|
|
341
|
-
end
|
|
342
|
-
|
|
343
511
|
# A comment fence (%%% or longer) versus a one-line comment. The fence
|
|
344
512
|
# has to be tried first: %%% also matches the one-line form. Only a
|
|
345
513
|
# run of the SAME length closes it, so the width is closed over the
|
|
346
|
-
# way #push_fence does it
|
|
347
|
-
#
|
|
514
|
+
# way #push_fence does it. No closer is looked for: an unclosed span
|
|
515
|
+
# ends at a line left of its opener, which misses the opener's owner,
|
|
516
|
+
# or at the end of the document. Text after either run is comment.
|
|
348
517
|
rule %r/^(#{MARGIN})(%%%+)([^\n]*)(\n)/ do |m|
|
|
349
518
|
closer = %r/^([ \t]*)(%{#{m[2].length}})(?!%)([^\n]*)$/
|
|
350
|
-
|
|
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
|
|
519
|
+
outside = m[1].empty? ? nil : %r/^(?=[ \t]{0,#{m[1].length - 1}}[^ \t\n])/
|
|
359
520
|
|
|
360
521
|
groups Text, Comment::Preproc, Comment, Text
|
|
361
522
|
push do
|
|
523
|
+
rule(outside) { pop! } if outside
|
|
362
524
|
rule closer do
|
|
363
525
|
groups Text, Comment::Preproc, Comment
|
|
364
526
|
pop!
|
|
@@ -371,19 +533,31 @@ module Rouge
|
|
|
371
533
|
groups Text, Comment::Preproc, Comment
|
|
372
534
|
end
|
|
373
535
|
|
|
536
|
+
rule %r/^(\+)()$#{PLUS_NEXT}/ do |m|
|
|
537
|
+
if continuation_host_before?
|
|
538
|
+
groups Operator, Text
|
|
539
|
+
else
|
|
540
|
+
token Text, m[0]
|
|
541
|
+
push :paragraph
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
374
545
|
mixin :blockopeners
|
|
375
546
|
|
|
376
|
-
# A standalone attribute block, which may span lines
|
|
377
|
-
|
|
378
|
-
|
|
547
|
+
# A standalone attribute block, which may span lines indented past its
|
|
548
|
+
# `{` (carve#5080). One that does not is a paragraph.
|
|
549
|
+
rule %r/^(#{MARGIN})(#{ATTRS_LINE})/ do |m|
|
|
550
|
+
width = m[1].length
|
|
551
|
+
if m[2].split("\n").drop(1).all? { |line| line[/\A[ \t]*/].length > width }
|
|
552
|
+
groups Text, Name::Attribute
|
|
553
|
+
else
|
|
554
|
+
@current_stream.unscan
|
|
555
|
+
start_paragraph
|
|
556
|
+
end
|
|
379
557
|
end
|
|
380
558
|
|
|
381
559
|
# Any other line with content starts a paragraph.
|
|
382
|
-
rule
|
|
383
|
-
@lone_image = !@current_stream.check(IMAGE_LINE).nil?
|
|
384
|
-
@indented = @current_stream.check(/[ \t]/) ? true : false
|
|
385
|
-
push :paragraph
|
|
386
|
-
end
|
|
560
|
+
rule(%r/^(?=#{MARGIN}\S)/) { start_paragraph }
|
|
387
561
|
|
|
388
562
|
mixin :inline
|
|
389
563
|
end
|
|
@@ -400,8 +574,10 @@ module Rouge
|
|
|
400
574
|
|
|
401
575
|
state :paraline do
|
|
402
576
|
rule(%r/^(?=[ \t]*(?:\n|\z))/) { pop! }
|
|
577
|
+
rule(%r/^(?=\+$#{PLUS_NEXT})/) do
|
|
578
|
+
continuation_host_before? ? pop! : continue_paragraph(:paragraph)
|
|
579
|
+
end
|
|
403
580
|
rule(%r/^(?=#{MARGIN}(?:#{INTERRUPT}))/) { pop! }
|
|
404
|
-
rule(%r/^(?=#{MARGIN}(`{3,}|~{3,}))/) { |m| fence_closed_below?(m[1]) ? pop! : continue_paragraph(:paragraph) }
|
|
405
581
|
rule(%r/^(?=#{MARGIN}\^[ \t])/) { @lone_image ? pop! : continue_paragraph(:paragraph) }
|
|
406
582
|
# An indented paragraph sits in a container, so a list marker below it
|
|
407
583
|
# opens a sublist or the next item rather than continuing it.
|
|
@@ -414,16 +590,26 @@ module Rouge
|
|
|
414
590
|
{ listitem: Text, dlterm: Generic::Heading }.each do |name, tok|
|
|
415
591
|
line = :"#{name}line"
|
|
416
592
|
state name do
|
|
417
|
-
|
|
593
|
+
if name == :dlterm
|
|
594
|
+
rule(/\n/) { token Text; pop! }
|
|
595
|
+
else
|
|
596
|
+
rule(/\n/) { token Text; goto line }
|
|
597
|
+
end
|
|
418
598
|
mixin :inlinecontent
|
|
419
599
|
rule PLAIN, tok
|
|
420
600
|
rule(/./) { token plain_token }
|
|
421
601
|
end
|
|
602
|
+
next if name == :dlterm
|
|
422
603
|
|
|
423
604
|
state line do
|
|
424
605
|
rule(%r/^(?=[ \t]*(?:\n|\z))/) { pop! }
|
|
606
|
+
if name == :listitem
|
|
607
|
+
rule %r/^(\+)()$#{PLUS_NEXT}/ do
|
|
608
|
+
groups Operator, Text
|
|
609
|
+
goto name
|
|
610
|
+
end
|
|
611
|
+
end
|
|
425
612
|
rule(%r/^(?=#{MARGIN}(?:#{INTERRUPT}|#{LIST_MARKER}|:[ \t]))/) { pop! }
|
|
426
|
-
rule(%r/^(?=#{MARGIN}(`{3,}|~{3,}))/) { |m| fence_closed_below?(m[1]) ? pop! : goto(name) }
|
|
427
613
|
rule(//) { goto name }
|
|
428
614
|
end
|
|
429
615
|
end
|
|
@@ -478,6 +664,9 @@ module Rouge
|
|
|
478
664
|
rule %r/$/, Text, :pop!
|
|
479
665
|
# A header cell marker, optionally carrying an alignment run. The pipe
|
|
480
666
|
# is part of it, so `|=` reads as one construct.
|
|
667
|
+
# An escaped pipe is consumed by the escape rule first, so a `|` here
|
|
668
|
+
# always ends a cell.
|
|
669
|
+
rule(%r/(\|=?)#{CELL_BRACE}/) { |m| braced_cell(m) }
|
|
481
670
|
rule %r/\|=[<>^v~]*/, Operator
|
|
482
671
|
# A separator row cell, in the native and the GFM spelling.
|
|
483
672
|
rule %r/(?<=\|)[ \t]*:?-{2,}:?[ \t]*(?=\|)/, Punctuation
|
|
@@ -705,12 +894,12 @@ module Rouge
|
|
|
705
894
|
|
|
706
895
|
# A mention and a tag, each one token: the sigil is part of the name
|
|
707
896
|
# rather than punctuation beside it.
|
|
708
|
-
rule %r/(?<![\w\/])@[\w][\w.-]*/, Name::Variable::Magic
|
|
709
|
-
rule %r/(?<![\w&])\#[\w][\w-]*/, Name::Variable::Instance
|
|
897
|
+
rule %r/(?<![\w\/])@(?=[\w.-]*?[A-Za-z])[\w][\w.-]*/, Name::Variable::Magic
|
|
898
|
+
rule %r/(?<![\w&])\#(?=[\w-]*?[A-Za-z])[\w][\w-]*/, Name::Variable::Instance
|
|
710
899
|
|
|
711
900
|
# Typographic runs, longest first: an arrow is not an en dash plus a
|
|
712
901
|
# stray angle bracket, and `---` is not `--` plus `-`.
|
|
713
|
-
rule %r
|
|
902
|
+
rule %r/<-->|<--(?!-)|(?<!-)-->|<=>|==>|<==/, Operator
|
|
714
903
|
rule %r/(?<!-)---(?!-)|(?<!-)--(?!-)/, Punctuation
|
|
715
904
|
rule %r/\.\.\./, Punctuation
|
|
716
905
|
end
|
|
@@ -832,15 +1021,6 @@ module Rouge
|
|
|
832
1021
|
state :"#{body}_#{scope}" do
|
|
833
1022
|
rule %r/\]/, Punctuation, :pop!
|
|
834
1023
|
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
1024
|
mixin :inlinecontent
|
|
845
1025
|
rule PLAIN_NO_BRACKET, tok
|
|
846
1026
|
rule %r/[^\]\n]/, tok
|
|
@@ -849,8 +1029,50 @@ module Rouge
|
|
|
849
1029
|
end
|
|
850
1030
|
end
|
|
851
1031
|
|
|
1032
|
+
# The lexer states cannot carry the open containers' marker columns, so
|
|
1033
|
+
# the `+` scope is settled afterwards from a document-level pass: a `+`
|
|
1034
|
+
# it names is the continuation operator, every other `+` the states
|
|
1035
|
+
# gave Operator is text.
|
|
1036
|
+
def stream_tokens(str, &block)
|
|
1037
|
+
markers = CarveContinuation.markers(str).to_h { |offset| [offset, true] }
|
|
1038
|
+
candidates = CarveContinuation.candidates(str).to_h { |offset| [offset, true] }
|
|
1039
|
+
pos = 0
|
|
1040
|
+
super(str) do |tok, val|
|
|
1041
|
+
if val.include?('+') && candidates.any?
|
|
1042
|
+
retag_plus(tok, val, pos, markers, candidates, &block)
|
|
1043
|
+
else
|
|
1044
|
+
yield tok, val
|
|
1045
|
+
end
|
|
1046
|
+
pos += val.length
|
|
1047
|
+
end
|
|
1048
|
+
end
|
|
1049
|
+
|
|
852
1050
|
private
|
|
853
1051
|
|
|
1052
|
+
def retag_plus(tok, val, pos, markers, candidates)
|
|
1053
|
+
run = +''
|
|
1054
|
+
val.each_char.with_index do |ch, i|
|
|
1055
|
+
scope = if ch != '+' || !candidates[pos + i] || opaque?(tok) then tok
|
|
1056
|
+
elsif markers[pos + i] then Operator
|
|
1057
|
+
elsif tok == Operator then Text
|
|
1058
|
+
else tok
|
|
1059
|
+
end
|
|
1060
|
+
if scope != tok
|
|
1061
|
+
yield tok, run unless run.empty?
|
|
1062
|
+
run = +''
|
|
1063
|
+
yield scope, ch
|
|
1064
|
+
else
|
|
1065
|
+
run << ch
|
|
1066
|
+
end
|
|
1067
|
+
end
|
|
1068
|
+
yield tok, run unless run.empty?
|
|
1069
|
+
end
|
|
1070
|
+
|
|
1071
|
+
# Comment and literal payloads keep their scope.
|
|
1072
|
+
def opaque?(tok)
|
|
1073
|
+
tok <= Comment || tok <= Literal
|
|
1074
|
+
end
|
|
1075
|
+
|
|
854
1076
|
# The token a state gives a character that opens nothing.
|
|
855
1077
|
PLAIN_TOKENS = {
|
|
856
1078
|
heading: Generic::Heading, quoteline: Generic::Emph,
|
|
@@ -1033,20 +1255,10 @@ module Rouge
|
|
|
1033
1255
|
entry[1].bsearch { |at| at >= pos } ? scope : nil
|
|
1034
1256
|
end
|
|
1035
1257
|
|
|
1036
|
-
# Where a body opened at +pos+ must end: the first block end
|
|
1037
|
-
# first closed fence, after it.
|
|
1258
|
+
# Where a body opened at +pos+ must end: the first block end after it.
|
|
1038
1259
|
def body_end(text, pos, scope)
|
|
1039
1260
|
ends = BODY_ENDS[scope]
|
|
1040
|
-
|
|
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
|
|
1261
|
+
text.index(ends, pos) || text.length
|
|
1050
1262
|
end
|
|
1051
1263
|
|
|
1052
1264
|
BRACKET_SCAN = /\\.|(?<!`)(`+)(?!`).*?(?<!`)\1(?!`)|\]/.freeze
|
|
@@ -1083,7 +1295,9 @@ module Rouge
|
|
|
1083
1295
|
# opener that is a single token. After a blockquote marker the opener
|
|
1084
1296
|
# replaces the quote's start state, and a single-token opener hands the
|
|
1085
1297
|
# rest of the line to quoted text.
|
|
1086
|
-
def open_block(name = nil, quoted = name || :quoteline)
|
|
1298
|
+
def open_block(name = nil, quoted = name || :quoteline, host: nil)
|
|
1299
|
+
kind = host || (name == :listitem ? :list : name == :quoteline ? :quote : nil)
|
|
1300
|
+
remember_host(kind) if kind
|
|
1087
1301
|
if state?(:quotestart)
|
|
1088
1302
|
goto quoted
|
|
1089
1303
|
elsif name
|
|
@@ -1091,18 +1305,212 @@ module Rouge
|
|
|
1091
1305
|
end
|
|
1092
1306
|
end
|
|
1093
1307
|
|
|
1308
|
+
def remember_host(kind)
|
|
1309
|
+
@host_stream = @current_stream
|
|
1310
|
+
@last_host_pos = @current_stream.pos
|
|
1311
|
+
@last_host_kind = kind
|
|
1312
|
+
if kind == :term
|
|
1313
|
+
@term_stream = @current_stream
|
|
1314
|
+
@term_pos = @last_host_pos
|
|
1315
|
+
@term_quote_depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1316
|
+
elsif kind != :quote
|
|
1317
|
+
@term_pos = nil
|
|
1318
|
+
end
|
|
1319
|
+
@host_scan = StringScanner.new(@current_stream.string)
|
|
1320
|
+
@host_scan.pos = @last_host_pos
|
|
1321
|
+
@host_scan.scan(/[^\n]*\n/)
|
|
1322
|
+
@host_scan_valid = true
|
|
1323
|
+
@host_scan_fence = nil
|
|
1324
|
+
@host_scan_previous = nil
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
def quote_depth_on_line
|
|
1328
|
+
line_start = line_start_byte
|
|
1329
|
+
@quote_line_start == line_start ? @quote_depth_current : 0
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
def record_quote_depth(depth)
|
|
1333
|
+
@quote_line_start = line_start_byte
|
|
1334
|
+
@quote_depth_current = depth
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
def line_start_byte
|
|
1338
|
+
text = @current_stream.string
|
|
1339
|
+
pos = @current_stream.pos
|
|
1340
|
+
pos -= 1 while pos.positive? && text.getbyte(pos - 1) != 10
|
|
1341
|
+
pos
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
def next_line_after_current
|
|
1345
|
+
scan = StringScanner.new(@current_stream.string)
|
|
1346
|
+
scan.pos = @current_stream.pos
|
|
1347
|
+
return nil unless scan.scan(/[^\n]*\n/)
|
|
1348
|
+
|
|
1349
|
+
scan.scan(/[^\n]*/)
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
def quote_level_follows?(depth)
|
|
1353
|
+
line = next_line_after_current
|
|
1354
|
+
return false unless line
|
|
1355
|
+
depth.times do
|
|
1356
|
+
return false unless line.start_with?('> ')
|
|
1357
|
+
|
|
1358
|
+
line = line[2..]
|
|
1359
|
+
end
|
|
1360
|
+
!line.empty? && !line.match?(/\A[ \t]/)
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
def continuation_host_before?
|
|
1364
|
+
return false unless @host_stream.equal?(@current_stream) && @last_host_pos
|
|
1365
|
+
return false if @last_host_kind == :term
|
|
1366
|
+
return false if @last_host_kind == :quote && !quote_block_follows?
|
|
1367
|
+
|
|
1368
|
+
return false unless @host_scan_valid
|
|
1369
|
+
|
|
1370
|
+
limit = @current_stream.pos
|
|
1371
|
+
while (line = @host_scan.check(/[^\n]*\n/))
|
|
1372
|
+
break if @host_scan.pos + line.bytesize > limit
|
|
1373
|
+
|
|
1374
|
+
@host_scan.scan(/[^\n]*\n/)
|
|
1375
|
+
line = line.chomp("\n")
|
|
1376
|
+
if @host_scan_fence
|
|
1377
|
+
fence = @host_scan_fence
|
|
1378
|
+
@host_scan_fence = nil if line.match?(/\A#{Regexp.escape(fence)}[#{Regexp.escape(fence[0])}]*[ \t]*\z/)
|
|
1379
|
+
elsif line.match?(/\A[ \t]*\z/)
|
|
1380
|
+
@host_scan_valid = false
|
|
1381
|
+
elsif (opening = line.match(/\A(`{3,}|~{3,})/))
|
|
1382
|
+
@host_scan_valid = false unless @host_scan_previous&.match?(/\A\+[ \t]*\z/)
|
|
1383
|
+
@host_scan_fence = opening[1]
|
|
1384
|
+
elsif line.match?(/\A(?:\#{1,6} +|::{1,}|\^ +|\||(?:[-*_][ \t]*){3,}|\[[^\]]+\]: )/)
|
|
1385
|
+
@host_scan_valid = false unless @host_scan_previous&.match?(/\A\+[ \t]*\z/)
|
|
1386
|
+
end
|
|
1387
|
+
@host_scan_previous = line
|
|
1388
|
+
return false unless @host_scan_valid
|
|
1389
|
+
end
|
|
1390
|
+
true
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
def definition_term_before?
|
|
1394
|
+
return false unless @term_stream.equal?(@current_stream) && @term_pos
|
|
1395
|
+
depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1396
|
+
return false unless depth == @term_quote_depth
|
|
1397
|
+
|
|
1398
|
+
between = @current_stream.string.byteslice(@term_pos...@current_stream.pos)
|
|
1399
|
+
lines = between.split("\n", -1)
|
|
1400
|
+
lines.shift
|
|
1401
|
+
lines.pop
|
|
1402
|
+
if depth.positive?
|
|
1403
|
+
prefix = '> ' * depth
|
|
1404
|
+
return false unless lines.all? { |line| line.empty? || line.start_with?(prefix) }
|
|
1405
|
+
|
|
1406
|
+
lines.map! { |line| line.empty? ? line : line[prefix.length..] }
|
|
1407
|
+
end
|
|
1408
|
+
# A term is one line (carve#4894): only a blank may stand between it
|
|
1409
|
+
# and its `: ` line.
|
|
1410
|
+
lines.size <= 1 && lines.all? { |line| line.match?(/\A[ \t]*\z/) }
|
|
1411
|
+
end
|
|
1412
|
+
|
|
1413
|
+
# A `:` left of the `::` column of every open term above it ends the
|
|
1414
|
+
# definition list and is paragraph text (markup-carve/carve#3458).
|
|
1415
|
+
def separator_below_term?
|
|
1416
|
+
text = @current_stream.string
|
|
1417
|
+
unless @separator_source.equal?(text)
|
|
1418
|
+
@separator_source = text
|
|
1419
|
+
@separator_bytes = text.b
|
|
1420
|
+
@separator_below = {}
|
|
1421
|
+
end
|
|
1422
|
+
start = line_start_byte
|
|
1423
|
+
depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1424
|
+
strip = lambda do |line|
|
|
1425
|
+
depth.times { line = line.start_with?('> ') ? line[2..] : line.sub(/\A>\z/, '') }
|
|
1426
|
+
line
|
|
1427
|
+
end
|
|
1428
|
+
indent = ->(line) { line[/\A[ \t]*/].gsub("\t", ' ').length }
|
|
1429
|
+
column = indent.call(strip.call(text.byteslice(start...(@separator_bytes.index("\n", start) || text.bytesize))))
|
|
1430
|
+
term = nil
|
|
1431
|
+
below = nil
|
|
1432
|
+
finish = start - 1
|
|
1433
|
+
while finish.positive?
|
|
1434
|
+
from = (@separator_bytes.rindex("\n", finish - 1) || -1) + 1
|
|
1435
|
+
line = strip.call(text.byteslice(from...finish))
|
|
1436
|
+
finish = from - 1
|
|
1437
|
+
next if line.strip.empty?
|
|
1438
|
+
|
|
1439
|
+
if line.match?(/\A[ \t]*::[ \t]/)
|
|
1440
|
+
term = [term, indent.call(line)].compact.min
|
|
1441
|
+
break if term <= column
|
|
1442
|
+
elsif line.match?(/\A[ \t]*:[ \t]/)
|
|
1443
|
+
# An earlier separator at this column with no term between reads the same.
|
|
1444
|
+
if term.nil? && indent.call(line) == column && @separator_below.key?(from)
|
|
1445
|
+
below = @separator_below[from]
|
|
1446
|
+
break
|
|
1447
|
+
end
|
|
1448
|
+
elsif term && indent.call(line) < term
|
|
1449
|
+
break
|
|
1450
|
+
end
|
|
1451
|
+
end
|
|
1452
|
+
below = !term.nil? && column < term if below.nil?
|
|
1453
|
+
@separator_below[start] = below
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
# After a definition, only a heading, quote, table or div ends the list.
|
|
1457
|
+
def definition_before?
|
|
1458
|
+
return false unless @def_stream.equal?(@current_stream) && @def_pos
|
|
1459
|
+
depth = state?(:quotestart) ? quote_depth_on_line : 0
|
|
1460
|
+
return false unless depth == @def_quote_depth
|
|
1461
|
+
|
|
1462
|
+
lines = @current_stream.string.byteslice(@def_pos...@current_stream.pos).split("\n", -1)
|
|
1463
|
+
lines.shift
|
|
1464
|
+
lines.pop
|
|
1465
|
+
prefix = '> ' * depth
|
|
1466
|
+
blank = false
|
|
1467
|
+
lines.all? do |line|
|
|
1468
|
+
if line.empty? || line == prefix.rstrip
|
|
1469
|
+
blank = true
|
|
1470
|
+
next true
|
|
1471
|
+
end
|
|
1472
|
+
next false unless line.start_with?(prefix)
|
|
1473
|
+
|
|
1474
|
+
rest = line[prefix.length..]
|
|
1475
|
+
# a flush-left block after a blank line, or one that interrupts, ends it
|
|
1476
|
+
next false if blank && rest.match?(/\A[^ \t]/)
|
|
1477
|
+
|
|
1478
|
+
blank = false
|
|
1479
|
+
!rest.match?(/\A(?:\#{1,6} +|:{3,}|> |\||::|`{3,}|~{3,}|(?:[-*_][ \t]*){3,}\z)/)
|
|
1480
|
+
end
|
|
1481
|
+
end
|
|
1482
|
+
|
|
1483
|
+
def quote_block_follows?
|
|
1484
|
+
line = next_line_after_current
|
|
1485
|
+
return false unless line
|
|
1486
|
+
return false if line.empty? || line.match?(/\A[ \t]/)
|
|
1487
|
+
|
|
1488
|
+
while line.start_with?('> ')
|
|
1489
|
+
line = line[2..]
|
|
1490
|
+
return false if line.empty? || line.match?(/\A[ \t]/)
|
|
1491
|
+
end
|
|
1492
|
+
line != '>'
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1094
1495
|
def continue_paragraph(name)
|
|
1095
1496
|
@lone_image = false
|
|
1096
1497
|
goto name
|
|
1097
1498
|
end
|
|
1098
1499
|
|
|
1099
|
-
#
|
|
1100
|
-
#
|
|
1101
|
-
def
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1500
|
+
# The tokens of a cell marker and its braced alignment block, from a
|
|
1501
|
+
# match whose last five groups are the marker and CELL_BRACE.
|
|
1502
|
+
def braced_cell(m)
|
|
1503
|
+
pipe, open, align, rest, close = m.captures.last(5)
|
|
1504
|
+
token pipe == '|' ? Punctuation : Operator, pipe
|
|
1505
|
+
token Name::Attribute, open
|
|
1506
|
+
token Operator, align
|
|
1507
|
+
token Name::Attribute, rest + close
|
|
1508
|
+
end
|
|
1509
|
+
|
|
1510
|
+
def start_paragraph
|
|
1511
|
+
@lone_image = !@current_stream.check(IMAGE_LINE).nil?
|
|
1512
|
+
@indented = @current_stream.check(/[ \t]/) ? true : false
|
|
1513
|
+
push :paragraph
|
|
1106
1514
|
end
|
|
1107
1515
|
|
|
1108
1516
|
# Lex the tail of a fence or container opener line without leaving the
|
|
@@ -1199,6 +1607,9 @@ module Rouge
|
|
|
1199
1607
|
def fence_lexer(language)
|
|
1200
1608
|
return nil if language.nil? || language.empty?
|
|
1201
1609
|
|
|
1610
|
+
return CsvTable.new if language == 'csv'
|
|
1611
|
+
return TsvTable.new if language == 'tsv'
|
|
1612
|
+
|
|
1202
1613
|
Lexer.find_fancy(language)
|
|
1203
1614
|
rescue StandardError
|
|
1204
1615
|
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.5
|
|
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-
|
|
11
|
+
date: 2026-11-12 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
|