Skip to content

Commit

Permalink
Add Examples (#82)
Browse files Browse the repository at this point in the history
* Add examples
* Test examples
  • Loading branch information
siefkenj authored Feb 19, 2024
1 parent 05ed77e commit e3a07de
Show file tree
Hide file tree
Showing 12 changed files with 634 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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 <example file>`.

- `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.
59 changes: 59 additions & 0 deletions examples/count-macros.ts
Original file line number Diff line number Diff line change
@@ -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<string, string[]> = {};
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);
47 changes: 47 additions & 0 deletions examples/custom-macros.ts
Original file line number Diff line number Diff line change
@@ -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])
);
139 changes: 139 additions & 0 deletions examples/expanding-or-replacing-macros.ts
Original file line number Diff line number Diff line change
@@ -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<void[], Ast.Root, Ast.Root> =
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));
59 changes: 59 additions & 0 deletions examples/ignore-defaults.ts
Original file line number Diff line number Diff line change
@@ -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(" "));
5 changes: 5 additions & 0 deletions examples/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": { "rootDir": "./" },
"include": ["./**/*.ts"],
"extends": "../tsconfig.build.json",
}
Loading

0 comments on commit e3a07de

Please sign in to comment.