rouge-ddot 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 030aed92e51b80504fd3f03b850fcb27e2d0177946f18b180259ca922c9a26c4
4
- data.tar.gz: 6b0ff837c2198a05e3b1170cef2b26177d746f3b7afe7b352b2fe523943af2af
3
+ metadata.gz: a99cbaae721a52151bcfff4eb8d656c88aade697eab78160a3841f8274c0041e
4
+ data.tar.gz: adda0083411ae5755d34944ca3a194947b95827f73af93b8c55094a7660df8cd
5
5
  SHA512:
6
- metadata.gz: c11702ea5d91f5deaadf0bdd1da69386c03d16371e942f88fdd2183228823cedb7ac7dea774a58fcd87ed9c4fcb2193fb3c01f6c7852d84dbfa9e75d7d2c552c
7
- data.tar.gz: f169ad082eb380492f30e27360b08b3e45eb70020a364320ad7fb777244793a8dc6e89a7f43fbd0e51ee7056a3d9817593796790777542a0b0bed811c5445732
6
+ metadata.gz: 285d6debe396c45d56abd71d15998bb414c877d74d6d792a6d2cd3531957bd97187cfd2e3fcde1a84aab842a3cf94a15029a66d0b4f3cab17832b80b5a3d2f47
7
+ data.tar.gz: 422c3c2721efe2b5ecb062f23cd65e7bc518f3733f2aa2f1859f3f9ea3615680610caaf967440da55f18e8803f640294678909a54378f1b5f5642b0ba461e1ac
data/CHANGELOG.md ADDED
@@ -0,0 +1,105 @@
1
+ # Changelog
2
+
3
+ ## 0.1.3
4
+
5
+ No lexer changes — token output is identical to 0.1.2.
6
+
7
+ ### Added
8
+
9
+ - **`require 'rouge-ddot'` now works.** The gem shipped only
10
+ `lib/rouge/lexers/ddot.rb`, so requiring it by gem *name* raised
11
+ `LoadError: cannot load such file -- rouge-ddot`. Since Rouge does not
12
+ auto-discover third-party lexers, and `Bundler.require` (Jekyll's
13
+ `:jekyll_plugins` group, Rails initializers, …) requires gems by name,
14
+ hosts could not load the lexer the conventional way: adding it to a
15
+ `:jekyll_plugins` group *crashed the build*. Every host therefore had to
16
+ know the internal path and write `require 'rouge/lexers/ddot'` into a plugin
17
+ file of its own. A `lib/rouge-ddot.rb` entry point now requires the lexer,
18
+ so this is enough to highlight `[source,ddot.it]` blocks in Jekyll:
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ group :jekyll_plugins do
23
+ gem 'rouge-ddot', '~> 0.1.3'
24
+ end
25
+ ```
26
+
27
+ `require 'rouge/lexers/ddot'` keeps working unchanged.
28
+
29
+ ## 0.1.2
30
+
31
+ Brings the lexer in line with the ddot.it Parse and Highlight specifications.
32
+ Conformance is asserted against all 33 cases of the cross-implementation
33
+ corpus (`../ddot.it/test-data/cases/`), with the TextMate grammar canonical.
34
+
35
+ ### Fixed
36
+
37
+ - **`FrozenError` on any block containing a non-triple line.** The file carried
38
+ `# frozen_string_literal: true`, but Rouge mutates the string handed to
39
+ `token`, so lexing a `!!block` body, an annotation line or an end marker
40
+ raised `can't modify frozen String` (Rouge 4.7). This crashed Asciidoctor
41
+ outright, making 0.1.1 unusable on real ddot.it documentation.
42
+ - **`Error` token on the blank line inside a `?end=` block.** The blank-line
43
+ rule matched zero-width, so Rouge could not advance and emitted `Error` for
44
+ the newline — an error-styled blank line in every marked block. The rule now
45
+ consumes the newline. Invisible to the corpus, whose harness drops
46
+ whitespace-only chunks; it showed only in rendered output.
47
+
48
+ ### Added
49
+
50
+ - **`!!off` / `!!on` recognised anywhere on a line**, not only alone on one, so
51
+ the commands work inside host-language comments (`<!-- ddot.it/off -->`,
52
+ `# !!off`, `// !!on`). Only the command itself is coloured; the line yields no
53
+ triple and the region starts on the next line. The command *name* is still
54
+ matched exactly, so `!!office` is not `!!off`.
55
+ - **Metadata continuation after a block** (Parse Spec *block-continuation*): a
56
+ line whose first token is `,,` continues the triple whose **object** a block
57
+ filled, and `;;` continues after a block **meta object**. Previously both read
58
+ as a fresh triple with the subject `,,` / `;;`, and the unmatched delimiter
59
+ produced an `Error` token.
60
+ - **Continuation lines that open a block** (`..knows.. !!block`), so one logical
61
+ line can carry a block subject *and* a block object.
62
+
63
+ ### Changed
64
+
65
+ - **Canonical token names** (`test-data/tokens.md`), which changes the emitted
66
+ Rouge token types:
67
+
68
+ | ddot.it token | 0.1.1 | 0.1.2 |
69
+ |-----------------|----------------------|-----------------------------|
70
+ | `doubledot` | `Operator` → *operator* | `Operator` |
71
+ | `meta-doubledot`| `Comment::Hashbang` → *meta-operator* | `Comment::Hashbang` |
72
+ | `excluded` | `Comment::Single` → *disabled* | `Comment::Single` |
73
+ | `meta-separator`| — | `Punctuation` |
74
+ | `command-param` | — | `Name::Attribute` |
75
+ | `block-end` | — | `Name::Label` |
76
+ | `verbatim` | — | `Literal::String::Backtick` |
77
+
78
+ Themes that styled `operator`, `meta-operator` or `disabled` by *name* need
79
+ updating; those styling by Rouge token class are unaffected except for the
80
+ four newly emitted classes.
81
+ - **Lexical tokens follow the spec's counting rule.** `DT2`/`DT4`/`CM2`/`SC2`
82
+ are runs of *exactly* n, expressed with lookarounds, so `...` and `......`
83
+ are text rather than operators.
84
+ - **The line-shape gate.** A line is ddot.it only when it carries a complete
85
+ operator skeleton — two `DT2`, or one `DT4` / `.. ..`. Prose with a lone `..`,
86
+ an ellipsis, `Node.js` or `U.S.A.` stays plain.
87
+ - **Commands require the slash and a name**: bare `ddot.it` is a document
88
+ marker, not a command, and `?`/`#` introduce a command's query and fragment
89
+ instead of terminating it (`!!block?end=EOS` is one command).
90
+ - **`!!block` as a field value** in the Subject, Object and MetaObject
91
+ positions, blank-line and `?end=MARKER` terminated, with the body emitted as
92
+ `verbatim`.
93
+ - **`;;` separates inline metadata pairs only.** Inside a `,,` block a metadata
94
+ object runs to the end of its line, so `;;` there is ordinary text.
95
+
96
+ ## 0.1.1
97
+
98
+ First release published via OIDC trusted publishing
99
+ (`Calpano/ddot.it-syntax-tools` → RubyGems). No functional
100
+ changes vs 0.1.0.
101
+
102
+ ## 0.1.0
103
+
104
+ Initial release. Rouge lexer for ddot.it. See
105
+ [`design.adoc`](./design.adoc).
data/README.md CHANGED
@@ -6,35 +6,53 @@ graph notation (https://ddot.it).
6
6
  ## Usage
7
7
 
8
8
  ```ruby
9
- require 'rouge'
10
- require 'rouge/lexers/ddot'
9
+ require 'rouge-ddot'
11
10
 
12
11
  Rouge::Lexers::Ddot.new.lex("Project Eagle ..started in.. 2024") do |tok, val|
13
12
  puts [tok.qualname, val].inspect
14
13
  end
15
14
  ```
16
15
 
16
+ Rouge does not auto-discover third-party lexers, so the gem has to be required
17
+ before a `ddot.it` block can be highlighted. Requiring it by name is enough
18
+ (since 0.1.3); `require 'rouge/lexers/ddot'` also works.
19
+
20
+ In **Jekyll**, put it in the `:jekyll_plugins` group and Bundler requires it at
21
+ startup — no `_plugins/` file needed:
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ group :jekyll_plugins do
26
+ gem 'rouge-ddot', '~> 0.1.3'
27
+ end
28
+ ```
29
+
17
30
  In Asciidoctor with `:source-highlighter: rouge` set, `[source,ddot]` blocks
18
- are dispatched here automatically — no extra wiring per document.
31
+ are dispatched here automatically — no extra wiring per document. The lexer
32
+ registers under the tag `ddot`, with the aliases `ddotit` and `ddot.it`.
19
33
 
20
34
  ## Token mapping
21
35
 
22
36
  The lexer emits standard Rouge tokens; they map to the canonical ddot.it
23
37
  token vocabulary in [`../../ddot.it/test-data/tokens.md`](../../ddot.it/test-data/tokens.md):
24
38
 
25
- | Rouge | Canonical |
26
- |-----------------------------|-----------------|
27
- | `Name::Class` | `subject` |
28
- | `Name::Function` | `relation` |
29
- | `Literal::String::Symbol` | `object` |
30
- | `Operator` | `operator` |
31
- | `Keyword::Pseudo` | `command` |
32
- | `Comment::Preproc` | `meta-delim` |
33
- | `Comment::Hashbang` | `meta-operator` |
34
- | `Comment::Doc` | `meta-relation` |
35
- | `Comment::Special` | `meta-object` |
36
- | `Comment::Multiline` | `meta-text` |
37
- | `Comment::Single` | `disabled` |
39
+ | Rouge | Canonical |
40
+ |-----------------------------|------------------|
41
+ | `Name::Class` | `subject` |
42
+ | `Name::Function` | `relation` |
43
+ | `Literal::String::Symbol` | `object` |
44
+ | `Operator` | `doubledot` |
45
+ | `Keyword::Pseudo` | `command` |
46
+ | `Name::Attribute` | `command-param` |
47
+ | `Name::Label` | `block-end` |
48
+ | `Comment::Preproc` | `meta-delim` |
49
+ | `Punctuation` | `meta-separator` |
50
+ | `Comment::Hashbang` | `meta-doubledot` |
51
+ | `Comment::Doc` | `meta-relation` |
52
+ | `Comment::Special` | `meta-object` |
53
+ | `Comment::Multiline` | `meta-text` |
54
+ | `Comment::Single` | `excluded` |
55
+ | `Literal::String::Backtick` | `verbatim` |
38
56
 
39
57
  ## Conformance
40
58
 
@@ -57,9 +75,11 @@ lockstep.
57
75
  ```
58
76
  rouge/
59
77
  ├── lib/
78
+ │ ├── rouge-ddot.rb # entry point: require 'rouge-ddot'
60
79
  │ └── rouge/
61
80
  │ └── lexers/
62
81
  │ └── ddot.rb # the lexer
82
+ ├── CHANGELOG.md
63
83
  ├── README.md
64
84
  └── rouge-ddot.gemspec
65
85
  ```
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
- #
3
1
  # Rouge lexer for ddot.it (https://ddot.it). Emits tokens that — after the
4
2
  # scope-name mapping in `tools/conformance-rouge.rb` — equal the canonical
5
3
  # token streams in `../../ddot.it/test-data/cases/*/expected.tokens.json`.
@@ -8,18 +6,24 @@
8
6
  # Name::Class → subject
9
7
  # Name::Function → relation
10
8
  # Literal::String::Symbol → object
11
- # Operator → operator
9
+ # Operator → doubledot
12
10
  # Keyword::Pseudo → command
11
+ # Name::Attribute → command-param
12
+ # Name::Label → block-end
13
13
  # Comment::Preproc → meta-delim
14
- # Comment::Hashbang → meta-operator
14
+ # Punctuation → meta-separator
15
+ # Comment::Hashbang → meta-doubledot
15
16
  # Comment::Doc → meta-relation
16
17
  # Comment::Special → meta-object
17
18
  # Comment::Multiline → meta-text
18
- # Comment::Single → disabled
19
+ # Comment::Single → excluded
20
+ # Literal::String::Backtick → verbatim
21
+ #
22
+ # NOT `# frozen_string_literal: true`: Rouge mutates the string handed to
23
+ # `token`, so a frozen literal raises FrozenError (observed with Rouge 4.7).
19
24
  #
20
25
  # State machine is flat by design: we use `goto` exclusively (no `push`), so
21
26
  # the stack is always one deep and `\n` simply transitions back to `:root`.
22
- # This keeps line-by-line dispatch clean — every line starts fresh.
23
27
  #
24
28
  # Rouge gotcha: `rule pat, Token do |m| ... end` IGNORES the Token arg when
25
29
  # a block is given. To both emit a token AND transition, use a block that
@@ -37,34 +41,144 @@ module Rouge
37
41
  filenames '*.ddot'
38
42
  mimetypes 'text/x-ddot'
39
43
 
40
- OFF_RE = /(?:ddot\.it\/off|!!off)/.freeze
41
- ON_RE = /(?:ddot\.it\/on|!!on)/.freeze
42
- CMD_RE = /(?:ddot\.it(?:\/[\w.-]+)?|!![\w.-]*)/.freeze
44
+ # --- lexical tokens (see the Parse Spec's "Token Sequences") ----------
45
+ # Each is a run of EXACTLY n, neither preceded nor followed by the same
46
+ # character — the lookarounds are what express that. `\.{2}` alone would
47
+ # happily match the first two dots of `...`.
48
+ DT2 = /(?<!\.)\.{2}(?!\.)/.freeze
49
+ DT4 = /(?<!\.)\.{4}(?!\.)/.freeze
50
+ QUAD = /(?<!\.)(?:\.{4}|\.{2}[ \t]+\.{2})(?!\.)/.freeze
51
+ CM2 = /(?<!,),,(?!,)/.freeze
52
+ SC2 = /(?<!;);;(?!;)/.freeze
53
+
54
+ OFF_RE = %r{(?:ddot\.it/off|!!off)}.freeze
55
+ ON_RE = %r{(?:ddot\.it/on|!!on)}.freeze
56
+ # Pins the command NAME: `!!office` must not read as `!!off`.
57
+ BOUND_SRC = /(?![^\s?#])/.freeze
58
+ # A command needs the slash and a non-empty name; bare `ddot.it` is not one.
59
+ CMD_RE = %r{(?:(?:https?://)?ddot\.it/|!!)[^\s?#]+(?:\?[^\s#]*)?(?:\#\S*)?}.freeze
60
+ BLOCK_RE = %r{(?:(?:https?://)?ddot\.it/|!!)block}.freeze
61
+
62
+ # The line-shape gate: a line is ddot.it only if it carries a complete
63
+ # operator skeleton — two DT2, or one DT4 / '.. ..'. A lone `..` in prose
64
+ # does not qualify, which is what keeps 23-not-a-triple entirely plain.
65
+ GATE = /(?=[^\n]*(?:#{DT4}|#{DT2}[^\n]*#{DT2}))/.freeze
66
+
67
+ # `!!block` filling a whole field: it must END its physical line.
68
+ BLOCK_OPENER = /#{BLOCK_RE}(?:(\?end=)(\S+))?[ \t]*(?=\n|\z)/.freeze
69
+
70
+ def reset!
71
+ super
72
+ @block_end = nil
73
+ end
74
+
75
+ # Emit a slot's text as `slot_token`, unless it is exactly a command —
76
+ # then it is a command (tokens.md: "a slot whose text is a command is
77
+ # emitted as command, not subject").
78
+ def slot(match, slot_token)
79
+ text = match[0]
80
+ token(text.match?(/\A#{CMD_RE}\z/) ? Keyword::Pseudo : slot_token, text)
81
+ end
82
+
83
+ # A `!!block` opener was matched: emit its parts, remember any ?end=
84
+ # marker, and switch to verbatim.
85
+ def open_block(match)
86
+ raw = match[0]
87
+ lead = raw[/\A[ \t]*/]
88
+ body = raw[lead.length..].rstrip
89
+ tail = raw[(lead.length + body.length)..]
90
+ token Text, lead unless lead.empty?
91
+ if match[2]
92
+ token Keyword::Pseudo, body[0...body.index('?end=')]
93
+ token Name::Attribute, match[1]
94
+ token Name::Label, match[2]
95
+ @block_end = match[2]
96
+ else
97
+ token Keyword::Pseudo, body
98
+ @block_end = nil
99
+ end
100
+ token Text, tail unless tail.nil? || tail.empty?
101
+ goto :verbatim
102
+ end
43
103
 
