All files / scripts/lib/parseReadme docs.js

94.18% Statements 81/86
75.86% Branches 88/116
85.71% Functions 12/14
98.36% Lines 60/61

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 10010x     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 };