All files / scripts/lib/docs extractApiDocs.js

89.79% Statements 44/49
68.05% Branches 49/72
100% Functions 8/8
94.87% Lines 37/39

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 805x 5x     37x       29x 29x 49x 15x 23x 11x 11x 6x 4x     45x 13x 13x 13x 13x 3x           22x       22x 37x 29x 13x 4x       18x       18x 7x 7x 7x 7x 2x     16x 16x 15x                           29x 29x             5x  
const DEBUG_FETCH = process.env.DEBUG_FETCH === '1' || process.env.DEBUG_FETCH === 'true';
const API_PREFIX_RE = /^[•\-*.\s📌📡🚀]*\s*/i;
 
function isCompleteApiLabel(label) {
  return /^Complete\s+API\b/i.test((label || '').replace(API_PREFIX_RE, '').trim());
}
 
function findCompleteApiInAst(ast, ctx) {
  Iif (!ast || !Array.isArray(ast.children)) return null;
  for (const n of ast.children) {
    if (n.type === 'paragraph') {
      for (const ch of (n.children || [])) {
        if (ch.type !== 'link' || !ch.url) continue;
        const label = (ch.children || []).map(c => c.value || '').join('').trim();
        if (!isCompleteApiLabel(label)) continue;
        const desc = (n.children || []).filter(c => c.type === 'text').map(c => c.value).join(' ').trim();
        return { title: label || 'Complete API', link: ch.url, description: ctx.strip(desc) };
      }
    }
    if (n.type === 'list') {
      for (const li of (n.children || [])) {
        const flat = ctx.parseReadme.flattenNodeText(li || '').replace(/\r?\n/g, ' ');
        for (const m of String(flat).matchAll(/\[([^\]]+)\]\(([^)]+)\)/ig)) {
          if (isCompleteApiLabel((m[1] || '').trim())) {
            return { title: (m[1] || '').trim(), link: (m[2] || '').trim(), description: '' };
          }
        }
      }
    }
  }
  return null;
}
 
function findCompleteApiInText(readmeText) {
  for (const line of readmeText.split(/\r?\n/)) {
    if (/^\s*#{1,6}\s/.test(line)) continue;
    for (const m of line.matchAll(/\[([^\]]+)\]\(([^)]+)\)/ig)) {
      if (isCompleteApiLabel((m[1] || '').trim())) {
        return { title: (m[1] || '').trim(), link: (m[2] || '').trim(), description: '' };
      }
    }
  }
  return null;
}
 
function findAnyApiLink(readmeText, toRawGithub) {
  for (const m of readmeText.matchAll(/\[([^\]]*)\]\((https?:\/\/[^)\s]+|\.?\/?[^)\s]+)\)/ig)) {
    const label = (m[1] || '').trim();
    const url = (m[2] || '').trim();
    Iif (!url) continue;
    if (/api/i.test(label) || /api\.(?:md|html)$/i.test(url) || /src\/(?:main\/)?docs\/.+api/i.test(url)) {
      return { title: label || 'API Documentation', link: toRawGithub(url), description: '' };
    }
  }
  const rawAny = readmeText.match(/https?:\/\/raw\.githubusercontent\.com\/keglev\/[^/]+\/(?:main|master)\/(.+api.+\.(?:md|html))/i);
  if (rawAny) return { title: 'API Documentation', link: toRawGithub(rawAny[0]), description: '' };
  return null;
}
 
/**
 * Finds the first "Complete API" or any API-related link in the README.
 * Search order: AST paragraph nodes → AST list items → raw text scan.
 * "Complete API" is the canonical label used in project READMEs for the full API reference.
 *
 * @param {object} ast - Parsed README AST
 * @param {string} readmeText - Raw README string (fallback when AST scan finds nothing)
 * @param {object} ctx - Shared context: { toRawGithub, parseReadme, strip }
 * @returns {{ title: string, link: string, description: string }|null}
 */
function extractApiDocumentation(ast, readmeText, ctx) {
  try {
    return findCompleteApiInAst(ast, ctx) || findCompleteApiInText(readmeText) || findAnyApiLink(readmeText, ctx.toRawGithub);
  } catch (e) {
    if (DEBUG_FETCH) console.log('extractApiDocumentation error', e && e.message);
    return null;
  }
}
 
module.exports = { extractApiDocumentation };