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.
@@ -1,4 +1,4 @@
1
- // PurgeTSS v7.5.1
1
+ // PurgeTSS v7.5.2
2
2
  // Created by César Estrada
3
3
  // https://purgetss.com
4
4
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "purgetss",
4
- "version": "7.5.1",
4
+ "version": "7.5.2",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "purgetss": "bin/purgetss"
@@ -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 (lineMatch || columnMatch) {
151
- const lineNum = lineMatch ? parseInt(lineMatch[1]) : '?'
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 lines = xmlText.split('\n')
159
- const startLine = Math.max(0, lineNum - 2)
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 === lineNum)
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, comments, and closing tags
197
- if (!trimmed || trimmed.startsWith('<!--') || trimmed.startsWith('</') || trimmed.startsWith('<Alloy') || 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 line starting with uppercase letter followed by space and common Alloy attributes
202
- // Pattern: "Label id=", "View class=","Button onClick=", etc. WITHOUT opening <
203
- const tagWithoutOpening = trimmed.match(/^[A-Z][a-zA-Z0-9_]+\s+(id|class|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/)
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
- if (tagWithoutOpening) {
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
- const relativePath = filePath.replace(process.cwd() + '/', '')
208
-
209
- // Create a custom error with details for the caller to handle
210
- const error = new Error(`XML Syntax Error in ${relativePath}:${i + 1}`)
211
- error.isPreValidationError = true
212
- error.filePath = relativePath
213
- error.lineNumber = i + 1
214
- error.lineContent = line.trim()
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
- throw error
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