Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ md-tree stats README.md
md-tree toc README.md --max-level 3
```

### Check links

```bash
md-tree check-links README.md
md-tree check-links README.md --recursive
```

### Complete CLI options

```bash
Expand Down Expand Up @@ -144,13 +151,17 @@ console.log(sectionMarkdown);
### Advanced Usage

```javascript
import { MarkdownTreeParser, createParser, extractSection } from 'markdown-tree-parser';
import {
MarkdownTreeParser,
createParser,
extractSection,
} from 'markdown-tree-parser';

// Create parser with custom options
const parser = createParser({
bullet: '-', // Use '-' for lists
emphasis: '_', // Use '_' for emphasis
strong: '__' // Use '__' for strong
bullet: '-', // Use '-' for lists
emphasis: '_', // Use '_' for emphasis
strong: '__', // Use '__' for strong
});

// Extract all sections at level 2
Expand Down Expand Up @@ -178,8 +189,7 @@ const codeBlocks = parser.selectAll(tree, 'code');

// Custom search
const customNode = parser.findNode(tree, (node) => {
return node.type === 'heading' &&
parser.getHeadingText(node).includes('API');
return node.type === 'heading' && parser.getHeadingText(node).includes('API');
});

// Transform content
Expand All @@ -191,7 +201,9 @@ parser.transform(tree, (node) => {

// Get document statistics
const stats = parser.getStats(tree);
console.log(`Document has ${stats.wordCount} words and ${stats.headings.total} headings`);
console.log(
`Document has ${stats.wordCount} words and ${stats.headings.total} headings`
);

// Generate table of contents
const toc = parser.generateTableOfContents(tree, 3);
Expand Down Expand Up @@ -234,7 +246,7 @@ for (let i = 0; i < sections.length; i++) {
#### Constructor

```javascript
new MarkdownTreeParser(options = {})
new MarkdownTreeParser((options = {}));
```

#### Methods
Expand Down Expand Up @@ -265,18 +277,18 @@ The library supports powerful CSS-like selectors for searching:

```javascript
// Element selectors
parser.selectAll(tree, 'heading') // All headings
parser.selectAll(tree, 'paragraph') // All paragraphs
parser.selectAll(tree, 'link') // All links
parser.selectAll(tree, 'heading'); // All headings
parser.selectAll(tree, 'paragraph'); // All paragraphs
parser.selectAll(tree, 'link'); // All links

// Attribute selectors
parser.selectAll(tree, 'heading[depth=1]') // H1 headings
parser.selectAll(tree, 'heading[depth=2]') // H2 headings
parser.selectAll(tree, 'link[url*="github"]') // Links containing "github"
parser.selectAll(tree, 'heading[depth=1]'); // H1 headings
parser.selectAll(tree, 'heading[depth=2]'); // H2 headings
parser.selectAll(tree, 'link[url*="github"]'); // Links containing "github"

// Pseudo selectors
parser.selectAll(tree, ':first-child') // First child elements
parser.selectAll(tree, ':last-child') // Last child elements
parser.selectAll(tree, ':first-child'); // First child elements
parser.selectAll(tree, ':last-child'); // Last child elements
```

## 🧪 Testing
Expand Down
46 changes: 46 additions & 0 deletions bin/md-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const MESSAGES = {
USAGE_SEARCH: '❌ Usage: md-tree search <file> <selector>',
USAGE_STATS: '❌ Usage: md-tree stats <file>',
USAGE_TOC: '❌ Usage: md-tree toc <file>',
USAGE_CHECK_LINKS: '❌ Usage: md-tree check-links <file>',
INDEX_NOT_FOUND: 'index.md not found in',
NO_MAIN_TITLE: 'No main title found in index.md',
NO_SECTION_FILES: 'No section files found in TOC',
Expand Down Expand Up @@ -138,6 +139,7 @@ Commands:
search <file> <selector> Search using CSS-like selectors
stats <file> Show document statistics
toc <file> Generate table of contents
check-links <file> Verify that links are reachable
version Show version information
help Show this help message

Expand All @@ -146,6 +148,7 @@ Options:
--level, -l <number> Heading level to work with
--format, -f <json|text> Output format (default: text)
--max-level <number> Maximum heading level for TOC (default: 3)
--recursive, -r Recursively check linked markdown files

Examples:
md-tree list README.md
Expand All @@ -157,6 +160,7 @@ Examples:
md-tree search README.md "heading[depth=2]"
md-tree stats README.md
md-tree toc README.md --max-level 2
md-tree check-links README.md --recursive

For more information, visit: https://github.com/ksylvan/markdown-tree-parser
`);
Expand Down Expand Up @@ -355,6 +359,34 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
console.log(`🖼️ Images: ${stats.images}`);
}

async checkLinks(filePath, recursive = false) {
const content = await this.readFile(filePath);
const tree = await this.parser.parse(content);
const results = await this.parser.checkLinks(tree, {
baseDir: path.dirname(path.resolve(filePath)),
recursive,
});

console.log(`\n🔗 Checking links in ${path.basename(filePath)}:\n`);

const bad = [];
results.forEach((r) => {
if (r.ok) {
console.log(`✅ ${r.url}`);
} else {
console.log(`❌ ${r.url} - ${r.error || r.status}`);
bad.push(r);
}
});

if (bad.length > 0) {
console.log(`\n${MESSAGES.WARNING} ${bad.length} broken link(s) found.`);
process.exitCode = 1;
} else {
console.log('\nAll links look good!');
}
}

