import type { BlokConfig } from '../../../../../types/configs/blok-config'; import type { SanitizerConfig } from '../../../../../types/configs/sanitizer-config'; import type { BlokModules } from '../../../../types-internal/blok-modules'; import { Dom as dom$ } from '../../../dom'; import { ensureStrongElement } from '../../../inline-tools/utils/bold-dom-utils'; import type { BlockToolAdapter } from '../../../tools/block'; import { isObject } from '../../../utils'; import { applyLinkConfig } from '../../../utils/apply-link-config'; import { clean } from '../../../utils/sanitizer'; import { COLUMNS_CANDIDATE_ATTR, SAFE_STRUCTURAL_TAGS, collectTagNames } from '../constants'; import type { SanitizerConfigBuilder } from '../sanitizer-config'; import type { ToolRegistry } from '../tool-registry'; import type { HandlerContext, PasteData } from '../types'; import type { PasteHandler } from './base'; import { BasePasteHandler } from './base'; import { parseUntrustedHtml } from '../../../utils/inert-html'; /** * A node queued for block mapping, optionally expanded out of a container * (DETAILS/ASIDE children, columns-candidate table cells). */ interface ExpandedNodeEntry { node: Node; /** Index of this entry's parent within the expanded list. */ parentExpandedIndex?: number; /** Fixed tool name bypassing tag substitution (column_list / column). */ toolOverride?: string; /** Creation-time tool data (e.g. noSeed) for synthesized containers. */ toolData?: Record; /** Keep the entry even though its content is empty (layout containers). */ keepEmpty?: boolean; } /** * HTML Handler Priority. * Handles HTML string content. */ export class HtmlHandler extends BasePasteHandler implements PasteHandler { private readonly linkConfig?: BlokConfig['link']; constructor( Blok: BlokModules, toolRegistry: ToolRegistry, sanitizerBuilder: SanitizerConfigBuilder, config?: BlokConfig ) { super(Blok, toolRegistry, sanitizerBuilder); this.linkConfig = config?.link; } canHandle(data: unknown): number { if (typeof data !== 'string') { return 0; } if (!data.trim() || !dom$.isHTMLString(data)) { return 0; } return 40; } async handle(data: unknown, context: HandlerContext): Promise { if (typeof data !== 'string') { return false; } const dataToInsert = this.processHTML(data); if (!dataToInsert.length) { return false; } await this.insertPasteData(dataToInsert, context.canReplaceCurrentBlock); return true; } /** * Split HTML string to blocks and return it as array of Block data. */ private processHTML(innerHTML: string): PasteData[] { const { Tools } = this.Blok; const wrapper = parseUntrustedHtml(innerHTML); // Normalize legacy → on the DETACHED wrapper, before the block // is rendered and the caret lands inside the pasted content. The live bold // MutationObserver deliberately skips any that contains the caret (to // avoid scrambling actively-typed collapsed-bold), and on paste the caret is // moved to the block END — landing inside a freshly pasted — so that // observer would leave it as forever. Converting here (no caret present) // is race-free and mirrors the preprocessNestedLists pre-splitter pass. Note // the wrapper is detached, so BoldNormalizationPass (which only converts // CONNECTED nodes) is a no-op here — ensureStrongElement swaps in place. wrapper.querySelectorAll('b').forEach(bold => ensureStrongElement(bold)); // Quote is a leaf tool: its onPaste stores innerHTML as inline rich text // and its sanitizer strips list tags — a list left inside a
// would silently flatten on save. Split lists out first, then stamp them. this.splitListsOutOfBlockquotes(wrapper); // Notion/GDocs wrap quote text in

; the registered P tag would force // the blockquote to descend into loose paragraphs and lose quote-ness. this.unwrapBlockquoteParagraphs(wrapper); // Preserve nested-list depth and ordered/unordered context BEFORE the splitter // detaches each

  • from its ancestor