From a8169164b4324d9242fd45b1141778626950a8f2 Mon Sep 17 00:00:00 2001 From: Guy Sartorelli Date: Thu, 5 May 2022 17:39:43 +1200 Subject: [PATCH] FIX Don't break headings when they include sub-elements. tags added to headings via backticks are the main culprit, but ultimately if any elements other than text were being added to the heading, only part of the heading would display, and any custom ID for the heading anchor link would not be used. --- src/utils/parseHTML.ts | 2 +- src/utils/rewriteHeader.ts | 64 ++++++++++++++++++++++++-------------- src/utils/rewriteLink.ts | 9 +++++- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/utils/parseHTML.ts b/src/utils/parseHTML.ts index 0d6c48e..df93fb4 100644 --- a/src/utils/parseHTML.ts +++ b/src/utils/parseHTML.ts @@ -29,7 +29,7 @@ const parseHTML = (html: string): ReactElement | ReactElement[] | string => { const domChildren = children || []; if (name && attribs) { if (name === 'a') { - return rewriteLink(attribs, domChildren, parseOptions); + return rewriteLink(attribs, domChildren, parseOptions, domNode); } if (name === 'table') { return rewriteTable(domChildren, parseOptions); diff --git a/src/utils/rewriteHeader.ts b/src/utils/rewriteHeader.ts index a96653d..f3e67f0 100644 --- a/src/utils/rewriteHeader.ts +++ b/src/utils/rewriteHeader.ts @@ -1,6 +1,9 @@ import { createElement, ReactElement } from "react"; -import { DomElement } from 'html-react-parser'; +import { DomElement, domToReact, htmlToDOM } from 'html-react-parser'; +/** + * Generate the ID for a heading + */ const generateID = (title: string): string => { return title .replace('&', '-and-') @@ -11,45 +14,58 @@ const generateID = (title: string): string => { .replace(/-$/g, '') .toLowerCase(); } + +/** + * Get the full plain text of the heading for checking and generating the ID + */ +const getFullHeading = (element: DomElement): string => { + let text = ''; + if (element.type === 'text') { + text += element.data; + } + + if (element.children) { + for (const child of element.children) { + text += getFullHeading(child); + } + } + + return text; +} + /** * If a header has a {#explicit-id}, add it as an attribute - * @param domNode */ const rewriteHeaders = (domNode: DomElement): ReactElement | false => { if (!domNode.name) { return false; } - const firstChild = domNode.children ? domNode.children[0] : null; - if (firstChild && firstChild.type === 'text') { - const { data } = firstChild; - const matches = data.match(/^(.*?){#([A-Za-z0-9_-]+)\}$/); + + const plainText = getFullHeading(domNode); + + if (plainText) { + // const plainText = getFullHeading(firstChild); + const matches = plainText.match(/^(.*?)\{#([A-Za-z0-9_-]+)\}$/); let header; let id; if (matches) { header = matches[1]; id = matches[2]; } else { - header = data; - id = generateID(data); + header = plainText; + id = generateID(plainText); } - const anchor = createElement( - 'a', - { - "aria-hidden": true, - className: 'anchor', - href: `#${id}`, - id, - key: id, - }, - '#' - ); + const anchor = htmlToDOM(``)[0]; - return createElement( - domNode.name, - {}, - [ header, anchor ] - ); + const lastChild = domNode.children ? domNode.children[domNode.children.length - 1] : null; + if (lastChild && lastChild.type === 'text') { + lastChild.data = lastChild.data.replace(/\s*{#([A-Za-z0-9_-]+)\}$/, ''); + } + + domNode.children?.push(anchor); + + return domToReact([domNode]); } diff --git a/src/utils/rewriteLink.ts b/src/utils/rewriteLink.ts index 4203aaf..1acb7af 100644 --- a/src/utils/rewriteLink.ts +++ b/src/utils/rewriteLink.ts @@ -8,6 +8,7 @@ import { SilverstripeDocument } from '../types'; interface LinkAttributes { href?: string; + class?: string; }; @@ -31,7 +32,8 @@ const relativeLink = (currentNode: SilverstripeDocument, href: string): string = const rewriteLink = ( attribs: LinkAttributes, children: DomElement[], - parseOptions: HTMLReactParserOptions + parseOptions: HTMLReactParserOptions, + domNode: DomElement ): ReactElement|false => { const { href } = attribs; if (!href) { @@ -43,6 +45,11 @@ const rewriteLink = ( // hash links if (href.startsWith('#')) { + // Just let normal parsing occur for heading links + if (attribs.class === 'anchor') { + return domToReact(domNode); + } + // rewrite all other hashlinks return createElement( Link, {