Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Examples #82

Merged
merged 5 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading