summaryrefslogtreecommitdiffstats
path: root/src/utils
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2021-12-21 15:05:04 +0100
committerArmand Philippot <git@armandphilippot.com>2021-12-21 15:05:04 +0100
commitae17973618c5ad5500ae69738da222187a09b019 (patch)
tree20edaca351c3d9b0df1b986e12255b9be7b9052d /src/utils
parenta367f605b842ad0a71a63025da15ac62ed0364a5 (diff)
chore: add a hook to build headings tree
Diffstat (limited to 'src/utils')
-rw-r--r--src/utils/helpers/slugify.ts18
-rw-r--r--src/utils/hooks/useHeadingsTree.tsx100
2 files changed, 118 insertions, 0 deletions
diff --git a/src/utils/helpers/slugify.ts b/src/utils/helpers/slugify.ts
new file mode 100644
index 0000000..37619bc
--- /dev/null
+++ b/src/utils/helpers/slugify.ts
@@ -0,0 +1,18 @@
+/**
+ * Convert a text into a slug or id.
+ * https://gist.github.com/codeguy/6684588#gistcomment-3332719
+ *
+ * @param {string} text Text to slugify.
+ */
+export const slugify = (text: string) => {
+ return text
+ .toString()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, '-')
+ .replace(/[^\w\-]+/g, '-')
+ .replace(/\-\-+/g, '-')
+ .replace(/^-|-$/g, '');
+};
diff --git a/src/utils/hooks/useHeadingsTree.tsx b/src/utils/hooks/useHeadingsTree.tsx
new file mode 100644
index 0000000..8bb70c8
--- /dev/null
+++ b/src/utils/hooks/useHeadingsTree.tsx
@@ -0,0 +1,100 @@
+import { slugify } from '@utils/helpers/slugify';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+type Heading = {
+ depth: number;
+ id: string;
+ children: Heading[];
+ title: string;
+};
+
+const useHeadingsTree = (wrapper: string) => {
+ const [headingsTree, setHeadingsTree] = useState<Heading[]>([]);
+ const depths = useMemo(() => ['h2', 'h3', 'h4', 'h5', 'h6'], []);
+
+ const getElementDepth = useCallback(
+ (el: HTMLHeadingElement) => {
+ const elDepth = depths.findIndex((depth) => depth === el.localName);
+
+ return elDepth;
+ },
+ [depths]
+ );
+
+ const formatHeadings = useCallback(
+ (headings: NodeListOf<HTMLHeadingElement>): Heading[] => {
+ const formattedHeadings: Heading[] = [];
+
+ Array.from(headings).forEach((heading) => {
+ const title: string = heading.textContent!;
+ const id = slugify(title);
+ const depth = getElementDepth(heading);
+ const children: Heading[] = [];
+
+ heading.id = id;
+
+ formattedHeadings.push({
+ depth,
+ id,
+ children,
+ title,
+ });
+ });
+
+ return formattedHeadings;
+ },
+ [getElementDepth]
+ );
+
+ const buildSubTree = useCallback(
+ (parent: Heading, currentHeading: Heading): void => {
+ if (parent.depth === currentHeading.depth - 1) {
+ parent.children.push(currentHeading);
+ } else {
+ const lastItem = parent.children[parent.children.length - 1];
+ buildSubTree(lastItem, currentHeading);
+ }
+ },
+ []
+ );
+
+ const buildTree = useCallback(
+ (headings: Heading[]): Heading[] => {
+ const tree: Heading[] = [];
+
+ headings.forEach((heading) => {
+ if (heading.depth === 0) {
+ tree.push(heading);
+ } else {
+ const lastItem = tree[tree.length - 1];
+ buildSubTree(lastItem, heading);
+ }
+ });
+
+ return tree;
+ },
+ [buildSubTree]
+ );
+
+ const getHeadingsList = useCallback(
+ (headings: NodeListOf<HTMLHeadingElement>): Heading[] => {
+ const formattedHeadings = formatHeadings(headings);
+ const headingsList = buildTree(formattedHeadings);
+
+ return headingsList;
+ },
+ [formatHeadings, buildTree]
+ );
+
+ useEffect(() => {
+ const query = depths.map((depth) => `${wrapper} ${depth}`).join(', ');
+ const headings: NodeListOf<HTMLHeadingElement> =
+ document.querySelectorAll(query);
+ const headingsList = getHeadingsList(headings);
+ setHeadingsTree(headingsList);
+ }, [depths, wrapper, getHeadingsList]);
+
+ return headingsTree;
+};
+
+export default useHeadingsTree;