@pythonidaer/complexity-report 1.0.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -0
  4. package/assets/prettify.css +1 -0
  5. package/assets/prettify.js +2 -0
  6. package/assets/sort-arrow-sprite.png +0 -0
  7. package/complexity-breakdown.js +53 -0
  8. package/decision-points/ast-utils.js +127 -0
  9. package/decision-points/decision-type.js +92 -0
  10. package/decision-points/function-matching.js +185 -0
  11. package/decision-points/in-params.js +262 -0
  12. package/decision-points/index.js +6 -0
  13. package/decision-points/node-helpers.js +89 -0
  14. package/decision-points/parent-map.js +62 -0
  15. package/decision-points/parse-main.js +101 -0
  16. package/decision-points/ternary-multiline.js +86 -0
  17. package/export-generators/helpers.js +309 -0
  18. package/export-generators/index.js +143 -0
  19. package/export-generators/md-exports.js +160 -0
  20. package/export-generators/txt-exports.js +262 -0
  21. package/function-boundaries/arrow-brace-body.js +302 -0
  22. package/function-boundaries/arrow-helpers.js +93 -0
  23. package/function-boundaries/arrow-jsx.js +73 -0
  24. package/function-boundaries/arrow-object-literal.js +65 -0
  25. package/function-boundaries/arrow-single-expr.js +72 -0
  26. package/function-boundaries/brace-scanning.js +151 -0
  27. package/function-boundaries/index.js +67 -0
  28. package/function-boundaries/named-helpers.js +227 -0
  29. package/function-boundaries/parse-utils.js +456 -0
  30. package/function-extraction/ast-utils.js +112 -0
  31. package/function-extraction/extract-callback.js +65 -0
  32. package/function-extraction/extract-from-eslint.js +91 -0
  33. package/function-extraction/extract-name-ast.js +133 -0
  34. package/function-extraction/extract-name-regex.js +267 -0
  35. package/function-extraction/index.js +6 -0
  36. package/function-extraction/utils.js +29 -0
  37. package/function-hierarchy.js +427 -0
  38. package/html-generators/about.js +75 -0
  39. package/html-generators/file-boundary-builders.js +36 -0
  40. package/html-generators/file-breakdown.js +412 -0
  41. package/html-generators/file-data.js +50 -0
  42. package/html-generators/file-helpers.js +100 -0
  43. package/html-generators/file-javascript.js +430 -0
  44. package/html-generators/file-line-render.js +160 -0
  45. package/html-generators/file.css +370 -0
  46. package/html-generators/file.js +207 -0
  47. package/html-generators/folder.js +424 -0
  48. package/html-generators/index.js +6 -0
  49. package/html-generators/main-index.js +346 -0
  50. package/html-generators/shared.css +471 -0
  51. package/html-generators/utils.js +15 -0
  52. package/index.js +36 -0
  53. package/integration/eslint/index.js +94 -0
  54. package/integration/threshold/index.js +45 -0
  55. package/package.json +64 -0
  56. package/report/cli.js +58 -0
  57. package/report/index.js +559 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Breakdown and FCB (Function Complexity Breakdown) logic for file page
