recipe-tmlanguage 0.3.3 → 0.3.4
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/bin/commands/generate.ts +37 -0
- package/bin/commands/verify.ts +53 -0
- package/bin/recipe-tmlang.mjs +76 -74
- package/bin/recipe-tmlang.ts +7 -89
- package/package.json +4 -2
- package/src/grammar.ts +1 -1
- package/src/verifier.ts +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { buildGrammar, serializeGrammar } from "#grammar";
|
|
2
|
+
import { command, flag } from "@kjanat/dreamcli";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { cwd } from "node:process";
|
|
7
|
+
|
|
8
|
+
const indentOf = (raw: string): "tab" | number => (raw === "tab" ? "tab" : Number(raw));
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const DEFAULT_OUT = `${dirname(require.resolve("#pkg"))}/recipe.tmLanguage.json`;
|
|
11
|
+
|
|
12
|
+
export const generateCmd = command("generate")
|
|
13
|
+
.description("Build the TextMate grammar from the tree-sitter-recipe vocabulary")
|
|
14
|
+
.flag("out", flag.string().alias("o").default(DEFAULT_OUT).describe("Output JSON path"))
|
|
15
|
+
.flag("indent", flag.enum(["tab", "2", "4"]).default("tab").describe("JSON indent"))
|
|
16
|
+
.flag("quiet", flag.boolean().alias("q").default(false).describe("Suppress stats on success"))
|
|
17
|
+
.action(({ flags, out }) => {
|
|
18
|
+
const { grammar, stats } = buildGrammar();
|
|
19
|
+
const serialized = serializeGrammar(grammar, indentOf(flags.indent));
|
|
20
|
+
const outAbs = resolve(cwd(), flags.out);
|
|
21
|
+
mkdirSync(dirname(outAbs), { recursive: true });
|
|
22
|
+
writeFileSync(outAbs, serialized);
|
|
23
|
+
const { json, jsonMode, log } = out;
|
|
24
|
+
|
|
25
|
+
if (jsonMode) {
|
|
26
|
+
json({ ok: true, outPath: outAbs, bytes: serialized.length, stats });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (flags.quiet) return;
|
|
30
|
+
|
|
31
|
+
log(`wrote ${outAbs}`);
|
|
32
|
+
log(` ${stats.topLevelPatterns} top-level patterns · ${serialized.length} bytes`);
|
|
33
|
+
const v = stats.vocab;
|
|
34
|
+
log(
|
|
35
|
+
` vocab: ${v.frequency} frequency · ${v.timing.single}+${v.timing.multi} timing · ${v.route.single}+${v.route.multi} route · ${v.dispensing.single}+${v.dispensing.multi} dispensing · ${v.forms.single}+${v.forms.multi} forms · ${v.compounding.single}+${v.compounding.multi} compounding · ${v.conditional.single}+${v.conditional.multi} conditional · ${v.warning} warning · ${v.units} units`,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { verify } from "#verifier";
|
|
2
|
+
import { command, flag } from "@kjanat/dreamcli";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { cwd, exit } from "node:process";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const TS_RX_DIR = resolve(dirname(require.resolve("tree-sitter-recipe/package.json")));
|
|
9
|
+
const DEFAULT_FIXTURES_DIR = resolve(TS_RX_DIR, "test/highlight");
|
|
10
|
+
const DEFAULT_ONIG_WASM = require.resolve("vscode-oniguruma/release/onig.wasm");
|
|
11
|
+
const DEFAULT_OUT = `${dirname(require.resolve("#pkg"))}/recipe.tmLanguage.json`;
|
|
12
|
+
|
|
13
|
+
export const verifyCmd = command("verify")
|
|
14
|
+
.description("Tokenize tree-sitter-recipe highlight fixtures and assert scope matches")
|
|
15
|
+
.flag("grammar", flag.string().alias("g").default(DEFAULT_OUT).describe("Path to .tmLanguage.json"))
|
|
16
|
+
.flag("fixtures", flag.string().alias("f").default(DEFAULT_FIXTURES_DIR).describe("Directory of .recipe fixtures"))
|
|
17
|
+
.flag("onig-wasm", flag.string().default(DEFAULT_ONIG_WASM).describe("Path to oniguruma WASM"))
|
|
18
|
+
.flag("max-failures", flag.number().default(40).describe("Max failures to print (0 = all)"))
|
|
19
|
+
.action(async ({ flags, out }) => {
|
|
20
|
+
const result = await verify({
|
|
21
|
+
grammarPath: resolve(cwd(), flags.grammar),
|
|
22
|
+
fixturesDir: resolve(cwd(), flags.fixtures),
|
|
23
|
+
onigWasmPath: resolve(cwd(), flags["onig-wasm"]),
|
|
24
|
+
});
|
|
25
|
+
const failuresLen = result.failures.length;
|
|
26
|
+
const { json, jsonMode, setExitCode, log } = out;
|
|
27
|
+
|
|
28
|
+
if (jsonMode) {
|
|
29
|
+
json(result);
|
|
30
|
+
if (failuresLen > 0) {
|
|
31
|
+
setExitCode(1);
|
|
32
|
+
exit();
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log(`${result.pass} / ${result.total} assertions pass`);
|
|
38
|
+
if (failuresLen === 0) return;
|
|
39
|
+
|
|
40
|
+
log("");
|
|
41
|
+
log("── failures ──");
|
|
42
|
+
const limit = flags["max-failures"] === 0 ? failuresLen : flags["max-failures"];
|
|
43
|
+
for (const f of result.failures.slice(0, limit)) {
|
|
44
|
+
const gotStr = f.got
|
|
45
|
+
? f.got.filter((s) => s !== "source.recipe").join(" · ") || "(root only)"
|
|
46
|
+
: "(no token)";
|
|
47
|
+
log(` ${f.fixture}:${f.line}:${f.col} expected ${f.capture} got [${gotStr}]`);
|
|
48
|
+
}
|
|
49
|
+
if (failuresLen > limit) {
|
|
50
|
+
log(` … +${failuresLen - limit} more`);
|
|
51
|
+
}
|
|
52
|
+
setExitCode(1);
|
|
53
|
+
});
|
package/bin/recipe-tmlang.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
-
import { cwd, exit } from "node:process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { cli, command, flag } from "dreamcli";
|
|
8
3
|
import { COUNTERS, NUMBER_WORDS, PERIODS, PERIOD_PLURALS } from "tree-sitter-recipe/grammar/dutch";
|
|
9
4
|
import { COMPOUNDING, COMPOUNDING_MULTIWORD, CONDITIONAL, CONDITIONAL_MULTIWORD, DISPENSING, DISPENSING_MULTIWORD, FORMS, FORMS_MULTIWORD, FREQUENCY, ROUTE, ROUTE_MULTIWORD, TIMING, TIMING_MULTIWORD, WARNING } from "tree-sitter-recipe/grammar/latin";
|
|
10
5
|
import { UNITS } from "tree-sitter-recipe/grammar/units";
|
|
6
|
+
import { cli, command, flag } from "@kjanat/dreamcli";
|
|
7
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { cwd, exit } from "node:process";
|
|
11
10
|
//#region src/grammar.ts
|
|
12
11
|
/**
|
|
13
12
|
* @file Pure grammar builder — imports the tree-sitter-recipe vocabulary and
|
|
@@ -45,7 +44,7 @@ const SCOPE = {
|
|
|
45
44
|
const REGEX_METACHARS = /[.*+?^${}()|[\]\\]/g;
|
|
46
45
|
const escapeRegex = (s) => s.replace(REGEX_METACHARS, "\\$&");
|
|
47
46
|
const alt = (items) => [...new Set(items)].sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|
|
48
|
-
const altMultiword = (items) => [...new Set(items)].sort((a, b) => b.length - a.length).map((s) => s
|
|
47
|
+
const altMultiword = (items) => [...new Set(items)].sort((a, b) => b.length - a.length).map((s) => escapeRegex(s).replace(/\s+/g, "\\s+")).join("|");
|
|
49
48
|
const wb = (pattern) => `(?<![\\w.])(?:${pattern})(?![\\w.])`;
|
|
50
49
|
function buildGrammar() {
|
|
51
50
|
const doseMatch = {
|
|
@@ -267,6 +266,36 @@ function serializeGrammar(g, indent) {
|
|
|
267
266
|
return `${JSON.stringify(g, null, indent === "tab" ? " " : indent)}\n`;
|
|
268
267
|
}
|
|
269
268
|
//#endregion
|
|
269
|
+
//#region bin/commands/generate.ts
|
|
270
|
+
const indentOf = (raw) => raw === "tab" ? "tab" : Number(raw);
|
|
271
|
+
const DEFAULT_OUT$1 = `${dirname(createRequire(import.meta.url).resolve("#pkg"))}/recipe.tmLanguage.json`;
|
|
272
|
+
const generateCmd = command("generate").description("Build the TextMate grammar from the tree-sitter-recipe vocabulary").flag("out", flag.string().alias("o").default(DEFAULT_OUT$1).describe("Output JSON path")).flag("indent", flag.enum([
|
|
273
|
+
"tab",
|
|
274
|
+
"2",
|
|
275
|
+
"4"
|
|
276
|
+
]).default("tab").describe("JSON indent")).flag("quiet", flag.boolean().alias("q").default(false).describe("Suppress stats on success")).action(({ flags, out }) => {
|
|
277
|
+
const { grammar, stats } = buildGrammar();
|
|
278
|
+
const serialized = serializeGrammar(grammar, indentOf(flags.indent));
|
|
279
|
+
const outAbs = resolve(cwd(), flags.out);
|
|
280
|
+
mkdirSync(dirname(outAbs), { recursive: true });
|
|
281
|
+
writeFileSync(outAbs, serialized);
|
|
282
|
+
const { json, jsonMode, log } = out;
|
|
283
|
+
if (jsonMode) {
|
|
284
|
+
json({
|
|
285
|
+
ok: true,
|
|
286
|
+
outPath: outAbs,
|
|
287
|
+
bytes: serialized.length,
|
|
288
|
+
stats
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (flags.quiet) return;
|
|
293
|
+
log(`wrote ${outAbs}`);
|
|
294
|
+
log(` ${stats.topLevelPatterns} top-level patterns · ${serialized.length} bytes`);
|
|
295
|
+
const v = stats.vocab;
|
|
296
|
+
log(` vocab: ${v.frequency} frequency · ${v.timing.single}+${v.timing.multi} timing · ${v.route.single}+${v.route.multi} route · ${v.dispensing.single}+${v.dispensing.multi} dispensing · ${v.forms.single}+${v.forms.multi} forms · ${v.compounding.single}+${v.compounding.multi} compounding · ${v.conditional.single}+${v.conditional.multi} conditional · ${v.warning} warning · ${v.units} units`);
|
|
297
|
+
});
|
|
298
|
+
//#endregion
|
|
270
299
|
//#region src/verifier.ts
|
|
271
300
|
/**
|
|
272
301
|
* @file Pure verifier — tokenizes tree-sitter-recipe's own highlight fixtures
|
|
@@ -276,9 +305,9 @@ function serializeGrammar(g, indent) {
|
|
|
276
305
|
* No CLI concerns here; the caller supplies paths and decides how to present
|
|
277
306
|
* the result (text table / JSON / exit code).
|
|
278
307
|
*/
|
|
279
|
-
const require = createRequire(import.meta.url);
|
|
280
|
-
const oniguruma = require("vscode-oniguruma");
|
|
281
|
-
const { parseRawGrammar, Registry } = require("vscode-textmate");
|
|
308
|
+
const require$1 = createRequire(import.meta.url);
|
|
309
|
+
const oniguruma = require$1("vscode-oniguruma");
|
|
310
|
+
const { parseRawGrammar, Registry } = require$1("vscode-textmate");
|
|
282
311
|
const CAPTURE_EXPECTS = {
|
|
283
312
|
"keyword.directive": "keyword.control.directive",
|
|
284
313
|
"keyword.repeat": "keyword.other.frequency",
|
|
@@ -340,7 +369,7 @@ async function verify(opts) {
|
|
|
340
369
|
const rawGrammar = parseRawGrammar(readFileSync(opts.grammarPath, "utf-8"), opts.grammarPath);
|
|
341
370
|
const grammar = await new Registry({
|
|
342
371
|
onigLib,
|
|
343
|
-
loadGrammar:
|
|
372
|
+
loadGrammar: () => Promise.resolve(null)
|
|
344
373
|
}).addGrammar(rawGrammar);
|
|
345
374
|
const result = {
|
|
346
375
|
pass: 0,
|
|
@@ -380,55 +409,11 @@ async function verify(opts) {
|
|
|
380
409
|
return result;
|
|
381
410
|
}
|
|
382
411
|
//#endregion
|
|
383
|
-
//#region
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"url": "git+https://github.com/kjanat/recipe-tmlanguage.git"
|
|
389
|
-
};
|
|
390
|
-
//#endregion
|
|
391
|
-
//#region bin/recipe-tmlang.ts
|
|
392
|
-
/**
|
|
393
|
-
* recipe-tmlang — TextMate grammar generator & verifier for recipe-tmlanguage.
|
|
394
|
-
*
|
|
395
|
-
* Subcommands
|
|
396
|
-
* - generate: Build dist/recipe.tmLanguage.json from the tree-sitter-recipe vocab.
|
|
397
|
-
* - verify: Tokenize tree-sitter-recipe's highlight fixtures and assert scopes.
|
|
398
|
-
*
|
|
399
|
-
* Zero manual argparse — argument parsing, help, and completions all come from
|
|
400
|
-
* {@link https://github.com/kjanat/dreamcli | DreamCLI}. `--json` is a DreamCLI built-in;
|
|
401
|
-
* we branch on {@linkcode Out.jsonMode}.
|
|
402
|
-
*/
|
|
403
|
-
const DEFAULT_OUT = `${resolve(import.meta.dirname, "..")}/recipe.tmLanguage.json`;
|
|
404
|
-
const DEFAULT_FIXTURES_DIR = resolve(resolve(dirname(fileURLToPath(import.meta.resolve("tree-sitter-recipe/package.json")))), "test/highlight");
|
|
405
|
-
const DEFAULT_ONIG_WASM = fileURLToPath(import.meta.resolve("vscode-oniguruma/release/onig.wasm"));
|
|
406
|
-
const indentOf = (raw) => raw === "tab" ? "tab" : Number(raw);
|
|
407
|
-
const generate = command("generate").description("Build the TextMate grammar from the tree-sitter-recipe vocabulary").flag("out", flag.string().alias("o").default(DEFAULT_OUT).describe("Output JSON path")).flag("indent", flag.enum([
|
|
408
|
-
"tab",
|
|
409
|
-
"2",
|
|
410
|
-
"4"
|
|
411
|
-
]).default("tab").describe("JSON indent")).flag("quiet", flag.boolean().alias("q").default(false).describe("Suppress stats on success")).action(({ flags, out }) => {
|
|
412
|
-
const { grammar, stats } = buildGrammar();
|
|
413
|
-
const serialized = serializeGrammar(grammar, indentOf(flags.indent));
|
|
414
|
-
const outAbs = resolve(cwd(), flags.out);
|
|
415
|
-
mkdirSync(dirname(outAbs), { recursive: true });
|
|
416
|
-
writeFileSync(outAbs, serialized);
|
|
417
|
-
if (out.jsonMode) {
|
|
418
|
-
out.json({
|
|
419
|
-
ok: true,
|
|
420
|
-
outPath: outAbs,
|
|
421
|
-
bytes: serialized.length,
|
|
422
|
-
stats
|
|
423
|
-
});
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
if (flags.quiet) return;
|
|
427
|
-
out.log(`wrote ${outAbs}`);
|
|
428
|
-
out.log(` ${stats.topLevelPatterns} top-level patterns · ${serialized.length} bytes`);
|
|
429
|
-
const v = stats.vocab;
|
|
430
|
-
out.log(` vocab: ${v.frequency} frequency · ${v.timing.single}+${v.timing.multi} timing · ${v.route.single}+${v.route.multi} route · ${v.dispensing.single}+${v.dispensing.multi} dispensing · ${v.forms.single}+${v.forms.multi} forms · ${v.compounding.single}+${v.compounding.multi} compounding · ${v.conditional.single}+${v.conditional.multi} conditional · ${v.warning} warning · ${v.units} units`);
|
|
431
|
-
});
|
|
412
|
+
//#region bin/commands/verify.ts
|
|
413
|
+
const require = createRequire(import.meta.url);
|
|
414
|
+
const DEFAULT_FIXTURES_DIR = resolve(resolve(dirname(require.resolve("tree-sitter-recipe/package.json"))), "test/highlight");
|
|
415
|
+
const DEFAULT_ONIG_WASM = require.resolve("vscode-oniguruma/release/onig.wasm");
|
|
416
|
+
const DEFAULT_OUT = `${dirname(require.resolve("#pkg"))}/recipe.tmLanguage.json`;
|
|
432
417
|
const verifyCmd = command("verify").description("Tokenize tree-sitter-recipe highlight fixtures and assert scope matches").flag("grammar", flag.string().alias("g").default(DEFAULT_OUT).describe("Path to .tmLanguage.json")).flag("fixtures", flag.string().alias("f").default(DEFAULT_FIXTURES_DIR).describe("Directory of .recipe fixtures")).flag("onig-wasm", flag.string().default(DEFAULT_ONIG_WASM).describe("Path to oniguruma WASM")).flag("max-failures", flag.number().default(40).describe("Max failures to print (0 = all)")).action(async ({ flags, out }) => {
|
|
433
418
|
const result = await verify({
|
|
434
419
|
grammarPath: resolve(cwd(), flags.grammar),
|
|
@@ -436,31 +421,48 @@ const verifyCmd = command("verify").description("Tokenize tree-sitter-recipe hig
|
|
|
436
421
|
onigWasmPath: resolve(cwd(), flags["onig-wasm"])
|
|
437
422
|
});
|
|
438
423
|
const failuresLen = result.failures.length;
|
|
439
|
-
|
|
440
|
-
|
|
424
|
+
const { json, jsonMode, setExitCode, log } = out;
|
|
425
|
+
if (jsonMode) {
|
|
426
|
+
json(result);
|
|
441
427
|
if (failuresLen > 0) {
|
|
442
|
-
|
|
428
|
+
setExitCode(1);
|
|
443
429
|
exit();
|
|
444
430
|
}
|
|
445
431
|
return;
|
|
446
432
|
}
|
|
447
|
-
|
|
433
|
+
log(`${result.pass} / ${result.total} assertions pass`);
|
|
448
434
|
if (failuresLen === 0) return;
|
|
449
|
-
|
|
450
|
-
|
|
435
|
+
log("");
|
|
436
|
+
log("── failures ──");
|
|
451
437
|
const limit = flags["max-failures"] === 0 ? failuresLen : flags["max-failures"];
|
|
452
438
|
for (const f of result.failures.slice(0, limit)) {
|
|
453
439
|
const gotStr = f.got ? f.got.filter((s) => s !== "source.recipe").join(" · ") || "(root only)" : "(no token)";
|
|
454
|
-
|
|
440
|
+
log(` ${f.fixture}:${f.line}:${f.col} expected ${f.capture} got [${gotStr}]`);
|
|
455
441
|
}
|
|
456
|
-
if (failuresLen > limit)
|
|
457
|
-
|
|
442
|
+
if (failuresLen > limit) log(` … +${failuresLen - limit} more`);
|
|
443
|
+
setExitCode(1);
|
|
458
444
|
});
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region bin/recipe-tmlang.ts
|
|
447
|
+
/**
|
|
448
|
+
* recipe-tmlang — TextMate grammar generator & verifier for recipe-tmlanguage.
|
|
449
|
+
*
|
|
450
|
+
* Subcommands
|
|
451
|
+
* - generate: Build dist/recipe.tmLanguage.json from the tree-sitter-recipe vocab.
|
|
452
|
+
* - verify: Tokenize tree-sitter-recipe's highlight fixtures and assert scopes.
|
|
453
|
+
*
|
|
454
|
+
* Zero manual argparse — argument parsing, help, and completions all come from
|
|
455
|
+
* {@link https://github.com/kjanat/dreamcli | DreamCLI}. `--json` is a DreamCLI built-in;
|
|
456
|
+
* we branch on {@linkcode out.jsonMode | https://dreamcli.kjanat.com/reference/symbols/main/Out#jsonmode}.
|
|
457
|
+
*/
|
|
458
|
+
const app = cli("recipe-tmlanguage").packageJson({
|
|
459
|
+
repository: {
|
|
460
|
+
"type": "git",
|
|
461
|
+
"url": "git+https://github.com/kjanat/recipe-tmlanguage.git"
|
|
462
|
+
},
|
|
463
|
+
homepage: "https://github.com/kjanat/recipe-tmlanguage#recipe-tmlanguage",
|
|
464
|
+
version: "0.3.4"
|
|
465
|
+
}).links().description("TextMate grammar generator & verifier for the recipe DSL").command(generateCmd).command(verifyCmd).completions();
|
|
464
466
|
if (import.meta.main) app.run();
|
|
465
467
|
//#endregion
|
|
466
|
-
export {
|
|
468
|
+
export {};
|
package/bin/recipe-tmlang.ts
CHANGED
|
@@ -8,98 +8,16 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Zero manual argparse — argument parsing, help, and completions all come from
|
|
10
10
|
* {@link https://github.com/kjanat/dreamcli | DreamCLI}. `--json` is a DreamCLI built-in;
|
|
11
|
-
* we branch on {@linkcode
|
|
11
|
+
* we branch on {@linkcode out.jsonMode | https://dreamcli.kjanat.com/reference/symbols/main/Out#jsonmode}.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { generateCmd } from "#bin/commands/generate";
|
|
14
|
+
import { verifyCmd } from "#bin/commands/verify";
|
|
15
|
+
import { homepage, name, repository, version } from "#pkg" with { type: "json" };
|
|
16
|
+
import { cli } from "@kjanat/dreamcli";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
import type { Out } from "dreamcli";
|
|
20
|
-
|
|
21
|
-
import { buildGrammar, serializeGrammar } from "#grammar";
|
|
22
|
-
import { verify } from "#verifier";
|
|
23
|
-
|
|
24
|
-
import { homepage, repository, version } from "#pkg" with { type: "json" };
|
|
25
|
-
|
|
26
|
-
const DEFAULT_OUT = `${resolve(import.meta.dirname, "..")}/recipe.tmLanguage.json`;
|
|
27
|
-
|
|
28
|
-
const TS_RX_DIR = resolve(dirname(fileURLToPath(import.meta.resolve("tree-sitter-recipe/package.json"))));
|
|
29
|
-
const DEFAULT_FIXTURES_DIR = resolve(TS_RX_DIR, "test/highlight");
|
|
30
|
-
const DEFAULT_ONIG_WASM = fileURLToPath(import.meta.resolve("vscode-oniguruma/release/onig.wasm"));
|
|
31
|
-
|
|
32
|
-
const indentOf = (raw: string): "tab" | number => (raw === "tab" ? "tab" : Number(raw));
|
|
33
|
-
|
|
34
|
-
const generate = command("generate")
|
|
35
|
-
.description("Build the TextMate grammar from the tree-sitter-recipe vocabulary")
|
|
36
|
-
.flag("out", flag.string().alias("o").default(DEFAULT_OUT).describe("Output JSON path"))
|
|
37
|
-
.flag("indent", flag.enum(["tab", "2", "4"]).default("tab").describe("JSON indent"))
|
|
38
|
-
.flag("quiet", flag.boolean().alias("q").default(false).describe("Suppress stats on success"))
|
|
39
|
-
.action(({ flags, out }) => {
|
|
40
|
-
const { grammar, stats } = buildGrammar();
|
|
41
|
-
const serialized = serializeGrammar(grammar, indentOf(flags.indent));
|
|
42
|
-
const outAbs = resolve(cwd(), flags.out);
|
|
43
|
-
mkdirSync(dirname(outAbs), { recursive: true });
|
|
44
|
-
writeFileSync(outAbs, serialized);
|
|
45
|
-
if (out.jsonMode) {
|
|
46
|
-
out.json({ ok: true, outPath: outAbs, bytes: serialized.length, stats });
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (flags.quiet) return;
|
|
50
|
-
|
|
51
|
-
out.log(`wrote ${outAbs}`);
|
|
52
|
-
out.log(` ${stats.topLevelPatterns} top-level patterns · ${serialized.length} bytes`);
|
|
53
|
-
const v = stats.vocab;
|
|
54
|
-
out.log(
|
|
55
|
-
` vocab: ${v.frequency} frequency · ${v.timing.single}+${v.timing.multi} timing · ${v.route.single}+${v.route.multi} route · ${v.dispensing.single}+${v.dispensing.multi} dispensing · ${v.forms.single}+${v.forms.multi} forms · ${v.compounding.single}+${v.compounding.multi} compounding · ${v.conditional.single}+${v.conditional.multi} conditional · ${v.warning} warning · ${v.units} units`,
|
|
56
|
-
);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const verifyCmd = command("verify")
|
|
60
|
-
.description("Tokenize tree-sitter-recipe highlight fixtures and assert scope matches")
|
|
61
|
-
.flag("grammar", flag.string().alias("g").default(DEFAULT_OUT).describe("Path to .tmLanguage.json"))
|
|
62
|
-
.flag("fixtures", flag.string().alias("f").default(DEFAULT_FIXTURES_DIR).describe("Directory of .recipe fixtures"))
|
|
63
|
-
.flag("onig-wasm", flag.string().default(DEFAULT_ONIG_WASM).describe("Path to oniguruma WASM"))
|
|
64
|
-
.flag("max-failures", flag.number().default(40).describe("Max failures to print (0 = all)"))
|
|
65
|
-
.action(async ({ flags, out }) => {
|
|
66
|
-
const result = await verify({
|
|
67
|
-
grammarPath: resolve(cwd(), flags.grammar),
|
|
68
|
-
fixturesDir: resolve(cwd(), flags.fixtures),
|
|
69
|
-
onigWasmPath: resolve(cwd(), flags["onig-wasm"]),
|
|
70
|
-
});
|
|
71
|
-
const failuresLen = result.failures.length;
|
|
72
|
-
|
|
73
|
-
if (out.jsonMode) {
|
|
74
|
-
out.json(result);
|
|
75
|
-
if (failuresLen > 0) {
|
|
76
|
-
out.setExitCode(1);
|
|
77
|
-
exit();
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
out.log(`${result.pass} / ${result.total} assertions pass`);
|
|
83
|
-
if (failuresLen === 0) return;
|
|
84
|
-
|
|
85
|
-
out.log("");
|
|
86
|
-
out.log("── failures ──");
|
|
87
|
-
const limit = flags["max-failures"] === 0 ? failuresLen : flags["max-failures"];
|
|
88
|
-
for (const f of result.failures.slice(0, limit)) {
|
|
89
|
-
const gotStr = f.got
|
|
90
|
-
? f.got.filter((s) => s !== "source.recipe").join(" · ") || "(root only)"
|
|
91
|
-
: "(no token)";
|
|
92
|
-
out.log(` ${f.fixture}:${f.line}:${f.col} expected ${f.capture} got [${gotStr}]`);
|
|
93
|
-
}
|
|
94
|
-
if (failuresLen > limit) {
|
|
95
|
-
out.log(` … +${failuresLen - limit} more`);
|
|
96
|
-
}
|
|
97
|
-
out.setExitCode(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export const app = cli("recipe-tmlang").packageJson({ repository, homepage, version }).links()
|
|
18
|
+
const app = cli(name).packageJson({ repository, homepage, version }).links()
|
|
101
19
|
.description("TextMate grammar generator & verifier for the recipe DSL")
|
|
102
|
-
.command(
|
|
20
|
+
.command(generateCmd)
|
|
103
21
|
.command(verifyCmd)
|
|
104
22
|
.completions();
|
|
105
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recipe-tmlanguage",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "TextMate grammar for the recipe (.recipe) pharmacological notation language.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dreamcli",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"type": "module",
|
|
23
23
|
"imports": {
|
|
24
|
+
"#bin/*": "./bin/*.ts",
|
|
24
25
|
"#pkg": "./package.json",
|
|
25
26
|
"#grammar": "./src/grammar.ts",
|
|
26
27
|
"#verifier": "./src/verifier.ts"
|
|
@@ -53,11 +54,12 @@
|
|
|
53
54
|
"lint": "biome lint",
|
|
54
55
|
"prepack": "bun run generate && bun run bundle",
|
|
55
56
|
"recipe-tmlang": "bun bin/recipe-tmlang.ts",
|
|
57
|
+
"test": "bun test",
|
|
56
58
|
"typecheck": "tsc --noEmit",
|
|
57
59
|
"verify": "run recipe-tmlang verify"
|
|
58
60
|
},
|
|
59
61
|
"dependencies": {
|
|
60
|
-
"dreamcli": "
|
|
62
|
+
"@kjanat/dreamcli": "^2.4.1",
|
|
61
63
|
"tree-sitter-recipe": "^0.3.1",
|
|
62
64
|
"vscode-oniguruma": "^2.0.1",
|
|
63
65
|
"vscode-textmate": "^9.2.0"
|
package/src/grammar.ts
CHANGED
|
@@ -74,7 +74,7 @@ const alt = (items: readonly string[]): string =>
|
|
|
74
74
|
const altMultiword = (items: readonly string[]): string =>
|
|
75
75
|
[...new Set(items)]
|
|
76
76
|
.sort((a, b) => b.length - a.length)
|
|
77
|
-
.map((s) => s
|
|
77
|
+
.map((s) => escapeRegex(s).replace(/\s+/g, "\\s+"))
|
|
78
78
|
.join("|");
|
|
79
79
|
|
|
80
80
|
// Word boundary that treats `.` as part of the token so `a.c.` doesn't match
|
package/src/verifier.ts
CHANGED
|
@@ -117,7 +117,7 @@ export async function verify(opts: VerifyOptions): Promise<VerifyResult> {
|
|
|
117
117
|
readFileSync(opts.grammarPath, "utf-8"),
|
|
118
118
|
opts.grammarPath,
|
|
119
119
|
);
|
|
120
|
-
const registry = new Registry({ onigLib, loadGrammar:
|
|
120
|
+
const registry = new Registry({ onigLib, loadGrammar: () => Promise.resolve(null) });
|
|
121
121
|
const grammar = await registry.addGrammar(rawGrammar);
|
|
122
122
|
|
|
123
123
|
const result: VerifyResult = { pass: 0, total: 0, failures: [] };
|