diff --git a/packages/parser/src/lezer-to-dast/gobble-function-arguments.ts b/packages/parser/src/lezer-to-dast/gobble-function-arguments.ts index e7401cdbb..551db4d04 100644 --- a/packages/parser/src/lezer-to-dast/gobble-function-arguments.ts +++ b/packages/parser/src/lezer-to-dast/gobble-function-arguments.ts @@ -71,10 +71,12 @@ export function gobbleFunctionArguments( i++; continue; } - if (nextNode.value === "(" && nodes[i - 1]?.type === "function") { + if (nextNode.value === "(") { parenDepth++; } - if (nextNode.value === ",") { + // Commas separate arguments, but they may appear inside of balanced parenthesis. E.g. `$$f( (a,b) )` + // is a function with exactly one argument of `(a,b)`. + if (nextNode.value === "," && parenDepth <= 1) { functionNode.input!.push( trimWhitespace(currentFunctionArg as DastElementContent[]), ); diff --git a/packages/parser/src/macros/macros.peggy b/packages/parser/src/macros/macros.peggy index a342a669b..a3cb27df7 100644 --- a/packages/parser/src/macros/macros.peggy +++ b/packages/parser/src/macros/macros.peggy @@ -112,13 +112,31 @@ FunctionArgumentList return [start, ...rest]; } -FunctionArgument - = arg:( +// A function argument cannot contain commas unless those commas are inside of balanced parens. +// For example `$$f( (0,1) )` should be parsed as a function with a single argument. +FunctionArgument = BalancedParenTextNoComma + +BalancedParenTextNoComma + = x:( + Macro + / FunctionMacro + / TextWithoutParenOrComma + / a:OpenParen rest:BalancedParenText b:CloseParen { + return [a, ...rest, b]; + } + )* { return x.flat(); } + / x:EmptyString { return [x]; } + +BalancedParenText + = x:( Macro / FunctionMacro - / TextWithoutClosingParenOrComma - / EmptyString - ) { return [arg]; } + / TextWithoutParen + / a:OpenParen rest:BalancedParenText b:CloseParen { + return [a, ...rest, b]; + } + )* { return x.flat(); } + / x:EmptyString { return [x]; } PropAttrs = "{" _? @(@Attr _?)* "}" @@ -154,12 +172,20 @@ EmptyString return withPosition({ type: "text", value: "", - position: location(), }); } -TextWithoutClosingParenOrComma - = value:($[^),$]+ / [^),]) { return withPosition({ type: "text", value }); } +OpenParen = "(" { return withPosition({ type: "text", value: "(" }); } + +CloseParen = ")" { return withPosition({ type: "text", value: ")" }); } + +TextWithoutParenOrComma + = value:($[^(),$]+ / [^(),]) { + return withPosition({ type: "text", value }); + } + +TextWithoutParen + = value:($[^()$]+ / [^()]) { return withPosition({ type: "text", value }); } TextWithoutQuote = value:$([^'$]+ / [^']) { return withPosition({ type: "text", value }); } diff --git a/packages/parser/src/macros/types.ts b/packages/parser/src/macros/types.ts index d7ac820cf..409f694d9 100644 --- a/packages/parser/src/macros/types.ts +++ b/packages/parser/src/macros/types.ts @@ -100,10 +100,12 @@ export type FunctionMacro = input: FunctionInput | null; }); export type FunctionInput = FunctionArgumentList; -export type FunctionArgumentList = [FunctionArgument, ...FunctionArgument[]]; -export type FunctionArgument = [ - Macro | FunctionMacro | TextWithoutClosingParenOrComma | EmptyString +export type FunctionArgumentList = [ + BalancedParenTextNoComma, + ...BalancedParenTextNoComma[] ]; +export type BalancedParenTextNoComma = (Macro | FunctionMacro | Text)[]; +export type BalancedParenText = (Macro | FunctionMacro | Text)[]; export type PropAttrs = Attr[]; export type PropIndex = { position: { @@ -139,16 +141,26 @@ export type EmptyString = { start: { offset: number; line: number; column: number }; end: { offset: number; line: number; column: number }; }; -} & { - type: "text"; - value: ""; +} & { type: "text"; value: "" }; +export type OpenParen = { position: { - source: string | undefined; start: { offset: number; line: number; column: number }; end: { offset: number; line: number; column: number }; }; -}; -export type TextWithoutClosingParenOrComma = { +} & { type: "text"; value: "(" }; +export type CloseParen = { + position: { + start: { offset: number; line: number; column: number }; + end: { offset: number; line: number; column: number }; + }; +} & { type: "text"; value: ")" }; +export type TextWithoutParenOrComma = { + position: { + start: { offset: number; line: number; column: number }; + end: { offset: number; line: number; column: number }; + }; +} & { type: "text"; value: string }; +export type TextWithoutParen = { position: { start: { offset: number; line: number; column: number }; end: { offset: number; line: number; column: number }; diff --git a/packages/parser/test/dast-advanced.test.ts b/packages/parser/test/dast-advanced.test.ts index ac5add743..fe3f1b0b2 100644 --- a/packages/parser/test/dast-advanced.test.ts +++ b/packages/parser/test/dast-advanced.test.ts @@ -714,4 +714,266 @@ describe("DAST", async () => { } `); }); + it("Function macros can have balanced parens in their arguments", () => { + let source: string; + + source = `$$f((x-2), 7)`; + expect(filterPositionInfo(lezerToDast(source))).toMatchInlineSnapshot(` + { + "children": [ + { + "input": [ + [ + { + "type": "text", + "value": "(", + }, + { + "type": "text", + "value": "x-2", + }, + { + "type": "text", + "value": ")", + }, + ], + [ + { + "type": "text", + "value": "7", + }, + ], + ], + "macro": { + "accessedProp": null, + "attributes": [], + "path": [ + { + "index": [], + "name": "f", + "type": "pathPart", + }, + ], + "type": "macro", + }, + "type": "function", + }, + ], + "type": "root", + } + `); + + source = `$$f((3,4), (5,6))`; + expect(filterPositionInfo(lezerToDast(source))).toMatchInlineSnapshot(` + { + "children": [ + { + "input": [ + [ + { + "type": "text", + "value": "(", + }, + { + "type": "text", + "value": "3,4", + }, + { + "type": "text", + "value": ")", + }, + ], + [ + { + "type": "text", + "value": "(", + }, + { + "type": "text", + "value": "5,6", + }, + { + "type": "text", + "value": ")", + }, + ], + ], + "macro": { + "accessedProp": null, + "attributes": [], + "path": [ + { + "index": [], + "name": "f", + "type": "pathPart", + }, + ], + "type": "macro", + }, + "type": "function", + }, + ], + "type": "root", + } + `); + + source = `$$f(1,(alpha))`; + expect(filterPositionInfo(lezerToDast(source))).toMatchInlineSnapshot(` + { + "children": [ + { + "input": [ + [ + { + "type": "text", + "value": "1", + }, + ], + [ + { + "type": "text", + "value": "(", + }, + { + "attributes": [], + "children": [ + { + "type": "text", + "value": "alpha", + }, + ], + "name": "math", + "type": "element", + }, + { + "type": "text", + "value": ")", + }, + ], + ], + "macro": { + "accessedProp": null, + "attributes": [], + "path": [ + { + "index": [], + "name": "f", + "type": "pathPart", + }, + ], + "type": "macro", + }, + "type": "function", + }, + ], + "type": "root", + } + `); + + source = `$$f(x, alpha)`; + expect(filterPositionInfo(lezerToDast(source))).toMatchInlineSnapshot(` + { + "children": [ + { + "input": [ + [ + { + "type": "text", + "value": "x", + }, + ], + [ + { + "attributes": [], + "children": [ + { + "type": "text", + "value": "alpha", + }, + ], + "name": "math", + "type": "element", + }, + ], + ], + "macro": { + "accessedProp": null, + "attributes": [], + "path": [ + { + "index": [], + "name": "f", + "type": "pathPart", + }, + ], + "type": "macro", + }, + "type": "function", + }, + ], + "type": "root", + } + `); + + source = `$$f((x,y), alpha)`; + expect(filterPositionInfo(lezerToDast(source))).toMatchInlineSnapshot(` + { + "children": [ + { + "input": [ + [ + { + "type": "text", + "value": "(", + }, + { + "type": "text", + "value": "x", + }, + { + "type": "text", + "value": ",", + }, + { + "type": "text", + "value": "y", + }, + { + "type": "text", + "value": ")", + }, + ], + [ + { + "attributes": [], + "children": [ + { + "type": "text", + "value": "alpha", + }, + ], + "name": "math", + "type": "element", + }, + ], + ], + "macro": { + "accessedProp": null, + "attributes": [], + "path": [ + { + "index": [], + "name": "f", + "type": "pathPart", + }, + ], + "type": "macro", + }, + "type": "function", + }, + ], + "type": "root", + } + `); + }); }); diff --git a/packages/parser/test/pretty-print.test.ts b/packages/parser/test/pretty-print.test.ts index 001e50e9a..83e4f4551 100644 --- a/packages/parser/test/pretty-print.test.ts +++ b/packages/parser/test/pretty-print.test.ts @@ -71,8 +71,12 @@ describe("Prettier", async () => { const cases = [ { inStr: "$$f(x)", outStr: "$$f(x)" }, { inStr: "$$f(x,y)", outStr: "$$f(x, y)" }, + { inStr: "$$f(x,$y)", outStr: "$$f(x, $y)" }, + { inStr: "$$f(x,$y z)", outStr: "$$f(x, $y z)" }, + { inStr: "$$f((),())", outStr: "$$f((), ())" }, { inStr: "$$f(x,$$g(y,z))", outStr: "$$f(x, $$g(y, z))" }, { inStr: "$$f(x,alpha)", outStr: "$$f(x, alpha)" }, + { inStr: "$$f((x),alpha)", outStr: "$$f((x), alpha)" }, { inStr: "$$f(x, alpha)", outStr: "$$f(x, alpha)" }, { inStr: "

$$f(x, alpha)

", outStr: "

\n $$f(x, alpha)\n

" }, ];