mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-22 14:58:16 +00:00
[Glitch] Adds new HTMLBlock component
Port e07b9dfdc1 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import htmlConfig from '../../../config/html-tags.json';
|
||||
|
||||
// NB: This function can still return unsafe HTML
|
||||
export const unescapeHTML = (html: string) => {
|
||||
const wrapper = document.createElement('div');
|
||||
@@ -10,64 +12,49 @@ export const unescapeHTML = (html: string) => {
|
||||
return wrapper.textContent;
|
||||
};
|
||||
|
||||
interface AllowedTag {
|
||||
/* True means allow, false disallows global attributes, string renames the attribute name for React. */
|
||||
attributes?: Record<string, boolean | string>;
|
||||
/* If false, the tag cannot have children. Undefined or true means allowed. */
|
||||
children?: boolean;
|
||||
}
|
||||
|
||||
type AllowedTagsType = {
|
||||
[Tag in keyof React.ReactHTML]?: AllowedTag;
|
||||
};
|
||||
|
||||
const globalAttributes: Record<string, boolean | string> = htmlConfig.global;
|
||||
const defaultAllowedTags: AllowedTagsType = htmlConfig.tags;
|
||||
|
||||
interface QueueItem {
|
||||
node: Node;
|
||||
parent: React.ReactNode[];
|
||||
depth: number;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
export interface HTMLToStringOptions<Arg extends Record<string, unknown>> {
|
||||
maxDepth?: number;
|
||||
onText?: (text: string) => React.ReactNode;
|
||||
onText?: (text: string, extra: Arg) => React.ReactNode;
|
||||
onElement?: (
|
||||
element: HTMLElement,
|
||||
children: React.ReactNode[],
|
||||
extra: Arg,
|
||||
) => React.ReactNode;
|
||||
onAttribute?: (
|
||||
name: string,
|
||||
value: string,
|
||||
tagName: string,
|
||||
extra: Arg,
|
||||
) => [string, unknown] | null;
|
||||
allowedTags?: Set<string>;
|
||||
allowedTags?: AllowedTagsType;
|
||||
extraArgs?: Arg;
|
||||
}
|
||||
const DEFAULT_ALLOWED_TAGS: ReadonlySet<string> = new Set([
|
||||
'a',
|
||||
'abbr',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'cite',
|
||||
'code',
|
||||
'del',
|
||||
'dfn',
|
||||
'dl',
|
||||
'dt',
|
||||
'em',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'i',
|
||||
'li',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'small',
|
||||
'span',
|
||||
'strong',
|
||||
'sub',
|
||||
'sup',
|
||||
'time',
|
||||
'u',
|
||||
'ul',
|
||||
]);
|
||||
|
||||
export function htmlStringToComponents(
|
||||
let uniqueIdCounter = 0;
|
||||
|
||||
export function htmlStringToComponents<Arg extends Record<string, unknown>>(
|
||||
htmlString: string,
|
||||
options: Options = {},
|
||||
options: HTMLToStringOptions<Arg> = {},
|
||||
) {
|
||||
const wrapper = document.createElement('template');
|
||||
wrapper.innerHTML = htmlString;
|
||||
@@ -79,10 +66,11 @@ export function htmlStringToComponents(
|
||||
|
||||
const {
|
||||
maxDepth = 10,
|
||||
allowedTags = DEFAULT_ALLOWED_TAGS,
|
||||
allowedTags = defaultAllowedTags,
|
||||
onAttribute,
|
||||
onElement,
|
||||
onText,
|
||||
extraArgs = {} as Arg,
|
||||
} = options;
|
||||
|
||||
while (queue.length > 0) {
|
||||
@@ -109,9 +97,9 @@ export function htmlStringToComponents(
|
||||
// Text can be added directly if it has any non-whitespace content.
|
||||
case Node.TEXT_NODE: {
|
||||
const text = node.textContent;
|
||||
if (text && text.trim() !== '') {
|
||||
if (text) {
|
||||
if (onText) {
|
||||
parent.push(onText(text));
|
||||
parent.push(onText(text, extraArgs));
|
||||
} else {
|
||||
parent.push(text);
|
||||
}
|
||||
@@ -127,7 +115,9 @@ export function htmlStringToComponents(
|
||||
}
|
||||
|
||||
// If the tag is not allowed, skip it and its children.
|
||||
if (!allowedTags.has(node.tagName.toLowerCase())) {
|
||||
const tagName = node.tagName.toLowerCase();
|
||||
const tagInfo = allowedTags[tagName as keyof typeof allowedTags];
|
||||
if (!tagInfo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -137,7 +127,8 @@ export function htmlStringToComponents(
|
||||
|
||||
// If onElement is provided, use it to create the element.
|
||||
if (onElement) {
|
||||
const component = onElement(node, children);
|
||||
const component = onElement(node, children, extraArgs);
|
||||
|
||||
// Check for undefined to allow returning null.
|
||||
if (component !== undefined) {
|
||||
element = component;
|
||||
@@ -147,25 +138,56 @@ export function htmlStringToComponents(
|
||||
// If the element wasn't created, use the default conversion.
|
||||
if (element === undefined) {
|
||||
const props: Record<string, unknown> = {};
|
||||
props.key = `html-${uniqueIdCounter++}`; // Get the current key and then increment it.
|
||||
for (const attr of node.attributes) {
|
||||
let name = attr.name.toLowerCase();
|
||||
|
||||
// Custom attribute handler.
|
||||
if (onAttribute) {
|
||||
const result = onAttribute(
|
||||
attr.name,
|
||||
name,
|
||||
attr.value,
|
||||
node.tagName.toLowerCase(),
|
||||
extraArgs,
|
||||
);
|
||||
if (result) {
|
||||
const [name, value] = result;
|
||||
props[name] = value;
|
||||
const [cbName, value] = result;
|
||||
props[cbName] = value;
|
||||
}
|
||||
} else {
|
||||
props[attr.name] = attr.value;
|
||||
// Check global attributes first, then tag-specific ones.
|
||||
const globalAttr = globalAttributes[name];
|
||||
const tagAttr = tagInfo.attributes?.[name];
|
||||
|
||||
// Exit if neither global nor tag-specific attribute is allowed.
|
||||
if (!globalAttr && !tagAttr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rename if needed.
|
||||
if (typeof tagAttr === 'string') {
|
||||
name = tagAttr;
|
||||
} else if (typeof globalAttr === 'string') {
|
||||
name = globalAttr;
|
||||
}
|
||||
|
||||
let value: string | boolean | number = attr.value;
|
||||
|
||||
// Handle boolean attributes.
|
||||
if (value === 'true') {
|
||||
value = true;
|
||||
} else if (value === 'false') {
|
||||
value = false;
|
||||
}
|
||||
|
||||
props[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
element = React.createElement(
|
||||
node.tagName.toLowerCase(),
|
||||
tagName,
|
||||
props,
|
||||
children,
|
||||
tagInfo.children !== false ? children : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user