@cube-drone/marquee-parser 0.3.0 → 0.4.0

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.
package/dist/inlines.js CHANGED
@@ -5,12 +5,17 @@
5
5
  //
6
6
  // Works on code points (Array.from), mirroring Rust's Vec<char>, so the two
7
7
  // implementations scan identical units.
8
+ //
9
+ // Positions (SPEC.md, "Source positions"): when a PosCtx rides along, every
10
+ // node gets a [start, end) span in the side-table - UTF-16 offsets into the
11
+ // normalized source, translated from code-point indices via `map`. The AST
12
+ // itself is untouched either way: spans live outside the wire contract.
8
13
  import { isName, nameLen, parseAttrs, parseValue, utf8Len } from "./attrs.js";
9
14
  import { MAX_TARGET_BYTES } from "./blocks.js";
10
15
  export const MAX_INLINE_DEPTH = 16;
11
16
  export const MAX_EMOJI_SLUG_BYTES = 64;
12
17
  function rootFrame() {
13
- return { openerRaw: "", name: "", attrs: {}, children: [], delims: [] };
18
+ return { openerRaw: "", name: "", attrs: {}, children: [], delims: [], openerCp: 0, openerCpEnd: 0 };
14
19
  }
15
20
  const ASCII_PUNCT = new Set("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~");
16
21
  /** Grammar whitespace is ASCII only (SPEC.md, front-door normalization):
@@ -21,13 +26,20 @@ function isWs(c) {
21
26
  function isSlugChar(c) {
22
27
  return /^[a-z0-9_+-]$/.test(c);
23
28
  }
24
- export function parseInlines(text) {
25
- return parseInlinesAt(text, 0);
29
+ export function parseInlines(text, P = null) {
30
+ return parseInlinesAt(text, 0, P);
31
+ }
32
+ /** Record a node's span from code-point range; returns the node. */
33
+ function mk(P, node, cpStart, cpEnd) {
34
+ if (P !== null) {
35
+ P.spans.set(node, { start: P.map[cpStart], end: P.map[cpEnd] });
36
+ }
37
+ return node;
26
38
  }
27
39
  /** `base` is the inline depth already spent by enclosing link text: spans,
28
40
  * delimiters, and link nesting share the one <= 16 cap (a per-construct cap
29
41
  * would let composition multiply past it). */
