Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix @-ignoring expressions #157

Merged
merged 6 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/backup.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ function makeLocalsBackup (keys, locals) {
function revertBackupedLocals (keys, locals, backup) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// remove key from locals
// Remove key from locals
delete locals[key]

// revert copied key value
// Revert copied key value
if (Object.prototype.hasOwnProperty.call(backup, key)) locals[key] = backup[key]
}

Expand Down
128 changes: 70 additions & 58 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,33 +119,35 @@ module.exports = function postHTMLExpressions (options) {
removeScriptLocals: false
}, options)

// set tags
// Set tags
loops = options.loopTags
scopes = options.scopeTags
conditionals = options.conditionalTags
switches = options.switchTags
ignored = options.ignoredTag

// make a RegExp's to search for placeholders
// Define regex to search for placeholders
let before = escapeRegexpString(options.delimiters[0])
let after = escapeRegexpString(options.delimiters[1])

const delimitersRegexp = new RegExp(`(?<!@)${before}(.+?)${after}`, 'g')
const delimitersRegexp = new RegExp(`(?<!@${options.delimiters[0][0]}?)${before}(.+?)${after}`, 'g')

before = escapeRegexpString(options.unescapeDelimiters[0])
after = escapeRegexpString(options.unescapeDelimiters[1])

const unescapeDelimitersRegexp = new RegExp(`(?<!@)${before}(.+?)${after}`, 'g')
const unescapeDelimitersRegexp = new RegExp(`(?<!@${options.unescapeDelimiters[0][0]}?)${before}(.+?)${after}`, 'g')

// make array of delimiters
// Create an array of delimiters
const delimiters = [
{ text: options.delimiters, regexp: delimitersRegexp, escape: true },
{ text: options.unescapeDelimiters, regexp: unescapeDelimitersRegexp, escape: false }
]

