-
Notifications
You must be signed in to change notification settings - Fork 60.3k
/
remove-liquid-statements.js
267 lines (224 loc) · 12.5 KB
/
remove-liquid-statements.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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import { getLiquidConditionalsWithContent } from './get-liquid-conditionals.js'
import getVersionBlocks from './get-version-blocks.js'
import { allVersions } from '../../lib/all-versions.js'
import supportedOperators from '../../lib/liquid-tags/ifversion-supported-operators.js'
import { Tokenizer } from 'liquidjs'
const supportedShortVersions = Object.values(allVersions).map((v) => v.shortName)
const ghaeRangeRegex = new RegExp(`ghae (${supportedOperators.join('|')})`)
const updateRangeKeepGhes = 'updateRangeKeepGhes'
const updateRangeRemoveGhes = 'updateRangeRemoveGhes'
const removeRangeAndContent = 'removeRangeAndContent'
const removeConditionals = 'removeConditionals'
const tokenize = (str) => {
const tokenizer = new Tokenizer(str)
return tokenizer.readTopLevelTokens()
}
// This module is used by script/enterprise-server-deprecations/remove-version-markup.js to remove
// and update Liquid conditionals when a GHES release is being deprecated. It is also used by
// tests/content/remove-liquid-statements.js.
export default function removeLiquidStatements(content, release, nextOldestRelease, file) {
let newContent = content
// Get an array of ifversion blocks with their content included.
const blocks = getLiquidConditionalsWithContent(newContent, 'ifversion')
if (!blocks.length) return newContent
// Decorate those blocks with more GHES versioning information.
const versionBlocks = getVersionBlocks(blocks)
// Determine whether we should remove the GHES range only or the GHES range and the content.
for (const versionBlock of versionBlocks) {
const actionMap = {}
versionBlock.isGhesOnly = versionBlock.condArgs.every((arg) => arg.includes('ghes'))
versionBlock.hasSingleRange = versionBlock.ranges.length === 1
versionBlock.andGhesRanges = versionBlock.condArgs.filter((arg) => arg.includes('and ghes'))
const isSafeToRemoveContent =
versionBlock.isGhesOnly && (versionBlock.hasSingleRange || versionBlock.andGhesRanges.length)
if (canConditionalBeRemoved(supportedShortVersions, versionBlock.condOnly)) {
actionMap[removeConditionals] = true
}
for (const rangeArgs of versionBlock.ranges) {
const rangeOperator = rangeArgs[1]
const releaseNumber = rangeArgs[2]
// We are only concerned with the release we are deprecating and the next oldest release..
if (!(releaseNumber === release || releaseNumber === nextOldestRelease)) continue
// But are not concerned with conditionals _equal_ to the next oldest release, as those are still valid.
if (rangeOperator === '=' && releaseNumber === nextOldestRelease) continue
// Remove Liquid in these scenarios.
const greaterThanVersionToDeprecate = rangeOperator === '>' && releaseNumber === release
const notEqualsVersionToDeprecate = rangeOperator === '!=' && releaseNumber === release
// Remove Liquid and content in these scenarios, IF AND ONLY IF it's safe to remove content.
// For example, when there is no other versioning in the conditional.
const lessThanNextOldestVersion =
rangeOperator === '<' && (releaseNumber === nextOldestRelease || releaseNumber === release)
const equalsVersionToDeprecate = rangeOperator === '=' && releaseNumber === release
let action
// Determine which action to take: removeRangeAndContent, updateRangeRemoveGhes, updateRangeKeepGhes.
if (greaterThanVersionToDeprecate || notEqualsVersionToDeprecate) {
// If the original is `ghes > 2.21` or `fpt or ghes > 2.21`, we want to replace it with `ghes`;
// If the original is `ghes > 2.21 and ghes < 3.0`, we want to remove the range and leave just `ghes < 3.0`.
action = versionBlock.hasSingleRange ? updateRangeKeepGhes : updateRangeRemoveGhes
}
if (lessThanNextOldestVersion || equalsVersionToDeprecate) {
// If it's not safe to remove content, we want to at least remove `ghes` from the range.
action = isSafeToRemoveContent ? removeRangeAndContent : updateRangeRemoveGhes
}
if (action) {
actionMap[action] = versionBlock.condArgs.find((arg) => arg.endsWith(rangeArgs.join(' ')))
}
}
versionBlock.action = Object.keys(actionMap).length ? actionMap : 'none'
}
// Create the new content and add it to each block.
versionBlocks
.filter((versionBlock) => versionBlock.action !== 'none')
.forEach((versionBlock) => {
const indexOfLastEndif = lastIndexOfRegex(versionBlock.content, /{%-? endif -?%}/g)
// ----- REMOVE RANGE AND CONTENT -----
if (versionBlock.action.removeRangeAndContent) {
// If the block has an else, remove the content up to the else, plus the final endif, leaving
// the content inside the else block as is.
if (versionBlock.hasElse) {
// If the ifversion conditional starts at the beginning of a line
// we can remove the newline after the else. If the if condition
// doesn't start at the beginning of a new line, the content before
// the ifversion tag will be concatenated to the content after the
// else condition on a single line.
const replaceRegex = versionBlock.startTagColumn1
? new RegExp(`${versionBlock.condWithLiquid}[\\S\\s]+?{%-? else -?%}\n?`)
: new RegExp(`${versionBlock.condWithLiquid}[\\S\\s]+?{%-? else -?%}`)
versionBlock.newContent = versionBlock.content
.slice(0, indexOfLastEndif)
.replace(replaceRegex, '')
if (versionBlock.endTagColumn1 && versionBlock.newContent.endsWith('\n'))
versionBlock.newContent = versionBlock.newContent.slice(0, -1)
}
// If the block has an elsif, remove the content up to the elsif, and change the elsif to an if (or leave it
// an elsif this this block is itself an elsif), leaving the content inside the elsif block as is. The elsif
// condition is evaluated separately so we don't need to worry about evaluating it here.
if (versionBlock.hasElsif) {
const replaceRegex = new RegExp(`${versionBlock.condWithLiquid}[\\S\\s]+?({%-?) elsif`)
versionBlock.newContent = versionBlock.content.replace(
replaceRegex,
`$1 ${versionBlock.condKeyword}`
)
}
// For all other scenarios, remove the Liquid and the content.
if (!(versionBlock.hasElse || versionBlock.hasElsif)) {
versionBlock.newContent = ''
}
}
// ----- UPDATE RANGE AND REMOVE `GHES` -----
if (versionBlock.action.updateRangeRemoveGhes) {
// Make the replacement and get the new conditional.
const newCondWithLiquid = versionBlock.condWithLiquid
.replace(versionBlock.action.updateRangeRemoveGhes, '')
.replace(/\s\s+/, ' ')
// Update the conditional.
versionBlock.newContent = versionBlock.content.replace(
versionBlock.condWithLiquid,
newCondWithLiquid
)
}
// ----- UPDATE RANGE AND KEEP `GHES` -----
let canBeRemoved
if (versionBlock.action.updateRangeKeepGhes) {
const replacement = versionBlock.action.updateRangeKeepGhes.replace(/ghes.+$/, 'ghes')
// Make the replacement and get the new conditional.
const newCondWithLiquid = versionBlock.condWithLiquid
.replace(versionBlock.action.updateRangeKeepGhes, replacement)
.replace(/\s\s+/, ' ')
// If the new conditional contains all the currently supported versions, no conditional
// is actually needed, and it can be removed. Any `else` statements and their content should
// also be removed.
canBeRemoved = canConditionalBeRemoved(supportedShortVersions, newCondWithLiquid)
if (!canBeRemoved) {
versionBlock.newContent = versionBlock.content.replace(
versionBlock.condWithLiquid,
newCondWithLiquid
)
}
}
// ----- REMOVE CONDITIONALS -----
// this happens if either:
// (a) the the conditional was updated in a previous step to contain all the currently supported versions, or
// (b) the conditional was not touched but its arguments already contained all supported versions, making it unnecessary
if (canBeRemoved || versionBlock.action.removeConditionals) {
versionBlock.newContent = versionBlock.content
// If this block does not contain else/elsifs, start by removing the final endif.
// (We'll handle the endif separately in those scenarios.)
if (!versionBlock.hasElse && !versionBlock.hasElsif) {
const indexOfLastEndif = lastIndexOfRegex(versionBlock.content, /{%-? endif -?%}/g)
versionBlock.newContent = versionBlock.newContent.slice(0, indexOfLastEndif)
if (versionBlock.endTagColumn1 && versionBlock.newContent.endsWith('\n'))
versionBlock.newContent = versionBlock.newContent.slice(0, -1)
}
// If start tag is on it's own line, remove line ending (\\n?)
// and remove white space (//s*) after line ending to
// preserve indentation of next line
const removeStartTagRegex = versionBlock.startTagColumn1
? new RegExp(`${versionBlock.condWithLiquid}\\n?\\s*`)
: new RegExp(`${versionBlock.condWithLiquid}`)
// For ALL scenarios, remove the start tag.
versionBlock.newContent = versionBlock.newContent.replace(removeStartTagRegex, '')
// If the block has an elsif, change the elsif to an if (or leave it an elsif this this block is itself an elsif),
// leaving the content inside the elsif block as is. Also leave the endif in this scenario.
if (versionBlock.hasElsif) {
versionBlock.newContent = versionBlock.newContent.replace(
/({%-?) elsif/,
`$1 ${versionBlock.condKeyword}`
)
}
// If the block has an else, remove the else, its content, and the endif.
if (versionBlock.hasElse) {
let elseStartIndex
let ifCondFlag = false
// tokenize the content including the nested conditionals to find
// the unmatched else tag. Remove content from the start of the
// else tag to the end of the content. The tokens return have different
// `kind`s and can be liquid tags, HTML, and a variety of things.
// A value of 4 is a liquid tag. See https://liquidjs.com/api/enums/parser_token_kind_.tokenkind.html.
tokenize(versionBlock.newContent)
.filter((elem) => elem.kind === 4)
.forEach((tag) => {
if (tag.name === 'ifversion' || tag.name === 'if') {
ifCondFlag = true
} else if (tag.name === 'endif' && ifCondFlag === true) {
ifCondFlag = false
} else if (tag.name === 'else' && ifCondFlag === false) {
elseStartIndex = tag.begin
}
})
versionBlock.newContent = versionBlock.newContent.slice(0, elseStartIndex)
}
}
})
// Now that we have the old and new content attached to each block, make the replacement
// in the general content and return the updated general content.
versionBlocks.forEach((versionBlock) => {
if (versionBlock.action !== 'none') {
const newBlockContent = versionBlock.newContent.replaceAll(/\n\n\n+?/g, '\n\n')
newContent = newContent
.replaceAll(versionBlock.content, newBlockContent)
.replaceAll(/({%-? ifversion |{%-? elsif )(and|or) /g, '$1') // clean up stray and/ors :/
.replaceAll(/\n\n\n+?/g, '\n\n')
if (file && file.includes('/data/')) {
newContent = newContent.trim()
}
}
})
return newContent
}
// Hack to use a regex with lastIndexOf.
// Inspired by https://stackoverflow.com/a/21420210
function lastIndexOfRegex(str, regex, fromIndex) {
const myStr = fromIndex ? str.substring(0, fromIndex) : str
const match = myStr.match(regex)
return match ? myStr.lastIndexOf(match[match.length - 1]) : -1
}
// Checks if a conditional is necessary given all the supported versions and the arguments in a conditional
// If all supported versions show up in the arguments, it's not necessary! Additionally, builds in support
// for when feature-based versioning is used, which looks like "issue" versions for upcoming GHAE releases
function canConditionalBeRemoved(supportedVersions, conditional) {
if (typeof conditional !== 'string') throw new Error('Expecting a string.')
const containsAllVersions = supportedVersions.every((arg) => conditional.includes(arg))
const hasGhaeRange = ghaeRangeRegex.test(conditional)
return containsAllVersions && !hasGhaeRange
}