spec-up-t-healthcheck 1.1.0 â 1.1.1
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,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Markdown table validation health check module
|
|
3
|
+
*
|
|
4
|
+
* This module validates markdown tables in specification files, checking for:
|
|
5
|
+
* - Proper table structure (header, separator, body rows)
|
|
6
|
+
* - Consistent column counts across all rows
|
|
7
|
+
* - Valid separator line syntax
|
|
8
|
+
* - Problematic characters that can break table parsing
|
|
9
|
+
* - Mismatched quotes or backticks in table cells
|
|
10
|
+
*
|
|
11
|
+
* This validator catches errors that can cause issues with markdown-it-attrs
|
|
12
|
+
* and other markdown table parsing plugins.
|
|
13
|
+
*
|
|
14
|
+
* @author spec-up-t-healthcheck
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createHealthCheckResult, createErrorResult } from '../health-check-utils.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The identifier for this health check, used in reports and registries.
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
export const CHECK_ID = 'markdown-tables';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Human-readable name for this health check.
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
export const CHECK_NAME = 'Markdown Table Validation';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Description of what this health check validates.
|
|
33
|
+
* @type {string}
|
|
34
|
+
*/
|
|
35
|
+
export const CHECK_DESCRIPTION = 'Validates markdown table structure and syntax';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Represents a table found in a markdown file.
|
|
39
|
+
* @typedef {Object} TableInfo
|
|
40
|
+
* @property {string} file - Path to the file containing the table
|
|
41
|
+
* @property {number} startLine - Line number where the table starts
|
|
42
|
+
* @property {number} endLine - Line number where the table ends
|
|
43
|
+
* @property {string[]} content - Array of table lines
|
|
44
|
+
* @property {number} headerColumns - Number of columns in header
|
|
45
|
+
* @property {number} separatorColumns - Number of columns in separator
|
|
46
|
+
* @property {boolean} hasSeparator - Whether table has a separator line
|
|
47
|
+
* @property {boolean} columnMismatch - Whether column counts don't match
|
|
48
|
+
* @property {Object[]} issues - Array of specific issues found
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extracts all tables from markdown content.
|
|
53
|
+
*
|
|
54
|
+
* This function parses markdown content line by line and identifies table structures.
|
|
55
|
+
* Tables are detected by lines containing pipe characters (|).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} content - The markdown content to parse
|
|
58
|
+
* @param {string} filePath - Path to the file (for error reporting)
|
|
59
|
+
* @returns {TableInfo[]} Array of table information objects
|
|
60
|
+
*/
|
|
61
|
+
function extractTables(content, filePath) {
|
|
62
|
+
const lines = content.split('\n');
|
|
63
|
+
const tables = [];
|
|
64
|
+
let inTable = false;
|
|
65
|
+
let tableStartLine = 0;
|
|
66
|
+
let tableContent = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
const trimmedLine = line.trim();
|
|
71
|
+
|
|
72
|
+
// Detect table start (line with pipes)
|
|
73
|
+
if (trimmedLine.match(/^\|.*\|/)) {
|
|
74
|
+
if (!inTable) {
|
|
75
|
+
inTable = true;
|
|
76
|
+
tableStartLine = i + 1; // 1-indexed for user display
|
|
77
|
+
tableContent = [];
|
|
78
|
+
}
|
|
79
|
+
tableContent.push({ lineNum: i + 1, content: line });
|
|
80
|
+
} else if (inTable && (trimmedLine === '' || !line.includes('|'))) {
|
|
81
|
+
// Table ended - analyze and store it
|
|
82
|
+
if (tableContent.length > 0) {
|
|
83
|
+
const tableInfo = analyzeTable(tableContent, tableStartLine, filePath);
|
|
84
|
+
tables.push(tableInfo);
|
|
85
|
+
}
|
|
86
|
+
inTable = false;
|
|
87
|
+
} else if (inTable && line.includes('|')) {
|
|
88
|
+
// Continuation of table (might be in code block or other context)
|
|
89
|
+
tableContent.push({ lineNum: i + 1, content: line });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle table at end of file
|
|
94
|
+
if (inTable && tableContent.length > 0) {
|
|
95
|
+
const tableInfo = analyzeTable(tableContent, tableStartLine, filePath);
|
|
96
|
+
tables.push(tableInfo);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return tables;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Analyzes a single table for structural issues.
|
|
104
|
+
*
|
|
105
|
+
* This function performs comprehensive validation of table structure:
|
|
106
|
+
* - Counts columns in each row
|
|
107
|
+
* - Validates separator line format
|
|
108
|
+
* - Checks for problematic characters
|
|
109
|
+
* - Detects mismatched quotes and backticks
|
|
110
|
+
*
|
|
111
|
+
* @param {Array<{lineNum: number, content: string}>} tableLines - Array of table lines
|
|
112
|
+
* @param {number} startLine - Starting line number
|
|
113
|
+
* @param {string} filePath - Path to the file
|
|
114
|
+
* @returns {TableInfo} Analyzed table information
|
|
115
|
+
*/
|
|
116
|
+
function analyzeTable(tableLines, startLine, filePath) {
|
|
117
|
+
const issues = [];
|
|
118
|
+
|
|
119
|
+
// Extract header and separator
|
|
120
|
+
const headerLine = tableLines[0];
|
|
121
|
+
const separatorLine = tableLines.length > 1 ? tableLines[1] : null;
|
|
122
|
+
|
|
123
|
+
// Count columns
|
|
124
|
+
const headerColumns = countColumns(headerLine.content);
|
|
125
|
+
const separatorColumns = separatorLine ? countColumns(separatorLine.content) : 0;
|
|
126
|
+
const hasSeparator = separatorLine && isSeparatorLine(separatorLine.content);
|
|
127
|
+
const columnMismatch = hasSeparator && (headerColumns !== separatorColumns);
|
|
128
|
+
|
|
129
|
+
// Check each row for issues
|
|
130
|
+
tableLines.forEach((line, idx) => {
|
|
131
|
+
const rowIssues = validateTableRow(line.content, line.lineNum, idx === 1);
|
|
132
|
+
issues.push(...rowIssues);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Validate separator line specifically
|
|
136
|
+
if (separatorLine && !hasSeparator) {
|
|
137
|
+
issues.push({
|
|
138
|
+
type: 'missing-separator',
|
|
139
|
+
line: separatorLine.lineNum,
|
|
140
|
+
message: 'Table separator line is missing or malformed (should contain only |, -, and :)',
|
|
141
|
+
content: separatorLine.content
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for column count mismatches
|
|
146
|
+
if (columnMismatch) {
|
|
147
|
+
issues.push({
|
|
148
|
+
type: 'column-mismatch',
|
|
149
|
+
line: startLine,
|
|
150
|
+
message: `Column count mismatch: header has ${headerColumns} columns, separator has ${separatorColumns}`,
|
|
151
|
+
content: `Header: ${headerLine.content}\nSeparator: ${separatorLine.content}`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate body row column counts
|
|
156
|
+
if (hasSeparator && tableLines.length > 2) {
|
|
157
|
+
const expectedColumns = headerColumns;
|
|
158
|
+
for (let i = 2; i < tableLines.length; i++) {
|
|
159
|
+
const rowColumns = countColumns(tableLines[i].content);
|
|
160
|
+
if (rowColumns !== expectedColumns) {
|
|
161
|
+
issues.push({
|
|
162
|
+
type: 'row-column-mismatch',
|
|
163
|
+
line: tableLines[i].lineNum,
|
|
164
|
+
message: `Row has ${rowColumns} columns, expected ${expectedColumns}`,
|
|
165
|
+
content: tableLines[i].content
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
file: filePath,
|
|
173
|
+
startLine,
|
|
174
|
+
endLine: tableLines[tableLines.length - 1].lineNum,
|
|
175
|
+
content: tableLines.map(l => l.content),
|
|
176
|
+
headerColumns,
|
|
177
|
+
separatorColumns,
|
|
178
|
+
hasSeparator,
|
|
179
|
+
columnMismatch,
|
|
180
|
+
issues
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Counts the number of columns in a table row.
|
|
186
|
+
*
|
|
187
|
+
* Columns are delimited by pipe characters (|). The count excludes
|
|
188
|
+
* leading and trailing pipes.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} line - The table row line
|
|
191
|
+
* @returns {number} Number of columns
|
|
192
|
+
*/
|
|
193
|
+
function countColumns(line) {
|
|
194
|
+
// Remove leading/trailing whitespace and pipes
|
|
195
|
+
const trimmed = line.trim();
|
|
196
|
+
if (!trimmed) return 0;
|
|
197
|
+
|
|
198
|
+
// Count pipe separators (excluding leading/trailing)
|
|
199
|
+
const withoutEnds = trimmed.replace(/^\|/, '').replace(/\|$/, '');
|
|
200
|
+
const pipes = (withoutEnds.match(/\|/g) || []).length;
|
|
201
|
+
|
|
202
|
+
return pipes + 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Checks if a line is a valid table separator.
|
|
207
|
+
*
|
|
208
|
+
* A valid separator line contains only pipes, hyphens, colons, and whitespace.
|
|
209
|
+
* Format: |:---:|:---:| or |---|---| etc.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} line - The line to check
|
|
212
|
+
* @returns {boolean} True if the line is a valid separator
|
|
213
|
+
*/
|
|
214
|
+
function isSeparatorLine(line) {
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
// Valid separator contains only: |, -, :, and whitespace
|
|
217
|
+
return /^[\s|:\-]+$/.test(trimmed) && trimmed.includes('-');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validates a single table row for common issues.
|
|
222
|
+
*
|
|
223
|
+
* This function checks for:
|
|
224
|
+
* - Mismatched quotes (opening quote without closing)
|
|
225
|
+
* - Mismatched backticks
|
|
226
|
+
* - Problematic character combinations
|
|
227
|
+
* - Extra quotes inside code spans
|
|
228
|
+
*
|
|
229
|
+
* @param {string} line - The table row line
|
|
230
|
+
* @param {number} lineNum - Line number in file
|
|
231
|
+
* @param {boolean} isSeparator - Whether this is the separator row
|
|
232
|
+
* @returns {Array<Object>} Array of issues found
|
|
233
|
+
*/
|
|
234
|
+
function validateTableRow(line, lineNum, isSeparator) {
|
|
235
|
+
const issues = [];
|
|
236
|
+
|
|
237
|
+
// Skip validation for separator lines
|
|
238
|
+
if (isSeparator) {
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Split line into cells
|
|
243
|
+
const cells = line.split('|').slice(1, -1); // Remove first/last empty elements
|
|
244
|
+
|
|
245
|
+
cells.forEach((cell, cellIdx) => {
|
|
246
|
+
const trimmedCell = cell.trim();
|
|
247
|
+
|
|
248
|
+
// Check for problematic quote patterns in code spans
|
|
249
|
+
// Pattern: `'text or `text'
|
|
250
|
+
const backtickQuotePattern = /`['"]|['"]`/g;
|
|
251
|
+
const backtickWithQuote = trimmedCell.match(backtickQuotePattern);
|
|
252
|
+
if (backtickWithQuote) {
|
|
253
|
+
issues.push({
|
|
254
|
+
type: 'quote-in-code',
|
|
255
|
+
line: lineNum,
|
|
256
|
+
cell: cellIdx + 1,
|
|
257
|
+
message: `Cell ${cellIdx + 1} contains potentially problematic quote/backtick combination: ${backtickWithQuote.join(', ')}`,
|
|
258
|
+
content: cell,
|
|
259
|
+
severity: 'warning'
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for mismatched backticks in cell
|
|
264
|
+
const backticks = (trimmedCell.match(/`/g) || []).length;
|
|
265
|
+
if (backticks % 2 !== 0) {
|
|
266
|
+
issues.push({
|
|
267
|
+
type: 'mismatched-backticks',
|
|
268
|
+
line: lineNum,
|
|
269
|
+
cell: cellIdx + 1,
|
|
270
|
+
message: `Cell ${cellIdx + 1} has mismatched backticks`,
|
|
271
|
+
content: cell,
|
|
272
|
+
severity: 'error'
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for quote patterns that might indicate typos
|
|
277
|
+
// Pattern: opening quote at start of backtick span
|
|
278
|
+
const codeSpanStartQuote = /`'[^'`]*'?`/g;
|
|
279
|
+
const matches = trimmedCell.match(codeSpanStartQuote);
|
|
280
|
+
if (matches) {
|
|
281
|
+
// Check if any match has quotes both at start AND end inside backticks
|
|
282
|
+
matches.forEach(match => {
|
|
283
|
+
if (match.match(/`'.*'`/)) {
|
|
284
|
+
// This is likely intentional (quoted string in code)
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Only opening quote inside backticks - likely error
|
|
288
|
+
if (match.match(/`'[^']*`$/)) {
|
|
289
|
+
issues.push({
|
|
290
|
+
type: 'likely-typo',
|
|
291
|
+
line: lineNum,
|
|
292
|
+
cell: cellIdx + 1,
|
|
293
|
+
message: `Cell ${cellIdx + 1} has likely typo: opening quote inside backticks without matching closing quote`,
|
|
294
|
+
content: cell,
|
|
295
|
+
example: match,
|
|
296
|
+
severity: 'error'
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check for attributes syntax {...} which requires careful table structure
|
|
303
|
+
if (trimmedCell.includes('{') && trimmedCell.includes('}')) {
|
|
304
|
+
const hasAttributes = /\{[.#][^\}]+\}/.test(trimmedCell);
|
|
305
|
+
if (hasAttributes) {
|
|
306
|
+
issues.push({
|
|
307
|
+
type: 'has-attributes',
|
|
308
|
+
line: lineNum,
|
|
309
|
+
cell: cellIdx + 1,
|
|
310
|
+
message: `Cell ${cellIdx + 1} contains attribute syntax {.class} or {#id} - ensure table structure is correct`,
|
|
311
|
+
content: cell,
|
|
312
|
+
severity: 'info'
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return issues;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Checks markdown tables in specification files.
|
|
323
|
+
*
|
|
324
|
+
* This health check validates all markdown tables found in specification files,
|
|
325
|
+
* ensuring proper structure and catching common errors that can cause parsing issues.
|
|
326
|
+
*
|
|
327
|
+
* The check performs the following validations:
|
|
328
|
+
* - Table header and separator presence
|
|
329
|
+
* - Consistent column counts across rows
|
|
330
|
+
* - Valid separator line syntax
|
|
331
|
+
* - Detection of problematic characters
|
|
332
|
+
* - Validation of quotes and backticks in cells
|
|
333
|
+
*
|
|
334
|
+
* @param {import('../providers.js').Provider} provider - The provider instance for file operations
|
|
335
|
+
* @returns {Promise<import('../health-check-utils.js').HealthCheckResult>} The health check result
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```javascript
|
|
339
|
+
* const provider = createLocalProvider('/path/to/repo');
|
|
340
|
+
* const result = await checkMarkdownTables(provider);
|
|
341
|
+
* console.log(result.status); // 'pass', 'warning', or 'error'
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
export async function checkMarkdownTables(provider) {
|
|
345
|
+
try {
|
|
346
|
+
// Discover specification files
|
|
347
|
+
const specFiles = await discoverSpecificationFiles(provider);
|
|
348
|
+
|
|
349
|
+
if (specFiles.length === 0) {
|
|
350
|
+
return createHealthCheckResult(
|
|
351
|
+
CHECK_ID,
|
|
352
|
+
'warn',
|
|
353
|
+
'No specification files found to validate tables',
|
|
354
|
+
{
|
|
355
|
+
filesChecked: 0,
|
|
356
|
+
tablesFound: 0,
|
|
357
|
+
tablesWithIssues: 0
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let totalTables = 0;
|
|
363
|
+
let tablesWithIssues = 0;
|
|
364
|
+
let totalIssues = 0;
|
|
365
|
+
const fileResults = [];
|
|
366
|
+
|
|
367
|
+
// Check each file for tables
|
|
368
|
+
for (const filePath of specFiles) {
|
|
369
|
+
try {
|
|
370
|
+
const content = await provider.readFile(filePath);
|
|
371
|
+
const tables = extractTables(content, filePath);
|
|
372
|
+
|
|
373
|
+
totalTables += tables.length;
|
|
374
|
+
|
|
375
|
+
// Analyze tables for issues
|
|
376
|
+
const tablesWithProblems = tables.filter(t => t.issues.length > 0);
|
|
377
|
+
tablesWithIssues += tablesWithProblems.length;
|
|
378
|
+
totalIssues += tablesWithProblems.reduce((sum, t) => sum + t.issues.length, 0);
|
|
379
|
+
|
|
380
|
+
if (tablesWithProblems.length > 0) {
|
|
381
|
+
fileResults.push({
|
|
382
|
+
file: filePath,
|
|
383
|
+
tablesCount: tables.length,
|
|
384
|
+
tablesWithIssues: tablesWithProblems.length,
|
|
385
|
+
tables: tablesWithProblems
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn(`Could not read file ${filePath}:`, error.message);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Determine status
|
|
394
|
+
let status = 'pass';
|
|
395
|
+
let message = `All ${totalTables} tables are valid`;
|
|
396
|
+
|
|
397
|
+
if (tablesWithIssues > 0) {
|
|
398
|
+
// Check if any issues are errors vs warnings
|
|
399
|
+
const hasErrors = fileResults.some(fr =>
|
|
400
|
+
fr.tables.some(t =>
|
|
401
|
+
t.issues.some(i => i.severity === 'error' || i.type === 'column-mismatch' || i.type === 'missing-separator')
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
status = hasErrors ? 'fail' : 'warn';
|
|
406
|
+
message = `Found ${totalIssues} issue(s) in ${tablesWithIssues} of ${totalTables} tables`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return createHealthCheckResult(
|
|
410
|
+
CHECK_ID,
|
|
411
|
+
status,
|
|
412
|
+
message,
|
|
413
|
+
{
|
|
414
|
+
filesChecked: specFiles.length,
|
|
415
|
+
tablesFound: totalTables,
|
|
416
|
+
tablesWithIssues,
|
|
417
|
+
totalIssues,
|
|
418
|
+
details: fileResults
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return createErrorResult(
|
|
424
|
+
CHECK_ID,
|
|
425
|
+
`Failed to validate markdown tables: ${error.message}`,
|
|
426
|
+
{ error: error.message, stack: error.stack }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Discovers specification files to check for tables.
|
|
433
|
+
*
|
|
434
|
+
* This function searches for markdown files in common spec directories
|
|
435
|
+
* and the repository root.
|
|
436
|
+
*
|
|
437
|
+
* @param {import('../providers.js').Provider} provider - The provider instance
|
|
438
|
+
* @returns {Promise<string[]>} Array of file paths to check
|
|
439
|
+
* @private
|
|
440
|
+
*/
|
|
441
|
+
async function discoverSpecificationFiles(provider) {
|
|
442
|
+
const files = [];
|
|
443
|
+
const searchPaths = ['spec/', 'specs/', 'docs/', ''];
|
|
444
|
+
|
|
445
|
+
for (const searchPath of searchPaths) {
|
|
446
|
+
try {
|
|
447
|
+
const entries = await provider.listFiles(searchPath);
|
|
448
|
+
|
|
449
|
+
// Filter for markdown files only (not subdirectories)
|
|
450
|
+
const mdFiles = entries.filter(entry =>
|
|
451
|
+
entry.isFile &&
|
|
452
|
+
(entry.name.endsWith('.md') || entry.name.endsWith('.markdown'))
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Use the full path from the entry
|
|
456
|
+
files.push(...mdFiles.map(entry => entry.path));
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// Directory doesn't exist or can't be read, continue
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return [...new Set(files)]; // Remove duplicates
|
|
463
|
+
}
|
package/lib/checks/specsjson.js
CHANGED
|
@@ -341,17 +341,14 @@ function validateFieldTypes(spec, results) {
|
|
|
341
341
|
|
|
342
342
|
// Validate source object structure
|
|
343
343
|
if (spec.source && typeof spec.source === 'object') {
|
|
344
|
-
|
|
344
|
+
// Note: 'branch' field is no longer required as it's obtained from the spec-up-t:github-repo-info meta tag
|
|
345
|
+
const requiredSourceFields = ['host', 'account', 'repo'];
|
|
345
346
|
let sourceValid = true;
|
|
346
347
|
|
|
347
348
|
requiredSourceFields.forEach(field => {
|
|
348
349
|
if (!(field in spec.source) || !spec.source[field]) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
} else {
|
|
352
|
-
results.errors.push(`Source field "${field}" is missing or empty`);
|
|
353
|
-
sourceValid = false;
|
|
354
|
-
}
|
|
350
|
+
results.errors.push(`Source field "${field}" is missing or empty`);
|
|
351
|
+
sourceValid = false;
|
|
355
352
|
}
|
|
356
353
|
});
|
|
357
354
|
|
|
@@ -364,7 +361,7 @@ function validateFieldTypes(spec, results) {
|
|
|
364
361
|
if (spec.external_specs) {
|
|
365
362
|
if (Array.isArray(spec.external_specs)) {
|
|
366
363
|
spec.external_specs.forEach((extSpec, index) => {
|
|
367
|
-
const requiredExtFields = ['external_spec', 'gh_page', 'url'
|
|
364
|
+
const requiredExtFields = ['external_spec', 'gh_page', 'url'];
|
|
368
365
|
requiredExtFields.forEach(field => {
|
|
369
366
|
if (!(field in extSpec) || !extSpec[field]) {
|
|
370
367
|
results.errors.push(`External spec ${index} missing "${field}"`);
|
|
@@ -156,6 +156,12 @@ export function formatResultDetails(details) {
|
|
|
156
156
|
return html;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Special handling for markdown-tables check
|
|
160
|
+
if (details.details && Array.isArray(details.details)) {
|
|
161
|
+
html += formatMarkdownTablesDetails(details);
|
|
162
|
+
return html;
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
// Display errors array with clickable URLs
|
|
160
166
|
// Errors are shown with strong red styling to draw immediate attention
|
|
161
167
|
if (details.errors && details.errors.length > 0) {
|
|
@@ -372,6 +378,54 @@ export function formatConsoleMessagesTable(details) {
|
|
|
372
378
|
return html;
|
|
373
379
|
}
|
|
374
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Formats markdown tables details into HTML.
|
|
383
|
+
*
|
|
384
|
+
* @param {Object} details - Details object containing markdown tables data
|
|
385
|
+
* @returns {string} HTML string with formatted table issues
|
|
386
|
+
*/
|
|
387
|
+
export function formatMarkdownTablesDetails(details) {
|
|
388
|
+
const { details: fileResults } = details;
|
|
389
|
+
|
|
390
|
+
let html = '<div class="mt-2">';
|
|
391
|
+
html += '<div class="table-responsive">';
|
|
392
|
+
html += '<table class="table table-sm table-striped">';
|
|
393
|
+
html += '<thead class="table-light">';
|
|
394
|
+
html += '<tr>';
|
|
395
|
+
html += '<th>File</th>';
|
|
396
|
+
html += '<th>Line</th>';
|
|
397
|
+
html += '<th>Issue</th>';
|
|
398
|
+
html += '<th>Content</th>';
|
|
399
|
+
html += '</tr>';
|
|
400
|
+
html += '</thead>';
|
|
401
|
+
html += '<tbody>';
|
|
402
|
+
|
|
403
|
+
fileResults.forEach(fileResult => {
|
|
404
|
+
fileResult.tables.forEach(table => {
|
|
405
|
+
table.issues.forEach(issue => {
|
|
406
|
+
const severityClass = issue.severity === 'error' ? 'text-danger' :
|
|
407
|
+
issue.severity === 'warning' ? 'text-warning' : 'text-info';
|
|
408
|
+
const severityIcon = issue.severity === 'error' ? 'bi-exclamation-triangle-fill' :
|
|
409
|
+
issue.severity === 'warning' ? 'bi-exclamation-circle-fill' : 'bi-info-circle-fill';
|
|
410
|
+
|
|
411
|
+
html += '<tr>';
|
|
412
|
+
html += `<td><small><code>${escapeHtml(fileResult.file)}</code></small></td>`;
|
|
413
|
+
html += `<td><small>${issue.line}</small></td>`;
|
|
414
|
+
html += `<td><small class="${severityClass}"><i class="bi ${severityIcon}"></i> ${escapeHtml(issue.message)}</small></td>`;
|
|
415
|
+
html += `<td><small class="text-muted">${escapeHtml(issue.content || '')}</small></td>`;
|
|
416
|
+
html += '</tr>';
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
html += '</tbody>';
|
|
422
|
+
html += '</table>';
|
|
423
|
+
html += '</div>';
|
|
424
|
+
html += '</div>';
|
|
425
|
+
|
|
426
|
+
return html;
|
|
427
|
+
}
|
|
428
|
+
|
|
375
429
|
/**
|
|
376
430
|
* Formats a single health check result into a table row HTML string.
|
|
377
431
|
*
|
package/lib/formatters.js
CHANGED
|
@@ -98,6 +98,20 @@ export function formatResultsAsText(healthCheckOutput, useColors = false) {
|
|
|
98
98
|
if (result.details.packageData) {
|
|
99
99
|
output.push(` Package: ${result.details.packageData.name}@${result.details.packageData.version}`);
|
|
100
100
|
}
|
|
101
|
+
// Special handling for markdown-tables check
|
|
102
|
+
if (result.check === 'markdown-tables' && result.details.details) {
|
|
103
|
+
const fileResults = result.details.details;
|
|
104
|
+
fileResults.forEach(fileResult => {
|
|
105
|
+
output.push(` đ ${fileResult.file}:`);
|
|
106
|
+
fileResult.tables.forEach(table => {
|
|
107
|
+
output.push(` Table at line ${table.startLine}:`);
|
|
108
|
+
table.issues.forEach(issue => {
|
|
109
|
+
const severityIcon = issue.severity === 'error' ? 'â' : issue.severity === 'warning' ? 'â ī¸' : 'âšī¸';
|
|
110
|
+
output.push(` ${severityIcon} Line ${issue.line}: ${issue.message}`);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
output.push('');
|
|
@@ -253,6 +253,7 @@ export class HealthCheckRegistry {
|
|
|
253
253
|
const gitignoreModule = await import('./checks/gitignore.js');
|
|
254
254
|
const specDirectoryAndFilesModule = await import('./checks/spec-directory-and-files.js');
|
|
255
255
|
const consoleMessagesModule = await import('./checks/console-messages.js');
|
|
256
|
+
const markdownTablesModule = await import('./checks/markdown-tables.js');
|
|
256
257
|
|
|
257
258
|
// Only import link-checker in Node.js environments (not browsers)
|
|
258
259
|
// Link checker requires linkinator which uses Node.js streams
|
|
@@ -349,6 +350,18 @@ export class HealthCheckRegistry {
|
|
|
349
350
|
});
|
|
350
351
|
}
|
|
351
352
|
|
|
353
|
+
// Register markdown tables check
|
|
354
|
+
if (markdownTablesModule.checkMarkdownTables && markdownTablesModule.CHECK_ID) {
|
|
355
|
+
this.register({
|
|
356
|
+
id: markdownTablesModule.CHECK_ID,
|
|
357
|
+
name: markdownTablesModule.CHECK_NAME || 'Markdown Table Validation',
|
|
358
|
+
description: markdownTablesModule.CHECK_DESCRIPTION || 'Validates markdown table structure and syntax',
|
|
359
|
+
checkFunction: markdownTablesModule.checkMarkdownTables,
|
|
360
|
+
category: 'content',
|
|
361
|
+
priority: 25 // Run after spec files discovery, before external validations
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
352
365
|
// Register link checker (Node.js only - not available in browsers)
|
|
353
366
|
if (linkCheckerModule && linkCheckerModule.checkLinks && linkCheckerModule.CHECK_ID) {
|
|
354
367
|
this.register({
|