diff --git a/examples/tsconfig.json b/examples/tsconfig.json index 78d9d703..192a09bb 100644 --- a/examples/tsconfig.json +++ b/examples/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { "rootDir": "./" }, "include": ["./**/*.ts"], - "extends": "../tsconfig.build.json", + "extends": "../tsconfig.build.json" } diff --git a/package.json b/package.json index cd0874bd..ad70b754 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:packages-esm": "cd test/esm && npm install && npm run test", "test:packages-cjs": "cd test/cjs && npm install && npm run test", "test:packages-install": "cd test && npx vite-node make-packages.ts", - "prettier": "prettier \"**/*.ts\" \"**/*.json\" --write", + "prettier": "prettier \"**/*.ts\" \"**/*.json\" --ignore-path .gitignore --write", "eslint": "eslint \"**/*.ts\" --ignore-pattern dist" }, "prettier": { diff --git a/packages/unified-latex-types/index.ts b/packages/unified-latex-types/index.ts index e3985eec..381f8745 100644 --- a/packages/unified-latex-types/index.ts +++ b/packages/unified-latex-types/index.ts @@ -2,6 +2,9 @@ export * from "./libs/ast-types"; export * from "./libs/type-guard"; export * from "./libs/info-specs"; +// Export something for importing packages +export default {}; + // NOTE: The docstring comment must be the last item in the index.ts file! /** * ## What is this? diff --git a/packages/unified-latex-types/libs/ast-types.ts b/packages/unified-latex-types/libs/ast-types.ts index a9757048..a7a34070 100644 --- a/packages/unified-latex-types/libs/ast-types.ts +++ b/packages/unified-latex-types/libs/ast-types.ts @@ -11,8 +11,9 @@ export interface GenericNode { // Abstract nodes interface BaseNode { type: string; - _renderInfo?: (MacroInfo["renderInfo"] | EnvInfo["renderInfo"]) & - Record; + _renderInfo?: (MacroInfo["renderInfo"] | EnvInfo["renderInfo"]) & { + defaultArg?: string; + } & Record; position?: { start: { offset: number; line: number; column: number }; end: { offset: number; line: number; column: number }; diff --git a/packages/unified-latex-types/package.json b/packages/unified-latex-types/package.json index a5999c86..014db447 100644 --- a/packages/unified-latex-types/package.json +++ b/packages/unified-latex-types/package.json @@ -13,12 +13,14 @@ "exports": { ".": { "prebuilt": "./dist/index.js", - "import": "./index.ts" + "import": "./index.ts", + "require": "./dist/index.cjs" }, "./*js": "./dist/*js", "./*": { "prebuilt": "./dist/*.js", - "import": "./*.ts" + "import": "./*.ts", + "require": "./dist/*.cjs" } }, "scripts": { diff --git a/packages/unified-latex-util-argspec/libs/argspec-parser.ts b/packages/unified-latex-util-argspec/libs/argspec-parser.ts index 8c3015c1..aed449d3 100644 --- a/packages/unified-latex-util-argspec/libs/argspec-parser.ts +++ b/packages/unified-latex-util-argspec/libs/argspec-parser.ts @@ -31,9 +31,11 @@ export function printRaw( } const decorators = getDecorators(node); - const defaultArg = (node as ArgSpec.DefaultArgument).defaultArg - ? printRaw((node as ArgSpec.DefaultArgument).defaultArg!) - : ""; + const defaultArg = printDefaultArg( + "defaultArg" in node ? node.defaultArg : undefined, + // `embellishment`s are the only spec that can have multiple default args + node.type === "embellishment" + ); let spec = decorators; const type = node.type; @@ -101,3 +103,19 @@ export function parse(str = ""): ArgSpec.Node[] { parseCache[str] = parseCache[str] || PegParser.parse(str); return parseCache[str]; } + +function printDefaultArg( + args: string | string[] | undefined, + multipleArgs: boolean +): string { + if (!args) { + return ""; + } + if (typeof args === "string") { + args = [args]; + } + if (!multipleArgs) { + return `{${args.join("")}}`; + } + return `{${args.map((a) => `{${a}}`).join("")}}`; +} diff --git a/packages/unified-latex-util-argspec/libs/argspec-types.ts b/packages/unified-latex-util-argspec/libs/argspec-types.ts index a6dfca7c..0be0549b 100644 --- a/packages/unified-latex-util-argspec/libs/argspec-types.ts +++ b/packages/unified-latex-util-argspec/libs/argspec-types.ts @@ -12,7 +12,10 @@ export interface LeadingWhitespace { noLeadingWhitespace: boolean | undefined; } export interface DefaultArgument { - defaultArg?: Group; + defaultArg?: string; +} +export interface DefaultArguments { + defaultArg?: string[]; } interface Verbatim extends Arg { type: "verbatim"; @@ -27,7 +30,7 @@ interface OptionalToken extends LeadingWhitespace, AstNode { type: "optionalToken"; token: string; } -export interface Embellishment extends DefaultArgument, AstNode { +export interface Embellishment extends DefaultArguments, AstNode { type: "embellishment"; embellishmentTokens: string[]; } diff --git a/packages/unified-latex-util-argspec/tests/__snapshots__/argspec-parser.test.ts.snap b/packages/unified-latex-util-argspec/tests/__snapshots__/argspec-parser.test.ts.snap index c0568d13..e95580fc 100644 --- a/packages/unified-latex-util-argspec/tests/__snapshots__/argspec-parser.test.ts.snap +++ b/packages/unified-latex-util-argspec/tests/__snapshots__/argspec-parser.test.ts.snap @@ -27,30 +27,7 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin [ { "closeBrace": "]", - "defaultArg": { - "content": [ - "n", - "e", - "s", - "t", - "e", - "d", - { - "content": [ - "d", - "e", - "f", - "a", - "u", - "l", - "t", - "s", - ], - "type": "group", - }, - ], - "type": "group", - }, + "defaultArg": "nested{defaults}", "openBrace": "[", "type": "optional", }, @@ -61,22 +38,7 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin [ { "closeBrace": "]", - "defaultArg": { - "content": [ - "s", - "o", - "m", - "e", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - ], - "type": "group", - }, + "defaultArg": "somedefault", "openBrace": "[", "type": "optional", }, @@ -93,6 +55,17 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin ] `; +exports[`unified-latex-util-argspec > parses xparse argument specification string "R\\a1{default}" 1`] = ` +[ + { + "closeBrace": "1", + "defaultArg": "default", + "openBrace": "\\\\a", + "type": "mandatory", + }, +] +`; + exports[`unified-latex-util-argspec > parses xparse argument specification string "d++ D--{def}" 1`] = ` [ { @@ -102,14 +75,7 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin }, { "closeBrace": "-", - "defaultArg": { - "content": [ - "d", - "e", - "f", - ], - "type": "group", - }, + "defaultArg": "def", "openBrace": "-", "type": "optional", }, @@ -219,6 +185,16 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin ] `; +exports[`unified-latex-util-argspec > parses xparse argument specification string "r\\abc\\d" 1`] = ` +[ + { + "closeBrace": "\\\\d", + "openBrace": "\\\\abc", + "type": "mandatory", + }, +] +`; + exports[`unified-latex-util-argspec > parses xparse argument specification string "s m" 1`] = ` [ { diff --git a/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts b/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts index f4e49953..7d02b666 100644 --- a/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts +++ b/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { VFile } from "unified-lint-rule/lib"; import util from "util"; -import * as argspecParser from ".."; +import * as argspecParser from "../index"; /* eslint-env jest */ @@ -18,6 +18,7 @@ function removeWhitespace(x: string) { describe("unified-latex-util-argspec", () => { let value: string | undefined; let file: VFile | undefined; + let ast: ReturnType; const SPEC_STRINGS = [ "", @@ -35,6 +36,8 @@ describe("unified-latex-util-argspec", () => { "u{xx;}", "u;", "u{ }", + "r\\abc\\d", + "R\\a1{default}", ]; for (const spec of SPEC_STRINGS) { @@ -45,17 +48,98 @@ describe("unified-latex-util-argspec", () => { }); } + it("Default args need not be enclosed in braces", () => { + ast = argspecParser.parse("Ox"); + expect(ast).toEqual([ + { + closeBrace: "]", + defaultArg: "x", + openBrace: "[", + type: "optional", + }, + ]); + + ast = argspecParser.parse("D(ab"); + expect(ast).toEqual([ + { + closeBrace: "a", + defaultArg: "b", + openBrace: "(", + type: "optional", + }, + ]); + }); + + it("Embellishment tokens can be single characters specified without a group", () => { + ast = argspecParser.parse("e^"); + expect(ast).toEqual([ + { + type: "embellishment", + embellishmentTokens: ["^"], + }, + ]); + + // Macros count as a single token + ast = argspecParser.parse("e\\foo"); + expect(ast).toEqual([ + { + type: "embellishment", + embellishmentTokens: ["\\foo"], + }, + ]); + + ast = argspecParser.parse("Ex{}"); + expect(ast).toEqual([ + { + type: "embellishment", + embellishmentTokens: ["x"], + defaultArg: [], + }, + ]); + }); + + it("Embellishment tokens ignore whitespace", () => { + ast = argspecParser.parse("e { ^ }"); + expect(ast).toEqual([ + { + type: "embellishment", + embellishmentTokens: ["^"], + }, + ]); + }); + + it("Embellishment default args can be a mix of tokens and groups", () => { + ast = argspecParser.parse("E{\\token^}{{D1}2}"); + expect(ast).toEqual([ + { + defaultArg: ["D1", "2"], + embellishmentTokens: ["\\token", "^"], + type: "embellishment", + }, + ]); + }); + it("Embellishments always return a string", () => { - let ast = argspecParser.parse("e{{{x}}y{z}}"); + ast = argspecParser.parse("e{{x}y{z}}"); expect(ast).toEqual([ { type: "embellishment", embellishmentTokens: ["x", "y", "z"] }, ]); - ast = argspecParser.parse("E{{{x}}y{z}}{}"); + ast = argspecParser.parse("E{{x}y{z}}{}"); + expect(ast).toEqual([ + { + type: "embellishment", + embellishmentTokens: ["x", "y", "z"], + defaultArg: [], + }, + ]); + }); + it("Embellishments keep default args", () => { + ast = argspecParser.parse("E{{x}y{z}}{{One}{Two}{Three}}"); expect(ast).toEqual([ { type: "embellishment", embellishmentTokens: ["x", "y", "z"], - defaultArg: { type: "group", content: [] }, + defaultArg: ["One", "Two", "Three"], }, ]); }); diff --git a/packages/unified-latex-util-arguments/libs/gobble-arguments.ts b/packages/unified-latex-util-arguments/libs/gobble-arguments.ts index a68a2655..94664ec2 100644 --- a/packages/unified-latex-util-arguments/libs/gobble-arguments.ts +++ b/packages/unified-latex-util-arguments/libs/gobble-arguments.ts @@ -6,6 +6,7 @@ import { parse as parseArgspec, } from "@unified-latex/unified-latex-util-argspec"; import { gobbleSingleArgument } from "./gobble-single-argument"; +import { updateRenderInfo } from "@unified-latex/unified-latex-util-render-info"; /** * Gobbles an argument of whose type is specified @@ -38,7 +39,12 @@ export function gobbleArguments( // we need to keep gobbling arguments until we've got them all. const remainingTokens = new Set(spec.embellishmentTokens); const argForToken = Object.fromEntries( - spec.embellishmentTokens.map((t) => [t, emptyArg()]) + spec.embellishmentTokens.map((t, i) => { + // For empty arguments, we also store their default. + const defaultArg = + "defaultArg" in spec ? spec.defaultArg?.[i] : undefined; + return [t, emptyArg(defaultArg)]; + }) ); let { argument, nodesRemoved: removed } = gobbleSingleArgument( @@ -66,7 +72,10 @@ export function gobbleArguments( spec, startPos ); - args.push(argument || emptyArg()); + // For empty arguments, we also store their default. + const defaultArg = + "defaultArg" in spec ? spec.defaultArg : undefined; + args.push(argument || emptyArg(defaultArg)); nodesRemoved += removed; } } @@ -87,6 +96,10 @@ function embellishmentSpec(tokens: Set): ArgSpec.Embellishment { /** * Create an empty argument. */ -function emptyArg(): Ast.Argument { - return arg([], { openMark: "", closeMark: "" }); +function emptyArg(defaultArg?: string): Ast.Argument { + const ret = arg([], { openMark: "", closeMark: "" }); + if (defaultArg != null) { + updateRenderInfo(ret, { defaultArg }); + } + return ret; } diff --git a/packages/unified-latex-util-arguments/tests/get-args-content.test.ts b/packages/unified-latex-util-arguments/tests/get-args-content.test.ts index 2a6de7d8..d06cda3a 100644 --- a/packages/unified-latex-util-arguments/tests/get-args-content.test.ts +++ b/packages/unified-latex-util-arguments/tests/get-args-content.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from "vitest"; import util from "util"; import * as Ast from "@unified-latex/unified-latex-types"; import { attachMacroArgs } from "../libs/attach-arguments"; diff --git a/packages/unified-latex-util-arguments/tests/unifiex-latex-attach-arguments.test.ts b/packages/unified-latex-util-arguments/tests/unifiex-latex-attach-arguments.test.ts index 64bf5e6c..4bf11989 100644 --- a/packages/unified-latex-util-arguments/tests/unifiex-latex-attach-arguments.test.ts +++ b/packages/unified-latex-util-arguments/tests/unifiex-latex-attach-arguments.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from "vitest"; import { unified } from "unified"; import { VFile } from "unified-lint-rule/lib"; import util from "util"; diff --git a/packages/unified-latex-util-pegjs/grammars/xparse-argspec.pegjs b/packages/unified-latex-util-pegjs/grammars/xparse-argspec.pegjs index a2e6c403..b332d12f 100644 --- a/packages/unified-latex-util-pegjs/grammars/xparse-argspec.pegjs +++ b/packages/unified-latex-util-pegjs/grammars/xparse-argspec.pegjs @@ -11,12 +11,16 @@ const computedOptions = DEFAULT_OPTIONS[type] || {}; return { type, ...computedOptions, ...options }; } + /** - * Recursively return the content of a group until there are no more groups + * Convert a group to a string, preserving {} braces. */ - function groupContent(node) { + function groupToStr(node) { + if (typeof node !== "object" || !node) { + return node; + } if (node.type === "group") { - return node.content.map(groupContent).flat(); + return `{${node.content.map(groupToStr).join("")}}`; } return node; } @@ -56,7 +60,7 @@ optional } optional_delimited - = "D" braceSpec:brace_spec defaultArg:braced_group { + = "D" braceSpec:brace_spec defaultArg:arg { return createNode("optional", { ...braceSpec, defaultArg }); } / "d" braceSpec:brace_spec { return createNode("optional", braceSpec); } @@ -64,20 +68,18 @@ optional_delimited optional_star = "s" { return createNode("optionalStar"); } optional_standard - = "O" g:braced_group { return createNode("optional", { defaultArg: g }); } + = "O" whitespace g:arg { return createNode("optional", { defaultArg: g }); } / "o" { return createNode("optional"); } optional_embellishment - = "e" args:braced_group { - // Embellishments ignore groups around tokens. E.g. `e{x}` and `e{{x}}` - // are the same. + = "e" whitespace args:args { return createNode("embellishment", { - embellishmentTokens: args.content.map(groupContent).flat(), + embellishmentTokens: args, }); } - / "E" args:braced_group g:braced_group { + / "E" whitespace args:args whitespace g:args { return createNode("embellishment", { - embellishmentTokens: args.content.map(groupContent).flat(), + embellishmentTokens: args, defaultArg: g, }); } @@ -87,7 +89,7 @@ optional_token // Required arguments required - = "R" braceSpec:brace_spec defaultArg:braced_group { + = "R" braceSpec:brace_spec defaultArg:arg { return createNode("mandatory", { ...braceSpec, defaultArg }); } / "r" braceSpec:brace_spec { return createNode("mandatory", braceSpec); } @@ -98,6 +100,10 @@ until return createNode("until", { stopTokens }); } +// +// HELPER RULES +// + until_stop_token = ![{ ] x:. { return [x]; } / g:braced_group { return g.content; } @@ -107,18 +113,42 @@ mandatory = "m" { return createNode("mandatory"); } // Used to specify a pair of opening and closing braces brace_spec - = openBrace:$(!whitespace_token .)? closeBrace:$(!whitespace_token .)? { + = openBrace:$(!whitespace_token (macro / .))? + closeBrace:$(!whitespace_token (macro / .))? { return { openBrace, closeBrace }; } -braced_group - = "{" content:($(!"}" !braced_group .) / braced_group)* "}" { - return { type: "group", content: content }; +// A `default_arg` is a braced group, but its content will be processed as a string (or array of strings). +// For example `{foo}` -> `["foo"]` and `{{foo}{bar}}` -> `["foo", "bar"]` +arg + = token + / g:braced_group { return g.content.map(groupToStr).join(""); } + +args + = t:token { return [t]; } + / "{" args:(arg / whitespace_token)* "}" { + return args.filter((a) => !a.match(/^\s*$/)); } +braced_group + = "{" + content:( + $(!"}" !braced_group (token / whitespace_token)) + / braced_group + )* + "}" { return { type: "group", content: content }; } + whitespace = whitespace_token* { return ""; } whitespace_token = " " / "\n" / "\r" + +macro + = $("\\" [a-zA-Z]+) + / $("\\" ![a-zA-Z] .) + +token + = macro + / ![{}] !whitespace_token @. diff --git a/packages/unified-latex-util-pegjs/package.json b/packages/unified-latex-util-pegjs/package.json index 6cc57c65..5df9d25f 100644 --- a/packages/unified-latex-util-pegjs/package.json +++ b/packages/unified-latex-util-pegjs/package.json @@ -67,6 +67,7 @@ "index.ts", "libs/**/*.ts", "libs/**/*.json", + "grammars/**/*.pegjs", "tsconfig.json", "vite.config.ts" ], @@ -83,6 +84,7 @@ "index.ts", "libs/**/*.ts", "libs/**/*.json", + "grammars/**/*.pegjs", "tsconfig.json", "vite.config.ts" ], diff --git a/packages/unified-latex-util-render-info/index.ts b/packages/unified-latex-util-render-info/index.ts index 97737676..93e324f9 100644 --- a/packages/unified-latex-util-render-info/index.ts +++ b/packages/unified-latex-util-render-info/index.ts @@ -9,7 +9,7 @@ import { visit } from "@unified-latex/unified-latex-util-visit"; * *This operation mutates `node`* */ export function updateRenderInfo( - node: Ast.Node, + node: Ast.Node | Ast.Argument, renderInfo: object | null | undefined ) { if (renderInfo != null) { diff --git a/tsconfig.test.json b/tsconfig.test.json index c0b42a32..69bc32e1 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,6 +1,11 @@ { "extends": "./tsconfig.build.json", - "include": ["**/*.ts"], + "include": [ + "**/*.ts", + // I have no idea why this needs to be included specifically + // and none of the other packages do. + "packages/unified-latex-types/dist/index.js" + ], "exclude": [ "**/*.d.ts", "node_modules", @@ -12,7 +17,7 @@ "compilerOptions": { "rootDir": "./packages", "paths": { - "@unified-latex/*": ["./packages/*/dist"] + "@unified-latex/*": ["./packages/*"] }, "types": ["vitest/globals"] }