@knowvah/dot-engine 2.6.4 → 2.6.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.
Files changed (88) hide show
  1. package/dist/common/arrows-shapes-poly.d.ts.map +1 -1
  2. package/dist/common/bezier.d.ts +12 -0
  3. package/dist/common/bezier.d.ts.map +1 -0
  4. package/dist/common/htmltable-attr-warn.d.ts +41 -0
  5. package/dist/common/htmltable-attr-warn.d.ts.map +1 -0
  6. package/dist/common/htmltable-fontinfo.d.ts +42 -0
  7. package/dist/common/htmltable-fontinfo.d.ts.map +1 -0
  8. package/dist/common/htmltable-lex-expat.d.ts +63 -0
  9. package/dist/common/htmltable-lex-expat.d.ts.map +1 -0
  10. package/dist/common/htmltable-lex-units.d.ts +32 -0
  11. package/dist/common/htmltable-lex-units.d.ts.map +1 -0
  12. package/dist/common/htmltable-lex.d.ts +10 -2
  13. package/dist/common/htmltable-lex.d.ts.map +1 -1
  14. package/dist/common/htmltable-parse-attrs.d.ts +7 -5
  15. package/dist/common/htmltable-parse-attrs.d.ts.map +1 -1
  16. package/dist/common/htmltable-parse-font.d.ts +33 -0
  17. package/dist/common/htmltable-parse-font.d.ts.map +1 -0
  18. package/dist/common/htmltable-parse-state.d.ts +39 -0
  19. package/dist/common/htmltable-parse-state.d.ts.map +1 -0
  20. package/dist/common/htmltable-parse.d.ts +18 -23
  21. package/dist/common/htmltable-parse.d.ts.map +1 -1
  22. package/dist/common/htmltable-pos.d.ts.map +1 -1
  23. package/dist/common/htmltable-types.d.ts +33 -3
  24. package/dist/common/htmltable-types.d.ts.map +1 -1
  25. package/dist/common/htmltable.d.ts +3 -2
  26. package/dist/common/htmltable.d.ts.map +1 -1
  27. package/dist/common/spline-midpoint.d.ts.map +1 -1
  28. package/dist/common/splines-geom.d.ts +5 -6
  29. package/dist/common/splines-geom.d.ts.map +1 -1
  30. package/dist/common/splines-routespl.d.ts.map +1 -1
  31. package/dist/common/taper.d.ts.map +1 -1
  32. package/dist/gvc/anchor.d.ts.map +1 -1
  33. package/dist/index.js +1362 -647
  34. package/dist/index.js.map +4 -4
  35. package/dist/layout/dot/compound-clip.d.ts +5 -5
  36. package/dist/layout/dot/compound-clip.d.ts.map +1 -1
  37. package/dist/layout/dot/compound-geom.d.ts +2 -7
  38. package/dist/layout/dot/compound-geom.d.ts.map +1 -1
  39. package/dist/layout/dot/compound.d.ts +1 -1
  40. package/dist/layout/dot/compound.d.ts.map +1 -1
  41. package/dist/layout/dot/edge-route-clip.d.ts +1 -1
  42. package/dist/layout/dot/edge-route-clip.d.ts.map +1 -1
  43. package/dist/layout/dot/ortho-adapter.d.ts +13 -4
  44. package/dist/layout/dot/ortho-adapter.d.ts.map +1 -1
  45. package/dist/layout/dot/splines-label.d.ts.map +1 -1
  46. package/dist/layout/neato/bfs.d.ts +10 -3
  47. package/dist/layout/neato/bfs.d.ts.map +1 -1
  48. package/dist/layout/neato/init.d.ts +6 -11
  49. package/dist/layout/neato/init.d.ts.map +1 -1
  50. package/dist/layout/neato/splines.d.ts.map +1 -1
  51. package/dist/parser/index.d.ts.map +1 -1
  52. package/dist/parser/scan-token.d.ts +40 -0
  53. package/dist/parser/scan-token.d.ts.map +1 -0
  54. package/dist/render/svg-edge-split.d.ts.map +1 -1
  55. package/dist/render.js +1384 -669
  56. package/dist/render.js.map +4 -4
  57. package/package.json +1 -1
  58. package/src/common/arrows-shapes-poly.ts +9 -10
  59. package/src/common/bezier.ts +33 -0
  60. package/src/common/htmltable-attr-warn.ts +270 -0
  61. package/src/common/htmltable-fontinfo.ts +83 -0
  62. package/src/common/htmltable-lex-expat.ts +357 -0
  63. package/src/common/htmltable-lex-units.ts +75 -0
  64. package/src/common/htmltable-lex.ts +221 -160
  65. package/src/common/htmltable-parse-attrs.ts +10 -14
  66. package/src/common/htmltable-parse-font.ts +92 -0
  67. package/src/common/htmltable-parse-state.ts +64 -0
  68. package/src/common/htmltable-parse.ts +138 -98
  69. package/src/common/htmltable-pos.ts +9 -8
  70. package/src/common/htmltable-types.ts +35 -3
  71. package/src/common/htmltable.ts +14 -7
  72. package/src/common/spline-midpoint.ts +2 -22
  73. package/src/common/splines-geom.ts +5 -25
  74. package/src/common/splines-routespl.ts +21 -13
  75. package/src/common/taper.ts +2 -14
  76. package/src/gvc/anchor.ts +5 -14
  77. package/src/layout/dot/compound-clip.ts +9 -19
  78. package/src/layout/dot/compound-geom.ts +6 -23
  79. package/src/layout/dot/compound.ts +1 -1
  80. package/src/layout/dot/edge-route-clip.ts +6 -29
  81. package/src/layout/dot/ortho-adapter.ts +5 -5
  82. package/src/layout/dot/splines-label.ts +3 -29
  83. package/src/layout/neato/bfs.ts +13 -4
  84. package/src/layout/neato/init.ts +15 -15
  85. package/src/layout/neato/splines.ts +4 -8
  86. package/src/parser/index.ts +13 -1
  87. package/src/parser/scan-token.ts +289 -0
  88. package/src/render/svg-edge-split.ts +5 -13
