improve template, lines, alignment processing with tests
This commit is contained in:
parent
d6d7086d98
commit
833c3b03bf
4 changed files with 204 additions and 74 deletions
|
|
@ -225,7 +225,7 @@ export default processLines
|
|||
const
|
||||
ALIGNMENT_REGEX = /\s*\|(CENTER|CENTRE|RIGHT|LEFT)$/m,
|
||||
// %, followed by a letter, optionally followed by letters/numbers, followed by whitespace or EoI
|
||||
ARG_REGEX = /(%[a-zA-Z]\w*)(?:$|\s)/gm
|
||||
ARG_REGEX = /(%[a-zA-Z]\w*)/gm
|
||||
|
||||
export const processTemplate = blockDefinition => {
|
||||
const { template, inputs={}, fields={} } = blockDefinition
|
||||
|
|
@ -233,92 +233,113 @@ export const processTemplate = blockDefinition => {
|
|||
|
||||
const
|
||||
msgAndArgs = {},
|
||||
covered = []
|
||||
// break template on newlines, each line:
|
||||
niceTemplate(template).split("\n").forEach((line, idx) => {
|
||||
// - extract alignment
|
||||
let alignment = DEFAULT_ALIGNMENT
|
||||
const alignMatches = line.match(ALIGNMENT_REGEX)
|
||||
if(alignMatches) {
|
||||
// remember the alignment
|
||||
alignment = alignMatches[1]
|
||||
// purge from the line
|
||||
line = line.replace(alignMatches[0], "")
|
||||
}
|
||||
alignment = alignment.replace("ER", "RE") // anglocize CENTER
|
||||
covered = [],
|
||||
// break template on newlines
|
||||
givenLines = niceTemplate(template).split("\n")
|
||||
|
||||
// - extract data refs, replace with indices
|
||||
const matches = line.match(ARG_REGEX) || []
|
||||
matches.forEach((match, matchIdx) => {
|
||||
line = line.replace(trim(match), `%${matchIdx+1}`)
|
||||
})
|
||||
let lineIndex = 0
|
||||
|
||||
// - append default index if no others present
|
||||
if(!line.includes("%1")) {
|
||||
line += " %1"
|
||||
}
|
||||
|
||||
msgAndArgs[`message${idx}`] = line
|
||||
|
||||
// build up the args for this line
|
||||
const args = []
|
||||
givenLines.forEach(lineWithAlignment => {
|
||||
const
|
||||
[line, alignment] = breakLineAndAlignment(lineWithAlignment),
|
||||
matches = line.match(ARG_REGEX) || [],
|
||||
subLines = []
|
||||
|
||||
// create a sub-line each time we encounter an input
|
||||
let endOfLastMatch = 0
|
||||
matches.forEach(match => {
|
||||
const matchName = trim(match.slice(1)) // strip the leading %
|
||||
// early out if not an input
|
||||
if (!inputs[matchName]) { return }
|
||||
|
||||
// error if already covered
|
||||
if(covered.includes(matchName)) {
|
||||
throw new Error(`Duplicate input/field name (${matchName}) referenced in template (for block: ${blockDefinition.type})`)
|
||||
}
|
||||
|
||||
// find the match input or field
|
||||
if(inputs[matchName]) {
|
||||
// add the input arg
|
||||
args.push({
|
||||
type: "input_value",
|
||||
name: matchName,
|
||||
align: alignment
|
||||
})
|
||||
|
||||
} else if(fields[matchName]) {
|
||||
// add the field arg
|
||||
const
|
||||
fieldData = fields[matchName],
|
||||
type = fieldTypeFromProperties(fieldData)
|
||||
|
||||
args.push({
|
||||
name: matchName,
|
||||
type,
|
||||
checked: fieldData.checked,
|
||||
options: fieldData.options.map(option => option.slice(0,2)), // slice out option documentation
|
||||
text: fieldData.text || fieldData.multiline_text || fieldData.label || "",
|
||||
spellcheck: fieldData.spellcheck,
|
||||
value: fieldData.value,
|
||||
})
|
||||
|
||||
} else {
|
||||
throw new Error(`No input or field with name ${matchName} (processing block: ${blockDefinition.type})`)
|
||||
}
|
||||
|
||||
// remember we covered this one
|
||||
covered.push(matchName)
|
||||
// add a sub-line up to it
|
||||
const endOfMatch = line.indexOf(match) + match.length
|
||||
subLines.push(line.slice(endOfLastMatch, endOfMatch))
|
||||
endOfLastMatch = endOfMatch
|
||||
})
|
||||
|
||||
// add a dummy if no others
|
||||
if(!args.length){
|
||||
args.push({
|
||||
type: "input_dummy",
|
||||
align: alignment
|
||||
})
|
||||
// add another line if there is content after the last input
|
||||
if(endOfLastMatch < line.length) {
|
||||
subLines.push(line.slice(endOfLastMatch, line.length))
|
||||
}
|
||||
|
||||
msgAndArgs[`args${idx}`] = args
|
||||
subLines.forEach(subLine => {
|
||||
// rematch for just this sub-line
|
||||
const subMatches = subLine.match(ARG_REGEX) || []
|
||||
// - extract data refs, replace with indices
|
||||
subMatches.forEach((match, matchIdx) => {
|
||||
subLine = subLine.replace(trim(match), `%${matchIdx+1}`)
|
||||
})
|
||||
|
||||
// - append default index if no others present
|
||||
if(!subLine.includes("%1")) {
|
||||
subLine += " %1"
|
||||
}
|
||||
|
||||
msgAndArgs[`message${lineIndex}`] = subLine
|
||||
|
||||
// build up the args for this line
|
||||
const args = []
|
||||
|
||||
subMatches.forEach(match => {
|
||||
const matchName = trim(match.slice(1)) // strip the leading %
|
||||
|
||||
// error if already covered
|
||||
if(covered.includes(matchName)) {
|
||||
throw new Error(`Duplicate input/field name (${matchName}) referenced in template (for block: ${blockDefinition.type})`)
|
||||
}
|
||||
|
||||
// find the match input or field
|
||||
if(inputs[matchName]) {
|
||||
// add the input arg
|
||||
args.push({
|
||||
type: "input_value",
|
||||
name: matchName,
|
||||
align: alignment
|
||||
})
|
||||
|
||||
} else if(fields[matchName]) {
|
||||
// add the field arg
|
||||
const
|
||||
fieldData = fields[matchName],
|
||||
type = fieldTypeFromProperties(fieldData)
|
||||
|
||||
args.push({
|
||||
name: matchName,
|
||||
type,
|
||||
checked: fieldData.checked,
|
||||
options: fieldData.options?.map(option => option.slice(0,2)), // slice out option documentation
|
||||
text: fieldData.text || fieldData.multiline_text || fieldData.label || "",
|
||||
spellcheck: fieldData.spellcheck,
|
||||
value: fieldData.value,
|
||||
})
|
||||
|
||||
} else {
|
||||
throw new Error(`No input or field with name ${matchName} (processing block: ${blockDefinition.type})`)
|
||||
}
|
||||
|
||||
// remember we covered this one
|
||||
covered.push(matchName)
|
||||
})
|
||||
|
||||
// add a dummy if no others
|
||||
if(!args.length){
|
||||
args.push({
|
||||
type: "input_dummy",
|
||||
align: alignment
|
||||
})
|
||||
}
|
||||
|
||||
msgAndArgs[`args${lineIndex}`] = args
|
||||
|
||||
lineIndex += 1
|
||||
})
|
||||
})
|
||||
|
||||
// warn on any uncovered inputs or fields
|
||||
const unusedInputs = without(keys(inputs), ...covered)
|
||||
if(!isEmpty(unusedInputs)) {
|
||||
throw new Error(`Some inputs where not used in the template: ${unusedInputs}`)
|
||||
throw new Error(`Some inputs were not used in the template: ${unusedInputs}`)
|
||||
}
|
||||
const unusedFields = without(keys(fields), ...covered)
|
||||
if(!isEmpty(unusedFields)) {
|
||||
|
|
@ -328,6 +349,20 @@ export const processTemplate = blockDefinition => {
|
|||
return msgAndArgs
|
||||
}
|
||||
|
||||
const breakLineAndAlignment = line => {
|
||||
// - extract alignment
|
||||
let alignment = DEFAULT_ALIGNMENT
|
||||
const alignMatches = line.match(ALIGNMENT_REGEX)
|
||||
if(alignMatches) {
|
||||
// remember the alignment
|
||||
alignment = alignMatches[1]
|
||||
// purge from the line
|
||||
line = line.replace(alignMatches[0], "")
|
||||
}
|
||||
|
||||
return [ line, alignment.replace("ER", "RE")] // anglocize CENTER
|
||||
}
|
||||
|
||||
const fieldTypeFromProperties = fieldData => {
|
||||
const fieldKeys = keys(fieldData)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export const
|
|||
firstLineBlank = /^\s*$/.test(lines[0]),
|
||||
remainingLines = lines.slice(1, -1),
|
||||
indentCounts = map(remainingLines, line => line.search(/\S/)),
|
||||
firstLineLeastIndented = indentCounts[0] >= Math.min(...indentCounts.slice(1, -1))
|
||||
firstLineLeastIndented = indentCounts[0] >= Math.min(...indentCounts)
|
||||
|
||||
// ensure first line is blank and every other line has at least as much whitespace as the first line
|
||||
if(firstLineBlank && firstLineLeastIndented) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { describe, it } from 'node:test'
|
||||
import { range, flatMap } from 'lodash-es'
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { processTemplate } from '#src/exporters/block_processor/lines.js'
|
||||
|
|
@ -24,7 +25,7 @@ const TEMPLATE_FIXTURE = {
|
|||
},
|
||||
}
|
||||
|
||||
describe("Block template processing", { only: true }, () => {
|
||||
describe("Block template processing", () => {
|
||||
it("works", () => {
|
||||
const templateProperties = processTemplate(TEMPLATE_FIXTURE)
|
||||
|
||||
|
|
@ -54,4 +55,63 @@ describe("Block template processing", { only: true }, () => {
|
|||
}
|
||||
])
|
||||
})
|
||||
|
||||
it("template to block examples", () => {
|
||||
const template = `
|
||||
%A %B %C %D
|
||||
`
|
||||
|
||||
const givenPropsExpectLineCount = [
|
||||
[{ inputs: ["A", "B", "C", "D"]}, 4],
|
||||
[{ inputs: ["A", "B"], fields: ["C", "D"]}, 3],
|
||||
[{ inputs: [], fields: ["A", "B", "C", "D"]}, 1],
|
||||
[{ inputs: ["A"], fields: ["B", "C", "D"]}, 2],
|
||||
[{ inputs: ["D"], fields: ["A", "B", "C"]}, 1],
|
||||
]
|
||||
|
||||
givenPropsExpectLineCount.forEach(([given, expect]) => {
|
||||
const
|
||||
inputs = given.inputs.reduce((acc, keyName) => {
|
||||
acc[keyName] = {}
|
||||
return acc
|
||||
}, {}),
|
||||
fields = given.fields?.reduce((acc, keyName) => {
|
||||
acc[keyName] = { type: 'blah' }
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
assertNumberOfLines(processTemplate({
|
||||
template,
|
||||
inputs,
|
||||
fields
|
||||
}), expect)
|
||||
})
|
||||
})
|
||||
|
||||
it("splits multiple inputs on 1 line into multiple messages/args", () => {
|
||||
const blockProps = processTemplate({
|
||||
template: `%A %B`,
|
||||
inputs: { A: {}, B: {} }
|
||||
})
|
||||
|
||||
assertNumberOfLines(blockProps, 2)
|
||||
})
|
||||
|
||||
it("data prop detection", () => {
|
||||
const
|
||||
template = "(%A,%B)",
|
||||
inputs = { A: {}, B: {} },
|
||||
blockProps = processTemplate({ template, inputs })
|
||||
|
||||
assert.equal(blockProps.message0, "(%1")
|
||||
assert.equal(blockProps.message1, ",%1")
|
||||
assert.equal(blockProps.message2, ") %1")
|
||||
})
|
||||
})
|
||||
|
||||
const assertNumberOfLines = (blockProps, expectedLines) => {
|
||||
const keys = flatMap(range(expectedLines), index =>
|
||||
[ `message${index}`, `args${index}` ]
|
||||
)
|
||||
assert.hasAllKeys(blockProps, keys, `Expected block properties to have ${expectedLines} lines, had ${Object.keys(blockProps).length/2}`)
|
||||
}
|
||||
|
|
|
|||
35
test/src/nice_template.test.js
Normal file
35
test/src/nice_template.test.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, it } from 'node:test'
|
||||
import { assert } from 'chai'
|
||||
|
||||
import { niceTemplate } from '#src/util.js'
|
||||
|
||||
|
||||
describe("Nice Templates", () => {
|
||||
it("examples", () => {
|
||||
const givenTemplateExpectRender = [
|
||||
[ // drops empty leading/trailing line, trims leading whitespace
|
||||
`
|
||||
ABC
|
||||
`, "ABC"
|
||||
],
|
||||
[ // keeps newlines in content
|
||||
`
|
||||
ABC
|
||||
DEF
|
||||
`, "ABC\nDEF"
|
||||
],
|
||||
[ // keeps extra whitespace after first line
|
||||
`
|
||||
ABC
|
||||
DEF
|
||||
`, "ABC\n DEF"
|
||||
],
|
||||
]
|
||||
|
||||
givenTemplateExpectRender.forEach(([given, expect]) => {
|
||||
assert.equal(niceTemplate(given), expect,
|
||||
`Expected template \`${given}\` to produce output "${expect}"`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue