All files / scripts/lib/parseReadme images.js

95.58% Statements 65/68
89.53% Branches 77/86
100% Functions 7/7
97.95% Lines 48/49

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                21x 18x 18x 8x                       17x 13x 13x 13x 13x 17x 17x 5x 5x 4x 4x 4x 4x 2x 1x 1x   1x   1x 1x 1x 1x             9x   13x 13x   9x 10x   9x 22x 22x 9x 9x   13x 1x 1x   13x   9x   9x 10x   6x 5x   3x 2x   2x     10x  
/**
 * Returns true when a URL looks like a CI badge or shield rather than a real image.
 * Used to skip badge images when selecting the representative project image.
 *
 * @param {string} u - Image URL to test
 * @returns {boolean}
 */
function isBadgeLike(u) {
  if (!u) return false;
  const s = String(u).toLowerCase();
  if (s.includes('badge') || s.includes('shield') || s.includes('status') || s.includes('travis') || s.includes('circleci') || s.includes('shields.io') || s.includes('actions/workflows') || s.includes('github.com/badges')) return true;
  return false;
}
 
/**
 * Selects the best image URL from a parsed README AST.
 * Priority: image under a screenshots/gallery heading → explicit project-image path →
 * any raster image that is not badge-like → any non-SVG image → first image found.
 *
 * @param {object} ast - Parsed README AST
 * @returns {string|null} Image URL, or null if no candidate exists
 */
function findImageCandidateFromAst(ast) {
  if (!ast || !ast.children) return null;
  const candidates = [];
  const findUnderHeading = (titleRe) => {
    Iif (!ast || !Array.isArray(ast.children)) return null;
    for (let i = 0; i < ast.children.length; i++) {
      const n = ast.children[i];
      if (n.type === 'heading') {
        const txt = (n.children || []).map(c => c.value || '').join('').toLowerCase();
        if (titleRe.test(txt)) {
          let j = i + 1;
          while (j < ast.children.length && ast.children[j].type !== 'heading') {
            const nn = ast.children[j];
            if (nn.type === 'image' && nn.url) return nn.url;
            if (nn.type === 'html' && typeof nn.value === 'string') {
              const m = nn.value.match(/<img[^>]+src=["']?([^"' >]+)["']?/i);
              Eif (m && m[1]) return m[1];
            }
            Eif (nn.children && Array.isArray(nn.children)) {
              // flatten node text instead of serializing the AST
              const helpers = require('./helpers');
              const flat = helpers.flattenNodeText(nn || '').replace(/\r?\n/g,' ');
              const r = flat.match(/https?:\/\/[^"']+|\/.+?\.(png|jpe?g|gif|svg)/i);
              Eif (r && r[0]) return r[0];
            }
            j++;
          }
        }
      }
    }
    return null;
  };
  const preferred = findUnderHeading(/screenshots|screenshot|images|gallery/i);
  if (preferred) return preferred;
 
  const isRaster = (u) => /\.(png|jpe?g|gif)$/i.test(u);
  const isLikelyProjectImage = (u) => /project[-_]?image|src\/assets\/imgs|src\/assets|assets\/imgs|assets\/img|project-image/i.test(u);
 
  const walk = (n) => {
    Iif (!n) return;
    if (n.type === 'image' && n.url) {
      candidates.push(n.url);
      return;
    }
    if (n.type === 'html' && typeof n.value === 'string') {
      const m = n.value.match(/<img[^>]+src=["']?([^"' >]+)["']?/i);
      Eif (m && m[1]) candidates.push(m[1]);
    }
    if (n.children && Array.isArray(n.children)) for (const c of n.children) walk(c);
  };
  walk(ast);
 
  for (const u of candidates) {
    if (isLikelyProjectImage(u) && isRaster(u)) return u;
  }
  for (const u of candidates) {
    if (isRaster(u) && !isBadgeLike(u)) return u;
  }
  for (const u of candidates) {
    if (!/\.svg$/i.test(u) && !isBadgeLike(u)) return u;
  }
  return candidates.length ? candidates[0] : null;
}
 
module.exports = { findImageCandidateFromAst, isBadgeLike };