style: format web/src with oxfmt defaults (via vp fmt)
One-time mechanical sweep, isolated from any functional change. - oxfmt defaults: double quotes, semicolons, 2-space indent, LF, printWidth 100, trailing commas, bracket spacing, arrow parens. - vite.config.ts gains an fmt.ignorePatterns block for web/static: vendored/minified assets (htmx.min.js, compiled css) must never be reformatted. - Verified: rebuild after the sweep produces byte-identical JS/CSS bundles; only sourcemaps changed (they embed sourcesContent).
This commit is contained in:
1 parent
8aeecaa0ef
commit
77cd268f4e
37 files changed
+1151
-1853
No files matched your search
+58
-58
@@ -14,35 +14,35 @@ const md = new MarkdownIt({ html: false, linkify: false });
|
||||
md.use(footnote);
|
||||
|
||||
export function renderNoteHtml(source: string): string {
|
||||
if (!source) return "";
|
||||
const raw = md.render(source);
|
||||
// Disallowing img/input/style/form by tag and keeping DOMPurify's default
|
||||
// safe-URI scheme check (http/https/mailto + same-page anchors only)
|
||||
// closes the link-injection hole, which is the one markdown pivot into
|
||||
// script execution.
|
||||
const clean = DOMPurify.sanitize(raw, {
|
||||
ALLOWED_ATTR: ["href", "class", "id", "role"],
|
||||
FORBID_TAGS: ["img", "input", "style", "form"],
|
||||
});
|
||||
return clean;
|
||||
if (!source) return "";
|
||||
const raw = md.render(source);
|
||||
// Disallowing img/input/style/form by tag and keeping DOMPurify's default
|
||||
// safe-URI scheme check (http/https/mailto + same-page anchors only)
|
||||
// closes the link-injection hole, which is the one markdown pivot into
|
||||
// script execution.
|
||||
const clean = DOMPurify.sanitize(raw, {
|
||||
ALLOWED_ATTR: ["href", "class", "id", "role"],
|
||||
FORBID_TAGS: ["img", "input", "style", "form"],
|
||||
});
|
||||
return clean;
|
||||
}
|
||||
|
||||
// Compact rows may show a plain line instead of rich markup.
|
||||
export function stripNote(source: string): string {
|
||||
if (!source) return "";
|
||||
const holder = document.createElement("div");
|
||||
holder.innerHTML = renderNoteHtml(source);
|
||||
return (holder.textContent || "").replace(/\s+/g, " ").trim();
|
||||
if (!source) return "";
|
||||
const holder = document.createElement("div");
|
||||
holder.innerHTML = renderNoteHtml(source);
|
||||
return (holder.textContent || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// ----- rich paste: HTML flavor -> markdown source -----
|
||||
const turndown = new TurndownService({
|
||||
// Keep the converted output inside our supported syntax: headings,
|
||||
// emphasis, lists, code, quotes, links, tables. Everything else degrades.
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
bulletListMarker: "-",
|
||||
hr: "---",
|
||||
// Keep the converted output inside our supported syntax: headings,
|
||||
// emphasis, lists, code, quotes, links, tables. Everything else degrades.
|
||||
headingStyle: "atx",
|
||||
codeBlockStyle: "fenced",
|
||||
bulletListMarker: "-",
|
||||
hr: "---",
|
||||
});
|
||||
turndown.use(gfm);
|
||||
turndown.use(tables);
|
||||
@@ -53,50 +53,50 @@ turndown.addRule("noImages", { filter: "img", replacement: () => "" });
|
||||
// headingless ones as raw HTML — which our no-raw-HTML parser would then
|
||||
// show as literal tags. Convert those to pipe syntax with a blank header.
|
||||
turndown.addRule("tablesNoHeading", {
|
||||
filter: (node: Node) => {
|
||||
const el = node as HTMLElement;
|
||||
if (el.nodeName !== "TABLE" || !el.querySelector) return false;
|
||||
return !el.querySelector("th") && el.querySelectorAll("tr").length > 0;
|
||||
},
|
||||
replacement: (_content: string, node: Node) => {
|
||||
const table = node as HTMLTableElement;
|
||||
const rows = Array.from(table.querySelectorAll("tr")).map((tr) =>
|
||||
Array.from(tr.querySelectorAll("td, th")).map((cell) =>
|
||||
(cell.textContent ?? "").replace(/\s+/g, " ").trim().replace(/\|/g, "\\|"),
|
||||
),
|
||||
);
|
||||
const cols = Math.max(1, ...rows.map((r) => r.length));
|
||||
const pad = (r: string[]) => {
|
||||
const copy = [...r];
|
||||
while (copy.length < cols) copy.push("");
|
||||
return copy.map((c) => c || " ");
|
||||
};
|
||||
const head = pad(rows[0] ?? []);
|
||||
const lines = [
|
||||
"| " + head.join(" | ") + " |",
|
||||
"|" + Array.from({ length: cols }, () => " --- ").join("|") + "|",
|
||||
...rows.slice(1).map((r) => "| " + pad(r).join(" | ") + " |"),
|
||||
];
|
||||
return "\n\n" + lines.join("\n") + "\n\n";
|
||||
},
|
||||
filter: (node: Node) => {
|
||||
const el = node as HTMLElement;
|
||||
if (el.nodeName !== "TABLE" || !el.querySelector) return false;
|
||||
return !el.querySelector("th") && el.querySelectorAll("tr").length > 0;
|
||||
},
|
||||
replacement: (_content: string, node: Node) => {
|
||||
const table = node as HTMLTableElement;
|
||||
const rows = Array.from(table.querySelectorAll("tr")).map((tr) =>
|
||||
Array.from(tr.querySelectorAll("td, th")).map((cell) =>
|
||||
(cell.textContent ?? "").replace(/\s+/g, " ").trim().replace(/\|/g, "\\|"),
|
||||
),
|
||||
);
|
||||
const cols = Math.max(1, ...rows.map((r) => r.length));
|
||||
const pad = (r: string[]) => {
|
||||
const copy = [...r];
|
||||
while (copy.length < cols) copy.push("");
|
||||
return copy.map((c) => c || " ");
|
||||
};
|
||||
const head = pad(rows[0] ?? []);
|
||||
const lines = [
|
||||
"| " + head.join(" | ") + " |",
|
||||
"|" + Array.from({ length: cols }, () => " --- ").join("|") + "|",
|
||||
...rows.slice(1).map((r) => "| " + pad(r).join(" | ") + " |"),
|
||||
];
|
||||
return "\n\n" + lines.join("\n") + "\n\n";
|
||||
},
|
||||
});
|
||||
|
||||
export function htmlToMarkdown(html: string): string {
|
||||
return turndown.turndown(html);
|
||||
return turndown.turndown(html);
|
||||
}
|
||||
|
||||
/** Reads the paste event's clipboard and returns markdown converted from the
|
||||
* rich (text/html) flavor, or null when the clipboard holds no usable rich
|
||||
* content (plain pastes must fall through to the browser default). */
|
||||
export function richPasteToMarkdown(event: ClipboardEvent): string | null {
|
||||
const data = event.clipboardData;
|
||||
if (!data) return null;
|
||||
const html = data.getData("text/html");
|
||||
if (!html || !html.trim()) return null;
|
||||
// A rich flavor that is just an HTML wrapper of plain text (some apps
|
||||
// emit <p>...</p> wrappers for everything Chrome copies) still converts
|
||||
// harmlessly to the same text, so no content sniffing is needed here.
|
||||
const markdown = htmlToMarkdown(html);
|
||||
if (!markdown || !markdown.trim()) return null;
|
||||
return markdown;
|
||||
const data = event.clipboardData;
|
||||
if (!data) return null;
|
||||
const html = data.getData("text/html");
|
||||
if (!html || !html.trim()) return null;
|
||||
// A rich flavor that is just an HTML wrapper of plain text (some apps
|
||||
// emit <p>...</p> wrappers for everything Chrome copies) still converts
|
||||
// harmlessly to the same text, so no content sniffing is needed here.
|
||||
const markdown = htmlToMarkdown(html);
|
||||
if (!markdown || !markdown.trim()) return null;
|
||||
return markdown;
|
||||
}
|
||||
Reference in new issue
Block a user