Skip to content

Commit 41cc020

Browse files
committed
{feature} place number suffix parsing behind a flag
Number suffix parsing was added as an experiment, and the experimental implementation is very unlikely to resemble what will finally land in the spec. The 0.4 release is stuck on pre-releases as long as it deviates from the spec, so hiding this deviation behind a flag makes it possible to release 0.4.0 without waiting on the spec to land the number suffix PR.
1 parent 64d2d1e commit 41cc020

File tree

19 files changed

+417
-77
lines changed

19 files changed

+417
-77
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "test/upstream-v1"]
55
path = test/upstream-v1
66
url = https://github.com/kdl-org/kdl
7+
[submodule "test/upstream-suffixed-numbers"]
8+
path = test/upstream-suffixed-numbers
9+
url = https://github.com/kdl-org/kdl

documentation/src/api/parse.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,35 @@ The pride flag that consists of four code points is a single grapheme.
5252
Tracking by code points is the default for the simple reason that it seems to match how columns are tracked in editors like VS Code or Zed.
5353
There's also a 6.5x speed difference between the two methods, but even with `graphemeLocations` enabled the parser succeeds in parsing thousands of documents per second.
5454

55+
## Parser flags
56+
57+
The `parse` function accepts optional flags to define parser behaviour.
58+
There is currently a single flag:
59+
60+
- [`experimentalSuffixedNumbers`](./reference/index/index.md#experimentalsuffixednumbers): if enabled, numbers can have a suffix tag (e.g. `10px` is equivalent to `(px)10`).
61+
This suffix is used as tag for the value, which implies a number cannot have both a tag and a suffix.
62+
The following limitations apply unless a `#` is used as separator:
63+
64+
- Binary, octal, and hexadecimal numbers cannot have a suffix, only decimal numbers
65+
- Decimal numbers cannot have both an exponent and a suffix, e.g. `1e1lorem` is invalid
66+
- A suffix cannot start with a letter followed by a digit or an underscore (`_`)
67+
- A suffix cannot start with `x` or `X` followed by the letter a through f or A through F
68+
- A suffix cannot start on a dot (`.`) or comma (`,`)
69+
70+
```js
71+
import {parse} from "@bgotink/kdl";
72+
73+
assert.throws(() => parse("node 10px"));
74+
75+
assert.doesNotThrow(() =>
76+
parse("node 10px", {
77+
flags: {
78+
experimentalSuffixedNumbers: true,
79+
},
80+
}),
81+
);
82+
```
83+
5584
## Quirks
5685

5786
This package turns KDL documents into JavaScript objects and vice versa. It is therefore limited by the JavaScript language.

src/flags.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @typedef {object} ParserFlags
3+
* Flags to turn language features on or off
4+
*
5+
* @prop {boolean} experimentalSuffixedNumbers
6+
* Support suffixed numbers using a proposal that might not make it into the language
7+
*
8+
* If enabled, decimal numbers and can have a suffix.
9+
* This suffix is used as tag for the value, which implies a number cannot have both a tag and a suffix.
10+
*
11+
* The following limitations apply:
12+
* - Binary, octal, and hexadecimal numbers cannot have a suffix, only decimal numbers
13+
* - Decimal numbers cannot have both an exponent and a suffix, e.g. `1e1lorem` is invalid
14+
* - A suffix cannot start with a letter followed by a digit or an underscore (`_`)
15+
* - A suffix cannot start with `x` or `X` followed by the letter a through f or A through F
16+
* - A suffix cannot start on a dot (`.`) or comma (`,`)
17+
*
18+
* A suffix can start with a `#` character, in which case none of the limitations above apply.
19+
* The `#` itself is a separator character, and not part of the suffix itself.
20+
*/
21+
22+
/**
23+
* @param {Partial<ParserFlags>} [flags]
24+
* @returns {ParserFlags}
25+
*/
26+
export function resolveFlags({experimentalSuffixedNumbers = false} = {}) {
27+
return {experimentalSuffixedNumbers};
28+
}

src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ export {format} from "./format.js";
55
export {getLocation} from "./locations.js";
66
export {parse} from "./parse.js";
77

8+
/**
9+
* @typedef {import('./flags.js').ParserFlags} ParserFlags
10+
*/
11+
812
/**
913
* @typedef {import('./model.js').Primitive} Primitive
1014
*/

src/parse.d.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type {ParserFlags} from "./flags.js";
12
import type {Document, Entry, Identifier, Node, Value} from "./model.js";
23
import type {LineSpace, NodeSpace} from "./model/whitespace.js";
34

@@ -35,6 +36,7 @@ export function parse(
3536
as: "value";
3637
storeLocations?: boolean;
3738
graphemeLocations?: boolean;
39+
flags?: Partial<ParserFlags>;
3840
},
3941
): Value;
4042
/**
@@ -58,6 +60,7 @@ export function parse(
5860
as: "identifier";
5961
storeLocations?: boolean;
6062
graphemeLocations?: boolean;
63+
flags?: Partial<ParserFlags>;
6164
},
6265
): Identifier;
6366
/**
@@ -81,6 +84,7 @@ export function parse(
8184
as: "entry";
8285
storeLocations?: boolean;
8386
graphemeLocations?: boolean;
87+
flags?: Partial<ParserFlags>;
8488
},
8589
): Entry;
8690
/**
@@ -103,6 +107,7 @@ export function parse(
103107
as: "node";
104108
storeLocations?: boolean;
105109
graphemeLocations?: boolean;
110+
flags?: Partial<ParserFlags>;
106111
},
107112
): Node;
108113
/**
@@ -123,6 +128,7 @@ export function parse(
123128
as: "whitespace in document";
124129
storeLocations?: boolean;
125130
graphemeLocations?: boolean;
131+
flags?: Partial<ParserFlags>;
126132
},
127133
): LineSpace;
128134
/**
@@ -143,6 +149,7 @@ export function parse(
143149
as: "whitespace in node";
144150
storeLocations?: boolean;
145151
graphemeLocations?: boolean;
152+
flags?: Partial<ParserFlags>;
146153
},
147154
): NodeSpace;
148155
/**
@@ -165,6 +172,7 @@ export function parse(
165172
as?: "document";
166173
storeLocations?: boolean;
167174
graphemeLocations?: boolean;
175+
flags?: Partial<ParserFlags>;
168176
},
169177
): Document;
170178
/**
@@ -183,5 +191,10 @@ export function parse<T extends keyof ParserResult>(
183191
| Uint32Array
184192
| Int32Array
185193
| DataView,
186-
options: {as: T; storeLocations?: boolean; graphemeLocations?: boolean},
194+
options: {
195+
as: T;
196+
storeLocations?: boolean;
197+
graphemeLocations?: boolean;
198+
flags?: Partial<ParserFlags>;
199+
},
187200
): ParserResult[T];

src/parse.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {InvalidKdlError} from "./error.js";
2+
import {resolveFlags} from "./flags.js";
13
import {tokenize} from "./parser/tokenize.js";
24
import {
35
finalize,
@@ -12,7 +14,6 @@ import {
1214
parseWhitespaceInDocument,
1315
parseWhitespaceInNode,
1416
} from "./parser/parse-whitespace.js";
15-
import {InvalidKdlError} from "./error.js";
1617

1718
const methods = /** @type {const} */ ({
1819
value: parseValue,
@@ -31,8 +32,9 @@ const methods = /** @type {const} */ ({
3132
* @param {keyof typeof methods} [options.as]
3233
* @param {boolean} [options.storeLocations]
3334
* @param {boolean} [options.graphemeLocations]
35+
* @param {Partial<import('./flags.js').ParserFlags>} [options.flags]
3436
*/
35-
export function parse(text, {as = "document", ...parserOptions} = {}) {
37+
export function parse(text, {as = "document", flags, ...parserOptions} = {}) {
3638
const parserMethod = methods[as];
3739
if (parserMethod == null) {
3840
throw new TypeError(`Invalid "as" target passed: ${JSON.stringify(as)}`);
@@ -50,10 +52,15 @@ export function parse(text, {as = "document", ...parserOptions} = {}) {
5052
text = decoder.decode(text);
5153
}
5254

53-
const tokens = tokenize(text, parserOptions);
55+
const reoslvedFlags = resolveFlags(flags);
56+
57+
const tokens = tokenize(text, {...parserOptions, flags: reoslvedFlags});
5458
// console.log(Array.from(tokens));
5559

56-
const ctx = createParserCtx(text, tokens, parserOptions);
60+
const ctx = createParserCtx(text, tokens, {
61+
...parserOptions,
62+
flags: reoslvedFlags,
63+
});
5764

5865
let value;
5966
try {

src/parser/parse-whitespace.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,15 @@ export function parseWhitespaceInNode(ctx) {
227227
if (slashDash[1][0]) {
228228
result.push(
229229
...parseWhitespaceInNode(
230-
createParserCtx(slashDash[1][0], tokenize(slashDash[1][0], {})),
230+
createParserCtx(
231+
slashDash[1][0],
232+
tokenize(slashDash[1][0], {
233+
flags: ctx.flags,
234+
}),
235+
{
236+
flags: ctx.flags,
237+
},
238+
),
231239
),
232240
);
233241
}

src/parser/parse.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
* @prop {Token} lastToken
4747
* @prop {boolean} storeLocations
4848
* @prop {InvalidKdlError[]} errors
49+
* @prop {import('../flags.js').ParserFlags} flags
4950
*/
5051

5152
/** @param {ParserCtx} ctx */
@@ -103,11 +104,12 @@ export function concatenate(one = "", two = "", three = "") {
103104
/**
104105
* @param {string} text
105106
* @param {Iterable<Token>} tokens
106-
* @param {object} [options]
107+
* @param {object} options
107108
* @param {boolean} [options.storeLocations]
109+
* @param {import('../flags.js').ParserFlags} options.flags
108110
* @returns {ParserCtx}
109111
*/
110-
export function createParserCtx(text, tokens, {storeLocations = false} = {}) {
112+
export function createParserCtx(text, tokens, {storeLocations = false, flags}) {
111113
const iterator = tokens[Symbol.iterator]();
112114

113115
return {
@@ -131,6 +133,7 @@ export function createParserCtx(text, tokens, {storeLocations = false} = {}) {
131133
errors: null,
132134
},
133135
errors: [],
136+
flags,
134137
};
135138
}
136139

@@ -220,6 +223,13 @@ function parseNonStringValue(ctx) {
220223
switch (token?.type) {
221224
case T_NUMBER_WITH_SUFFIX:
222225
{
226+
if (!ctx.flags.experimentalSuffixedNumbers) {
227+
throw new InvalidKdlError(
228+
"Unreachable code: got a suffixed number from the tokenizer but the suffixed number flag is not set",
229+
{token},
230+
);
231+
}
232+
223233
const startOfSuffix = token.text.search(/[^0-9._]/);
224234

225235
representation = token.text.slice(0, startOfSuffix);
@@ -327,7 +337,9 @@ function parseNonStringValue(ctx) {
327337

328338
pop(ctx);
329339

330-
const tagToken = consume(ctx, T_KEYWORD_OR_HASHED_IDENT);
340+
const tagToken =
341+
ctx.flags.experimentalSuffixedNumbers &&
342+
consume(ctx, T_KEYWORD_OR_HASHED_IDENT);
331343
if (tagToken) {
332344
if (checkSeparatedSuffix) {
333345
suffixTagRepresentation = tagToken.text;

src/parser/tokenize/context.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,17 @@ function iterateCodePoints(text) {
4040
* @prop {Location} start
4141
* @prop {boolean} graphemeLocations
4242
* @prop {InvalidKdlError[] | null} errorsInToken
43+
* @prop {import('../../flags.js').ParserFlags} flags
4344
*/
4445

4546
/**
4647
* @param {string} text
47-
* @param {{graphemeLocations?: boolean}} opts
48+
* @param {object} opts
49+
* @param {boolean} [opts.graphemeLocations]
50+
* @param {import('../../flags.js').ParserFlags} opts.flags
4851
* @returns {TokenizeContext}
4952
*/
50-
export function createContext(text, opts) {
51-
const graphemeLocations = opts.graphemeLocations ?? false;
52-
53+
export function createContext(text, {flags, graphemeLocations = false}) {
5354
const iterator =
5455
graphemeLocations ? iterateGraphemes(text) : iterateCodePoints(text);
5556
const currentIter = iterator.next();
@@ -86,6 +87,8 @@ export function createContext(text, opts) {
8687
start: {line: 1, column: 1, offset: 0},
8788

8889
errorsInToken: null,
90+
91+
flags,
8992
};
9093
}
9194

src/parser/tokenize/tokenize-query.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ characterHandlers[0xa0] = handleWhitespaceCharacter; // No-Break Space
146146

147147
/**
148148
* @param {string} t
149-
* @param {{graphemeLocations?: boolean}} opts
149+
* @param {object} opts
150+
* @param {boolean} [opts.graphemeLocations]
151+
* @param {import('../../flags.js').ParserFlags} opts.flags
150152
* @returns {Generator<Token, void>}
151153
*/
152154
export function* tokenizeQuery(t, opts) {

0 commit comments

Comments
 (0)