purgetss 7.5.1 → 7.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/purgetss.ui.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/purge.js +175 -35
package/dist/purgetss.ui.js
CHANGED
package/package.json
CHANGED
|
@@ -144,25 +144,30 @@ function validateXML(xmlText, filePath) {
|
|
|
144
144
|
const columnMatch = error.message.match(/Column:\s*(\d+)/)
|
|
145
145
|
const charMatch = error.message.match(/Char:\s*(.+)/)
|
|
146
146
|
|
|
147
|
+
const lines = xmlText.split('\n')
|
|
148
|
+
const parserLine = lineMatch ? parseInt(lineMatch[1]) : null
|
|
149
|
+
|
|
150
|
+
// Try to find the real source of the error by scanning for suspicious tags.
|
|
151
|
+
// xml2json often reports errors far from the actual problem (e.g. at EOF)
|
|
152
|
+
// because it only notices the mismatch when nesting fails to close.
|
|
153
|
+
const suspectLine = findSuspectLine(lines)
|
|
154
|
+
|
|
155
|
+
const reportLine = suspectLine || parserLine
|
|
156
|
+
|
|
147
157
|
let errorMessage = chalk.red(`\n::PurgeTSS:: XML Syntax Error\n`) +
|
|
148
158
|
chalk.yellow(`File: "${filePath}"\n`)
|
|
149
159
|
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
const colNum = columnMatch ? parseInt(columnMatch[1]) : '?'
|
|
153
|
-
const badChar = charMatch ? charMatch[1] : '?'
|
|
154
|
-
|
|
155
|
-
errorMessage += chalk.yellow(`Error near line: ${lineNum}\n\n`)
|
|
160
|
+
if (reportLine) {
|
|
161
|
+
errorMessage += chalk.yellow(`Error near line: ${reportLine}\n\n`)
|
|
156
162
|
|
|
157
163
|
// Extract and show context: line before, error line, and line after
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const endLine = Math.min(lines.length, lineNum + 2)
|
|
164
|
+
const startLine = Math.max(0, reportLine - 2)
|
|
165
|
+
const endLine = Math.min(lines.length, reportLine + 2)
|
|
161
166
|
|
|
162
167
|
errorMessage += chalk.gray('Context:\n')
|
|
163
168
|
for (let i = startLine; i < endLine; i++) {
|
|
164
169
|
const lineNumDisplay = i + 1
|
|
165
|
-
const isTargetLine = (lineNumDisplay ===
|
|
170
|
+
const isTargetLine = (lineNumDisplay === reportLine)
|
|
166
171
|
const prefix = isTargetLine ? chalk.red('>>>') : chalk.gray(' ')
|
|
167
172
|
const lineContent = lines[i] || ''
|
|
168
173
|
|
|
@@ -181,54 +186,189 @@ function validateXML(xmlText, filePath) {
|
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
|
|
189
|
+
// Scan XML lines for common malformations that xml2json can't pinpoint.
|
|
190
|
+
// Returns the 1-based line number of the first suspect, or null.
|
|
191
|
+
function findSuspectLine(lines) {
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
const trimmed = lines[i].trim()
|
|
194
|
+
if (!trimmed || trimmed.startsWith('<!--')) continue
|
|
195
|
+
|
|
196
|
+
// Opening tag without tag name: "< class=..."
|
|
197
|
+
if (/^<\s+\w+=/.test(trimmed)) return i + 1
|
|
198
|
+
|
|
199
|
+
// Double slash in closing tag: "<//View>"
|
|
200
|
+
if (/^<\/\/\w+>/.test(trimmed)) return i + 1
|
|
201
|
+
|
|
202
|
+
// Closing tag with extra characters: "</View/>" or "</View >>" etc.
|
|
203
|
+
if (/^<\/[a-zA-Z0-9_]+[^>]+>/.test(trimmed) && !/^<\/[a-zA-Z0-9_:]+\s*>/.test(trimmed)) return i + 1
|
|
204
|
+
|
|
205
|
+
// Opening < immediately followed by non-alpha, non-slash, non-! (e.g. "< >" or "<=>")
|
|
206
|
+
if (/^<[^a-zA-Z/!?\s]/.test(trimmed)) return i + 1
|
|
207
|
+
|
|
208
|
+
// Space between < and tag name: "< View"
|
|
209
|
+
if (/^<\s+[A-Z]/.test(trimmed)) return i + 1
|
|
210
|
+
|
|
211
|
+
// Backslash instead of forward slash: \>
|
|
212
|
+
if (/\\>/.test(trimmed)) return i + 1
|
|
213
|
+
|
|
214
|
+
// Double closing bracket: >>
|
|
215
|
+
if (/>>/.test(trimmed)) return i + 1
|
|
216
|
+
|
|
217
|
+
// Unclosed tag: starts with < but doesn't end with > and next line starts a new tag
|
|
218
|
+
if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.endsWith('>')) {
|
|
219
|
+
const nextTrimmed = (lines[i + 1] || '').trim()
|
|
220
|
+
if (nextTrimmed.startsWith('<')) return i + 1
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
184
226
|
/**
|
|
185
227
|
* Pre-validate XML for common Alloy malformations
|
|
186
228
|
* Returns Error if problem found, null if OK
|
|
187
229
|
*/
|
|
188
230
|
function preValidateXML(xmlText, filePath) {
|
|
189
231
|
const lines = xmlText.split('\n')
|
|
232
|
+
const relativePath = filePath.replace(process.cwd() + '/', '')
|
|
190
233
|
|
|
191
|
-
// Check for tags without opening < (common mistake: Label, View, etc. without <)
|
|
192
234
|
for (let i = 0; i < lines.length; i++) {
|
|
193
235
|
const line = lines[i]
|
|
194
236
|
const trimmed = line.trim()
|
|
195
237
|
|
|
196
|
-
// Skip empty lines
|
|
197
|
-
if (!trimmed || trimmed.startsWith('<!--') || trimmed.startsWith('
|
|
238
|
+
// Skip empty lines and comments
|
|
239
|
+
if (!trimmed || trimmed.startsWith('<!--') || trimmed.startsWith('<Alloy')) {
|
|
198
240
|
continue
|
|
199
241
|
}
|
|
200
242
|
|
|
201
|
-
// Check for
|
|
202
|
-
|
|
203
|
-
|
|
243
|
+
// Check for opening tag without tag name: "< class=..." or "< id=..."
|
|
244
|
+
if (/^<\s+(class|id|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/.test(trimmed)) {
|
|
245
|
+
throwPreValidationError({
|
|
246
|
+
relativePath,
|
|
247
|
+
lineNumber: i + 1,
|
|
248
|
+
lineContent: trimmed,
|
|
249
|
+
message: 'Opening tag is missing its tag name',
|
|
250
|
+
fix: `Add the tag name after "<", e.g. "<View ${trimmed.slice(1).trim()}"`
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check for double slash in closing tags: "<//View>"
|
|
255
|
+
const doubleSlash = trimmed.match(/^<\/\/([a-zA-Z0-9_]+)>/)
|
|
256
|
+
if (doubleSlash) {
|
|
257
|
+
throwPreValidationError({
|
|
258
|
+
relativePath,
|
|
259
|
+
lineNumber: i + 1,
|
|
260
|
+
lineContent: trimmed,
|
|
261
|
+
message: `Closing tag "</${doubleSlash[1]}>" has an extra "/"`,
|
|
262
|
+
fix: `Change "${trimmed}" to "</${doubleSlash[1]}>"`
|
|
263
|
+
})
|
|
264
|
+
}
|
|
204
265
|
|
|
205
|
-
|
|
266
|
+
// Check for tags without opening < (common mistake: Label, View, etc. without <)
|
|
267
|
+
// Pattern: "Label id=", "View class=","Button onClick=", etc. WITHOUT opening <
|
|
268
|
+
if (/^[A-Z][a-zA-Z0-9_]+\s+(id|class|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/.test(trimmed)) {
|
|
206
269
|
const tagName = trimmed.split(/\s+|[>=]/)[0]
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
error.tagName = tagName
|
|
216
|
-
|
|
217
|
-
// Print error using logger (still throw, but caller can catch and handle)
|
|
218
|
-
logger.error('XML Syntax Error')
|
|
219
|
-
logger.warn(`File: "${relativePath}"`)
|
|
220
|
-
logger.warn(`Line: ${i + 1}`)
|
|
221
|
-
logger.warn(`Content: "${line.trim()}"`)
|
|
222
|
-
logger.warn(`Error: Tag "<${tagName}>" is missing opening "<"`)
|
|
223
|
-
logger.warn(`Fix: Change "${tagName}" to "<${tagName}>"`)
|
|
270
|
+
throwPreValidationError({
|
|
271
|
+
relativePath,
|
|
272
|
+
lineNumber: i + 1,
|
|
273
|
+
lineContent: trimmed,
|
|
274
|
+
message: `Tag "<${tagName}>" is missing opening "<"`,
|
|
275
|
+
fix: `Change "${tagName}" to "<${tagName}>"`
|
|
276
|
+
})
|
|
277
|
+
}
|
|
224
278
|
|
|
225
|
-
|
|
279
|
+
// Check for closing tag with extra slash: "</View/>"
|
|
280
|
+
const closingExtraSlash = trimmed.match(/^<\/([a-zA-Z0-9_]+)\/>/)
|
|
281
|
+
if (closingExtraSlash) {
|
|
282
|
+
throwPreValidationError({
|
|
283
|
+
relativePath,
|
|
284
|
+
lineNumber: i + 1,
|
|
285
|
+
lineContent: trimmed,
|
|
286
|
+
message: `Closing tag "</${closingExtraSlash[1]}>" has an extra "/" at the end`,
|
|
287
|
+
fix: `Change "${trimmed}" to "</${closingExtraSlash[1]}>"`
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check for space between < and tag name: "< View class=..."
|
|
292
|
+
const spaceBeforeName = trimmed.match(/^<\s+([A-Z][a-zA-Z0-9_]+)\s/)
|
|
293
|
+
if (spaceBeforeName) {
|
|
294
|
+
throwPreValidationError({
|
|
295
|
+
relativePath,
|
|
296
|
+
lineNumber: i + 1,
|
|
297
|
+
lineContent: trimmed,
|
|
298
|
+
message: `Extra space between "<" and tag name "${spaceBeforeName[1]}"`,
|
|
299
|
+
fix: `Change "< ${spaceBeforeName[1]}" to "<${spaceBeforeName[1]}"`
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for attribute without = sign: <View class"foo">
|
|
304
|
+
const attrNoEquals = trimmed.match(/^<([a-zA-Z0-9_]+)\s+[a-zA-Z]+"/)
|
|
305
|
+
if (attrNoEquals && !trimmed.includes('=')) {
|
|
306
|
+
throwPreValidationError({
|
|
307
|
+
relativePath,
|
|
308
|
+
lineNumber: i + 1,
|
|
309
|
+
lineContent: trimmed,
|
|
310
|
+
message: `Attribute is missing "=" sign`,
|
|
311
|
+
fix: `Check attributes in this tag — each one needs an "=" before its value`
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check for backslash in self-closing tag: <Label text="hi" \>
|
|
316
|
+
if (/\\>/.test(trimmed)) {
|
|
317
|
+
throwPreValidationError({
|
|
318
|
+
relativePath,
|
|
319
|
+
lineNumber: i + 1,
|
|
320
|
+
lineContent: trimmed,
|
|
321
|
+
message: `Tag has a backslash "\\>" instead of forward slash "/>"`,
|
|
322
|
+
fix: `Change "\\>" to "/>"`
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check for double closing bracket: <View class="foo">>
|
|
327
|
+
if (/>>/.test(trimmed) && !trimmed.includes('<!--')) {
|
|
328
|
+
throwPreValidationError({
|
|
329
|
+
relativePath,
|
|
330
|
+
lineNumber: i + 1,
|
|
331
|
+
lineContent: trimmed,
|
|
332
|
+
message: `Tag has a double ">>" closing bracket`,
|
|
333
|
+
fix: `Remove the extra ">"`
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check for unclosed opening tag (line ends without > and next line starts a new tag)
|
|
338
|
+
if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.startsWith('<!--') && !trimmed.endsWith('>') && !trimmed.endsWith('-->')) {
|
|
339
|
+
const nextTrimmed = (lines[i + 1] || '').trim()
|
|
340
|
+
if (nextTrimmed.startsWith('<')) {
|
|
341
|
+
throwPreValidationError({
|
|
342
|
+
relativePath,
|
|
343
|
+
lineNumber: i + 1,
|
|
344
|
+
lineContent: trimmed,
|
|
345
|
+
message: `Tag is missing its closing ">"`,
|
|
346
|
+
fix: `Add ">" at the end of this tag`
|
|
347
|
+
})
|
|
348
|
+
}
|
|
226
349
|
}
|
|
227
350
|
}
|
|
228
351
|
|
|
229
352
|
return false
|
|
230
353
|
}
|
|
231
354
|
|
|
355
|
+
function throwPreValidationError({ relativePath, lineNumber, lineContent, message, fix }) {
|
|
356
|
+
const error = new Error(`XML Syntax Error in ${relativePath}:${lineNumber}`)
|
|
357
|
+
error.isPreValidationError = true
|
|
358
|
+
error.filePath = relativePath
|
|
359
|
+
error.lineNumber = lineNumber
|
|
360
|
+
error.lineContent = lineContent
|
|
361
|
+
|
|
362
|
+
logger.error('XML Syntax Error\n')
|
|
363
|
+
logger.info(`File: "${relativePath}"`)
|
|
364
|
+
logger.info(`Line: ${lineNumber}`)
|
|
365
|
+
logger.info(`Content: "${lineContent}"\n`)
|
|
366
|
+
logger.error(message)
|
|
367
|
+
logger.info(chalk.green(`Fix: ${fix}\n`))
|
|
368
|
+
|
|
369
|
+
throw error
|
|
370
|
+
}
|
|
371
|
+
|
|
232
372
|
/**
|
|
233
373
|
* Extract classes from file content
|
|
234
374
|
* COPIED exactly from original extractClasses() function
|