codescoop 1.0.0

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.
@@ -0,0 +1,251 @@
1
+ /**
2
+ * CSS Specificity Calculator
3
+ * Uses the battle-tested 'specificity' npm library to calculate specificity
4
+ * and predict which CSS rules will actually render
5
+ */
6
+
7
+ const { calculate } = require('specificity');
8
+
9
+ /**
10
+ * Calculate specificity for a CSS selector using the npm library
11
+ * Returns [inline, ids, classes, elements] tuple
12
+ * @param {string} selector - CSS selector
13
+ * @returns {number[]} Specificity tuple [inline, ids, classes, elements]
14
+ */
15
+ function calculateSpecificity(selector) {
16
+ if (!selector || typeof selector !== 'string') {
17
+ return [0, 0, 0, 0];
18
+ }
19
+
20
+ try {
21
+ const result = calculate(selector);
22
+ if (result) {
23
+ // Library returns { A: ids, B: classes, C: elements }
24
+ // We add inline (0) as the first element for consistency
25
+ return [0, result.A || 0, result.B || 0, result.C || 0];
26
+ }
27
+ } catch (error) {
28
+ // If the library can't parse it, return zeroes
29
+ // console.warn(`Could not calculate specificity for "${selector}": ${error.message}`);
30
+ }
31
+
32
+ return [0, 0, 0, 0];
33
+ }
34
+
35
+ /**
36
+ * Compare two specificity tuples
37
+ * @param {number[]} a - First specificity
38
+ * @param {number[]} b - Second specificity
39
+ * @returns {number} -1 if a < b, 0 if equal, 1 if a > b
40
+ */
41
+ function compareSpecificity(a, b) {
42
+ for (let i = 0; i < 4; i++) {
43
+ if (a[i] > b[i]) return 1;
44
+ if (a[i] < b[i]) return -1;
45
+ }
46
+ return 0;
47
+ }
48
+
49
+ /**
50
+ * Format specificity as readable string
51
+ * @param {number[]} spec - Specificity tuple
52
+ * @returns {string} Formatted string like "(0,1,2,3)"
53
+ */
54
+ function formatSpecificity(spec) {
55
+ return `(${spec.join(',')})`;
56
+ }
57
+
58
+ /**
59
+ * Check if a CSS value has !important
60
+ * @param {string} value - CSS property value
61
+ * @returns {boolean}
62
+ */
63
+ function hasImportant(value) {
64
+ return typeof value === 'string' && value.includes('!important');
65
+ }
66
+
67
+ /**
68
+ * Analyze CSS rules and predict which ones will render
69
+ * @param {Array} cssResults - CSS analysis results from the tool
70
+ * @param {Object} linkedFiles - Information about linked CSS files
71
+ * @returns {Object} Conflict analysis with winners/losers
72
+ */
73
+ function analyzeConflicts(cssResults, linkedFiles = { css: [] }) {
74
+ // Group rules by property
75
+ const propertyRules = {};
76
+
77
+ // Track file order (later = higher priority for same specificity)
78
+ const fileOrder = {};
79
+ linkedFiles.css.forEach((file, index) => {
80
+ fileOrder[file] = index;
81
+ });
82
+
83
+ let globalOrder = 0;
84
+
85
+ for (const result of cssResults) {
86
+ const filePath = result.filePath || result.file;
87
+ const fileOrderValue = fileOrder[filePath] ?? 999;
88
+
89
+ for (const match of result.matches || []) {
90
+ const selector = match.selector || '';
91
+ const specificity = calculateSpecificity(selector);
92
+ const content = match.content || '';
93
+
94
+ // Parse properties from content
95
+ const properties = parseProperties(content);
96
+
97
+ for (const [prop, value] of Object.entries(properties)) {
98
+ if (!propertyRules[prop]) {
99
+ propertyRules[prop] = [];
100
+ }
101
+
102
+ propertyRules[prop].push({
103
+ selector,
104
+ specificity,
105
+ value,
106
+ hasImportant: hasImportant(value),
107
+ file: filePath,
108
+ fileOrder: fileOrderValue,
109
+ globalOrder: globalOrder++,
110
+ startLine: match.startLine,
111
+ endLine: match.endLine
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ // Determine winner for each property
118
+ const conflicts = {};
119
+
120
+ for (const [prop, rules] of Object.entries(propertyRules)) {
121
+ if (rules.length <= 1) {
122
+ continue; // No conflict
123
+ }
124
+
125
+ // Sort by priority: !important > specificity > file order > declaration order
126
+ const sorted = [...rules].sort((a, b) => {
127
+ // !important always wins
128
+ if (a.hasImportant && !b.hasImportant) return 1;
129
+ if (!a.hasImportant && b.hasImportant) return -1;
130
+
131
+ // Compare specificity
132
+ const specCompare = compareSpecificity(a.specificity, b.specificity);
133
+ if (specCompare !== 0) return specCompare;
134
+
135
+ // Same specificity: later file wins
136
+ if (a.fileOrder !== b.fileOrder) return a.fileOrder - b.fileOrder;
137
+
138
+ // Same file: later declaration wins
139
+ return a.globalOrder - b.globalOrder;
140
+ });
141
+
142
+ const winner = sorted[sorted.length - 1];
143
+ const losers = sorted.slice(0, -1);
144
+
145
+ conflicts[prop] = {
146
+ winner,
147
+ losers,
148
+ hasConflict: losers.length > 0
149
+ };
150
+ }
151
+
152
+ return conflicts;
153
+ }
154
+
155
+ /**
156
+ * Parse CSS properties from a rule content
157
+ * @param {string} content - CSS rule content
158
+ * @returns {Object} Property-value pairs
159
+ */
160
+ function parseProperties(content) {
161
+ const properties = {};
162
+
163
+ // Remove selector and braces if present
164
+ let body = content;
165
+ const braceMatch = content.match(/\{([^}]+)\}/);
166
+ if (braceMatch) {
167
+ body = braceMatch[1];
168
+ }
169
+
170
+ // Split by semicolons and parse
171
+ const declarations = body.split(';');
172
+ for (const decl of declarations) {
173
+ const colonIndex = decl.indexOf(':');
174
+ if (colonIndex > 0) {
175
+ const prop = decl.substring(0, colonIndex).trim();
176
+ const value = decl.substring(colonIndex + 1).trim();
177
+ if (prop && value) {
178
+ properties[prop] = value;
179
+ }
180
+ }
181
+ }
182
+
183
+ return properties;
184
+ }
185
+
186
+ /**
187
+ * Format conflict analysis as markdown
188
+ * @param {Object} conflicts - Conflict analysis from analyzeConflicts
189
+ * @returns {string} Markdown formatted output
190
+ */
191
+ function formatConflictsMarkdown(conflicts) {
192
+ const conflictingProps = Object.entries(conflicts).filter(([_, data]) => data.hasConflict);
193
+
194
+ if (conflictingProps.length === 0) {
195
+ return '';
196
+ }
197
+
198
+ let md = `\n---\n\n## ⚔️ CSS Conflicts Detected\n\n`;
199
+ md += `> Multiple CSS rules are competing for the same properties. Here's what will actually render:\n`;
200
+ md += `> *Note: Rules are sorted by specificity. If equal, the browser uses the rule loaded last.*\n\n`;
201
+
202
+ for (const [prop, data] of conflictingProps) {
203
+ const { winner, losers } = data;
204
+
205
+ md += `### \`${prop}\`\n\n`;
206
+ md += `| Status | File | Selector | Value | Specificity |\n`;
207
+ md += `|--------|------|----------|-------|-------------|\n`;
208
+
209
+ // Show winner first
210
+ md += `| ✅ **Winner** | \`${getFileName(winner.file)}\` | \`${winner.selector}\` | \`${truncateValue(winner.value)}\` | ${formatSpecificity(winner.specificity)}${winner.hasImportant ? ' **!important**' : ''} |\n`;
211
+
212
+ // Show losers
213
+ for (const loser of losers.reverse()) {
214
+ md += `| ❌ Overridden | \`${getFileName(loser.file)}\` | \`${loser.selector}\` | \`${truncateValue(loser.value)}\` | ${formatSpecificity(loser.specificity)}${loser.hasImportant ? ' !important' : ''} |\n`;
215
+ }
216
+
217
+ md += '\n';
218
+ }
219
+
220
+ md += `**${conflictingProps.length}** property conflict(s) detected.\n`;
221
+
222
+ return md;
223
+ }
224
+
225
+ /**
226
+ * Truncate long CSS values for display
227
+ */
228
+ function truncateValue(value) {
229
+ if (!value) return '';
230
+ if (value.length > 40) {
231
+ return value.substring(0, 37) + '...';
232
+ }
233
+ return value;
234
+ }
235
+
236
+ /**
237
+ * Get just the filename from a path
238
+ */
239
+ function getFileName(filePath) {
240
+ if (!filePath) return 'unknown';
241
+ const parts = filePath.replace(/\\/g, '/').split('/');
242
+ return parts[parts.length - 1];
243
+ }
244
+
245
+ module.exports = {
246
+ calculateSpecificity,
247
+ compareSpecificity,
248
+ formatSpecificity,
249
+ analyzeConflicts,
250
+ formatConflictsMarkdown
251
+ };
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Template Parser Module
3
+ * Parses PHP, Blade, Twig, and other template files by stripping server-side code
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Supported template types and their patterns
11
+ */
12
+ const TEMPLATE_PATTERNS = {
13
+ php: {
14
+ extensions: ['.php', '.phtml', '.inc'],
15
+ // Match <?php ... ?> and <?= ... ?> and <? ... ?>
16
+ patterns: [
17
+ /<\?php[\s\S]*?\?>/gi,
18
+ /<\?=[\s\S]*?\?>/gi,
19
+ /<\?(?!xml)[\s\S]*?\?>/gi
20
+ ],
21
+ // PHP echo shortcuts to preserve (convert to placeholders)
22
+ echoPatterns: [
23
+ { regex: /<\?=\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*\?>/g, placeholder: '{{$1}}' },
24
+ { regex: /<\?php\s+echo\s+\$([a-zA-Z_][a-zA-Z0-9_]*)\s*;\s*\?>/g, placeholder: '{{$1}}' }
25
+ ]
26
+ },
27
+ blade: {
28
+ extensions: ['.blade.php'],
29
+ // Blade directives
30
+ patterns: [
31
+ /@(if|else|elseif|endif|foreach|endforeach|for|endfor|while|endwhile|switch|case|break|default|endswitch|unless|endunless|isset|endisset|empty|endempty|auth|endauth|guest|endguest|hasSection|yield|section|endsection|show|parent|include|extends|component|endcomponent|slot|endslot|push|endpush|stack|prepend|endprepend|php|endphp|verbatim|endverbatim|error|enderror|once|endonce|env|endenv|production|endproduction|props|aware|class|disabled|readonly|required|checked|selected)\b[^@]*/gi,
32
+ /\{\{--[\s\S]*?--\}\}/g, // Blade comments
33
+ /\{\!\![\s\S]*?\!\!\}/g, // Unescaped output
34
+ ],
35
+ // Preserve Blade echo as placeholders
36
+ echoPatterns: [
37
+ { regex: /\{\{\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, placeholder: '{{$1}}' }
38
+ ]
39
+ },
40
+ twig: {
41
+ extensions: ['.twig', '.html.twig'],
42
+ patterns: [
43
+ /\{%[\s\S]*?%\}/g, // Twig tags
44
+ /\{#[\s\S]*?#\}/g, // Twig comments
45
+ ],
46
+ echoPatterns: [
47
+ { regex: /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g, placeholder: '{{$1}}' }
48
+ ]
49
+ },
50
+ ejs: {
51
+ extensions: ['.ejs'],
52
+ patterns: [
53
+ /<%[\s\S]*?%>/g, // EJS tags
54
+ /<%#[\s\S]*?%>/g, // EJS comments
55
+ ],
56
+ echoPatterns: [
57
+ { regex: /<%=\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*%>/g, placeholder: '{{$1}}' }
58
+ ]
59
+ },
60
+ erb: {
61
+ extensions: ['.erb', '.html.erb'],
62
+ patterns: [
63
+ /<%[\s\S]*?%>/g,
64
+ /<%#[\s\S]*?%>/g,
65
+ ],
66
+ echoPatterns: [
67
+ { regex: /<%=\s*@?([a-zA-Z_][a-zA-Z0-9_.]*)\s*%>/g, placeholder: '{{$1}}' }
68
+ ]
69
+ },
70
+ handlebars: {
71
+ extensions: ['.hbs', '.handlebars'],
72
+ patterns: [
73
+ /\{\{#[\s\S]*?\}\}/g, // Block helpers
74
+ /\{\{\/[\s\S]*?\}\}/g, // Close blocks
75
+ /\{\{!--[\s\S]*?--\}\}/g, // Comments
76
+ /\{\{![\s\S]*?\}\}/g, // Inline comments
77
+ ],
78
+ echoPatterns: [
79
+ { regex: /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g, placeholder: '{{$1}}' }
80
+ ]
81
+ },
82
+ jsp: {
83
+ extensions: ['.jsp', '.jspf'],
84
+ patterns: [
85
+ /<%@[\s\S]*?%>/g, // Directives
86
+ /<%![\s\S]*?%>/g, // Declarations
87
+ /<%[\s\S]*?%>/g, // Scriptlets
88
+ /<%--[\s\S]*?--%>/g, // Comments
89
+ ],
90
+ echoPatterns: [
91
+ { regex: /\$\{([a-zA-Z_][a-zA-Z0-9_.]*)\}/g, placeholder: '{{$1}}' }
92
+ ]
93
+ },
94
+ asp: {
95
+ extensions: ['.asp', '.aspx', '.cshtml', '.vbhtml'],
96
+ patterns: [
97
+ /<%[\s\S]*?%>/g,
98
+ /@\{[\s\S]*?\}/g, // Razor code blocks
99
+ /@[\w]+\([^)]*\)/g, // Razor helpers
100
+ ],
101
+ echoPatterns: [
102
+ { regex: /@([a-zA-Z_][a-zA-Z0-9_.]*)/g, placeholder: '{{$1}}' }
103
+ ]
104
+ }
105
+ };
106
+
107
+ /**
108
+ * Detect template type from file extension
109
+ * @param {string} filePath - Path to template file
110
+ * @returns {string|null} Template type or null
111
+ */
112
+ function detectTemplateType(filePath) {
113
+ const ext = path.extname(filePath).toLowerCase();
114
+ const basename = path.basename(filePath).toLowerCase();
115
+
116
+ // Check for compound extensions first (like .blade.php)
117
+ for (const [type, config] of Object.entries(TEMPLATE_PATTERNS)) {
118
+ for (const extension of config.extensions) {
119
+ if (basename.endsWith(extension)) {
120
+ return type;
121
+ }
122
+ }
123
+ }
124
+
125
+ // Check simple extension
126
+ for (const [type, config] of Object.entries(TEMPLATE_PATTERNS)) {
127
+ if (config.extensions.includes(ext)) {
128
+ return type;
129
+ }
130
+ }
131
+
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Check if file is a template file
137
+ * @param {string} filePath - Path to check
138
+ * @returns {boolean}
139
+ */
140
+ function isTemplateFile(filePath) {
141
+ return detectTemplateType(filePath) !== null;
142
+ }
143
+
144
+ /**
145
+ * Parse template file and extract HTML
146
+ * @param {string} filePath - Path to template file
147
+ * @param {Object} options - Parse options
148
+ * @returns {Object} Parsed result with HTML and metadata
149
+ */
150
+ function parseTemplateFile(filePath, options = {}) {
151
+ const {
152
+ preserveEchos = true,
153
+ preserveComments = false
154
+ } = options;
155
+
156
+ const content = fs.readFileSync(filePath, 'utf-8');
157
+ return parseTemplateContent(content, filePath, { preserveEchos, preserveComments });
158
+ }
159
+
160
+ /**
161
+ * Parse template content and extract HTML
162
+ * @param {string} content - Template content
163
+ * @param {string} filePath - Original file path (for type detection)
164
+ * @param {Object} options - Parse options
165
+ * @returns {Object} Parsed result
166
+ */
167
+ function parseTemplateContent(content, filePath, options = {}) {
168
+ const {
169
+ preserveEchos = true,
170
+ preserveComments = false
171
+ } = options;
172
+
173
+ const templateType = detectTemplateType(filePath);
174
+ if (!templateType) {
175
+ // Not a template, return as-is
176
+ return {
177
+ html: content,
178
+ templateType: null,
179
+ dynamicClasses: [],
180
+ warnings: []
181
+ };
182
+ }
183
+
184
+ const config = TEMPLATE_PATTERNS[templateType];
185
+ let html = content;
186
+ const dynamicClasses = [];
187
+ const warnings = [];
188
+
189
+ // First, extract potential dynamic class patterns
190
+ const classPatterns = [
191
+ /class\s*=\s*["'][^"']*<\?[\s\S]*?\?>[^"']*["']/g,
192
+ /class\s*=\s*["'][^"']*\{\{[\s\S]*?\}\}[^"']*["']/g,
193
+ /class\s*=\s*["'][^"']*<%[\s\S]*?%>[^"']*["']/g,
194
+ /:class\s*=\s*["']\{[\s\S]*?\}["']/g, // Vue-like
195
+ /className\s*=\s*\{[\s\S]*?\}/g, // React-like in templates
196
+ ];
197
+
198
+ for (const pattern of classPatterns) {
199
+ const matches = content.match(pattern);
200
+ if (matches) {
201
+ matches.forEach(m => {
202
+ dynamicClasses.push({
203
+ original: m,
204
+ warning: 'Dynamic class detected - may not be fully extracted'
205
+ });
206
+ });
207
+ }
208
+ }
209
+
210
+ if (dynamicClasses.length > 0) {
211
+ warnings.push(`Found ${dynamicClasses.length} dynamic class attribute(s) - these may contain additional classes at runtime`);
212
+ }
213
+
214
+ // Preserve echo patterns as placeholders if requested
215
+ if (preserveEchos && config.echoPatterns) {
216
+ for (const echo of config.echoPatterns) {
217
+ html = html.replace(echo.regex, echo.placeholder);
218
+ }
219
+ }
220
+
221
+ // Remove server-side code patterns
222
+ for (const pattern of config.patterns) {
223
+ html = html.replace(pattern, '');
224
+ }
225
+
226
+ // Clean up excess whitespace but preserve structure
227
+ html = html
228
+ .replace(/^\s*[\r\n]/gm, '\n') // Remove blank lines
229
+ .replace(/\n{3,}/g, '\n\n') // Max 2 newlines
230
+ .trim();
231
+
232
+ return {
233
+ html,
234
+ templateType,
235
+ originalContent: content,
236
+ dynamicClasses,
237
+ warnings
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Get list of supported template extensions
243
+ * @returns {string[]}
244
+ */
245
+ function getSupportedExtensions() {
246
+ const extensions = [];
247
+ for (const config of Object.values(TEMPLATE_PATTERNS)) {
248
+ extensions.push(...config.extensions);
249
+ }
250
+ return [...new Set(extensions)];
251
+ }
252
+
253
+ module.exports = {
254
+ TEMPLATE_PATTERNS,
255
+ detectTemplateType,
256
+ isTemplateFile,
257
+ parseTemplateFile,
258
+ parseTemplateContent,
259
+ getSupportedExtensions
260
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * URL Fetcher Module
3
+ * Fetches rendered HTML from live URLs for WordPress/Magento/dynamic sites
4
+ */
5
+
6
+ const https = require('https');
7
+ const http = require('http');
8
+
9
+ /**
10
+ * Fetch HTML content from a URL
11
+ * @param {string} url - URL to fetch
12
+ * @param {Object} options - Fetch options
13
+ * @returns {Promise<{html: string, url: string, statusCode: number}>}
14
+ */
15
+ async function fetchURL(url, options = {}) {
16
+ const {
17
+ timeout = 30000,
18
+ userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
19
+ followRedirects = true,
20
+ maxRedirects = 5
21
+ } = options;
22
+
23
+ return new Promise((resolve, reject) => {
24
+ const parsedUrl = new URL(url);
25
+ const protocol = parsedUrl.protocol === 'https:' ? https : http;
26
+
27
+ const requestOptions = {
28
+ hostname: parsedUrl.hostname,
29
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
30
+ path: parsedUrl.pathname + parsedUrl.search,
31
+ method: 'GET',
32
+ headers: {
33
+ 'User-Agent': userAgent,
34
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
35
+ 'Accept-Language': 'en-US,en;q=0.5',
36
+ 'Accept-Encoding': 'identity',
37
+ 'Connection': 'keep-alive'
38
+ },
39
+ timeout
40
+ };
41
+
42
+ const req = protocol.request(requestOptions, (res) => {
43
+ // Handle redirects
44
+ if (followRedirects && [301, 302, 303, 307, 308].includes(res.statusCode)) {
45
+ if (maxRedirects <= 0) {
46
+ reject(new Error('Too many redirects'));
47
+ return;
48
+ }
49
+ const redirectUrl = res.headers.location;
50
+ if (!redirectUrl) {
51
+ reject(new Error('Redirect without location header'));
52
+ return;
53
+ }
54
+ // Resolve relative URLs
55
+ const absoluteUrl = redirectUrl.startsWith('http')
56
+ ? redirectUrl
57
+ : new URL(redirectUrl, url).toString();
58
+
59
+ fetchURL(absoluteUrl, { ...options, maxRedirects: maxRedirects - 1 })
60
+ .then(resolve)
61
+ .catch(reject);
62
+ return;
63
+ }
64
+
65
+ let data = '';
66
+ res.setEncoding('utf8');
67
+
68
+ res.on('data', (chunk) => {
69
+ data += chunk;
70
+ });
71
+
72
+ res.on('end', () => {
73
+ resolve({
74
+ html: data,
75
+ url: url,
76
+ statusCode: res.statusCode,
77
+ contentType: res.headers['content-type'] || ''
78
+ });
79
+ });
80
+ });
81
+
82
+ req.on('error', (error) => {
83
+ reject(new Error(`Failed to fetch URL: ${error.message}`));
84
+ });
85
+
86
+ req.on('timeout', () => {
87
+ req.destroy();
88
+ reject(new Error(`Request timeout after ${timeout}ms`));
89
+ });
90
+
91
+ req.end();
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Check if input is a URL
97
+ * @param {string} input - Input string
98
+ * @returns {boolean}
99
+ */
100
+ function isURL(input) {
101
+ try {
102
+ const url = new URL(input);
103
+ return url.protocol === 'http:' || url.protocol === 'https:';
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Extract base URL for relative path resolution
111
+ * @param {string} url - Full URL
112
+ * @returns {string} Base URL
113
+ */
114
+ function getBaseURL(url) {
115
+ const parsed = new URL(url);
116
+ return `${parsed.protocol}//${parsed.host}`;
117
+ }
118
+
119
+ module.exports = {
120
+ fetchURL,
121
+ isURL,
122
+ getBaseURL
123
+ };