forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
get-liquid-conditionals.js
152 lines (121 loc) · 5.67 KB
/
get-liquid-conditionals.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/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 }