From c8fe6150ff8fbbb8a76b9977620bb709adcdd15a Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 12:04:13 -0500 Subject: [PATCH 1/9] Add examples --- examples/README.md | 11 ++ examples/count-macros.ts | 59 +++++++++ examples/custom-macros.ts | 47 ++++++++ examples/expanding-or-replacing-macros.ts | 139 ++++++++++++++++++++++ examples/ignore-defaults.ts | 59 +++++++++ examples/using-unified.ts | 127 ++++++++++++++++++++ 6 files changed, 442 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/count-macros.ts create mode 100644 examples/custom-macros.ts create mode 100644 examples/expanding-or-replacing-macros.ts create mode 100644 examples/ignore-defaults.ts create mode 100644 examples/using-unified.ts diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..4fe6f538 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,11 @@ +# Examples + +Here you can find annotated examples of how to use `unified-latex` to accomplish common tasks. The examples can be run +with `npx vite-node `. + +- `count-macros.ts` - goes through the basics of parsing source to a `unified-latex` AST and walking the tree to gather + information about its contents. +- `custom-macros.ts` - shows how to add your own macros to the parse process. +- `ignore-defaults.ts` - shows how to parse a string without including any default packages (not even LaTeX2e standard ones). +- `expanding-or-replacing-macros.ts` - shows how to expand or replace macros present in an AST. +- `using-unified.ts` - shows how to use `unified` in combination with `unified-latex` to build a processing pipeline. diff --git a/examples/count-macros.ts b/examples/count-macros.ts new file mode 100644 index 00000000..87a97509 --- /dev/null +++ b/examples/count-macros.ts @@ -0,0 +1,59 @@ +/** + * This example shows how to count macros in a tex string and print out statistics. + */ +import { getParser } from "@unified-latex/unified-latex-util-parse"; +import { visit } from "@unified-latex/unified-latex-util-visit"; +import { anyMacro } from "@unified-latex/unified-latex-util-match"; +import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; + +const TEX_SOURCE = String.raw` +This is \textbf{an} example of a \LaTeX{} document with \textit{some} macros. +\[ + e^x = \sum_{n=0}^{\infty} \frac{x^n}{n!}. +\] +What an \textit{\textbf{amazing}} formula! +`; + +// The quickest way to get started is to create a parser with `getParser`. +const parser = getParser(); +const ast = parser.parse(TEX_SOURCE); + +const macroInfo: Record = {}; +const mathMacros: string[] = []; + +visit(ast, (node, info) => { + if (!anyMacro(node)) { + return; + } + // If we're here, we are a macro node. + macroInfo[node.content] = macroInfo[node.content] || []; + // `printRaw` will print `node` (and its content) without any formatting. + macroInfo[node.content].push(printRaw(node)); + + // `info.context` contains information about where in the parse tree we currently are. + if (info.context.inMathMode) { + // Save just the macro "name". + mathMacros.push(node.content); + } +}); + +// Prints +// +// ``` +// Macro statistics: +// +// All macros: { +// textbf: [ '\\textbf{an}', '\\textbf{amazing}' ], +// LaTeX: [ '\\LaTeX' ], +// textit: [ '\\textit{some}', '\\textit{\\textbf{amazing}}' ], +// '^': [ '^{x}', '^{\\infty}', '^{n}' ], +// sum: [ '\\sum' ], +// _: [ '_{n=0}' ], +// infty: [ '\\infty' ], +// frac: [ '\\frac{x^{n}}{n!}' ] +// } +// Math mode macros: [ '^', 'sum', '_', '^', 'infty', 'frac', '^' ] +// ``` +console.log("Macro statistics:\n"); +console.log("All macros:", macroInfo); +console.log("Math mode macros:", mathMacros); diff --git a/examples/custom-macros.ts b/examples/custom-macros.ts new file mode 100644 index 00000000..b48bd028 --- /dev/null +++ b/examples/custom-macros.ts @@ -0,0 +1,47 @@ +/** + * This example shows how include your own macros for parsing. + */ +import { unified } from "unified"; +import { unifiedLatexFromString } from "@unified-latex/unified-latex-util-parse"; +import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; + +const TEX_SOURCE = String.raw` +My \textbf{custom} \abc{macro}. +`; + +// The default parser for `unified-latex` recognizes macros coming from several packages (those listed in `unified-latex-ctan/package`), +// but you may want to add your own macros to the parsing pipeline. + +// Parser with defaults +const processor1 = unified().use(unifiedLatexFromString); +const ast1 = processor1.parse(TEX_SOURCE); +// Prints `\textbf{custom} \abc`. Notice how the argument of `\xxx` is not included. +console.log(printRaw(ast1.content[2]), printRaw(ast1.content[4])); + +/** + * Adding a custom macro specification + */ + +// We can pass in custom macro (and environment) specifications to the parser. +const processor2 = unified().use(unifiedLatexFromString, { + // We define the macro `\abc` to take one mandatory argument. The `signature` is specified + // using the syntax of the `xparse` package: https://ctan.org/pkg/xparse + macros: { abc: { signature: "m" } }, +}); +const ast2 = processor2.parse(TEX_SOURCE); +// Prints `\textbf{custom} \abc{macro}`. Notice how the argument of `\abc` is included. +console.log(printRaw(ast2.content[2]), printRaw(ast2.content[4])); + +// Any specification you add take precedence over the built in ones. +const processor3 = unified().use(unifiedLatexFromString, { + macros: { textbf: { signature: "" }, abc: { signature: "m" } }, +}); +const ast3 = processor3.parse(TEX_SOURCE); +// Prints `\textbf \abc{macro}`. +// Notice how the argument of `\textbf` is not included. Te index of `\abc` also changed +// because there are additional nodes (since `\textbf` didn't take its argument). +console.log( + printRaw(ast3.content[2]), + printRaw(ast3.content[5]), + printRaw(ast3.content[4]) +); diff --git a/examples/expanding-or-replacing-macros.ts b/examples/expanding-or-replacing-macros.ts new file mode 100644 index 00000000..b5814902 --- /dev/null +++ b/examples/expanding-or-replacing-macros.ts @@ -0,0 +1,139 @@ +/** + * This example shows how expand or replace macros. + */ +import { unified, Plugin } from "unified"; +import { unifiedLatexFromString } from "@unified-latex/unified-latex-util-parse"; +import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; +import { replaceNode } from "@unified-latex/unified-latex-util-replace"; +import { match } from "@unified-latex/unified-latex-util-match"; +import { visit } from "@unified-latex/unified-latex-util-visit"; +import { + expandMacrosExcludingDefinitions, + listNewcommands, +} from "@unified-latex/unified-latex-util-macros"; +import { attachMacroArgs } from "@unified-latex/unified-latex-util-arguments"; +import * as Ast from "@unified-latex/unified-latex-types"; +import { unifiedLatexStringCompiler } from "@unified-latex/unified-latex-util-to-string"; + +const TEX_SOURCE = String.raw` +\newcommand{\abc}[1]{ABC} +My \textbf{custom} \abc{macro}. +`; + +/** + * Replacing macros with `replaceNode` + */ + +// A common task is to replace a macro with some other content. One way to do this is with `replaceNode`. + +const processor1 = unified().use(unifiedLatexFromString, { + // Parse `\abc` as taking a mandatory argument. + macros: { abc: { signature: "m" } }, +}); +const ast1 = processor1.parse(TEX_SOURCE); +// Prints: `\newcommand{\abc}[1]{ABC} My \textbf{custom} \abc{macro}.` +console.log(printRaw(ast1)); + +replaceNode(ast1, (node) => { + if (match.macro(node, "newcommand")) { + // Remove any `\newcommand` macros from the tree. + return null; + } + if (match.macro(node, "abc")) { + // Replace `\abc` with `ABC`. + return { type: "string", content: "ABC" }; + } +}); +// Prints: ` My \textbf{custom} ABC.` +console.log(printRaw(ast1)); + +/** + * Replacing macros only in math mode + */ + +// Using the `info` object, you can get extra information about context before replacing a node. +const ast2 = processor1.parse(String.raw`\abc{fun} $x=\abc{times}$`); +replaceNode(ast2, (node, info) => { + if (info.context.inMathMode && match.macro(node, "abc")) { + // Replace `\abc` with `ABC` only in math mode. + return { type: "string", content: "ABC" }; + } +}); +// Prints: `\abc{fun} $x=ABC$` +console.log(printRaw(ast2)); + +/** + * Replacing during `visit` + */ + +// `replaceNode` is really just a wrapper around `visit`. You can use `visit` directly to replace nodes. +const ast3 = processor1.parse(TEX_SOURCE); +visit(ast3, (node, info) => { + if (match.macro(node, "newcommand")) { + // Replace `\newcommand` with the empty string. + // `replaceNode` actually _removes_ nodes from the tree, which we could do too, + // but it would involve quite a bit more work. + + // We are directly manipulating a node and changing its type, + // TypeScript doesn't like this, so we have to do some casting. + node = node as unknown as Ast.String; + node.type = "string"; + node.content = ""; + } + if (match.macro(node, "abc")) { + // Replace `\abc` with `ABC`. + + // We are directly manipulating a node and changing its type, + // TypeScript doesn't like this, so we have to do some casting. + node = node as unknown as Ast.String; + node.type = "string"; + node.content = "ABC"; + } +}); +// Prints: ` My \textbf{custom} ABC.` +console.log(printRaw(ast3)); + +/** + * Expanding `\newcommand` directly + */ + +// We can expand macros defined via `\newcommand`, `\NewDocumentCommand`, etc. by creating a plugin. + +/** + * Plugin that expands the specified macros by name. These macros must be defined in the document via + * `\newcommand...` or equivalent. + */ +export const expandDocumentMacrosPlugin: Plugin = + function () { + return (tree) => { + const newcommands = listNewcommands(tree); + + const macroInfo = Object.fromEntries( + newcommands.map((m) => [m.name, { signature: m.signature }]) + ); + // We need to attach the arguments to each macro before we process it! + attachMacroArgs(tree, macroInfo); + // We want to expand all macros, except ones mentioned in actual `\newcommand` commands. + expandMacrosExcludingDefinitions(tree, newcommands); + + // Finally, let's remove the `\newcommand`s from the tree. + // Our document could have used `\newcommand` or `\NewDocumentCommand`, etc. We will remove + // all of these. + const newcommandsUsed = Object.fromEntries( + newcommands.map((x) => [x.definition.content, true]) + ); + replaceNode(tree, (node) => { + if (match.anyMacro(node) && newcommandsUsed[node.content]) { + return null; + } + }); + }; + }; + +const processor4 = unified() + .use(unifiedLatexFromString) + .use(expandDocumentMacrosPlugin) + .use(unifiedLatexStringCompiler, { pretty: true }); +const processed4 = processor4.processSync(TEX_SOURCE); +// Prints: ` My \textbf{custom} ABC.` +console.log(String(processed4)); diff --git a/examples/ignore-defaults.ts b/examples/ignore-defaults.ts new file mode 100644 index 00000000..f2615433 --- /dev/null +++ b/examples/ignore-defaults.ts @@ -0,0 +1,59 @@ +/** + * This example shows how ignore all default parsing and use exclusively custom macros. + */ +import { unified } from "unified"; +import { + unifiedLatexAstComplier, + unifiedLatexFromStringMinimal, + unifiedLatexProcessMacrosAndEnvironmentsWithMathReparse, +} from "@unified-latex/unified-latex-util-parse"; +import { printRaw } from "@unified-latex/unified-latex-util-print-raw"; +import { macros as xcolorMacros } from "@unified-latex/unified-latex-ctan/package/xcolor"; +import { Root } from "@unified-latex/unified-latex-types"; + +const TEX_SOURCE = String.raw` +My \textbf{custom} \abc{macro}. +`; + +// The default parser for `unified-latex` recognizes macros coming from several packages (those listed in `unified-latex-ctan/package`), +// but your use case may involve only custom macros (or you may want the speed boost of not processing many macros). +// Parsing with `unifiedLatexFromStringMinimal` parses a string into its "most abstract" form, where no macro arguments are attached. +// This means that a string like `\textbf{foo}` will be parsed as the macro `\textbf` followed by the group containing `foo`. + +// Parser with defaults +const processor1 = unified().use(unifiedLatexFromStringMinimal); +const ast1 = processor1.parse(TEX_SOURCE); +// Prints `\textbf \abc`. Notice how `\xxx` is at position 3 (instead of 2 like in `custom-macros.ts`). +// This is because `unifiedLatexFromStringMinimal` doesn't trim any leading or trailing whitespace. +console.log(printRaw(ast1.content[3]), printRaw(ast1.content[6])); + +// You may want to process a string as if it were in math mode. This can be done by setting `mode: "math"` in the parser options. +const processor2 = unified().use(unifiedLatexFromStringMinimal, { + mode: "math", +}); +const ast2 = processor2.parse(`x^2`); +// Prints `^`. +console.log(printRaw(ast2.content[1])); + +/** + * Using specific packages + */ + +// We can build a parsing pipeline that only recognizes macros from specific packages. +const processor3 = unified() + .use(unifiedLatexFromStringMinimal) + // We could manually use `attachMacroArgs` and write a custom plugin, + // but the `unifiedLatexProcessMacrosAndEnvironmentsWithMathReparse` is already here for us. + // It will also reparse the content of custom "math" environments so their content is in math mode. + // (Ths is how `\begin{equation}...\end{equation}` end up with their contents parsed in math mode.) + .use(unifiedLatexProcessMacrosAndEnvironmentsWithMathReparse, { + // Only process macros from the `xcolor` package. + macros: xcolorMacros, + environments: {}, + }) + .use(unifiedLatexAstComplier); +const processed3 = processor3.processSync(String.raw`\color{blue}\textbf{foo}`) + .result as Root; +// Print the parsed AST with a space between each node. +// Prints `\color{blue} \textbf {foo}`. +console.log(processed3.content.map((c) => printRaw(c)).join(" ")); diff --git a/examples/using-unified.ts b/examples/using-unified.ts new file mode 100644 index 00000000..c8afc72f --- /dev/null +++ b/examples/using-unified.ts @@ -0,0 +1,127 @@ +/** + * This example shows how to build a parser with the `unified` library. + */ +import { unified } from "unified"; +import { + unifiedLatexAstComplier, + unifiedLatexFromString, +} from "@unified-latex/unified-latex-util-parse"; +import { unifiedLatexStringCompiler } from "@unified-latex/unified-latex-util-to-string"; +import { visit } from "@unified-latex/unified-latex-util-visit"; +import { match } from "@unified-latex/unified-latex-util-match"; +import type { Root } from "@unified-latex/unified-latex-types"; + +const TEX_SOURCE = String.raw` +This is \textbf{an} example of a \LaTeX{} document with \textit{some} macros. +\[ + e^x = \sum_{n=0}^{\infty} \frac{x^n}{n!}. +\] +What an \textit{\textbf{amazing}} formula! +`; + +// The `unified` framework runs in three steps: +// 1. Parse a string to an AST. +// 2. Transform the AST. +// 3. Compile the AST to a final format (normally a string). +// We can interface with `unified` at any of these steps. + +/** + * Basic Parsing + */ + +// This processor can only parse (step 1). +const processor1 = unified().use(unifiedLatexFromString); +const ast1 = processor1.parse(TEX_SOURCE); +// Prints `{ type: "macro", content: "textbf", ... }` +console.log(ast1.content[4]); + +/** + * Parsing and Transforming + */ + +// We can add a transformer to the processor to transform the AST (step 2). +// These take the form of `unified` plugins. +const processor2 = unified() + .use(unifiedLatexFromString) + .use(function transformer() { + return (ast) => { + visit(ast, (node) => { + if (match.macro(node, "textbf")) { + // Change all `\textbf` macros into `\emph` macros. + node.content = "emph"; + } + }); + }; + }); +// To use the transformer, `processor2` must be called in two steps. +const ast2 = processor2.parse(TEX_SOURCE); +// `processor2.run` executes all the transformer plugins. These operations mutate +// the source. +processor2.run(ast2); +// Prints `{ type: "macro", content: "emph", ... }` +console.log(ast2.content[4]); + +/** + * Parsing, Transforming, and Stringifying + */ + +// If we want unified to run all steps together, we need to provide a _compiler_ plugin. +const processor3 = unified() + .use(unifiedLatexFromString) + // Same transformer as before. + .use(function transformer() { + return (ast) => { + visit(ast, (node) => { + if (match.macro(node, "textbf")) { + // Change all `\textbf` macros into `\emph` macros. + node.content = "emph"; + } + }); + }; + }) + // When we turn the LaTeX into a string, pretty-print it. + .use(unifiedLatexStringCompiler, { pretty: true }); +const processed3 = processor3.processSync(TEX_SOURCE); +// `processSync` returns a `VFile` object which contains the output string along with +// additional information. Calling `String(...)` ont the `VFile` is the preferred way +// to get the output string. +// +// Prints: +// ``` +// This is \emph{an} example of a \LaTeX{} document with \textit{some} macros. +// \[ +// e^{x} = \sum_{n=0}^{\infty}\frac{x^{n}}{n!}. +// \] +// What an \textit{\emph{amazing}} formula! +// ``` +console.log(String(processed3)); + +/** + * Parsing, Transforming, and _not_ Stringifying + */ + +// Sometimes you wan to use the convenience of `processSync` without the overhead. +// Since `processSync` only runs if all three steps are present in your processor, +// `unified-latex` provides a cheat: `unifiedLatexAstComplier` is a compiler plugin +// that doesn't do anything--just returns the AST as it was. +const processor4 = unified() + .use(unifiedLatexFromString) + // Same transformer as before. + .use(function transformer() { + return (ast) => { + visit(ast, (node) => { + if (match.macro(node, "textbf")) { + // Change all `\textbf` macros into `\emph` macros. + node.content = "emph"; + } + }); + }; + }) + // This processor won't touch the AST + .use(unifiedLatexAstComplier); +const processed4 = processor4.processSync(TEX_SOURCE); +// The AST is stored in the `result` prop of the `VFile`. +// Unfortunately, type information is lost here, but we know it's an `Ast.Root`. +const ast4 = processed4.result as Root; +// Prints `{ type: "macro", content: "emph", ... }` +console.log(ast4.content[4]); From c68457233d0f648a5ca76d5dedcc739ec75f35ce Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 12:04:41 -0500 Subject: [PATCH 2/9] Prettier --- examples/count-macros.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/count-macros.ts b/examples/count-macros.ts index 87a97509..b9231469 100644 --- a/examples/count-macros.ts +++ b/examples/count-macros.ts @@ -41,7 +41,7 @@ visit(ast, (node, info) => { // // ``` // Macro statistics: -// +// // All macros: { // textbf: [ '\\textbf{an}', '\\textbf{amazing}' ], // LaTeX: [ '\\LaTeX' ], From 93e3fef280b4071bf9bcc0f252d084dc68a51312 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 13:13:51 -0500 Subject: [PATCH 3/9] Test examples --- examples/tsconfig.json | 5 + .../__snapshots__/doc-examples.test.ts.snap | 114 ++++++++++++++++++ .../tests/doc-examples.test.ts | 69 +++++++++++ packages/unified-latex-types/package.json | 6 +- tsconfig.test.json | 9 +- 5 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 examples/tsconfig.json create mode 100644 packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap create mode 100644 packages/unified-latex-cli/tests/doc-examples.test.ts diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 00000000..78d9d703 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { "rootDir": "./" }, + "include": ["./**/*.ts"], + "extends": "../tsconfig.build.json", +} diff --git a/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap b/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap new file mode 100644 index 00000000..ca93c6c4 --- /dev/null +++ b/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap @@ -0,0 +1,114 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`unified-latex-doc-examples > example count-macros.ts 1`] = ` +"Building in mode: development. + +Macro statistics: + +All macros: { + textbf: [ '\\\\\\\\textbf{an}', '\\\\\\\\textbf{amazing}' ], + LaTeX: [ '\\\\\\\\LaTeX' ], + textit: [ '\\\\\\\\textit{some}', '\\\\\\\\textit{\\\\\\\\textbf{amazing}}' ], + '^': [ '^{x}', '^{\\\\\\\\infty}', '^{n}' ], + sum: [ '\\\\\\\\sum' ], + _: [ '_{n=0}' ], + infty: [ '\\\\\\\\infty' ], + frac: [ '\\\\\\\\frac{x^{n}}{n!}' ] +} +Math mode macros: [ '^', 'sum', '_', '^', 'infty', 'frac', '^' ] +" +`; + +exports[`unified-latex-doc-examples > example custom-macros.ts 1`] = ` +"Building in mode: development. + +\\\\textbf{custom} \\\\abc +\\\\textbf{custom} \\\\abc{macro} +\\\\textbf \\\\abc{macro} +" +`; + +exports[`unified-latex-doc-examples > example expanding-or-replacing-macros.ts 1`] = ` +"Building in mode: development. + +\\\\newcommand{\\\\abc}[1]{ABC} My \\\\textbf{custom} \\\\abc{macro}. + My \\\\textbf{custom} ABC. +\\\\abc{fun} $x=ABC$ + My \\\\textbf{custom} ABC. + My \\\\textbf{custom} ABC. +" +`; + +exports[`unified-latex-doc-examples > example ignore-defaults.ts 1`] = ` +"Building in mode: development. + +\\\\textbf \\\\abc +^ +\\\\color{blue} \\\\textbf {foo} +" +`; + +exports[`unified-latex-doc-examples > example using-unified.ts 1`] = ` +"Building in mode: development. + +{ + type: 'macro', + content: 'textbf', + position: { + source: undefined, + start: { offset: 9, line: 2, column: 9 }, + end: { offset: 16, line: 2, column: 16 } + }, + _renderInfo: { inParMode: true }, + args: [ + { + type: 'argument', + content: [Array], + openMark: '{', + closeMark: '}' + } + ] +} +{ + type: 'macro', + content: 'emph', + position: { + source: undefined, + start: { offset: 9, line: 2, column: 9 }, + end: { offset: 16, line: 2, column: 16 } + }, + _renderInfo: { inParMode: true }, + args: [ + { + type: 'argument', + content: [Array], + openMark: '{', + closeMark: '}' + } + ] +} +This is \\\\emph{an} example of a \\\\LaTeX{} document with \\\\textit{some} macros. +\\\\[ + e^{x} = \\\\sum_{n=0}^{\\\\infty}\\\\frac{x^{n}}{n!}. +\\\\] +What an \\\\textit{\\\\emph{amazing}} formula! +{ + type: 'macro', + content: 'emph', + position: { + source: undefined, + start: { offset: 9, line: 2, column: 9 }, + end: { offset: 16, line: 2, column: 16 } + }, + _renderInfo: { inParMode: true }, + args: [ + { + type: 'argument', + content: [Array], + openMark: '{', + closeMark: '}' + } + ] +} +" +`; diff --git a/packages/unified-latex-cli/tests/doc-examples.test.ts b/packages/unified-latex-cli/tests/doc-examples.test.ts new file mode 100644 index 00000000..be0476f6 --- /dev/null +++ b/packages/unified-latex-cli/tests/doc-examples.test.ts @@ -0,0 +1,69 @@ +/** + * Rus the files in `../../examples`. This test need not be in this directory, but it was a convenient place to put it (2024-02-19). + */ +import { describe, it, expect } from "vitest"; +import util from "util"; +import "../../test-common"; +import { exec as _exec } from "node:child_process"; +import * as path from "node:path"; +import spawn from "cross-spawn"; +import glob from "glob"; + +/* eslint-env jest */ + +// Make console.log pretty-print by default +const origLog = console.log; +console.log = (...args) => { + origLog(...args.map((x) => util.inspect(x, false, 10, true))); +}; + +describe( + "unified-latex-doc-examples", + () => { + const examplesPath = path.resolve(__dirname, "../../../examples"); + const exampleFiles = glob.sync(`${examplesPath}/*.ts`); + for (const exampleFile of exampleFiles) { + it(`example ${exampleFile.split("/").pop()}`, async () => { + const stdout = await executeCommand(`npx`, [ + "vite-node", + exampleFile, + ]); + expect(stdout).toMatchSnapshot(); + }); + } + }, + { + timeout: 60 * 1000, + } +); + +/** + * Run commands with arguments using "cross-spawn", which correctly escapes arguments + * so that end results are the same across different shells. + */ +function executeCommand( + executablePath: string, + args: string[] +): Promise { + return new Promise((resolve, reject) => { + const childProcess = spawn(executablePath, args, { stdio: "pipe" }); + + let stdoutData = ""; + + childProcess.stdout!.on("data", (data) => { + stdoutData += data.toString(); + }); + + childProcess.on("error", (err) => { + reject(err); + }); + + childProcess.on("close", (code) => { + if (code === 0) { + resolve(stdoutData); + } else { + reject(new Error(`Child process exited with code ${code}`)); + } + }); + }); +} diff --git a/packages/unified-latex-types/package.json b/packages/unified-latex-types/package.json index 8254b97b..a5999c86 100644 --- a/packages/unified-latex-types/package.json +++ b/packages/unified-latex-types/package.json @@ -12,11 +12,13 @@ ], "exports": { ".": { - "default": "./index.ts" + "prebuilt": "./dist/index.js", + "import": "./index.ts" }, "./*js": "./dist/*js", "./*": { - "default": "./dist/*.ts" + "prebuilt": "./dist/*.js", + "import": "./*.ts" } }, "scripts": { diff --git a/tsconfig.test.json b/tsconfig.test.json index 92e95d24..c0b42a32 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,7 +1,14 @@ { "extends": "./tsconfig.build.json", "include": ["**/*.ts"], - "exclude": ["**/*.d.ts", "node_modules", "scripts", "*.config.ts"], + "exclude": [ + "**/*.d.ts", + "node_modules", + "scripts", + "*.config.ts", + "examples/**/*", + "test/**/*" + ], "compilerOptions": { "rootDir": "./packages", "paths": { From 735d4506cff1b5a09e484f4830a9aacd4f962f33 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 13:15:45 -0500 Subject: [PATCH 4/9] Add reference in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 97c5bf8f..20631d46 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ based on knowledge of special macros. (e.g., some macros are known to take an argument, like `\mathbb`. Such arguments are not detected in the PEG processing stage). +See the [`examples/`](https://github.com/siefkenj/unified-latex/tree/main/examples) folder for usage samples. + ## Development You should develop in each project's subfolder in the `packages/` directory. From 20e137bf1fad44b9d6a22592e076e7598d30b417 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 13:28:26 -0500 Subject: [PATCH 5/9] Update snapshots --- .../__snapshots__/doc-examples.test.ts.snap | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap b/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap index ca93c6c4..2d9b826b 100644 --- a/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap +++ b/packages/unified-latex-cli/tests/__snapshots__/doc-examples.test.ts.snap @@ -1,9 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`unified-latex-doc-examples > example count-macros.ts 1`] = ` -"Building in mode: development. - -Macro statistics: +"Macro statistics: All macros: { textbf: [ '\\\\\\\\textbf{an}', '\\\\\\\\textbf{amazing}' ], @@ -20,18 +18,14 @@ Math mode macros: [ '^', 'sum', '_', '^', 'infty', 'frac', '^' ] `; exports[`unified-latex-doc-examples > example custom-macros.ts 1`] = ` -"Building in mode: development. - -\\\\textbf{custom} \\\\abc +"\\\\textbf{custom} \\\\abc \\\\textbf{custom} \\\\abc{macro} \\\\textbf \\\\abc{macro} " `; exports[`unified-latex-doc-examples > example expanding-or-replacing-macros.ts 1`] = ` -"Building in mode: development. - -\\\\newcommand{\\\\abc}[1]{ABC} My \\\\textbf{custom} \\\\abc{macro}. +"\\\\newcommand{\\\\abc}[1]{ABC} My \\\\textbf{custom} \\\\abc{macro}. My \\\\textbf{custom} ABC. \\\\abc{fun} $x=ABC$ My \\\\textbf{custom} ABC. @@ -40,18 +34,14 @@ exports[`unified-latex-doc-examples > example expanding-or-replacing-macros.ts 1 `; exports[`unified-latex-doc-examples > example ignore-defaults.ts 1`] = ` -"Building in mode: development. - -\\\\textbf \\\\abc +"\\\\textbf \\\\abc ^ \\\\color{blue} \\\\textbf {foo} " `; exports[`unified-latex-doc-examples > example using-unified.ts 1`] = ` -"Building in mode: development. - -{ +"{ type: 'macro', content: 'textbf', position: { From 51d12a63c073c2af47bec0768ab56045085f1504 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 16:09:01 -0500 Subject: [PATCH 6/9] Update argspec to handle default arguments better --- .../libs/argspec-parser.ts | 24 ++++- .../libs/argspec-types.ts | 7 +- .../__snapshots__/argspec-parser.test.ts.snap | 72 +++++---------- .../tests/argspec-parser.test.ts | 92 ++++++++++++++++++- .../grammars/xparse-argspec.pegjs | 62 +++++++++---- .../unified-latex-util-pegjs/package.json | 2 + 6 files changed, 186 insertions(+), 73 deletions(-) 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..be1d479c 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).toMatchInlineSnapshot([ + { + 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.skip("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-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" ], From 668472ac1774aaaf1630c6b157ccc3db780e36d0 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 16:10:04 -0500 Subject: [PATCH 7/9] Unskip test --- .../unified-latex-util-argspec/tests/argspec-parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 be1d479c..b4be56b7 100644 --- a/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts +++ b/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts @@ -133,7 +133,7 @@ describe("unified-latex-util-argspec", () => { }, ]); }); - it.skip("Embellishments keep default args", () => { + it("Embellishments keep default args", () => { ast = argspecParser.parse("E{{x}y{z}}{{One}{Two}{Three}}"); expect(ast).toEqual([ { From 30fec48873f1342fd148677237f7072cba7a8650 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 16:20:12 -0500 Subject: [PATCH 8/9] Fix test --- .../unified-latex-util-argspec/tests/argspec-parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b4be56b7..7d02b666 100644 --- a/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts +++ b/packages/unified-latex-util-argspec/tests/argspec-parser.test.ts @@ -110,7 +110,7 @@ describe("unified-latex-util-argspec", () => { it("Embellishment default args can be a mix of tokens and groups", () => { ast = argspecParser.parse("E{\\token^}{{D1}2}"); - expect(ast).toMatchInlineSnapshot([ + expect(ast).toEqual([ { defaultArg: ["D1", "2"], embellishmentTokens: ["\\token", "^"], From bebf6c66382fe33faa98c11621da89f07a704f78 Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Mon, 19 Feb 2024 17:04:42 -0500 Subject: [PATCH 9/9] Store default arguments in `_renderInfo` for each arg --- examples/tsconfig.json | 2 +- package.json | 2 +- packages/unified-latex-types/index.ts | 3 +++ .../unified-latex-types/libs/ast-types.ts | 5 +++-- packages/unified-latex-types/package.json | 6 ++++-- .../libs/gobble-arguments.ts | 21 +++++++++++++++---- .../tests/get-args-content.test.ts | 1 + .../unifiex-latex-attach-arguments.test.ts | 1 + .../unified-latex-util-render-info/index.ts | 2 +- tsconfig.test.json | 9 ++++++-- 10 files changed, 39 insertions(+), 13 deletions(-) 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-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-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"] }