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

Default arguments support #24 #62

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ee4c8e5
feat: Attach default arguments to AST
theseanl Dec 31, 2023
58077cd
feat: Support default arguments for embellishments
theseanl Jan 2, 2024
76d88c1
test: add macro with default argument case in CLI test, make it work on
theseanl Jan 2, 2024
48c30a7
chore: remove an obsolete snapshot
theseanl Jan 3, 2024
c07f7b8
Merge branch 'main' into default-arguments
theseanl Jan 5, 2024
af91693
[WIP] fix 'until' behavior, support multi-token stop
theseanl Jan 17, 2024
0c3e598
build: Switch to nodejs export conditions from TS project references
theseanl Jan 20, 2024
da0e74c
Merge branch 'main' into ditch-ts-project-references
theseanl Jan 20, 2024
b69517b
build: use _bundle export condition on esm bundle as well
theseanl Jan 20, 2024
9035e2a
chore: make prettier format json
theseanl Jan 20, 2024
768a308
build: set forceConsistentCasingInFileNames
theseanl Jan 20, 2024
191c53c
Merge branch 'ditch-ts-project-references' into default-arguments
theseanl Jan 20, 2024
2067555
feat: support multi-stop token in 'until', macro delimiters
theseanl Jan 20, 2024
6cb5599
refactor: move default argument logic to expandMacros
theseanl Jan 21, 2024
20976aa
feat: Support default arguments referencing other arguments
theseanl Jan 21, 2024
21a1733
chore: revert early return in gobble-single-argument.ts
theseanl Jan 21, 2024
bd1f972
feat: default argument support for \newcommand
theseanl Jan 22, 2024
7d12d13
chore: simplify codes
theseanl Jan 22, 2024
af5e25d
build: use import instead of _bundle
theseanl Jan 22, 2024
8de3294
Merge branch 'ditch-ts-project-references' into default-arguments
theseanl Jan 24, 2024
d63b24e
chore: fix cli test, regenerate package lock
theseanl Jan 24, 2024
dcb22be
Merge branch 'main' into default-arguments
theseanl Jan 24, 2024
916e10f
chore: minor change as per reviews
theseanl Jan 24, 2024
38a18e2
Merge branch 'main' into default-arguments
theseanl Jan 29, 2024
6b9e249
fix: optional argument without default value
theseanl Jan 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6,583 changes: 3,791 additions & 2,792 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"devDependencies": {
"@types/node": "^20.5.9",
"@types/prettier": "^2.7.3",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"esbuild": "^0.19.2",
"esbuild-runner": "^2.2.2",
"lerna": "^7.2.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/unified-latex-builder/libs/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ export function arg(
return { type: "argument", content: args, openMark, closeMark };
}

/**
* Creates an empty argument. This can only present in Ast as a result of -NoValue-.
*/
export function emptyArg() {
return arg([], {
openMark: "",
closeMark: "",
});
}

