Skip to content

Commit

Permalink
fix(hugo): improve complex templating support in the live editor
Browse files Browse the repository at this point in the history
  • Loading branch information
bglw committed Feb 24, 2022
1 parent 77ec44b commit 5706627
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 28 deletions.
80 changes: 52 additions & 28 deletions javascript-modules/engines/hugo-engine/lib/translateTextTemplate.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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}{{ \`<!--bookshop-live stack-->\` | safeHTML }}`;

let matchingEnd = endTags.pop();
matchingEnd.text = `{{ \`<!--bookshop-live unstack-->\` | 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 \`<!--bookshop-live context(.: (index (${iterator}) %d))-->\` ${index_variable}) | safeHTML }}`
`{{${r[0]} (printf \`<!--bookshop-live context(.: (index (${tidy(iterator)}) %d))-->\` ${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 context(.: (index (${iterator}) %d))-->\` $bookshop__live__iterator) | safeHTML }}`,
`{{${r[0]} (printf \`<!--bookshop-live context(.: (index (${tidy(iterator)}) %d))-->\` $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}{{ \`<!--bookshop-live context(${identifier}: (${value}))-->\` | safeHTML }}`
} else if (liveMarkup && tokens.WITH.test(raw)) {
let [, value] = raw.match(tokens.WITH);
outputToken.text = `${outputToken.text}{{ \`<!--bookshop-live context(.: (${value}))-->\` | safeHTML }}`
} else if (liveMarkup && tokens.BOOKSHOP.test(raw)) {
let [, name, params] = raw.match(tokens.BOOKSHOP);
outputToken.text = `{{ \`<!--bookshop-live name(${name}) params(.: (${params}))-->\` | safeHTML }}${outputToken.text}{{ \`<!--bookshop-live end-->\` | 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]} \`<!--bookshop-live context(${identifier}: (${tidy(value)}))-->\`${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]} \`<!--bookshop-live reassign(${identifier}: (${tidy(value)}))-->\`${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]} \`<!--bookshop-live context(.: (${tidy(value)}))-->\`${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]} \`<!--bookshop-live name(${name}) params(.: (${tidy(params)}))-->\`${r[1]} | safeHTML }}${outputToken.text}{{ \`<!--bookshop-live end-->\` | safeHTML }}`
} else if (liveMarkup && TOKENS.BOOKSHOP_SCOPED.test(raw)) {
outputToken.text = [`{{ if reflect.IsSlice . }}{{ (printf \`<!--bookshop-live name(%s) params(.: .)-->\` (index . 0)) | safeHTML }}`,
`{{- else if reflect.IsMap . -}}{{ (printf \`<!--bookshop-live name(%s) params(.: .)-->\` ._bookshop_name) | safeHTML }}{{ end }}`,
`${outputToken.text}`,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ test("add live markup to assigns", t => {
input = `{{ $a := .b | chomp }}`;
expected = `{{ $a := .b | chomp }}{{ \`<!--bookshop-live context($a: (.b | chomp))-->\` | safeHTML }}`;
t.is(translateTextTemplate(input, {}), expected);

input = `{{ $a = .b }}`;
expected = `{{ $a = .b }}{{ \`<!--bookshop-live reassign($a: (.b))-->\` | safeHTML }}`;
t.is(translateTextTemplate(input, {}), expected);
});

test("add live markup to withs", t => {
Expand Down Expand Up @@ -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 \`<!--bookshop-live context($a: ("hiBKSH_BACKTICK:)"))-->\` "BKSH_BACKTICK" "\`" | safeHTML }}`;
t.is(translateTextTemplate(input, {}), expected);
});

test("add live markup to complex end structures", t => {
const input = `
{{ range .items }}
Expand Down Expand Up @@ -108,6 +118,52 @@ test("add live markup to complex end structures", t => {
{{ \`<!--bookshop-live unstack-->\` | safeHTML }}{{end}}
{{ end }}
{{ \`<!--bookshop-live unstack-->\` | 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 \`<h%s class="%s">\` $level $level_class }}
{{ $close := printf \`</h%s>\` $level }}
{{ with .copy }}
{{ safeHTML $open }}
{{ markdownify . }} | bookshop
{{ safeHTML $close }}
{{ end }}`;
const expected = `
{{ $level := default 2 .level }}{{ \`<!--bookshop-live context($level: (default 2 .level))-->\` | safeHTML }}
{{ $level = string
$level }}{{ \`<!--bookshop-live reassign($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"
}}{{ \`<!--bookshop-live context($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" }}{{ \`<!--bookshop-live context($level_class: ("lg"))-->\` | safeHTML }}
{{ with index $level_classes $level }}{{ \`<!--bookshop-live stack-->\` | safeHTML }}{{ \`<!--bookshop-live context(.: (index $level_classes $level))-->\` | safeHTML }}
{{ $level_class = . }}{{ \`<!--bookshop-live reassign($level_class: (.))-->\` | safeHTML }}
{{ \`<!--bookshop-live unstack-->\` | safeHTML }}{{ end }}
{{ $open := printf \`<h%s class="%s">\` $level $level_class }}{{ replace \`<!--bookshop-live context($open: (printf BKSH_BACKTICK<h%s class="%s">BKSH_BACKTICK $level $level_class))-->\` "BKSH_BACKTICK" "\`" | safeHTML }}
{{ $close := printf \`</h%s>\` $level }}{{ replace \`<!--bookshop-live context($close: (printf BKSH_BACKTICK</h%s>BKSH_BACKTICK $level))-->\` "BKSH_BACKTICK" "\`" | safeHTML }}
{{ with .copy }}{{ \`<!--bookshop-live stack-->\` | safeHTML }}{{ \`<!--bookshop-live context(.: (.copy))-->\` | safeHTML }}
{{ safeHTML $open }}
{{ markdownify . }} | bookshop
{{ safeHTML $close }}
{{ \`<!--bookshop-live unstack-->\` | safeHTML }}{{ end }}`;
t.is(translateTextTemplate(input, {}), expected);
});
24 changes: 24 additions & 0 deletions javascript-modules/live/lib/app/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down

0 comments on commit 5706627

Please sign in to comment.