Initial commit with translated description
This commit is contained in:
439
index.js
Normal file
439
index.js
Normal file
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* Markdown-Formatter - Format and beautify markdown documents
|
||||
* Vernox v1.0 - Autonomous Revenue Agent
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Pattern matches
|
||||
const PATTERNS = {
|
||||
trailingWhitespace: /[ \t]+$/gm,
|
||||
multipleBlankLines: /\n{3,}/g,
|
||||
inconsistentListDash: /^\s{0,2}([*-])\s/g,
|
||||
inconsistentListAsterisk: /^\s{0,2}([*])\s/g,
|
||||
inconsistentListPlus: /^\s{0,2}([+])\s/g,
|
||||
inconsistentListTask: /^\s{0,2}(- \[ )\]\s/g,
|
||||
atxHeading: /^#{1,2}\s[^#]+/,
|
||||
setextHeading: /^#{1,2}\s[=-]+/,
|
||||
codeBlockFenced: /^```/g,
|
||||
codeBlockIndented: /^ {4}/,
|
||||
strikethrough: /~~(.+?)~~/g,
|
||||
autoLink: /<https?:\/\/[^>\s]+>/gi
|
||||
};
|
||||
|
||||
// Constants for style guides
|
||||
const STYLE_GUIDES = {
|
||||
commonmark: {
|
||||
headingLevels: ['#', '##', '###'],
|
||||
emphasis: {
|
||||
underscore: '__text__',
|
||||
asterisk: '**text**'
|
||||
},
|
||||
links: {
|
||||
inline: '[text](url)',
|
||||
reference: '[text][id]'
|
||||
}
|
||||
},
|
||||
github: {
|
||||
headingLevels: ['#', '##', '###'],
|
||||
emphasis: {
|
||||
underscore: '__text__',
|
||||
asterisk: '**text**'
|
||||
},
|
||||
links: {
|
||||
inline: '[text](url)',
|
||||
reference: '[text][id]'
|
||||
},
|
||||
codeBlocks: {
|
||||
fenced: '```',
|
||||
indented: ' '
|
||||
},
|
||||
lists: {
|
||||
unordered: '-',
|
||||
ordered: '1.',
|
||||
task: '- [ ]'
|
||||
},
|
||||
strikethrough: '~~text~~',
|
||||
autoLinks: '<https://url>'
|
||||
},
|
||||
tables: '| column | column | '
|
||||
},
|
||||
consistent: {
|
||||
headingLevels: ['#', '##', '###'],
|
||||
emphasis: {
|
||||
underscore: false,
|
||||
asterisk: true
|
||||
},
|
||||
links: {
|
||||
inline: '[text](url)'
|
||||
},
|
||||
lists: {
|
||||
unordered: '-',
|
||||
ordered: '1.'
|
||||
},
|
||||
codeBlocks: {
|
||||
fenced: '```'
|
||||
},
|
||||
tables: '| column | column | '
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format markdown content according to style guide
|
||||
*/
|
||||
function formatMarkdown(params) {
|
||||
const { markdown, style, options = {} } = params;
|
||||
|
||||
if (!markdown) {
|
||||
throw new Error('markdown is required');
|
||||
}
|
||||
|
||||
const styleGuide = STYLE_GUIDES[style] || STYLE_GUIDES.github;
|
||||
const opts = { ...STYLE_GUIDES.github, ...options };
|
||||
|
||||
let formatted = markdown;
|
||||
const warnings = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
// 1. Normalize line endings
|
||||
formatted = formatted.replace(/\r\n/g, '\n');
|
||||
|
||||
// 2. Remove trailing whitespace from each line
|
||||
const lines = formatted.split('\n');
|
||||
formatted = lines.map(line => line.trimRight()).join('\n');
|
||||
|
||||
// 3. Fix inconsistent list markers
|
||||
if (opts.fixLists) {
|
||||
const fixedLists = fixListMarkers(lines, styleGuide);
|
||||
formatted = fixedLists.markdown;
|
||||
if (fixedLists.warnings.length > 0) {
|
||||
warnings.push(...fixedLists.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Normalize heading styles
|
||||
if (styleGuide.headingLevels) {
|
||||
const fixedHeadings = normalizeHeadings(lines, styleGuide, opts.headingStyle);
|
||||
formatted = fixedHeadings.markdown;
|
||||
if (fixedHeadings.warnings.length > 0) {
|
||||
warnings.push(...fixedHeadings.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Normalize emphasis
|
||||
if (styleGuide.emphasis) {
|
||||
const fixedEmphasis = normalizeEmphasis(formatted, styleGuide.emphasis);
|
||||
formatted = fixedEmphasis.markdown;
|
||||
if (fixedEmphasis.warnings.length > 0) {
|
||||
warnings.push(...fixedEmphasis.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Fix code blocks
|
||||
if (styleGuide.codeBlocks) {
|
||||
const fixedCode = fixCodeBlocks(formatted, styleGuide.codeBlocks);
|
||||
formatted = fixedCode.markdown;
|
||||
if (fixedCode.warnings.length > 0) {
|
||||
warnings.push(...fixedCode.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Fix tables
|
||||
if (styleGuide.tables) {
|
||||
const fixedTables = fixTables(formatted);
|
||||
formatted = fixedTables.markdown;
|
||||
if (fixedTables.warnings.length > 0) {
|
||||
warnings.push(...fixedTables.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Remove multiple consecutive blank lines
|
||||
if (opts.normalizeSpacing) {
|
||||
formatted = formatted.replace(PATTERNS.multipleBlankLines, '\n\n\n');
|
||||
}
|
||||
|
||||
// 9. Wrap long lines
|
||||
if (opts.maxWidth) {
|
||||
const wrappedLines = lines.map(line => wrapLine(line, opts.maxWidth));
|
||||
formatted = wrappedLines.join('\n');
|
||||
}
|
||||
|
||||
// 10. Add spacing around emphasis
|
||||
if (styleGuide.emphasis && styleGuide.emphasis !== 'none') {
|
||||
formatted = addEmphasisSpacing(formatted, styleGuide.emphasis);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const stats = {
|
||||
originalLength: markdown.length,
|
||||
formattedLength: formatted.length,
|
||||
warnings: warnings.length
|
||||
};
|
||||
|
||||
return {
|
||||
formattedMarkdown: formatted,
|
||||
warnings,
|
||||
stats,
|
||||
lintResult: {
|
||||
errors: [],
|
||||
warnings: warnings,
|
||||
fixed: markdown
|
||||
},
|
||||
processingTime: endTime - startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix inconsistent list markers
|
||||
*/
|
||||
function fixListMarkers(lines, styleGuide) {
|
||||
const warnings = [];
|
||||
let markdown = lines.join('\n');
|
||||
|
||||
// Find and standardize unordered lists
|
||||
if (styleGuide.lists) {
|
||||
const unorderedPattern = /^\s{0,2}([*-])\s/g;
|
||||
const matches = markdown.match(unorderedPattern);
|
||||
|
||||
if (matches) {
|
||||
const replacement = opts.lists === 'asterisk' ? '- ' : opts.lists === 'plus' ? '+ ' : '-';
|
||||
markdown = markdown.replace(unorderedPattern, `$1$2$3`);
|
||||
} else if (styleGuide.lists === 'dash') {
|
||||
markdown = markdown.replace(/^\s{0,2}([*-])\s/g, '$1$2$3');
|
||||
}
|
||||
}
|
||||
|
||||
return { markdown, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize heading styles
|
||||
*/
|
||||
function normalizeHeadings(lines, styleGuide, headingStyle) {
|
||||
const warnings = [];
|
||||
let markdown = lines.join('\n');
|
||||
|
||||
if (headingStyle === 'atx') {
|
||||
// Ensure ATX headings (### instead of #)
|
||||
const atxCount = (markdown.match(/#+\s+/g) || []).length;
|
||||
if (atxCount > 0) {
|
||||
warnings.push(`${atxCount} ATX-style headings found (should use ###)`);
|
||||
markdown = markdown.replace(/#+\s+/g, '### ');
|
||||
}
|
||||
} else if (headingStyle === 'setext') {
|
||||
// Ensure Setext headings (==== instead of #)
|
||||
const setextCount = (markdown.match(/^={4,}\s/g) || []).length;
|
||||
if (setextCount > 0) {
|
||||
warnings.push(`${setextCount} Setext headings found (should use #)`);
|
||||
markdown = markdown.replace(/^={4,}\s/g, '#');
|
||||
}
|
||||
} else if (headingStyle === 'underlined') {
|
||||
// Ensure underlined headings (=== or ---)
|
||||
const underlinePattern = /^(={3,}|-{3,})\n/gm;
|
||||
const underlineCount = (markdown.match(underlinePattern) || []).length;
|
||||
if (underlineCount > 0) {
|
||||
warnings.push(`${underlineCount} underline headings found (should use #)`);
|
||||
markdown = markdown.replace(underlinePattern, '#$1');
|
||||
}
|
||||
}
|
||||
|
||||
return { markdown, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize emphasis
|
||||
*/
|
||||
function normalizeEmphasis(markdown, emphasis) {
|
||||
const warnings = [];
|
||||
|
||||
if (emphasis === 'asterisk') {
|
||||
markdown = markdown.replace(/\*\*(?!.*?\*)/g, '*');
|
||||
markdown = markdown.replace(/_{2,}/g, '_');
|
||||
markdown = markdown.replace(/__{2,}/g, '__');
|
||||
} else if (emphasis === 'underscore') {
|
||||
markdown = markdown.replace(/__(.+?)__/g, '*$1*');
|
||||
}
|
||||
|
||||
return { markdown, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix code blocks
|
||||
*/
|
||||
function fixCodeBlocks(markdown, codeBlockStyle) {
|
||||
const warnings = [];
|
||||
|
||||
if (codeBlockStyle === 'fenced') {
|
||||
const lines = markdown.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (/^```\s*$/.test(line) && !line.endsWith('```')) {
|
||||
warnings.push(`Unclosed fenced code block at line ${i + 1}`);
|
||||
lines[i] = line + '```';
|
||||
}
|
||||
}
|
||||
markdown = lines.join('\n');
|
||||
} else if (codeBlockStyle === 'indented') {
|
||||
markdown = markdown.replace(/^ {4}/gm, ' ');
|
||||
}
|
||||
|
||||
return { markdown, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix tables
|
||||
*/
|
||||
function fixTables(markdown) {
|
||||
const warnings = [];
|
||||
const headerPattern = /\|[^|\n]+?\|/g;
|
||||
markdown = markdown.replace(headerPattern, '| ');
|
||||
|
||||
return { markdown, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap line at maxWidth
|
||||
*/
|
||||
function wrapLine(line, maxWidth) {
|
||||
if (line.length <= maxWidth) return line;
|
||||
return line.substring(0, maxWidth) + '\n' + line.substring(maxWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add spacing around emphasis
|
||||
*/
|
||||
function addEmphasisSpacing(markdown, emphasis) {
|
||||
if (emphasis === 'asterisk') {
|
||||
return markdown.replace(/([^\s\*])(\*)([^\s])/g, '$1 $2$3');
|
||||
}
|
||||
return markdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lint markdown for issues
|
||||
*/
|
||||
function lintMarkdown(params) {
|
||||
const { markdown, style, options = {} } = params;
|
||||
const styleGuide = STYLE_GUIDES[style] || STYLE_GUIDES.github;
|
||||
const opts = { ...STYLE_GUIDES.github, ...options };
|
||||
|
||||
const warnings = [];
|
||||
const errors = [];
|
||||
|
||||
// Check heading levels
|
||||
if (opts.checkHeadingLevels) {
|
||||
const lines = markdown.split('\n');
|
||||
const headings = lines.filter(line => /^#+\s/.test(line));
|
||||
|
||||
let prevLevel = 0;
|
||||
headings.forEach((heading, index) => {
|
||||
const match = heading.match(/^(#{1,2})\s+(.*)/);
|
||||
const level = match[1].length;
|
||||
|
||||
if (index > 0 && level > prevLevel + 1) {
|
||||
errors.push({
|
||||
type: 'heading_skip',
|
||||
message: `Heading skipped ${level - prevLevel - 1} levels at line ${index + 1}`,
|
||||
line: heading
|
||||
});
|
||||
}
|
||||
|
||||
prevLevel = level;
|
||||
});
|
||||
}
|
||||
|
||||
const stats = {
|
||||
headingLevels: (markdown.match(/#+/g) || []).length,
|
||||
listMarkers: (markdown.match(/[-*+]/g) || []).length,
|
||||
emphasisMarkers: (markdown.match(/[*_]/g) || []).length,
|
||||
codeBlocks: (markdown.match(/```/g) || []).length / 2,
|
||||
tables: (markdown.match(/\|[^|\n]+?\|/g) || []).length / 3
|
||||
};
|
||||
|
||||
return { errors, warnings, stats, suggestions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format multiple markdown files
|
||||
*/
|
||||
function formatBatch(params) {
|
||||
const { markdownFiles, style, options = {} } = params;
|
||||
|
||||
if (!markdownFiles || !Array.isArray(markdownFiles)) {
|
||||
throw new Error('markdownFiles must be an array of file paths');
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const results = [];
|
||||
const totalWarnings = [];
|
||||
|
||||
for (const filePath of markdownFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const result = formatMarkdown({
|
||||
markdown: content,
|
||||
style,
|
||||
options
|
||||
});
|
||||
|
||||
results.push({
|
||||
file: filePath,
|
||||
formattedMarkdown: result.formattedMarkdown,
|
||||
warnings: result.warnings,
|
||||
stats: result.stats
|
||||
});
|
||||
|
||||
totalWarnings.push(...result.warnings);
|
||||
} catch (error) {
|
||||
results.push({
|
||||
file: filePath,
|
||||
error: error.message || error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
return {
|
||||
results,
|
||||
totalFiles: markdownFiles.length,
|
||||
totalWarnings: totalWarnings.length,
|
||||
processingTime: endTime - startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function - handles tool invocations
|
||||
*/
|
||||
function main(action, params) {
|
||||
switch (action) {
|
||||
case 'formatMarkdown':
|
||||
return formatMarkdown(params);
|
||||
case 'formatBatch':
|
||||
return formatBatch(params);
|
||||
case 'lintMarkdown':
|
||||
return lintMarkdown(params);
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const action = args[0];
|
||||
|
||||
try {
|
||||
const params = JSON.parse(args[1] || '{}');
|
||||
const result = main(action, params);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
error: error.message || error
|
||||
}, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { main, formatMarkdown, formatBatch, lintMarkdown };
|
||||
Reference in New Issue
Block a user