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());