@@ -0,0 +1,357 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * The slice of libexpat the html lexer's diagnostics depend on. C hands expat
4
+ * one lexer unit per `XML_Parse` call, and expat does not always act on a
5
+ * unit at once: a token that is still incomplete (an unterminated comment or
6
+ * attribute quote, a tag cut off by the end of the label) stays in expat's
7
+ * buffer, and since expat 2.6 it is not re-read until the buffered bytes have
8
+ * doubled (the quadratic-reparse guard). Where an error surfaces, and which
9
+ * events a single call delivers, follow from that, so this models the buffer
10
+ * and the token grammar well enough to report both. The deferral rule and
11
+ * buffer growth are taken from expat's xmlparse.c (`callProcessor`,
12
+ * `XML_GetBuffer`) and checked against native dot 16.1.0's output.
13
+ *
14
+ * @see lib/common/htmllex.c:htmllex
15
+ */
16
+
17
+ import { HTML_ENTITIES } from './html-entities.js';
18
+
19
+ /** expat `XML_ErrorString(XML_ERROR_INVALID_TOKEN)`. */
20
+ export const XML_ERROR_INVALID_TOKEN = 'not well-formed (invalid token)';
21
+
22
+ /** xmlparse.c `XML_CONTEXT_BYTES`: input kept behind the read pointer. */
23
+ const XML_CONTEXT_BYTES = 1024;
24
+ /** xmlparse.c `INIT_BUFFER_SIZE`. */
25
+ const INIT_BUFFER_SIZE = 1024;
26
+
27
+ /** One complete expat token of the content: its extent in the label source. */
28
+ export interface ExpatToken {
29
+ kind: 'text' | 'tag' | 'comment' | 'pi';
30
+ start: number;
31
+ end: number;
32
+ /** Offset of the second occurrence of an attribute name in a start tag. */
33
+ dup?: number;
34
+ }
35
+
36
+ /** The first well-formedness error of a parse call. */
37
+ export interface ExpatInvalid {
38
+ message: string;
39
+ offset: number;
40
+ }
41
+
42
+ /** What one `XML_Parse` call did: the tokens it completed, then maybe an error. */
43
+ export interface ExpatResult {
44
+ tokens: ExpatToken[];
45
+ invalid?: ExpatInvalid;
46
+ }
47
+
48
+ type Scan =
49
+ | { kind: 'complete'; end: number; dup?: number }
50
+ | { kind: 'partial' }
51
+ | { kind: 'invalid'; offset: number };
52
+
53
+ const COMPLETE_END = (end: number, dup?: number): Scan =>
54
+ dup === undefined ? { kind: 'complete', end } : { kind: 'complete', end, dup };
55
+ const PARTIAL: Scan = { kind: 'partial' };
56
+ const INVALID = (offset: number): Scan => ({ kind: 'invalid', offset });
57
+
58
+ /** XML names: a start character, then name characters (non-ASCII counts as either). */
59
+ const XML_NAME = /^[A-Za-z_:\u0080-\uFFFF][\w.:\u0080-\uFFFF-]*/;
60
+ const isSpace = (c: string | undefined): boolean => c !== undefined && /\s/.test(c);
61
+
62
+ function skipSpace(text: string, i: number): number {
63
+ let j = i;
64
+ while (isSpace(text[j])) j++;
65
+ return j;
66
+ }
67
+
68
+ /** UTF-8 length: XML_Parse takes bytes. */
69
+ const byteLength = (s: string): number => new TextEncoder().encode(s).length;
70
+
71
+ /**
72
+ * The number of bytes C passes to `XML_Parse` for a text unit: the buffer
73
+ * `findNext` filled (named entities rewritten as `&#N;`, see `scanEntity`)
74
+ * with a trailing `]` escaped (`protect_rsqb`). Other units go as written.
75
+ * @see lib/common/htmllex.c:protect_rsqb
76
+ */
77
+ export function fedBytes(unit: string, isText: boolean): number {
78
+ if (!isText) return byteLength(unit);
79
+ let text = unit.replace(/&([^;&<]*);/g, (m, name: string) => {
80
+ return name.length >= 2 && Object.hasOwn(HTML_ENTITIES, name) ? `&#${HTML_ENTITIES[name]};` : m;
81
+ });
82
+ if (text.endsWith(']')) text = `${text.slice(0, -1)}&#93;`;
83
+ return byteLength(text);
84
+ }
85
+
86
+ interface Pair {
87
+ name: string;
88
+ next: number;
89
+ }
90
+
91
+ /** The quoted value opening at `j`: the index after it and any white space, or how scanning stops. */
92
+ function scanValue(text: string, j: number): number | Scan {
93
+ const q = text[j];
94
+ if (q !== '"' && q !== "'") return INVALID(j);
95
+ const close = text.indexOf(q, j + 1);
96
+ if (close < 0) {
97
+ const lt = text.indexOf('<', j + 1);
98
+ return lt >= 0 ? INVALID(lt) : PARTIAL;
99
+ }
100
+ const next = skipSpace(text, close + 1);
101
+ // Attributes are separated by white space.
102
+ if (next === close + 1 && !'>/'.includes(text[next] ?? '>')) return INVALID(next);
103
+ return next;
104
+ }
105
+
106
+ /** The `name = "value"` pair at `i`, or how expat's tokenizer stops there. */
107
+ function scanPair(text: string, i: number): Pair | Scan {
108
+ const name = XML_NAME.exec(text.slice(i))?.[0];
109
+ if (name === undefined) return INVALID(i);
110
+ const eq = skipSpace(text, i + name.length);
111
+ if (eq >= text.length) return PARTIAL;
112
+ if (text[eq] !== '=') return INVALID(eq);
113
+ const j = skipSpace(text, eq + 1);
114
+ if (j >= text.length) return PARTIAL;
115
+ const next = scanValue(text, j);
116
+ return typeof next === 'number' ? { name, next } : next;
117
+ }
118
+
119
+ /** `>` or `/>` at `i` ends the tag; end of data leaves it partial; otherwise more attributes. */
120
+ function tagEdge(text: string, i: number): Scan | undefined {
121
+ const c = text[i];
122
+ if (c === undefined) return PARTIAL;
123
+ if (c === '>') return COMPLETE_END(i + 1);
124
+ if (c !== '/') return undefined;
125
+ if (i + 1 >= text.length) return PARTIAL;
126
+ return text[i + 1] === '>' ? COMPLETE_END(i + 2) : undefined;
127
+ }
128
+
129
+ /** The pairs of a start tag from `from`; a duplicate only counts once the tag tokenizes whole. */
130
+ function scanAttributes(text: string, from: number): Scan {
131
+ const seen = new Set<string>();
132
+ let dup: number | undefined;
133
+ let i = skipSpace(text, from);
134
+ for (;;) {
135
+ const edge = tagEdge(text, i);
136
+ if (edge?.kind === 'complete') return COMPLETE_END(edge.end, dup);
137
+ if (edge !== undefined) return edge;
138
+ const pair = scanPair(text, i);
139
+ if ('kind' in pair) return pair;
140
+ if (seen.has(pair.name)) dup ??= i;
141
+ seen.add(pair.name);
142
+ i = pair.next;
143
+ }
144
+ }
145
+
146
+ /** The name at `at` and the index after it, or the scan that stops there. */
147
+ function scanName(text: string, at: number): { next: number } | Scan {
148
+ const name = XML_NAME.exec(text.slice(at))?.[0];
149
+ if (name === undefined) return at >= text.length ? PARTIAL : INVALID(at);
150
+ return { next: at + name.length };
151
+ }
152
+
153
+ /** A start tag at `p`: a name, then white space, `>` or `/>`, then the attributes. */
154
+ function scanStartTag(text: string, p: number): Scan {
155
+ const name = scanName(text, p + 1);
156
+ if ('kind' in name) return name;
157
+ const c = text[name.next];
158
+ if (c === undefined) return PARTIAL;
159
+ if (!isSpace(c) && c !== '>' && c !== '/') return INVALID(name.next);
160
+ return scanAttributes(text, name.next);
161
+ }
162
+
163
+ /** An end tag at `p`: a name, optional white space, `>`. */
164
+ function scanEndTag(text: string, p: number): Scan {
165
+ const name = scanName(text, p + 2);
166
+ if ('kind' in name) return name;
167
+ const i = skipSpace(text, name.next);
168
+ if (i >= text.length) return PARTIAL;
169
+ return text[i] === '>' ? COMPLETE_END(i + 1) : INVALID(i);
170
+ }
171
+
172
+ /** A processing instruction at `p`, up to `?>`. */
173
+ function scanPi(text: string, p: number): Scan {
174
+ const e = text.indexOf('?>', p + 2);
175
+ return e < 0 ? PARTIAL : COMPLETE_END(e + 2);
176
+ }
177
+
178
+ /** `<!...>` other than a comment: CDATA stays with the tag path, any other declaration is invalid here. */
179
+ function scanDeclaration(text: string, p: number): Scan {
180
+ const cdata = '<![CDATA['.startsWith(text.slice(p, p + 9));
181
+ if (!cdata && text.length - p >= 3) return INVALID(p + 2);
182
+ for (let i = p + 1; i < text.length; i++) {
183
+ if (text[i] === '>') return COMPLETE_END(i + 1);
184
+ if (text[i] === '<') return INVALID(i);
185
+ }
186
+ return PARTIAL;
187
+ }
188
+
189
+ /**
190
+ * A comment at `p` (`<!--` already seen): `--` must be followed by `>`.
191
+ * @see expat xmltok_impl.c:PREFIX(scanComment)
192
+ */
193
+ function scanComment(text: string, p: number): Scan {
194
+ for (let i = p + 4; i < text.length; i++) {
195
+ if (text[i] !== '-') continue;
196
+ if (i + 1 >= text.length) return PARTIAL;
197
+ if (text[i + 1] !== '-') continue;
198
+ if (i + 2 >= text.length) return PARTIAL;
199
+ return text[i + 2] === '>' ? COMPLETE_END(i + 3) : INVALID(i + 2);
200
+ }
201
+ return PARTIAL;
202
+ }
203
+
204
+ /** The markup token at `p`, which holds a `<`. */
205
+ function scanMarkup(text: string, p: number): { scan: Scan; kind: 'tag' | 'comment' | 'pi' } {
206
+ const rest = text.slice(p, p + 4);
207
+ if (rest === '<!--') return { scan: scanComment(text, p), kind: 'comment' };
208
+ if (text.length - p < 4 && '<!--'.startsWith(rest)) return { scan: PARTIAL, kind: 'comment' };
209
+ if (rest.startsWith('<!-')) return { scan: INVALID(p + 3), kind: 'comment' };
210
+ const c = text[p + 1];
211
+ if (c === '/') return { scan: scanEndTag(text, p), kind: 'tag' };
212
+ if (c === '?') return { scan: scanPi(text, p), kind: 'pi' };
213
+ if (c === '!') return { scan: scanDeclaration(text, p), kind: 'tag' };
214
+ return { scan: scanStartTag(text, p), kind: 'tag' };
215
+ }
216
+
217
+ interface ContentScan {
218
+ tokens: ExpatToken[];
219
+ /** Offset where the unconsumed (partial) token starts. */
220
+ consumed: number;
221
+ invalid?: number;
222
+ }
223
+
224
+ /** Tokenize `text` (a label slice at `base`) as far as expat can: tokens, then partial or invalid. */
225
+ function scanContent(text: string, base: number): ContentScan {
226
+ const tokens: ExpatToken[] = [];
227
+ let p = 0;
228
+ while (p < text.length) {
229
+ if (text[p] !== '<') {
230
+ const e = text.indexOf('<', p);
231
+ const end = e < 0 ? text.length : e;
232
+ tokens.push({ kind: 'text', start: base + p, end: base + end });
233
+ p = end;
234
+ continue;
235
+ }
236
+ const { scan, kind } = scanMarkup(text, p);
237
+ if (scan.kind === 'invalid') return { tokens, consumed: p, invalid: base + scan.offset };
238
+ if (scan.kind === 'partial') return { tokens, consumed: p };
239
+ const tok: ExpatToken = { kind, start: base + p, end: base + scan.end };
240
+ if (scan.dup !== undefined) tok.dup = base + scan.dup;
241
+ tokens.push(tok);
242
+ p = scan.end;
243
+ }
244
+ return { tokens, consumed: p };
245
+ }
246
+
247
+ /**
248
+ * expat's input buffer: `XML_GetBuffer` sizing and the read pointer, kept only
249
+ * as far as `callProcessor`'s reparse deferral depends on them.
250
+ */
251
+ class InputBuffer {
252
+ private size = 0;
253
+ private ptr = 0;
254
+ private end = 0;
255
+ private lastRequest = 0;
256
+ /** Bytes the last attempt could not consume; 0 once something was. */
257
+ hadBefore = 0;
258
+
259
+ /** `XML_GetBuffer(len)` then the append of `len` bytes. */
260
+ append(len: number): void {
261
+ this.lastRequest = len;
262
+ if (len > this.size - this.end) {
263
+ const keep = Math.min(this.ptr, XML_CONTEXT_BYTES);
264
+ const needed = len + (this.end - this.ptr) + keep;
265
+ if (needed > this.size) {
266
+ let grown = this.size;
267
+ if (grown === 0) grown = INIT_BUFFER_SIZE;
268
+ do grown *= 2; while (grown < needed);
269
+ this.size = grown;
270
+ }
271
+ this.end = this.end - this.ptr + keep;
272
+ this.ptr = keep;
273
+ }
274
+ this.end += len;
275
+ }
276
+
277
+ /** Bytes between the read pointer and the end of the data. */
278
+ get have(): number {
279
+ return this.end - this.ptr;
280
+ }
281
+
282
+ /** `callProcessor`: is it worth re-reading the partial token now? */
283
+ get enough(): boolean {
284
+ const available = this.ptr - Math.min(this.ptr, XML_CONTEXT_BYTES) + (this.size - this.end);
285
+ return this.have >= 2 * this.hadBefore || this.lastRequest > available;
286
+ }
287
+
288
+ /** After an attempt that left `remaining` bytes unconsumed. */
289
+ consumedTo(remaining: number, madeProgress: boolean): void {
290
+ this.hadBefore = madeProgress ? 0 : this.have;
291
+ this.ptr = this.end - remaining;
292
+ }
293
+ }
294
+
295
+ /** The parser state across `XML_Parse` calls on one label. */
296
+ export class ExpatFeed {
297
+ private readonly buffer = new InputBuffer();
298
+ private heldStart = 0;
299
+ private fedEnd = 0;
300
+
301
+ constructor(private readonly src: string) {}
302
+
303
+ /** Start of the token expat still holds, or undefined when it holds nothing. */
304
+ get held(): number | undefined {
305
+ return this.heldStart < this.fedEnd ? this.heldStart : undefined;
306
+ }
307
+
308
+ /** One `XML_Parse(parser, src[start, end), nbytes, 0)`. */
309
+ feed(start: number, end: number, nbytes: number): ExpatResult {
310
+ this.buffer.append(nbytes);
311
+ this.fedEnd = end;
312
+ if (!this.buffer.enough) return { tokens: [] };
313
+ const text = this.src.slice(this.heldStart, end);
314
+ const scan = scanContent(text, this.heldStart);
315
+ if (scan.invalid !== undefined) {
316
+ return { tokens: scan.tokens, invalid: { message: XML_ERROR_INVALID_TOKEN, offset: scan.invalid } };
317
+ }
318
+ this.heldStart += scan.consumed;
319
+ this.buffer.consumedTo(byteLength(this.src.slice(this.heldStart, end)), scan.consumed > 0);
320
+ return { tokens: scan.tokens };
321
+ }
322
+ }
323
+
324
+ /** Entities expat knows without a DTD; `scanEntity` leaves them as written. */
325
+ const XML_PREDEFINED_ENTITIES = new Set(['lt', 'gt', 'amp', 'quot', 'apos']);
326
+
327
+ const ENTITY_NAME = /^[A-Za-z_:][\w.:-]*$/;
328
+
329
+ /** Offset of the first `&name;` that scanEntity leaves for expat to reject. */
330
+ export function findUndefinedEntity(text: string): number {
331
+ for (let t = text.indexOf('&'); t >= 0; t = text.indexOf('&', t + 1)) {
332
+ if (text[t + 1] === '#') continue;
333
+ const semi = text.indexOf(';', t + 1);
334
+ if (semi < 0) continue;
335
+ const key = text.slice(t + 1, semi);
336
+ if (ENTITY_NAME.test(key) && !Object.hasOwn(HTML_ENTITIES, key) && !XML_PREDEFINED_ENTITIES.has(key)) {
337
+ return t;
338
+ }
339
+ }
340
+ return -1;
341
+ }
342
+
343
+ export interface TagChunk {
344
+ close: boolean;
345
+ selfClose: boolean;
346
+ name: string;
347
+ }
348
+
349
+ /** Element name of a complete `<..>` chunk; undefined for comments etc. */
350
+ export function parseTagChunk(chunk: string): TagChunk | undefined {
351
+ if (!chunk.endsWith('>')) return undefined;
352
+ const body = chunk.slice(1, -1);
353
+ if (body.startsWith('!')) return undefined;
354
+ const close = body.startsWith('/');
355
+ const name = /^[^\s/>]*/.exec(close ? body.slice(1) : body)?.[0] ?? '';
356
+ return { close, selfClose: !close && body.endsWith('/'), name };
357
+ }
@@ -0,0 +1,75 @@
1
+ // SPDX-License-Identifier: EPL-2.0
2
+ /**
3
+ * The unit scanner of the html lexer: how C's `findNext` cuts the label string
4
+ * into the chunks it hands to expat one at a time, and the two warnings it
5
+ * prints on the way. Positions are indices into the label string; its end
6
+ * stands for C's terminating NUL.
7
+ *
8
+ * @see lib/common/htmllex.c:eatComment
9
+ * @see lib/common/htmllex.c:findNext
10
+ */
11
+
12
+ const UNCLOSED_COMMENT = 'Warning: Unclosed comment\n';
13
+ const LABEL_CLOSED = 'Warning: Label closed before end of HTML element\n';
14
+
15
+ /** One lexer unit: it ends before `end`, and `warnings`/`warn` are what C did finding it. */
16
+ export interface Unit {
17
+ end: number;
18
+ warnings: string[];
19
+ warn: boolean;
20
+ }
21
+
22
+ /**
23
+ * Given the index after `<!--`, eat characters up to the comment close,
24
+ * returning the index of the closing `>` if it exists, or the label's end
25
+ * otherwise. It relies on HTML strings having matched nested `<>`.
26
+ * @see lib/common/htmllex.c:eatComment
27
+ */
28
+ export function eatComment(label: string, p: number): { at: number; warnings: string[] } {
29
+ let depth = 1;
30
+ let s = p;
31
+ while (depth > 0 && s < label.length) {
32
+ const c = label[s++];
33
+ if (c === '<') depth++;
34
+ else if (c === '>') depth--;
35
+ }
36
+ // C's `s--` moves back to the NUL or the '>'; reading the NUL advanced past it.
37
+ if (depth === 0) s--;
38
+ const warnings: string[] = [];
39
+ if (s < label.length) {
40
+ const t = s - 2;
41
+ if (t < p || !label.startsWith('--', t)) warnings.push(UNCLOSED_COMMENT);
42
+ }
43
+ return { at: s, warnings };
44
+ }
45
+
46
+ /**
47
+ * The next XML unit starting at `s`: `<..>`, an HTML comment `<!-- ... -->`,
48
+ * or the characters up to the next `<`.
49
+ * @see lib/common/htmllex.c:findNext
50
+ */
51
+ export function findNext(label: string, s: number): Unit {
52
+ let t = s + 1;
53
+ const warnings: string[] = [];
54
+ let warn = false;
55
+ if (label[s] === '<') {
56
+ if (label.startsWith('!--', t)) {
57
+ const eaten = eatComment(label, t + 3);
58
+ t = eaten.at;
59
+ warnings.push(...eaten.warnings);
60
+ warn = eaten.warnings.length > 0;
61
+ } else {
62
+ while (t < label.length && label[t] !== '>') t++;
63
+ }
64
+ if (label[t] !== '>') {
65
+ warnings.push(LABEL_CLOSED);
66
+ warn = true;
67
+ } else {
68
+ t++;
69
+ }
70
+ } else {
71
+ t = s;
72
+ while (t < label.length && label[t] !== '<') t++;
73
+ }
74
+ return { end: t, warnings, warn };
75
+ }