stoat-for-desktop/components/markdown/plugins/spoiler.tsx

163 lines
4.0 KiB
TypeScript

import { createSignal } from "solid-js";
import { Handler } from "mdast-util-to-hast";
import { styled } from "styled-system/jsx";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
const Spoiler = styled("span", {
base: {
padding: "0 2px",
borderRadius: "var(--borderRadius-md)",
},
variants: {
shown: {
true: {
color: "var(--md-sys-color-inverse-on-surface)",
background: "var(--md-sys-color-inverse-surface)",
},
false: {
cursor: "pointer",
userSelect: "none",
color: "transparent",
background: "#151515",
"> *": {
opacity: 0,
pointerEvents: "none",
},
},
},
},
});
export function RenderSpoiler(props: {
children: Element;
disabled?: boolean;
}) {
const [shown, setShown] = createSignal(false);
return (
<Spoiler
shown={shown()}
onClick={props.disabled ? undefined : () => setShown(true)}
>
{props.children}
</Spoiler>
);
}
export const remarkSpoiler: Plugin = () => (tree) => {
visit(
tree,
"paragraph",
(
node: {
children: (
| { type: "text"; value: string }
| { type: "paragraph"; children: unknown[] }
| { type: "spoiler"; children: unknown[] }
)[];
},
_idx,
_parent,
) => {
// Visitor state
let searchingForEnd = -1;
let spoilerContent: object[] = [];
// Visit all children of paragraphs
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
// Find the next text element to start a spoiler from
if (child.type === "text") {
const components = child.value.split("||");
if (components.length === 1) continue; // no spoilers
// Handle terminating spoiler tag
if (searchingForEnd !== -1) {
// Get all preceding elements
const elements = node.children.splice(
searchingForEnd,
i - searchingForEnd,
);
// Create a spoiler
node.children.splice(i, 0, {
type: "spoiler",
children: [
...spoilerContent,
...elements,
{
type: "text",
value: components.shift(),
},
],
});
// Adjust our current index
i += elements.length + 1;
searchingForEnd = -1;
spoilerContent = [];
}
// Replace current child with next component
child.value = components.shift()!;
// Check how many spoilers we have to process
const spillOver = components.length % 2 === 1;
const innerElements = (components.length - (spillOver ? 1 : 0)) / 2;
// Convert inner elements into spoilers
if (innerElements) {
for (let j = 0; j < innerElements; j++) {
node.children.splice(
i + 1,
0,
{
type: "spoiler",
children: [
{
type: "text",
value: components.shift(),
},
],
},
{
type: "text",
value: components.shift()!,
},
);
i += 2;
}
}
// Update state if we are looking for the end of a spoiler
if (spillOver) {
searchingForEnd = i + 1;
spoilerContent.push({
type: "text",
value: components.pop(),
});
}
}
}
},
);
};
export const spoilerHandler: Handler = (h, node) => {
return {
type: "element" as const,
tagName: "spoiler",
children: h.all({
type: "paragraph",
children: node.children,
}),
properties: {},
};
};