30
- function parseInlinesAt(text, base) {
42
+ function parseInlinesAt(text, base, P) {
31
43
  const chars = Array.from(text);
32
44
  const frames = [rootFrame()];
33
45
  let i = 0;
@@ -36,15 +48,15 @@ function parseInlinesAt(text, base) {
36
48
  if (c === "\\") {
37
49
  const n = chars[i + 1];
38
50
  if (n === "\n") {
39
- top(frames).children.push({ type: "hard_break" });
51
+ top(frames).children.push(mk(P, { type: "hard_break" }, i, i + 2));
40
52
  i += 2;
41
53
  }
42
54
  else if (n !== undefined && ASCII_PUNCT.has(n)) {
43
- pushStr(frames, n);
55
+ pushStr(frames, n, P, i, i + 2);
44
56
  i += 2;
45
57
  }
46
58
  else {
47
- pushStr(frames, "\\");
59
+ pushStr(frames, "\\", P, i, i + 1);
48
60
  i += 1;
49
61
  }
50
62
  }
@@ -52,19 +64,19 @@ function parseInlinesAt(text, base) {
52
64
  const n = runLen(chars, i, "`");
53
65
  const k = findBacktickCloser(chars, i + n, n);
54
66
  if (k !== null) {
55
- top(frames).children.push({ type: "code_span", text: chars.slice(i + n, k).join("") });
67
+ top(frames).children.push(mk(P, { type: "code_span", text: chars.slice(i + n, k).join("") }, i, k + n));
56
68
  i = k + n;
57
69
  }
58
70
  else {
59
- pushStr(frames, "`".repeat(n));
71
+ pushStr(frames, "`".repeat(n), P, i, i + n);
60
72
  i += n;
61
73
  }
62
74
  }
63
75
  else if (c === "[") {
64
- i = bracket(chars, i, false, frames, base);
76
+ i = bracket(chars, i, false, frames, base, P);
65
77
  }
66
78
  else if (c === "!" && chars[i + 1] === "[") {
67
- i = bracket(chars, i + 1, true, frames, base);
79
+ i = bracket(chars, i + 1, true, frames, base, P);
68
80
  }
69
81
  else if (c === ":") {
70
82
  let k = i + 1;
@@ -73,54 +85,54 @@ function parseInlinesAt(text, base) {
73
85
  }
74
86
  const slugLen = k - (i + 1);
75
87
  if (slugLen >= 1 && slugLen <= MAX_EMOJI_SLUG_BYTES && chars[k] === ":") {
76
- top(frames).children.push({ type: "emoji", slug: chars.slice(i + 1, k).join("") });
88
+ top(frames).children.push(mk(P, { type: "emoji", slug: chars.slice(i + 1, k).join("") }, i, k + 1));
77
89
  i = k + 1;
78
90
  }
79
91
  else {
80
- pushStr(frames, ":");
92
+ pushStr(frames, ":", P, i, i + 1);
81
93
  i += 1;
82
94
  }
83
95
  }
84
96
  else if (c === "*") {
85
97
  const n = runLen(chars, i, "*");
86
98
  if (n === 1) {
87
- i = delimiter(chars, i, n, "em", "*", frames, base);
99
+ i = delimiter(chars, i, n, "em", "*", frames, base, P);
88
100
  }
89
101
  else if (n === 2) {
90
- i = delimiter(chars, i, n, "strong", "**", frames, base);
102
+ i = delimiter(chars, i, n, "strong", "**", frames, base, P);
91
103
  }
92
104
  else {
93
- pushStr(frames, "*".repeat(n));
105
+ pushStr(frames, "*".repeat(n), P, i, i + n);
94
106
  i += n;
95
107
  }
96
108
  }
97
109
  else if (c === "~") {
98
110
  const n = runLen(chars, i, "~");
99
111
  if (n === 2) {
100
- i = delimiter(chars, i, n, "strike", "~~", frames, base);
112
+ i = delimiter(chars, i, n, "strike", "~~", frames, base, P);
101
113
  }
102
114
  else {
103
- pushStr(frames, "~".repeat(n));
115
+ pushStr(frames, "~".repeat(n), P, i, i + n);
104
116
  i += n;
105
117
  }
106
118
  }
107
119
  else {
108
- pushStr(frames, c);
120
+ pushStr(frames, c, P, i, i + 1);
109
121
  i += 1;
110
122
  }
111
123
  }
112
124
  // Container end: unclosed spans and delimiters revert to literal text.
113
125
  while (frames.length > 1) {
114
126
  const frame = frames.pop();
115
- top(frames).children.push(...revertFrame(frame));
127
+ top(frames).children.push(...revertFrame(frame, P));
116
128
  }
117
129
  const root = frames.pop();
118
- return normalize(revertDelims(root.children, root.delims));
130
+ return normalize(revertDelims(root.children, root.delims, P), P);
119
131
  }
120
132
  function top(frames) {
121
133
  return frames[frames.length - 1];
122
134
  }
123
- function pushStr(frames, s) {
135
+ function pushStr(frames, s, P, cpStart, cpEnd) {
124
136
  const frame = top(frames);
125
137
  // An open delimiter sits (invisibly, until it closes) at its recorded
126
138
  // index: text on its far side must not merge into text before it.
@@ -130,10 +142,16 @@ function pushStr(frames, s) {
130
142
  const last = children[children.length - 1];
131
143
  if (last.type === "text") {
132
144
  last.value += s;
145
+ if (P !== null) {
146
+ const span = P.spans.get(last);
147
+ if (span !== undefined) {
148
+ span.end = P.map[cpEnd];
149
+ }
150
+ }
133
151
  return;
134
152
  }
135
153
  }
136
- children.push({ type: "text", value: s });
154
+ children.push(mk(P, { type: "text", value: s }, cpStart, cpEnd));
137
155
  }
138
156
  function runLen(chars, i, c) {
139
157
  let n = 0;
@@ -165,7 +183,7 @@ function totalDepth(frames) {
165
183
  }
166
184
  return depth;
167
185
  }
168
- function delimiter(chars, i, n, kind, raw, frames, base) {
186
+ function delimiter(chars, i, n, kind, raw, frames, base, P) {
169
187
  const canClose = i > 0 && !isWs(chars[i - 1]);
170
188
  const next = chars[i + n];
171
189
  const canOpen = next !== undefined && !isWs(next);
@@ -174,27 +192,27 @@ function delimiter(chars, i, n, kind, raw, frames, base) {
174
192
  const innermost = frame.delims[frame.delims.length - 1];
175
193
  if (canClose && innermost !== undefined && innermost.kind === kind) {
176
194
  frame.delims.pop();
177
- const inner = normalize(frame.children.splice(innermost.idx));
178
- frame.children.push(kind === "em"
195
+ const inner = normalize(frame.children.splice(innermost.idx), P);
196
+ frame.children.push(mk(P, kind === "em"
179
197
  ? { type: "emphasis", children: inner }
180
198
  : kind === "strong"
181
199
  ? { type: "strong", children: inner }
182
- : { type: "strikethrough", children: inner });
200
+ : { type: "strikethrough", children: inner }, innermost.cp, i + n));
183
201
  }
184
202
  else if (canOpen && !deep) {
185
- frame.delims.push({ kind, idx: frame.children.length, raw });
203
+ frame.delims.push({ kind, idx: frame.children.length, raw, cp: i });
186
204
  }
187
205
  else {
188
- pushStr(frames, raw);
206
+ pushStr(frames, raw, P, i, i + n);
189
207
  }
190
208
  return i + n;
191
209
  }
192
210
  /** Handle a bracket construct starting at `chars[open]` (which is `[`).
193
211
  * `embed` means a `!` sits just before it. Returns the new position. */
194
- function bracket(chars, open, embed, frames, base) {
212
+ function bracket(chars, open, embed, frames, base, P) {
195
213
  const bang = embed ? open - 1 : open;
196
214
  const fallback = () => {
197
- pushStr(frames, chars[bang]);
215
+ pushStr(frames, chars[bang], P, bang, bang + 1);
198
216
  return bang + 1;
199
217
  };
200
218
  // Find the matching `]` (balanced, escape-aware).
@@ -230,9 +248,12 @@ function bracket(chars, open, embed, frames, base) {
230
248
  const lexed = lexTarget(chars, k + 2);
231
249
  if (lexed !== null) {
232
250
  const depth = base + totalDepth(frames) + 1;
233
- top(frames).children.push(embed
251
+ // The link text's own positions: the interior is a contiguous slice
252
+ // of this logical text, so its map is a slice of ours.
253
+ const subP = P === null ? null : { map: P.map.slice(open + 1, k + 1), spans: P.spans };
254
+ top(frames).children.push(mk(P, embed
234
255
  ? { type: "embed", target: lexed.target, alt: resolveEscapes(interior) }
235
- : { type: "link", target: lexed.target, children: parseInlinesAt(interior, depth) });
256
+ : { type: "link", target: lexed.target, children: parseInlinesAt(interior, depth, subP) }, bang, lexed.end));
236
257
  return lexed.end;
237
258
  }
238
259
  return fallback();
@@ -242,16 +263,16 @@ function bracket(chars, open, embed, frames, base) {
242
263
  const name = interior.slice(1);
243
264
  if (isName(name)) {
244
265
  if (embed) {
245
- pushStr(frames, "!"); // the ! belongs to links/embeds, not spans
266
+ pushStr(frames, "!", P, bang, bang + 1); // the ! belongs to links/embeds, not spans
246
267
  }
247
268
  if (frames.length > 1 && top(frames).name === name) {
248
269
  const frame = frames.pop();
249
- const children = normalize(revertDelims(frame.children, frame.delims));
250
- top(frames).children.push({ type: "span", name: frame.name, attrs: frame.attrs, children });
270
+ const children = normalize(revertDelims(frame.children, frame.delims, P), P);
271
+ top(frames).children.push(mk(P, { type: "span", name: frame.name, attrs: frame.attrs, children }, frame.openerCp, k + 1));
251
272
  }
252
273
  else {
253
274
  // Well-formed but mismatched or orphan: the characters back.
254
- pushStr(frames, `[/${name}]`);
275
+ pushStr(frames, `[/${name}]`, P, open, k + 1);
255
276
  }
256
277
  return k + 1;
257
278
  }
@@ -262,10 +283,10 @@ function bracket(chars, open, embed, frames, base) {
262
283
  const opener = parseSpanOpener(interior);
263
284
  if (opener !== null) {
264
285
  if (embed) {
265
- pushStr(frames, "!"); // the ! belongs to links/embeds, not spans
286
+ pushStr(frames, "!", P, bang, bang + 1); // the ! belongs to links/embeds, not spans
266
287
  }
267
288
  if (base + totalDepth(frames) >= MAX_INLINE_DEPTH) {
268
- pushStr(frames, chars.slice(open, k + 1).join(""));
289
+ pushStr(frames, chars.slice(open, k + 1).join(""), P, open, k + 1);
269
290
  }
270
291
  else {
271
292
  frames.push({
@@ -274,6 +295,8 @@ function bracket(chars, open, embed, frames, base) {
274
295
  attrs: opener.attrs,
275
296
  children: [],
276
297
  delims: [],
298
+ openerCp: open,
299
+ openerCpEnd: k + 1,
277
300
  });
278
301
  }
279
302
  return k + 1;
@@ -369,19 +392,22 @@ function resolveEscapes(s) {
369
392
  }
370
393
  /** An unclosed span reverts: its opener text, then its children, flattened
371
394
  * into the parent (its own unmatched delimiters reverting first). */
372
- function revertFrame(frame) {
373
- const out = [{ type: "text", value: frame.openerRaw }];
374
- out.push(...revertDelims(frame.children, frame.delims));
395
+ function revertFrame(frame, P) {
396
+ const out = [
397
+ mk(P, { type: "text", value: frame.openerRaw }, frame.openerCp, frame.openerCpEnd),
398
+ ];
399
+ out.push(...revertDelims(frame.children, frame.delims, P));
375
400
  return out;
376
401
  }
377
- function revertDelims(children, delims) {
402
+ function revertDelims(children, delims, P) {
378
403
  for (let d = delims.length - 1; d >= 0; d -= 1) {
379
- children.splice(delims[d].idx, 0, { type: "text", value: delims[d].raw });
404
+ const delim = delims[d];
405
+ children.splice(delim.idx, 0, mk(P, { type: "text", value: delim.raw }, delim.cp, delim.cp + delim.raw.length));
380
406
  }
381
407
  return children;
382
408
  }
383
409
  /** Canonical text: adjacent literals merge, empty text nodes vanish. */
384
- function normalize(children) {
410
+ function normalize(children, P) {
385
411
  const out = [];
386
412
  for (const node of children) {
387
413
  if (node.type === "text") {
@@ -391,6 +417,13 @@ function normalize(children) {
391
417
  const last = out[out.length - 1];
392
418
  if (last !== undefined && last.type === "text") {
393
419
  last.value += node.value;
420
+ if (P !== null) {
421
+ const lastSpan = P.spans.get(last);
422
+ const nodeSpan = P.spans.get(node);
423
+ if (lastSpan !== undefined && nodeSpan !== undefined) {
424
+ lastSpan.end = Math.max(lastSpan.end, nodeSpan.end);
425
+ }
426
+ }
394
427
  continue;
395
428
  }
396
429
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cube-drone/marquee-parser",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Reference parser for the Marquee markup language",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/cube-drone/marqueemarkup.git",
9
- "directory": "ts/parser"
9
+ "directory": "ts/marquee-parser"
10
10
  },
11
11
  "type": "module",
12
12
  "engines": {