PR #830 - Code Quality & Markdown Tooling
Tooling Created: - tools/markdown/check-md-conformance.js - Multi-rule checker with CI exit codes - tools/markdown/fix-fence-languages.js - Auto-fixer with dry-run support, handles 3+ backtick fences Configuration Updates: - eslint.config.mjs - Added .patch/** to global ignores - package.json - Added 7 markdown check scripts, integrated into pre-release pipeline Code Quality Fixes: - 580 Prettier formatting violations fixed across entire codebase - 11 ESLint violations fixed in markdown tools
This commit is contained in:
parent
9dfe22151c
commit
49b97b824f
|
|
@ -62,6 +62,5 @@ z*/
|
|||
|
||||
# Patch archives (exclude from VCS)
|
||||
.claude/
|
||||
.husky/
|
||||
.patch/
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export default [
|
|||
'dist/**',
|
||||
'coverage/**',
|
||||
'**/*.min.js',
|
||||
'.patch/**',
|
||||
'test/template-test-generator/**',
|
||||
'test/template-test-generator/**/*.js',
|
||||
'test/template-test-generator/**/*.md',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* MD Conformance Checker (CommonMark-oriented)
|
||||
*
|
||||
|
|
@ -67,7 +66,7 @@ function isFenceStart(line) {
|
|||
|
||||
function fenceLanguage(line) {
|
||||
const m = line.match(/^\s*```\s*([a-zA-Z0-9_+-]+)?/);
|
||||
return m ? (m[1] || '') : '';
|
||||
return m ? m[1] || '' : '';
|
||||
}
|
||||
|
||||
function isBlank(line) {
|
||||
|
|
@ -84,17 +83,16 @@ function checkFile(filePath) {
|
|||
let fenceStartLine = -1;
|
||||
|
||||
// Pass 1: fence tracking to avoid interpreting list/table inside code blocks
|
||||
const excluded = new Array(lines.length).fill(false);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const excluded = Array.from({ length: lines.length }).fill(false);
|
||||
for (const [i, line] of lines.entries()) {
|
||||
if (isFenceStart(line)) {
|
||||
if (!inFence) {
|
||||
inFence = true;
|
||||
fenceStartLine = i;
|
||||
} else {
|
||||
if (inFence) {
|
||||
// closing fence
|
||||
inFence = false;
|
||||
fenceStartLine = -1;
|
||||
} else {
|
||||
inFence = true;
|
||||
fenceStartLine = i;
|
||||
}
|
||||
excluded[i] = true;
|
||||
continue;
|
||||
|
|
@ -109,7 +107,19 @@ function checkFile(filePath) {
|
|||
if (excluded[i]) {
|
||||
if (isFenceStart(lines[i])) {
|
||||
// Fence boundary
|
||||
if (!inFence) {
|
||||
if (inFence) {
|
||||
// closing
|
||||
inFence = false;
|
||||
// blank line after?
|
||||
const next = i + 1;
|
||||
if (next < lines.length && !isBlank(lines[next])) {
|
||||
violations.push({
|
||||
type: 'fence-blank-after',
|
||||
line: i + 1,
|
||||
message: 'Missing blank line after code fence',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// opening
|
||||
inFence = true;
|
||||
// language present?
|
||||
|
|
@ -130,18 +140,6 @@ function checkFile(filePath) {
|
|||
message: 'Missing blank line before code fence',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// closing
|
||||
inFence = false;
|
||||
// blank line after?
|
||||
const next = i + 1;
|
||||
if (next < lines.length && !isBlank(lines[next])) {
|
||||
violations.push({
|
||||
type: 'fence-blank-after',
|
||||
line: i + 1,
|
||||
message: 'Missing blank line after code fence',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
|
@ -211,8 +209,7 @@ function checkFile(filePath) {
|
|||
const start = i;
|
||||
// scan forward while lines look like table lines
|
||||
let end = start;
|
||||
while (end < lines.length && isTableLine(lines[end])) end++;
|
||||
|
||||
while (end < lines.length && !excluded[end] && isTableLine(lines[end])) end++;
|
||||
// Require immediate previous line to be blank
|
||||
const prev = start - 1;
|
||||
if (prev >= 0 && !isBlank(lines[prev])) {
|
||||
|
|
@ -292,3 +289,16 @@ if (require.main === module) {
|
|||
}
|
||||
|
||||
module.exports = { checkFile };
|
||||
{
|
||||
console.log(`\n- ${path.relative(process.cwd(), file)}`);
|
||||
for (const v of violations) {
|
||||
console.log(` L${v.line.toString().padStart(4, ' ')} ${v.type} ${v.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { checkFile };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fix Fence Languages - Add language identifiers to code fences
|
||||
*
|
||||
|
|
@ -14,6 +13,7 @@
|
|||
* Exit codes:
|
||||
* 0 -> No issues found or all fixed successfully
|
||||
* 1 -> Issues found (dry-run mode) or errors during fix
|
||||
* 2 -> Invalid usage (missing file arguments)
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
|
@ -26,19 +26,17 @@ const DRY_RUN = process.argv.includes('--dry-run');
|
|||
*/
|
||||
function detectLanguage(content) {
|
||||
const trimmed = content.trim();
|
||||
|
||||
|
||||
// Empty fence
|
||||
if (!trimmed) return 'text';
|
||||
|
||||
|
||||
// YAML detection
|
||||
if (/^[a-zA-Z_][a-zA-Z0-9_-]*:\s*/.test(trimmed) ||
|
||||
/^---\s*$/m.test(trimmed)) {
|
||||
if (/^[a-zA-Z_][a-zA-Z0-9_-]*:\s*/.test(trimmed) || /^---\s*$/m.test(trimmed)) {
|
||||
return 'yaml';
|
||||
}
|
||||
|
||||
|
||||
// JSON detection
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
||||
try {
|
||||
JSON.parse(trimmed);
|
||||
return 'json';
|
||||
|
|
@ -46,36 +44,36 @@ function detectLanguage(content) {
|
|||
// Not valid JSON, continue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Shell/Bash detection
|
||||
if (/^(npm|yarn|pnpm|git|node|npx|cd|mkdir|rm|cp|mv|ls|cat|echo|export|source|\$)\s/.test(trimmed) ||
|
||||
/^\$/.test(trimmed) ||
|
||||
/^#!\/bin\/(ba)?sh/.test(trimmed)) {
|
||||
if (
|
||||
/^(npm|yarn|pnpm|git|node|npx|cd|mkdir|rm|cp|mv|ls|cat|echo|export|source|\$)\s/.test(trimmed) ||
|
||||
/^\$/.test(trimmed) ||
|
||||
/^#!\/bin\/(ba)?sh/.test(trimmed)
|
||||
) {
|
||||
return 'bash';
|
||||
}
|
||||
|
||||
|
||||
// JavaScript/TypeScript detection
|
||||
if (/^(import|export|const|let|var|function|class|async|await)\s/.test(trimmed) ||
|
||||
/^\/\//.test(trimmed) ||
|
||||
/^\/\*/.test(trimmed)) {
|
||||
if (/^(import|export|const|let|var|function|class|async|await)\s/.test(trimmed) || /^\/\//.test(trimmed) || /^\/\*/.test(trimmed)) {
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
|
||||
// XML/HTML detection
|
||||
if (/^<[a-zA-Z][^>]*>/.test(trimmed)) {
|
||||
return 'xml';
|
||||
}
|
||||
|
||||
|
||||
// Markdown detection (for nested examples)
|
||||
if (/^#{1,6}\s/.test(trimmed) || /^\[.*\]\(.*\)/.test(trimmed)) {
|
||||
return 'markdown';
|
||||
}
|
||||
|
||||
|
||||
// Flow/diagram detection (arrows, boxes)
|
||||
if (/[→↓←↑]/.test(trimmed) || /[┌┐└┘├┤┬┴┼─│]/.test(trimmed)) {
|
||||
return 'text';
|
||||
}
|
||||
|
||||
|
||||
// Default to text for unknown content
|
||||
return 'text';
|
||||
}
|
||||
|
|
@ -93,47 +91,51 @@ function fixFile(filePath) {
|
|||
// Track any outer fence (of any backtick length >=3) to avoid touching nested content
|
||||
const fenceStack = [];
|
||||
|
||||
// State for a target triple-backtick fence without language that we intend to fix
|
||||
// State for a target fence (3+ backticks) without language that we intend to fix
|
||||
let fixing = false;
|
||||
let fixFenceStart = -1;
|
||||
let fixOpenIndent = '';
|
||||
let fixOpenTicks = '';
|
||||
let fixOpenLen = 0;
|
||||
let fenceContent = [];
|
||||
|
||||
const newLines = [];
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
for (const [i, line] of lines.entries()) {
|
||||
// If we are currently fixing a fence (collecting content until closing ```)
|
||||
if (fixing) {
|
||||
const closeMatch = line.match(/^(\s*)(`{3})(\s*)$/);
|
||||
const closeMatch = line.match(/^(\s*)(`+)(\s*)$/);
|
||||
if (closeMatch) {
|
||||
// Closing the target fence
|
||||
const language = detectLanguage(fenceContent.join('\n'));
|
||||
const fixedOpenLine = `${fixOpenIndent}\`\`\`${language}`;
|
||||
const closeTicks = closeMatch[2] || '';
|
||||
// Only treat as closing if the number of backticks is >= opening length
|
||||
if (closeTicks.length >= fixOpenLen) {
|
||||
// Closing the target fence
|
||||
const language = detectLanguage(fenceContent.join('\n'));
|
||||
const fixedOpenLine = `${fixOpenIndent}\`\`\`${language}`;
|
||||
|
||||
newLines.push(fixedOpenLine);
|
||||
newLines.push(...fenceContent);
|
||||
newLines.push(line);
|
||||
newLines.push(fixedOpenLine, ...fenceContent, line);
|
||||
|
||||
fixes.push({
|
||||
line: fixFenceStart + 1,
|
||||
original: '```',
|
||||
fixed: fixedOpenLine,
|
||||
detectedLanguage: language,
|
||||
contentPreview: fenceContent.slice(0, 2).join('\n').substring(0, 60) + '...'
|
||||
});
|
||||
fixes.push({
|
||||
line: fixFenceStart + 1,
|
||||
original: fixOpenTicks,
|
||||
fixed: fixedOpenLine,
|
||||
detectedLanguage: language,
|
||||
contentPreview: fenceContent.slice(0, 2).join('\n').slice(0, 60) + '...',
|
||||
});
|
||||
|
||||
modified = true;
|
||||
fixing = false;
|
||||
fixFenceStart = -1;
|
||||
fixOpenIndent = '';
|
||||
fenceContent = [];
|
||||
continue;
|
||||
} else {
|
||||
fenceContent.push(line);
|
||||
continue;
|
||||
modified = true;
|
||||
fixing = false;
|
||||
fixFenceStart = -1;
|
||||
fixOpenIndent = '';
|
||||
fixOpenTicks = '';
|
||||
fixOpenLen = 0;
|
||||
fenceContent = [];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Not a valid closing line yet; keep collecting content
|
||||
fenceContent.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not currently fixing; detect any fence line (opening or closing)
|
||||
|
|
@ -141,37 +143,44 @@ function fixFile(filePath) {
|
|||
if (fenceLineMatch) {
|
||||
const indent = fenceLineMatch[1] || '';
|
||||
const ticks = fenceLineMatch[2] || '';
|
||||
const rest = (fenceLineMatch[3] || '').trim();
|
||||
const hasLanguage = rest.length > 0; // simplistic but effective for our cases
|
||||
const ticksLen = ticks.length;
|
||||
const rest = fenceLineMatch[3] || '';
|
||||
const restTrim = rest.trim();
|
||||
const hasLanguage = restTrim.length > 0; // simplistic but effective for our cases
|
||||
|
||||
// Determine if this is a closing fence for the current outer fence
|
||||
if (fenceStack.length > 0 && fenceStack[fenceStack.length - 1].ticks === ticks) {
|
||||
// Closing existing fence scope
|
||||
fenceStack.pop();
|
||||
newLines.push(line);
|
||||
continue;
|
||||
if (fenceStack.length > 0) {
|
||||
const top = fenceStack.at(-1);
|
||||
if (restTrim === '' && ticksLen >= top.ticks.length) {
|
||||
// Closing existing fence scope
|
||||
fenceStack.pop();
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If inside any outer fence, don't attempt to fix nested fences
|
||||
if (fenceStack.length > 0) {
|
||||
newLines.push(line);
|
||||
// Start a nested fence scope if this appears to be an opening fence
|
||||
// Start a nested fence scope
|
||||
fenceStack.push({ ticks });
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Outside any fence
|
||||
if (ticks === '```' && !hasLanguage) {
|
||||
// Target: opening triple backtick without language; begin fixing mode
|
||||
if (ticksLen >= 3 && restTrim === '') {
|
||||
// Opening fence without language (3+ backticks): begin fixing mode
|
||||
fixing = true;
|
||||
fixFenceStart = i;
|
||||
fixOpenIndent = indent;
|
||||
fixOpenTicks = ticks;
|
||||
fixOpenLen = ticksLen;
|
||||
fenceContent = [];
|
||||
// Do not push the original opening line; we'll emit the fixed one at close
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any other fence (with language or more backticks): treat as an outer fence start
|
||||
// Any other fence: treat as an outer fence start
|
||||
fenceStack.push({ ticks });
|
||||
newLines.push(line);
|
||||
continue;
|
||||
|
|
@ -187,7 +196,7 @@ function fixFile(filePath) {
|
|||
filePath,
|
||||
fixes: [],
|
||||
modified: false,
|
||||
newContent: content
|
||||
newContent: content,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +204,7 @@ function fixFile(filePath) {
|
|||
filePath,
|
||||
fixes,
|
||||
modified,
|
||||
newContent: newLines.join('\n') + (content.endsWith('\n') ? '\n' : '')
|
||||
newContent: newLines.join('\n') + (content.endsWith('\n') ? '\n' : ''),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -203,67 +212,67 @@ function fixFile(filePath) {
|
|||
* Main execution
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2).filter(arg => arg !== '--dry-run');
|
||||
|
||||
const args = process.argv.slice(2).filter((arg) => arg !== '--dry-run');
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: node tools/markdown/fix-fence-languages.js [--dry-run] <file1> [file2...]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
|
||||
const results = [];
|
||||
let totalFixes = 0;
|
||||
|
||||
|
||||
for (const filePath of args) {
|
||||
const absPath = path.resolve(filePath);
|
||||
|
||||
|
||||
if (!fs.existsSync(absPath)) {
|
||||
console.error(`File not found: ${absPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (!absPath.toLowerCase().endsWith('.md')) {
|
||||
console.error(`Skipping non-markdown file: ${absPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const result = fixFile(absPath);
|
||||
|
||||
|
||||
if (result.fixes.length > 0) {
|
||||
results.push(result);
|
||||
totalFixes += result.fixes.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Print results
|
||||
if (results.length === 0) {
|
||||
console.log('✓ No fence language issues found');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
if (DRY_RUN) {
|
||||
console.log(`\n🔍 DRY RUN: Found ${totalFixes} fence(s) without language in ${results.length} file(s)\n`);
|
||||
} else {
|
||||
console.log(`\n🔧 Fixing ${totalFixes} fence(s) in ${results.length} file(s)\n`);
|
||||
}
|
||||
|
||||
|
||||
for (const result of results) {
|
||||
console.log(`📄 ${path.relative(process.cwd(), result.filePath)}`);
|
||||
|
||||
|
||||
for (const fix of result.fixes) {
|
||||
console.log(` L${fix.line.toString().padStart(4, ' ')} ${fix.original.trim() || '```'}`);
|
||||
console.log(` → \`\`\`${fix.detectedLanguage}`);
|
||||
console.log(` Content: ${fix.contentPreview}`);
|
||||
}
|
||||
|
||||
|
||||
console.log('');
|
||||
|
||||
|
||||
// Apply fixes if not dry-run
|
||||
if (!DRY_RUN) {
|
||||
fs.writeFileSync(result.filePath, result.newContent, 'utf8');
|
||||
console.log(` ✓ Fixed and saved\n`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (DRY_RUN) {
|
||||
console.log('💡 Run without --dry-run to apply these fixes\n');
|
||||
process.exit(1);
|
||||
|
|
|
|||
Loading…
Reference in New Issue