3
+ */
4
+
5
+ /**
6
+ * Detects which columns are completely empty across all functions
7
+ */
8
+ export function detectEmptyColumns(functions, functionBreakdowns, columnStructure) {
9
+ const emptyColumns = new Set();
10
+ const allColumnKeys = columnStructure.groups.flatMap(group =>
11
+ group.columns.map(col => col.key)
12
+ );
13
+ allColumnKeys.forEach(columnKey => {
14
+ let hasAnyValue = false;
15
+ functions.forEach(func => {
16
+ const breakdown = functionBreakdowns.get(func.line);
17
+ if (breakdown && breakdown.breakdown) {
18
+ const value = breakdown.breakdown[columnKey] || 0;
19
+ if (value > 0) hasAnyValue = true;
20
+ }
21
+ });
22
+ if (!hasAnyValue) emptyColumns.add(columnKey);
23
+ });
24
+ return emptyColumns;
25
+ }
26
+
27
+ /**
28
+ * Defines the breakdown column structure with groupings.
29
+ */
30
+ export function getBreakdownColumnStructure(variant = 'classic') {
31
+ const controlFlowBase = [
32
+ { key: 'if', label: 'if' },
33
+ { key: 'else if', label: 'else if' },
34
+ { key: 'for', label: 'for' },
35
+ { key: 'for...of', label: 'for...of' },
36
+ { key: 'for...in', label: 'for...in' },
37
+ { key: 'while', label: 'while' },
38
+ { key: 'do...while', label: 'do...while' },
39
+ ...(variant === 'modified' ? [{ key: 'switch', label: 'switch' }] : [{ key: 'case', label: 'case' }]),
40
+ { key: 'catch', label: 'catch' },
41
+ ];
42
+ return {
43
+ groups: [
44
+ { name: 'Control Flow', columns: controlFlowBase },
45
+ {
46
+ name: 'Expressions',
47
+ columns: [
48
+ { key: 'ternary', label: '?:' },
49
+ { key: '&&', label: '&&' },
50
+ { key: '||', label: '||' },
51
+ { key: '??', label: '??' },
52
+ { key: '?.', label: '?.' },
53
+ ],
54
+ },
55
+ {
56
+ name: 'Function Parameters',
57
+ columns: [{ key: 'default parameter', label: 'default parameter' }],
58
+ },
59
+ ],
60
+ baseColumn: { key: 'base', label: 'base' },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Finds the boundary for a function
66
+ */
67
+ export function findBoundaryForFunction(functionLine, functionBoundaries) {
68
+ let boundary = functionBoundaries.get(functionLine);
69
+ if (!boundary) {
70
+ for (const [, b] of functionBoundaries.entries()) {
71
+ if ((functionLine >= b.start && functionLine <= b.end) ||
72
+ (functionLine < b.start && b.start === functionLine + 1)) {
73
+ boundary = b;
74
+ break;
75
+ }
76
+ }
77
+ }
78
+ return boundary;
79
+ }
80
+
81
+ /**
82
+ * Finds the breakdown line for a function
83
+ */
84
+ export function findBreakdownLine(functionLine) {
85
+ return functionLine;
86
+ }
87
+
88
+ /**
89
+ * Logs complexity mismatches for debugging
90
+ */
91
+ export function logComplexityMismatch(func, breakdown, _functionBoundaries) {
92
+ const actualComplexity = parseInt(func.complexity, 10);
93
+ const calculatedTotal = breakdown.calculatedTotal;
94
+ if (Math.abs(calculatedTotal - actualComplexity) > 1) {
95
+ console.warn(`Complexity mismatch for ${func.functionName} at line ${func.line}: ESLint reports ${actualComplexity}, calculated ${calculatedTotal}`);
96
+ if (breakdown.decisionPoints && breakdown.decisionPoints.length > 0) {
97
+ console.warn(` Decision points found:`, breakdown.decisionPoints.map(dp => `${dp.type} at line ${dp.line}`).join(', '));
98
+ } else {
99
+ console.warn(` Decision points found: (none)`);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Calculates complexity breakdowns for all functions
106
+ */
107
+ export function calculateFunctionBreakdowns(
108
+ functions,
109
+ functionBoundaries,
110
+ decisionPoints,
111
+ calculateComplexityBreakdown
112
+ ) {
113
+ const functionBreakdowns = new Map();
114
+ functions.forEach(func => {
115
+ findBoundaryForFunction(func.line, functionBoundaries);
116
+ const breakdownLine = findBreakdownLine(func.line);
117
+ const breakdown = calculateComplexityBreakdown(breakdownLine, decisionPoints, 1);
118
+ logComplexityMismatch(func, breakdown, functionBoundaries);
119
+ functionBreakdowns.set(func.line, breakdown);
120
+ });
121
+ return functionBreakdowns;
122
+ }
123
+
124
+ /**
125
+ * Calculates decision point totals from function breakdowns
126
+ */
127
+ export function calculateDecisionPointTotalsFromBreakdowns(
128
+ functionBreakdowns
129
+ ) {
130
+ const controlFlowTypes = [
131
+ 'if',
132
+ 'else if',
133
+ 'for',
134
+ 'for...of',
135
+ 'for...in',
136
+ 'while',
137
+ 'do...while',
138
+ 'switch',
139
+ 'case',
140
+ 'catch',
141
+ ];
142
+ const expressionTypes = ['ternary', '&&', '||', '??', '?.'];
143
+ const functionParameterTypes = ['default parameter'];
144
+ let controlFlowTotal = 0;
145
+ let expressionsTotal = 0;
146
+ let functionParametersTotal = 0;
147
+ functionBreakdowns.forEach((breakdown) => {
148
+ const breakdownObj = breakdown.breakdown || {};
149
+ controlFlowTypes.forEach((type) => {
150
+ controlFlowTotal += breakdownObj[type] || 0;
151
+ });
152
+ expressionTypes.forEach((type) => {
153
+ expressionsTotal += breakdownObj[type] || 0;
154
+ });
155
+ functionParameterTypes.forEach((type) => {
156
+ functionParametersTotal += breakdownObj[type] || 0;
157
+ });
158
+ });
159
+ return {
160
+ controlFlow: controlFlowTotal,
161
+ expressions: expressionsTotal,
162
+ functionParameters: functionParametersTotal,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Generates summary section HTML
168
+ */
169
+ export function generateSummarySection(
170
+ decisionPointTotals,
171
+ totalFunctions,
172
+ withinThreshold,
173
+ _withinThresholdPercentage
174
+ ) {
175
+ const { controlFlow, expressions, functionParameters } = decisionPointTotals;
176
+ const formatPercentage = (num, den) => (den === 0 ? '0%' : ((num / den) * 100) % 1 === 0 ? `${(num / den) * 100}%` : `${((num / den) * 100).toFixed(2)}%`);
177
+ const functionsPercentage = formatPercentage(withinThreshold, totalFunctions);
178
+ return `
179
+ <div class="clearfix">
180
+ <div class='fl pad1y space-right2'>
181
+ <span class="strong">${functionsPercentage}</span>
182
+ <span class="quiet">Functions</span>
183
+ <span class='fraction'>${withinThreshold}/${totalFunctions}</span>
184
+ </div>
185
+ <div class='fl pad1y space-right2'>
186
+ <span class="quiet">Control Flow</span>
187
+ <span class='fraction'>${controlFlow}</span>
188
+ </div>
189
+ <div class='fl pad1y space-right2'>
190
+ <span class="quiet">Expressions</span>
191
+ <span class='fraction'>${expressions}</span>
192
+ </div>
193
+ <div class='fl pad1y space-right2'>
194
+ <span class="quiet">Default Parameters</span>
195
+ <span class='fraction'>${functionParameters}</span>
196
+ </div>
197
+ </div>`;
198
+ }
199
+
200
+ /**
201
+ * Calculates file-level statistics
202
+ */
203
+ export function calculateFileStatistics(
204
+ functions,
205
+ complexityThreshold = 10
206
+ ) {
207
+ const totalFunctions = functions.length;
208
+ const withinThreshold = functions.filter((f) =>
209
+ parseInt(f.complexity, 10) <= complexityThreshold
210
+ ).length;
211
+ const maxComplexity = functions.length > 0
212
+ ? Math.max(...functions.map((f) => parseInt(f.complexity, 10)))
213
+ : 0;
214
+ const avgComplexity = functions.length > 0
215
+ ? Math.round(
216
+ functions.reduce((sum, f) => sum + parseInt(f.complexity, 10), 0) /
217
+ functions.length
218
+ )
219
+ : 0;
220
+ const percentage = totalFunctions > 0
221
+ ? Math.round((withinThreshold / totalFunctions) * 100)
222
+ : 100;
223
+ const level =
224
+ percentage >= 80
225
+ ? 'high'
226
+ : percentage >= 60
227
+ ? 'high'
228
+ : percentage >= 40
229
+ ? 'medium'
230
+ : 'low';
231
+ return {
232
+ totalFunctions,
233
+ withinThreshold,
234
+ maxComplexity,
235
+ avgComplexity,
236
+ percentage,
237
+ level,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Computes display options for the breakdown section
243
+ */
244
+ export function getBreakdownSectionOptions(
245
+ totalBreakdownCols,
246
+ hideLinesInitially,
247
+ hideTableInitially,
248
+ initialShowAllColumns,
249
+ hideHighlightsInitially
250
+ ) {
251
+ const colspanWithLines = 3 + totalBreakdownCols;
252
+ const colspanWithoutLines = 2 + totalBreakdownCols;
253
+ return {
254
+ colspanWithLines,
255
+ colspanWithoutLines,
256
+ initialColspan: hideLinesInitially ? colspanWithoutLines : colspanWithLines,
257
+ tableClass: hideLinesInitially ? 'complexity-breakdown-table hide-lines' : 'complexity-breakdown-table',
258
+ showAllColumnsChecked: initialShowAllColumns ? 'checked' : '',
259
+ showLinesChecked: hideLinesInitially ? '' : 'checked',
260
+ showTableChecked: hideTableInitially ? '' : 'checked',
261
+ showHighlightsChecked: hideHighlightsInitially ? '' : 'checked',
262
+ tableDisplay: hideTableInitially ? 'none' : 'table',
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Generates breakdown section HTML
268
+ */
269
+ export function generateBreakdownSectionHTML(
270
+ functions,
271
+ initialColumns,
272
+ breakdownItems,
273
+ columnStructure,
274
+ initialShowAllColumns,
275
+ hideTableInitially,
276
+ hideLinesInitially = false,
277
+ hideHighlightsInitially = false
278
+ ) {
279
+ if (functions.length === 0) return '';
280
+ const opts = getBreakdownSectionOptions(
281
+ initialColumns.totalBreakdownCols,
282
+ hideLinesInitially,
283
+ hideTableInitially,
284
+ initialShowAllColumns,
285
+ hideHighlightsInitially
286
+ );
287
+ const groupHeadersHTML = initialColumns.visibleGroups.map(group => {
288
+ if (group.columns.length === 0) return '';
289
+ return `<th colspan="${group.columns.length}" class="breakdown-group-header" data-group="${group.name}" data-total-cols="${group.totalColumns}">${group.name}</th>`;
290
+ }).filter(Boolean).join('');
291
+ const colHeadersHTML = initialColumns.visibleGroups.map(group =>
292
+ group.columns.map(col =>
293
+ `<th class="breakdown-col-header sortable" data-column-key="${col.key}" onclick="sortTable('${col.key}')" id="sort-${col.key}-header">${col.label} <span class="sort-indicator" id="sort-${col.key}-indicator"></span></th>`
294
+ ).join('')
295
+ ).join('');
296
+ return `
297
+ <div class="complexity-breakdown">
298
+ <div class="quiet" style="display: flex; align-items: center; gap: 15px; margin-top: 14px;">
299
+ <div>Filter: <input type="search" id="breakdown-search" oninput="filterFunctions(this.value)"></div>
300
+ <label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-all-columns" onchange="toggleEmptyColumns()" ${opts.showAllColumnsChecked}> Show All Columns</label>
301
+ <label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-lines" onchange="toggleLineColumn()" ${opts.showLinesChecked}> Show Lines</label>
302
+ <label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-table" onchange="toggleTableVisibility()" ${opts.showTableChecked}> Show Table</label>
303
+ <label style="margin: 0; font-weight: normal;"><input type="checkbox" id="breakdown-show-highlights" onchange="toggleHighlights()" ${opts.showHighlightsChecked}> Show Highlights</label>
304
+ </div>
305
+ <table class="${opts.tableClass}" id="complexity-breakdown-table" style="display: ${opts.tableDisplay};" data-colspan-with-lines="${opts.colspanWithLines}" data-colspan-without-lines="${opts.colspanWithoutLines}">
306
+ <thead id="complexity-breakdown-thead">
307
+ <tr><th colspan="${opts.initialColspan}" class="breakdown-header" id="breakdown-header-span">Function Complexity Breakdown</th></tr>
308
+ <tr id="breakdown-group-headers-row">
309
+ <th rowspan="2" class="breakdown-function-header sortable" onclick="sortTable('function')" id="sort-function-header">Function (base = 1) <span class="sort-indicator" id="sort-function-indicator"></span></th>
310
+ <th rowspan="2" class="breakdown-line-header breakdown-line-column sortable" onclick="sortTable('line')" id="sort-line-header">Line <span class="sort-indicator" id="sort-line-indicator"></span></th>
311
+ <th rowspan="2" class="breakdown-complexity-header sortable" onclick="sortTable('complexity')" id="sort-complexity-header">Complexity <span class="sort-indicator" id="sort-complexity-indicator"></span></th>
312
+ ${groupHeadersHTML}
313
+ </tr>
314
+ <tr id="breakdown-col-headers-row">${colHeadersHTML}</tr>
315
+ </thead>
316
+ <tbody id="complexity-breakdown-tbody">
317
+ ${breakdownItems}
318
+ <tr id="no-matches-row" style="display: none;"><td colspan="${opts.initialColspan}" class="no-matches-message">No functions match the current filter.</td></tr>
319
+ </tbody>
320
+ </table>
321
+ </div>
322
+ `;
323
+ }
324
+
325
+ /**
326
+ * Generates statistics HTML section
327
+ */
328
+ export function _generateStatisticsHTML(
329
+ totalFunctions,
330
+ withinThreshold,
331
+ maxComplexity,
332
+ avgComplexity
333
+ ) {
334
+ const maxComplexityHTML = maxComplexity > 0
335
+ ? `
336
+ <div class='fl pad1y space-right2'>
337
+ <span class="strong">${maxComplexity} </span>
338
+ <span class="quiet">Max Complexity</span>
339
+ </div>
340
+ <div class='fl pad1y space-right2'>
341
+ <span class="strong">${avgComplexity} </span>
342
+ <span class="quiet">Avg Complexity</span>
343
+ </div>`
344
+ : '';
345
+ return maxComplexityHTML;
346
+ }
347
+
348
+ /**
349
+ * Prepares file-level data for HTML generation
350
+ */
351
+ export function prepareFileLevelData(
352
+ functions,
353
+ functionBreakdowns,
354
+ complexityThreshold
355
+ ) {
356
+ const stats = calculateFileStatistics(functions, complexityThreshold);
357
+ const decisionPointTotals =
358
+ calculateDecisionPointTotalsFromBreakdowns(functionBreakdowns);
359
+ const withinThresholdPercentage = stats.totalFunctions > 0
360
+ ? Math.round((stats.withinThreshold / stats.totalFunctions) * 100)
361
+ : 100;
362
+ return { ...stats, decisionPointTotals, withinThresholdPercentage };
363
+ }
364
+
365
+ /**
366
+ * Prepares breakdown column structure
367
+ */
368
+ export function prepareBreakdownColumns(
369
+ functions,
370
+ functionBreakdowns,
371
+ showAllColumnsInitially,
372
+ variant = 'classic'
373
+ ) {
374
+ const columnStructure = getBreakdownColumnStructure(variant);
375
+ const emptyColumns = detectEmptyColumns(
376
+ functions,
377
+ functionBreakdowns,
378
+ columnStructure
379
+ );
380
+ const initialColumns = buildVisibleColumns(
381
+ columnStructure,
382
+ emptyColumns,
383
+ showAllColumnsInitially
384
+ );
385
+ return { columnStructure, emptyColumns, initialColumns };
386
+ }
387
+
388
+ /**
389
+ * Builds visible column structure
390
+ */
391
+ export function buildVisibleColumns(
392
+ columnStructure,
393
+ emptyColumns,
394
+ showAll
395
+ ) {
396
+ const visibleGroups = columnStructure.groups.map((group) => {
397
+ const visibleColumns = showAll
398
+ ? group.columns
399
+ : group.columns.filter((col) => !emptyColumns.has(col.key));
400
+ return {
401
+ name: group.name,
402
+ columns: visibleColumns,
403
+ totalColumns: group.columns.length,
404
+ visibleColumns: visibleColumns.length,
405
+ };
406
+ });
407
+ const totalBreakdownCols = visibleGroups.reduce(
408
+ (sum, group) => sum + group.columns.length,
409
+ 0
410
+ );
411
+ return { visibleGroups, totalBreakdownCols };
412
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Data maps for file page (line-to-function, line-to-decision-point)
3
+ */
4
+
5
+ /**
6
+ * Creates line-to-function map
7
+ * @param {Array} functions - Functions array
8
+ * @returns {Map} Line to function map
9
+ */
10
+ export function createLineToFunctionMap(functions) {
11
+ const lineToFunction = new Map();
12
+ functions.forEach(func => {
13
+ lineToFunction.set(func.line, func);
14
+ });
15
+ return lineToFunction;
16
+ }
17
+
18
+ /**
19
+ * Creates decision point line map for highlighting.
20
+ * Expands multi-line DPs: when dp.lines exists, adds one entry per
21
+ * line with that line's column range.
22
+ * @param {Array} decisionPoints - Decision points array
23
+ * @returns {Map} Line to decision points map
24
+ */
25
+ export function createDecisionPointLineMap(decisionPoints) {
26
+ const lineToDecisionPoint = new Map();
27
+ decisionPoints.forEach(dp => {
28
+ if (dp.lines && Array.isArray(dp.lines)) {
29
+ dp.lines.forEach(({ line: lineNum, column, endColumn }) => {
30
+ if (!lineToDecisionPoint.has(lineNum)) {
31
+ lineToDecisionPoint.set(lineNum, []);
32
+ }
33
+ lineToDecisionPoint.get(lineNum).push({
34
+ type: dp.type,
35
+ line: lineNum,
36
+ functionLine: dp.functionLine,
37
+ name: dp.name,
38
+ column,
39
+ endColumn,
40
+ });
41
+ });
42
+ } else {
43
+ if (!lineToDecisionPoint.has(dp.line)) {
44
+ lineToDecisionPoint.set(dp.line, []);
45
+ }
46
+ lineToDecisionPoint.get(dp.line).push(dp);
47
+ }
48
+ });
49
+ return lineToDecisionPoint;
50
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Utility helpers for file page generation
3
+ */
4
+ import { readFileSync, existsSync } from 'fs';
5
+
6
+ /**
7
+ * Detects language from file extension for syntax highlighting
8
+ * @param {string} filePath - File path
9
+ * @returns {string} Language class for prettify (e.g., 'lang-js', 'lang-ts')
10
+ */
11
+ export function detectLanguage(filePath) {
12
+ const ext = filePath.split('.').pop()?.toLowerCase();
13
+ const langMap = {
14
+ 'js': 'lang-js',
15
+ 'jsx': 'lang-js',
16
+ 'ts': 'lang-ts',
17
+ 'tsx': 'lang-ts',
18
+ 'css': 'lang-css',
19
+ 'html': 'lang-html',
20
+ 'json': 'lang-json',
21
+ 'md': 'lang-md',
22
+ 'py': 'lang-py',
23
+ 'java': 'lang-java',
24
+ 'c': 'lang-c',
25
+ 'cpp': 'lang-cpp',
26
+ 'cs': 'lang-cs',
27
+ 'rb': 'lang-rb',
28
+ 'php': 'lang-php',
29
+ 'go': 'lang-go',
30
+ 'rs': 'lang-rs',
31
+ 'sh': 'lang-sh',
32
+ 'sql': 'lang-sql',
33
+ };
34
+ return langMap[ext] || 'lang-js';
35
+ }
36
+
37
+ /**
38
+ * Calculates relative path to prettify files based on directory depth
39
+ * @param {string} filePath - Relative file path (e.g., 'src/components/Button.tsx')
40
+ * @returns {string} Relative path to prettify files (e.g., '../../prettify.css')
41
+ */
42
+ export function getPrettifyRelativePath(filePath) {
43
+ const fileDir = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : '';
44
+ if (!fileDir) return '';
45
+ const depth = fileDir.split('/').length;
46
+ return '../'.repeat(depth);
47
+ }
48
+
49
+ /**
50
+ * Returns indent length in character units (tabs expanded to 2 spaces).
51
+ * @param {string} line - Source line
52
+ * @returns {number} Indent in characters
53
+ */
54
+ export function getIndentChars(line) {
55
+ const m = line && line.match(/^(\s*)/);
56
+ if (!m) return 0;
57
+ return m[1].replace(/\t/g, ' ').length;
58
+ }
59
+
60
+ /**
61
+ * Reads source file and returns code and lines
62
+ * @param {string} fullPath - Absolute path to file
63
+ * @param {string} filePath - Relative path for error messages
64
+ * @returns {{ sourceCode: string, sourceLines: string[] }}
65
+ */
66
+ export function readSourceFile(fullPath, filePath) {
67
+ let sourceCode = '';
68
+ let sourceLines = [];
69
+ try {
70
+ if (existsSync(fullPath)) {
71
+ sourceCode = readFileSync(fullPath, 'utf-8');
72
+ sourceLines = sourceCode.split('\n');
73
+ }
74
+ } catch (error) {
75
+ console.warn(`Warning: Could not read source file ${filePath}:`, error.message);
76
+ }
77
+ return { sourceCode, sourceLines };
78
+ }
79
+
80
+ /**
81
+ * Returns paths for file page assets
82
+ * @param {string} filePath - Relative file path
83
+ * @param {string} fileDir - Directory part of file path
84
+ * @returns {Object} Paths: backLink, folderIndexPath, aboutPath,
85
+ * prettifyCssPath, prettifyJsPath, sharedCssPath, fileCssPath
86
+ */
87
+ export function getFilePagePaths(filePath, fileDir) {
88
+ const depth = fileDir ? fileDir.split('/').length : 0;
89
+ const prefix = depth > 0 ? '../'.repeat(depth) : '';
90
+ const prettifyPath = getPrettifyRelativePath(filePath);
91
+ return {
92
+ backLink: prefix ? `${prefix}index.html` : 'index.html',
93
+ folderIndexPath: fileDir ? 'index.html' : 'index.html',
94
+ aboutPath: prefix ? `${prefix}about.html` : 'about.html',
95
+ prettifyCssPath: prettifyPath ? `${prettifyPath}prettify.css` : 'prettify.css',
96
+ prettifyJsPath: prettifyPath ? `${prettifyPath}prettify.js` : 'prettify.js',
97
+ sharedCssPath: prettifyPath ? `${prettifyPath}shared.css` : 'shared.css',
98
+ fileCssPath: prettifyPath ? `${prettifyPath}file.css` : 'file.css',
99
+ };
100
+ }