Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | 10x 21x 21x 2x 2x 23x 34x 34x 17x 17x 18x 18x 17x 12x 12x 6x 4x 4x 4x 2x 2x 2x 2x 2x 8x 23x 23x 50x 50x 6x 6x 6x 6x 6x 4x 4x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 17x 26x 26x 26x 23x 23x 23x 8x 7x 23x 1x 10x | const { flattenNodeText, extractLinkFromParagraphNode, extractLinkFromListNode } = require('./helpers');
function toRaw(href, repo) {
Iif (!href) return href;
if (/^https?:\/\//i.test(href)) return href;
const p = String(href).trim().replace(/^\.#?\//, '').replace(/^\//, '');
return repo ? `https://raw.githubusercontent.com/keglev/${repo}/main/${p}` : p;
}
function extractDocSection(ast, headingRegex, defaultTitle, repo) {
for (let i = 0; i < ast.children.length; i++) {
const n = ast.children[i];
if (n.type !== 'heading' || !headingRegex.test((flattenNodeText(n) || '').toLowerCase())) continue;
const depth = n.depth || 2;
for (let k = i + 1; k < ast.children.length; k++) {
const nn = ast.children[k];
if (nn && nn.type === 'heading' && typeof nn.depth === 'number' && nn.depth <= depth) break;
if (nn.type === 'paragraph' && nn.children) {
const found = extractLinkFromParagraphNode(nn);
if (found) return { title: found.title || defaultTitle, link: toRaw(found.link, repo), description: found.description };
}
if (nn.type === 'list' && nn.children) {
for (const li of nn.children) {
const linkChild = (li.children || []).flatMap(ch => (ch.children || [])).find(c => c && c.type === 'link');
if (linkChild) {
const desc = (li.children || []).flatMap(ch => (ch.children || [])).filter(c => c.type === 'text').map(c => c.value).join(' ').trim();
return { title: (linkChild.children && linkChild.children[0] && linkChild.children[0].value) || defaultTitle, link: toRaw(linkChild.url, repo), description: desc };
}
const extracted = extractLinkFromListNode(li);
Eif (extracted && extracted.link) {
return { title: extracted.title || defaultTitle, link: toRaw(extracted.link, repo), description: '' };
}
}
}
}
}
return null;
}
function extractApiSection(ast, repo) {
const headingRegex = /api documentation|api docs|api documentation hub/i;
for (let i = 0; i < ast.children.length; i++) {
const n = ast.children[i];
if (n.type !== 'heading' || !headingRegex.test((flattenNodeText(n) || '').toLowerCase())) continue;
const depth = n.depth || 2;
for (let k = i + 1; k < ast.children.length; k++) {
const nn = ast.children[k];
Iif (nn && nn.type === 'heading' && typeof nn.depth === 'number' && nn.depth <= depth) break;
if (nn.type === 'paragraph' && nn.children) {
const found = extractLinkFromParagraphNode(nn);
Eif (found) return { title: found.title || 'API Documentation', link: toRaw(found.link, repo), description: found.description };
}
Eif (nn.type === 'list' && nn.children) {
for (const li of nn.children) {
const linkChild = (li.children || []).flatMap(ch => (ch.children || [])).find(c => c && c.type === 'link');
if (linkChild) {
const desc = (li.children || []).flatMap(ch => (ch.children || [])).filter(c => c.type === 'text').map(c => c.value).join(' ').trim();
return { title: (linkChild.children && linkChild.children[0] && linkChild.children[0].value) || 'API Documentation', link: toRaw(linkChild.url, repo), description: desc };
}
const extracted = extractLinkFromListNode(li);
Eif (extracted && extracted.link) {
const u = extracted.link;
const flatLi = flattenNodeText(li || '').toLowerCase();
Eif (/api|openapi|swagger|docs?/.test(u.toLowerCase()) || /\bapi\b/.test(flatLi)) {
return { title: extracted.title || 'API Documentation', link: toRaw(u, repo), description: '' };
}
return { title: 'API Documentation', link: toRaw(u, repo), description: '' };
}
}
}
}
}
return null;
}
/**
* Extracts documentation and API documentation links from a README AST.
* Also populates `docs.legacy` with the first available link and title so
* older consumers that read `docsLink`/`docsTitle` directly continue to work.
*
* @param {object} ast - Parsed README AST
* @param {string} repo - Repository name (used to resolve relative links)
* @returns {{ documentation: object|null, apiDocumentation: object|null, legacy: { docsLink: string|null, docsTitle: string|null } }}
*/
function extractDocsFromAst(ast, repo) {
try {
const docs = { documentation: null, apiDocumentation: null };
if (!ast || !Array.isArray(ast.children)) return docs;
docs.documentation = extractDocSection(ast, /\bdocumentation\b/, 'Documentation', repo);
docs.apiDocumentation = extractApiSection(ast, repo);
// legacy fields maintain backward compatibility with consumers that read docsLink/docsTitle directly
if (docs.documentation) docs.legacy = { docsLink: docs.documentation.link, docsTitle: docs.documentation.title };
else if (docs.apiDocumentation) docs.legacy = { docsLink: docs.apiDocumentation.link, docsTitle: docs.apiDocumentation.title };
else docs.legacy = { docsLink: null, docsTitle: null };
return docs;
} catch (e) { return { documentation: null, apiDocumentation: null, legacy: { docsLink: null, docsTitle: null } }; }
}
module.exports = { extractDocsFromAst };
|