@idfkit/core 0.2.0-rc.2 → 0.2.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.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The IDF character rules, in one place.
3
+ *
4
+ * The grammar is small:
5
+ * - `!` starts a comment running to the end of its line
6
+ * - `,` separates fields
7
+ * - `;` terminates a statement
8
+ * - everything else is field text
9
+ *
10
+ * There are no string literals and no escape sequences, so a comma cannot occur inside a field
11
+ * value and an `!` always starts a comment. Real files depend on both.
12
+ *
13
+ * A hand-written character walk rather than a regex. The Python library matches objects with a
14
+ * `(?:[^;!]*(?:![^\n]*\n)?)*?` inner loop; that is a nested quantifier, so it backtracks badly on
15
+ * malformed input and cannot report where the problem was. A scan is about the same amount of
16
+ * code, is linear in the input, and always knows its line number.
17
+ *
18
+ * **Why this file exists at all.** Every position this library reports depends on the syntax layer
19
+ * and the model-building read agreeing about where a field begins and ends. Two implementations of
20
+ * "step over a comment between a separator and its value" that differ by one character put a
21
+ * finding on the wrong field, and that class of bug stays invisible until a file puts a comment
22
+ * somewhere unusual. The rules used to exist twice, once inside `lex` and once inside the
23
+ * `fieldLine` helper of `idf.ts`; both are now callers of this scan (research R3).
24
+ *
25
+ * **This module is internal and stays internal.** It is not exported from the package root. The
26
+ * syntax layer, which is public, is what a consumer outside the package reaches for; keeping the
27
+ * scan itself unexported is the reason the layer ships in core rather than beside the language
28
+ * service, because a package boundary drawn around both halves would have forced this file into a
29
+ * published export purely so the other half could reach it.
30
+ *
31
+ * @internal
32
+ */
33
+ const EXCLAMATION = 0x21;
34
+ const COMMA = 0x2c;
35
+ const SEMICOLON = 0x3b;
36
+ const LINE_FEED = 0x0a;
37
+ /**
38
+ * Walk IDF text, reporting what the handler asked for.
39
+ *
40
+ * One linear pass, no allocation of its own, and it never throws: text that violates the grammar
41
+ * is reported as what it is rather than stopped at.
42
+ *
43
+ * @internal
44
+ */
45
+ export function scan(text, handler, options = {}) {
46
+ const length = text.length;
47
+ let index = options.from ?? 0;
48
+ let line = options.line ?? 1;
49
+ /** Offset of the current line's first character, which is what turns an offset into a column. */
50
+ let lineStart = index - ((options.column ?? 1) - 1);
51
+ /** Start of the current field's text run, moved past every comment that interrupts it. */
52
+ let fieldStart = index;
53
+ /** 0 for the type name, 1 for the first field after it. */
54
+ let fieldIndex = 0;
55
+ /** True once the current statement's first non-blank, non-comment character has been seen. */
56
+ let open = false;
57
+ /** First non-blank character of the current field's value, or -1 while it has none. */
58
+ let valueStart = -1;
59
+ /** One past the last non-blank character of the run `valueStart` falls in. */
60
+ let valueEnd = -1;
61
+ /** Line `valueStart` falls on. */
62
+ let valueLine = 1;
63
+ /** True once a comment has ended the run holding `valueStart`. */
64
+ let valueClosed = false;
65
+ /**
66
+ * Report the field the scan is inside, closed at `at`. False when a handler asked to stop.
67
+ *
68
+ * A field with no value text is positioned at `at`, which is the separator, the terminator, or
69
+ * the end of input that closed it. That is the offset a value would have begun at, because
70
+ * everything between the field's start and `at` was whitespace or comment.
71
+ */
72
+ const closeField = (at) => {
73
+ if (at > fieldStart && handler.fieldText?.(fieldStart, at) === false)
74
+ return false;
75
+ const empty = valueStart < 0;
76
+ const stop = handler.fieldEnd?.(fieldIndex, empty ? at : valueStart, empty ? at : valueEnd, empty ? line : valueLine) === false;
77
+ valueStart = -1;
78
+ valueEnd = -1;
79
+ valueClosed = false;
80
+ return !stop;
81
+ };
82
+ while (index < length) {
83
+ const code = text.charCodeAt(index);
84
+ if (code === EXCLAMATION) {
85
+ // Text seen before the comment still belongs to the field, and so does text after the line
86
+ // feed: the comment interrupts the field without ending it. This is what lets
87
+ // `Zone1, !- Name` work, where the comment is no part of the value and the value is no part
88
+ // of the comment.
89
+ if (index > fieldStart && handler.fieldText?.(fieldStart, index) === false)
90
+ return;
91
+ const feed = text.indexOf('\n', index);
92
+ const end = feed === -1 ? length : feed;
93
+ if (handler.comment?.(index, end) === false)
94
+ return;
95
+ if (valueStart >= 0)
96
+ valueClosed = true;
97
+ if (feed === -1) {
98
+ fieldStart = length;
99
+ break;
100
+ }
101
+ index = feed + 1;
102
+ fieldStart = index;
103
+ line += 1;
104
+ lineStart = index;
105
+ continue;
106
+ }
107
+ const blank = isSpace(code);
108
+ if (!open && !blank) {
109
+ open = true;
110
+ if (handler.statementStart?.(index, line, index - lineStart + 1) === false)
111
+ return;
112
+ }
113
+ if (code === COMMA || code === SEMICOLON) {
114
+ if (!closeField(index))
115
+ return;
116
+ if (code === COMMA) {
117
+ if (handler.separator?.(index) === false)
118
+ return;
119
+ fieldIndex += 1;
120
+ }
121
+ else {
122
+ if (handler.terminator?.(index) === false)
123
+ return;
124
+ if (handler.statementEnd?.(index + 1, false) === false)
125
+ return;
126
+ fieldIndex = 0;
127
+ open = false;
128
+ }
129
+ index += 1;
130
+ fieldStart = index;
131
+ continue;
132
+ }
133
+ if (code === LINE_FEED) {
134
+ line += 1;
135
+ lineStart = index + 1;
136
+ }
137
+ else if (!blank) {
138
+ if (valueStart < 0) {
139
+ valueStart = index;
140
+ valueEnd = index + 1;
141
+ valueLine = line;
142
+ }
143
+ else if (!valueClosed) {
144
+ valueEnd = index + 1;
145
+ }
146
+ }
147
+ index += 1;
148
+ }
149
+ // Input ran out inside a statement. Its last field is reported like any other, so a caller reads
150
+ // what was written rather than having to reconstruct it from the leftovers.
151
+ if (!open)
152
+ return;
153
+ if (!closeField(length))
154
+ return;
155
+ handler.statementEnd?.(length, true);
156
+ }
157
+ /**
158
+ * Whether a character is whitespace, by the same definition `String.prototype.trim` uses.
159
+ *
160
+ * A field's value is trimmed, so a scan that disagreed with `trim` about one character would put
161
+ * a region beside the value rather than on it. ASCII is one comparison; the rest of the set costs
162
+ * a branch nothing outside a comment ever takes.
163
+ */
164
+ function isSpace(code) {
165
+ if (code < 0x80)
166
+ return code === 0x20 || (code >= 0x09 && code <= 0x0d);
167
+ return (code === 0xa0 ||
168
+ code === 0x1680 ||
169
+ (code >= 0x2000 && code <= 0x200a) ||
170
+ code === 0x2028 ||
171
+ code === 0x2029 ||
172
+ code === 0x202f ||
173
+ code === 0x205f ||
174
+ code === 0x3000 ||
175
+ code === 0xfeff);
176
+ }
177
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/parse/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { SyntaxLayer } from './layer.js';
2
+ import type { Token } from './tokens.js';
3
+ /**
4
+ * Every character of the text, as tokens, in source order.
5
+ *
6
+ * Two things happen here that the layer deliberately does not pay to store.
7
+ *
8
+ * The first is `trivia`. Whitespace is the complement of the stored tokens, so storing it would
9
+ * roughly double the token count to hold something derivable by subtraction. This function fills
10
+ * each gap as it walks, which is what makes the layer's complete coverage observable (FR-016)
11
+ * without the layer carrying it: the sequence begins at offset 0, ends at `text.length`, and has
12
+ * no gap and no overlap anywhere between.
13
+ *
14
+ * The second is the line split. No yielded token crosses a line boundary (FR-047), while a stored
15
+ * `value` region may, because the format lets a field be written across two lines and real files
16
+ * do it. Every token encoding in use, the Language Server Protocol's included, expresses a token
17
+ * as a length on a single line, so a token spanning one cannot be encoded at all. The split adds
18
+ * tokens and moves no boundary, so the tiling holds exactly as before, and the stored region stays
19
+ * whole for everything that is not being drawn.
20
+ *
21
+ * A generator, so a consumer colouring a viewport stops where it stops rather than materialising
22
+ * four hundred thousand tokens to read the first fifty.
23
+ */
24
+ export declare function classify(layer: SyntaxLayer): Iterable<Token>;
25
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../src/syntax/classify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,KAAK,EAAE,KAAK,EAAa,MAAM,aAAa,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAiB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,CAkC7D"}
@@ -0,0 +1,59 @@
1
+ import { lineStartsOf } from './region.js';
2
+ /**
3
+ * Every character of the text, as tokens, in source order.
4
+ *
5
+ * Two things happen here that the layer deliberately does not pay to store.
6
+ *
7
+ * The first is `trivia`. Whitespace is the complement of the stored tokens, so storing it would
8
+ * roughly double the token count to hold something derivable by subtraction. This function fills
9
+ * each gap as it walks, which is what makes the layer's complete coverage observable (FR-016)
10
+ * without the layer carrying it: the sequence begins at offset 0, ends at `text.length`, and has
11
+ * no gap and no overlap anywhere between.
12
+ *
13
+ * The second is the line split. No yielded token crosses a line boundary (FR-047), while a stored
14
+ * `value` region may, because the format lets a field be written across two lines and real files
15
+ * do it. Every token encoding in use, the Language Server Protocol's included, expresses a token
16
+ * as a length on a single line, so a token spanning one cannot be encoded at all. The split adds
17
+ * tokens and moves no boundary, so the tiling holds exactly as before, and the stored region stays
18
+ * whole for everything that is not being drawn.
19
+ *
20
+ * A generator, so a consumer colouring a viewport stops where it stops rather than materialising
21
+ * four hundred thousand tokens to read the first fifty.
22
+ */
23
+ export function* classify(layer) {
24
+ const text = layer.text;
25
+ const tokens = layer.tokens;
26
+ const starts = tokens.starts;
27
+ const ends = tokens.ends;
28
+ // The layer's own line index, built once and shared with every position query made against it.
29
+ const lineStarts = lineStartsOf(layer);
30
+ /** The line the walk is on. It only ever moves forward, so the whole pass stays linear. */
31
+ let line = 0;
32
+ /** One token per line the span touches, split at each line start strictly inside it. */
33
+ function* perLine(from, to, kind) {
34
+ while (line + 1 < lineStarts.length && lineStarts[line + 1] <= from)
35
+ line += 1;
36
+ let at = from;
37
+ while (line + 1 < lineStarts.length && lineStarts[line + 1] < to) {
38
+ line += 1;
39
+ const boundary = lineStarts[line];
40
+ yield { start: at, end: boundary, kind };
41
+ at = boundary;
42
+ }
43
+ if (at < to)
44
+ yield { start: at, end: to, kind };
45
+ }
46
+ /** One past the last character yielded so far, which is where the next gap would begin. */
47
+ let covered = 0;
48
+ for (let index = 0; index < tokens.length; index += 1) {
49
+ const start = starts[index];
50
+ if (start > covered)
51
+ yield* perLine(covered, start, 'trivia');
52
+ const end = ends[index];
53
+ yield* perLine(start, end, tokens.kindAt(index));
54
+ covered = end;
55
+ }
56
+ if (covered < text.length)
57
+ yield* perLine(covered, text.length, 'trivia');
58
+ }
59
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/syntax/classify.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,SAAS,CAAC,CAAC,QAAQ,CAAC,KAAkB;IAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;IACzB,+FAA+F;IAC/F,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAEvC,2FAA2F;IAC3F,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,wFAAwF;IACxF,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAY,EAAE,EAAU,EAAE,IAAe;QACzD,OAAO,IAAI,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,IAAI,UAAU,CAAC,IAAI,GAAG,CAAC,CAAE,IAAI,IAAI;YAAE,IAAI,IAAI,CAAC,CAAC;QAChF,IAAI,EAAE,GAAG,IAAI,CAAC;QACd,OAAO,IAAI,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,IAAI,UAAU,CAAC,IAAI,GAAG,CAAC,CAAE,GAAG,EAAE,EAAE,CAAC;YAClE,IAAI,IAAI,CAAC,CAAC;YACV,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAE,CAAC;YACnC,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACzC,EAAE,GAAG,QAAQ,CAAC;QAChB,CAAC;QACD,IAAI,EAAE,GAAG,EAAE;YAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAClD,CAAC;IAED,2FAA2F;IAC3F,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAE,CAAC;QAC7B,IAAI,KAAK,GAAG,OAAO;YAAE,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAE,CAAC;QACzB,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACjD,OAAO,GAAG,GAAG,CAAC;IAChB,CAAC;IACD,IAAI,OAAO,GAAG,IAAI,CAAC,MAAM;QAAE,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,67 @@
1
+ import type { Region } from './region.js';
2
+ import { TokenStore } from './tokens.js';
3
+ /**
4
+ * One statement as the text writes it, terminated or not.
5
+ *
6
+ * Not an object in the model and carrying no schema meaning (FR-006): a statement whose type the
7
+ * schema never heard of, or which carries twice the fields its type defines, is still a statement.
8
+ * Deciding what any of it means is the reader's job, and the layer is what a reader positions
9
+ * against.
10
+ */
11
+ export interface Statement {
12
+ /** The whole statement, from its first non-blank character through its terminator. */
13
+ readonly region: Region;
14
+ /** Just the type name. Empty, at the offset one would have begun, when none was written. */
15
+ readonly typeName: Region;
16
+ /** Type name text, exactly as written and not case-folded. */
17
+ readonly typeNameText: string;
18
+ /**
19
+ * Fields after the type name, in positional order, one region each.
20
+ *
21
+ * Index 0 is the first field after the type name, matching `RawObject.values`, which has already
22
+ * had the type name shifted off. A field written empty still gets a region, an empty one
23
+ * positioned where its value would have begun, so that positional indexing never shifts and a
24
+ * blank slot in the middle of an extensible group can still be pointed at.
25
+ *
26
+ * The count is what was written rather than what the type defines. A statement carrying more or
27
+ * fewer fields than its type is a finding, not a representation problem.
28
+ */
29
+ readonly fields: readonly Region[];
30
+ /** True when no terminator was found, meaning the statement runs to end of input. */
31
+ readonly unterminated: boolean;
32
+ }
33
+ /**
34
+ * Everything the text contains and where it was.
35
+ *
36
+ * The layer holds the text it was built from, which is what makes byte-identical reconstruction a
37
+ * consequence rather than a feature: a reconstruction defined as concatenating slices of that text
38
+ * returns the text by construction. What can actually break is the tiling, so that is what the
39
+ * corpus test asserts and what {@link classify} makes observable.
40
+ *
41
+ * Nothing builds a layer implicitly (FR-005). `lex` and `parseIdf` read the same characters
42
+ * through the same scan and construct none of this, so a caller who never names `scanIdf` pays
43
+ * neither its time nor its memory.
44
+ */
45
+ export interface SyntaxLayer {
46
+ /** The text this layer was built from. Held so regions can be resolved. */
47
+ readonly text: string;
48
+ /** Statements in source order. */
49
+ readonly statements: readonly Statement[];
50
+ /** Every meaningful token in source order, packed. Whitespace is not among them. */
51
+ readonly tokens: TokenStore;
52
+ }
53
+ /**
54
+ * Scan IDF text into a syntax layer.
55
+ *
56
+ * Text and nothing else: no schema, because the layer records what the text says and never what it
57
+ * means (FR-006). It never throws, for any input, and text that violates the grammar is
58
+ * represented rather than stopped at (FR-004): an unterminated final statement runs to end of
59
+ * input and says so, a statement written without a type name still tiles, and empty text produces
60
+ * a layer with no statements and no tokens, which satisfies the tiling invariant vacuously.
61
+ *
62
+ * One linear pass over the shared scan, which is the same pass `lex` makes. The budget is a
63
+ * quarter over a plain read of the same text; the work above the scan is a push per token and a
64
+ * record per statement.
65
+ */
66
+ export declare function scanIdf(text: string): SyntaxLayer;
67
+ //# sourceMappingURL=layer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.d.ts","sourceRoot":"","sources":["../../src/syntax/layer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAkB,MAAM,aAAa,CAAC;AAEzD;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,sFAAsF;IACtF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4FAA4F;IAC5F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,qFAAqF;IACrF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kCAAkC;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;IAC1C,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAiHjD"}
@@ -0,0 +1,134 @@
1
+ import { scan } from '../parse/scan.js';
2
+ import { TokenStore } from './tokens.js';
3
+ /**
4
+ * Scan IDF text into a syntax layer.
5
+ *
6
+ * Text and nothing else: no schema, because the layer records what the text says and never what it
7
+ * means (FR-006). It never throws, for any input, and text that violates the grammar is
8
+ * represented rather than stopped at (FR-004): an unterminated final statement runs to end of
9
+ * input and says so, a statement written without a type name still tiles, and empty text produces
10
+ * a layer with no statements and no tokens, which satisfies the tiling invariant vacuously.
11
+ *
12
+ * One linear pass over the shared scan, which is the same pass `lex` makes. The budget is a
13
+ * quarter over a plain read of the same text; the work above the scan is a push per token and a
14
+ * record per statement.
15
+ */
16
+ export function scanIdf(text) {
17
+ const tokens = new TokenStore(initialCapacity(text.length));
18
+ const statements = [];
19
+ /**
20
+ * Comments seen since the last field closed, as flat `[start, end]` pairs in source order.
21
+ *
22
+ * They are buffered rather than pushed on sight because a comment can interrupt a field, in
23
+ * which case the scan reports it before the field it sits inside: the field's own bounds are
24
+ * only known once the field closes. Emitting them at that point, ordered against the value,
25
+ * is what keeps the store in source order.
26
+ */
27
+ const comments = [];
28
+ /** Index into {@link comments} of the first pair not yet pushed. */
29
+ let nextComment = 0;
30
+ /** Raw text runs of the field being read, as flat `[start, end]` pairs in source order. */
31
+ const runs = [];
32
+ /** Offset the current statement opened at. */
33
+ let openedAt = 0;
34
+ /** The current statement's type name, replaced when its field closes. */
35
+ let typeName = EMPTY_REGION;
36
+ /** The current statement's fields. Reassigned per statement, so a pushed record keeps its own. */
37
+ let fields = [];
38
+ /** Push every buffered comment that begins before `offset`, keeping the store in source order. */
39
+ const flushComments = (offset) => {
40
+ while (nextComment < comments.length && comments[nextComment] < offset) {
41
+ tokens.push(comments[nextComment], comments[nextComment + 1], 'comment');
42
+ nextComment += 2;
43
+ }
44
+ };
45
+ scan(text, {
46
+ statementStart(offset) {
47
+ openedAt = offset;
48
+ fields = [];
49
+ },
50
+ /**
51
+ * The runs are recorded only for the sake of a field written on both sides of a comment that
52
+ * interrupts it. Every other reader of this scan wants the field's value, which arrives whole
53
+ * below.
54
+ */
55
+ fieldText(start, end) {
56
+ runs.push(start, end);
57
+ },
58
+ fieldEnd(index, start, end) {
59
+ const region = { start, end };
60
+ if (index === 0)
61
+ typeName = region;
62
+ else
63
+ fields.push(region);
64
+ const kind = index === 0 ? 'typeName' : 'value';
65
+ flushComments(start);
66
+ // A field written empty has an empty region, which is a position rather than a span, so it
67
+ // is recorded above and is no token.
68
+ if (end > start)
69
+ tokens.push(start, end, kind);
70
+ // Only a comment can separate two runs of one field, so a run beginning at or after the
71
+ // value ends is text written after an interrupting comment: `A !- why\n B,` writes both `A`
72
+ // and `B` into one field. The scan bounds the value at `A` deliberately, so that a value
73
+ // region and a comment region can never overlap, which leaves `B` reached by no region at
74
+ // all, and a gap holding something other than whitespace is what the tiling invariant
75
+ // forbids. Every other run holds the value itself or the whitespace before it, and both
76
+ // begin before it ends, so a well-formed field never enters the body and never allocates.
77
+ for (let at = 0; at < runs.length; at += 2) {
78
+ const runStart = runs[at];
79
+ if (runStart < end)
80
+ continue;
81
+ const runEnd = runs[at + 1];
82
+ const raw = text.slice(runStart, runEnd);
83
+ const from = runStart + (raw.length - raw.trimStart().length);
84
+ const to = runEnd - (raw.length - raw.trimEnd().length);
85
+ if (from >= to)
86
+ continue;
87
+ flushComments(from);
88
+ tokens.push(from, to, kind);
89
+ }
90
+ flushComments(Infinity);
91
+ comments.length = 0;
92
+ nextComment = 0;
93
+ runs.length = 0;
94
+ },
95
+ separator(offset) {
96
+ tokens.push(offset, offset + 1, 'separator');
97
+ },
98
+ terminator(offset) {
99
+ tokens.push(offset, offset + 1, 'terminator');
100
+ },
101
+ comment(start, end) {
102
+ comments.push(start, end);
103
+ },
104
+ statementEnd(end, unterminated) {
105
+ statements.push({
106
+ region: { start: openedAt, end },
107
+ typeName,
108
+ typeNameText: text.slice(typeName.start, typeName.end),
109
+ fields,
110
+ unterminated,
111
+ });
112
+ },
113
+ });
114
+ // Comments after the last terminator close no field, so nothing has flushed them. Text that is
115
+ // only comments reaches here having reported no statement at all.
116
+ flushComments(Infinity);
117
+ return { text, statements, tokens };
118
+ }
119
+ /**
120
+ * Stands in until a statement's first field closes, which it always does before the statement
121
+ * ends, so no statement is ever recorded holding this.
122
+ */
123
+ const EMPTY_REGION = { start: 0, end: 0 };
124
+ /**
125
+ * A starting size for the token store, so a large file does not grow it a dozen times.
126
+ *
127
+ * A commented IDF line runs to about a dozen characters per token, so one per sixteen undershoots
128
+ * slightly and costs at most one doubling, which is the safer side to be wrong on: a file that is
129
+ * mostly whitespace or mostly comment would otherwise pay for capacity it never fills.
130
+ */
131
+ function initialCapacity(length) {
132
+ return length < 1024 ? 64 : length >> 4;
133
+ }
134
+ //# sourceMappingURL=layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.js","sourceRoot":"","sources":["../../src/syntax/layer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A span of source text, half-open.
3
+ *
4
+ * `end` is one past the last character, so `text.slice(start, end)` is exactly the text the
5
+ * region selects, an empty region is `start === end`, and two adjacent regions share a number
6
+ * rather than straddling one. An empty region is what a cursor between two characters is.
7
+ *
8
+ * Offsets are indices into the JavaScript string, which are UTF-16 code units. Nothing converts
9
+ * them, because no consumer wants them converted: the Language Server Protocol's default
10
+ * `positionEncoding` is UTF-16, Monaco measures columns in UTF-16 code units, and CodeMirror
11
+ * addresses its document by the same offsets. The Python library counts code points instead,
12
+ * because Python string indices are code points; the two agree for every character below the
13
+ * astral planes and differ for anything above them, which in practice means an emoji in a
14
+ * comment. That divergence is registered rather than papered over.
15
+ */
16
+ export interface Region {
17
+ /** Offset of the first character, into the source string. */
18
+ readonly start: number;
19
+ /** Offset one past the last character. */
20
+ readonly end: number;
21
+ }
22
+ /**
23
+ * A position as a human reads it, derived from an offset rather than stored.
24
+ *
25
+ * Storing line and column on every region would double its size to hold two numbers that are a
26
+ * function of one. Both count from one, which is what the existing findings already report; a
27
+ * consumer whose own convention counts a column from zero subtracts one, and that subtraction is
28
+ * the consumer's, because a service that guessed which convention its caller wanted would be
29
+ * modelling a protocol.
30
+ */
31
+ export interface LineColumn {
32
+ /** 1-based, counting from the start of the text. */
33
+ readonly line: number;
34
+ /** 1-based, in UTF-16 code units, from the start of the line. */
35
+ readonly column: number;
36
+ }
37
+ /**
38
+ * The minimum a position query needs: the text its offsets index into.
39
+ *
40
+ * Declared structurally rather than by importing `SyntaxLayer`, so that positions stay usable
41
+ * without one and this module depends on nothing. A `SyntaxLayer` holds its text and therefore
42
+ * satisfies this, which is what lets `lineColumnAt(layer, offset)` read as the contract writes it.
43
+ */
44
+ export interface TextSource {
45
+ /** The text offsets are measured into. */
46
+ readonly text: string;
47
+ }
48
+ /**
49
+ * The line and column an offset falls on.
50
+ *
51
+ * Binary search over the line index, so a query costs the logarithm of the line count rather than
52
+ * a scan of the text. An offset outside `[0, text.length]` is clamped into range rather than
53
+ * throwing: a cursor arrives from an editor that may be a keystroke ahead of the text this layer
54
+ * was built from, and refusing to answer is worse than answering about the nearest character.
55
+ *
56
+ * The column of a *statement* is the column of its first non-blank character, not of the
57
+ * whitespace indenting it. This function does not enforce that rule, because a statement's region
58
+ * already starts at that character; it is stated here because every column this feature reports
59
+ * obeys it, and because the conformance corpus compares findings on their line and type name and
60
+ * would notice the number moving.
61
+ */
62
+ export declare function lineColumnAt(source: TextSource, offset: number): LineColumn;
63
+ /**
64
+ * The offset a line and column names.
65
+ *
66
+ * The inverse of {@link lineColumnAt} for every position that exists, and clamped for every one
67
+ * that does not: a line past the end resolves on the last line, a column past the end of its line
68
+ * resolves at the line's last position, which is the offset of the break that ends it. Round
69
+ * tripping an out-of-range position therefore returns the clamped position rather than the one
70
+ * asked for, which is the only honest answer available.
71
+ */
72
+ export declare function offsetAt(source: TextSource, position: LineColumn): number;
73
+ //# sourceMappingURL=region.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"region.d.ts","sourceRoot":"","sources":["../../src/syntax/region.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,MAAM;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAgCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAK3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,GAAG,MAAM,CAUzE"}