recipe-tmlanguage 0.3.4 → 0.3.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.
@@ -1,12 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ import { cli, command, flag } from "@kjanat/dreamcli";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { COUNTERS, NUMBER_WORDS, PERIODS, PERIOD_PLURALS } from "tree-sitter-recipe/grammar/dutch";
4
7
  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";
5
8
  import { UNITS } from "tree-sitter-recipe/grammar/units";
6
- import { cli, command, flag } from "@kjanat/dreamcli";
7
9
  import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
8
- import { dirname, resolve } from "node:path";
9
10
  import { cwd, exit } from "node:process";
11
+ import { readFile } from "node:fs/promises";
12
+ //#region bin/lib/utils.ts
13
+ const require$1 = createRequire(import.meta.url);
14
+ const toPath = (resolved) => resolved.startsWith("file:") ? fileURLToPath(resolved) : resolved;
15
+ function packageDir(specifier) {
16
+ return resolve(dirname(toPath(require$1.resolve(specifier))));
17
+ }
18
+ function resolveImportMeta(specifier) {
19
+ return toPath(import.meta.resolve(specifier));
20
+ }
21
+ /**
22
+ * Default path of the generated grammar: `recipe.tmLanguage.json` at the
23
+ * package root. Anchored on `#pkg` (package.json / deno.json — always present)
24
+ * rather than `#tmLang`, because the grammar is generated + gitignored and may
25
+ * not exist yet on a fresh checkout, and `import.meta.resolve` throws on a
26
+ * missing target. Existence is enforced later, where the file is actually read.
27
+ */
28
+ function defaultGrammarPath() {
29
+ return resolve(dirname(resolveImportMeta("#pkg")), "recipe.tmLanguage.json");
30
+ }
31
+ //#endregion
10
32
  //#region src/grammar.ts
11
33
  /**
12
34
  * @file Pure grammar builder — imports the tree-sitter-recipe vocabulary and
@@ -15,6 +37,8 @@ import { cwd, exit } from "node:process";
15
37
  *
16
38
  * Scopes are standard TextMate names with a `.recipe` suffix so themes paint
17
39
  * recipe blocks without a custom theme shipment.
40
+ *
41
+ * @module recipe-tmlanguage/src/grammar
18
42
  */
