/**
* Pre-process Notion clipboard HTML before sanitization.
*
* Notion encodes block/inline state with classes (`to-do-list`, `checkbox-on`,
* `toggle`, `callout`, `block-color-*`) and non-semantic markup that Blok's
* sanitizer strips. This function rewrites that markup into Blok's allowed
* semantic tag set BEFORE the sanitizer runs. It mirrors
* `preprocessGoogleDocsHtml` and is a no-op when the source is not Notion.
*
* Scope note: this is the HTML *fallback* path (Notion desktop app /
* cross-browser paste). The higher-fidelity path parses Notion's proprietary
* `text/_notion-blocks-v3-production` JSON flavor directly when present
* (web→web); see docs/plans/2026-06-22-notion-paste-migration-design.md §4.1b.
*
* @param html - raw clipboard HTML string
* @returns preprocessed HTML string (unchanged when not Notion)
*/
import { parseUntrustedHtml } from '../../utils/inert-html';
export function preprocessNotionHtml(html: string): string {
const wrapper = parseUntrustedHtml(html);
if (!isNotionClipboard(wrapper)) {
return html;
}
normalizeInlineMarks(wrapper);
return wrapper.innerHTML;
}
/**
* Centralized Notion clipboard signature. This is the ONE place to update
* when Notion changes its class names.
*/
const NOTION_SIGNATURE_SELECTOR = [
'figure.callout',
'ul.to-do-list',
'ul.toggle',
'ol.numbered-list',
'ul.bulleted-list',
'.notion-text-equation-token',
'[class*="block-color-"]',
'figure.equation',
].join(', ');
/**
* Whether the pasted markup originated from Notion.
*/
function isNotionClipboard(wrapper: HTMLElement): boolean {
return wrapper.querySelector(NOTION_SIGNATURE_SELECTOR) !== null;
}
/**
* Rewrite inline marks that Blok's sanitizer strips into allowed equivalents.
*
* Notion emits strikethrough as ``/``, but Blok's strikethrough
* inline tool only whitelists `` — so the others are dropped unless rewritten.
*/
function normalizeInlineMarks(wrapper: HTMLElement): void {
wrapper.querySelectorAll('del, strike').forEach((el) => {
// Created in el's own (inert) document: a live would load whatever the
// pasted markup carries the moment its innerHTML is written.
const s = el.ownerDocument.createElement('s');
s.innerHTML = el.innerHTML;
el.replaceWith(s);
});
}