From 41d3cb10461f7c7b6dabe5103ee06bca89c83474 Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Wed, 16 Sep 2026 04:06:33 +0700 Subject: [PATCH] docs(text): require JSDoc on production exports Assisted-by: Codex:gpt-6-astra Signed-off-by: Hoang Pham --- eslint.config.js | 8 +++ src/comparison/comparisonAlignment.ts | 34 +++++++++++++ src/comparison/comparisonDocumentIndex.ts | 17 +++++++ src/comparison/comparisonNavigation.ts | 43 ++++++++++++++++ src/comparison/comparisonPresentation.ts | 10 ++++ src/comparison/comparisonSections.ts | 19 +++++++ src/comparison/createComparisonEditor.ts | 6 +++ .../hierarchicalMarkdownComparisonModel.ts | 9 ++++ src/comparison/markdownComparison.ts | 30 +++++++++++ .../markdownComparisonClassification.ts | 51 +++++++++++++++++++ src/comparison/markdownComparisonMoves.ts | 8 +++ src/comparison/markdownSourceComparison.ts | 17 +++++++ .../markdownSourceComparisonProtocol.ts | 5 ++ src/comparison/markdownSourceDisplay.ts | 11 ++++ src/composables/useEditorMethods.ts | 6 +++ src/createMarkdownContentComparison.ts | 7 +++ 16 files changed, 281 insertions(+) diff --git a/eslint.config.js b/eslint.config.js index 5cc66dd99df..1c78c701bf3 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,6 +10,14 @@ import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig( ...recommended, globalIgnores(['src/tests/fixtures/*']), + { + name: 'production-public-jsdoc', + files: ['src/**/*.{js,ts,vue}'], + ignores: ['**/*.test.*', '**/*.spec.*', '**/*.cy.*', '**/test/**', '**/tests/**', '**/__tests__/**', '**/__mocks__/**'], + rules: { + 'jsdoc/require-jsdoc': ['warn', { publicOnly: true }], + }, + }, { files: ['cypress/**/*.js'], extends: [pluginCypress.configs.globals], diff --git a/src/comparison/comparisonAlignment.ts b/src/comparison/comparisonAlignment.ts index 40b4747fa4e..9ce98725042 100644 --- a/src/comparison/comparisonAlignment.ts +++ b/src/comparison/comparisonAlignment.ts @@ -43,6 +43,9 @@ type Region = ComparisonAlignmentRegion type Pair = ExactComparisonPair type Ledger = ComparisonWorkLedger +/** + * Create a mutable work budget shared by all axes of one comparison. + */ export function createComparisonWorkLedger(): Ledger { return { remainingCells: DEFAULT_COMPARISON_CELL_LEDGER, @@ -50,6 +53,11 @@ export function createComparisonWorkLedger(): Ledger { } } +/** + * Keep only pairs present in every longest increasing alignment. + * + * @param pairs Candidate indices on the Before and After axes. + */ export function forcedIncreasingPairs(pairs: readonly Pair[]): readonly Pair[] { if (pairs.length < 2) { return pairs @@ -73,6 +81,11 @@ export function forcedIncreasingPairs(pairs: readonly Pair[]): readonly Pair[] { && candidatesPerLevel[left[index]!] === 1) } +/** + * Find a strictly increasing subsequence and the best length ending at each input index. + * + * @param values Axis indices in candidate order. + */ export function increasingSubsequence(values: readonly number[]) { const tails: number[] = [] const previous = new Int32Array(values.length).fill(-1) @@ -100,6 +113,13 @@ export function increasingSubsequence(values: readonly number[]) { return { lengths, indices: indices.reverse() } } +/** + * Align an axis around unique exact matches, preserving coarse regions when attribution is uncertain. + * + * @param before Original axis items. + * @param after Replacement axis items. + * @param options Matching functions and the shared work budget consumed by gap solving. + */ export function alignComparisonAxis(before: readonly T[], after: readonly T[], options: Options): readonly Region[] { const beforeKeys = before.map(options.fingerprint) const afterKeys = after.map(options.fingerprint) @@ -107,6 +127,13 @@ export function alignComparisonAxis(before: readonly T[], after: readonly T[] ?? planAxis(before, after, beforeKeys, afterKeys, options, uniqueExactPairs(beforeKeys, afterKeys), true) } +/** + * Align columns using occurrence rank when repeated exact columns have equal counts. + * + * @param before Original columns. + * @param after Replacement columns. + * @param options Matching functions and the shared work budget consumed by gap solving. + */ export function alignComparisonColumns(before: readonly T[], after: readonly T[], options: Options): readonly Region[] { const beforeKeys = before.map(options.fingerprint) const afterKeys = after.map(options.fingerprint) @@ -245,6 +272,13 @@ interface AlignmentState { signatures: readonly number[] } +/** + * Resolve a gap to array-index pairs, or return a coarse reason for ambiguity or exhausted work. + * + * @param before Original items within this gap. + * @param after Replacement items within this gap. + * @param options Matching functions and mutable budget charged before solving. + */ export function solveWeightedGap(before: readonly T[], after: readonly T[], options: Options): { steps: readonly Step[] } | { coarseReason: CoarseReason } { const cellCharge = before.length * after.length if (cellCharge > options.work.remainingCells) { diff --git a/src/comparison/comparisonDocumentIndex.ts b/src/comparison/comparisonDocumentIndex.ts index 6a8383714c9..5c519789e6c 100644 --- a/src/comparison/comparisonDocumentIndex.ts +++ b/src/comparison/comparisonDocumentIndex.ts @@ -28,6 +28,11 @@ interface Mutable extends Omit { const minimalRootsCache = new WeakMap() +/** + * Index original document positions and parent/child paths. Looking up an absent path throws. + * + * @param doc Document whose nodes remain unchanged. + */ export function createComparisonDocumentIndex(doc: Node): ComparisonDocumentIndex { const byPath = new Map() const locateChildren = ( @@ -70,6 +75,12 @@ export function createComparisonDocumentIndex(doc: Node): ComparisonDocumentInde } } +/** + * Find intersecting nodes and their ancestors in document order. Empty ranges include touching boundaries. + * + * @param range Range in original ProseMirror coordinates. + * @param roots Indexed subtrees to search. + */ export function findComparisonNodes(range: Range, roots: readonly Location[]) { const found = new Map() const add = (location: Location) => found.set(pathKey(location.path), location) @@ -89,6 +100,12 @@ export function findComparisonNodes(range: Range, roots: readonly Location[]) { return [...found.values()].toSorted((a, b) => a.from - b.from || a.path.length - b.path.length) } +/** + * Read a range without duplicating nested roots, using newlines for blocks and U+FFFC for leaves. + * + * @param range Original document range; an empty range returns an empty string. + * @param roots Indexed subtrees containing the range. + */ export function comparisonRangeText(range: Range, roots: readonly Location[]) { if (range.from === range.to) { return '' diff --git a/src/comparison/comparisonNavigation.ts b/src/comparison/comparisonNavigation.ts index c7ff5008a20..9e60242f37c 100644 --- a/src/comparison/comparisonNavigation.ts +++ b/src/comparison/comparisonNavigation.ts @@ -5,10 +5,22 @@ import type { ComparisonEdit as Edit, ComparisonSide as Side } from './markdownComparisonTypes.ts' +/** + * Check whether every descriptor changes formatting alone. + * + * @param edit Semantic edit to classify. + */ export function isPureFormatting(edit: Edit) { return edit.descriptors.every(({ facets }) => facets.length === 1 && facets[0] === 'formatting') } +/** + * Keep the current edit if visible, otherwise prefer the next visible edit, then the previous one. + * + * @param edits All edits in navigation order. + * @param activeIds Visible edit IDs; an empty list clears the selection. + * @param currentId Previously selected edit, if any. + */ export function currentIdAfterFilter( edits: readonly Edit[], activeIds: readonly string[], @@ -37,6 +49,13 @@ export function currentIdAfterFilter( return edits.find(({ id }) => active.has(id))?.id ?? null } +/** + * Move through visible edits with wraparound, returning null for an empty list. + * + * @param activeIds Visible edit IDs in navigation order. + * @param currentId Selected ID; an absent ID starts from the first edit. + * @param offset Signed number of edits to move. + */ export function moveCurrentId(activeIds: readonly string[], currentId: string | null, offset: number) { if (activeIds.length === 0) { return null @@ -46,21 +65,45 @@ export function moveCurrentId(activeIds: readonly string[], currentId: string | return activeIds[next]! } +/** + * Return a one-based visible ordinal, or zero when no visible edit is selected. + * + * @param activeIds Visible edit IDs in navigation order. + * @param currentId Selected ID, if any. + */ export function currentOrdinal(activeIds: readonly string[], currentId: string | null) { const index = currentId ? activeIds.indexOf(currentId) : -1 return index < 0 ? 0 : index + 1 } +/** + * Map arrow, Home and End keys to a side, returning null for other keys. + * + * @param key KeyboardEvent key value. + */ export function comparisonSideForKey(key: string): Side | null { if (key === 'ArrowLeft' || key === 'ArrowUp' || key === 'Home') { return 'before' } return key === 'ArrowRight' || key === 'ArrowDown' || key === 'End' ? 'after' : null } +/** + * Use immediate scrolling when the reader requests reduced motion. + */ export function comparisonScrollBehavior(): ScrollBehavior { return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' } +/** + * Center a change in its pane while preserving horizontal scroll and clamping to the scrollable range. + * Return false when the pane, scroller or target geometry is unavailable. + * + * @param pane Visible pane containing the change decorations. + * @param scroller Scroll container inside the pane. + * @param id Descriptor ID to locate. + * @param behavior Requested browser scroll behavior. + * @param fallbackRect Geometry for an undecorated range, such as an insertion boundary. + */ export function locateComparisonTarget(pane: HTMLElement | null, scroller: HTMLElement | null, id: string, behavior: ScrollBehavior, fallbackRect?: () => { top: number, height: number } | null) { if (!pane || !scroller || !pane.contains(scroller) || pane.hidden || pane.style.display === 'none') { return false diff --git a/src/comparison/comparisonPresentation.ts b/src/comparison/comparisonPresentation.ts index abc94771666..05c2d97359a 100644 --- a/src/comparison/comparisonPresentation.ts +++ b/src/comparison/comparisonPresentation.ts @@ -38,12 +38,22 @@ const marks: Record = { 'inline-code': [101, () => t('text', 'Inline code')], } +/** + * Choose the highest-priority signal, keeping the first on ties and returning undefined for an empty list. + * + * @param signals Signals attached to a descriptor. + */ export function selectComparisonSignal(signals: readonly Signal[]): Signal | undefined { return signals.reduce((selected, signal) => ( !selected || signalPriority(signal) > signalPriority(selected) ? signal : selected ), undefined) } +/** + * Localize attribute and mark signals; other signal kinds have no label here. + * + * @param signal Descriptor signal to describe. + */ export function comparisonSignalLabel(signal: Signal) { if (signal.type === 'attribute') { return attributes[signal.attribute][1]() diff --git a/src/comparison/comparisonSections.ts b/src/comparison/comparisonSections.ts index e3ec07a0af6..d0419f0f701 100644 --- a/src/comparison/comparisonSections.ts +++ b/src/comparison/comparisonSections.ts @@ -20,6 +20,11 @@ export interface ComparisonSection { } type Heading = ComparisonHeading +/** + * Collect nonempty top-level headings at their original document positions. + * + * @param doc Document to inspect. + */ export function headingLocations(doc: Node): readonly Heading[] { const headings: Heading[] = [] doc.forEach((node, from) => { @@ -44,6 +49,12 @@ function nearestHeadingIndex(headings: readonly Heading[], position: number) { } return lower - 1 } +/** + * Return the heading at or before a position, or an empty title before the first heading. + * + * @param headings Headings ordered by document position. + * @param position Original ProseMirror position. + */ export function nearestHeading(headings: readonly Heading[], position: number) { return headings[nearestHeadingIndex(headings, position)]?.text ?? '' } @@ -128,6 +139,14 @@ function resolveSection(edit: ComparisonEdit, before: HeadingIndex, after: Headi return side.keys[nearestHeadingIndex(side.headings, position)] ?? '' } +/** + * Group consecutive edits under correlated headings, preserving edit order. + * Heading correlation groups changes; it does not establish unchanged-block correspondence. + * + * @param edits Edits in display order. + * @param beforeDocument Original Before document. + * @param afterDocument Original After document. + */ export function buildComparisonSections(edits: readonly ComparisonEdit[], beforeDocument: Node, afterDocument: Node): readonly ComparisonSection[] { const beforeHeadings = headingLocations(beforeDocument) const afterHeadings = headingLocations(afterDocument) diff --git a/src/comparison/createComparisonEditor.ts b/src/comparison/createComparisonEditor.ts index 9adff9bcd34..e5c2561d544 100644 --- a/src/comparison/createComparisonEditor.ts +++ b/src/comparison/createComparisonEditor.ts @@ -17,6 +17,12 @@ interface ComparisonEditorOptions { schema?: Schema } +/** + * Create a read-only embedded Markdown editor. The caller owns attachment and destruction. + * + * @param content Markdown snapshot; non-string input throws. + * @param options Accessibility, resource, link and optional shared-schema settings. + */ export function createComparisonEditor(content: string, options: ComparisonEditorOptions = {}) { if (typeof content !== 'string') { throw new TypeError('Comparison content must be a string') diff --git a/src/comparison/hierarchicalMarkdownComparisonModel.ts b/src/comparison/hierarchicalMarkdownComparisonModel.ts index 3f9122bfaa0..227dcce695e 100644 --- a/src/comparison/hierarchicalMarkdownComparisonModel.ts +++ b/src/comparison/hierarchicalMarkdownComparisonModel.ts @@ -79,6 +79,15 @@ const ABSENT_CELL_TOKEN = '\u0006' const profileCache = new WeakMap() +/** + * Build a recursively frozen model in original document coordinates without modifying either document. + * Schema normalization must preserve content and positions. Ambiguous regions stay coarse; + * exceeding the descriptor limit throws ComparisonModelLimitError. + * + * @param originalBefore Earlier document. + * @param originalAfter Later document, whose schema is used for comparison. + * @param options Optional descriptor budget. + */ export function createHierarchicalMarkdownComparisonModel(originalBefore: Node, originalAfter: Node, options: ComparisonModelOptions = {}): Model { const comparisonBefore = normalizeSchema(originalBefore, originalAfter) const originalBeforeIndex = indexDocument(originalBefore) diff --git a/src/comparison/markdownComparison.ts b/src/comparison/markdownComparison.ts index ae0e19a4a05..c24f1e4b5cc 100644 --- a/src/comparison/markdownComparison.ts +++ b/src/comparison/markdownComparison.ts @@ -51,6 +51,12 @@ export class ComparisonProjectionError extends Error { } } +/** + * Check each snapshot against the rendered comparison character, line and line-length limits. + * + * @param before Earlier Markdown snapshot. + * @param after Later Markdown snapshot. + */ export function exceedsRenderedComparisonLimit(before: string, after: string): boolean { return [before, after].some((content) => { if (content.length > LIMITS.maximumCharactersPerSnapshot) { @@ -74,6 +80,15 @@ export function exceedsRenderedComparisonLimit(before: string, after: string): b } let pluginId = 0 +/** + * Create an independently keyed decoration plugin for one unchanged comparison document. + * Projection errors propagate; document edits clear the decorations. + * + * @param descriptors Original-coordinate change descriptors. + * @param side Document side to decorate. + * @param markerLabel Localized accessible label for change markers. + * @param initialState Active and selected descriptor IDs; all descriptors start active by default. + */ export function createComparisonDecorationPlugin(descriptors: readonly Descriptor[], side: Side, markerLabel: string, initialState: State = { activeIds: descriptors.map(({ id }) => id), currentIds: [] }) { const key = new ProseMirrorPluginKey(`markdown-comparison-${side}-${pluginId++}`) const plugin = new Plugin({ @@ -103,6 +118,13 @@ export function createComparisonDecorationPlugin(descriptors: readonly Descripto return { key, plugin } } +/** + * Dispatch a metadata-only transaction to update active and current decorations. + * + * @param editor Mounted editor receiving the update. + * @param key Key returned with its comparison plugin. + * @param state Active and selected descriptor IDs. + */ export function setComparisonDecorationState(editor: Editor, key: ComparisonDecorationKey, state: State) { editor.view.dispatch(editor.state.tr.setMeta(key, state)) } @@ -127,6 +149,14 @@ function normalizeDecorationState(descriptors: readonly Descriptor[], state: Sta } } +/** + * Project nonempty descriptor ranges onto original document nodes without altering content. + * Empty ranges need no decoration; unprojectable ranges throw ComparisonProjectionError. + * + * @param doc Document to decorate. + * @param descriptors Change descriptors in original coordinates. + * @param side Descriptor side to project. + */ export function prepareComparisonDecorations(doc: Node, descriptors: readonly Descriptor[], side: Side) { const index = createComparisonDocumentIndex(doc) return descriptors.flatMap((descriptor): Prepared[] => { diff --git a/src/comparison/markdownComparisonClassification.ts b/src/comparison/markdownComparisonClassification.ts index 42f48c1902e..c34d0b88707 100644 --- a/src/comparison/markdownComparisonClassification.ts +++ b/src/comparison/markdownComparisonClassification.ts @@ -100,6 +100,11 @@ const meaningfulAttributes: Record> = { tableCell: { align: 'table-alignment', colspan: 'table-span', rowspan: 'table-span' }, tableHeader: { align: 'table-alignment', colspan: 'table-span', rowspan: 'table-span' }, } +/** + * Serialize values deterministically with code-unit-sorted object keys, preserving array order. + * + * @param value Acyclic JSON-like value to encode. + */ function serialize(value: unknown): string { if (value === null || typeof value !== 'object') { return JSON.stringify(value) ?? String(value) @@ -114,6 +119,11 @@ function serialize(value: unknown): string { } export { serialize as stableSerialize } +/** + * Cache the stable serialization of an immutable node. Callers still verify equality when confirming moves. + * + * @param node Original ProseMirror node. + */ function fingerprint(node: Node) { let value = fingerprints.get(node) if (value === undefined) { @@ -124,6 +134,12 @@ function fingerprint(node: Node) { } export { fingerprint as nodeFingerprint } +/** + * Compare strings by UTF-16 code units independently of locale. + * + * @param a First string. + * @param b Second string. + */ export function compareCodeUnits(a: string, b: string) { return a < b ? -1 : a > b ? 1 : 0 } @@ -153,6 +169,19 @@ export const semanticTokenEncoder = { return a === b }, } +/** + * Classify a bounded original-coordinate range pair, including context and previews. + * The returned descriptor has an empty ID for the model builder to assign. + * + * @param beforeDoc Original Before document. + * @param afterDoc Original After document. + * @param before Requested Before range. + * @param after Requested After range. + * @param beforeRoots Indexed Before subtrees. + * @param afterRoots Indexed After subtrees. + * @param detail Inline or block projection detail. + * @param excluded Attribute changes already accounted for elsewhere. + */ export function classifyComparisonDescriptor(beforeDoc: Node, afterDoc: Node, before: Range, after: Range, beforeRoots: readonly Location[], afterRoots: readonly Location[], detail: Descriptor['detail'] = 'inline', excluded: readonly Attr[] = []): Descriptor { const safeBefore = boundedRange(before, beforeDoc.content.size) const safeAfter = boundedRange(after, afterDoc.content.size) @@ -192,6 +221,16 @@ export function classifyComparisonDescriptor(beforeDoc: Node, afterDoc: Node, be signals: deduplicateSignals(signals), } } +/** + * Describe direct node markup changes, returning null when type, attributes and marks match. + * + * @param beforeDoc Original Before document. + * @param afterDoc Original After document. + * @param before Requested Before range, clamped to the document. + * @param after Requested After range, clamped to the document. + * @param beforeRoot Indexed Before node. + * @param afterRoot Indexed After node. + */ export function classifyNodeMarkupDescriptor(beforeDoc: Node, afterDoc: Node, before: Range, after: Range, beforeRoot: Location, afterRoot: Location): Descriptor | null { const safeBefore = boundedRange(before, beforeDoc.content.size) const safeAfter = boundedRange(after, afterDoc.content.size) @@ -502,6 +541,12 @@ function coversRange(location: Location, code: ContextCode, range: Range | undef function normalizePreview(value: string) { return value.replace(/\s+/gu, ' ').trim() } +/** + * Truncate with an ellipsis at a grapheme boundary, falling back to code points without Intl.Segmenter. + * + * @param value Preview text. + * @param maximum Nonnegative whole number of segments to keep. + */ export function truncateGraphemes(value: string, maximum: number) { let count = 0 let truncated = '' @@ -529,6 +574,12 @@ function boundedRange(range: Range, maximum: number) { function clamp(value: number, minimum: number, maximum: number) { return Math.min(Math.max(value, minimum), maximum) } +/** + * Freeze an object and its unfrozen enumerable descendants in place, returning the same value. + * Already frozen objects are not traversed; no clone or deeper TypeScript type is created. + * + * @param value Value whose object descendants should be frozen. + */ export function deepFreeze(value: T): T { if (value && typeof value === 'object' && !Object.isFrozen(value)) { Object.freeze(value) diff --git a/src/comparison/markdownComparisonMoves.ts b/src/comparison/markdownComparisonMoves.ts index c5eb8adb044..2cb5d97a6ef 100644 --- a/src/comparison/markdownComparisonMoves.ts +++ b/src/comparison/markdownComparisonMoves.ts @@ -15,6 +15,14 @@ export interface ReservedExactMovePair { } type Pair = ReservedExactMovePair +/** + * Confirm unique, equal move candidates and group pairs contiguous on both axes. + * Fingerprints repeated anywhere in either document are rejected. + * + * @param before Earlier document used to count fingerprints. + * @param after Later document used to count fingerprints. + * @param candidates Reserved exact pairs to verify. + */ export function confirmReservedExactMoves( before: Node, after: Node, diff --git a/src/comparison/markdownSourceComparison.ts b/src/comparison/markdownSourceComparison.ts index 78e52f47668..1d32c2c2752 100644 --- a/src/comparison/markdownSourceComparison.ts +++ b/src/comparison/markdownSourceComparison.ts @@ -89,6 +89,14 @@ export const SOURCE_DIFF_LIMITS = Object.freeze({ }) const LIMITS = SOURCE_DIFF_LIMITS +/** + * Compare literal source with original line numbers and line endings. + * Size or work limits return a limited result; cancellation and worker failures reject. + * + * @param before Earlier raw Markdown. + * @param after Later raw Markdown. + * @param signal Optional cancellation signal; abort terminates an active worker. + */ export async function createMarkdownSourceComparison(before: string, after: string, signal?: AbortSignal): Promise { if ( before.length + after.length > LIMITS.maximumCharacters @@ -119,6 +127,15 @@ export async function createMarkdownSourceComparison(before: string, after: stri ) } +/** + * Read a bounded page of unchanged source rows, normalizing offsets and capping the page size. + * + * @param before Original earlier source. + * @param after Original later source. + * @param gap Gap from the corresponding source model. + * @param maximumRows Requested page size, capped at the source gap limit. + * @param offset Row offset within the gap, clamped to its bounds. + */ export function materializeSourceDiffGap(before: string, after: string, gap: SourceDiffGap, maximumRows: number = LIMITS.maximumGapPageRows, offset: number = 0) { const finiteMaximum = Number.isFinite(maximumRows) ? Math.trunc(maximumRows) diff --git a/src/comparison/markdownSourceComparisonProtocol.ts b/src/comparison/markdownSourceComparisonProtocol.ts index a6e4883993b..7dd69d43504 100644 --- a/src/comparison/markdownSourceComparisonProtocol.ts +++ b/src/comparison/markdownSourceComparisonProtocol.ts @@ -16,6 +16,11 @@ export interface SourceComparisonWorkerRequest { export type SourceComparisonWorkerResponse = { status: 'ready', changes: Change[] } | { status: 'limited' } +/** + * Run the synchronous bounded line diff used by the worker, returning ready changes or a limited result. + * + * @param request Normalized source strings and the edit-length/time budgets. + */ export function compareMarkdownSourceLines(request: SourceComparisonWorkerRequest): SourceComparisonWorkerResponse { const changes = diffLines(request.before, request.after, { stripTrailingCr: false, diff --git a/src/comparison/markdownSourceDisplay.ts b/src/comparison/markdownSourceDisplay.ts index 16f799222b4..7ed6e111015 100644 --- a/src/comparison/markdownSourceDisplay.ts +++ b/src/comparison/markdownSourceDisplay.ts @@ -73,10 +73,21 @@ function renderMarkdownSource(source: string, maximumCharacters: number, maximum return { text: visible, complete: true } } +/** + * Render control and bidi characters as visible tokens without splitting surrogate pairs or tokens. + * + * @param source Literal source to display. + * @param maximumCharacters Maximum rendered UTF-16 length; excess output is omitted. + */ export function displayMarkdownSource(source: string, maximumCharacters = Number.POSITIVE_INFINITY) { return renderMarkdownSource(source, maximumCharacters).text } +/** + * Render source under input, output and per-line limits, reporting whether any content was omitted. + * + * @param source Raw source; a truncated prefix does not split a valid surrogate pair. + */ export function displayBoundedMarkdownSource(source: string) { const input = sourcePrefix(source, LIMITS.maximumInputCharactersPerSide) const visible = renderMarkdownSource( diff --git a/src/composables/useEditorMethods.ts b/src/composables/useEditorMethods.ts index 7dfab0f7f7a..01dbf17d417 100644 --- a/src/composables/useEditorMethods.ts +++ b/src/composables/useEditorMethods.ts @@ -12,6 +12,12 @@ import Markdown from '../extensions/Markdown.js' import markdownit from '../markdownit/index.js' import { isUser } from '../services/SyncService.ts' +/** + * Render Markdown as editor HTML with a trailing paragraph, or escape plain text inside a pre element. + * + * @param content Source content to render. + * @param markdown Whether to interpret the source as Markdown. + */ export function renderEditorContent(content: string, markdown: boolean) { return markdown ? markdownit.render(content) + '

' diff --git a/src/createMarkdownContentComparison.ts b/src/createMarkdownContentComparison.ts index c525e532cd9..851c3fc3c5c 100644 --- a/src/createMarkdownContentComparison.ts +++ b/src/createMarkdownContentComparison.ts @@ -24,6 +24,13 @@ export interface MarkdownContentComparisonInstance { destroy: () => void } +/** + * Replace the mount children with a comparison and return an idempotent destroy handle. + * Detached mounts are supported: readiness means initialization or fallback, not visible geometry. + * Invalid inputs reject. The loaded callback is awaited, but its errors do not remove the comparison. + * + * @param options Mount, snapshots, resource context and optional loaded/link callbacks. + */ export async function createMarkdownContentComparison(options: MarkdownContentComparisonOptions): Promise { if (!(options?.el instanceof HTMLElement)) { throw new TypeError('Comparison el must be an HTMLElement')