// we arrange delimiter search order by length, since it's possible that one
// delimiter could 'contain' another delimiter, like '{{' and '{{{'. But if
// you sort by length, the longer one will always match first.
/**
* We arrange delimiter search order by length, since it's possible that one
* delimiter could 'contain' another delimiter, like `{{{` contains `{{`.
* But if we sort by length, the longer one will always match first.
*/
if (options.delimiters.join().length > options.unescapeDelimiters.join().length) {
delimitersSettings[0] = delimiters[0]
delimitersSettings[1] = delimiters[1]
Expand All @@ -157,7 +159,7 @@ module.exports = function postHTMLExpressions (options) {
delimitersReplace = new RegExp(`@${escapeRegexpString(delimitersSettings[1].text[0])}`, 'g')
unescapeDelimitersReplace = new RegExp(`@${escapeRegexpString(delimitersSettings[0].text[0])}`, 'g')

// kick off the parsing
// Kick off the parsing
return function (tree) {
const { locals } = scriptDataLocals(tree, options)

Expand All @@ -174,26 +176,28 @@ module.exports = function postHTMLExpressions (options) {
}

function walk (opts, nodes) {
// the context in which expressions are evaluated
// The context in which expressions are evaluated
const ctx = vm.createContext(opts.locals)

// After a conditional has been resolved, we remove the conditional elements
// from the tree. This variable determines how many to skip afterwards.
/**
* After a conditional has been resolved, we remove the conditional elements
* from the tree. This variable determines how many to skip afterwards.
* */
let skip

// loop through each node in the tree
// Loop through each node in the tree
return [].concat(nodes).reduce((m, node, i) => {
// if we're skipping this node, return immediately
// If we're skipping this node, return immediately
if (skip) { skip--; return m }

// don't parse ignoredTag
// Don't parse `ignoredTag` from options
if (node.tag === ignored) {
m.push(node)

return m
}

// if we have a string, match and replace it
// If we have a string, match and replace it
if (typeof node === 'string') {
node = placeholders(node, ctx, delimitersSettings, opts)
node = node
Expand All @@ -205,7 +209,7 @@ function walk (opts, nodes) {
return m
}

// if not, we have an object, so we need to run the attributes and contents
// If not, we have an object, so we need to run the attributes and contents
if (node.attrs) {
for (const key in node.attrs) {
if (typeof node.attrs[key] === 'string') {
Expand All @@ -215,7 +219,7 @@ function walk (opts, nodes) {
.replace(delimitersReplace, delimitersSettings[1].text[0])
}

// if key is parametr
// If `key` is a parameter
const _key = placeholders(key, ctx, delimitersSettings, opts)
if (key !== _key) {
node.attrs[_key] = node.attrs[key]
Expand All @@ -224,27 +228,31 @@ function walk (opts, nodes) {
}
}

// if the node has content, recurse (unless it's a loop, handled later)
// If the node has content, recurse (unless it's a loop, which we handle later)
if (node.content && loops.includes(node.tag) === false && node.tag !== scopes[0]) {
node.content = walk(opts, node.content)
}

// if we have an element matching "if", we've got a conditional
// this comes after the recursion to correctly handle nested loops
/**
* If we have an element matching `<if>`, we've got a conditional; this
* comes after the recursion, to correctly handle nested loops.
* */
if (node.tag === conditionals[0]) {
// throw an error if it's missing the "condition" attribute
// Throw an error if it's missing the "condition" attribute
if (!(node.attrs && node.attrs.condition)) {
throw new Error(`the "${conditionals[0]}" tag must have a "condition" attribute`)
}

// сalculate the first path of condition expression
// Calculate the first path of condition expression
let expressionIndex = 1
let expression = `if (${node.attrs.condition}) { 0 } `

const branches = [node.content]

// move through the nodes and collect all others that are part of the same
// conditional statement
/**
* Move through the nodes and collect all others that
* are part of the same conditional statement
* */
let computedNextTag = getNextTag(nodes, ++i)

let current = computedNextTag[0]
Expand All @@ -254,24 +262,26 @@ function walk (opts, nodes) {
let statement = nextTag.tag
let condition = ''

// ensure the "else" tag is represented in our little AST as 'else',
// even if a custom tag was used
/**
* Ensure the "else" tag is represented in our little AST as 'else',
* even if a custom tag was used.
* */
if (nextTag.tag === conditionals[2]) statement = 'else'

// add the condition if it's an else if
// Add the condition if it's an else if
if (nextTag.tag === conditionals[1]) {
// throw an error if an "else if" is missing a condition
// Throw an error if an "else if" is missing a condition
if (!(nextTag.attrs && nextTag.attrs.condition)) {
throw new Error(`the "${conditionals[1]}" tag must have a "condition" attribute`)
}
condition = nextTag.attrs.condition

// while we're here, expand "elseif" to "else if"
// While we're here, expand "elseif" to "else if"
statement = 'else if'
}
branches.push(nextTag.content)

// calculate next part of condition expression
// Calculate next part of condition expression
expression += statement + (condition ? ` (${condition})` : '') + ` { ${expressionIndex++} } `

computedNextTag = getNextTag(nodes, ++current)
Expand All @@ -280,7 +290,7 @@ function walk (opts, nodes) {
nextTag = computedNextTag[1]
}

// evaluate the expression, get the winning condition branch
// Evaluate the expression and get the winning condition branch
let branch
try {
branch = branches[vm.runInContext(expression, ctx)]
Expand All @@ -290,25 +300,27 @@ function walk (opts, nodes) {
}
}

// remove all of the conditional tags from the tree
// we subtract 1 from i as it's incremented from the initial if statement
// in order to get the next node
/**
* Remove all of the conditional tags from the tree.
* We subtract 1 from i as it's incremented from the initial if statement
* in order to get the next node.
* */
skip = current - i

// recursive evaluate of condition branch
// Recursive evaluate of condition branch
if (branch) Array.prototype.push.apply(m, walk(opts, branch))

return m
}

// switch tag
// Switch tag
if (node.tag === switches[0]) {
// throw an error if it's missing the "expression" attribute
// Throw an error if it's missing the "expression" attribute
if (!(node.attrs && node.attrs.expression)) {
throw new Error(`the "${switches[0]}" tag must have a "expression" attribute`)
}

// сalculate the first path of condition expression
// Calculate the first path of condition expression
let expressionIndex = 0
let expression = `switch(${node.attrs.expression}) {`

Expand All @@ -321,7 +333,7 @@ function walk (opts, nodes) {
}

if (currentNode.tag === switches[1]) {
// throw an error if it's missing the "n" attribute
// Throw an error if it's missing the "n" attribute
if (!(currentNode.attrs && currentNode.attrs.n)) {
throw new Error(`the "${switches[1]}" tag must have a "n" attribute`)
}
Expand All @@ -336,23 +348,23 @@ function walk (opts, nodes) {

expression += '}'

// evaluate the expression, get the winning switch branch
// Evaluate the expression, get the winning switch branch
const branch = branches[vm.runInContext(expression, ctx)]

// recursive evaluate of branch
// Recursive evaluate of branch
Array.prototype.push.apply(m, walk(opts, branch.content))

return m
}

// parse loops
// Parse loops
if (loops.includes(node.tag)) {
// handle syntax error
// Handle syntax error
if (!(node.attrs && node.attrs.loop)) {
throw new Error(`the "${node.tag}" tag must have a "loop" attribute`)
}

// parse the "loop" param
// Parse the "loop" param
const loopParams = parseLoopStatement(node.attrs.loop)
let target = {}
try {
Expand All @@ -363,7 +375,7 @@ function walk (opts, nodes) {
}
}

// handle additional syntax errors
// Handle additional syntax errors
if (typeof target !== 'object' && opts.strictMode) {
throw new Error('You must provide an array or object to loop through')
}
Expand All @@ -372,14 +384,14 @@ function walk (opts, nodes) {
throw new Error('You must provide at least one loop argument')
}

// converts nodes to a string. These nodes will be changed within the loop
// Converts nodes to a string. These nodes will be changed within the loop
const treeString = JSON.stringify(node.content)
const keys = loopParams.keys

// creates a copy of the keys that will be changed within the loop
// Creates a copy of the keys that will be changed within the loop
const localsBackup = makeLocalsBackup(keys, opts.locals)

// run the loop, different types of loops for arrays and objects
// Run the loop, different types of loops for arrays and objects
if (Array.isArray(target)) {
for (let index = 0; index < target.length; index++) {
opts.locals.loop = getLoopMeta(index, target)
Expand All @@ -392,42 +404,42 @@ function walk (opts, nodes) {
}
}

// returns the original keys values that was changed within the loop
// Returns the original keys values that was changed within the loop
opts.locals = revertBackupedLocals(keys, opts.locals, localsBackup)

// return directly out of the loop, which will skip the "each" tag
// Return directly out of the loop, which will skip the "each" tag
return m
}

// parse scopes
// Parse scopes
if (node.tag === scopes[0]) {
// handle syntax error
// Handle syntax error
if (!node.attrs || !node.attrs.with) {
throw new Error(`the "${scopes[0]}" tag must have a "with" attribute`)
}

const target = vm.runInContext(node.attrs.with, ctx)

// handle additional syntax errors
// Handle additional syntax errors
if (typeof target !== 'object' || Array.isArray(target)) {
throw new Error('You must provide an object to make scope')
}

const keys = Object.keys(target)

// creates a copy of the keys that will be changed within the loop
// Creates a copy of the keys that will be changed within the loop
const localsBackup = makeLocalsBackup(keys, opts.locals)

m.push(executeScope(target, opts.locals, node))

// returns the original keys values that was changed within the loop
// Returns the original keys values that was changed within the loop
opts.locals = revertBackupedLocals(keys, opts.locals, localsBackup)

// return directly out of the loop, which will skip the "scope" tag
// Return directly out of the loop, which will skip the "scope" tag
return m
}

// return the node
// Return the node
m.push(node)

return m
Expand Down
6 changes: 3 additions & 3 deletions lib/loops.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
* @return {Object} {} Keys && Expression
*/
function parseLoopStatement (input) {
// try to find ` in ` keyword
// Try to find ` in ` keyword
const inKeywordIndex = input.search(/\sin\s/)

// if we reach the end of the string without getting "in", it's an error
// If we reach the end of the string without getting "in", it's an error
if (inKeywordIndex === -1) {
throw new Error("Loop statement lacking 'in' keyword")
}

// expression is always after `in` keyword
// Expression is always after `in` keyword
const expression = input.substr(inKeywordIndex + 4)

// keys is always before `in` keyword
Expand Down
6 changes: 4 additions & 2 deletions lib/placeholders.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ function escapeHTML (unescaped) {
* @return {String} input Replaced Input
*/
function placeholders (input, ctx, settings, opts) {
// Since we are matching multiple sets of delimiters, we need to run a loop
// here to match each one.
/**
* Since we are matching multiple sets of delimiters,
* we need to run a loop here to match each one.
*/
for (let i = 0; i < settings.length; i++) {
const matches = input.match(settings[i].regexp)

Expand Down
2 changes: 1 addition & 1 deletion lib/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* @return {Array} [] Array containing the next tag
*/
function getNextTag (nodes, i) {
// loop until we get the next tag (bypassing newlines etc)
// Loop until we get the next tag (bypassing newlines etc)
while (i < nodes.length) {
const node = nodes[i]

Expand Down
Loading
Loading