FIX Don't break headings when they include sub-elements.

<code> 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.
This commit is contained in:
Guy Sartorelli 2022-05-05 17:39:43 +12:00
parent 6fe74b1fd8
commit a8169164b4
3 changed files with 49 additions and 26 deletions

View File

@ -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);

View File

@ -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('&amp;', '-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(`<a id="${id}" class="anchor" aria-hidden="true" href="#${id}">#</a>`)[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]);
}

View File

@ -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,
{