aboutsummaryrefslogtreecommitdiffstats
path: root/jest.setup.js
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-15 22:14:03 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-15 22:25:12 +0100
commit9eae4703c97c50e82d959a3e0859fe1553889b15 (patch)
tree46605bbd1911ef370cc460d6710ad0ff87782e73 /jest.setup.js
parent4dc0005999c72b78d195bc05193926328030fe78 (diff)
feat: add HTTP security headers
I also renamed and changed the format of some environment variables so I can reuse them inside the CSP security header.
Diffstat (limited to 'jest.setup.js')
0 files changed, 0 insertions, 0 deletions
a> 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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
import { useState, useCallback, type RefCallback } from 'react';
import type { HeadingLevel } from '../../../components';

export type HeadingsTreeNode = {
  /**
   * The heading children.
   */
  children: HeadingsTreeNode[];
  /**
   * The heading depth.
   */
  depth: number;
  /**
   * The heading id.
   */
  id: string;
  /**
   * The heading label.
   */
  label: string;
};

const headingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as const;

type HeadingTagNames = (typeof headingTags)[number];

export type UseHeadingsTreeOptions = {
  /**
   * Look for headings starting from this level (1 = `h1`, ...).
   *
   * @default undefined
   */
  fromLevel?: HeadingLevel;
  /**
   * Look for headings ending with this level (1 = `h1`, ...).
   *
   * @default undefined
   */
  toLevel?: HeadingLevel;
};

/**
 * Retrieve a list of heading tags.
 *
 * @param {UseHeadingsTreeOptions} options - An options object.
 * @returns {HeadingTagNames[]} The heading tags list.
 */
const getHeadingTagsList = (
  options?: UseHeadingsTreeOptions
): HeadingTagNames[] => {
  const tagsList = headingTags.slice(0);

  if (options?.toLevel) tagsList.length = options.toLevel;
  if (options?.fromLevel) tagsList.splice(0, options.fromLevel - 1);

  return tagsList;
};

type HeadingsTreeNodeWithParentIndex = HeadingsTreeNode & {
  parentIndex: number;
};

/**
 * Convert a node list of heading elements to an array of indexed nodes.
 *
 * @param {NodeListOf<HTMLHeadingElement>} nodes - The heading elements list.
 * @returns {HeadingsTreeNodeWithParentIndex[]} The headings nodes.
 */
const getHeadingNodesFrom = (
  nodes: NodeListOf<HTMLHeadingElement>
): HeadingsTreeNodeWithParentIndex[] => {
  const depthLastIndexes = Array.from({ length: headingTags.length }, () => -1);

  return Array.from(nodes).map(
    (node, index): HeadingsTreeNodeWithParentIndex => {
      const depth = headingTags.findIndex((tag) => tag === node.localName);
      const parentDepthIndexes = depthLastIndexes.slice(0, depth);

      depthLastIndexes[depth] = index;

      return {
        children: [],
        depth,
        id: node.id,
        label: node.textContent ?? '',
        parentIndex: Math.max(...parentDepthIndexes),
      };
    }
  );
};

/**
 * Build an headings tree from a list of heading elements.
 *
 * @param {NodeListOf<HTMLHeadingElement>} nodes - The heading nodes.
 * @returns {HeadingsTreeNode[]} The headings tree.
 */
const buildHeadingsTreeFrom = (
  nodes: NodeListOf<HTMLHeadingElement>
): HeadingsTreeNode[] => {
  const headings = getHeadingNodesFrom(nodes);
  const treeNodes: HeadingsTreeNode[] = [];

  for (const heading of headings) {
    const { parentIndex, ...node } = heading;

    if (parentIndex >= 0) headings[parentIndex].children.push(node);
    else treeNodes.push(node);
  }

  return treeNodes;
};

export type UseHeadingsTreeReturn<T extends HTMLElement> = {
  /**
   * A callback function to set a ref.
   */
  ref: RefCallback<T>;
  /**
   * The headings tree.
   */
  tree: HeadingsTreeNode[];
};

/**
 * React hook to retrieve the headings tree in a document or in a given wrapper.
 *
 * @param {UseHeadingsTreeOptions} options - The headings tree config.
 * @returns {UseHeadingsTreeReturn<T>} The headings tree and a ref callback.
 */
export const useHeadingsTree = <T extends HTMLElement = HTMLElement>(
  options?: UseHeadingsTreeOptions
): UseHeadingsTreeReturn<T> => {
  if (
    options?.fromLevel &&
    options.toLevel &&
    options.fromLevel > options.toLevel
  )
    throw new Error(
      'Invalid options: `fromLevel` must be lower or equal to `toLevel`.'
    );

  const [tree, setTree] = useState<HeadingsTreeNode[]>([]);
  const requestedHeadingTags = getHeadingTagsList(options);
  const query = requestedHeadingTags.join(', ');
  const ref: RefCallback<T> = useCallback(
    (el) => {
      const headingNodes = el?.querySelectorAll<HTMLHeadingElement>(query);

      if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes));
    },
    [query]
  );

  return { ref, tree };
};