stoat-for-desktop/components/markdown/solid-markdown/ast-to-solid.tsx

442 lines
11 KiB
TypeScript

/* eslint-disable */
// @ts-nocheck
/**
* This file is provided under the MIT License
* Copyright (c) 2015 Espen Hovlandsdal
* https://github.com/andi23rosca/solid-markdown/blob/master/license
*/
import { Component, JSX } from "solid-js";
import { Dynamic } from "solid-js/web";
import { stringify as commas } from "comma-separated-tokens";
import type {
Comment,
DocType,
Element,
ElementContent,
Root,
Text,
} from "hast";
import type { Schema } from "property-information";
import { find, hastToReact, svg } from "property-information";
import { stringify as spaces } from "space-separated-tokens";
import style from "style-to-object";
import type { NormalComponents, SolidMarkdownProps } from "./complex-types";
export type Position = {
start: { line: number | null; column: number | null; offset: number | null };
end: { line: number | null; column: number | null; offset: number | null };
};
type Raw = {
type: "raw";
value: string;
};
type Context = {
options: Options;
schema: Schema;
listDepth: number;
};
type TransformLink = (
href: string,
children: ElementContent[],
title?: string,
) => string;
type TransformImage = (src: string, alt: string, title?: string) => string;
type TransformLinkTargetType =
| "_self"
| "_blank"
| "_parent"
| "_top"
| (string & {});
type TransformLinkTarget = (
href: string,
children: ElementContent[],
title?: string,
) => TransformLinkTargetType | undefined;
type SolidMarkdownNames = keyof JSX.IntrinsicElements;
type CodeComponent = Component<
JSX.IntrinsicElements["code"] & SolidMarkdownProps & { inline?: boolean }
>;
type HeadingComponent = Component<
JSX.IntrinsicElements["h1"] & SolidMarkdownProps & { level: number }
>;
type LiComponent = Component<
JSX.IntrinsicElements["li"] &
SolidMarkdownProps & {
checked: boolean | null;
index: number;
ordered: boolean;
}
>;
type OrderedListComponent = Component<
JSX.IntrinsicElements["ol"] &
SolidMarkdownProps & { depth: number; ordered: true }
>;
type TableCellComponent = Component<
JSX.IntrinsicElements["table"] &
SolidMarkdownProps & { style?: Record<string, unknown>; isHeader: boolean }
>;
type TableRowComponent = Component<
JSX.IntrinsicElements["tr"] & SolidMarkdownProps & { isHeader: boolean }
>;
type UnorderedListComponent = Component<
JSX.IntrinsicElements["ul"] &
SolidMarkdownProps & { depth: number; ordered: false }
>;
type SpecialComponents = {
code: CodeComponent | SolidMarkdownNames;
h1: HeadingComponent | SolidMarkdownNames;
h2: HeadingComponent | SolidMarkdownNames;
h3: HeadingComponent | SolidMarkdownNames;
h4: HeadingComponent | SolidMarkdownNames;
h5: HeadingComponent | SolidMarkdownNames;
h6: HeadingComponent | SolidMarkdownNames;
li: LiComponent | SolidMarkdownNames;
ol: OrderedListComponent | SolidMarkdownNames;
td: TableCellComponent | SolidMarkdownNames;
th: TableCellComponent | SolidMarkdownNames;
tr: TableRowComponent | SolidMarkdownNames;
ul: UnorderedListComponent | SolidMarkdownNames;
};
type Components = Partial<Omit<NormalComponents, keyof SpecialComponents>> &
Partial<SpecialComponents>;
export type Options = {
sourcePos: boolean;
rawSourcePos: boolean;
skipHtml: boolean;
includeElementIndex: boolean;
transformLinkUri: null | false | TransformLink;
transformImageUri?: TransformImage;
linkTarget: TransformLinkTargetType | TransformLinkTarget;
components: Components;
};
const own = {}.hasOwnProperty;
// The table-related elements that must not contain whitespace text according
// to React.
const tableElements = new Set(["table", "thead", "tbody", "tfoot", "tr"]);
export function childrenToSolid(
context: Context,
node: Element | Root,
): JSX.Element[] {
const children: JSX.Element[] = [];
let childIndex = -1;
// let child: Comment | DocType | Element | Raw | Text;
while (++childIndex < node.children.length) {
const child = node.children[childIndex] as
| Comment
| DocType
| Element
| Raw
| Text;
if (child.type === "element") {
children.push(toSolid(context, child, childIndex, node));
} else if (child.type === "text") {
// React does not permit whitespace text elements as children of table:
// cf. https://github.com/remarkjs/react-markdown/issues/576
if (
node.type !== "element" ||
!tableElements.has(node.tagName) ||
child.value !== "\n"
) {
children.push(child.value);
}
} else if (child.type === "raw" && !context.options.skipHtml) {
// Default behavior is to show (encoded) HTML.
children.push(child.value);
}
}
return children;
}
function toSolid(
context: Context,
node: Element,
index: number,
parent: Element | Root,
): JSX.Element {
const options = context.options;
const parentSchema = context.schema;
const name = node.tagName as SolidMarkdownNames;
const properties: Record<string, unknown> = {};
let schema = parentSchema;
let property: string;
if (parentSchema.space === "html" && name === "svg") {
schema = svg;
context.schema = schema;
}
if (node.properties) {
for (property in node.properties) {
if (own.call(node.properties, property)) {
addProperty(properties, property, node.properties[property], context);
}
}
}
if (name === "ol" || name === "ul") {
context.listDepth++;
}
const children = childrenToSolid(context, node);
if (name === "ol" || name === "ul") {
context.listDepth--;
}
// Restore parent schema.
context.schema = parentSchema;
// Nodes created by plugins do not have positional info, in which case we use
// an object that matches the position interface.
const position = node.position || {
start: { line: null, column: null, offset: null },
end: { line: null, column: null, offset: null },
};
const component =
options.components && own.call(options.components, name)
? options.components[name]
: name;
const basic = typeof component === "string"; //|| component === React.Fragment;
// TODO Reimplement is Valid
// if (!ReactIs.isValidElementType(component)) {
// throw new TypeError(
// `Component for name \`${name}\` not defined or is not renderable`
// );
// }
properties.key = [
name,
position.start.line,
position.start.column,
index,
].join("-");
if (name === "a" && options.linkTarget) {
properties.target =
typeof options.linkTarget === "function"
? options.linkTarget(
String(properties.href || ""),
node.children,
typeof properties.title === "string" ? properties.title : undefined,
)
: options.linkTarget;
}
if (name === "a" && options.transformLinkUri) {
properties.href = options.transformLinkUri(
String(properties.href || ""),
node.children,
typeof properties.title === "string" ? properties.title : undefined,
);
}
if (
!basic &&
name === "code" &&
parent.type === "element" &&
parent.tagName !== "pre"
) {
properties.inline = true;
}
if (
!basic &&
(name === "h1" ||
name === "h2" ||
name === "h3" ||
name === "h4" ||
name === "h5" ||
name === "h6")
) {
properties.level = Number.parseInt(name.charAt(1), 10);
}
if (name === "img" && options.transformImageUri) {
properties.src = options.transformImageUri(
String(properties.src || ""),
String(properties.alt || ""),
typeof properties.title === "string" ? properties.title : undefined,
);
}
if (!basic && name === "li" && parent.type === "element") {
const input = getInputElement(node);
properties.checked =
input && input.properties ? Boolean(input.properties.checked) : null;
properties.index = getElementsBeforeCount(parent, node);
properties.ordered = parent.tagName === "ol";
}
if (!basic && (name === "ol" || name === "ul")) {
properties.ordered = name === "ol";
properties.depth = context.listDepth;
}
if (name === "td" || name === "th") {
if (properties.align) {
if (!properties.style) properties.style = {};
// @ts-expect-error assume `style` is an object
properties.style.textAlign = properties.align;
delete properties.align;
}
if (!basic) {
properties.isHeader = name === "th";
}
}
if (!basic && name === "tr" && parent.type === "element") {
properties.isHeader = Boolean(parent.tagName === "thead");
}
// If `sourcePos` is given, pass source information (line/column info from markdown source).
if (options.sourcePos) {
properties["data-sourcepos"] = flattenPosition(position);
}
if (!basic && options.rawSourcePos) {
properties.sourcePosition = node.position;
}
// If `includeElementIndex` is given, pass node index info to components.
if (!basic && options.includeElementIndex) {
properties.index = getElementsBeforeCount(parent, node);
properties.siblingCount = getElementsBeforeCount(parent);
}
if (!basic) {
properties.node = node;
}
return (
<Dynamic component={component as any} {...properties}>
{children}
</Dynamic>
);
}
function getInputElement(node: Element | Root): Element | null {
let index = -1;
while (++index < node.children.length) {
const child = node.children[index];
if (child.type === "element" && child.tagName === "input") {
return child;
}
}
return null;
}
function getElementsBeforeCount(
parent: Element | Root,
node?: Element,
): number {
let index = -1;
let count = 0;
while (++index < parent.children.length) {
if (parent.children[index] === node) break;
if (parent.children[index].type === "element") count++;
}
return count;
}
function addProperty(
props: Record<string, unknown>,
prop: string,
value: unknown,
ctx: Context,
) {
const info = find(ctx.schema, prop);
let result = value;
// Ignore nullish and `NaN` values.
if (result === null || result === undefined || result !== result) {
return;
}
// Accept `array`.
// Most props are space-separated.
if (Array.isArray(result)) {
result = info.commaSeparated ? commas(result) : spaces(result);
}
if (info.property === "style" && typeof result === "string") {
result = parseStyle(result);
}
if (info.space && info.property) {
props[
own.call(hastToReact, info.property)
? (hastToReact as any)[info.property]
: info.property
] = result;
} else if (info.attribute) {
props[info.attribute] = result;
}
}
function parseStyle(value: string): Record<string, string> {
const result: Record<string, string> = {};
try {
style(value, iterator);
} catch {
// Silent.
}
return result;
function iterator(name: string, v: string) {
const k = name.slice(0, 4) === "-ms-" ? `ms-${name.slice(4)}` : name;
result[k.replace(/-([a-z])/g, styleReplacer)] = v;
}
}
function styleReplacer(_: unknown, $1: string) {
return $1.toUpperCase();
}
function flattenPosition(
pos:
| Position
| {
start: { line: null; column: null; offset: null };
end: { line: null; column: null; offset: null };
},
): string {
return [
pos.start.line,
":",
pos.start.column,
"-",
pos.end.line,
":",
pos.end.column,
]
.map((d) => String(d))
.join("");
}