UNPKG

purgetss

Version:

A package that simplifies mobile app creation for Titanium developers.

365 lines (323 loc) 11.8 kB
/** * PurgeTSS v7.1.0 - Core Analyzer: Class Extractor * Functions for extracting and analyzing CSS classes from XML and controller files * * COPIED from src/index.js during refactorization - NO CHANGES to logic. * * @since 7.1.0 * @author César Estrada */ import fs from 'fs' import path from 'path' import _ from 'lodash' import chalk from 'chalk' import convert from 'xml-js' import traverse from 'traverse' import * as acorn from 'acorn' /** * Get unique classes from all XML and controller files - COPIED exactly from original getUniqueClasses() function * NO CHANGES to logic, preserving 100% of original functionality * * @returns {Array} Array of unique class names found in project files */ export function getUniqueClasses() { localStart() const allClasses = [] const viewPaths = getViewPaths() _.each(viewPaths, viewPath => { const file = fs.readFileSync(viewPath, 'utf8') if (file) allClasses.push((configFile.purge.mode === 'all') ? file.match(/[^<>"'`\s]*[^<>"'`\s:]/g) : extractClasses(file, viewPath)) }) const controllerPaths = getControllerPaths() _.each(controllerPaths, controllerPath => { const data = fs.readFileSync(controllerPath, 'utf8') if (data) allClasses.push(processControllers(data)) }) if (configOptions.safelist) _.each(configOptions.safelist, safe => allClasses.push(safe)) const uniqueClasses = [] _.each(_.uniq(_.flattenDeep(allClasses)).sort(), uniqueClass => { if (filterCharacters(uniqueClass)) uniqueClasses.push(uniqueClass) }) localFinish('Get Unique Classes') return uniqueClasses.sort() } /** * Extract classes from XML content - COPIED exactly from original extractClasses() function * NO CHANGES to logic, preserving 100% of original functionality * * @param {string} currentText - XML content to parse * @param {string} currentFile - File path for error reporting * @returns {Array} Array of class names found in XML */ export function extractClasses(currentText, currentFile) { try { const jsontext = convert.xml2json(encodeHTML(currentText), { compact: true }) return traverse(JSON.parse(jsontext)).reduce(function(acc, value) { if (this.key === 'class' || this.key === 'id') acc.push(value.split(' ')) return acc }, []) } catch (error) { throw chalk.red(`::PurgeTSS:: Error processing: "${currentFile}"\n`, error) } } /** * Extract SVG image references from XML content. * * For each XML node whose `image` or `backgroundImage` attribute ends in `.svg`, * capture the SVG src alongside the same node's `class` attribute (split into * tokens). Powers the SVG image pipeline so it can pair each reference with the * classes that determine its rendered size. * * Multiple references to the same SVG from different nodes are returned as * separate entries — the caller is responsible for de-duplicating and reducing * to a single resolved dimension. * * @param {string} currentText - XML content to parse. * @param {string} currentFile - File path for error reporting. * @returns {Array<{ src: string, classes: string[] }>} References found. */ export function extractSvgRefsFromXml(currentText, currentFile) { try { const jsontext = convert.xml2json(encodeHTML(currentText), { compact: true }) const json = JSON.parse(jsontext) const refs = [] walkXmlForSvgRefs(json, refs) return refs } catch (error) { throw chalk.red(`::PurgeTSS:: Error processing: "${currentFile}"\n`, error) } } function walkXmlForSvgRefs(node, out) { if (!node || typeof node !== 'object') return if (Array.isArray(node)) { for (const item of node) walkXmlForSvgRefs(item, out) return } const attrs = node._attributes if (attrs && typeof attrs === 'object') { const candidates = [] if (typeof attrs.image === 'string') candidates.push(attrs.image) if (typeof attrs.backgroundImage === 'string') candidates.push(attrs.backgroundImage) for (const src of candidates) { if (!src.toLowerCase().endsWith('.svg')) continue const cls = typeof attrs.class === 'string' ? attrs.class : '' const classes = cls.split(/\s+/).filter(Boolean) out.push({ src, classes }) } } for (const key of Object.keys(node)) { if (key === '_attributes') continue walkXmlForSvgRefs(node[key], out) } } /** * Extract only classes from XML content - COPIED exactly from original extractClassesOnly() function * NO CHANGES to logic, preserving 100% of original functionality * * @param {string} currentText - XML content to parse * @param {string} currentFile - File path for error reporting * @returns {Array} Array of class names found in XML */ export function extractClassesOnly(currentText, currentFile) { try { const jsontext = convert.xml2json(encodeHTML(currentText), { compact: true }) return traverse(JSON.parse(jsontext)).reduce(function(acc, value) { if (this.key === 'class' || this.key === 'classes' || this.key === 'icon' || this.key === 'activeIcon') acc.push(value.split(' ')) return acc }, []) } catch (error) { throw chalk.red(`::PurgeTSS:: Error processing: "${currentFile}"\n`, error) } } /** * Extract words from controller line - COPIED exactly from original extractWordsFromLine() function * NO CHANGES to logic, preserving 100% of original functionality * * @param {string} line - Line of code from controller file * @returns {Array} Array of class names found in line */ function extractWordsFromLineRegex(line) { const patterns = [ { // apply: 'classes' regex: /apply:\s*'([^']+)'/, process: match => match[1].split(/\s+/) }, { // classes: ['class1', 'class2'] o classes: ['class1 class2'] regex: /classes:\s*\[([^\]]+)\]/, process: match => match[1].split(',').map(item => item.trim().replace(/['"]/g, '')) }, { // classes: 'class1 class2' regex: /classes:\s*'([^']+)'/, process: match => match[1].split(/\s+/) } ] // Process simple patterns const words = patterns.reduce((acc, { regex, process }) => { const match = regex.exec(line) return match ? [...acc, ...process(match)] : acc }, []) // Process addClass, removeClass, resetClass const classFunctionRegex = /(?:\.\w+Class|resetClass)\([^,]+,\s*(?:'([^']+)'|\[([^\]]+)\])/g let classFunctionMatch while ((classFunctionMatch = classFunctionRegex.exec(line)) !== null) { const content = classFunctionMatch[1] || classFunctionMatch[2] if (content) { const classes = content.includes(',') ? content.split(',').map(item => item.trim().replace(/['"]/g, '')) : content.replace(/['"]/g, '').split(/\s+/) words.push(...classes) } } // Matching generic arrays // const genericArrayRegex = /(\w+:\s*\[([^\]]+)\])/g // let genericArrayMatch // while ((genericArrayMatch = genericArrayRegex.exec(line)) !== null) { // const genericArrayContent = genericArrayMatch[2] // const genericArray = genericArrayContent.split(',').map(item => item.trim().replace(/['"]/g, '')) // words = words.concat(genericArray) // } return words } /** * Process controller file content - COPIED exactly from original processControllers() function * NO CHANGES to logic, preserving 100% of original functionality * * @param {string} data - Controller file content * @returns {Array} Array of class names found in controller */ function processControllersRegex(data) { const allWords = [] const lines = data.split(/\r?\n/) lines.forEach(line => { const words = extractWordsFromLineRegex(line) if (words.length > 0) { allWords.push(...words) } }) return allWords } const AST_META_KEYS = new Set(['type', 'loc', 'range', 'start', 'end', 'sourceType', 'comments']) function collectLiterals(node, out) { if (!node || typeof node !== 'object' || !node.type) return switch (node.type) { case 'Literal': if (typeof node.value === 'string') { node.value.split(/\s+/).forEach(token => { if (token) out.push(token) }) } return case 'TemplateLiteral': if (node.expressions.length === 0 && node.quasis.length > 0) { const cooked = node.quasis[0].value.cooked if (typeof cooked === 'string') { cooked.split(/\s+/).forEach(token => { if (token) out.push(token) }) } } return case 'ArrayExpression': for (const element of node.elements) { if (element && element.type !== 'SpreadElement') collectLiterals(element, out) } return case 'ConditionalExpression': collectLiterals(node.consequent, out) collectLiterals(node.alternate, out) return default: return } } function walkAST(node, out) { if (!node || typeof node !== 'object') return if (Array.isArray(node)) { for (const child of node) walkAST(child, out) return } if (!node.type) return if (node.type === 'Property' && !node.computed && !node.shorthand && node.key) { const keyName = node.key.type === 'Identifier' ? node.key.name : (node.key.type === 'Literal' ? node.key.value : undefined) if (keyName === 'classes' || keyName === 'apply') { collectLiterals(node.value, out) } } if (node.type === 'CallExpression' && node.callee && node.arguments.length >= 2) { const callee = node.callee let match = false if ( callee.type === 'MemberExpression' && !callee.computed && callee.property && callee.property.type === 'Identifier' && /Class$/.test(callee.property.name) ) { match = true } else if (callee.type === 'Identifier' && callee.name === 'resetClass') { match = true } if (match) collectLiterals(node.arguments[1], out) } for (const key of Object.keys(node)) { if (AST_META_KEYS.has(key)) continue walkAST(node[key], out) } } /** * Process a controller file's source and return utility classes referenced by * the whitelisted expression shapes (`classes:`, `apply:`, `.xxxClass(target, value)`, * `resetClass(target, value)`). Falls back to the per-line regex scanner when * the parser rejects the source. * * @param {string} data - Controller file content * @returns {Array} Array of class name tokens */ export function processControllers(data) { try { const ast = acorn.parse(data, { ecmaVersion: 'latest', sourceType: 'script', allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, allowImportExportEverywhere: true, allowHashBang: true }) const out = [] walkAST(ast, out) return out } catch { return processControllersRegex(data) } } /** * Encode HTML entities - COPIED exactly from original encodeHTML() function * NO CHANGES to logic, preserving 100% of original functionality * * @param {string} str - String to encode * @returns {string} Encoded string */ function encodeHTML(str) { const code = { '&': '&amp;' } return str.replace(/&/gm, i => code[i]) } // Placeholder imports - these will be replaced with proper imports once modules are extracted let localStart, localFinish, getViewPaths, getControllerPaths, configFile, configOptions, filterCharacters /** * Initialize function references from main index * This is a temporary solution until all modules are extracted * * @param {Object} functions - Function references from main index */ export function initializeClassExtractorFunctions(functions) { localStart = functions.localStart localFinish = functions.localFinish getViewPaths = functions.getViewPaths getControllerPaths = functions.getControllerPaths configFile = functions.configFile configOptions = functions.configOptions filterCharacters = functions.filterCharacters }