diff --git a/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.js b/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.js index 1b01d9c5..d3c4f497 100644 --- a/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.js +++ b/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.js @@ -1,17 +1,26 @@ import { Tokenizer } from 'liquidjs'; +// Homebrewed pretty-regex writer const tokens = { - END: /{{\s*end\s*}}/, - BEGIN: /{{\s*(if)/, - BEGIN_SCOPED: /{{\s*(range|with|define|block|template)/, - LOOP: /{{\s*range\s+(.+?)\s*}}/, - INDEX_LOOP: /{{\s*range\s+(\$.+), \$.+ := (.+?)\s*}}/, - ASSIGN: /{{\s*(\$\S+)\s+:=\s+(.+?)\s*}}/, - WITH: /{{\s*with\s+(.+?)\s*}}/, - BOOKSHOP: /{{\s*partial\s+"bookshop"\s+\(\s*slice\s+"(.+?)" (.+?)\s*\)\s*}}/, - BOOKSHOP_SCOPED: /{{\s*partial\s+"bookshop"\s+\(?\s*\.\s*\)?\s*}}/, + END: `{{ end }}`, + BEGIN: `{{ (if)`, + BEGIN_SCOPED: `{{ (range|with|define|block|template)`, + LOOP: `{{ range () }}`, + INDEX_LOOP: `{{ range (\\$.+), \\$.+ := () }}`, + ASSIGN: `{{ (\\$\\S+) := () }}`, + REASSIGN: `{{ (\\$\\S+) = () }}`, + WITH: `{{ with () }}`, + BOOKSHOP: `{{ partial "bookshop" \\( slice "()" () \\) }}`, + BOOKSHOP_SCOPED: `{{ partial "bookshop" \\(? \\. \\)? }}`, } +const TOKENS = {}; +Object.entries(tokens).forEach(([name, r]) => { + TOKENS[name] = new RegExp(r + .replace(/\(\)/g, '([\\S\\s]+?)') // Empty capturing group defaults to lazy multiline capture + .replace(/ /g, '[\\n\\r\\s]+') // Two spaces actually means one or more blanks + .replace(/ /g, '[\\n\\r\\s]*')); // One space means zero or more blanks +}); /** * Parse a go text/template using the liquidjs parser * that we already have in the bundle. @@ -25,44 +34,53 @@ const rewriteTag = function (token, src, endTags, liveMarkup) { // Skip non-value tags if (token.kind !== 8) return outputToken; - if (tokens.END.test(raw)) { + if (TOKENS.END.test(raw)) { endTags.push(outputToken); return outputToken; } - if (tokens.BEGIN.test(raw)) { + if (TOKENS.BEGIN.test(raw)) { endTags.pop(); } - if (tokens.BEGIN_SCOPED.test(raw)) { + if (TOKENS.BEGIN_SCOPED.test(raw)) { outputToken.text = `${outputToken.text}{{ \`\` | safeHTML }}`; let matchingEnd = endTags.pop(); matchingEnd.text = `{{ \`\` | safeHTML }}${matchingEnd.text}`; } - if (liveMarkup && tokens.INDEX_LOOP.test(raw)) { - let [, index_variable, iterator] = raw.match(tokens.INDEX_LOOP); + if (liveMarkup && TOKENS.INDEX_LOOP.test(raw)) { + let [, index_variable, iterator] = raw.match(TOKENS.INDEX_LOOP); + const r = required_wrapper_hugo_func(iterator); outputToken.text = [`${outputToken.text}`, - `{{ (printf \`\` ${index_variable}) | safeHTML }}` + `{{${r[0]} (printf \`\` ${index_variable})${r[1]} | safeHTML }}` ].join('') - } else if (liveMarkup && tokens.LOOP.test(raw)) { - let [, iterator] = raw.match(tokens.LOOP); + } else if (liveMarkup && TOKENS.LOOP.test(raw)) { + let [, iterator] = raw.match(TOKENS.LOOP); + const r = required_wrapper_hugo_func(iterator); outputToken.text = [`{{ $bookshop__live__iterator := 0 }}`, `${outputToken.text}`, - `{{ (printf \`\` $bookshop__live__iterator) | safeHTML }}`, + `{{${r[0]} (printf \`\` $bookshop__live__iterator)${r[1]} | safeHTML }}`, `{{ $bookshop__live__iterator = (add $bookshop__live__iterator 1) }}` ].join('') - } else if (liveMarkup && tokens.ASSIGN.test(raw)) { - let [, identifier, value] = raw.match(tokens.ASSIGN); - outputToken.text = `${outputToken.text}{{ \`\` | safeHTML }}` - } else if (liveMarkup && tokens.WITH.test(raw)) { - let [, value] = raw.match(tokens.WITH); - outputToken.text = `${outputToken.text}{{ \`\` | safeHTML }}` - } else if (liveMarkup && tokens.BOOKSHOP.test(raw)) { - let [, name, params] = raw.match(tokens.BOOKSHOP); - outputToken.text = `{{ \`\` | safeHTML }}${outputToken.text}{{ \`\` | safeHTML }}` - } else if (liveMarkup && tokens.BOOKSHOP_SCOPED.test(raw)) { + } else if (liveMarkup && TOKENS.ASSIGN.test(raw)) { + let [, identifier, value] = raw.match(TOKENS.ASSIGN); + const r = required_wrapper_hugo_func(value); + outputToken.text = `${outputToken.text}{{${r[0]} \`\`${r[1]} | safeHTML }}` + } else if (liveMarkup && TOKENS.REASSIGN.test(raw)) { + let [, identifier, value] = raw.match(TOKENS.REASSIGN); + const r = required_wrapper_hugo_func(value); + outputToken.text = `${outputToken.text}{{${r[0]} \`\`${r[1]} | safeHTML }}` + } else if (liveMarkup && TOKENS.WITH.test(raw)) { + let [, value] = raw.match(TOKENS.WITH); + const r = required_wrapper_hugo_func(value); + outputToken.text = `${outputToken.text}{{${r[0]} \`\`${r[1]} | safeHTML }}` + } else if (liveMarkup && TOKENS.BOOKSHOP.test(raw)) { + let [, name, params] = raw.match(TOKENS.BOOKSHOP); + const r = required_wrapper_hugo_func(params); + outputToken.text = `{{${r[0]} \`\`${r[1]} | safeHTML }}${outputToken.text}{{ \`\` | safeHTML }}` + } else if (liveMarkup && TOKENS.BOOKSHOP_SCOPED.test(raw)) { outputToken.text = [`{{ if reflect.IsSlice . }}{{ (printf \`\` (index . 0)) | safeHTML }}`, `{{- else if reflect.IsMap . -}}{{ (printf \`\` ._bookshop_name) | safeHTML }}{{ end }}`, `${outputToken.text}`, @@ -73,6 +91,12 @@ const rewriteTag = function (token, src, endTags, liveMarkup) { return outputToken; } +// limit comments to one line & escape backticks to something we undo later +const tidy = val => val.replace(/[\r\n]/g, ' ').replace(/`/g, 'BKSH_BACKTICK'); + +// The replace function we need to add to undo the backtick tidy above +const required_wrapper_hugo_func = val => /`/.test(val) ? [` replace`, ` "BKSH_BACKTICK" "\`"`] : [``, ``]; + export default function (text, opts) { opts = { liveMarkup: true, diff --git a/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.test.js b/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.test.js index 5b64a50c..cc9abe93 100644 --- a/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.test.js +++ b/javascript-modules/engines/hugo-engine/lib/translateTextTemplate.test.js @@ -36,6 +36,10 @@ test("add live markup to assigns", t => { input = `{{ $a := .b | chomp }}`; expected = `{{ $a := .b | chomp }}{{ \`\` | safeHTML }}`; t.is(translateTextTemplate(input, {}), expected); + + input = `{{ $a = .b }}`; + expected = `{{ $a = .b }}{{ \`\` | safeHTML }}`; + t.is(translateTextTemplate(input, {}), expected); }); test("add live markup to withs", t => { @@ -76,6 +80,12 @@ test("add live markup to loops with iterators", t => { t.is(translateTextTemplate(input, {}), expected); }); +test("escape backticks in values", t => { + let input = `{{ $a := "hi\`:)" }}`; + let expected = `{{ $a := "hi\`:)" }}{{ replace \`\` "BKSH_BACKTICK" "\`" | safeHTML }}`; + t.is(translateTextTemplate(input, {}), expected); +}); + test("add live markup to complex end structures", t => { const input = ` {{ range .items }} @@ -108,6 +118,52 @@ test("add live markup to complex end structures", t => { {{ \`\` | safeHTML }}{{end}} {{ end }} +{{ \`\` | safeHTML }}{{ end }}`; + t.is(translateTextTemplate(input, {}), expected); +}); + +test("add live markup to complex components", t => { + const input = ` +{{ $level := default 2 .level }} +{{ $level = string + $level }} +{{ $level_classes := dict + "1" "border-b py-8 text-4xl" + "2" "border-b py-6 text-3xl" + "3" "border-b py-4 text-2xl font-bold" + "4" "border-b text-xl font-bold" +}} +{{ $level_class := "lg" }} +{{ with index $level_classes $level }} + {{ $level_class = . }} +{{ end }} +{{ $open := printf \`\` $level $level_class }} +{{ $close := printf \`\` $level }} +{{ with .copy }} + {{ safeHTML $open }} + {{ markdownify . }} | bookshop + {{ safeHTML $close }} +{{ end }}`; + const expected = ` +{{ $level := default 2 .level }}{{ \`\` | safeHTML }} +{{ $level = string + $level }}{{ \`\` | safeHTML }} +{{ $level_classes := dict + "1" "border-b py-8 text-4xl" + "2" "border-b py-6 text-3xl" + "3" "border-b py-4 text-2xl font-bold" + "4" "border-b text-xl font-bold" +}}{{ \`\` | safeHTML }} +{{ $level_class := "lg" }}{{ \`\` | safeHTML }} +{{ with index $level_classes $level }}{{ \`\` | safeHTML }}{{ \`\` | safeHTML }} + {{ $level_class = . }}{{ \`\` | safeHTML }} +{{ \`\` | safeHTML }}{{ end }} +{{ $open := printf \`\` $level $level_class }}{{ replace \`\` "BKSH_BACKTICK" "\`" | safeHTML }} +{{ $close := printf \`\` $level }}{{ replace \`\` "BKSH_BACKTICK" "\`" | safeHTML }} +{{ with .copy }}{{ \`\` | safeHTML }}{{ \`\` | safeHTML }} + {{ safeHTML $open }} + {{ markdownify . }} | bookshop + {{ safeHTML $close }} {{ \`\` | safeHTML }}{{ end }}`; t.is(translateTextTemplate(input, {}), expected); }); \ No newline at end of file diff --git a/javascript-modules/live/lib/app/core.js b/javascript-modules/live/lib/app/core.js index 97050721..57362fe6 100644 --- a/javascript-modules/live/lib/app/core.js +++ b/javascript-modules/live/lib/app/core.js @@ -115,6 +115,30 @@ const evaluateTemplate = async (liveInstance, documentNode, parentPathStack, tem } } + // Hunt through the stack and try to reassign an existing variable. + // This is currently only done in Hugo templates + for (const [name, identifier] of parseParams(liveTag?.reassign)) { + for (let i = stack.length - 1; i >= 0; i -= 1) { + if (stack[i].scope[name] !== undefined) { + stack[i].scope[name] = await liveInstance.eval(identifier, combinedScope()); + break; + } + } + for (let i = pathStack.length - 1; i >= 0; i -= 1) { + if (pathStack[i][name] !== undefined) { + const normalizedIdentifier = liveInstance.normalize(identifier); + if (typeof normalizedIdentifier === 'object' && !Array.isArray(normalizedIdentifier)) { + Object.values(normalizedIdentifier).forEach(value => { + return storeResolvedPath(name, value, [pathStack[i]]) + }); + } else { + storeResolvedPath(name, normalizedIdentifier, [pathStack[i]]); + } + break; + } + } + } + if (liveTag?.end) { currentScope().endNode = currentNode; await templateBlockHandler(stack.pop());