/**
* Create a Macro with the given `name`. The macro
* may be followed by any number of arguments.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import * as Ast from "@unified-latex/unified-latex-types";
import { attachMacroArgs } from "@unified-latex/unified-latex-util-arguments";
import {
expandMacros,
expandMacrosExcludingDefinitions,
listNewcommands,
} from "@unified-latex/unified-latex-util-macros";
import { Plugin } from "unified";
import { attachMacroArgs } from "@unified-latex/unified-latex-util-arguments";

type PluginOptions = {
macros?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ hi $\\\\x fooArg$ and baz!
"
`;

exports[`unified-latex-cli > can expand macro 3`] = `
"% Expand via> tests/needs-expanding.tex --stats -e \\"\\\\\\\\newcommand{foo}[1]{FOO(#1)}\\" -e '{name: \\"bar\\", body: \\"baz\\"}'
hi FOO(fooArg) and baz!
"
`;

exports[`unified-latex-cli > can expand macros defined in document 1`] = `
"\\\\newcommand{\\\\foo}[1]{$BAR #1$}
\\\\DeclareDocumentCommand{\\\\baz}{m}{\\\\foo{xxx} .#1.}
Expand Down
10 changes: 10 additions & 0 deletions packages/unified-latex-cli/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ describe(
]);
expect(stdout).toMatchSnapshot();
}
{
let stdout = await execCLI([
`${examplesPath}/needs-expanding.tex`,
`-e`,
"\\newcommand{foo}[2][FOO]{#1(#2)}",
`-e`,
'{name: "bar", signature: "O{baz}", body: "#1"}',
]);
expect(stdout).toMatchSnapshot();
}
});
it("can expand macros defined in document", async () => {
const stdout = await execCLI([
Expand Down
6 changes: 1 addition & 5 deletions packages/unified-latex-prettier/libs/printer/argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@ export function printArgument(
);

// We can return early for empty arguments (this is common for omitted optional arguments)
if (
node.openMark === "" &&
node.closeMark === "" &&
node.content.length === 0
) {
if (match.blankArgument(node)) {
return [];
}
const parentNode = path.getParentNode();
Expand Down
2 changes: 2 additions & 0 deletions packages/unified-latex-prettier/libs/printer/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export function printLatexAst(
return printVerbatimEnvironment(path, print, options);
case "whitespace":
return line;
case "hash_number":
return `#${node.number}`;
default:
console.warn("Printing unknown type", node);
return printRaw(node);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export function toHastDirect(
);
case "root":
return h("root");
case "hash_number":
return {
type: "text",
value: `#${node.number}`,
};
default: {
const _exhaustiveCheck: never = node;
throw new Error(
Expand Down
5 changes: 5 additions & 0 deletions packages/unified-latex-to-hast/libs/html-subs/to-hast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ export function toHastWithLoggerFactory(
);
case "root":
return node.content.flatMap(toHast);
case "hash_number":
return {
type: "text",
value: `#${node.number}`,
};
default: {
const _exhaustiveCheck: never = node;
throw new Error(
Expand Down
9 changes: 8 additions & 1 deletion packages/unified-latex-types/libs/ast-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ export interface Argument extends ContentNode {
closeMark: string;
}

// Only available during macro expansion
export interface HashNumber extends BaseNode {
type: "hash_number";
number: number;
}

export type Node =
| Root
| String
Expand All @@ -104,6 +110,7 @@ export type Node =
| InlineMath
| DisplayMath
| Group
| Verb;
| Verb
| HashNumber;

export type Ast = Node | Argument | Node[];
107 changes: 85 additions & 22 deletions packages/unified-latex-util-argspec/libs/argspec-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as ArgSpec from "./argspec-types";
*/
function getDecorators(node: ArgSpec.Node): string {
let ret = "";
if ((node as ArgSpec.LeadingWhitespace).noLeadingWhitespace) {
if (node.noLeadingWhitespace) {
ret += "!";
}
return ret;
Expand All @@ -29,13 +29,11 @@ export function printRaw(
const sepToken = root ? " " : "";
return node.map((tok) => printRaw(tok)).join(sepToken);
}

return printRawInner(node);
}
function printRawInner(node: ArgSpec.Node) {
const decorators = getDecorators(node);
const defaultArg = (node as ArgSpec.DefaultArgument).defaultArg
? printRaw((node as ArgSpec.DefaultArgument).defaultArg!)
: "";
let spec = decorators;

const type = node.type;
switch (type) {
case "body":
Expand All @@ -52,7 +50,10 @@ export function printRaw(
spec += node.defaultArg ? "D" : "d";
spec += node.openBrace + node.closeBrace;
}
return spec + defaultArg;
if (node.defaultArg) {
spec = appendTokenOrGroup(spec, node.defaultArg);
}
return spec;
case "mandatory":
// {...} is the default enclosure for mandatory arguments
if (node.openBrace === "{" && node.closeBrace === "}") {
Expand All @@ -61,32 +62,94 @@ export function printRaw(
spec += node.defaultArg ? "R" : "r";
spec += node.openBrace + node.closeBrace;
}
return spec + defaultArg;
if (node.defaultArg) {
spec = appendTokenOrGroup(spec, node.defaultArg);
}
return spec;
case "embellishment":
spec += node.defaultArg ? "E" : "e";
return (
spec +
"{" +
printRaw(node.embellishmentTokens) +
"}" +
defaultArg
);
spec += node.defaultArgs ? "E" : "e";
spec += printTokenOrCollection(node.tokens);
if (node.defaultArgs) {
spec += printTokenOrCollection(node.defaultArgs);
}
return spec;
case "verbatim":
return spec + "v" + node.openBrace;
case "group":
return spec + "{" + printRaw(node.content) + "}";
case "until": {
const stopTokens = printRaw(node.stopTokens);
return stopTokens.length > 1 || stopTokens[0] === " "
? `u{${stopTokens}}`
: `u${stopTokens}`;
spec += "u";
spec += printTokenOrCollection(node.stopTokens);
return spec;
}
default:
const neverType: never = type;
console.warn(`Unknown node type "${neverType}" for node`, node);
return "";
}
}
/**
* See xparse-argspec.pegjs - token_or_group is parsed to an array of strings.
* This function will reconstruct a representative in an inverse image of token_or_group
* for a given array of strings, and append it to a given string.
* In order to avoid parsing ambiguity, we force enclose the representative with braces in some case.
*
* Examples)
* Appending a `token_or_group` representing an embellishment tokens, with existingString `e`.
* token of several chars "ab" --> {ab} will be appended, so the result will be `e{ab}`.
* token of single char "^" ---> ^ will be appended, so the result will be `e^`.
* token of single char " " ---> { } will be appended, so the result will be `e{ }`.
*
* Appending a `token_or_group` representing a bracespec of a delimited argument spec.
* Already constructed a string until the opening brace, existingString = "r\open".
* If the bracespec is a single alphabetric char "a", then we need to append it with braces, so the result will be "r\open{a}".
*
* Appending an embellishment token, with existingString = "e{a\token".
* If a next token is again a control world, say \anotherToken, we can append it as-is. "e{a\token\anotherToken".
* If a next token is a single char, say "b", we may either enclose it with braces or separate it with a space,
* because in this circumstance, space can be used to separate tokens (as conveyed by allowWhitespace = true).
* Then the result will be "e{a\token b".
*/
function appendTokenOrGroup(
existingString: string,
tokenOrGroup: string,
allowWhitespace = false
) {
// If a previous token consists of more than one chars and ends with letters,
// then we need to separate the next token by enclosing it with braces.
// This can happen with control words such as \asdf.
const followsControlWord = /\\[a-zA-Z]+$/.test(existingString);
if (
(!followsControlWord &&
tokenOrGroup.length === 1 &&
(allowWhitespace || tokenOrGroup !== " ")) ||
tokenOrGroup.startsWith("\\")
) {
return existingString + tokenOrGroup;
}
// In normalization, prefer whitespace because it occupies less space.
return (
existingString +
(allowWhitespace && tokenOrGroup.length === 1
? " " + tokenOrGroup
: "{" + tokenOrGroup + "}")
);
}
/**
* See xparse-argspec.pegjs, token_or_collection is used by embellishment tokens, embellishment default arguments,
* and stop tokens for `until`.
*/
function printTokenOrCollection(tokenOrCollection: string[]) {
if (tokenOrCollection.length <= 1) {
const token = tokenOrCollection[0];
if (token.length === 1 && token !== " ") {
return token;
}
}
let out = "";
for (let token of tokenOrCollection) {
out = appendTokenOrGroup(out, token, true);
}
return "{" + out + "}";
}

const parseCache: { [argStr: string]: ArgSpec.Node[] } = {};

Expand Down
48 changes: 33 additions & 15 deletions packages/unified-latex-util-argspec/libs/argspec-types.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
export type Ast = Node[] | Node;
export type Node = Optional | Mandatory | Verbatim | Body | Group | Until;
export type Node = Optional | Mandatory | Verbatim | Body | Until;
type Optional = OptionalArg | OptionalStar | OptionalToken | Embellishment;
interface AstNode {

// Make several optional properties available in all `AstNode`s
interface AstNode
extends Partial<Arg>,
Partial<LeadingWhitespace>,
DefaultArgument {
type: string;
}
interface Arg extends AstNode {

// Mixins
interface Arg {
openBrace: string;
closeBrace: string;
}
export interface LeadingWhitespace {
interface LeadingWhitespace {
noLeadingWhitespace: boolean | undefined;
}
export interface DefaultArgument {
defaultArg?: Group;
interface DefaultArgument {
defaultArg?: string;
}
interface Verbatim extends Arg {

// Make `openBrace` and `closeBrace` required by extending both.
// Typescript requires specifying those properties to be specified in interface body,
// otherwise the compilation will error out.
interface Verbatim extends AstNode, Arg {
type: "verbatim";
openBrace: string;
closeBrace: string;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very strange to me and seems to be missing the benefits of the mixins. Let's just use types for this.

type Braces = {openBrace: string; closeBrace: string};
type DefaultArg = {defaultArg?: string};
type LeadingWhitespace = {leadingWhitespace: boolean};

type AstNode = Partial<Braces> & Partial<LeadingWhitespace> & DefaultArg;

type Verbatim = AstNode & Braces;

...

}
interface OptionalArg extends LeadingWhitespace, DefaultArgument, Arg {
interface OptionalArg extends LeadingWhitespace, DefaultArgument, AstNode, Arg {
type: "optional";
openBrace: string;
closeBrace: string;
noLeadingWhitespace: boolean | undefined;
defaultArg?: string;
}
interface OptionalStar extends LeadingWhitespace, AstNode {
type: "optionalStar";
noLeadingWhitespace: boolean | undefined;
}
interface OptionalToken extends LeadingWhitespace, AstNode {
type: "optionalToken";
token: string;
noLeadingWhitespace: boolean | undefined;
}
export interface Embellishment extends DefaultArgument, AstNode {
export interface Embellishment extends AstNode {
type: "embellishment";
embellishmentTokens: string[];
tokens: string[];
defaultArgs?: string[]; // Embellishment default arguments are always a collection of arguments
}
interface Mandatory extends LeadingWhitespace, DefaultArgument, Arg {
interface Mandatory extends DefaultArgument, AstNode, Arg {
type: "mandatory";
}
export interface Group extends AstNode {
type: "group";
content: (Group | string)[];
openBrace: string;
closeBrace: string;
}
interface Body extends AstNode {
type: "body";
Expand Down