All files / src/html headerProcessor.ts

75% Statements 27/36
57.69% Branches 15/26
83.33% Functions 5/6
76.47% Lines 26/34

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                                          235x   235x 235x   235x 235x       235x   235x 198x 198x   37x   235x                 1x 1x       1x 1x 1x   1x 1x                                   1x 2x     1x   1x 1x 1x       1x         1x      
import cheerio from 'cheerio';
import slugify from '@sindresorhus/slugify';
import _ from 'lodash';
import { getVslotShorthandName } from './vueSlotSyntaxProcessor.js';
import type { NodeProcessorConfig } from './NodeProcessor.js';
import { MbNode, NodeOrText } from '../utils/node.js';
 
/**
 * Increment the counter for same heading text, and set the id of the heading node
 *
 * If the addHeaderCount is false, the counter for the same heading text will not be incremented.
 * The heading id will also not have the count appended to it.
 *
 * So for, only SiteLinkManager uses this function with addHeaderCount set to false.
 * This is for validating intralinks in a side-effect free manner.
 *
 * Heading text refers to textContent of h1-h6 Nodes.
 */
export function setHeadingId(node: MbNode,
                             config: NodeProcessorConfig,
                             addHeaderCount: boolean = true) {
  const textContent = cheerio(node).text();
  // remove the '&lt;' and '&gt;' symbols that markdown-it uses to escape '<' and '>'
  const cleanedContent = textContent.replace(/&lt;|&gt;/g, '');
  const slugifiedHeading = slugify(cleanedContent, { decamelize: false });
 
  let headerId = slugifiedHeading;
  Iif (!addHeaderCount) {
    node.attribs.id = headerId;
    return;
  }
  const { headerIdMap } = config;
 
  if (headerIdMap[slugifiedHeading]) {
    headerId = `${slugifiedHeading}-${headerIdMap[slugifiedHeading]}`;
    headerIdMap[slugifiedHeading] += 1;
  } else {
    headerIdMap[slugifiedHeading] = 2;
  }
  node.attribs.id = headerId;
}
 
/**
 * Traverses the dom breadth-first from the specified element to find a html heading child.
 * @param node Root element to search from
 * @returns  The header element, or undefined if none is found.
 */
function _findHeaderElement(node: MbNode): undefined | NodeOrText {
  const elements = node.children;
  Iif (!elements || !elements.length) {
    return undefined;
  }
 
  const elementQueue = elements.slice(0);
  while (elementQueue.length) {
    const nextEl = elementQueue.shift();
 
    if (nextEl?.name && (/^h[1-6]$/).test(nextEl.name)) {
      return nextEl;
    }
 
    Iif (nextEl?.children) {
      nextEl.children.forEach(child => elementQueue.push(child));
    }
  }
 
  return undefined;
}
 
/**
 * Assigns an id to the root element of a panel component using the heading specified in the
 * panel's header slot or attribute (if any), with the header slot having priority if present.
 * This is to ensure anchors still work when panels are in their minimized form.
 * @param node The root panel element
 */
export function assignPanelId(node: MbNode) {
  const slotChildren = node.children
    ? node.children.filter(child => getVslotShorthandName(child) !== '')
    : [];
 
  const headerSlot = slotChildren.find(child => getVslotShorthandName(child) === 'header');
 
  if (headerSlot) {
    const header = _findHeaderElement(headerSlot as MbNode);
    Iif (!header) {
      return;
    }
 
    Iif (!header.attribs || !_.has(header.attribs, 'id')) {
      throw new Error('Found a panel heading without an assigned id.\n'
          + 'Please report this to the MarkBind developers. Thank you!');
    }
 
    node.attribs.panelId = header.attribs.id;
  }
}