import type { BlokConfig } from '../../../../types/configs/blok-config'; import type { SanitizerConfig } from '../../../../types/configs/sanitizer-config'; import type { BlockToolAdapter } from '../../tools/block'; import type { ToolsCollection } from '../../tools/collection'; import { isObject } from '../../utils'; import { clean , composeSanitizerConfig } from '../../utils/sanitizer'; import { parseUntrustedHtml } from '../../utils/inert-html'; import { SAFE_STRUCTURAL_TAGS, STRUCTURAL_TAG_ATTRIBUTES, collectTagNames } from './constants'; import type { TagSubstitute } from './types'; /** * Sanitizer Config Builder builds sanitizer configs from tool configurations. * Handles structural tags detection and config merging. */ export class SanitizerConfigBuilder { constructor( private readonly tools: ToolsCollection, private readonly config: BlokConfig ) {} /** * Build sanitizer config for all tool tags. */ public buildToolsTagsConfig(toolsTags: { [tag: string]: TagSubstitute }): SanitizerConfig { return Object.fromEntries( Object.keys(toolsTags).map((tag) => [ tag.toLowerCase(), toolsTags[tag].sanitizationConfig ?? {}, ]) ); } /** * Detect structural tags in HTML node. */ public getStructuralTagsConfig(node: HTMLElement): SanitizerConfig { const config: SanitizerConfig = {}; const nodesToProcess: Element[] = [ node ]; while (nodesToProcess.length > 0) { const current = nodesToProcess.pop(); if (!current) { continue; } const tagName = current.tagName.toLowerCase(); if (SAFE_STRUCTURAL_TAGS.has(tagName)) { config[tagName] = config[tagName] ?? STRUCTURAL_TAG_ATTRIBUTES[tagName] ?? {}; } nodesToProcess.push(...Array.from(current.children)); } return config; } /** * Build sanitizer config for a specific tool. */ public buildToolConfig(tool: BlockToolAdapter): SanitizerConfig { if (tool.pasteConfig === false) { return {}; } const tagsOrSanitizeConfigs = tool.pasteConfig?.tags || []; const toolTags: SanitizerConfig = {}; tagsOrSanitizeConfigs.forEach((tagOrSanitizeConfig) => { const tags = collectTagNames(tagOrSanitizeConfig); tags.forEach((tag) => { const sanitizationConfig = isObject(tagOrSanitizeConfig) ? (tagOrSanitizeConfig)[tag] : null; toolTags[tag.toLowerCase()] = sanitizationConfig ?? {}; }); }); return toolTags; } /** * Compose multiple sanitizer configs. */ public composeConfigs(...configs: SanitizerConfig[]): SanitizerConfig { return composeSanitizerConfig({}, ...configs); } /** * Special handling for table sanitization. */ public sanitizeTable(table: HTMLElement, config: SanitizerConfig): HTMLElement | null { /** * Allow `` inside table cells so pasted images (e.g. from Google Docs * tables) survive sanitization and end up in the cell's paragraph content * rather than being stripped silently. Checkbox `` likewise — it * carries checklist state for task lists pasted inside cells. */ const tableConfig: SanitizerConfig = { ...config, img: { src: true, alt: true, width: true, height: true }, input: { type: true, checked: true }, }; const cleanTableHTML = clean(table.outerHTML, tableConfig); // `img: { src: true }` above survives the clean, so a live re-parse would // fetch every pasted image URL right here. const firstChild = parseUntrustedHtml(cleanTableHTML).firstChild; if (!firstChild || !(firstChild instanceof HTMLElement)) { return null; } return firstChild; } /** * Check if a tag is a structural tag. */ public isStructuralTag(tagName: string): boolean { return SAFE_STRUCTURAL_TAGS.has(tagName.toLowerCase()); } }