import { DOMParser as PMDOMParser, DOMSerializer, Node as PMNode, Schema, Slice, } from 'prosemirror-model'; import { sanitizeHtml } from './sanitize'; const parserCache = new WeakMap(); const serializerCache = new WeakMap(); function getParser(schema: Schema): PMDOMParser { let parser = parserCache.get(schema); if (!parser) { parser = PMDOMParser.fromSchema(schema); parserCache.set(schema, parser); } return parser; } function getSerializer(schema: Schema): DOMSerializer { let serializer = serializerCache.get(schema); if (!serializer) { serializer = DOMSerializer.fromSchema(schema); serializerCache.set(schema, serializer); } return serializer; } /** Parses an HTML string into a document node, sanitizing by default. */ export function htmlToDoc(schema: Schema, html: string, sanitize = true): PMNode { const clean = sanitize ? sanitizeHtml(html) : html; const dom = new DOMParser().parseFromString(`${clean}`, 'text/html'); return getParser(schema).parse(dom.body); } /** Parses an HTML string into a slice suitable for replaceSelection. */ export function htmlToSlice(schema: Schema, html: string, sanitize = true): Slice { const clean = sanitize ? sanitizeHtml(html) : html; const dom = new DOMParser().parseFromString(`${clean}`, 'text/html'); return getParser(schema).parseSlice(dom.body, { preserveWhitespace: true }); } /** Serializes a document node back to an HTML string. */ export function docToHtml(doc: PMNode): string { const schema = doc.type.schema; const container = document.createElement('div'); container.appendChild(getSerializer(schema).serializeFragment(doc.content)); return container.innerHTML; } /** Plain-text projection of the document (blocks joined by newlines). */ export function docToText(doc: PMNode): string { return doc.textBetween(0, doc.content.size, '\n', ' '); } export function docToJson(doc: PMNode): Record { return doc.toJSON(); } export function jsonToDoc(schema: Schema, json: Record): PMNode { return PMNode.fromJSON(schema, json); } /** Word and character counts over the document's text content. */ export function countWords(doc: PMNode): { words: number; characters: number } { const text = docToText(doc); const words = text.trim() ? text.trim().split(/\s+/).length : 0; return { words, characters: text.replace(/\n/g, '').length }; } /** True when the document has no visible content. */ export function isDocEmpty(doc: PMNode): boolean { if (doc.childCount > 1) return false; const first = doc.firstChild; return !first || (first.type.name === 'paragraph' && first.content.size === 0); }