import { createEffect, on, onMount } from "solid-js";
// M15/THREAT-006: allow-list sanitizer for note bodies. The editor writes
// arbitrary HTML directly to innerHTML; without this any note body could
// inject iframe/base/meta/form/script. Allow-list is intentional — adding
// to it requires reviewing the attack surface of the new tag.
const ALLOWED_NOTE_TAGS = new Set([
"b", "i", "u", "s", "strong", "em",
"h2", "h3", "blockquote", "ul", "ol", "li", "br", "hr", "p",
]);
function sanitizeNoteElement(el: Element): void {
for (const child of Array.from(el.children)) {
if (!ALLOWED_NOTE_TAGS.has(child.tagName.toLowerCase())) {
child.replaceWith(document.createTextNode(child.textContent ?? ""));
} else {
while (child.attributes.length > 0) {
child.removeAttribute(child.attributes[0]!.name);
}
sanitizeNoteElement(child);
}
}
}
export function toHtml(raw: string): string {
if (!/<\/?[a-z]/i.test(raw)) {
return raw
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/\n/g, "
");
}
const doc = new DOMParser().parseFromString(raw, "text/html");
sanitizeNoteElement(doc.body);
return doc.body.innerHTML;
}
const BoldIcon = () => B;
const ItalicIcon = () => I;
const UnderlineIcon = () => U;
const StrikeIcon = () => S;
const HeadingIcon = () => H;
const BulletIcon = () => (
);
const NumberedIcon = () => (
);
const RuleIcon = () => (
);
export type ExecFn = (cmd: string, val?: string) => void;
export function Toolbar(props: { exec: ExecFn; class?: string }) {
const e = props.exec;
function toggleHeading() {
const val = document.queryCommandValue("formatBlock").toLowerCase();
e("formatBlock", val === "h2" ? "p" : "h2");
}
return (