Skip to content

Commit

Permalink
Merge pull request #26 from getlang-dev/template-optionals
Browse files Browse the repository at this point in the history
interpolated template optionals
  • Loading branch information
mattfysh authored Dec 6, 2024
2 parents 1ebda50 + 58bb154 commit d561a12
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 79 deletions.
6 changes: 6 additions & 0 deletions .changeset/two-points-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@getlang/parser": patch
"@getlang/get": patch
---

interpolated template optionals
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"packageManager": "[email protected]",
"scripts": {
"format": "biome check --write",
"fmt": "biome check --write",
"lint": "bun lint:check && bun lint:types && bun lint:unused && bun lint:repo",
"lint:check": "biome check",
"lint:types": "cd test && tsc --noEmit",
Expand All @@ -18,7 +18,7 @@
"test"
],
"devDependencies": {
"@biomejs/biome": "1.8.2",
"@biomejs/biome": "^1.9.4",
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.10",
"@types/bun": "^1.1.14",
Expand Down
6 changes: 4 additions & 2 deletions packages/get/src/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ export async function execute(
},
},

TemplateExpr(node) {
TemplateExpr(node, path) {
const firstNull = node.elements.find(el => el instanceof NullSelection)
if (firstNull) {
return firstNull
const parents = path.slice(0, -1)
const isRoot = !parents.find(n => n.kind === NodeKind.TemplateExpr)
return isRoot ? firstNull : ''
}
return node.elements
.map(el =>
Expand Down
82 changes: 78 additions & 4 deletions packages/parser/grammar.html
Original file line number Diff line number Diff line change
Expand Up @@ -1784,7 +1784,7 @@ <h1><code>object_entry</code></h1>
</div>
<h1><code>template</code></h1>
<div>
<svg class="railroad-diagram" width="329" height="130" viewBox="0 0 329 130">
<svg class="railroad-diagram" width="329" height="160" viewBox="0 0 329 160">
<g transform="translate(.5 .5)">
<path d="M 20 21 v 20 m 10 -20 v 20 m -10 -10 h 20.5"></path>
<g>
Expand Down Expand Up @@ -1844,13 +1844,27 @@ <h1><code>template</code></h1>
<path d="M218 91h10"></path>
</g>
<path d="M228 91a10 10 0 0 0 10 -10v-40a10 10 0 0 1 10 -10"></path>
<path d="M80 31a10 10 0 0 1 10 10v70a10 10 0 0 0 10 10"></path>
<g>
<path d="M100 121h0"></path>
<path d="M228 121h0"></path>
<path d="M100 121h10"></path>
<g>
<path d="M110 121h0"></path>
<path d="M218 121h0"></path>
<rect x="110" y="110" width="108" height="22"></rect>
<text x="164" y="125">interp&#95;tmpl</text>
</g>
<path d="M218 121h10"></path>
</g>
<path d="M228 121a10 10 0 0 0 10 -10v-70a10 10 0 0 1 10 -10"></path>
</g>
<path d="M248 31h10"></path>
<path d="M80 31a10 10 0 0 0 -10 10v59a10 10 0 0 0 10 10"></path>
<path d="M80 31a10 10 0 0 0 -10 10v89a10 10 0 0 0 10 10"></path>
<g>
<path d="M80 110h168"></path>
<path d="M80 140h168"></path>
</g>
<path d="M248 110a10 10 0 0 0 10 -10v-59a10 10 0 0 0 -10 -10"></path>
<path d="M248 140a10 10 0 0 0 10 -10v-89a10 10 0 0 0 -10 -10"></path>
</g>
<path d="M258 31h10"></path>
</g>
Expand Down Expand Up @@ -1920,6 +1934,66 @@ <h1><code>interp_expr</code></h1>
</g>
</svg>

</div>
<h1><code>interp_tmpl</code></h1>
<div>
<svg class="railroad-diagram" width="457" height="62" viewBox="0 0 457 62">
<g transform="translate(.5 .5)">
<path d="M 20 21 v 20 m 10 -20 v 20 m -10 -10 h 20.5"></path>
<g>
<path d="M40 31h0"></path>
<path d="M416 31h0"></path>
<path d="M40 31h20"></path>
<g>
<path d="M60 31h0"></path>
<path d="M396 31h0"></path>
<path d="M60 31h10"></path>
<g>
<path d="M70 31h0"></path>
<path d="M122 31h0"></path>
<rect x="70" y="20" width="52" height="22" rx="10" ry="10"></rect>
<text x="96" y="35">"$&#91;"</text>
</g>
<path d="M122 31h10"></path>
<path d="M132 31h10"></path>
<g>
<path d="M142 31h0"></path>
<path d="M170 31h0"></path>
<rect x="142" y="20" width="28" height="22"></rect>
<text x="156" y="35">&#95;</text>
</g>
<path d="M170 31h10"></path>
<path d="M180 31h10"></path>
<g>
<path d="M190 31h0"></path>
<path d="M274 31h0"></path>
<rect x="190" y="20" width="84" height="22"></rect>
<text x="232" y="35">template</text>
</g>
<path d="M274 31h10"></path>
<path d="M284 31h10"></path>
<g>
<path d="M294 31h0"></path>
<path d="M322 31h0"></path>
<rect x="294" y="20" width="28" height="22"></rect>
<text x="308" y="35">&#95;</text>
</g>
<path d="M322 31h10"></path>
<path d="M332 31h10"></path>
<g>
<path d="M342 31h0"></path>
<path d="M386 31h0"></path>
<rect x="342" y="20" width="44" height="22" rx="10" ry="10"></rect>
<text x="364" y="35">"&#93;"</text>
</g>
<path d="M386 31h10"></path>
</g>
<path d="M396 31h20"></path>
</g>
<path d="M 416 31 h 20 m -10 -10 v 20 m 10 -20 v 20"></path>
</g>
</svg>

</div>
<h1><code>slice</code></h1>
<div>
Expand Down
15 changes: 9 additions & 6 deletions packages/parser/src/ast/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const printVisitor: InterpretVisitor<Doc> = {
return group(['extract ', node.value])
},

ObjectLiteralExpr(node, orig) {
ObjectLiteralExpr(node, _path, orig) {
const shorthand: Doc[] = []
const shouldBreak = orig.entries.some(
e => e.value.kind === NodeKind.SelectorExpr,
Expand Down Expand Up @@ -131,21 +131,24 @@ const printVisitor: InterpretVisitor<Doc> = {
return node.context ? [node.context, indent([line, '-> ', obj])] : obj
},

TemplateExpr(node, orig) {
TemplateExpr(node, _path, orig) {
return node.elements.map((el, i) => {
const origEl = orig.elements[i]
if (!origEl) {
throw new Error('Unmatched object literal entry')
} else if ('offset' in origEl) {
return origEl.value
} else if (origEl.kind !== NodeKind.IdentifierExpr) {
throw new Error(`Unexpected template node: ${origEl?.kind}`)
}

// strip the leading `$` character
if (typeof el !== 'string' && !Array.isArray(el)) {
throw new Error(`Unsupported template node: ${el.type} command`)
} else if (origEl.kind === NodeKind.TemplateExpr) {
return ['$[', el, ']']
} else if (origEl.kind !== NodeKind.IdentifierExpr) {
throw new Error(`Unexpected template node: ${origEl?.kind}`)
}

// strip the leading `$` character
let ret = el.slice(1)

const nextEl = node.elements[i + 1]
Expand Down Expand Up @@ -174,7 +177,7 @@ const printVisitor: InterpretVisitor<Doc> = {
return [node.context, indent([line, arrow, node.selector])]
},

ModifierExpr(node, orig) {
ModifierExpr(node, _path, orig) {
const mod: Doc[] = ['@', node.value.value]
if (orig.options.entries.length) {
mod.push('(', node.options, ')')
Expand Down
3 changes: 3 additions & 0 deletions packages/parser/src/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,16 @@ const grammar: Grammar = {
{"name": "template$ebnf$1$subexpression$1", "symbols": [(lexer.has("literal") ? {type: "literal"} : literal)]},
{"name": "template$ebnf$1$subexpression$1", "symbols": [(lexer.has("interpvar") ? {type: "interpvar"} : interpvar)]},
{"name": "template$ebnf$1$subexpression$1", "symbols": ["interp_expr"]},
{"name": "template$ebnf$1$subexpression$1", "symbols": ["interp_tmpl"]},
{"name": "template$ebnf$1", "symbols": ["template$ebnf$1$subexpression$1"]},
{"name": "template$ebnf$1$subexpression$2", "symbols": [(lexer.has("literal") ? {type: "literal"} : literal)]},
{"name": "template$ebnf$1$subexpression$2", "symbols": [(lexer.has("interpvar") ? {type: "interpvar"} : interpvar)]},
{"name": "template$ebnf$1$subexpression$2", "symbols": ["interp_expr"]},
{"name": "template$ebnf$1$subexpression$2", "symbols": ["interp_tmpl"]},
{"name": "template$ebnf$1", "symbols": ["template$ebnf$1", "template$ebnf$1$subexpression$2"], "postprocess": (d) => d[0].concat([d[1]])},
{"name": "template", "symbols": ["template$ebnf$1"], "postprocess": p.template},
{"name": "interp_expr", "symbols": [{"literal":"${"}, "_", (lexer.has("identifier") ? {type: "identifier"} : identifier), "_", {"literal":"}"}], "postprocess": p.interpExpr},
{"name": "interp_tmpl", "symbols": [{"literal":"$["}, "_", "template", "_", {"literal":"]"}], "postprocess": p.interpTmpl},
{"name": "slice", "symbols": [(lexer.has("slice") ? {type: "slice"} : slice)], "postprocess": p.slice},
{"name": "modifier$ebnf$1$subexpression$1", "symbols": [{"literal":"("}, "object", {"literal":")"}]},
{"name": "modifier$ebnf$1", "symbols": ["modifier$ebnf$1$subexpression$1"], "postprocess": id},
Expand Down
3 changes: 2 additions & 1 deletion packages/parser/src/grammar/getlang.ne
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ object_entry -> id_expr "?":? {% p.objectEntryShorthandIdent %}


### LITERALS
template -> (%literal | %interpvar | interp_expr):+ {% p.template %}
template -> (%literal | %interpvar | interp_expr | interp_tmpl):+ {% p.template %}
interp_expr -> "${" _ %identifier _ "}" {% p.interpExpr %}
interp_tmpl -> "$[" _ template _ "]" {% p.interpTmpl %}
slice -> %slice {% p.slice %}
modifier -> %modifier ("(" object ")"):? {% p.modifier %}
id_expr -> %identifier_expr {% id %}
Expand Down
86 changes: 60 additions & 26 deletions packages/parser/src/grammar/lex/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,78 @@ export const until = (term: RegExp, opts: UntilOptions = {}) => {
}

type TemplateUntilOptions = {
interpSymbols?: string[]
interpTemplate?: boolean
interpParams?: boolean
next?: string
}

export const templateUntil = (
term: RegExp,
{ interpSymbols = ['$'], next }: TemplateUntilOptions = {},
) => ({
term: {
defaultType: 'literal',
match: new RegExp(`(?=${term.source})`),
lineBreaks: true,
...(next ? { next } : { pop: 1 }),
},
interpexpr: {
match: '${',
push: 'interpExpr',
},
interpvar: {
match: new RegExp(`[${interpSymbols.join('')}]\\w+`),
value: (text: string) => text.slice(1),
},
literal: {
match: until(
new RegExp(`[${interpSymbols.join('')}]\\w|\\$|${term.source}`),
),
value: (text: string) => text.replace(/\\(.)/g, '$1').replace(/\s/g, ' '),
lineBreaks: true,
},
})
opts: TemplateUntilOptions = {},
) => {
const { interpTemplate = true, interpParams = false, next } = opts
const interpSymbols = interpParams ? ['$:'] : ['$']
let interpTmplPush: string | undefined
if (interpTemplate) {
interpTmplPush = interpParams ? 'interpTmplParams' : 'interpTmpl'
}

return {
term: {
defaultType: 'literal',
match: new RegExp(`(?=${term.source})`),
lineBreaks: true,
...(next ? { next } : { pop: 1 }),
},
interpexpr: {
match: '${',
push: 'interpExpr',
},
...(interpTmplPush
? {
interptmpl: {
match: '$[',
push: interpTmplPush,
},
}
: {}),
interpvar: {
match: new RegExp(`[${interpSymbols.join('')}]\\w+`),
value: (text: string) => text.slice(1),
},
literal: {
match: until(
new RegExp(`[${interpSymbols.join('')}]\\w|\\$|${term.source}`),
),
value: (text: string) => text.replace(/\\(.)/g, '$1').replace(/\s/g, ' '),
lineBreaks: true,
},
}
}

// limited support for now, eventually to support expressions such as:
// ${a + b}
export const interpExpr = {
ws,
identifier,
rbrack: {
rbrace: {
match: '}',
pop: 1,
},
}

export const interpTmpl = {
rbrack: {
match: ']',
pop: 1,
},
...templateUntil(/]/),
}

export const interpTmplParams = {
rbrack: {
match: ']',
pop: 1,
},
...templateUntil(/]/, { interpParams: true }),
}
17 changes: 12 additions & 5 deletions packages/parser/src/grammar/lexer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import moo from 'moo'
import { identifier, identifierExpr, popAll, ws } from './lex/shared.js'
import { slice, slice_block } from './lex/slice.js'
import { interpExpr, templateUntil } from './lex/templates.js'
import {
interpExpr,
interpTmpl,
interpTmplParams,
templateUntil,
} from './lex/templates.js'

const verbs = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE']
const keywords = ['import', 'inputs', 'set']
Expand All @@ -10,7 +15,7 @@ const modifiers = ['html', 'json', 'js', 'cookies', 'link', 'headers']
const keywordsObj = Object.fromEntries(keywords.map(k => [`kw_${k}`, k]))

const exprOpeners = {
lbrack: '{',
lbrace: '{',
lparent: '(',
identifier_expr: {
match: identifierExpr,
Expand Down Expand Up @@ -53,7 +58,7 @@ const main = {
push: 'expr',
},
comma: ',',
rbrack: '}',
rbrace: '}',
rparen: ')',
optmark: '?',
...exprOpeners,
Expand Down Expand Up @@ -118,13 +123,15 @@ const lexer: any = moo.states({
$all: { err: moo.error },
main,
expr,
template: templateUntil(/\n|->|=>/),
template: templateUntil(/\n|->|=>/, { interpTemplate: false }),
request,
requestUrl: templateUntil(/\n/, { interpSymbols: ['$:'], next: 'request' }),
requestUrl: templateUntil(/\n/, { interpParams: true, next: 'request' }),
requestKey: templateUntil(/:/),
requestValue: templateUntil(/\n/),
requestBody: templateUntil(/\n[^\S\r\n]*\[\/body\]/),
interpExpr,
interpTmpl,
interpTmplParams,
})

export default lexer
Loading

0 comments on commit d561a12

Please sign in to comment.