All files / packages/design-system-2-site/src/lib/zeroheight pages.js

100% Statements 48/48
91.42% Branches 32/35
100% Functions 9/9
100% Lines 39/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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                            6x               6x 6x                 283x                     2x           6x 11x 7x 3x 3x   4x       2x 2x                 31x 31x   29x 29x 58x 58x   29x                         4x 4x   4x 4x 4x 35x 35x 35x 5x   35x     4x 4x 5x 5x 5x 5x 5x   4x               3x    
/**
 * Shape Zeroheight MCP and REST payloads into one page list.
 */
 
/**
 * @typedef {Object} ZhListedPage
 * @property {number} id
 * @property {string} name
 * @property {string} uid
 * @property {string} url
 * @property {number | null} releaseId
 * @property {string[]} categoryPath  e.g. ['Patterns', 'Search']
 */
 
const ZH_URL_RE = /\/v\/(\d+)\/p\/([0-9a-f]{6})(?:-|\/|$)/;
 
/**
 * Pull the release id and page uid out of a Zeroheight page URL, e.g.
 * `.../2e1ef8501/v/64878/p/78871c-badge` -> `{ releaseId: 64878, uid: '78871c' }`.
 * @param {string} url
 */
export function parseZhUrl(url) {
  const m = url.match(ZH_URL_RE);
  return m ? { releaseId: Number(m[1]), uid: m[2] } : { releaseId: null, uid: null };
}
 
/**
 * Normalize a page or tab name for comparisons: collapse whitespace (`\s`
 * includes nbsp), trim, lowercase.
 * @param {string} name
 */
export function normName(name) {
  return name.replace(/\s+/g, ' ').trim().toLowerCase();
}
 
/**
 * Flatten the MCP `list-pages` tree. Category nodes hold a `categoryId` plus
 * child pages/categories keyed by name; page nodes hold `id` and `url`.
 * @param {{ pages: Record<string, any> }} payload
 * @returns {ZhListedPage[]}
 */
export function flattenListPages(payload) {
  /** @type {ZhListedPage[]} */
  const out = [];
  /**
   * @param {Record<string, any>} node
   * @param {string[]} path
   */
  function walk(node, path) {
    for (const [key, value] of Object.entries(node)) {
      if (key === 'categoryId' || !value || typeof value !== 'object') continue;
      if ('id' in value && 'url' in value) {
        const { releaseId, uid } = parseZhUrl(value.url);
        Eif (uid) out.push({ id: value.id, name: key, uid, url: value.url, releaseId, categoryPath: path });
      } else {
        walk(value, [...path, key]);
      }
    }
  }
  walk(payload.pages, []);
  return out;
}
 
/**
 * Split MCP `get-page` markdown into its frontmatter fields and body.
 * @param {string} md
 * @returns {{ meta: Record<string, string>, body: string }}
 */
export function splitMcpMarkdown(md) {
  const m = md.match(/^---\n([\s\S]*?)\n---\n?/);
  if (!m) return { meta: {}, body: md };
  /** @type {Record<string, string>} */
  const meta = {};
  for (const line of m[1].split('\n')) {
    const i = line.indexOf(':');
    Eif (i > 0) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim();
  }
  return { meta, body: md.slice(m[0].length) };
}
 
/**
 * Split an MCP page body into tab sections. Each tab starts with `# <Tab>`
 * at the top of the body or right after a `---` separator. Only names in
 * `tabNames` count as boundaries, so content H1s stay inside their tab.
 * The MCP omits hidden tabs, so pass visible tab names only.
 * @param {string} body
 * @param {string[]} tabNames
 * @returns {Map<string, string>} keyed by `normName(tab)`
 */
export function splitTabs(body, tabNames) {
  const wanted = new Set(tabNames.map(normName));
  const lines = body.split('\n');
  /** @type {{ index: number, key: string }[]} */
  const starts = [];
  let prev = null;
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const h1 = line.match(/^# (.+)$/);
    if (h1 && wanted.has(normName(h1[1])) && (prev === null || prev === '---')) {
      starts.push({ index: i, key: normName(h1[1]) });
    }
    if (line.trim()) prev = line.trim();
  }
 
  const sections = new Map();
  starts.forEach(({ index, key }, n) => {
    const end = n + 1 < starts.length ? starts[n + 1].index : lines.length;
    const chunk = lines.slice(index + 1, end);
    while (chunk.length && !chunk[chunk.length - 1].trim()) chunk.pop();
    if (chunk.length && chunk[chunk.length - 1].trim() === '---') chunk.pop();
    sections.set(key, chunk.join('\n').trim());
  });
  return sections;
}
 
/**
 * API tabs in display order.
 * @param {{ tabs?: import('./api-client.js').ApiTab[] }} page
 */
export function sortedTabs(page) {
  return [...(page.tabs ?? [])].sort((a, b) => a.order - b.order);
}