import { Schema, NodeSpec, MarkSpec, Attrs, DOMOutputSpec, ParseRule } from 'prosemirror-model'; import { addListNodes } from 'prosemirror-schema-list'; import { tableNodes } from 'prosemirror-tables'; import OrderedMap from 'orderedmap'; import { EMBED_SANDBOX, EMBED_REFERRER_POLICY } from './plugins/embeds'; export type Alignment = 'left' | 'center' | 'right' | 'justify'; const ALIGN_VALUES: Alignment[] = ['left', 'center', 'right', 'justify']; function getAlignFromDom(dom: HTMLElement): Alignment | null { const value = dom.style.textAlign || dom.getAttribute('align') || ''; return (ALIGN_VALUES as string[]).includes(value) ? (value as Alignment) : null; } function alignStyle(align: Alignment | null): string | null { return align ? `text-align: ${align}` : null; } /** Indent depth in 40px steps, like CKEditor/Google Docs. */ export const INDENT_STEP_PX = 40; export const MAX_INDENT = 8; function getIndentFromDom(dom: HTMLElement): number { const raw = dom.style.marginInlineStart || dom.style.marginLeft || ''; const px = parseFloat(raw); if (!Number.isFinite(px) || px <= 0) return 0; return Math.min(MAX_INDENT, Math.round(px / INDENT_STEP_PX)); } /** * Line height as a unitless multiplier string ('1.5'). Percentages are * converted; px values pass through as-is so pasted content keeps its look. */ function getLineHeightFromDom(dom: HTMLElement): string | null { const raw = dom.style.lineHeight.trim(); if (!raw || raw === 'normal') return null; const num = parseFloat(raw); if (!Number.isFinite(num) || num <= 0) return null; if (raw.endsWith('%')) return String(num / 100); return raw; } /** Combined style string for align + indent + line-height textblock attrs. */ function blockStyle(attrs: { align?: Alignment | null; indent?: number; lineHeight?: string | null; }): string | null { const parts: string[] = []; const align = alignStyle(attrs.align ?? null); if (align) parts.push(align); if (attrs.indent) parts.push(`margin-inline-start: ${attrs.indent * INDENT_STEP_PX}px`); if (attrs.lineHeight) parts.push(`line-height: ${attrs.lineHeight}`); return parts.length ? parts.join('; ') : null; } /* ------------------------------------------------------------------------- * Author class / id passthrough * * Content elements keep the `class` and `id` an author gave them, so class/id * CSS (e.g. a scoped author