442 lines
11 KiB
TypeScript
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("");
|
|
}
|