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 5 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
7,872 changes: 4,322 additions & 3,550 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 @@ -24,6 +24,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
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
11 changes: 11 additions & 0 deletions packages/unified-latex-cli/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ describe(
]);
expect(stdout).toMatchSnapshot();
}
{
let stdout = await executeCommand(`node`, [
exePath,
`${examplesPath}/needs-expanding.tex`,
`-e`,
"\\newcommand{foo}[1]{FOO(#1)}",
`-e`,
'{name: "bar", signature: "O{baz}", body: "#1"}',
]);
expect(stdout).toMatchSnapshot();
}
});
it("can expand macros defined in document", async () => {
let { stdout, stderr } = await exec(
Expand Down
95 changes: 74 additions & 21 deletions packages/unified-latex-util-argspec/libs/argspec-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ 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;

function appendDefaultArg() {
if ("defaultArg" in node && node.defaultArg) {
spec = appendTokenOrGroup(spec, node.defaultArg);
}
}
function appendCollection(collection: string[]) {
spec += printTokenOrCollection(collection);
}
const type = node.type;
switch (type) {
case "body":
Expand All @@ -52,7 +58,8 @@ export function printRaw(
spec += node.defaultArg ? "D" : "d";
spec += node.openBrace + node.closeBrace;
}
return spec + defaultArg;
appendDefaultArg();
theseanl marked this conversation as resolved.
Show resolved Hide resolved
return spec;
case "mandatory":
// {...} is the default enclosure for mandatory arguments
if (node.openBrace === "{" && node.closeBrace === "}") {
Expand All @@ -61,32 +68,78 @@ export function printRaw(
spec += node.defaultArg ? "R" : "r";
spec += node.openBrace + node.closeBrace;
}
return spec + defaultArg;
appendDefaultArg();
return spec;
case "embellishment":
spec += node.defaultArg ? "E" : "e";
return (
spec +
"{" +
printRaw(node.embellishmentTokens) +
"}" +
defaultArg
);
spec += node.embellishmentDefaultArg ? "E" : "e";
appendCollection(node.embellishmentTokens);
if (node.embellishmentDefaultArg) {
appendCollection(node.embellishmentDefaultArg);
}
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";
appendCollection(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.
* For instance, if the given string ends with a control word such as "\asdf", and if the representative is a
* whitespace where we are in a circumstance where no whitespaces are allowed.
theseanl marked this conversation as resolved.
Show resolved Hide resolved
*/
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
9 changes: 3 additions & 6 deletions packages/unified-latex-util-argspec/libs/argspec-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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 {
type: string;
Expand All @@ -12,7 +12,7 @@ export interface LeadingWhitespace {
noLeadingWhitespace: boolean | undefined;
}
export interface DefaultArgument {
defaultArg?: Group;
defaultArg?: string;
}
interface Verbatim extends Arg {
type: "verbatim";
Expand All @@ -30,14 +30,11 @@ interface OptionalToken extends LeadingWhitespace, AstNode {
export interface Embellishment extends DefaultArgument, AstNode {
type: "embellishment";
embellishmentTokens: string[];
embellishmentDefaultArg?: string[];
}
interface Mandatory extends LeadingWhitespace, DefaultArgument, Arg {
type: "mandatory";
}
export interface Group extends AstNode {
type: "group";
content: (Group | string)[];
}
interface Body extends AstNode {
type: "body";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,38 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin

exports[`unified-latex-util-argspec > parses xparse argument specification string "" 1`] = `[]`;

exports[`unified-latex-util-argspec > parses xparse argument specification string "D(ab" 1`] = `
[
{
"closeBrace": "a",
"defaultArg": "b",
"openBrace": "(",
"type": "optional",
},
]
`;

exports[`unified-latex-util-argspec > parses xparse argument specification string "E{\\token ^}{{D1}2}" 1`] = `
[
{
"embellishmentDefaultArg": [
"D1",
"2",
],
"embellishmentTokens": [
"\\\\token",
"^",
],
"type": "embellishment",
},
]
`;

exports[`unified-latex-util-argspec > parses xparse argument specification string "O{nested{defaults}}" 1`] = `
[
{
"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",
},
Expand All @@ -61,22 +65,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",
},
Expand All @@ -93,6 +82,28 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin
]
`;

exports[`unified-latex-util-argspec > parses xparse argument specification string "Ox" 1`] = `
[
{
"closeBrace": "]",
"defaultArg": "x",
"openBrace": "[",
"type": "optional",
},
]
`;

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`] = `
[
{
Expand All @@ -102,21 +113,14 @@ exports[`unified-latex-util-argspec > parses xparse argument specification strin
},
{
"closeBrace": "-",
"defaultArg": {
"content": [
"d",
"e",
"f",
],
"type": "group",
},
"defaultArg": "def",
"openBrace": "-",
"type": "optional",
},
]
`;

exports[`unified-latex-util-argspec > parses xparse argument specification string "m e{^}" 1`] = `
exports[`unified-latex-util-argspec > parses xparse argument specification string "m e^" 1`] = `
[
{
"closeBrace": "}",
Expand Down Expand Up @@ -219,6 +223,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`] = `
[
{
Expand Down
13 changes: 9 additions & 4 deletions packages/unified-latex-util-argspec/tests/argspec-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("unified-latex-util-argspec", () => {
"o m o !o m",
"!o r() m",
"O{somedefault} m o",
"m e{^}",
Copy link
Owner

Choose a reason for hiding this comment

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

Why was this test removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was replaced with m e^. Now the printRaw function does something smarter, and it omits braces when there's a single embellishment token of single width (which is something that wasn't even accepted at pegjs grammar level before)

Copy link
Owner

Choose a reason for hiding this comment

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

I prefer to have it default to always using braces. You can add e^ as a separate test though, just to make sure we support the non-braced version.

"m e^",
"m e{_^}",
"s m",
"v!",
Expand All @@ -35,6 +35,11 @@ describe("unified-latex-util-argspec", () => {
"u{xx;}",
"u;",
"u{ }",
"Ox",
"r\\abc\\d",
"R\\a1{default}",
"D(ab",
"E{\\token ^}{{D1}2}",
];

for (const spec of SPEC_STRINGS) {
Expand All @@ -46,16 +51,16 @@ describe("unified-latex-util-argspec", () => {
}

it("Embellishments always return a string", () => {
let ast = argspecParser.parse("e{{{x}}y{z}}");
let 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}}{{One}{Two}{Three}}");
expect(ast).toEqual([
{
type: "embellishment",
embellishmentTokens: ["x", "y", "z"],
defaultArg: { type: "group", content: [] },
embellishmentDefaultArg: ["One", "Two", "Three"],
},
]);
});
Expand Down