19
43
  const SCOPE = {
20
44
  rxMarker: "keyword.control.directive.rx.recipe",
@@ -268,7 +292,7 @@ function serializeGrammar(g, indent) {
268
292
  //#endregion
269
293
  //#region bin/commands/generate.ts
270
294
  const indentOf = (raw) => raw === "tab" ? "tab" : Number(raw);
271
- const DEFAULT_OUT$1 = `${dirname(createRequire(import.meta.url).resolve("#pkg"))}/recipe.tmLanguage.json`;
295
+ const DEFAULT_OUT$1 = defaultGrammarPath();
272
296
  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
297
  "tab",
274
298
  "2",
@@ -296,6 +320,31 @@ const generateCmd = command("generate").description("Build the TextMate grammar
296
320
  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
321
  });
298
322
  //#endregion
323
+ //#region src/deps/oniguruma.ts
324
+ const require = createRequire(import.meta.url);
325
+ const oniguruma = require("vscode-oniguruma");
326
+ let loadPromise;
327
+ function loadOniguruma() {
328
+ loadPromise ??= (async () => {
329
+ const wasm = await readFile(require.resolve("vscode-oniguruma/release/onig.wasm"));
330
+ await oniguruma.loadWASM(wasm);
331
+ })();
332
+ return loadPromise;
333
+ }
334
+ async function createOnigLib() {
335
+ await loadOniguruma();
336
+ return {
337
+ createOnigScanner(patterns) {
338
+ return oniguruma.createOnigScanner(patterns);
339
+ },
340
+ createOnigString(string) {
341
+ return oniguruma.createOnigString(string);
342
+ }
343
+ };
344
+ }
345
+ const { loadWASM, createOnigScanner, createOnigString, OnigScanner, OnigString } = oniguruma;
346
+ const { parseRawGrammar, Registry, INITIAL } = createRequire(import.meta.url)("vscode-textmate");
347
+ //#endregion
299
348
  //#region src/verifier.ts
300
349
  /**
301
350
  * @file Pure verifier — tokenizes tree-sitter-recipe's own highlight fixtures
@@ -304,10 +353,9 @@ const generateCmd = command("generate").description("Build the TextMate grammar
304
353
  *
305
354
  * No CLI concerns here; the caller supplies paths and decides how to present
306
355
  * the result (text table / JSON / exit code).
356
+ *
357
+ * @module recipe-tmlanguage/src/verifier
307
358
  */
308
- const require$1 = createRequire(import.meta.url);
309
- const oniguruma = require$1("vscode-oniguruma");
310
- const { parseRawGrammar, Registry } = require$1("vscode-textmate");
311
359
  const CAPTURE_EXPECTS = {
312
360
  "keyword.directive": "keyword.control.directive",
313
361
  "keyword.repeat": "keyword.other.frequency",
@@ -360,12 +408,7 @@ function parseFixture(content, name) {
360
408
  };
361
409
  }
362
410
  async function verify(opts) {
363
- const wasmBin = readFileSync(opts.onigWasmPath);
364
- await oniguruma.loadWASM(wasmBin.buffer);
365
- const onigLib = Promise.resolve({
366
- createOnigScanner: (patterns) => new oniguruma.OnigScanner(patterns),
367
- createOnigString: (s) => new oniguruma.OnigString(s)
368
- });
411
+ const onigLib = createOnigLib();
369
412
  const rawGrammar = parseRawGrammar(readFileSync(opts.grammarPath, "utf-8"), opts.grammarPath);
370
413
  const grammar = await new Registry({
371
414
  onigLib,
@@ -410,15 +453,12 @@ async function verify(opts) {
410
453
  }
411
454
  //#endregion
412
455
  //#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`;
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 }) => {
456
+ const DEFAULT_FIXTURES_DIR = resolve(packageDir("tree-sitter-recipe/package.json"), "test/highlight");
457
+ const DEFAULT_OUT = defaultGrammarPath();
458
+ 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("max-failures", flag.number().default(40).describe("Max failures to print (0 = all)")).action(async ({ flags, out }) => {
418
459
  const result = await verify({
419
460
  grammarPath: resolve(cwd(), flags.grammar),
420
- fixturesDir: resolve(cwd(), flags.fixtures),
421
- onigWasmPath: resolve(cwd(), flags["onig-wasm"])
461
+ fixturesDir: resolve(cwd(), flags.fixtures)
422
462
  });
423
463
  const failuresLen = result.failures.length;
424
464
  const { json, jsonMode, setExitCode, log } = out;
@@ -443,7 +483,7 @@ const verifyCmd = command("verify").description("Tokenize tree-sitter-recipe hig
443
483
  setExitCode(1);
444
484
  });
445
485
  //#endregion
446
- //#region bin/recipe-tmlang.ts
486
+ //#region bin/cli.ts
447
487
  /**
448
488
  * recipe-tmlang — TextMate grammar generator & verifier for recipe-tmlanguage.
449
489
  *
@@ -454,14 +494,12 @@ const verifyCmd = command("verify").description("Tokenize tree-sitter-recipe hig
454
494
  * Zero manual argparse — argument parsing, help, and completions all come from
455
495
  * {@link https://github.com/kjanat/dreamcli | DreamCLI}. `--json` is a DreamCLI built-in;
456
496
  * we branch on {@linkcode out.jsonMode | https://dreamcli.kjanat.com/reference/symbols/main/Out#jsonmode}.
497
+ *
498
+ * @module recipe-tmlanguage/bin
457
499
  */
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"
500
+ const app = cli("recipe-tmlanguage").manifest({
501
+ from: import.meta.url,
502
+ files: ["package.json", "deno.json"]
465
503
  }).links().description("TextMate grammar generator & verifier for the recipe DSL").command(generateCmd).command(verifyCmd).completions();
466
504
  if (import.meta.main) app.run();
467
505
  //#endregion
@@ -9,13 +9,16 @@
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
11
  * we branch on {@linkcode out.jsonMode | https://dreamcli.kjanat.com/reference/symbols/main/Out#jsonmode}.
12
+ *
13
+ * @module recipe-tmlanguage/bin
12
14
  */
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
15
  import { cli } from "@kjanat/dreamcli";
16
+ import { generateCmd } from "./commands/generate.ts";
17
+ import { verifyCmd } from "./commands/verify.ts";
17
18
 
18
- const app = cli(name).packageJson({ repository, homepage, version }).links()
19
+ const app = cli("recipe-tmlanguage")
20
+ .manifest({ from: import.meta.url, files: ["package.json", "deno.json"] })
21
+ .links()
19
22
  .description("TextMate grammar generator & verifier for the recipe DSL")
20
23
  .command(generateCmd)
21
24
  .command(verifyCmd)
@@ -1,13 +1,12 @@
1
- import { buildGrammar, serializeGrammar } from "#grammar";
1
+ import { defaultGrammarPath } from "#bin/lib/utils.ts";
2
+ import { buildGrammar, serializeGrammar } from "#src/grammar.ts";
2
3
  import { command, flag } from "@kjanat/dreamcli";
3
4
  import { mkdirSync, writeFileSync } from "node:fs";
4
- import { createRequire } from "node:module";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { cwd } from "node:process";
7
7
 
8
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`;
9
+ const DEFAULT_OUT = defaultGrammarPath();
11
10
 
12
11
  export const generateCmd = command("generate")
13
12
  .description("Build the TextMate grammar from the tree-sitter-recipe vocabulary")
@@ -1,26 +1,21 @@
1
- import { verify } from "#verifier";
1
+ import { defaultGrammarPath, packageDir } from "#bin/lib/utils.ts";
2
+ import { verify } from "#src/verifier.ts";
2
3
  import { command, flag } from "@kjanat/dreamcli";
3
- import { createRequire } from "node:module";
4
- import { dirname, resolve } from "node:path";
4
+ import { resolve } from "node:path";
5
5
  import { cwd, exit } from "node:process";
6
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`;
7
+ const DEFAULT_FIXTURES_DIR = resolve(packageDir("tree-sitter-recipe/package.json"), "test/highlight");
8
+ const DEFAULT_OUT = defaultGrammarPath();
12
9
 
13
10
  export const verifyCmd = command("verify")
14
11
  .description("Tokenize tree-sitter-recipe highlight fixtures and assert scope matches")
15
12
  .flag("grammar", flag.string().alias("g").default(DEFAULT_OUT).describe("Path to .tmLanguage.json"))
16
13
  .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
14
  .flag("max-failures", flag.number().default(40).describe("Max failures to print (0 = all)"))
19
15
  .action(async ({ flags, out }) => {
20
16
  const result = await verify({
21
17
  grammarPath: resolve(cwd(), flags.grammar),
22
18
  fixturesDir: resolve(cwd(), flags.fixtures),
23
- onigWasmPath: resolve(cwd(), flags["onig-wasm"]),
24
19
  });
25
20
  const failuresLen = result.failures.length;
26
21
  const { json, jsonMode, setExitCode, log } = out;
@@ -0,0 +1,29 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ const toPath = (resolved: string) =>
8
+ resolved.startsWith("file:")
9
+ ? fileURLToPath(resolved)
10
+ : resolved;
11
+
12
+ export function packageDir(specifier: string): string {
13
+ return resolve(dirname(toPath(require.resolve(specifier))));
14
+ }
15
+
16
+ export function resolveImportMeta(specifier: string): string {
17
+ return toPath(import.meta.resolve(specifier));
18
+ }
19
+
20
+ /**
21
+ * Default path of the generated grammar: `recipe.tmLanguage.json` at the
22
+ * package root. Anchored on `#pkg` (package.json / deno.json — always present)
23
+ * rather than `#tmLang`, because the grammar is generated + gitignored and may
24
+ * not exist yet on a fresh checkout, and `import.meta.resolve` throws on a
25
+ * missing target. Existence is enforced later, where the file is actually read.
26
+ */
27
+ export function defaultGrammarPath(): string {
28
+ return resolve(dirname(resolveImportMeta("#pkg")), "recipe.tmLanguage.json");
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recipe-tmlanguage",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "TextMate grammar for the recipe (.recipe) pharmacological notation language.",
5
5
  "keywords": [
6
6
  "dreamcli",
@@ -21,18 +21,20 @@
21
21
  },
22
22
  "type": "module",
23
23
  "imports": {
24
- "#bin/*": "./bin/*.ts",
24
+ "#bin/*": "./bin/*",
25
+ "#src/*": "./src/*",
26
+ "#deps/*": "./src/deps/*",
25
27
  "#pkg": "./package.json",
26
- "#grammar": "./src/grammar.ts",
27
- "#verifier": "./src/verifier.ts"
28
+ "#tmLang": "./recipe.tmLanguage.json"
28
29
  },
29
30
  "exports": {
30
31
  ".": "./recipe.tmLanguage.json",
32
+ "./bin": "./bin/cli.mjs",
31
33
  "./package.json": "./package.json"
32
34
  },
33
35
  "main": "./recipe.tmLanguage.json",
34
36
  "module": "./recipe.tmLanguage.json",
35
- "bin": "bin/recipe-tmlang.mjs",
37
+ "bin": "bin/cli.mjs",
36
38
  "directories": {
37
39
  "lib": "/src",
38
40
  "bin": "/bin"
@@ -53,16 +55,17 @@
53
55
  "generate": "run recipe-tmlang generate",
54
56
  "lint": "biome lint",
55
57
  "prepack": "bun run generate && bun run bundle",
56
- "recipe-tmlang": "bun bin/recipe-tmlang.ts",
58
+ "recipe-tmlang": "bun bin/cli.ts",
57
59
  "test": "bun test",
58
60
  "typecheck": "tsc --noEmit",
59
- "verify": "run recipe-tmlang verify"
61
+ "verify": "run recipe-tmlang verify",
62
+ "version": "node bin/cli.mjs --version"
60
63
  },
61
64
  "dependencies": {
62
- "@kjanat/dreamcli": "^2.4.1",
65
+ "@kjanat/dreamcli": "^2.5.0",
63
66
  "tree-sitter-recipe": "^0.3.1",
64
67
  "vscode-oniguruma": "^2.0.1",
65
- "vscode-textmate": "^9.2.0"
68
+ "vscode-textmate": "^9.3.2"
66
69
  },
67
70
  "devDependencies": {
68
71
  "@biomejs/biome": "^2.5.1",
@@ -0,0 +1,35 @@
1
+ // src/deps/oniguruma.ts
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import type { IOnigLib } from "./textmate.ts";
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const oniguruma: typeof import("vscode-oniguruma") = require("vscode-oniguruma");
8
+
9
+ let loadPromise: Promise<void> | undefined;
10
+
11
+ export function loadOniguruma(): Promise<void> {
12
+ loadPromise ??= (async () => {
13
+ const wasmPath = require.resolve("vscode-oniguruma/release/onig.wasm");
14
+ const wasm = await readFile(wasmPath);
15
+
16
+ await oniguruma.loadWASM(wasm);
17
+ })();
18
+
19
+ return loadPromise;
20
+ }
21
+
22
+ export async function createOnigLib(): Promise<IOnigLib> {
23
+ await loadOniguruma();
24
+
25
+ return {
26
+ createOnigScanner(patterns) {
27
+ return oniguruma.createOnigScanner(patterns);
28
+ },
29
+ createOnigString(string) {
30
+ return oniguruma.createOnigString(string);
31
+ },
32
+ };
33
+ }
34
+
35
+ export const { loadWASM, createOnigScanner, createOnigString, OnigScanner, OnigString } = oniguruma;
@@ -0,0 +1,8 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ export type { IOnigLib, StateStack } from "vscode-textmate";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ const textmate: typeof import("vscode-textmate") = require("vscode-textmate");
7
+
8
+ export const { parseRawGrammar, Registry, INITIAL } = textmate;
package/src/grammar.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  *
6
6
  * Scopes are standard TextMate names with a `.recipe` suffix so themes paint
7
7
  * recipe blocks without a custom theme shipment.
8
+ *
9
+ * @module recipe-tmlanguage/src/grammar
8
10
  */
9
11
  import { COUNTERS, NUMBER_WORDS, PERIOD_PLURALS, PERIODS } from "tree-sitter-recipe/grammar/dutch";
10
12
  import {
package/src/verifier.ts CHANGED
@@ -5,17 +5,15 @@
5
5
  *
6
6
  * No CLI concerns here; the caller supplies paths and decides how to present
7
7
  * the result (text table / JSON / exit code).
8
+ *
9
+ * @module recipe-tmlanguage/src/verifier
8
10
  */
9
11
  import { readdirSync, readFileSync } from "node:fs";
10
- import { createRequire } from "node:module";
11
12
  import { resolve } from "node:path";
12
13
 
13
- import type { StateStack } from "vscode-textmate";
14
-
15
- const require = createRequire(import.meta.url);
16
- const oniguruma: typeof import("vscode-oniguruma") = require("vscode-oniguruma");
17
- const textmate: typeof import("vscode-textmate") = require("vscode-textmate");
18
- const { parseRawGrammar, Registry } = textmate;
14
+ import { createOnigLib } from "#deps/oniguruma.ts";
15
+ import type { StateStack } from "#deps/textmate.ts";
16
+ import { parseRawGrammar, Registry } from "#deps/textmate.ts";
19
17
 
20
18
  // ── capture → scope mapping (inverse of grammar.ts SCOPE) ───────────────────
21
19
  // Fixtures speak tree-sitter capture names; the tokenizer speaks TextMate
@@ -57,7 +55,6 @@ export type VerifyResult = {
57
55
  export type VerifyOptions = {
58
56
  grammarPath: string;
59
57
  fixturesDir: string;
60
- onigWasmPath: string;
61
58
  };
62
59
 
63
60
  // ── fixture parser ──────────────────────────────────────────────────────────
@@ -105,13 +102,7 @@ function parseFixture(content: string, name: string): { source: string; asserts:
105
102
 
106
103
  // ── main ────────────────────────────────────────────────────────────────────
107
104
  export async function verify(opts: VerifyOptions): Promise<VerifyResult> {
108
- const wasmBin = readFileSync(opts.onigWasmPath);
109
- await oniguruma.loadWASM(wasmBin.buffer as ArrayBuffer);
110
-
111
- const onigLib = Promise.resolve({
112
- createOnigScanner: (patterns: string[]) => new oniguruma.OnigScanner(patterns),
113
- createOnigString: (s: string) => new oniguruma.OnigString(s),
114
- });
105
+ const onigLib = createOnigLib();
115
106
 
116
107
  const rawGrammar = parseRawGrammar(
117
108
  readFileSync(opts.grammarPath, "utf-8"),