1
0
mirror of synced 2025-12-20 10:28:40 -05:00
Files
docs/script/helpers/get-liquid-conditionals.js
2022-05-13 12:40:06 -04:00

153 lines
5.7 KiB
JavaScript

#!/usr/bin/env node
// See https://github.com/harttle/liquidjs/discussions/294#discussioncomment-305068
import { Tokenizer } from 'liquidjs'
const tokenize = (str) => {
const tokenizer = new Tokenizer(str)
return tokenizer.readTopLevelTokens()
}
// Return an array of just the conditional strings.
function getLiquidConditionals(str, tagNames) {
if (!tagNames) throw new Error(`Must provide a tag name!`)
tagNames = Array.isArray(tagNames) ? tagNames : [tagNames]
return tokenize(str)
.filter((token) => tagNames.includes(token.name))
.map((token) => token.args)
}
// Return an array of objects, where the `conditional` prop contains the conditional string,
// and the `text` prop contains the contents between the start tag and the end tag.
function getLiquidConditionalsWithContent(str, tagName) {
if (!tagName) throw new Error(`Must provide a tag name!`)
if (typeof tagName !== 'string') throw new Error(`Must provide a single tag name as a string!`)
const numberOfTags = (str.match(new RegExp(`{%-? ${tagName}`, 'g')) || []).length
if (!numberOfTags) return []
const endTagName = tagName === 'ifversion' || tagName === 'elsif' ? 'endif' : `end${tagName}`
// Get the raw tokens, which includes versions, data tags, etc.,
// Also this captures start tags, content, and end tags as _individual_ tokens, but we want to group them.
const tokens = tokenize(str).map((token) => {
return {
conditional: token.name,
text: token.getText(),
position: token.getPosition(),
}
})
// Parse the raw tokens and group them, so that start tags, content, and end tags are
// all considered to be part of the same block, and return that block.
const grouped = groupTokens(tokens, tagName, endTagName)
// Run recursively so we can also capture nested conditionals.
const nestedConditionals = grouped.flatMap((group) => {
// Remove the start tag and the end tag so we are left with nested tags, if any.
const nested = group.text
.replace(group.conditional, '')
.split('')
.reverse()
.join('')
.replace(new RegExp(`{%-? ${endTagName} -?%}`), '')
.split('')
.reverse()
.join('')
const nestedGroups = getLiquidConditionalsWithContent(nested, tagName)
// Remove the start tag but NOT the end tag, so we are left with elsif tags and their endifs, if any.
const elsifs = group.text.replace(group.conditional, '')
const elsifGroups = getLiquidConditionalsWithContent(elsifs, 'elsif')
return [group].concat(nestedGroups, elsifGroups)
})
return nestedConditionals
}
function groupTokens(tokens, tagName, endTagName, newArray = []) {
const startIndex = tokens.findIndex((token) => token.conditional === tagName)
// The end tag name is currently in a separate token, but we want to group it with the start tag and content.
const endIndex = tokens.findIndex(
(token, index) => token.conditional === endTagName && index > startIndex
)
// Once all tags are grouped and removed from `tokens`, this findIndex will not find anything,
// so we can return the grouped result at this point.
if (startIndex === -1) return newArray
const condBlockArr = tokens.slice(startIndex, endIndex + 1)
if (!condBlockArr.length) return newArray
const [newBlockArr, newEndIndex] = handleNestedTags(
condBlockArr,
endIndex,
tagName,
endTagName,
tokens
)
// Combine the text of the groups so it's all together.
const condBlock = newBlockArr.map((t) => t.text).join('')
const startToken = tokens[startIndex]
const endToken = tokens[endIndex]
newArray.push({
conditional: startToken.text,
text: condBlock,
endIfText: endToken.text,
positionStart: startToken.position,
positionEnd: endToken.position,
})
// Remove the already-processed tokens.
const numberOfItemsToRemove = newEndIndex + 1 - startIndex
tokens.splice(startIndex, numberOfItemsToRemove)
// Run recursively until we reach the end of the tokens.
return groupTokens(tokens, tagName, endTagName, newArray)
}
function handleNestedTags(condBlockArr, endIndex, tagName, endTagName, tokens) {
// Return early if there are no nested tags to be handled.
if (!hasUnhandledNestedTags(condBlockArr, tagName, endTagName)) {
return [condBlockArr, endIndex]
}
// If a nested conditional is found, we have to peek forward to the next endif tag after the one we found.
const tempEndIndex = tokens
.slice(endIndex + 1)
.findIndex((token) => token.conditional === endTagName)
// Include the content up to the next endif tag.
const additionalTokens = tokens.slice(endIndex + 1, endIndex + tempEndIndex + 2)
const newBlockArray = condBlockArr.concat(...additionalTokens)
const newEndIndex = endIndex + tempEndIndex + 1
// Run this function recursively in case there are more nested tags to be handled.
return handleNestedTags(newBlockArray, newEndIndex, tagName, endTagName, tokens)
}
function hasUnhandledNestedTags(condBlockArr, tagName, endTagName) {
const startTags = condBlockArr.filter((t) => {
// some blocks that start with ifversion still have if tags nested inside
return tagName === 'ifversion'
? t.conditional === tagName || t.conditional === 'if'
: t.conditional === tagName
})
const endTags = condBlockArr.filter((t) => t.conditional === endTagName)
const hasMoreStartTagsThanEndTags = startTags.length > endTags.length
// Do not consider multiple elsifs an unhandled nesting. We only care about nested ifs.
const startTagsAreElsifs = startTags.every((t) => t.conditional === 'elsif')
const hasUnhandledNestedTags = hasMoreStartTagsThanEndTags && !startTagsAreElsifs
return hasUnhandledNestedTags
}
export { getLiquidConditionals, getLiquidConditionalsWithContent }