# All files / src/html headerProcessor.ts

82.5% Statements 33/40
58.33% Branches 14/24
83.33% Functions 5/6
84.21% Lines 32/38

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
    878x
    8x
    8x
    8x

    8x

    8x
    236x

    236x
    236x

    236x
    236x
    236x
    198x
    198x

    38x

    236x

    2x
    2x

    2x
    2x
    2x

    2x
    2x

    8x
    2x
    4x

    2x

    2x
    2x
    2x

    2x

    2x

     import cheerio from 'cheerio';
    import slugify from '@sindresorhus/slugify';
    import has from 'lodash/has';
    import { getVslotShorthandName } from './vueSlotSyntaxProcessor';
    import type { NodeProcessorConfig } from './NodeProcessor';
    import { MbNode, NodeOrText } from '../utils/node';

    const _ = {
      has,
    };

    /*
     * h1 - h6
     */
    export function setHeadingId(node: MbNode, config: NodeProcessorConfig) {
      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;
      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;
      }
    }