async generateTOC(filePath, maxLevel = 3) {
const content = await this.readFile(filePath);
const tree = await this.parser.parse(content);
Expand Down Expand Up @@ -384,6 +416,7 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
level: 2,
format: 'text',
maxLevel: 3,
recursive: false,
};

// Parse flags
Expand All @@ -402,6 +435,8 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
} else if (arg === '--max-level') {
options.maxLevel = parseInt(args[i + 1]) || 3;
i++; // skip next arg
} else if (arg === '--recursive' || arg === '-r') {
options.recursive = true;
} else if (!arg.startsWith('-')) {
filteredArgs.push(arg);
}
Expand Down Expand Up @@ -493,6 +528,14 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
await this.generateTOC(args[1], options.maxLevel);
}

async handleCheckLinksCommand(args, options) {
if (args.length < 2) {
console.error(MESSAGES.USAGE_CHECK_LINKS);
process.exit(1);
}
await this.checkLinks(args[1], options.recursive);
}

async run() {
const { command, args, options } = this.parseArgs();

Expand Down Expand Up @@ -531,6 +574,9 @@ For more information, visit: https://github.com/ksylvan/markdown-tree-parser
case 'toc':
await this.handleTocCommand(args, options);
break;
case 'check-links':
await this.handleCheckLinksCommand(args, options);
break;
default:
console.error(`${MESSAGES.ERROR} Unknown command: ${command}`);
console.log('Run "md-tree help" for usage information.');
Expand Down
14 changes: 13 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,16 @@ export async function generateTOC(markdown, maxLevel = 3, options = {}) {
const parser = new MarkdownTreeParser(options);
const tree = await parser.parse(markdown);
return parser.generateTableOfContents(tree, maxLevel);
}
}

/**
* Quick utility to check links in markdown
* @param {string} markdown - Markdown content
* @param {Object} options - Parser options
* @returns {Promise<Array>} Array of link check results
*/
export async function checkLinks(markdown, options = {}) {
const parser = new MarkdownTreeParser(options);
const tree = await parser.parse(markdown);
return parser.checkLinks(tree, options);
}
59 changes: 59 additions & 0 deletions lib/markdown-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,63 @@

return toc;
}

/**
* Check that all links in a document are reachable
* @param {Object} tree - Parsed markdown AST
* @param {Object} options - Options object
* @param {string} [options.baseDir='.'] - Base directory for resolving local links
* @param {boolean} [options.recursive=false] - Recursively check linked markdown files
* @param {Set<string>} [options._visited] - Internal set of visited files to prevent loops
* @returns {Promise<Array>} Array of result objects { url, ok, status?, error? }
*/
async checkLinks(
tree,
{ baseDir = '.', recursive = false, _visited = new Set() } = {}
) {
const links = [];
visit(tree, 'link', (node) => {
if (node.url) links.push(node.url);
});

const results = [];

for (const url of links) {
if (url.startsWith('http://') || url.startsWith('https://')) {
try {
const res = await fetch(url, { method: 'HEAD' });

Check failure on line 426 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (22.x)

'fetch' is not defined

Check failure on line 426 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (18.x)

'fetch' is not defined

Check failure on line 426 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (20.x)

'fetch' is not defined
results.push({ url, ok: res.ok, status: res.status });
} catch (err) {
results.push({ url, ok: false, error: err.message });
}
} else if (url.startsWith('#') || url.startsWith('mailto:')) {
// Assume local anchors and mailto links are valid
results.push({ url, ok: true });
} else {
const target = path.resolve(baseDir, url.split('#')[0]);

Check failure on line 435 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (22.x)

'path' is not defined

Check failure on line 435 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (18.x)

'path' is not defined

Check failure on line 435 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (20.x)

'path' is not defined
try {
await fs.access(target);

Check failure on line 437 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (22.x)

'fs' is not defined

Check failure on line 437 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (18.x)

'fs' is not defined

Check failure on line 437 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (20.x)

'fs' is not defined
results.push({ url, ok: true });

if (recursive && /\.md$/i.test(target)) {
if (!_visited.has(target)) {
_visited.add(target);
const content = await fs.readFile(target, 'utf-8');

Check failure on line 443 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (22.x)

'fs' is not defined

Check failure on line 443 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (18.x)

'fs' is not defined

Check failure on line 443 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (20.x)

'fs' is not defined
const subTree = await this.parse(content);
const subResults = await this.checkLinks(subTree, {
baseDir: path.dirname(target),

Check failure on line 446 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (22.x)

'path' is not defined

Check failure on line 446 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (18.x)

'path' is not defined

Check failure on line 446 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (20.x)

'path' is not defined
recursive,
_visited,
});
results.push(...subResults);
}
}
} catch (err) {

Check failure on line 453 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (22.x)

'err' is defined but never used

Check failure on line 453 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (18.x)

'err' is defined but never used

Check failure on line 453 in lib/markdown-parser.js

View workflow job for this annotation

GitHub Actions / test (20.x)

'err' is defined but never used
results.push({ url, ok: false, error: 'File not found' });
}
}
}

return results;
}
}
Loading