44
104
  # ─────────── line-dispatch root ───────────
45
105
  state :root do
46
106
  # Standalone ,, opens a free-form meta block.
47
- rule(%r/^[ \t]*,,[ \t]*(?=\n|\z)/) do |m|
107
+ rule(%r/^[ \t]*#{CM2}[ \t]*(?=\n|\z)/) do |m|
48
108
  token Comment::Preproc, m[0]
49
109
  goto :meta_block
50
110
  end
51
- # Off-marker enters disabled span.
52
- rule(%r/^[ \t]*#{OFF_RE}[ \t]*(?=\n|\z)/) do |m|
53
- token Keyword::Pseudo, m[0]
111
+ # Parse Spec "block-continuation": after a block that filled the OBJECT,
112
+ # a line whose first token is CM2 CONTINUES the logical line as metadata
113
+ # (and SC2 after a block meta object).
114
+ # Without this it lexes as a fresh triple whose subject is ",," (or, in
115
+ # this lexer, as an unmatched ",," followed by subject "since").
116
+ # Keyed on shape — a state machine over physical lines cannot see that a
117
+ # block object preceded: ",," then whitespace then "..".
118
+ rule(%r/^[ \t]*(#{CM2})(?=[ \t]+#{DT2})/) do |m|
119
+ token Comment::Preproc, m[1]
120
+ push :inline_meta
121
+ end
122
+ # ... and SC2 after a block META OBJECT (`;; ..until.. 2020`).
123
+ rule(%r/^[ \t]*(#{SC2})(?=[ \t]+#{DT2})/) do |m|
124
+ token Punctuation, m[1]
125
+ push :inline_meta
126
+ end
127
+ # Off-marker enters an excluded span.
128
+ # Recognised ANYWHERE on the line (host-language comments), but the whole
129
+ # line is consumed so only the command itself is coloured.
130
+ rule(%r/^([^\n]*?)(#{OFF_RE}#{BOUND_SRC})([^\n]*)(?=\n|\z)/) do |m|
131
+ token Text, m[1]
132
+ token Keyword::Pseudo, m[2]
133
+ token Text, m[3]
54
134
  goto :disabled
55
135
  end
136
+ # `!!block` alone on a line = a SUBJECT opener (block-as-field).
137
+ rule(%r/^[ \t]*#{BLOCK_OPENER}/) { |m| open_block(m) }
56
138
  # Empty / whitespace-only line.
57
139
  rule %r/^[ \t]*\n/, Text
58
- # Continuation line (starts with `..` or `....`).
59
- rule(/^(?=[ \t]*\.{2,4})/) { goto :after_subject }
60
- # Triple line — start with subject parsing.
61
- rule(/^/) { goto :subject }
140
+ # Gate: only lines with a full operator skeleton parse as triples.
141
+ rule(/^#{GATE}(?=[ \t]*(?:#{DT2}|#{QUAD}))/) { goto :after_subject }
142
+ rule(/^#{GATE}/) { goto :subject }
143
+ # Anything else is not a triple: consume the line, emit no token.
144
+ rule %r/[^\n]+/, Text
145
+ rule %r/\n/, Text
62
146
  end
63
147
 
64
- # ─────────── disabled span ───────────
148
+ # ─────────── verbatim (a !!block body) ───────────
149
+ state :verbatim do
150
+ # A blank line ends the unmarked form; in a marked block it is content.
151
+ # The newline MUST be consumed here: a zero-width match leaves the
152
+ # stream where it was, Rouge cannot advance, and it emits Error for the
153
+ # newline instead — visible as an error-styled blank line inside every
154
+ # `?end=` block. (The conformance harness drops whitespace-only chunks,
155
+ # so the corpus cannot catch this; it only shows in rendered output.)
156
+ rule(%r/^[ \t]*\n/) do |m|
157
+ token Text, m[0]
158
+ goto :root if @block_end.nil?
159
+ end
160
+ rule(%r/^[ \t]*\z/) do |m|
161
+ token Text, m[0]
162
+ goto :root if @block_end.nil?
163
+ end
164
+ rule(%r/^[^\n]+/) do |m|
165
+ if @block_end && m[0].strip == @block_end
166
+ token Name::Label, m[0]
167
+ @block_end = nil
168
+ goto :root
169
+ else
170
+ token Literal::String::Backtick, m[0]
171
+ end
172
+ end
173
+ rule %r/\n/, Text
174
+ end
175
+
176
+ # ─────────── excluded span ───────────
65
177
  state :disabled do
66
- rule(%r/^[ \t]*#{ON_RE}[ \t]*(?=\n|\z)/) do |m|
67
- token Keyword::Pseudo, m[0]
178
+ rule(%r/^([^\n]*?)(#{ON_RE}#{BOUND_SRC})([^\n]*)(?=\n|\z)/) do |m|
179
+ token Text, m[1]
180
+ token Keyword::Pseudo, m[2]
181
+ token Text, m[3]
68
182
  goto :root
69
183
  end
70
184
  rule %r/^[ \t]*\n/, Text
@@ -72,200 +186,211 @@ module Rouge
72
186
  rule %r/\n/, Text
73
187
  end
74
188
 
75
- # ─────────── multi-line meta block (free-form) ───────────
189
+ # ─────────── multi-line meta block (free-form or typed) ───────────
76
190
  state :meta_block do
77
- rule(%r/^[ \t]*,,[ \t]*(?=\n|\z)/) do |m|
191
+ rule(%r/^[ \t]*#{CM2}[ \t]*(?=\n|\z)/) do |m|
78
192
  token Comment::Preproc, m[0]
79
193
  goto :root
80
194
  end
81
195
  rule %r/^[ \t]*\n/, Text
82
- rule %r/[^\n]+/, Comment::Multiline
196
+ rule(/^(?=[ \t]*(?:#{DT2}|#{QUAD}))/) { goto :typed_meta_op }
197
+ rule %r/[^\n]+/, Comment::Multiline # free-form meta text
83
198
  rule %r/\n/, Text
84
199
  end
85
200
 
86
201
  # ─────────── triple line: subject phase ───────────
87
202
  state :subject do
88
203
  rule %r/[ \t]+/, Text
89
- rule(/#{CMD_RE}(?=[ \t]+\.{2,4}|[ \t]*,,|[ \t]*$|\n|\z)/) do |m|
90
- token Keyword::Pseudo, m[0]
91
- goto :after_subject
92
- end
93
- rule(/[^\s.,][^\n,]*?(?=[ \t]+\.{2,4}|[ \t]*,,|[ \t]*$|\n|\z)/) do |m|
94
- token Name::Class, m[0]
204
+ rule(/[^\s.,][^\n,]*?(?=[ \t]*(?:#{DT2}|#{QUAD})|[ \t]*#{CM2}|[ \t]*$|\n|\z)/) do |m|
205
+ slot m, Name::Class
95
206
  goto :after_subject
96
207
  end
97
- rule(%r/\n/) { token Text, "\n"; goto :root }
208
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
98
209
  end
99
210
 
100
- # ─────────── after subject (or continuation entry) ───────────
211
+ # ─────────── after subject (or continuation-line entry) ───────────
101
212
  state :after_subject do
102
213
  rule %r/[ \t]+/, Text
103
- # `.. ..` (spaced operator) emit as one operator token covering the
104
- # whole sequence, matching the canonical corpus.
105
- rule(%r/\.{2}[ \t]+\.{2}/) do |m|
106
- token Operator, m[0]
107
- goto :object
108
- end
109
- rule(%r/\.{4}/) do |m|
214
+ # QuadDot FIRST: '.. ..' is ONE operator, not two.
215
+ rule(%r/#{QUAD}/) do |m|
110
216
  token Operator, m[0]
111
217
  goto :object
112
218
  end
113
- rule(%r/\.{2}/) do |m|
219
+ rule(%r/#{DT2}/) do |m|
114
220
  token Operator, m[0]
115
221
  goto :relation
116
222
  end
117
- rule(%r/,,/) do |m|
223
+ rule(%r/#{CM2}/) do |m|
118
224
  token Comment::Preproc, m[0]
119
225
  goto :inline_meta
120
226
  end
121
- rule(%r/\n/) { token Text, "\n"; goto :root }
227
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
122
228
  end
123
229
 
124
230
  # ─────────── relation phase ───────────
125
231
  state :relation do
126
- rule(/[^\s.,][^\n.,]*?(?=[ \t]*\.{2}|[ \t]*$|\n|\z)/) do |m|
127
- token Name::Function, m[0]
232
+ rule %r/[ \t]+/, Text
233
+ rule(/[^\s.,][^\n.,]*?(?=[ \t]*#{DT2}|[ \t]*$|\n|\z)/) do |m|
234
+ slot m, Name::Function
128
235
  goto :after_relation
129
236
  end
130
- rule %r/[ \t]+/, Text
131
- rule(%r/\n/) { token Text, "\n"; goto :root }
237
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
132
238
  end
133
239
 
134
240
  state :after_relation do
135
241
  rule %r/[ \t]+/, Text
136
- rule(%r/\.{2}/) do |m|
242
+ rule(%r/#{DT2}/) do |m|
137
243
  token Operator, m[0]
138
244
  goto :object
139
245
  end
140
- rule(%r/,,/) do |m|
246
+ rule(%r/#{CM2}/) do |m|
141
247
  token Comment::Preproc, m[0]
142
248
  goto :inline_meta
143
249
  end
144
- rule(%r/\n/) { token Text, "\n"; goto :root }
250
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
145
251
  end
146
252
 
147
253
  # ─────────── object phase ───────────
254
+ # Object := TextExcept{NL,CM2} — it MAY contain `..` (20-object-dotdot),
255
+ # so it runs to end of line or to a `,,`; it does not stop at a DT2.
148
256
  state :object do
149
- rule(/[^\s,][^\n,]*?(?=[ \t]*,,|[ \t]*$|\n|\z)/) do |m|
150
- token Literal::String::Symbol, m[0]
257
+ rule %r/[ \t]+/, Text
258
+ rule(%r/#{BLOCK_OPENER}/) { |m| open_block(m) }
259
+ rule(/[^\s,][^\n,]*?(?=[ \t]*#{CM2}|[ \t]*$|\n|\z)/) do |m|
260
+ slot m, Literal::String::Symbol
151
261
  goto :after_object
152
262
  end
153
- rule %r/[ \t]+/, Text
154
- rule(%r/\n/) { token Text, "\n"; goto :root }
263
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
155
264
  end
156
265
 
157
266
  state :after_object do
158
267
  rule %r/[ \t]+/, Text
159
- # Trailing `,,` at end-of-line opens a typed multi-line meta block:
160
- # subsequent lines are `..key.. value` triples emitted as meta-*
161
- # until a closing standalone `,,`.
162
- rule(%r/,,(?=[ \t]*(?:\n|\z))/) do |m|
268
+ # Trailing `,,` at end-of-line opens a multi-line meta block.
269
+ rule(%r/#{CM2}(?=[ \t]*(?:\n|\z))/) do |m|
163
270
  token Comment::Preproc, m[0]
164
- goto :typed_meta_open
271
+ goto :meta_block_open
165
272
  end
166
- # Inline `,,` followed by content on the same line.
167
- rule(%r/,,/) do |m|
273
+ rule(%r/#{CM2}/) do |m|
168
274
  token Comment::Preproc, m[0]
169
275
  goto :inline_meta
170
276
  end
171
- rule(%r/\n/) { token Text, "\n"; goto :root }
277
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
172
278
  end
173
279
 
174
- # Right after trailing `,,` — wait for the newline, then enter
175
- # :typed_meta_block for subsequent lines.
176
- state :typed_meta_open do
280
+ state :meta_block_open do
177
281
  rule %r/[ \t]+/, Text
178
- rule(%r/\n/) { token Text, "\n"; goto :typed_meta_block }
179
- end
180
-
181
- # Typed meta block: each line is `..relation.. object`, closed by `,,`.
182
- state :typed_meta_block do
183
- rule(%r/^[ \t]*,,[ \t]*(?=\n|\z)/) do |m|
184
- token Comment::Preproc, m[0]
185
- goto :root
186
- end
187
- rule %r/^[ \t]*\n/, Text
188
- rule(/^(?=[ \t]*\.{2,4})/) { goto :typed_meta_op1 }
189
- rule %r/[^\n]+/, Comment::Multiline # free-form fallback
190
- rule %r/\n/, Text
282
+ rule(%r/\n/) { |m| token Text, m[0]; goto :meta_block }
191
283
  end
192
284
 
193
- state :typed_meta_op1 do
285
+ # ─────────── typed meta inside a ,, block ───────────
286
+ state :typed_meta_op do
194
287
  rule %r/[ \t]+/, Text
195
- rule(%r/\.{2}/) do |m|
288
+ rule(%r/#{QUAD}/) do |m| # implicit meta relation
289
+ token Comment::Hashbang, m[0]
290
+ goto :typed_meta_object
291
+ end
292
+ rule(%r/#{DT2}/) do |m|
196
293
  token Comment::Hashbang, m[0]
197
294
  goto :typed_meta_relation
198
295
  end
199
- rule(%r/\n/) { token Text, "\n"; goto :typed_meta_block }
296
+ rule(%r/\n/) { |m| token Text, m[0]; goto :meta_block }
200
297
  end
201
298
 
202
299
  state :typed_meta_relation do
203
- rule(/[^\s.,][^\n.,]*?(?=[ \t]*\.{2}|[ \t]*$|\n|\z)/) do |m|
204
- token Comment::Doc, m[0]
300
+ rule %r/[ \t]+/, Text
301
+ rule(/[^\s.,][^\n.,]*?(?=[ \t]*#{DT2}|[ \t]*$|\n|\z)/) do |m|
302
+ slot m, Comment::Doc
205
303
  goto :typed_meta_after_relation
206
304
  end
207
- rule %r/[ \t]+/, Text
208
- rule(%r/\n/) { token Text, "\n"; goto :typed_meta_block }
305
+ rule(%r/\n/) { |m| token Text, m[0]; goto :meta_block }
209
306
  end
210
307
 
211
308
  state :typed_meta_after_relation do
212
309
  rule %r/[ \t]+/, Text
213
- rule(%r/\.{2}/) do |m|
310
+ rule(%r/#{DT2}/) do |m|
214
311
  token Comment::Hashbang, m[0]
215
312
  goto :typed_meta_object
216
313
  end
217
- rule(%r/\n/) { token Text, "\n"; goto :typed_meta_block }
314
+ rule(%r/\n/) { |m| token Text, m[0]; goto :meta_block }
218
315
  end
219
316
 
317
+ # MetaObjectInBlock := TextExcept{NL} — `;;` is CONTENT here, not a
318
+ # separator (that is the inline form only). See 28-semicolon-in-meta-block.
220
319
  state :typed_meta_object do
221
- rule(/[^\s,][^\n,]*?(?=[ \t]*$|\n|\z)/) do |m|
222
- token Comment::Special, m[0]
320
+ rule %r/[ \t]+/, Text
321
+ rule(%r/#{BLOCK_OPENER}/) { |m| open_block(m) }
322
+ rule(/[^\s][^\n]*?(?=[ \t]*$|\n|\z)/) do |m|
323
+ slot m, Comment::Special
223
324
  goto :typed_meta_after_object
224
325
  end
225
- rule %r/[ \t]+/, Text
226
- rule(%r/\n/) { token Text, "\n"; goto :typed_meta_block }
326
+ rule(%r/\n/) { |m| token Text, m[0]; goto :meta_block }
227
327
  end
228
328
 
229
329
  state :typed_meta_after_object do
230
330
  rule %r/[ \t]+/, Text
231
- rule(%r/\n/) { token Text, "\n"; goto :typed_meta_block }
331
+ rule(%r/\n/) { |m| token Text, m[0]; goto :meta_block }
232
332
  end
233
333
 
234
334
  # ─────────── inline metadata (after `,,` on a triple line) ───────────
235
335
  state :inline_meta do
336
+ # A `..` / `....` right after the `,,` selects the triple form…
337
+ rule(/(?=[ \t]*(?:#{DT2}|#{QUAD}))/) { goto :inline_meta_op }
338
+ # …anything else is free-form meta text, trimmed of surrounding WS.
339
+ rule %r/[ \t]+/, Text
340
+ rule(/[^\s][^\n]*?(?=[ \t]*$|\n|\z)/) { |m| token Comment::Multiline, m[0] }
341
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
342
+ end
343
+
344
+ state :inline_meta_op do
236
345
  rule %r/[ \t]+/, Text
237
- rule(%r/\.{2}/) do |m|
346
+ rule(%r/#{QUAD}/) do |m| # implicit meta relation
347
+ token Comment::Hashbang, m[0]
348
+ goto :inline_meta_object
349
+ end
350
+ rule(%r/#{DT2}/) do |m|
238
351
  token Comment::Hashbang, m[0]
239
352
  goto :inline_meta_relation
240
353
  end
241
- rule(%r/\n/) { token Text, "\n"; goto :root }
354
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
242
355
  end
243
356
 
244
357
  state :inline_meta_relation do
245
- rule(/[^\s.,][^\n.,]*?(?=[ \t]*\.{2}|[ \t]*$|\n|\z)/) do |m|
246
- token Comment::Doc, m[0]
358
+ rule %r/[ \t]+/, Text
359
+ rule(/[^\s.,][^\n.,]*?(?=[ \t]*#{DT2}|[ \t]*$|\n|\z)/) do |m|
360
+ slot m, Comment::Doc
247
361
  goto :inline_meta_after_relation
248
362
  end
249
- rule %r/[ \t]+/, Text
250
- rule(%r/\n/) { token Text, "\n"; goto :root }
363
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
251
364
  end
252
365
 
253
366
  state :inline_meta_after_relation do
254
367
  rule %r/[ \t]+/, Text
255
- rule(%r/\.{2}/) do |m|
368
+ rule(%r/#{DT2}/) do |m|
256
369
  token Comment::Hashbang, m[0]
257
370
  goto :inline_meta_object
258
371
  end
259
- rule(%r/\n/) { token Text, "\n"; goto :root }
372
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
260
373
  end
261
374
 
375
+ # MetaObjectInline := TextExcept{NL,SC2} — stops at `;;`.
262
376
  state :inline_meta_object do
263
- rule(/[^\s,][^\n,]*?(?=[ \t]*$|\n|\z)/) do |m|
264
- token Comment::Special, m[0]
265
- goto :after_object
377
+ rule %r/[ \t]+/, Text
378
+ rule(%r/#{BLOCK_OPENER}/) { |m| open_block(m) }
379
+ rule(/[^\s;][^\n;]*?(?=[ \t]*#{SC2}|[ \t]*$|\n|\z)/) do |m|
380
+ slot m, Comment::Special
381
+ goto :inline_meta_next
266
382
  end
383
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
384
+ end
385
+
386
+ # `;;` divides several (relation, object) pairs about one triple.
387
+ state :inline_meta_next do
267
388
  rule %r/[ \t]+/, Text
268
- rule(%r/\n/) { token Text, "\n"; goto :root }
389
+ rule(%r/#{SC2}/) do |m|
390
+ token Punctuation, m[0]
391
+ goto :inline_meta_op
392
+ end
393
+ rule(%r/\n/) { |m| token Text, m[0]; goto :root }
269
394
  end
270
395
  end
271
396
  end
data/lib/rouge-ddot.rb ADDED
@@ -0,0 +1,13 @@
1
+ # Entry point for `require 'rouge-ddot'` — the path Bundler takes when the gem
2
+ # is listed in a Jekyll `:jekyll_plugins` group, or under any other
3
+ # `Bundler.require`, which requires a gem by its *name*.
4
+ #
5
+ # Rouge does not auto-discover third-party lexers: a lexer is registered only
6
+ # when its file is loaded. Up to 0.1.2 the gem shipped `lib/rouge/lexers/ddot.rb`
7
+ # alone, so `require 'rouge-ddot'` raised LoadError and every host had to know
8
+ # the internal path and write `require 'rouge/lexers/ddot'` into a plugin file
9
+ # of its own. This file makes the conventional require work.
10
+ #
11
+ # `require_relative` rather than `require` so loading does not depend on this
12
+ # gem's lib directory being on `$LOAD_PATH`. The lexer requires 'rouge' itself.
13
+ require_relative 'rouge/lexers/ddot'
data/rouge-ddot.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'rouge-ddot'
5
- s.version = '0.1.1'
5
+ s.version = '0.1.3'
6
6
  s.summary = 'Rouge lexer for ddot.it'
7
7
  s.description = 'Syntax highlighter for the ddot.it knowledge graph notation. ' \
8
8
  'Plugs into Rouge so any tool that uses Rouge for source ' \
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.files = Dir[
20
20
  'lib/**/*.rb',
21
21
  'README.md',
22
+ 'CHANGELOG.md',
22
23
  'rouge-ddot.gemspec'
23
24
  ]
24
25
  s.require_paths = ['lib']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rouge-ddot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Calpano
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-09 00:00:00.000000000 Z
11
+ date: 2026-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rouge
@@ -33,7 +33,9 @@ executables: []
33
33
  extensions: []
34
34
  extra_rdoc_files: []
35
35
  files:
36
+ - CHANGELOG.md
36
37
  - README.md
38
+ - lib/rouge-ddot.rb
37
39
  - lib/rouge/lexers/ddot.rb
38
40
  - rouge-ddot.gemspec
39
41
  homepage: https://ddot.it