All files / scripts/lib/docs extractReadmeDocs.js

89.13% Statements 41/46
88% Branches 44/50
100% Functions 4/4
94.28% Lines 33/35

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 825x 5x 5x 5x 5x 5x 5x   5x                   17x 17x 9x 9x 9x                               30x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x 29x     29x         29x   1x         28x                     2x     5x  
const parseReadme = require('../parseReadme');
const { toRawGithub, normalizeIfRelative } = require('./urlResolver');
const { extractArchitectureOverview } = require('./extractArchitecture');
const { extractApiDocumentation } = require('./extractApiDocs');
const { extractTestingLinks } = require('./extractTesting');
const { extractProductionUrl } = require('./extractProductionUrl');
const { translateDocFields } = require('./translateDocs');
 
const DEBUG_FETCH = process.env.DEBUG_FETCH === '1' || process.env.DEBUG_FETCH === 'true';
 
/**
 * Strips accidental AST JSON fragments that leak into description strings
 * when the README parser emits raw node objects instead of plain text.
 *
 * @param {string} s - Raw description string
 * @returns {string|null} Cleaned string, or null if nothing remains
 */
function stripAstJsonFragments(s) {
  try {
    if (!s || typeof s !== 'string') return s;
    let t = s.replace(/\{\s*"type"\s*:\s*"[a-z]+"[\s\S]*?\}/gi, '');
    t = t.replace(/\s+/g, ' ').trim();
    return t === '' ? null : t;
  } catch (e) { return s; }
}
 
/**
 * Parses a README and extracts structured doc fields: architecture overview,
 * API documentation, testing links, and production URL. Translates field values
 * to German when translateWithCache is provided. When no doc links are found,
 * returns a placeholder object so the UI always has something to render.
 *
 * @param {string} readmeText - Raw README markdown
 * @param {string} repoName - Repository name (used for URL resolution)
 * @param {Function} [translateWithCache] - Optional translation function(repo, text) → Promise
 * @returns {Promise<object|null>} Structured repoDocs object, or null for empty READMEs
 */
async function extractRepoDocsDetailed(readmeText, repoName, translateWithCache) {
  if (!readmeText || !readmeText.length) return null;
  const out = { architectureOverview: null, apiDocumentation: null, testing: null, productionUrl: null };
  try {
    const ast = parseReadme.parseMarkdown(readmeText);
    const ctx = { toRawGithub: (href) => toRawGithub(href, repoName), parseReadme, strip: stripAstJsonFragments };
    out.architectureOverview = extractArchitectureOverview(ast, readmeText, ctx);
    out.apiDocumentation = extractApiDocumentation(ast, readmeText, ctx);
    out.testing = extractTestingLinks(ast, readmeText, ctx);
    out.productionUrl = extractProductionUrl(ast, readmeText, ctx);
    if (out.architectureOverview && out.architectureOverview.link) out.architectureOverview.link = normalizeIfRelative(out.architectureOverview.link, repoName);
    if (out.apiDocumentation && out.apiDocumentation.link) out.apiDocumentation.link = normalizeIfRelative(out.apiDocumentation.link, repoName);
    Iif (out.testing && out.testing.testingDocs && out.testing.testingDocs.link) out.testing.testingDocs.link = normalizeIfRelative(out.testing.testingDocs.link, repoName);
    if (out.productionUrl && out.productionUrl.link) out.productionUrl.link = normalizeIfRelative(out.productionUrl.link, repoName);
    if (translateWithCache) await translateDocFields(out, repoName, translateWithCache);
  } catch (e) { if (DEBUG_FETCH) console.log('extractRepoDocsDetailed error', e && e.message); }
 
  const foundAny = (out.architectureOverview && out.architectureOverview.link)
    || (out.apiDocumentation && out.apiDocumentation.link)
    || (out.testing && out.testing.coverage && out.testing.coverage.length > 0)
    || (out.productionUrl && out.productionUrl.link);
 
  if (!foundAny) {
    // Return a placeholder so the RepoDocs UI always has a fallback to render
    return {
      architectureOverview: null, apiDocumentation: null, testing: null,
      placeholder: { title: 'Under Construction', title_de: 'Noch in Entwicklung', description: 'Documentation will be developed soon', description_de: 'Dokumentation wird bald entwickelt' }
    };
  }
  return out;
}
 
/**
 * Returns true for strings short enough to send to DeepL without exceeding
 * the free-tier per-request limit (300 characters).
 *
 * @param {string} s
 * @returns {boolean}
 */
function shouldTranslateUI(s) {
  try { return s && typeof s === 'string' && s.trim().length > 0 && s.trim().length <= 300; } catch (e) { return false; }
}
 
module.exports = { extractRepoDocsDetailed, shouldTranslateUI, stripAstJsonFragments };