@stll/folio-core
Advanced tools
| import { FolioAIEditSnapshot } from "./types.js"; | ||
| import { Node } from "prosemirror-model"; | ||
| //#region src/ai-edits/blockRange.d.ts | ||
| type DocPositionRange = { | ||
| from: number; | ||
| to: number; | ||
| }; | ||
| type ResolveFolioAIBlockRangeOptions = { | ||
| blockId: string; | ||
| doc: Node; | ||
| snapshot?: FolioAIEditSnapshot | null | undefined; | ||
| }; | ||
| declare const resolveFolioAIBlockRange: ({ | ||
| blockId, | ||
| doc, | ||
| snapshot | ||
| }: ResolveFolioAIBlockRangeOptions) => DocPositionRange | null; | ||
| /** | ||
| * Clamp a `{from, to}` pair so both endpoints fit inside a document of | ||
| * `docSize` (in PM content positions). Block-boundary snapshots and stale | ||
| * range data sometimes produce a `to` one past the last inline position; | ||
| * `view.state.doc.resolve(...)` rejects that with | ||
| * "Position … out of range", and `TextSelection.between` doesn't help — it | ||
| * needs *valid* resolved positions. Clamping before resolution is the cheap | ||
| * defensive step. | ||
| * | ||
| * Order is preserved: if both endpoints exceed `docSize`, the returned | ||
| * `from` may equal `to`, yielding a cursor selection at the doc end. | ||
| */ | ||
| declare function clampRangeToDocSize(docSize: number, range: DocPositionRange): DocPositionRange; | ||
| //#endregion | ||
| export { DocPositionRange, clampRangeToDocSize, resolveFolioAIBlockRange }; |
| import { getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex } from "../types/block-id.js"; | ||
| import { createFolioAIEditSnapshot } from "./snapshot.js"; | ||
| import { findParagraphByParaId } from "../prosemirror/utils/findParagraphByParaId.js"; | ||
| //#region src/ai-edits/blockRange.ts | ||
| const resolveFolioAIBlockRange = ({ blockId, doc, snapshot }) => { | ||
| const paraId = getFolioParaIdFromBlockId(blockId); | ||
| if (paraId !== null) { | ||
| const liveRange = findParagraphByParaId(doc, paraId); | ||
| if (liveRange !== null) return { | ||
| from: liveRange.from, | ||
| to: liveRange.to | ||
| }; | ||
| } | ||
| const resolvedSnapshot = snapshot ?? createFolioAIEditSnapshot(doc); | ||
| const anchor = resolvedSnapshot.anchors[blockId] ?? resolveSequentialBlockAnchor(blockId, resolvedSnapshot); | ||
| if (!anchor) return null; | ||
| return clampRangeToDocSize(doc.content.size, anchor); | ||
| }; | ||
| /** | ||
| * Resolve a `seq-NNNN` fallback id by document position. | ||
| * | ||
| * A sequential id is only minted for a paragraph the source DOCX left | ||
| * without a `w14:paraId`. The live editor's `ParaIdAllocator` fills | ||
| * that gap with a fresh random hex paraId, so a snapshot of the live | ||
| * document keys the block by that hex and never reproduces the | ||
| * server's `seq-NNNN`: the direct anchor lookup misses, and a | ||
| * paraId-based live lookup can't match either (no node carries a | ||
| * `seq-` paraId). The seq number is the block's 1-based position in | ||
| * the same non-empty-block walk the server extractor and | ||
| * `createFolioAIEditSnapshot` share, so it indexes the snapshot's | ||
| * ordered `blocks` directly. | ||
| */ | ||
| const resolveSequentialBlockAnchor = (blockId, snapshot) => { | ||
| const index = getSequentialFolioBlockIdIndex(blockId); | ||
| if (index === null) return; | ||
| const block = snapshot.blocks.at(index - 1); | ||
| return block ? snapshot.anchors[block.id] : void 0; | ||
| }; | ||
| /** | ||
| * Clamp a `{from, to}` pair so both endpoints fit inside a document of | ||
| * `docSize` (in PM content positions). Block-boundary snapshots and stale | ||
| * range data sometimes produce a `to` one past the last inline position; | ||
| * `view.state.doc.resolve(...)` rejects that with | ||
| * "Position … out of range", and `TextSelection.between` doesn't help — it | ||
| * needs *valid* resolved positions. Clamping before resolution is the cheap | ||
| * defensive step. | ||
| * | ||
| * Order is preserved: if both endpoints exceed `docSize`, the returned | ||
| * `from` may equal `to`, yielding a cursor selection at the doc end. | ||
| */ | ||
| function clampRangeToDocSize(docSize, range) { | ||
| return { | ||
| from: Math.min(Math.max(range.from, 0), docSize), | ||
| to: Math.min(Math.max(range.to, 0), docSize) | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { clampRangeToDocSize, resolveFolioAIBlockRange }; |
| import { FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAITextRangeHandle } from "./ai-edits/types.js"; | ||
| import { FolioAIEditView } from "./ai-edits/apply.js"; | ||
| //#region src/document-operations.d.ts | ||
| declare const FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION: 1; | ||
| declare const FOLIO_DOCUMENT_OPERATION_TYPES: readonly ["replaceInBlock", "replaceRange", "commentOnRange", "formatRange", "insertAfterBlock", "insertBeforeBlock", "replaceBlock", "deleteBlock", "commentOnBlock", "insertSignatureTable"]; | ||
| declare const FOLIO_DOCUMENT_OPERATION_MODES: readonly ["direct", "tracked-changes"]; | ||
| declare const FOLIO_DOCUMENT_OPERATION_STORIES: readonly ["main"]; | ||
| declare const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS: readonly ["blockTextHash"]; | ||
| declare const FOLIO_DOCUMENT_OPERATION_BATCH_MODES: readonly ["best-effort", "atomic"]; | ||
| type FolioDocumentOperation = FolioAIEditOperation; | ||
| type FolioDocumentOperationMode = FolioAIEditApplyMode; | ||
| type FolioDocumentOperationPrecondition = FolioAIEditPrecondition; | ||
| type FolioDocumentOperationType = FolioDocumentOperation["type"]; | ||
| declare const FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE: Readonly<{ | ||
| readonly replaceInBlock: readonly ["direct", "tracked-changes"]; | ||
| readonly replaceRange: readonly ["direct", "tracked-changes"]; | ||
| readonly commentOnRange: readonly ["direct", "tracked-changes"]; | ||
| readonly formatRange: readonly ["direct"]; | ||
| readonly insertAfterBlock: readonly ["direct", "tracked-changes"]; | ||
| readonly insertBeforeBlock: readonly ["direct", "tracked-changes"]; | ||
| readonly replaceBlock: readonly ["direct", "tracked-changes"]; | ||
| readonly deleteBlock: readonly ["direct", "tracked-changes"]; | ||
| readonly commentOnBlock: readonly ["direct", "tracked-changes"]; | ||
| readonly insertSignatureTable: readonly ["direct"]; | ||
| }>; | ||
| type FolioDocumentOperationCapabilities = { | ||
| readonly version: typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION; | ||
| readonly operationTypes: typeof FOLIO_DOCUMENT_OPERATION_TYPES; | ||
| readonly modes: typeof FOLIO_DOCUMENT_OPERATION_MODES; | ||
| readonly modesByOperationType: typeof FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE; | ||
| readonly batchModes: typeof FOLIO_DOCUMENT_OPERATION_BATCH_MODES; | ||
| readonly dryRun: true; | ||
| readonly preconditions: typeof FOLIO_DOCUMENT_OPERATION_PRECONDITIONS; | ||
| readonly stories: typeof FOLIO_DOCUMENT_OPERATION_STORIES; | ||
| }; | ||
| declare const getFolioDocumentOperationCapabilities: () => FolioDocumentOperationCapabilities; | ||
| declare const isFolioDocumentOperationModeSupported: (operationType: FolioDocumentOperationType, mode: FolioDocumentOperationMode) => boolean; | ||
| declare const isSupportedFolioDocumentOperationVersion: (value: unknown) => value is typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION; | ||
| declare const UnsupportedFolioDocumentOperationVersionError_base: import("better-result").TaggedErrorClass<"UnsupportedFolioDocumentOperationVersionError", { | ||
| message: string; | ||
| receivedVersion: unknown; | ||
| }>; | ||
| declare class UnsupportedFolioDocumentOperationVersionError extends UnsupportedFolioDocumentOperationVersionError_base {} | ||
| declare const InvalidFolioDocumentOperationBatchError_base: import("better-result").TaggedErrorClass<"InvalidFolioDocumentOperationBatchError", { | ||
| message: string; | ||
| path: string; | ||
| reason: string; | ||
| }>; | ||
| declare class InvalidFolioDocumentOperationBatchError extends InvalidFolioDocumentOperationBatchError_base {} | ||
| declare const assertSupportedFolioDocumentOperationVersion: (value: unknown) => typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION; | ||
| type FolioDocumentOperationBatch = { | ||
| readonly version: typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION; | ||
| operations: FolioDocumentOperation[]; | ||
| mode?: FolioDocumentOperationMode; | ||
| atomic?: boolean; | ||
| dryRun?: boolean; | ||
| }; | ||
| declare const parseFolioDocumentOperationBatch: (value: unknown) => FolioDocumentOperationBatch; | ||
| type FolioDocumentOperationStatus = "committed" | "previewed" | "rejected"; | ||
| type FolioDocumentOperationRecovery = "refreshDocument" | "narrowMatch" | "changeMode" | "changeTarget" | "removeOperation" | "inspectBatch"; | ||
| type FolioDocumentOperationIssue = { | ||
| operationId: string; | ||
| operationIndex: number; | ||
| path: `$.operations[${number}]`; | ||
| code: FolioAIEditSkippedOperation["reason"]; | ||
| retryable: boolean; | ||
| recovery: FolioDocumentOperationRecovery; | ||
| }; | ||
| /** One typed target affected by a successfully applied document operation. */ | ||
| type FolioDocumentOperationAffectedTarget = { | ||
| type: "block"; | ||
| story: "main"; | ||
| blockId: string; | ||
| effect: "updated" | "deleted" | "commented"; | ||
| } | { | ||
| type: "textRange"; | ||
| range: FolioAITextRangeHandle; | ||
| effect: "formatted" | "commented"; | ||
| } | { | ||
| type: "insertion"; | ||
| story: "main"; | ||
| anchorBlockId: string; | ||
| position: "before" | "after"; | ||
| content: "block" | "signatureTable"; | ||
| } | { | ||
| type: "comment"; | ||
| commentId: number; | ||
| }; | ||
| /** Input-ordered effect receipt for one successfully applied operation. */ | ||
| type FolioDocumentOperationReceipt = { | ||
| operationId: string; | ||
| operationIndex: number; | ||
| affected: FolioDocumentOperationAffectedTarget[]; | ||
| }; | ||
| type FolioDocumentOperationResult = { | ||
| version: typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION; | ||
| status: FolioDocumentOperationStatus; | ||
| applied: FolioAIEditAppliedOperation[]; | ||
| skipped: FolioAIEditSkippedOperation[]; | ||
| issues: FolioDocumentOperationIssue[]; /** Successful effects in input-operation order; skipped operations are omitted. */ | ||
| receipts: FolioDocumentOperationReceipt[]; | ||
| }; | ||
| declare const getFolioDocumentOperationIssues: (operations: readonly FolioDocumentOperation[], skipped: readonly FolioAIEditSkippedOperation[]) => FolioDocumentOperationIssue[]; | ||
| /** Build deterministic affected-target receipts from operations and their applied entries. */ | ||
| declare const getFolioDocumentOperationReceipts: (operations: readonly FolioDocumentOperation[], applied: readonly FolioAIEditAppliedOperation[]) => FolioDocumentOperationReceipt[]; | ||
| type ApplyFolioDocumentOperationsOptions = { | ||
| view: FolioAIEditView; | ||
| snapshot: FolioAIEditSnapshot; | ||
| batch: FolioDocumentOperationBatch; | ||
| author?: string; | ||
| createCommentId?: (text: string) => number; | ||
| }; | ||
| declare const applyFolioDocumentOperations: ({ | ||
| view, | ||
| snapshot, | ||
| batch, | ||
| author, | ||
| createCommentId | ||
| }: ApplyFolioDocumentOperationsOptions) => FolioDocumentOperationResult; | ||
| //#endregion | ||
| export { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch }; |
| import { applyFolioAIEditOperations, previewFolioAIEditOperations } from "./ai-edits/apply.js"; | ||
| import { TaggedError } from "better-result"; | ||
| //#region src/document-operations.ts | ||
| const FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION = 1; | ||
| const FOLIO_DOCUMENT_OPERATION_TYPES = Object.freeze([ | ||
| "replaceInBlock", | ||
| "replaceRange", | ||
| "commentOnRange", | ||
| "formatRange", | ||
| "insertAfterBlock", | ||
| "insertBeforeBlock", | ||
| "replaceBlock", | ||
| "deleteBlock", | ||
| "commentOnBlock", | ||
| "insertSignatureTable" | ||
| ]); | ||
| const FOLIO_DOCUMENT_OPERATION_MODES = Object.freeze(["direct", "tracked-changes"]); | ||
| const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze(["main"]); | ||
| const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS = Object.freeze(["blockTextHash"]); | ||
| const FOLIO_DOCUMENT_OPERATION_BATCH_MODES = Object.freeze(["best-effort", "atomic"]); | ||
| const DIRECT_AND_TRACKED_MODES = FOLIO_DOCUMENT_OPERATION_MODES; | ||
| const DIRECT_ONLY_MODES = Object.freeze(["direct"]); | ||
| const FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE = Object.freeze({ | ||
| replaceInBlock: DIRECT_AND_TRACKED_MODES, | ||
| replaceRange: DIRECT_AND_TRACKED_MODES, | ||
| commentOnRange: DIRECT_AND_TRACKED_MODES, | ||
| formatRange: DIRECT_ONLY_MODES, | ||
| insertAfterBlock: DIRECT_AND_TRACKED_MODES, | ||
| insertBeforeBlock: DIRECT_AND_TRACKED_MODES, | ||
| replaceBlock: DIRECT_AND_TRACKED_MODES, | ||
| deleteBlock: DIRECT_AND_TRACKED_MODES, | ||
| commentOnBlock: DIRECT_AND_TRACKED_MODES, | ||
| insertSignatureTable: DIRECT_ONLY_MODES | ||
| }); | ||
| const DOCUMENT_OPERATION_CAPABILITIES = Object.freeze({ | ||
| version: 1, | ||
| operationTypes: FOLIO_DOCUMENT_OPERATION_TYPES, | ||
| modes: FOLIO_DOCUMENT_OPERATION_MODES, | ||
| modesByOperationType: FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, | ||
| batchModes: FOLIO_DOCUMENT_OPERATION_BATCH_MODES, | ||
| dryRun: true, | ||
| preconditions: FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, | ||
| stories: FOLIO_DOCUMENT_OPERATION_STORIES | ||
| }); | ||
| const getFolioDocumentOperationCapabilities = () => DOCUMENT_OPERATION_CAPABILITIES; | ||
| const includesDocumentOperationMode = (supportedModes, mode) => supportedModes.includes(mode); | ||
| const isFolioDocumentOperationModeSupported = (operationType, mode) => { | ||
| const supportedModes = FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE[operationType]; | ||
| if (supportedModes === void 0) return false; | ||
| return includesDocumentOperationMode(supportedModes, mode); | ||
| }; | ||
| const isSupportedFolioDocumentOperationVersion = (value) => value === 1; | ||
| var UnsupportedFolioDocumentOperationVersionError = class extends TaggedError("UnsupportedFolioDocumentOperationVersionError")() {}; | ||
| var InvalidFolioDocumentOperationBatchError = class extends TaggedError("InvalidFolioDocumentOperationBatchError")() {}; | ||
| const assertSupportedFolioDocumentOperationVersion = (value) => { | ||
| if (isSupportedFolioDocumentOperationVersion(value)) return value; | ||
| throw new UnsupportedFolioDocumentOperationVersionError({ | ||
| message: "Unsupported document operation contract version.", | ||
| receivedVersion: value | ||
| }); | ||
| }; | ||
| const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value); | ||
| const invalidBatch = (path, reason) => { | ||
| throw new InvalidFolioDocumentOperationBatchError({ | ||
| message: `Invalid document operation batch at ${path}: ${reason}.`, | ||
| path, | ||
| reason | ||
| }); | ||
| }; | ||
| const assertAllowedKeys = (value, path, allowedKeys) => { | ||
| const unexpected = Object.keys(value).find((key) => !allowedKeys.includes(key)); | ||
| if (unexpected !== void 0) invalidBatch(`${path}.${unexpected}`, "unexpected property"); | ||
| }; | ||
| const readString = (value, key, path) => { | ||
| const candidate = value[key]; | ||
| if (typeof candidate === "string") return candidate; | ||
| return invalidBatch(`${path}.${key}`, "expected a string"); | ||
| }; | ||
| const readOptionalString = (value, key, path) => { | ||
| const candidate = value[key]; | ||
| if (candidate === void 0) return; | ||
| if (typeof candidate === "string") return candidate; | ||
| return invalidBatch(`${path}.${key}`, "expected a string when provided"); | ||
| }; | ||
| const readOptionalBoolean = (value, key, path) => { | ||
| const candidate = value[key]; | ||
| if (candidate === void 0) return; | ||
| if (typeof candidate === "boolean") return candidate; | ||
| return invalidBatch(`${path}.${key}`, "expected a boolean when provided"); | ||
| }; | ||
| const readNonNegativeInteger = (value, key, path) => { | ||
| const candidate = value[key]; | ||
| if (typeof candidate === "number" && Number.isInteger(candidate) && candidate >= 0) return candidate; | ||
| return invalidBatch(`${path}.${key}`, "expected a non-negative integer"); | ||
| }; | ||
| const readTextRange = (value, path) => { | ||
| const candidate = value["range"]; | ||
| const rangePath = `${path}.range`; | ||
| if (!isPlainObject(candidate)) return invalidBatch(rangePath, "expected an object"); | ||
| assertAllowedKeys(candidate, rangePath, [ | ||
| "type", | ||
| "story", | ||
| "blockId", | ||
| "startOffset", | ||
| "endOffset", | ||
| "selectedTextHash" | ||
| ]); | ||
| if (candidate["type"] !== "textRange") return invalidBatch(`${rangePath}.type`, "expected \"textRange\""); | ||
| if (candidate["story"] !== "main") return invalidBatch(`${rangePath}.story`, "expected \"main\""); | ||
| const blockId = readString(candidate, "blockId", rangePath); | ||
| if (blockId.length === 0) return invalidBatch(`${rangePath}.blockId`, "expected a non-empty string"); | ||
| const startOffset = readNonNegativeInteger(candidate, "startOffset", rangePath); | ||
| const endOffset = readNonNegativeInteger(candidate, "endOffset", rangePath); | ||
| if (endOffset <= startOffset) return invalidBatch(`${rangePath}.endOffset`, "expected a value greater than startOffset"); | ||
| const selectedTextHash = readString(candidate, "selectedTextHash", rangePath); | ||
| if (!/^h[0-9a-z]+$/.test(selectedTextHash)) return invalidBatch(`${rangePath}.selectedTextHash`, "expected a normalized text hash"); | ||
| return { | ||
| type: "textRange", | ||
| story: "main", | ||
| blockId, | ||
| startOffset, | ||
| endOffset, | ||
| selectedTextHash | ||
| }; | ||
| }; | ||
| const readInlineFormatting = (value, path) => { | ||
| const candidate = value["formatting"]; | ||
| const formattingPath = `${path}.formatting`; | ||
| if (!isPlainObject(candidate)) return invalidBatch(formattingPath, "expected an object"); | ||
| assertAllowedKeys(candidate, formattingPath, [ | ||
| "bold", | ||
| "italic", | ||
| "underline" | ||
| ]); | ||
| const bold = readOptionalBoolean(candidate, "bold", formattingPath); | ||
| const italic = readOptionalBoolean(candidate, "italic", formattingPath); | ||
| const underline = readOptionalBoolean(candidate, "underline", formattingPath); | ||
| if (bold === void 0 && italic === void 0 && underline === void 0) return invalidBatch(formattingPath, "expected at least one formatting property"); | ||
| return { | ||
| ...bold !== void 0 && { bold }, | ||
| ...italic !== void 0 && { italic }, | ||
| ...underline !== void 0 && { underline } | ||
| }; | ||
| }; | ||
| const readOptionalComment = (value, path) => { | ||
| const candidate = value["comment"]; | ||
| if (candidate === void 0) return; | ||
| if (isPlainObject(candidate)) { | ||
| assertAllowedKeys(candidate, `${path}.comment`, ["text"]); | ||
| return { text: readString(candidate, "text", `${path}.comment`) }; | ||
| } | ||
| return invalidBatch(`${path}.comment`, "expected an object when provided"); | ||
| }; | ||
| const readOptionalPrecondition = (value, path) => { | ||
| const candidate = value["precondition"]; | ||
| if (candidate === void 0) return; | ||
| if (!isPlainObject(candidate)) return invalidBatch(`${path}.precondition`, "expected an object when provided"); | ||
| const preconditionPath = `${path}.precondition`; | ||
| assertAllowedKeys(candidate, preconditionPath, ["blockTextHash"]); | ||
| const blockTextHash = readString(candidate, "blockTextHash", preconditionPath); | ||
| if (!/^h[0-9a-z]+$/.test(blockTextHash)) return invalidBatch(`${preconditionPath}.blockTextHash`, "expected a normalized block text hash"); | ||
| return { blockTextHash }; | ||
| }; | ||
| const isReviewSeverity = (value) => value === "low" || value === "medium" || value === "high"; | ||
| const readReviewMeta = (value, path) => { | ||
| const severity = value["severity"]; | ||
| if (severity !== void 0 && !isReviewSeverity(severity)) return invalidBatch(`${path}.severity`, "expected \"low\", \"medium\", or \"high\" when provided"); | ||
| const area = readOptionalString(value, "area", path); | ||
| return { | ||
| ...severity !== void 0 && { severity }, | ||
| ...area !== void 0 && { area } | ||
| }; | ||
| }; | ||
| const COMMON_OPERATION_KEYS = [ | ||
| "id", | ||
| "type", | ||
| "blockId", | ||
| "severity", | ||
| "area", | ||
| "precondition" | ||
| ]; | ||
| const parseSignatureParties = (value, path) => { | ||
| const parties = value["parties"]; | ||
| if (!Array.isArray(parties)) return invalidBatch(`${path}.parties`, "expected an array"); | ||
| return parties.map((party, index) => { | ||
| const partyPath = `${path}.parties[${index}]`; | ||
| if (!isPlainObject(party)) return invalidBatch(partyPath, "expected an object"); | ||
| assertAllowedKeys(party, partyPath, [ | ||
| "name", | ||
| "signatory", | ||
| "title" | ||
| ]); | ||
| const signatory = readOptionalString(party, "signatory", partyPath); | ||
| const title = readOptionalString(party, "title", partyPath); | ||
| const parsedParty = { name: readString(party, "name", partyPath) }; | ||
| if (signatory !== void 0) parsedParty.signatory = signatory; | ||
| if (title !== void 0) parsedParty.title = title; | ||
| return parsedParty; | ||
| }); | ||
| }; | ||
| const parseDocumentOperation = (value, index) => { | ||
| const path = `$.operations[${index}]`; | ||
| if (!isPlainObject(value)) return invalidBatch(path, "expected an object"); | ||
| const id = readString(value, "id", path); | ||
| const type = readString(value, "type", path); | ||
| const reviewMeta = readReviewMeta(value, path); | ||
| const precondition = readOptionalPrecondition(value, path); | ||
| const comment = readOptionalComment(value, path); | ||
| const operationMeta = { | ||
| ...reviewMeta, | ||
| ...precondition !== void 0 && { precondition } | ||
| }; | ||
| if (type === "replaceRange") { | ||
| assertAllowedKeys(value, path, [ | ||
| "id", | ||
| "type", | ||
| "range", | ||
| "replace", | ||
| "comment", | ||
| "severity", | ||
| "area", | ||
| "precondition" | ||
| ]); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| range: readTextRange(value, path), | ||
| replace: readString(value, "replace", path), | ||
| ...comment !== void 0 && { comment } | ||
| }; | ||
| } | ||
| if (type === "commentOnRange") { | ||
| assertAllowedKeys(value, path, [ | ||
| "id", | ||
| "type", | ||
| "range", | ||
| "comment", | ||
| "severity", | ||
| "area", | ||
| "precondition" | ||
| ]); | ||
| if (comment === void 0) return invalidBatch(`${path}.comment`, "expected an object"); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| range: readTextRange(value, path), | ||
| comment | ||
| }; | ||
| } | ||
| if (type === "formatRange") { | ||
| assertAllowedKeys(value, path, [ | ||
| "id", | ||
| "type", | ||
| "range", | ||
| "formatting", | ||
| "severity", | ||
| "area", | ||
| "precondition" | ||
| ]); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| range: readTextRange(value, path), | ||
| formatting: readInlineFormatting(value, path) | ||
| }; | ||
| } | ||
| const blockId = readString(value, "blockId", path); | ||
| if (type === "replaceInBlock") { | ||
| assertAllowedKeys(value, path, [ | ||
| ...COMMON_OPERATION_KEYS, | ||
| "find", | ||
| "replace", | ||
| "comment" | ||
| ]); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| blockId, | ||
| find: readString(value, "find", path), | ||
| replace: readString(value, "replace", path), | ||
| ...comment !== void 0 && { comment } | ||
| }; | ||
| } | ||
| if (type === "insertAfterBlock" || type === "insertBeforeBlock") { | ||
| assertAllowedKeys(value, path, [ | ||
| ...COMMON_OPERATION_KEYS, | ||
| "text", | ||
| "inheritFormatting", | ||
| "pageBreakBefore", | ||
| "styleId", | ||
| "comment" | ||
| ]); | ||
| const inheritFormatting = readOptionalBoolean(value, "inheritFormatting", path); | ||
| const pageBreakBefore = readOptionalBoolean(value, "pageBreakBefore", path); | ||
| const styleId = readOptionalString(value, "styleId", path); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| blockId, | ||
| text: readString(value, "text", path), | ||
| ...inheritFormatting !== void 0 && { inheritFormatting }, | ||
| ...pageBreakBefore !== void 0 && { pageBreakBefore }, | ||
| ...styleId !== void 0 && { styleId }, | ||
| ...comment !== void 0 && { comment } | ||
| }; | ||
| } | ||
| if (type === "replaceBlock") { | ||
| assertAllowedKeys(value, path, [ | ||
| ...COMMON_OPERATION_KEYS, | ||
| "text", | ||
| "preserveFormatting", | ||
| "styleId", | ||
| "comment" | ||
| ]); | ||
| const preserveFormatting = readOptionalBoolean(value, "preserveFormatting", path); | ||
| const styleId = readOptionalString(value, "styleId", path); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| blockId, | ||
| text: readString(value, "text", path), | ||
| ...preserveFormatting !== void 0 && { preserveFormatting }, | ||
| ...styleId !== void 0 && { styleId }, | ||
| ...comment !== void 0 && { comment } | ||
| }; | ||
| } | ||
| if (type === "deleteBlock") { | ||
| assertAllowedKeys(value, path, [...COMMON_OPERATION_KEYS, "comment"]); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| blockId, | ||
| ...comment !== void 0 && { comment } | ||
| }; | ||
| } | ||
| if (type === "commentOnBlock") { | ||
| assertAllowedKeys(value, path, [ | ||
| ...COMMON_OPERATION_KEYS, | ||
| "quote", | ||
| "comment" | ||
| ]); | ||
| if (comment === void 0) return invalidBatch(`${path}.comment`, "expected an object"); | ||
| const quote = readOptionalString(value, "quote", path); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| blockId, | ||
| ...quote !== void 0 && { quote }, | ||
| comment | ||
| }; | ||
| } | ||
| if (type === "insertSignatureTable") { | ||
| assertAllowedKeys(value, path, [ | ||
| ...COMMON_OPERATION_KEYS, | ||
| "position", | ||
| "parties", | ||
| "comment" | ||
| ]); | ||
| const position = value["position"]; | ||
| if (position !== void 0 && position !== "after" && position !== "before") return invalidBatch(`${path}.position`, "expected \"after\" or \"before\" when provided"); | ||
| return { | ||
| ...operationMeta, | ||
| id, | ||
| type, | ||
| blockId, | ||
| ...position !== void 0 && { position }, | ||
| parties: parseSignatureParties(value, path), | ||
| ...comment !== void 0 && { comment } | ||
| }; | ||
| } | ||
| return invalidBatch(`${path}.type`, `unsupported operation type "${type}"`); | ||
| }; | ||
| const parseFolioDocumentOperationBatch = (value) => { | ||
| if (!isPlainObject(value)) return invalidBatch("$", "expected an object"); | ||
| assertAllowedKeys(value, "$", [ | ||
| "version", | ||
| "operations", | ||
| "mode", | ||
| "atomic", | ||
| "dryRun" | ||
| ]); | ||
| const version = assertSupportedFolioDocumentOperationVersion(value["version"]); | ||
| const operations = value["operations"]; | ||
| if (!Array.isArray(operations)) return invalidBatch("$.operations", "expected an array"); | ||
| const mode = value["mode"]; | ||
| if (mode !== void 0 && mode !== "direct" && mode !== "tracked-changes") return invalidBatch("$.mode", "expected \"direct\" or \"tracked-changes\" when provided"); | ||
| const atomic = readOptionalBoolean(value, "atomic", "$"); | ||
| const dryRun = readOptionalBoolean(value, "dryRun", "$"); | ||
| const parsedOperations = operations.map(parseDocumentOperation); | ||
| const operationIds = /* @__PURE__ */ new Set(); | ||
| for (const [index, operation] of parsedOperations.entries()) { | ||
| if (operationIds.has(operation.id)) return invalidBatch(`$.operations[${index}].id`, "expected a unique operation id"); | ||
| operationIds.add(operation.id); | ||
| } | ||
| return { | ||
| version, | ||
| operations: parsedOperations, | ||
| ...mode !== void 0 && { mode }, | ||
| ...atomic !== void 0 && { atomic }, | ||
| ...dryRun !== void 0 && { dryRun } | ||
| }; | ||
| }; | ||
| const recoveryByReason = { | ||
| missingBlock: "refreshDocument", | ||
| changedBlock: "refreshDocument", | ||
| ambiguousFind: "narrowMatch", | ||
| missingFind: "refreshDocument", | ||
| unsupportedBlock: "changeTarget", | ||
| unsupportedMode: "changeMode", | ||
| atomicBatchRejected: "inspectBatch", | ||
| preconditionFailed: "refreshDocument", | ||
| staleRange: "refreshDocument", | ||
| emptyOperation: "removeOperation", | ||
| noopOperation: "removeOperation" | ||
| }; | ||
| const getFolioDocumentOperationIssues = (operations, skipped) => { | ||
| const indexById = /* @__PURE__ */ new Map(); | ||
| operations.forEach(({ id }, index) => { | ||
| indexById.set(id, index); | ||
| }); | ||
| return skipped.map(({ id, reason }) => { | ||
| const operationIndex = indexById.get(id) ?? -1; | ||
| return { | ||
| operationId: id, | ||
| operationIndex, | ||
| path: `$.operations[${operationIndex}]`, | ||
| code: reason, | ||
| retryable: reason !== "emptyOperation" && reason !== "noopOperation", | ||
| recovery: recoveryByReason[reason] | ||
| }; | ||
| }); | ||
| }; | ||
| const getPrimaryAffectedTarget = (operation) => { | ||
| switch (operation.type) { | ||
| case "replaceInBlock": | ||
| case "replaceBlock": return { | ||
| type: "block", | ||
| story: "main", | ||
| blockId: operation.blockId, | ||
| effect: "updated" | ||
| }; | ||
| case "replaceRange": return { | ||
| type: "block", | ||
| story: "main", | ||
| blockId: operation.range.blockId, | ||
| effect: "updated" | ||
| }; | ||
| case "commentOnRange": return { | ||
| type: "textRange", | ||
| range: operation.range, | ||
| effect: "commented" | ||
| }; | ||
| case "formatRange": return { | ||
| type: "textRange", | ||
| range: operation.range, | ||
| effect: "formatted" | ||
| }; | ||
| case "insertAfterBlock": | ||
| case "insertBeforeBlock": return { | ||
| type: "insertion", | ||
| story: "main", | ||
| anchorBlockId: operation.blockId, | ||
| position: operation.type === "insertBeforeBlock" ? "before" : "after", | ||
| content: "block" | ||
| }; | ||
| case "deleteBlock": return { | ||
| type: "block", | ||
| story: "main", | ||
| blockId: operation.blockId, | ||
| effect: "deleted" | ||
| }; | ||
| case "commentOnBlock": return { | ||
| type: "block", | ||
| story: "main", | ||
| blockId: operation.blockId, | ||
| effect: "commented" | ||
| }; | ||
| case "insertSignatureTable": return { | ||
| type: "insertion", | ||
| story: "main", | ||
| anchorBlockId: operation.blockId, | ||
| position: operation.position ?? "after", | ||
| content: "signatureTable" | ||
| }; | ||
| } | ||
| }; | ||
| /** Build deterministic affected-target receipts from operations and their applied entries. */ | ||
| const getFolioDocumentOperationReceipts = (operations, applied) => { | ||
| const appliedById = /* @__PURE__ */ new Map(); | ||
| applied.forEach((operation) => { | ||
| appliedById.set(operation.id, operation); | ||
| }); | ||
| const receipts = []; | ||
| operations.forEach((operation, operationIndex) => { | ||
| const appliedOperation = appliedById.get(operation.id); | ||
| if (!appliedOperation) return; | ||
| const affected = [getPrimaryAffectedTarget(operation)]; | ||
| if (appliedOperation.commentId !== void 0) affected.push({ | ||
| type: "comment", | ||
| commentId: appliedOperation.commentId | ||
| }); | ||
| receipts.push({ | ||
| operationId: operation.id, | ||
| operationIndex, | ||
| affected | ||
| }); | ||
| }); | ||
| return receipts; | ||
| }; | ||
| const applyFolioDocumentOperations = ({ view, snapshot, batch, author, createCommentId }) => { | ||
| const parsedBatch = parseFolioDocumentOperationBatch(batch); | ||
| const apply = ({ targetView, targetCreateCommentId = createCommentId, preview = false }) => { | ||
| return (preview ? previewFolioAIEditOperations : applyFolioAIEditOperations)({ | ||
| view: targetView, | ||
| snapshot, | ||
| operations: parsedBatch.operations, | ||
| mode: parsedBatch.mode ?? "tracked-changes", | ||
| ...author !== void 0 && { author }, | ||
| ...targetCreateCommentId !== void 0 && { createCommentId: targetCreateCommentId } | ||
| }); | ||
| }; | ||
| const preview = () => apply({ | ||
| targetView: view, | ||
| preview: true | ||
| }); | ||
| const atomicResult = (previewResult, status) => { | ||
| const skippedById = new Map(previewResult.skipped.map((operation) => [operation.id, operation])); | ||
| const skipped = parsedBatch.operations.map(({ id }) => skippedById.get(id) ?? { | ||
| id, | ||
| reason: "atomicBatchRejected" | ||
| }); | ||
| return { | ||
| version: 1, | ||
| status, | ||
| applied: [], | ||
| skipped, | ||
| issues: getFolioDocumentOperationIssues(parsedBatch.operations, skipped), | ||
| receipts: [] | ||
| }; | ||
| }; | ||
| if (parsedBatch.dryRun === true) { | ||
| const previewResult = preview(); | ||
| if (parsedBatch.atomic === true && previewResult.skipped.length > 0) return atomicResult(previewResult, "previewed"); | ||
| return { | ||
| version: 1, | ||
| status: "previewed", | ||
| applied: previewResult.applied.map(({ id }) => ({ id })), | ||
| skipped: previewResult.skipped, | ||
| issues: getFolioDocumentOperationIssues(parsedBatch.operations, previewResult.skipped), | ||
| receipts: getFolioDocumentOperationReceipts(parsedBatch.operations, previewResult.applied) | ||
| }; | ||
| } | ||
| if (parsedBatch.atomic === true) { | ||
| const previewResult = preview(); | ||
| if (previewResult.skipped.length > 0) return atomicResult(previewResult, "rejected"); | ||
| } | ||
| const result = apply({ targetView: view }); | ||
| return { | ||
| version: 1, | ||
| status: "committed", | ||
| ...result, | ||
| issues: getFolioDocumentOperationIssues(parsedBatch.operations, result.skipped), | ||
| receipts: getFolioDocumentOperationReceipts(parsedBatch.operations, result.applied) | ||
| }; | ||
| }; | ||
| //#endregion | ||
| export { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch }; |
| import { document_d_exports } from "../types/document.js"; | ||
| import { XmlElement } from "./xmlParser.js"; | ||
| //#region src/docx/groupDrawingParser.d.ts | ||
| /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */ | ||
| declare const parseGroupDrawing: (drawing: XmlElement) => document_d_exports.Image | null; | ||
| //#endregion | ||
| export { parseGroupDrawing }; |
| import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js"; | ||
| import { emuToPixels } from "../utils/units.js"; | ||
| import { parseImage } from "./imageParser.js"; | ||
| //#region src/docx/groupDrawingParser.ts | ||
| const HEX_COLOR = /^[0-9A-Fa-f]{6}$/u; | ||
| const DEFAULT_TEXT_COLOR = "000000"; | ||
| const DEFAULT_FONT_HALF_POINTS = 22; | ||
| const DEFAULT_LINE_WIDTH_EMU = 9525; | ||
| const HALF_POINT_TO_EMU = 6350; | ||
| const MAX_GROUP_SHAPES = 256; | ||
| const MAX_PATH_COMMANDS = 1e4; | ||
| const MAX_TEXT_CHARACTERS = 2e4; | ||
| const MAX_SVG_CHARACTERS = 1e6; | ||
| const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'"); | ||
| const numericAttr = (element, name) => { | ||
| const direct = parseNumericAttribute(element, null, name); | ||
| if (direct !== void 0) return direct; | ||
| const wordValue = getAttribute(element, "w", name); | ||
| if (!wordValue) return 0; | ||
| const parsed = Number.parseInt(wordValue, 10); | ||
| return Number.isNaN(parsed) ? 0 : parsed; | ||
| }; | ||
| const childTransform = (wsp) => { | ||
| const xfrm = findChildByLocalName(findChildByLocalName(wsp, "spPr"), "xfrm"); | ||
| const off = findChildByLocalName(xfrm, "off"); | ||
| const ext = findChildByLocalName(xfrm, "ext"); | ||
| return { | ||
| x: numericAttr(off, "x"), | ||
| y: numericAttr(off, "y"), | ||
| width: numericAttr(ext, "cx"), | ||
| height: numericAttr(ext, "cy") | ||
| }; | ||
| }; | ||
| const colorFrom = (parent, fallback) => { | ||
| if (parent && getLocalName(parent.name ?? "") === "color") { | ||
| const value = getAttribute(parent, "w", "val"); | ||
| if (value && HEX_COLOR.test(value)) return value.toUpperCase(); | ||
| } | ||
| const value = getAttribute(findChildByLocalName(findChildByLocalName(parent, "solidFill"), "srgbClr"), null, "val"); | ||
| if (value && HEX_COLOR.test(value)) return value.toUpperCase(); | ||
| return fallback; | ||
| }; | ||
| const wrapLine = (line, maxCharacters) => { | ||
| const words = line.trim().split(/\s+/u); | ||
| const lines = []; | ||
| let current = ""; | ||
| for (const word of words) { | ||
| const candidate = current ? `${current} ${word}` : word; | ||
| if (current && candidate.length > maxCharacters) { | ||
| lines.push(current); | ||
| current = word; | ||
| } else current = candidate; | ||
| } | ||
| if (current) lines.push(current); | ||
| return lines; | ||
| }; | ||
| const pathData = (path) => { | ||
| const commands = []; | ||
| for (const command of path.elements ?? []) { | ||
| if (commands.length >= MAX_PATH_COMMANDS) break; | ||
| if (command.type !== "element") continue; | ||
| const point = findChildByLocalName(command, "pt"); | ||
| const x = numericAttr(point, "x"); | ||
| const y = numericAttr(point, "y"); | ||
| const name = command.name?.split(":").at(-1); | ||
| if (name === "moveTo") commands.push(`M ${x} ${y}`); | ||
| else if (name === "lnTo") commands.push(`L ${x} ${y}`); | ||
| else if (name === "quadBezTo") { | ||
| const points = findChildrenByLocalName(command, "pt"); | ||
| if (points.length >= 2) commands.push(`Q ${numericAttr(points[0] ?? null, "x")} ${numericAttr(points[0] ?? null, "y")} ${numericAttr(points[1] ?? null, "x")} ${numericAttr(points[1] ?? null, "y")}`); | ||
| } else if (name === "cubicBezTo") { | ||
| const points = findChildrenByLocalName(command, "pt"); | ||
| if (points.length >= 3) commands.push(`C ${numericAttr(points[0] ?? null, "x")} ${numericAttr(points[0] ?? null, "y")} ${numericAttr(points[1] ?? null, "x")} ${numericAttr(points[1] ?? null, "y")} ${numericAttr(points[2] ?? null, "x")} ${numericAttr(points[2] ?? null, "y")}`); | ||
| } else if (name === "close") commands.push("Z"); | ||
| } | ||
| return commands.join(" "); | ||
| }; | ||
| const renderGeometry = (wsp) => { | ||
| const { x, y, width, height } = childTransform(wsp); | ||
| if (width <= 0 || height <= 0) return ""; | ||
| const spPr = findChildByLocalName(wsp, "spPr"); | ||
| const fill = colorFrom(spPr, "none"); | ||
| const line = findChildByLocalName(spPr, "ln"); | ||
| const stroke = colorFrom(line, "none"); | ||
| const strokeWidth = line ? parseNumericAttribute(line, null, "w") ?? DEFAULT_LINE_WIDTH_EMU : 0; | ||
| const paths = findAllDeep(findChildByLocalName(spPr, "custGeom"), "a", "path"); | ||
| if (paths.length === 0) return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`; | ||
| return paths.map((path) => { | ||
| const viewWidth = numericAttr(path, "w") || width; | ||
| const viewHeight = numericAttr(path, "h") || height; | ||
| const d = pathData(path); | ||
| if (!d) return ""; | ||
| return `<path d="${d}" transform="translate(${x} ${y}) scale(${width / viewWidth} ${height / viewHeight})" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`; | ||
| }).join(""); | ||
| }; | ||
| const renderTextBox = (wsp) => { | ||
| const { x, y, width, height } = childTransform(wsp); | ||
| if (width <= 0 || height <= 0) return ""; | ||
| const paragraphs = findAllDeep(wsp, "w", "p"); | ||
| const firstSize = findAllDeep(wsp, "w", "sz").at(0); | ||
| const halfPoints = numericAttr(firstSize ?? null, "val") || DEFAULT_FONT_HALF_POINTS; | ||
| const color = colorFrom(findAllDeep(wsp, "w", "color").at(0) ?? null, DEFAULT_TEXT_COLOR); | ||
| const fontSize = halfPoints * HALF_POINT_TO_EMU; | ||
| const maxCharacters = Math.max(1, Math.floor(width / (fontSize * .38))); | ||
| const lines = paragraphs.flatMap((paragraph) => wrapLine(getTextContent(paragraph).slice(0, MAX_TEXT_CHARACTERS), maxCharacters)).map(escapeXml); | ||
| if (lines.length === 0) return ""; | ||
| const lineHeight = fontSize * 1.15; | ||
| const svgFontSize = 1e3; | ||
| const scale = fontSize / svgFontSize; | ||
| const lineStep = lineHeight / fontSize * svgFontSize; | ||
| return `<text x="0" y="${svgFontSize}" transform="translate(${x} ${y}) scale(${scale})" font-family="Arial, sans-serif" font-size="${svgFontSize}" fill="#${color}">${lines.map((line, index) => `<tspan x="0" dy="${index === 0 ? 0 : lineStep}">${line}</tspan>`).join("")}</text>`; | ||
| }; | ||
| const createSvg = (group, width, height) => { | ||
| const content = findChildrenByLocalName(group, "wsp").slice(0, MAX_GROUP_SHAPES).map((wsp) => findChildByLocalName(wsp, "txbx") ? renderTextBox(wsp) : renderGeometry(wsp)).join(""); | ||
| return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`; | ||
| }; | ||
| /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */ | ||
| const parseGroupDrawing = (drawing) => { | ||
| const group = findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp"); | ||
| if (!group) return null; | ||
| const image = parseImage(drawing, void 0, void 0); | ||
| if (!image || image.size.width <= 0 || image.size.height <= 0) return null; | ||
| const svg = createSvg(group, image.size.width, image.size.height); | ||
| if (svg.length > MAX_SVG_CHARACTERS) return null; | ||
| image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; | ||
| image.mimeType = "image/svg+xml"; | ||
| image.filename = "wordprocessing-group.svg"; | ||
| return image; | ||
| }; | ||
| //#endregion | ||
| export { parseGroupDrawing }; |
| import { TextBoxBlock } from "./types.js"; | ||
| //#region src/layout-engine/textBoxGroup.d.ts | ||
| declare const setTextBoxGroupId: (textBox: TextBoxBlock, groupId: string) => void; | ||
| declare const getTextBoxGroupId: (textBox: TextBoxBlock) => string | undefined; | ||
| //#endregion | ||
| export { getTextBoxGroupId, setTextBoxGroupId }; |
| //#region src/layout-engine/textBoxGroup.ts | ||
| const textBoxGroupIds = /* @__PURE__ */ new WeakMap(); | ||
| const setTextBoxGroupId = (textBox, groupId) => { | ||
| textBoxGroupIds.set(textBox, groupId); | ||
| }; | ||
| const getTextBoxGroupId = (textBox) => textBoxGroupIds.get(textBox); | ||
| //#endregion | ||
| export { getTextBoxGroupId, setTextBoxGroupId }; |
| import { Layout } from "../layout-engine/types.js"; | ||
| import { Node } from "prosemirror-model"; | ||
| //#region src/paged-layout/pageText.d.ts | ||
| /** | ||
| * The plain text of a single rendered page (1-based), read by joining each of | ||
| * the page's layout fragments' `[pmStart, pmEnd)` document text with a | ||
| * newline. Fragments with no `pmStart`/`pmEnd` (e.g. a page-shell placeholder) | ||
| * are skipped. Returns `null` when the layout hasn't been computed yet or the | ||
| * page number is out of range. | ||
| */ | ||
| declare const getPageTextFromLayout: (layout: Layout | null, doc: Node, page: number) => string | null; | ||
| //#endregion | ||
| export { getPageTextFromLayout }; |
| //#region src/paged-layout/pageText.ts | ||
| /** | ||
| * The plain text of a single rendered page (1-based), read by joining each of | ||
| * the page's layout fragments' `[pmStart, pmEnd)` document text with a | ||
| * newline. Fragments with no `pmStart`/`pmEnd` (e.g. a page-shell placeholder) | ||
| * are skipped. Returns `null` when the layout hasn't been computed yet or the | ||
| * page number is out of range. | ||
| */ | ||
| const getPageTextFromLayout = (layout, doc, page) => { | ||
| if (!layout || !Number.isInteger(page) || page < 1) return null; | ||
| const layoutPage = layout.pages[page - 1]; | ||
| if (!layoutPage) return null; | ||
| const fragmentTexts = []; | ||
| for (const fragment of layoutPage.fragments) { | ||
| if (typeof fragment.pmStart !== "number" || typeof fragment.pmEnd !== "number") continue; | ||
| fragmentTexts.push(doc.textBetween(fragment.pmStart, fragment.pmEnd, "\n")); | ||
| } | ||
| return fragmentTexts.join("\n"); | ||
| }; | ||
| //#endregion | ||
| export { getPageTextFromLayout }; |
| import { document_d_exports } from "../../types/document.js"; | ||
| //#region src/prosemirror/commands/propertyChangeScope.d.ts | ||
| type AttrPatch = Record<string, unknown>; | ||
| /** | ||
| * Paragraph attrs governed by a `w:pPrChange` — the attrs | ||
| * `paragraphFormattingToAttrs` (conversion/toProseDoc.ts) can produce from a | ||
| * parsed `CT_PPrBase` (ECMA-376 §17.13.5.29). Rejecting a pPrChange restores | ||
| * the stored old pPr wholesale for exactly these keys: a key absent from the | ||
| * stored record resets to the schema default (`null`). | ||
| * | ||
| * Deliberately OUT of scope (preserved across a reject): | ||
| * - identity/structure: `paraId`, `textId`, `sectionBreakType`, | ||
| * `_sectionProperties`, `_propertyChanges`, `pPrMark`, `bookmarks`, | ||
| * `_emptyHyperlinks`, `renderedPageBreakBefore` | ||
| * - paragraph-mark run properties — `pPr/rPr` is CT_PPr-only, not part of the | ||
| * CT_PPrBase payload a pPrChange stores: `defaultTextFormatting`, | ||
| * `runInWithNext` (`w:specVanish` lives in that rPr) | ||
| * - load-time style/numbering bookkeeping the command layer cannot recompute | ||
| * without a style resolver: `numPrFromStyle`, the `list*` rendering attrs, | ||
| * `spacingFromDocDefaults` | ||
| */ | ||
| declare const PPR_CHANGE_SCOPED_ATTR_KEYS: readonly ["styleId", "numPr", "alignment", "spaceBefore", "spaceAfter", "lineSpacing", "lineSpacingRule", "spacingExplicit", "indentLeft", "indentRight", "indentFirstLine", "hangingIndent", "borders", "shading", "tabs", "pageBreakBefore", "keepNext", "keepLines", "widowControl", "contextualSpacing", "outlineLevel", "direction", "_autospacingBase"]; | ||
| /** | ||
| * Build the attr patch that rejecting one pPrChange applies to a paragraph: | ||
| * every in-scope key set to the stored previous value, or reset to `null` | ||
| * when the stored old pPr does not carry it. Keys the record captured beyond | ||
| * the scoped set (editor-created list suggestions snapshot list-rendering | ||
| * bookkeeping attrs) merge through 1:1 so their pre-change values restore too. | ||
| */ | ||
| declare function paragraphRejectAttrPatch(previousFormatting: Record<string, unknown> | null | undefined): AttrPatch; | ||
| /** | ||
| * Rebuild the paragraph's `_originalFormatting` (the serializer's pPr source) | ||
| * after a reject: the stored old pPr wholesale within CT_PPrBase scope, with | ||
| * the paragraph-mark rPr fields (`runProperties`, `runInWithNext`) preserved | ||
| * from the live original — a pPrChange cannot store them, so a reject must | ||
| * not drop them. | ||
| */ | ||
| declare function paragraphRejectOriginalFormatting(previousFormatting: Record<string, unknown> | null | undefined, liveOriginal: unknown): document_d_exports.ParagraphFormatting | null; | ||
| /** | ||
| * Rejected section properties: the stored old sectPr wholesale, preserving | ||
| * the live header/footer references — `EG_HdrFtrReferences` is not part of | ||
| * the `CT_SectPrBase` payload a `w:sectPrChange` stores (ECMA-376 | ||
| * §17.13.5.32), so those children survive a reject. The caller re-attaches | ||
| * whatever `propertyChanges` remain unresolved. | ||
| */ | ||
| declare function sectionRejectProperties(live: document_d_exports.SectionProperties, previousProperties: document_d_exports.SectionProperties | undefined): document_d_exports.SectionProperties; | ||
| /** | ||
| * Attr patch for rejecting a `w:tblPrChange`: the stored old tblPr wholesale | ||
| * (`w:tblPrChange` stores the complete previous tblPr, so every tblPr-derived | ||
| * attr resets when absent). `columnWidths` comes from `w:tblGrid`, not tblPr, | ||
| * and is untouched. | ||
| */ | ||
| declare function tableRejectAttrPatch(previousFormatting: document_d_exports.TableFormatting | undefined): AttrPatch; | ||
| /** Attr patch for rejecting a `w:trPrChange`: the stored old trPr wholesale. */ | ||
| declare function tableRowRejectAttrPatch(previousFormatting: document_d_exports.TableRowFormatting | undefined): AttrPatch; | ||
| /** | ||
| * Attr patch for rejecting a `w:tcPrChange`: the stored old tcPr wholesale | ||
| * for the non-structural attrs. `gridSpan` / `vMerge` are structural in | ||
| * ProseMirror (`colspan` / `rowspan` shape the table grid); restoring them | ||
| * from the record would desync the grid without a full table restructure, so | ||
| * the live structure is kept and the rebuilt `_originalFormatting` inherits | ||
| * the live original's `gridSpan` / `vMerge` to stay consistent with it. | ||
| * (Word would also restore tracked merges; folio deliberately does not.) | ||
| */ | ||
| declare function tableCellRejectAttrPatch(previousFormatting: document_d_exports.TableCellFormatting | undefined, liveOriginalFormatting: document_d_exports.TableCellFormatting | null | undefined): AttrPatch; | ||
| //#endregion | ||
| export { PPR_CHANGE_SCOPED_ATTR_KEYS, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch }; |
| import { directionFromBidi } from "../paragraphDirection.js"; | ||
| import { setAutospacingBaseValue } from "../autospacingBase.js"; | ||
| //#region src/prosemirror/commands/propertyChangeScope.ts | ||
| /** | ||
| * Paragraph attrs governed by a `w:pPrChange` — the attrs | ||
| * `paragraphFormattingToAttrs` (conversion/toProseDoc.ts) can produce from a | ||
| * parsed `CT_PPrBase` (ECMA-376 §17.13.5.29). Rejecting a pPrChange restores | ||
| * the stored old pPr wholesale for exactly these keys: a key absent from the | ||
| * stored record resets to the schema default (`null`). | ||
| * | ||
| * Deliberately OUT of scope (preserved across a reject): | ||
| * - identity/structure: `paraId`, `textId`, `sectionBreakType`, | ||
| * `_sectionProperties`, `_propertyChanges`, `pPrMark`, `bookmarks`, | ||
| * `_emptyHyperlinks`, `renderedPageBreakBefore` | ||
| * - paragraph-mark run properties — `pPr/rPr` is CT_PPr-only, not part of the | ||
| * CT_PPrBase payload a pPrChange stores: `defaultTextFormatting`, | ||
| * `runInWithNext` (`w:specVanish` lives in that rPr) | ||
| * - load-time style/numbering bookkeeping the command layer cannot recompute | ||
| * without a style resolver: `numPrFromStyle`, the `list*` rendering attrs, | ||
| * `spacingFromDocDefaults` | ||
| */ | ||
| const PPR_CHANGE_SCOPED_ATTR_KEYS = [ | ||
| "styleId", | ||
| "numPr", | ||
| "alignment", | ||
| "spaceBefore", | ||
| "spaceAfter", | ||
| "lineSpacing", | ||
| "lineSpacingRule", | ||
| "spacingExplicit", | ||
| "indentLeft", | ||
| "indentRight", | ||
| "indentFirstLine", | ||
| "hangingIndent", | ||
| "borders", | ||
| "shading", | ||
| "tabs", | ||
| "pageBreakBefore", | ||
| "keepNext", | ||
| "keepLines", | ||
| "widowControl", | ||
| "contextualSpacing", | ||
| "outlineLevel", | ||
| "direction", | ||
| "_autospacingBase" | ||
| ]; | ||
| const PPR_CHANGE_SCOPED_ATTR_KEY_SET = new Set(PPR_CHANGE_SCOPED_ATTR_KEYS); | ||
| /** | ||
| * Parser-shaped `ParagraphFormatting` keys that must NOT merge through to | ||
| * attrs verbatim: they either normalize to a differently named attr | ||
| * (`bidi` → `direction`, autospacing flags → `_autospacingBase`), have no | ||
| * attr representation and round-trip via `_originalFormatting` only | ||
| * (`frame`, `suppressLineNumbers`, `suppressAutoHyphens`), or sit outside | ||
| * the CT_PPrBase scope entirely (`runProperties`, `runInWithNext` — the | ||
| * paragraph-mark rPr) and so must never overwrite the live value on reject. | ||
| */ | ||
| const PPR_PARSER_ONLY_KEYS = /* @__PURE__ */ new Set([ | ||
| "bidi", | ||
| "beforeAutospacing", | ||
| "afterAutospacing", | ||
| "runProperties", | ||
| "runInWithNext", | ||
| "frame", | ||
| "suppressLineNumbers", | ||
| "suppressAutoHyphens", | ||
| "numPrFromStyle" | ||
| ]); | ||
| /** | ||
| * `ParagraphFormatting` keys inside the CT_PPrBase scope, used to rebuild | ||
| * `_originalFormatting` after a reject (old pPr wholesale within scope, | ||
| * paragraph-mark rPr fields preserved from the live original). | ||
| */ | ||
| const PPR_CHANGE_SCOPED_FORMATTING_KEYS = [ | ||
| "styleId", | ||
| "numPr", | ||
| "alignment", | ||
| "bidi", | ||
| "spaceBefore", | ||
| "spaceAfter", | ||
| "lineSpacing", | ||
| "lineSpacingRule", | ||
| "beforeAutospacing", | ||
| "afterAutospacing", | ||
| "spacingExplicit", | ||
| "indentLeft", | ||
| "indentRight", | ||
| "indentFirstLine", | ||
| "hangingIndent", | ||
| "borders", | ||
| "shading", | ||
| "tabs", | ||
| "pageBreakBefore", | ||
| "keepNext", | ||
| "keepLines", | ||
| "widowControl", | ||
| "contextualSpacing", | ||
| "outlineLevel", | ||
| "frame", | ||
| "suppressLineNumbers", | ||
| "suppressAutoHyphens" | ||
| ]; | ||
| /** | ||
| * Build the attr patch that rejecting one pPrChange applies to a paragraph: | ||
| * every in-scope key set to the stored previous value, or reset to `null` | ||
| * when the stored old pPr does not carry it. Keys the record captured beyond | ||
| * the scoped set (editor-created list suggestions snapshot list-rendering | ||
| * bookkeeping attrs) merge through 1:1 so their pre-change values restore too. | ||
| */ | ||
| function paragraphRejectAttrPatch(previousFormatting) { | ||
| const prev = previousFormatting ?? {}; | ||
| const patch = {}; | ||
| for (const key of PPR_CHANGE_SCOPED_ATTR_KEYS) patch[key] = Object.hasOwn(prev, key) ? prev[key] ?? null : null; | ||
| if (!Object.hasOwn(prev, "direction")) patch["direction"] = directionFromBidi(prev["bidi"]); | ||
| if (!Object.hasOwn(prev, "_autospacingBase")) patch["_autospacingBase"] = autospacingBaseFromFormatting(prev); | ||
| for (const [key, value] of Object.entries(prev)) { | ||
| if (PPR_CHANGE_SCOPED_ATTR_KEY_SET.has(key) || PPR_PARSER_ONLY_KEYS.has(key)) continue; | ||
| patch[key] = value ?? null; | ||
| } | ||
| return patch; | ||
| } | ||
| /** | ||
| * Rebuild the paragraph's `_originalFormatting` (the serializer's pPr source) | ||
| * after a reject: the stored old pPr wholesale within CT_PPrBase scope, with | ||
| * the paragraph-mark rPr fields (`runProperties`, `runInWithNext`) preserved | ||
| * from the live original — a pPrChange cannot store them, so a reject must | ||
| * not drop them. | ||
| */ | ||
| function paragraphRejectOriginalFormatting(previousFormatting, liveOriginal) { | ||
| const prev = previousFormatting ?? {}; | ||
| const result = {}; | ||
| for (const key of PPR_CHANGE_SCOPED_FORMATTING_KEYS) { | ||
| const value = Object.hasOwn(prev, key) ? prev[key] : void 0; | ||
| if (value != null) result[key] = value; | ||
| } | ||
| const live = isRecord(liveOriginal) ? liveOriginal : {}; | ||
| if (live["runProperties"] != null) result["runProperties"] = live["runProperties"]; | ||
| if (live["runInWithNext"] != null) result["runInWithNext"] = live["runInWithNext"]; | ||
| if (Object.keys(result).length === 0) return null; | ||
| return result; | ||
| } | ||
| function autospacingBaseFromFormatting(prev) { | ||
| const before = prev["beforeAutospacing"] === true; | ||
| const after = prev["afterAutospacing"] === true; | ||
| if (!before && !after) return null; | ||
| const base = {}; | ||
| if (before) setAutospacingBaseValue(base, "before", prev["spaceBefore"]); | ||
| if (after) setAutospacingBaseValue(base, "after", prev["spaceAfter"]); | ||
| return base; | ||
| } | ||
| /** | ||
| * Rejected section properties: the stored old sectPr wholesale, preserving | ||
| * the live header/footer references — `EG_HdrFtrReferences` is not part of | ||
| * the `CT_SectPrBase` payload a `w:sectPrChange` stores (ECMA-376 | ||
| * §17.13.5.32), so those children survive a reject. The caller re-attaches | ||
| * whatever `propertyChanges` remain unresolved. | ||
| */ | ||
| function sectionRejectProperties(live, previousProperties) { | ||
| const restored = { ...previousProperties }; | ||
| delete restored.propertyChanges; | ||
| if (live.headerReferences) restored.headerReferences = live.headerReferences; | ||
| else delete restored.headerReferences; | ||
| if (live.footerReferences) restored.footerReferences = live.footerReferences; | ||
| else delete restored.footerReferences; | ||
| return restored; | ||
| } | ||
| /** | ||
| * Attr patch for rejecting a `w:tblPrChange`: the stored old tblPr wholesale | ||
| * (`w:tblPrChange` stores the complete previous tblPr, so every tblPr-derived | ||
| * attr resets when absent). `columnWidths` comes from `w:tblGrid`, not tblPr, | ||
| * and is untouched. | ||
| */ | ||
| function tableRejectAttrPatch(previousFormatting) { | ||
| return { | ||
| styleId: previousFormatting?.styleId ?? null, | ||
| width: previousFormatting?.width?.value ?? null, | ||
| widthType: previousFormatting?.width?.type ?? null, | ||
| justification: previousFormatting?.justification ?? null, | ||
| floating: previousFormatting?.floating ?? null, | ||
| cellMargins: previousFormatting?.cellMargins ? cellMarginsToAttr(previousFormatting.cellMargins) : null, | ||
| look: previousFormatting?.look ?? null, | ||
| borders: previousFormatting?.borders ?? null, | ||
| _originalFormatting: previousFormatting ?? null | ||
| }; | ||
| } | ||
| /** Attr patch for rejecting a `w:trPrChange`: the stored old trPr wholesale. */ | ||
| function tableRowRejectAttrPatch(previousFormatting) { | ||
| return { | ||
| height: previousFormatting?.height?.value ?? null, | ||
| heightRule: previousFormatting?.heightRule ?? null, | ||
| isHeader: previousFormatting?.header ?? false, | ||
| hidden: previousFormatting?.hidden ?? null, | ||
| _originalFormatting: previousFormatting ?? null | ||
| }; | ||
| } | ||
| /** | ||
| * Attr patch for rejecting a `w:tcPrChange`: the stored old tcPr wholesale | ||
| * for the non-structural attrs. `gridSpan` / `vMerge` are structural in | ||
| * ProseMirror (`colspan` / `rowspan` shape the table grid); restoring them | ||
| * from the record would desync the grid without a full table restructure, so | ||
| * the live structure is kept and the rebuilt `_originalFormatting` inherits | ||
| * the live original's `gridSpan` / `vMerge` to stay consistent with it. | ||
| * (Word would also restore tracked merges; folio deliberately does not.) | ||
| */ | ||
| function tableCellRejectAttrPatch(previousFormatting, liveOriginalFormatting) { | ||
| const restoredOriginal = { ...previousFormatting }; | ||
| delete restoredOriginal.gridSpan; | ||
| delete restoredOriginal.vMerge; | ||
| if (liveOriginalFormatting?.gridSpan !== void 0) restoredOriginal.gridSpan = liveOriginalFormatting.gridSpan; | ||
| if (liveOriginalFormatting?.vMerge !== void 0) restoredOriginal.vMerge = liveOriginalFormatting.vMerge; | ||
| return { | ||
| width: previousFormatting?.width?.value ?? null, | ||
| widthType: previousFormatting?.width?.type ?? null, | ||
| verticalAlign: previousFormatting?.verticalAlign ?? null, | ||
| backgroundColor: previousFormatting?.shading?.fill?.rgb ?? null, | ||
| textDirection: previousFormatting?.textDirection ?? null, | ||
| noWrap: previousFormatting?.noWrap ?? null, | ||
| borders: previousFormatting?.borders ? cellBordersToAttr(previousFormatting.borders) : null, | ||
| margins: previousFormatting?.margins ? cellMarginsToAttr(previousFormatting.margins) : null, | ||
| _originalFormatting: Object.keys(restoredOriginal).length > 0 ? restoredOriginal : null | ||
| }; | ||
| } | ||
| function cellMarginsToAttr(margins) { | ||
| const result = {}; | ||
| if (margins.top?.value !== void 0) result.top = margins.top.value; | ||
| if (margins.bottom?.value !== void 0) result.bottom = margins.bottom.value; | ||
| if (margins.left?.value !== void 0) result.left = margins.left.value; | ||
| if (margins.right?.value !== void 0) result.right = margins.right.value; | ||
| return result; | ||
| } | ||
| function cellBordersToAttr(borders) { | ||
| const result = {}; | ||
| if (borders.top) result.top = borders.top; | ||
| if (borders.bottom) result.bottom = borders.bottom; | ||
| if (borders.left) result.left = borders.left; | ||
| if (borders.right) result.right = borders.right; | ||
| return result; | ||
| } | ||
| function isRecord(value) { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value); | ||
| } | ||
| //#endregion | ||
| export { PPR_CHANGE_SCOPED_ATTR_KEYS, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch }; |
| import { FolioAIEditApplyResult } from "./ai-edits/types.js"; | ||
| //#region src/redline.d.ts | ||
| /** Options for {@link generateRedlineDocx}. */ | ||
| type GenerateRedlineDocxOptions = { | ||
| /** Author recorded on the generated tracked changes. (default: `"folio compare"`) */author?: string; | ||
| }; | ||
| /** Result of {@link generateRedlineDocx}. */ | ||
| type GenerateRedlineDocxResult = FolioAIEditApplyResult & { | ||
| /** The redline `.docx`: the base document with base → revised tracked changes. */buffer: ArrayBuffer; | ||
| }; | ||
| /** | ||
| * Compare `base` and `revised` and return the base document with every | ||
| * difference recorded as a tracked change. See the module doc comment for | ||
| * semantics and limitations; `skipped` reports any block the generator could | ||
| * not redline (e.g. additions with no anchor in an empty base document). | ||
| */ | ||
| declare const generateRedlineDocx: (base: ArrayBuffer, revised: ArrayBuffer, options?: GenerateRedlineDocxOptions) => Promise<GenerateRedlineDocxResult>; | ||
| //#endregion | ||
| export { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, generateRedlineDocx }; |
+127
| import { FolioDocxReviewer } from "./ai-edits/headless.js"; | ||
| import { alignFolioBlocks } from "./version-comparison.js"; | ||
| //#region src/redline.ts | ||
| /** | ||
| * Redline generator: compare two `.docx` buffers and produce a THIRD `.docx` | ||
| * whose differences are recorded as real Word tracked changes (`w:ins` / | ||
| * `w:del`), ready for review in Word or the folio editor — the | ||
| * document-producing counterpart to {@link compareDocxVersions}'s structured | ||
| * diff. | ||
| * | ||
| * The pipeline is a composition of existing machinery, not a new engine: | ||
| * the base buffer is opened in a headless {@link FolioDocxReviewer}, the two | ||
| * snapshots are aligned with {@link alignFolioBlocks} (the same three-pass | ||
| * alignment the comparer uses), each alignment event maps onto a | ||
| * tracked-changes {@link FolioAIEditOperation}, and the reviewer's shared | ||
| * apply path records them as redlines. Word-level minimality comes free: | ||
| * the apply engine word-diffs a `replaceBlock` and only marks the divergent | ||
| * tokens. | ||
| * | ||
| * ## Semantics and limitations | ||
| * | ||
| * - **As-accepted inputs.** Pending tracked changes in either input count as | ||
| * applied before comparison (mirroring {@link compareDocxVersions}): the | ||
| * base's own pending redlines are accepted in the output, so the tracked | ||
| * changes it carries are exactly base → revised. This is intentionally | ||
| * lossy about the inputs' own revision history and authorship. | ||
| * - **Text redlines only.** Format-only changes and relocated blocks are | ||
| * redlined as plain edits (a move becomes a delete + insert, like Word | ||
| * compare with move detection off); block-level formatting is carried via | ||
| * the revised block's `styleId` on inserted blocks. | ||
| * - **Anchoring.** Insertions anchor `before` the next surviving base block | ||
| * so consecutive additions keep their order; additions past the last base | ||
| * block anchor `after` it. A base document with no content blocks offers | ||
| * no anchor at all — such additions surface in `skipped` rather than | ||
| * silently vanishing. | ||
| */ | ||
| /** | ||
| * For each event index, the base block id the next `revisedOnly` event | ||
| * should anchor before: the base side of the nearest later `pair` or | ||
| * `baseOnly` event, or `null` when only additions remain until the end of | ||
| * the document. One backward pass, so a long run of trailing additions | ||
| * (huge revised document vs. a small base) stays linear instead of | ||
| * re-scanning the tail per added block. | ||
| */ | ||
| const nextBaseBlockIdByIndex = (events) => { | ||
| const nextIds = Array.from({ length: events.length }); | ||
| let nextId = null; | ||
| for (let i = events.length - 1; i >= 0; i--) { | ||
| nextIds[i] = nextId; | ||
| const event = events[i]; | ||
| if (event?.type === "pair") nextId = event.baseBlock.id; | ||
| else if (event?.type === "baseOnly") nextId = event.block.id; | ||
| } | ||
| return nextIds; | ||
| }; | ||
| /** | ||
| * Compare `base` and `revised` and return the base document with every | ||
| * difference recorded as a tracked change. See the module doc comment for | ||
| * semantics and limitations; `skipped` reports any block the generator could | ||
| * not redline (e.g. additions with no anchor in an empty base document). | ||
| */ | ||
| const generateRedlineDocx = async (base, revised, options = {}) => { | ||
| const [baseReviewer, revisedReviewer] = await Promise.all([FolioDocxReviewer.fromBuffer(base, { author: options.author ?? "folio compare" }), FolioDocxReviewer.fromBuffer(revised)]); | ||
| baseReviewer.acceptAll(); | ||
| const baseSnapshot = baseReviewer.snapshot(); | ||
| const revisedBlocks = revisedReviewer.snapshot().blocks; | ||
| const events = alignFolioBlocks(baseSnapshot.blocks, revisedBlocks); | ||
| const anchorIds = nextBaseBlockIdByIndex(events); | ||
| const operations = []; | ||
| let operationSeq = 0; | ||
| const nextOperationId = () => `redline-${++operationSeq}`; | ||
| /** Additions past the last base block, inserted after it in reverse so the final order matches the revised document. */ | ||
| const trailingAdditions = []; | ||
| const lastBaseBlockId = baseSnapshot.blocks.at(-1)?.id ?? null; | ||
| events.forEach((event, eventIndex) => { | ||
| if (event.type === "pair") { | ||
| if (event.baseBlock.text !== event.revisedBlock.text) operations.push({ | ||
| id: nextOperationId(), | ||
| type: "replaceBlock", | ||
| blockId: event.baseBlock.id, | ||
| text: event.revisedBlock.text | ||
| }); | ||
| return; | ||
| } | ||
| if (event.type === "baseOnly") { | ||
| operations.push({ | ||
| id: nextOperationId(), | ||
| type: "deleteBlock", | ||
| blockId: event.block.id | ||
| }); | ||
| return; | ||
| } | ||
| const anchorId = anchorIds[eventIndex] ?? null; | ||
| if (anchorId === null) { | ||
| trailingAdditions.push({ | ||
| text: event.block.text, | ||
| ...event.block.styleId !== void 0 && { styleId: event.block.styleId } | ||
| }); | ||
| return; | ||
| } | ||
| operations.push({ | ||
| id: nextOperationId(), | ||
| type: "insertBeforeBlock", | ||
| blockId: anchorId, | ||
| text: event.block.text, | ||
| ...event.block.styleId !== void 0 && { styleId: event.block.styleId } | ||
| }); | ||
| }); | ||
| for (const addition of trailingAdditions.toReversed()) operations.push({ | ||
| id: nextOperationId(), | ||
| type: "insertAfterBlock", | ||
| blockId: lastBaseBlockId ?? "redline-unanchored", | ||
| text: addition.text, | ||
| ...addition.styleId !== void 0 && { styleId: addition.styleId } | ||
| }); | ||
| const { applied, skipped } = baseReviewer.applyOperations(operations, { | ||
| mode: "tracked-changes", | ||
| snapshot: baseSnapshot | ||
| }); | ||
| return { | ||
| buffer: await baseReviewer.toBuffer(), | ||
| applied, | ||
| skipped | ||
| }; | ||
| }; | ||
| //#endregion | ||
| export { generateRedlineDocx }; |
| //#region src/symbols.d.ts | ||
| /** | ||
| * Symbol catalog for the "Insert Symbol" dialog. | ||
| * | ||
| * Framework-neutral data + filtering shared by every folio adapter (React, | ||
| * Vue, ...), so the two never drift on which symbols they offer. Category | ||
| * display names are i18n keys (`dialogs.insertSymbol.categories.*`) resolved by | ||
| * each adapter's own translator; the `char`/`name` pairs are the raw catalog. | ||
| */ | ||
| type SymbolEntry = { | ||
| /** The character to insert. */char: string; /** English descriptive name, used for search matching. */ | ||
| name: string; | ||
| }; | ||
| /** i18n message keys for the symbol-category display labels. */ | ||
| type SymbolCategoryNameKey = "dialogs.insertSymbol.categories.common" | "dialogs.insertSymbol.categories.arrows" | "dialogs.insertSymbol.categories.math" | "dialogs.insertSymbol.categories.greek" | "dialogs.insertSymbol.categories.currency" | "dialogs.insertSymbol.categories.shapes"; | ||
| type SymbolCategory = { | ||
| /** Stable category id (also the default active tab). */name: string; /** i18n key for the category's display label. */ | ||
| nameKey: SymbolCategoryNameKey; | ||
| symbols: SymbolEntry[]; | ||
| }; | ||
| /** A catalog entry tagged with the category it belongs to (search results). */ | ||
| type SymbolSearchEntry = SymbolEntry & { | ||
| category: string; | ||
| }; | ||
| declare const SYMBOL_CATEGORIES: SymbolCategory[]; | ||
| /** Every catalog symbol, flattened and tagged with its category. */ | ||
| declare const ALL_SYMBOLS: SymbolSearchEntry[]; | ||
| /** | ||
| * Symbols whose descriptive name contains `query` (case-insensitive), or whose | ||
| * character is exactly `query`. An empty/whitespace query returns `[]` so | ||
| * callers fall back to the active category. | ||
| */ | ||
| declare function filterSymbols(query: string): SymbolSearchEntry[]; | ||
| //#endregion | ||
| export { ALL_SYMBOLS, SYMBOL_CATEGORIES, SymbolCategory, SymbolCategoryNameKey, SymbolEntry, SymbolSearchEntry, filterSymbols }; |
+512
| //#region src/symbols.ts | ||
| const SYMBOL_CATEGORIES = [ | ||
| { | ||
| name: "Common", | ||
| nameKey: "dialogs.insertSymbol.categories.common", | ||
| symbols: [ | ||
| { | ||
| char: "©", | ||
| name: "Copyright" | ||
| }, | ||
| { | ||
| char: "®", | ||
| name: "Registered" | ||
| }, | ||
| { | ||
| char: "™", | ||
| name: "Trademark" | ||
| }, | ||
| { | ||
| char: "•", | ||
| name: "Bullet" | ||
| }, | ||
| { | ||
| char: "…", | ||
| name: "Ellipsis" | ||
| }, | ||
| { | ||
| char: "—", | ||
| name: "Em dash" | ||
| }, | ||
| { | ||
| char: "–", | ||
| name: "En dash" | ||
| }, | ||
| { | ||
| char: "±", | ||
| name: "Plus-minus" | ||
| }, | ||
| { | ||
| char: "×", | ||
| name: "Multiply" | ||
| }, | ||
| { | ||
| char: "÷", | ||
| name: "Divide" | ||
| }, | ||
| { | ||
| char: "≠", | ||
| name: "Not equal" | ||
| }, | ||
| { | ||
| char: "≈", | ||
| name: "Approximately" | ||
| }, | ||
| { | ||
| char: "≤", | ||
| name: "Less or equal" | ||
| }, | ||
| { | ||
| char: "≥", | ||
| name: "Greater or equal" | ||
| }, | ||
| { | ||
| char: "°", | ||
| name: "Degree" | ||
| }, | ||
| { | ||
| char: "µ", | ||
| name: "Micro" | ||
| }, | ||
| { | ||
| char: "¶", | ||
| name: "Pilcrow" | ||
| }, | ||
| { | ||
| char: "§", | ||
| name: "Section" | ||
| }, | ||
| { | ||
| char: "†", | ||
| name: "Dagger" | ||
| }, | ||
| { | ||
| char: "‡", | ||
| name: "Double dagger" | ||
| }, | ||
| { | ||
| char: "¿", | ||
| name: "Inverted question" | ||
| }, | ||
| { | ||
| char: "¡", | ||
| name: "Inverted exclamation" | ||
| }, | ||
| { | ||
| char: "‰", | ||
| name: "Per mille" | ||
| }, | ||
| { | ||
| char: "∞", | ||
| name: "Infinity" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| name: "Arrows", | ||
| nameKey: "dialogs.insertSymbol.categories.arrows", | ||
| symbols: [ | ||
| { | ||
| char: "←", | ||
| name: "Left" | ||
| }, | ||
| { | ||
| char: "→", | ||
| name: "Right" | ||
| }, | ||
| { | ||
| char: "↑", | ||
| name: "Up" | ||
| }, | ||
| { | ||
| char: "↓", | ||
| name: "Down" | ||
| }, | ||
| { | ||
| char: "↔", | ||
| name: "Left-right" | ||
| }, | ||
| { | ||
| char: "↕", | ||
| name: "Up-down" | ||
| }, | ||
| { | ||
| char: "⇐", | ||
| name: "Double left" | ||
| }, | ||
| { | ||
| char: "⇒", | ||
| name: "Double right" | ||
| }, | ||
| { | ||
| char: "⇑", | ||
| name: "Double up" | ||
| }, | ||
| { | ||
| char: "⇓", | ||
| name: "Double down" | ||
| }, | ||
| { | ||
| char: "⇔", | ||
| name: "Double left-right" | ||
| }, | ||
| { | ||
| char: "➡", | ||
| name: "Heavy right" | ||
| }, | ||
| { | ||
| char: "↩", | ||
| name: "Return" | ||
| }, | ||
| { | ||
| char: "↪", | ||
| name: "Curved right" | ||
| }, | ||
| { | ||
| char: "↻", | ||
| name: "Clockwise" | ||
| }, | ||
| { | ||
| char: "↺", | ||
| name: "Counter-clockwise" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| name: "Math", | ||
| nameKey: "dialogs.insertSymbol.categories.math", | ||
| symbols: [ | ||
| { | ||
| char: "∑", | ||
| name: "Summation" | ||
| }, | ||
| { | ||
| char: "∏", | ||
| name: "Product" | ||
| }, | ||
| { | ||
| char: "∫", | ||
| name: "Integral" | ||
| }, | ||
| { | ||
| char: "√", | ||
| name: "Square root" | ||
| }, | ||
| { | ||
| char: "∂", | ||
| name: "Partial diff" | ||
| }, | ||
| { | ||
| char: "∇", | ||
| name: "Nabla" | ||
| }, | ||
| { | ||
| char: "∈", | ||
| name: "Element of" | ||
| }, | ||
| { | ||
| char: "∉", | ||
| name: "Not element" | ||
| }, | ||
| { | ||
| char: "⊂", | ||
| name: "Subset" | ||
| }, | ||
| { | ||
| char: "⊃", | ||
| name: "Superset" | ||
| }, | ||
| { | ||
| char: "∪", | ||
| name: "Union" | ||
| }, | ||
| { | ||
| char: "∩", | ||
| name: "Intersection" | ||
| }, | ||
| { | ||
| char: "∧", | ||
| name: "And" | ||
| }, | ||
| { | ||
| char: "∨", | ||
| name: "Or" | ||
| }, | ||
| { | ||
| char: "¬", | ||
| name: "Not" | ||
| }, | ||
| { | ||
| char: "∀", | ||
| name: "For all" | ||
| }, | ||
| { | ||
| char: "∃", | ||
| name: "Exists" | ||
| }, | ||
| { | ||
| char: "∅", | ||
| name: "Empty set" | ||
| }, | ||
| { | ||
| char: "∝", | ||
| name: "Proportional" | ||
| }, | ||
| { | ||
| char: "∠", | ||
| name: "Angle" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| name: "Greek", | ||
| nameKey: "dialogs.insertSymbol.categories.greek", | ||
| symbols: [ | ||
| { | ||
| char: "α", | ||
| name: "alpha" | ||
| }, | ||
| { | ||
| char: "β", | ||
| name: "beta" | ||
| }, | ||
| { | ||
| char: "γ", | ||
| name: "gamma" | ||
| }, | ||
| { | ||
| char: "δ", | ||
| name: "delta" | ||
| }, | ||
| { | ||
| char: "ε", | ||
| name: "epsilon" | ||
| }, | ||
| { | ||
| char: "ζ", | ||
| name: "zeta" | ||
| }, | ||
| { | ||
| char: "η", | ||
| name: "eta" | ||
| }, | ||
| { | ||
| char: "θ", | ||
| name: "theta" | ||
| }, | ||
| { | ||
| char: "λ", | ||
| name: "lambda" | ||
| }, | ||
| { | ||
| char: "μ", | ||
| name: "mu" | ||
| }, | ||
| { | ||
| char: "π", | ||
| name: "pi" | ||
| }, | ||
| { | ||
| char: "ρ", | ||
| name: "rho" | ||
| }, | ||
| { | ||
| char: "σ", | ||
| name: "sigma" | ||
| }, | ||
| { | ||
| char: "τ", | ||
| name: "tau" | ||
| }, | ||
| { | ||
| char: "φ", | ||
| name: "phi" | ||
| }, | ||
| { | ||
| char: "ψ", | ||
| name: "psi" | ||
| }, | ||
| { | ||
| char: "ω", | ||
| name: "omega" | ||
| }, | ||
| { | ||
| char: "Δ", | ||
| name: "Delta" | ||
| }, | ||
| { | ||
| char: "Σ", | ||
| name: "Sigma" | ||
| }, | ||
| { | ||
| char: "Ω", | ||
| name: "Omega" | ||
| }, | ||
| { | ||
| char: "Π", | ||
| name: "Pi" | ||
| }, | ||
| { | ||
| char: "Φ", | ||
| name: "Phi" | ||
| }, | ||
| { | ||
| char: "Ψ", | ||
| name: "Psi" | ||
| }, | ||
| { | ||
| char: "Θ", | ||
| name: "Theta" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| name: "Currency", | ||
| nameKey: "dialogs.insertSymbol.categories.currency", | ||
| symbols: [ | ||
| { | ||
| char: "$", | ||
| name: "Dollar" | ||
| }, | ||
| { | ||
| char: "€", | ||
| name: "Euro" | ||
| }, | ||
| { | ||
| char: "£", | ||
| name: "Pound" | ||
| }, | ||
| { | ||
| char: "¥", | ||
| name: "Yen" | ||
| }, | ||
| { | ||
| char: "₹", | ||
| name: "Rupee" | ||
| }, | ||
| { | ||
| char: "₽", | ||
| name: "Ruble" | ||
| }, | ||
| { | ||
| char: "₩", | ||
| name: "Won" | ||
| }, | ||
| { | ||
| char: "₿", | ||
| name: "Bitcoin" | ||
| }, | ||
| { | ||
| char: "¢", | ||
| name: "Cent" | ||
| }, | ||
| { | ||
| char: "₫", | ||
| name: "Dong" | ||
| }, | ||
| { | ||
| char: "₺", | ||
| name: "Lira" | ||
| }, | ||
| { | ||
| char: "₴", | ||
| name: "Hryvnia" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| name: "Shapes", | ||
| nameKey: "dialogs.insertSymbol.categories.shapes", | ||
| symbols: [ | ||
| { | ||
| char: "■", | ||
| name: "Black square" | ||
| }, | ||
| { | ||
| char: "□", | ||
| name: "White square" | ||
| }, | ||
| { | ||
| char: "▲", | ||
| name: "Up triangle" | ||
| }, | ||
| { | ||
| char: "▼", | ||
| name: "Down triangle" | ||
| }, | ||
| { | ||
| char: "●", | ||
| name: "Black circle" | ||
| }, | ||
| { | ||
| char: "○", | ||
| name: "White circle" | ||
| }, | ||
| { | ||
| char: "◆", | ||
| name: "Black diamond" | ||
| }, | ||
| { | ||
| char: "◇", | ||
| name: "White diamond" | ||
| }, | ||
| { | ||
| char: "★", | ||
| name: "Black star" | ||
| }, | ||
| { | ||
| char: "☆", | ||
| name: "White star" | ||
| }, | ||
| { | ||
| char: "♠", | ||
| name: "Spade" | ||
| }, | ||
| { | ||
| char: "♥", | ||
| name: "Heart" | ||
| }, | ||
| { | ||
| char: "♦", | ||
| name: "Diamond" | ||
| }, | ||
| { | ||
| char: "♣", | ||
| name: "Club" | ||
| }, | ||
| { | ||
| char: "✓", | ||
| name: "Check mark" | ||
| }, | ||
| { | ||
| char: "✗", | ||
| name: "Ballot X" | ||
| }, | ||
| { | ||
| char: "✦", | ||
| name: "Four pointed star" | ||
| }, | ||
| { | ||
| char: "◌", | ||
| name: "Dotted circle" | ||
| } | ||
| ] | ||
| } | ||
| ]; | ||
| /** Every catalog symbol, flattened and tagged with its category. */ | ||
| const ALL_SYMBOLS = SYMBOL_CATEGORIES.flatMap((category) => category.symbols.map((symbol) => ({ | ||
| ...symbol, | ||
| category: category.name | ||
| }))); | ||
| /** | ||
| * Symbols whose descriptive name contains `query` (case-insensitive), or whose | ||
| * character is exactly `query`. An empty/whitespace query returns `[]` so | ||
| * callers fall back to the active category. | ||
| */ | ||
| function filterSymbols(query) { | ||
| const q = query.trim().toLowerCase(); | ||
| if (!q) return []; | ||
| return ALL_SYMBOLS.filter((symbol) => symbol.name.toLowerCase().includes(q) || symbol.char === q); | ||
| } | ||
| //#endregion | ||
| export { ALL_SYMBOLS, SYMBOL_CATEGORIES, filterSymbols }; |
| //#region src/utils/zoom.d.ts | ||
| /** | ||
| * Canonical zoom limits shared by every folio adapter (React, Vue, ...). | ||
| * | ||
| * Adapters must source their zoom range from here rather than hard-coding their | ||
| * own, so the reachable zoom span stays identical across framework hosts. | ||
| * Historically React clamped 0.25-4x while Vue clamped 0.5-2x; this module is | ||
| * the single source of truth that keeps them from drifting again. | ||
| * | ||
| * Note: the discrete zoom-level menu each adapter shows in its toolbar dropdown | ||
| * is a separate, deliberately curated subset (50-200%); it is not derived from | ||
| * these limits. | ||
| */ | ||
| /** Minimum reachable zoom level (25%). */ | ||
| declare const ZOOM_MIN = 0.25; | ||
| /** Maximum reachable zoom level (400%). */ | ||
| declare const ZOOM_MAX = 4; | ||
| /** Step applied per zoom-in / zoom-out action and per keyboard shortcut. */ | ||
| declare const ZOOM_STEP = 0.1; | ||
| //#endregion | ||
| export { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP }; |
| //#region src/utils/zoom.ts | ||
| /** | ||
| * Canonical zoom limits shared by every folio adapter (React, Vue, ...). | ||
| * | ||
| * Adapters must source their zoom range from here rather than hard-coding their | ||
| * own, so the reachable zoom span stays identical across framework hosts. | ||
| * Historically React clamped 0.25-4x while Vue clamped 0.5-2x; this module is | ||
| * the single source of truth that keeps them from drifting again. | ||
| * | ||
| * Note: the discrete zoom-level menu each adapter shows in its toolbar dropdown | ||
| * is a separate, deliberately curated subset (50-200%); it is not derived from | ||
| * these limits. | ||
| */ | ||
| /** Minimum reachable zoom level (25%). */ | ||
| const ZOOM_MIN = .25; | ||
| /** Maximum reachable zoom level (400%). */ | ||
| const ZOOM_MAX = 4; | ||
| /** Step applied per zoom-in / zoom-out action and per keyboard shortcut. */ | ||
| const ZOOM_STEP = .1; | ||
| //#endregion | ||
| export { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP }; |
| import { FolioAIBlock } from "./ai-edits/types.js"; | ||
| import { WordDiffSegment } from "./ai-edits/word-diff.js"; | ||
| //#region src/version-comparison.d.ts | ||
| /** One word-level diff segment within a `modified` block. Mirrors {@link WordDiffSegment}. */ | ||
| type FolioVersionDiffSegment = WordDiffSegment; | ||
| /** Run-level formatting properties compared for `formatChanged` detection. */ | ||
| declare const FORMAT_PROPERTIES: readonly ["bold", "italic", "underline", "strike", "fontFamily", "fontSizePt", "color"]; | ||
| /** A run-level formatting property that can differ in a `formatChanged` block. */ | ||
| type FolioFormatProperty = (typeof FORMAT_PROPERTIES)[number]; | ||
| /** One block-level change between two document versions, in revised-side document order. */ | ||
| type FolioBlockDiff = { | ||
| type: "added"; | ||
| blockId: string; | ||
| kind: string; | ||
| text: string; | ||
| } | { | ||
| type: "deleted"; | ||
| blockId: string; | ||
| kind: string; | ||
| text: string; | ||
| } | { | ||
| type: "modified"; | ||
| blockId: string; | ||
| kind: string; | ||
| segments: FolioVersionDiffSegment[]; | ||
| } | { | ||
| type: "formatChanged"; | ||
| blockId: string; | ||
| kind: string; | ||
| text: string; | ||
| changedProperties: FolioFormatProperty[]; | ||
| } | { | ||
| type: "movedFrom"; | ||
| blockId: string; | ||
| kind: string; | ||
| text: string; | ||
| moveGroupId: number; | ||
| } | { | ||
| type: "movedTo"; | ||
| blockId: string; | ||
| kind: string; | ||
| text: string; | ||
| moveGroupId: number; | ||
| }; | ||
| /** Result of {@link compareDocxVersions}. */ | ||
| type FolioVersionDiff = { | ||
| /** Every changed block, in revised-side document order (deletions and move sources slotted where they sat). */changes: FolioBlockDiff[]; /** Counts across every paired/unpaired block, including the unchanged blocks `changes` omits. `moved` counts pairs, not entries. */ | ||
| summaryCounts: { | ||
| added: number; | ||
| deleted: number; | ||
| modified: number; | ||
| formatChanged: number; | ||
| moved: number; | ||
| unchanged: number; | ||
| }; | ||
| }; | ||
| /** True when an `unpairedBaseCount * unpairedRevisedCount` LCS table would exceed {@link MAX_LCS_CELLS}. */ | ||
| declare const exceedsLcsBudget: (unpairedBaseCount: number, unpairedRevisedCount: number) => boolean; | ||
| /** | ||
| * One step of a completed alignment, in revised-side document order with | ||
| * base-only blocks slotted where they sat. `pair` events cover pass 1/2 | ||
| * anchors and pass 3's positional zip alike; whether the pair is unchanged, | ||
| * modified, or format-changed is the consumer's call. | ||
| */ | ||
| type FolioAlignedBlockEvent = { | ||
| type: "pair"; | ||
| baseBlock: FolioAIBlock; | ||
| revisedBlock: FolioAIBlock; | ||
| } | { | ||
| type: "baseOnly"; | ||
| block: FolioAIBlock; | ||
| } | { | ||
| type: "revisedOnly"; | ||
| block: FolioAIBlock; | ||
| }; | ||
| /** | ||
| * Run the three-pass alignment (see the module doc comment) over two block | ||
| * snapshots and flatten it into an ordered event stream. Shared by | ||
| * {@link compareDocxVersions} and the redline generator so both interpret | ||
| * one document walk instead of re-deriving it. | ||
| */ | ||
| declare const alignFolioBlocks: (baseBlocks: readonly FolioAIBlock[], revisedBlocks: readonly FolioAIBlock[]) => FolioAlignedBlockEvent[]; | ||
| /** | ||
| * Compare two `.docx` buffers and return a structured, block-level diff. | ||
| * See the module doc comment for the as-accepted comparison semantics, the | ||
| * three-pass alignment algorithm, move detection, and format-only change | ||
| * detection. | ||
| */ | ||
| declare const compareDocxVersions: (base: ArrayBuffer, revised: ArrayBuffer) => Promise<FolioVersionDiff>; | ||
| //#endregion | ||
| export { FolioAlignedBlockEvent, FolioBlockDiff, FolioFormatProperty, FolioVersionDiff, FolioVersionDiffSegment, alignFolioBlocks, compareDocxVersions, exceedsLcsBudget }; |
| import { getFolioParaIdFromBlockId } from "./types/block-id.js"; | ||
| import { diffWordSegments } from "./ai-edits/word-diff.js"; | ||
| import { FolioDocxReviewer } from "./ai-edits/headless.js"; | ||
| //#region src/version-comparison.ts | ||
| /** | ||
| * Document version-diff engine: compare two `.docx` buffers block by block | ||
| * and produce a structured, LLM-summarizable diff. | ||
| * | ||
| * Both buffers are parsed through {@link FolioDocxReviewer} — the same | ||
| * headless parsing + clean-text path `read_document` / `read_changes` use — | ||
| * so the comparison runs over each document's AS-ACCEPTED view: any pending | ||
| * tracked changes already present in EITHER buffer count as applied before | ||
| * the two are compared. Two documents that agree once their own pending | ||
| * redlines are accepted report as unchanged, even if the underlying | ||
| * tracked-change history differs. | ||
| * | ||
| * ## Alignment | ||
| * | ||
| * Blocks are paired across the two snapshots in three passes, each only | ||
| * considering blocks the previous pass left unpaired: | ||
| * | ||
| * 1. **Stable-id pairing.** Blocks whose ids are equal AND not a `seq-NNNN` | ||
| * positional fallback ({@link getFolioParaIdFromBlockId} returns non-null) | ||
| * are paired directly. This covers both id shapes a snapshot can carry: | ||
| * - A real Word `w14:paraId`: stable identity, independent of text — an | ||
| * equal-id pair with different text is a genuine edit (`modified`). | ||
| * - `FolioDocxReviewer`'s deterministic fallback id (assigned when the | ||
| * source paragraph has no `w14:paraId`), which hashes the paragraph's | ||
| * TEXT plus its document ordinal. It is structurally indistinguishable | ||
| * from a real paraId ({@link getFolioParaIdFromBlockId} can't tell them | ||
| * apart), but pairing on equality is still safe: two equal deterministic | ||
| * ids necessarily came from identical text at an identical ordinal, so | ||
| * the pair is always text-equal (`unchanged`) — never a false | ||
| * `modified`. What it can't do is FIND a paragraph whose ordinal shifted | ||
| * (an insertion/deletion earlier in the document) even though its text | ||
| * is unchanged: that pair has two different fallback ids and falls | ||
| * through to pass 2. | ||
| * 2. **Exact-text pairing.** An order-preserving LCS over remaining blocks, | ||
| * matched by exact text equality. This is what recovers same-text blocks | ||
| * that pass 1 missed because a fallback id shifted with the ordinal. Its | ||
| * O(m·n) table is skipped ({@link exceedsLcsBudget}) once the unpaired | ||
| * counts on both sides would exceed a fixed cell budget, so a document | ||
| * with few/no stable ids can't force a quadratic-sized allocation; those | ||
| * blocks fall through to pass 3 instead. | ||
| * 3. **Positional fallback.** Whatever a monotonicity filter leaves | ||
| * unpaired is split into the gaps between anchored pairs (pass 1 + 2, | ||
| * time-ordered); within each gap the shorter side is zipped positionally | ||
| * against the longer one (`modified`), and any excess on either side is | ||
| * reported as `added` / `deleted`. | ||
| * | ||
| * The combined anchor set from passes 1 and 2 is re-filtered to the longest | ||
| * increasing subsequence by revised-side index before pass 3 runs, so a | ||
| * pathological crossing match (content reordered across versions) can't | ||
| * produce an out-of-order gap — the alignment always walks both documents | ||
| * forward. | ||
| * | ||
| * ## Move detection | ||
| * | ||
| * Relocated content would otherwise report as an unrelated `deleted` + | ||
| * `added` pair (both order-preserving passes drop crossing matches by | ||
| * design). A post-pass re-classifies such pairs: an `added` and a `deleted` | ||
| * block with identical text and at least {@link MOVE_MINIMUM_WORD_COUNT} | ||
| * words become `movedFrom` / `movedTo` entries sharing a `moveGroupId`. The | ||
| * word-count floor keeps boilerplate one-liners ("Confidential", empty | ||
| * headings) from pairing as spurious moves. Blocks a positional zip already | ||
| * mis-paired as `modified` are out of this pass's reach — a known limitation | ||
| * of the gap fallback, not of the move pass. | ||
| * | ||
| * ## Format-only changes | ||
| * | ||
| * A paired block whose text is byte-equal but whose run-level formatting | ||
| * (bold, italic, underline, strike, font family, font size, color) differs | ||
| * reports as `formatChanged` with the set of properties that differ, instead | ||
| * of silently counting as `unchanged`. Detection walks the two blocks' | ||
| * preview runs character-aligned; when a block carries non-text inline | ||
| * content that makes the preview texts disagree, detection backs off to | ||
| * `unchanged` rather than misattribute properties. | ||
| */ | ||
| /** Run-level formatting properties compared for `formatChanged` detection. */ | ||
| const FORMAT_PROPERTIES = [ | ||
| "bold", | ||
| "italic", | ||
| "underline", | ||
| "strike", | ||
| "fontFamily", | ||
| "fontSizePt", | ||
| "color" | ||
| ]; | ||
| const isStableBlockId = (id) => getFolioParaIdFromBlockId(id) !== null; | ||
| /** | ||
| * Longest increasing subsequence by `revisedIndex`, assuming `pairs` is | ||
| * already sorted by `baseIndex` ascending. Drops any pair that would make | ||
| * the alignment walk backward in the revised document — the guard against | ||
| * both id collisions (pass 1) and any crossing match (pass 1 + 2 combined). | ||
| */ | ||
| const longestIncreasingByRevisedIndex = (pairs) => { | ||
| if (pairs.length === 0) return []; | ||
| const lengths = new Int32Array(pairs.length).fill(1); | ||
| const predecessors = new Int32Array(pairs.length).fill(-1); | ||
| let bestEnd = 0; | ||
| for (let i = 0; i < pairs.length; i++) { | ||
| for (let j = 0; j < i; j++) { | ||
| const current = pairs[j]; | ||
| const candidate = pairs[i]; | ||
| if (!current || !candidate) continue; | ||
| if (current.revisedIndex < candidate.revisedIndex && (lengths[j] ?? 0) + 1 > (lengths[i] ?? 0)) { | ||
| lengths[i] = (lengths[j] ?? 0) + 1; | ||
| predecessors[i] = j; | ||
| } | ||
| } | ||
| if ((lengths[i] ?? 0) > (lengths[bestEnd] ?? 0)) bestEnd = i; | ||
| } | ||
| const ordered = []; | ||
| for (let cursor = bestEnd; cursor !== -1; cursor = predecessors[cursor] ?? -1) { | ||
| const pair = pairs[cursor]; | ||
| if (pair) ordered.push(pair); | ||
| } | ||
| return ordered.toReversed(); | ||
| }; | ||
| /** Pass 1: pair blocks with equal, non-`seq-NNNN` ids. See the module doc comment. */ | ||
| const pairByStableId = (base, revised) => { | ||
| const revisedIndexById = /* @__PURE__ */ new Map(); | ||
| revised.forEach((block, revisedIndex) => { | ||
| if (isStableBlockId(block.id)) revisedIndexById.set(block.id, revisedIndex); | ||
| }); | ||
| const candidates = []; | ||
| base.forEach((block, baseIndex) => { | ||
| if (!isStableBlockId(block.id)) return; | ||
| const revisedIndex = revisedIndexById.get(block.id); | ||
| if (revisedIndex !== void 0) candidates.push({ | ||
| baseIndex, | ||
| revisedIndex | ||
| }); | ||
| }); | ||
| return longestIncreasingByRevisedIndex(candidates); | ||
| }; | ||
| /** | ||
| * Cell budget for pass 2's O(m·n) exact-text LCS table (`dp` below allocates | ||
| * `(m + 1) * (n + 1)` numbers). A document with no `w14:paraId`s — or an | ||
| * adversarial one crafted to defeat pass 1 — can leave thousands of blocks | ||
| * unpaired on both sides; without a cap, `pairByExactText` would allocate a | ||
| * quadratic-sized table for it. Past this budget, {@link exceedsLcsBudget} | ||
| * makes pass 2 back off entirely so alignment falls through to pass 3's | ||
| * linear positional zip instead — pairing is less precise for these | ||
| * degenerate inputs, but memory use stays bounded. | ||
| */ | ||
| const MAX_LCS_CELLS = 4e6; | ||
| /** True when an `unpairedBaseCount * unpairedRevisedCount` LCS table would exceed {@link MAX_LCS_CELLS}. */ | ||
| const exceedsLcsBudget = (unpairedBaseCount, unpairedRevisedCount) => unpairedBaseCount * unpairedRevisedCount > MAX_LCS_CELLS; | ||
| /** Pass 2: order-preserving LCS by exact text equality over the blocks pass 1 left unpaired. */ | ||
| const pairByExactText = (base, revised) => { | ||
| const m = base.length; | ||
| const n = revised.length; | ||
| if (m === 0 || n === 0) return []; | ||
| if (exceedsLcsBudget(m, n)) return []; | ||
| const baseTexts = base.map(({ block }) => block.text); | ||
| const revisedTexts = revised.map(({ block }) => block.text); | ||
| const stride = n + 1; | ||
| const dp = new Int32Array((m + 1) * stride); | ||
| for (let i = m - 1; i >= 0; i--) { | ||
| const rowOffset = i * stride; | ||
| const nextRowOffset = (i + 1) * stride; | ||
| const baseText = baseTexts[i]; | ||
| for (let j = n - 1; j >= 0; j--) { | ||
| const revisedText = revisedTexts[j]; | ||
| dp[rowOffset + j] = baseText === revisedText ? (dp[nextRowOffset + j + 1] ?? 0) + 1 : Math.max(dp[nextRowOffset + j] ?? 0, dp[rowOffset + j + 1] ?? 0); | ||
| } | ||
| } | ||
| const pairs = []; | ||
| let i = 0; | ||
| let j = 0; | ||
| while (i < m && j < n) { | ||
| const baseEntry = base[i]; | ||
| const revisedEntry = revised[j]; | ||
| if (!baseEntry || !revisedEntry) break; | ||
| if (baseTexts[i] === revisedTexts[j]) { | ||
| pairs.push({ | ||
| baseIndex: baseEntry.index, | ||
| revisedIndex: revisedEntry.index | ||
| }); | ||
| i++; | ||
| j++; | ||
| continue; | ||
| } | ||
| if ((dp[(i + 1) * stride + j] ?? 0) >= (dp[i * stride + j + 1] ?? 0)) i++; | ||
| else j++; | ||
| } | ||
| return pairs; | ||
| }; | ||
| /** | ||
| * Run the three-pass alignment (see the module doc comment) over two block | ||
| * snapshots and flatten it into an ordered event stream. Shared by | ||
| * {@link compareDocxVersions} and the redline generator so both interpret | ||
| * one document walk instead of re-deriving it. | ||
| */ | ||
| const alignFolioBlocks = (baseBlocks, revisedBlocks) => { | ||
| const stableIdAnchors = pairByStableId(baseBlocks, revisedBlocks); | ||
| const usedBaseIndexes = new Set(stableIdAnchors.map((anchor) => anchor.baseIndex)); | ||
| const usedRevisedIndexes = new Set(stableIdAnchors.map((anchor) => anchor.revisedIndex)); | ||
| const baseRemaining = []; | ||
| baseBlocks.forEach((block, blockIndex) => { | ||
| if (!usedBaseIndexes.has(blockIndex)) baseRemaining.push({ | ||
| block, | ||
| index: blockIndex | ||
| }); | ||
| }); | ||
| const revisedRemaining = []; | ||
| revisedBlocks.forEach((block, blockIndex) => { | ||
| if (!usedRevisedIndexes.has(blockIndex)) revisedRemaining.push({ | ||
| block, | ||
| index: blockIndex | ||
| }); | ||
| }); | ||
| const exactTextAnchors = pairByExactText(baseRemaining, revisedRemaining); | ||
| const anchors = longestIncreasingByRevisedIndex([...stableIdAnchors, ...exactTextAnchors].toSorted((a, b) => a.baseIndex - b.baseIndex)); | ||
| const events = []; | ||
| /** Pass 3: positionally zip the leftover blocks in one gap between anchors. */ | ||
| const emitGap = (baseFrom, baseTo, revisedFrom, revisedTo) => { | ||
| const pairedCount = Math.min(baseTo - baseFrom, revisedTo - revisedFrom); | ||
| for (let k = 0; k < pairedCount; k++) { | ||
| const baseBlock = baseBlocks[baseFrom + k]; | ||
| const revisedBlock = revisedBlocks[revisedFrom + k]; | ||
| if (baseBlock && revisedBlock) events.push({ | ||
| type: "pair", | ||
| baseBlock, | ||
| revisedBlock | ||
| }); | ||
| } | ||
| for (let k = baseFrom + pairedCount; k < baseTo; k++) { | ||
| const block = baseBlocks[k]; | ||
| if (block) events.push({ | ||
| type: "baseOnly", | ||
| block | ||
| }); | ||
| } | ||
| for (let k = revisedFrom + pairedCount; k < revisedTo; k++) { | ||
| const block = revisedBlocks[k]; | ||
| if (block) events.push({ | ||
| type: "revisedOnly", | ||
| block | ||
| }); | ||
| } | ||
| }; | ||
| let baseCursor = 0; | ||
| let revisedCursor = 0; | ||
| for (const anchor of anchors) { | ||
| emitGap(baseCursor, anchor.baseIndex, revisedCursor, anchor.revisedIndex); | ||
| const baseBlock = baseBlocks[anchor.baseIndex]; | ||
| const revisedBlock = revisedBlocks[anchor.revisedIndex]; | ||
| if (baseBlock && revisedBlock) events.push({ | ||
| type: "pair", | ||
| baseBlock, | ||
| revisedBlock | ||
| }); | ||
| baseCursor = anchor.baseIndex + 1; | ||
| revisedCursor = anchor.revisedIndex + 1; | ||
| } | ||
| emitGap(baseCursor, baseBlocks.length, revisedCursor, revisedBlocks.length); | ||
| return events; | ||
| }; | ||
| const previewRunsText = (runs) => runs.map((run) => run.text).join(""); | ||
| /** | ||
| * Character-aligned formatting diff of two text-equal blocks. Walks both | ||
| * blocks' preview runs in parallel and collects every property whose value | ||
| * differs anywhere in the overlap. A side with `previewRuns === undefined` | ||
| * (the snapshot omits them when every run is unstyled) counts as one | ||
| * unstyled run spanning the whole text. When both sides carry runs but | ||
| * their concatenated texts disagree (non-text inline content can make the | ||
| * preview text drift from the block text), positions can't be aligned, so | ||
| * detection backs off and reports no change. | ||
| */ | ||
| const diffPreviewRunFormatting = (base, revised) => { | ||
| if (base.previewRuns === void 0 && revised.previewRuns === void 0) return []; | ||
| const baseText = base.previewRuns === void 0 ? null : previewRunsText(base.previewRuns); | ||
| const revisedText = revised.previewRuns === void 0 ? null : previewRunsText(revised.previewRuns); | ||
| if (baseText !== null && revisedText !== null && baseText !== revisedText) return []; | ||
| const text = baseText ?? revisedText; | ||
| if (text === null || text.length === 0) return []; | ||
| const baseRuns = base.previewRuns ?? [{ text }]; | ||
| const revisedRuns = revised.previewRuns ?? [{ text }]; | ||
| const changed = /* @__PURE__ */ new Set(); | ||
| let baseRunIndex = 0; | ||
| let revisedRunIndex = 0; | ||
| let baseOffset = 0; | ||
| let revisedOffset = 0; | ||
| while (baseRunIndex < baseRuns.length && revisedRunIndex < revisedRuns.length) { | ||
| const baseRun = baseRuns[baseRunIndex]; | ||
| const revisedRun = revisedRuns[revisedRunIndex]; | ||
| if (!baseRun || !revisedRun) break; | ||
| const step = Math.min(baseRun.text.length - baseOffset, revisedRun.text.length - revisedOffset); | ||
| if (step > 0) { | ||
| for (const property of FORMAT_PROPERTIES) if (baseRun[property] !== revisedRun[property]) changed.add(property); | ||
| } | ||
| baseOffset += step; | ||
| revisedOffset += step; | ||
| if (baseOffset >= baseRun.text.length) { | ||
| baseRunIndex++; | ||
| baseOffset = 0; | ||
| } | ||
| if (revisedOffset >= revisedRun.text.length) { | ||
| revisedRunIndex++; | ||
| revisedOffset = 0; | ||
| } | ||
| } | ||
| return FORMAT_PROPERTIES.filter((property) => changed.has(property)); | ||
| }; | ||
| /** | ||
| * Floor for move detection: an added/deleted text must hold at least this | ||
| * many whitespace-separated words before an identical pair re-classifies as | ||
| * a move. Short boilerplate ("Confidential", a bare heading word) recurs | ||
| * throughout real documents and would otherwise pair as spurious moves. | ||
| */ | ||
| const MOVE_MINIMUM_WORD_COUNT = 3; | ||
| const meetsMoveWordCount = (text) => { | ||
| const words = text.matchAll(/\S+/gu); | ||
| let count = 0; | ||
| while (!words.next().done) if (++count >= MOVE_MINIMUM_WORD_COUNT) return true; | ||
| return false; | ||
| }; | ||
| /** | ||
| * Re-classify `deleted` + `added` pairs with identical text as | ||
| * `movedFrom` / `movedTo` entries sharing a `moveGroupId`, in place, so each | ||
| * side keeps its slot in the revised-side document order. Matching is FIFO | ||
| * per text, so duplicated boilerplate above the word floor pairs | ||
| * first-to-first rather than fanning out. | ||
| */ | ||
| const detectMoves = (changes, counts) => { | ||
| const deletedIndexesByText = /* @__PURE__ */ new Map(); | ||
| changes.forEach((change, index) => { | ||
| if (change.type === "deleted" && meetsMoveWordCount(change.text)) { | ||
| const queue = deletedIndexesByText.get(change.text) ?? []; | ||
| queue.push(index); | ||
| deletedIndexesByText.set(change.text, queue); | ||
| } | ||
| }); | ||
| if (deletedIndexesByText.size === 0) return; | ||
| let moveGroupId = 0; | ||
| changes.forEach((change, index) => { | ||
| if (change.type !== "added") return; | ||
| const deletedIndex = deletedIndexesByText.get(change.text)?.shift(); | ||
| if (deletedIndex === void 0) return; | ||
| const deleted = changes[deletedIndex]; | ||
| if (!deleted || deleted.type !== "deleted") return; | ||
| moveGroupId++; | ||
| changes[deletedIndex] = { | ||
| type: "movedFrom", | ||
| blockId: deleted.blockId, | ||
| kind: deleted.kind, | ||
| text: deleted.text, | ||
| moveGroupId | ||
| }; | ||
| changes[index] = { | ||
| type: "movedTo", | ||
| blockId: change.blockId, | ||
| kind: change.kind, | ||
| text: change.text, | ||
| moveGroupId | ||
| }; | ||
| counts.deleted--; | ||
| counts.added--; | ||
| counts.moved++; | ||
| }); | ||
| }; | ||
| /** | ||
| * Compare two `.docx` buffers and return a structured, block-level diff. | ||
| * See the module doc comment for the as-accepted comparison semantics, the | ||
| * three-pass alignment algorithm, move detection, and format-only change | ||
| * detection. | ||
| */ | ||
| const compareDocxVersions = async (base, revised) => { | ||
| const [baseReviewer, revisedReviewer] = await Promise.all([FolioDocxReviewer.fromBuffer(base), FolioDocxReviewer.fromBuffer(revised)]); | ||
| const baseBlocks = baseReviewer.snapshot().blocks; | ||
| const revisedBlocks = revisedReviewer.snapshot().blocks; | ||
| const changes = []; | ||
| const counts = { | ||
| added: 0, | ||
| deleted: 0, | ||
| modified: 0, | ||
| formatChanged: 0, | ||
| moved: 0, | ||
| unchanged: 0 | ||
| }; | ||
| for (const event of alignFolioBlocks(baseBlocks, revisedBlocks)) { | ||
| if (event.type === "pair") { | ||
| const { baseBlock, revisedBlock } = event; | ||
| if (baseBlock.text !== revisedBlock.text) { | ||
| counts.modified++; | ||
| changes.push({ | ||
| type: "modified", | ||
| blockId: revisedBlock.id, | ||
| kind: revisedBlock.kind, | ||
| segments: diffWordSegments(baseBlock.text, revisedBlock.text) | ||
| }); | ||
| continue; | ||
| } | ||
| const changedProperties = diffPreviewRunFormatting(baseBlock, revisedBlock); | ||
| if (changedProperties.length > 0) { | ||
| counts.formatChanged++; | ||
| changes.push({ | ||
| type: "formatChanged", | ||
| blockId: revisedBlock.id, | ||
| kind: revisedBlock.kind, | ||
| text: revisedBlock.text, | ||
| changedProperties | ||
| }); | ||
| continue; | ||
| } | ||
| counts.unchanged++; | ||
| continue; | ||
| } | ||
| if (event.type === "baseOnly") { | ||
| counts.deleted++; | ||
| changes.push({ | ||
| type: "deleted", | ||
| blockId: event.block.id, | ||
| kind: event.block.kind, | ||
| text: event.block.text | ||
| }); | ||
| continue; | ||
| } | ||
| counts.added++; | ||
| changes.push({ | ||
| type: "added", | ||
| blockId: event.block.id, | ||
| kind: event.block.kind, | ||
| text: event.block.text | ||
| }); | ||
| } | ||
| detectMoves(changes, counts); | ||
| return { | ||
| changes, | ||
| summaryCounts: counts | ||
| }; | ||
| }; | ||
| //#endregion | ||
| export { alignFolioBlocks, compareDocxVersions, exceedsLcsBudget }; |
@@ -26,11 +26,5 @@ import { FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js"; | ||
| }; | ||
| declare const applyFolioAIEditOperations: ({ | ||
| view, | ||
| snapshot, | ||
| operations, | ||
| mode, | ||
| author, | ||
| createCommentId | ||
| }: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyResult; | ||
| declare const applyFolioAIEditOperations: (options: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyResult; | ||
| declare const previewFolioAIEditOperations: (options: ApplyFolioAIEditOperationsOptions) => FolioAIEditApplyResult; | ||
| //#endregion | ||
| export { FolioAIEditView, applyFolioAIEditOperations }; | ||
| export { FolioAIEditView, applyFolioAIEditOperations, previewFolioAIEditOperations }; |
+94
-14
@@ -196,3 +196,3 @@ import { getFolioParaIdFromBlockId } from "../types/block-id.js"; | ||
| }; | ||
| const applyFolioAIEditOperations = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", createCommentId }) => { | ||
| const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode = "tracked-changes", author = "AI", createCommentId, revisionIdSeed }) => { | ||
| const applied = []; | ||
@@ -214,2 +214,9 @@ const skipped = []; | ||
| for (const [index, operation] of operations.entries()) { | ||
| if (mode === "tracked-changes" && operation.type === "formatRange") { | ||
| skipped.push({ | ||
| id: operation.id, | ||
| reason: "unsupportedMode" | ||
| }); | ||
| continue; | ||
| } | ||
| const commentText = getOperationCommentText(operation); | ||
@@ -249,3 +256,3 @@ if (commentText !== void 0 && (!commentType || createCommentId === void 0)) { | ||
| let tr = view.state.tr; | ||
| let revisionSeed = nextRevisionSeed(resolved.length); | ||
| let revisionSeed = revisionIdSeed ?? nextRevisionSeed(resolved.length); | ||
| const date = (/* @__PURE__ */ new Date()).toISOString(); | ||
@@ -260,3 +267,4 @@ for (const item of resolved.toSorted((left, right) => { | ||
| switch (item.operation.type) { | ||
| case "replaceInBlock": { | ||
| case "replaceInBlock": | ||
| case "replaceRange": { | ||
| const revisionIdDelete = revisionSeed++; | ||
@@ -277,2 +285,13 @@ const revisionIdInsert = revisionSeed++; | ||
| } | ||
| case "commentOnRange": | ||
| if (commentMark) tr = tr.addMark(item.from, item.to, commentMark); | ||
| break; | ||
| case "formatRange": | ||
| for (const [name, enabled] of Object.entries(item.operation.formatting)) { | ||
| const markType = view.state.schema.marks[name]; | ||
| if (!markType) continue; | ||
| if (enabled) tr = tr.addMark(item.from, item.to, name === "underline" ? markType.create({ style: "single" }) : markType.create()); | ||
| else tr = tr.removeMark(item.from, item.to, markType); | ||
| } | ||
| break; | ||
| case "replaceBlock": { | ||
@@ -322,3 +341,3 @@ const revisionIdDelete = revisionSeed++; | ||
| id: item.operation.id, | ||
| reason: "unsupportedBlock" | ||
| reason: "unsupportedMode" | ||
| }); | ||
@@ -347,2 +366,3 @@ continue; | ||
| attrs["listLevelNumFmts"] = null; | ||
| attrs["listLevelStarts"] = null; | ||
| attrs["listAbstractNumId"] = null; | ||
@@ -360,3 +380,3 @@ attrs["listStartOverride"] = null; | ||
| id: item.operation.id, | ||
| reason: "unsupportedBlock" | ||
| reason: "unsupportedMode" | ||
| }); | ||
@@ -422,6 +442,27 @@ continue; | ||
| }; | ||
| const applyFolioAIEditOperations = (options) => applyFolioAIEditOperationsInternal(options); | ||
| const previewFolioAIEditOperations = (options) => { | ||
| const { view, createCommentId, ...applyOptions } = options; | ||
| let previewCommentId = -1; | ||
| const previewView = { | ||
| state: view.state, | ||
| dispatch: (transaction) => { | ||
| previewView.state = previewView.state.apply(transaction); | ||
| } | ||
| }; | ||
| const result = applyFolioAIEditOperationsInternal({ | ||
| ...applyOptions, | ||
| view: previewView, | ||
| ...createCommentId !== void 0 && { createCommentId: () => previewCommentId-- }, | ||
| revisionIdSeed: -1e9 | ||
| }); | ||
| return { | ||
| applied: result.applied.map(({ id }) => ({ id })), | ||
| skipped: result.skipped | ||
| }; | ||
| }; | ||
| const applyTextReplacement = ({ tr, item, mode, author, date, revisionIdDelete, revisionIdInsert, commentMark }) => { | ||
| let nextTr = tr; | ||
| const replacement = stripInlineEmphasisMarkers((() => { | ||
| if (item.operation.type === "replaceInBlock") return item.operation.replace; | ||
| if (item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") return item.operation.replace; | ||
| if (item.operation.type === "replaceBlock") return item.operation.text; | ||
@@ -431,3 +472,3 @@ return ""; | ||
| if (mode === "direct") { | ||
| if (item.operation.type === "replaceInBlock" && hasInlineEmphasis(item.operation.replace)) { | ||
| if ((item.operation.type === "replaceInBlock" || item.operation.type === "replaceRange") && hasInlineEmphasis(item.operation.replace)) { | ||
| const content = buildEmphasisInlineContent(nextTr.doc.type.schema, item.operation.replace, commentMark ? [commentMark] : []); | ||
@@ -460,2 +501,5 @@ return nextTr.replaceWith(item.from, item.to, content); | ||
| if (sourceCleanStart === -1) sourceText = null; | ||
| } else if (item.operation.type === "replaceRange") { | ||
| sourceCleanStart = item.operation.range.startOffset; | ||
| sourceText = cleanBlock.text.slice(sourceCleanStart, item.operation.range.endOffset); | ||
| } else if (item.operation.type === "replaceBlock") { | ||
@@ -532,3 +576,4 @@ sourceText = cleanBlock.text; | ||
| const resolveOperation = ({ snapshot, operation, liveBlocks, liveBlocksByParaId, doc }) => { | ||
| const anchor = snapshot.anchors[operation.blockId]; | ||
| const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId; | ||
| const anchor = snapshot.anchors[blockId]; | ||
| if (!anchor) return { | ||
@@ -538,3 +583,3 @@ type: "skip", | ||
| }; | ||
| const encodedParaId = getFolioParaIdFromBlockId(operation.blockId); | ||
| const encodedParaId = getFolioParaIdFromBlockId(blockId); | ||
| let live; | ||
@@ -548,3 +593,3 @@ if (encodedParaId !== null) { | ||
| } else { | ||
| const ordinal = ordinalAmongSameHash(snapshot, operation.blockId); | ||
| const ordinal = ordinalAmongSameHash(snapshot, blockId); | ||
| if (ordinal < 0) return { | ||
@@ -565,6 +610,40 @@ type: "skip", | ||
| const currentText = cleanBlock.text; | ||
| if (hashFolioAIBlockText(normalizeFolioAIBlockText(currentText)) !== anchor.textHash) return { | ||
| const currentTextHash = hashFolioAIBlockText(normalizeFolioAIBlockText(currentText)); | ||
| if (operation.precondition !== void 0 && currentTextHash !== operation.precondition.blockTextHash) return { | ||
| type: "skip", | ||
| reason: "preconditionFailed" | ||
| }; | ||
| if (currentTextHash !== anchor.textHash) return { | ||
| type: "skip", | ||
| reason: "changedBlock" | ||
| }; | ||
| if (operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange") { | ||
| const { startOffset, endOffset, selectedTextHash } = operation.range; | ||
| const from = cleanBlock.offsets[startOffset]; | ||
| const to = cleanBlock.offsets[endOffset]; | ||
| if (from === void 0 || to === void 0) return { | ||
| type: "skip", | ||
| reason: "staleRange" | ||
| }; | ||
| const selectedText = currentText.slice(startOffset, endOffset); | ||
| if (hashFolioAIBlockText(selectedText) !== selectedTextHash) return { | ||
| type: "skip", | ||
| reason: "staleRange" | ||
| }; | ||
| if (operation.type === "replaceRange" && selectedText === operation.replace) return { | ||
| type: "skip", | ||
| reason: "noopOperation" | ||
| }; | ||
| return { | ||
| type: "resolved", | ||
| operation: { | ||
| operation, | ||
| from, | ||
| to, | ||
| blockFrom, | ||
| blockTo, | ||
| blockNode | ||
| } | ||
| }; | ||
| } | ||
| if (operation.type === "insertAfterBlock" || operation.type === "insertBeforeBlock") { | ||
@@ -641,3 +720,4 @@ if (operation.text.length === 0 && operation.pageBreakBefore !== true) return { | ||
| }; | ||
| const range = resolveTextInCleanBlock(cleanBlock, getOperationQuote(operation) || currentText); | ||
| const quote = getOperationQuote(operation); | ||
| const range = resolveTextInCleanBlock(cleanBlock, quote || currentText); | ||
| if (range.type !== "resolved") return range; | ||
@@ -693,3 +773,3 @@ return { | ||
| }; | ||
| const getOperationCommentText = (operation) => operation.comment?.text; | ||
| const getOperationCommentText = (operation) => "comment" in operation ? operation.comment?.text : void 0; | ||
| const getOperationQuote = (operation) => { | ||
@@ -700,2 +780,2 @@ if (operation.type === "replaceInBlock") return operation.find; | ||
| //#endregion | ||
| export { applyFolioAIEditOperations }; | ||
| export { applyFolioAIEditOperations, previewFolioAIEditOperations }; |
| import { FolioAIBlock, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js"; | ||
| import { document_d_exports } from "../types/document.js"; | ||
| import { FolioDocumentOperationBatch, FolioDocumentOperationResult } from "../document-operations.js"; | ||
| import { FolioReviewChange, FolioReviewChangeKind } from "./read.js"; | ||
@@ -21,2 +22,4 @@ | ||
| }; | ||
| /** Options for {@link FolioDocxReviewer.applyDocumentOperations}. */ | ||
| type FolioApplyDocumentOperationsOptions = Omit<FolioApplyOperationsOptions, "mode">; | ||
| /** Options for {@link FolioDocxReviewer.getContentAsText}. */ | ||
@@ -31,2 +34,15 @@ type FolioGetContentAsTextOptions = { | ||
| }; | ||
| type FolioDocumentStoryHandle = { | ||
| type: "main"; | ||
| } | { | ||
| type: "header" | "footer"; | ||
| relationshipId: string; | ||
| } | { | ||
| type: "footnote" | "endnote"; | ||
| noteId: number; | ||
| }; | ||
| type FolioDocumentStory = { | ||
| handle: FolioDocumentStoryHandle; | ||
| text: string; | ||
| }; | ||
| /** Filter for {@link FolioDocxReviewer.getChanges}. */ | ||
@@ -111,2 +127,9 @@ type FolioReviewChangeFilter = { | ||
| /** | ||
| * Apply a versioned document-operation batch against the current state. | ||
| * This is the contract entry point for serialized callers; the legacy | ||
| * {@link applyOperations} method delegates here so both APIs keep identical | ||
| * edit semantics. | ||
| */ | ||
| applyDocumentOperations(batch: FolioDocumentOperationBatch, options?: FolioApplyDocumentOperationsOptions): FolioDocumentOperationResult; | ||
| /** | ||
| * The body blocks with their stable ids, in document order — the same | ||
@@ -137,2 +160,6 @@ * `FolioAIBlock` shape {@link snapshot} builds, reused verbatim so the ids | ||
| getNotesAsText(): string; | ||
| /** Discover every readable document story through a typed, serializable handle. */ | ||
| listStories(): FolioDocumentStory[]; | ||
| /** Read one discovered story; returns null when its handle is no longer present. */ | ||
| readStory(handle: FolioDocumentStoryHandle): FolioDocumentStory | null; | ||
| /** | ||
@@ -217,5 +244,7 @@ * The tracked changes (insertions and deletions) present in the body, read | ||
| * Count every tracked change an accept-all / reject-all sweep resolves: the | ||
| * inline insertion / deletion groups {@link getChanges} enumerates, plus | ||
| * paragraph-level changes (`pPrMark`, `_propertyChanges`) that live on | ||
| * paragraph attrs rather than inline marks. | ||
| * inline insertion / deletion groups {@link getChanges} enumerates, plus the | ||
| * property-change records living on node attrs rather than inline marks — | ||
| * paragraph-level (`pPrMark`, `_propertyChanges`, the inline sectPr's | ||
| * `propertyChanges`) and table-level (`tblPrChange` / `trPrChange` / | ||
| * `tcPrChange`). | ||
| */ | ||
@@ -258,2 +287,2 @@ private countTrackedChanges; | ||
| //#endregion | ||
| export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioGetContentAsTextOptions, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer }; | ||
| export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyDocumentOperationsOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocxReviewer, FolioDocxReviewerOptions, FolioGetContentAsTextOptions, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer }; |
@@ -0,7 +1,7 @@ | ||
| import { buildAnnotatedBlockText } from "./clean-text.js"; | ||
| import { createFolioAIEditSnapshot, normalizeFolioAIBlockText } from "./snapshot.js"; | ||
| import { applyFolioDocumentOperations } from "../document-operations.js"; | ||
| import { getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js"; | ||
| import { deterministicHexId } from "../utils/hexId.js"; | ||
| import { repackDocx } from "../docx/rezip.js"; | ||
| import { buildAnnotatedBlockText } from "./clean-text.js"; | ||
| import { createFolioAIEditSnapshot, normalizeFolioAIBlockText } from "./snapshot.js"; | ||
| import { applyFolioAIEditOperations } from "./apply.js"; | ||
| import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js"; | ||
@@ -213,3 +213,19 @@ import { createReply } from "../docx/replyToComment.js"; | ||
| applyOperations(operations, options = {}) { | ||
| const snapshot = options.snapshot ?? this.snapshot(); | ||
| const { applied, skipped } = this.applyDocumentOperations({ | ||
| version: 1, | ||
| operations, | ||
| mode: options.mode ?? "tracked-changes" | ||
| }, { ...options.snapshot !== void 0 && { snapshot: options.snapshot } }); | ||
| return { | ||
| applied, | ||
| skipped | ||
| }; | ||
| } | ||
| /** | ||
| * Apply a versioned document-operation batch against the current state. | ||
| * This is the contract entry point for serialized callers; the legacy | ||
| * {@link applyOperations} method delegates here so both APIs keep identical | ||
| * edit semantics. | ||
| */ | ||
| applyDocumentOperations(batch, options = {}) { | ||
| const view = { | ||
@@ -221,7 +237,6 @@ state: this.state, | ||
| }; | ||
| const result = applyFolioAIEditOperations({ | ||
| const result = applyFolioDocumentOperations({ | ||
| view, | ||
| snapshot, | ||
| operations, | ||
| mode: options.mode ?? "tracked-changes", | ||
| snapshot: options.snapshot ?? this.snapshot(), | ||
| batch, | ||
| author: this.author, | ||
@@ -264,3 +279,4 @@ createCommentId: (text) => { | ||
| const node = from === void 0 ? null : this.state.doc.nodeAt(from); | ||
| return formatBlockLine(block, node ? buildAnnotatedBlockText(node) : block.text); | ||
| const text = node ? buildAnnotatedBlockText(node) : block.text; | ||
| return formatBlockLine(block, text); | ||
| }).join("\n"); | ||
@@ -299,2 +315,46 @@ } | ||
| } | ||
| /** Discover every readable document story through a typed, serializable handle. */ | ||
| listStories() { | ||
| const pkg = this.baseDocument.package; | ||
| const stories = [{ | ||
| handle: { type: "main" }, | ||
| text: this.getContentAsText() | ||
| }]; | ||
| for (const [relationshipId, header] of pkg.headers ?? []) stories.push({ | ||
| handle: { | ||
| type: "header", | ||
| relationshipId | ||
| }, | ||
| text: normalizeFolioAIBlockText(getHeaderFooterText(header)) | ||
| }); | ||
| for (const [relationshipId, footer] of pkg.footers ?? []) stories.push({ | ||
| handle: { | ||
| type: "footer", | ||
| relationshipId | ||
| }, | ||
| text: normalizeFolioAIBlockText(getHeaderFooterText(footer)) | ||
| }); | ||
| for (const footnote of pkg.footnotes ?? []) if (!isSeparatorFootnote(footnote)) stories.push({ | ||
| handle: { | ||
| type: "footnote", | ||
| noteId: footnote.id | ||
| }, | ||
| text: normalizeFolioAIBlockText(getFootnoteText(footnote)) | ||
| }); | ||
| for (const endnote of pkg.endnotes ?? []) if (!isSeparatorEndnote(endnote)) stories.push({ | ||
| handle: { | ||
| type: "endnote", | ||
| noteId: endnote.id | ||
| }, | ||
| text: normalizeFolioAIBlockText(getEndnoteText(endnote)) | ||
| }); | ||
| return stories; | ||
| } | ||
| /** Read one discovered story; returns null when its handle is no longer present. */ | ||
| readStory(handle) { | ||
| const stories = this.listStories(); | ||
| if (handle.type === "main") return stories.at(0) ?? null; | ||
| if (handle.type === "header" || handle.type === "footer") return stories.find(({ handle: candidate }) => candidate.type === handle.type && candidate.relationshipId === handle.relationshipId) ?? null; | ||
| return stories.find(({ handle: candidate }) => (candidate.type === "footnote" || candidate.type === "endnote") && candidate.type === handle.type && candidate.noteId === handle.noteId) ?? null; | ||
| } | ||
| /** | ||
@@ -471,14 +531,22 @@ * The tracked changes (insertions and deletions) present in the body, read | ||
| * Count every tracked change an accept-all / reject-all sweep resolves: the | ||
| * inline insertion / deletion groups {@link getChanges} enumerates, plus | ||
| * paragraph-level changes (`pPrMark`, `_propertyChanges`) that live on | ||
| * paragraph attrs rather than inline marks. | ||
| * inline insertion / deletion groups {@link getChanges} enumerates, plus the | ||
| * property-change records living on node attrs rather than inline marks — | ||
| * paragraph-level (`pPrMark`, `_propertyChanges`, the inline sectPr's | ||
| * `propertyChanges`) and table-level (`tblPrChange` / `trPrChange` / | ||
| * `tcPrChange`). | ||
| */ | ||
| countTrackedChanges() { | ||
| const hasEntries = (value) => Array.isArray(value) && value.length > 0; | ||
| let count = this.getChanges().length; | ||
| this.state.doc.descendants((node) => { | ||
| if (node.type.name !== "paragraph") return; | ||
| const typeName = node.type.name; | ||
| if (typeName === "table" && hasEntries(node.attrs["tblPrChange"])) count += 1; | ||
| if (typeName === "tableRow" && hasEntries(node.attrs["trPrChange"])) count += 1; | ||
| if ((typeName === "tableCell" || typeName === "tableHeader") && hasEntries(node.attrs["tcPrChange"])) count += 1; | ||
| if (typeName !== "paragraph") return; | ||
| const pPrMark = node.attrs["pPrMark"]; | ||
| if (pPrMark !== null && pPrMark !== void 0) count += 1; | ||
| const propertyChanges = node.attrs["_propertyChanges"]; | ||
| if (Array.isArray(propertyChanges) && propertyChanges.length > 0) count += 1; | ||
| if (hasEntries(node.attrs["_propertyChanges"])) count += 1; | ||
| const sectionProperties = node.attrs["_sectionProperties"]; | ||
| if (hasEntries(sectionProperties?.propertyChanges)) count += 1; | ||
| return false; | ||
@@ -485,0 +553,0 @@ }); |
@@ -1,8 +0,10 @@ | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./types.js"; | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAIInlineFormatting, FolioAISignatureParty, FolioAITextRangeHandle } from "./types.js"; | ||
| import { FolioAIEditView, applyFolioAIEditOperations } from "./apply.js"; | ||
| import { buildAnnotatedBlockText } from "./clean-text.js"; | ||
| import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js"; | ||
| import { FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js"; | ||
| import { FolioDocumentStory, FolioDocumentStoryHandle } from "./headless.js"; | ||
| import { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js"; | ||
| import { getFolioParaIdFromBlockId } from "../types/block-id.js"; | ||
| import { WordDiffSegment, diffWordSegments } from "./word-diff.js"; | ||
| export { type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAISignatureParty, type FolioCommentAnchor, type FolioReviewChange, type FolioReviewChangeKind, type WordDiffSegment, applyFolioAIEditOperations, buildAnnotatedBlockText, createFolioAIEditSnapshot, diffWordSegments, getCommentAnchorsFromDoc, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, normalizeFolioAIBlockText }; | ||
| export { type ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAIInlineFormatting, type FolioAISignatureParty, type FolioAITextRangeHandle, type FolioCommentAnchor, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type FolioDocumentStory, type FolioDocumentStoryHandle, type FolioReviewChange, type FolioReviewChangeKind, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, applyFolioAIEditOperations, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, buildAnnotatedBlockText, createFolioAIEditSnapshot, createFolioAITextRangeHandle, diffWordSegments, getCommentAnchorsFromDoc, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch }; |
| import { getFolioParaIdFromBlockId } from "../types/block-id.js"; | ||
| import { buildAnnotatedBlockText } from "./clean-text.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js"; | ||
| import { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js"; | ||
| import { diffWordSegments } from "./word-diff.js"; | ||
| import { applyFolioAIEditOperations } from "./apply.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js"; | ||
| import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js"; | ||
| export { applyFolioAIEditOperations, buildAnnotatedBlockText, createFolioAIEditSnapshot, diffWordSegments, getCommentAnchorsFromDoc, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, normalizeFolioAIBlockText }; | ||
| export { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioAIEditOperations, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, buildAnnotatedBlockText, createFolioAIEditSnapshot, createFolioAITextRangeHandle, diffWordSegments, getCommentAnchorsFromDoc, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch }; |
@@ -1,2 +0,2 @@ | ||
| import { FolioAIEditSnapshot } from "./types.js"; | ||
| import { FolioAIEditSnapshot, FolioAITextRangeHandle } from "./types.js"; | ||
| import { Node } from "prosemirror-model"; | ||
@@ -7,4 +7,16 @@ | ||
| declare const hashFolioAIBlockText: (text: string) => string; | ||
| type CreateFolioAITextRangeHandleOptions = { | ||
| blockId: string; | ||
| text: string; | ||
| startOffset: number; | ||
| endOffset: number; | ||
| }; | ||
| declare const createFolioAITextRangeHandle: ({ | ||
| blockId, | ||
| text, | ||
| startOffset, | ||
| endOffset | ||
| }: CreateFolioAITextRangeHandleOptions) => FolioAITextRangeHandle | null; | ||
| declare const createFolioAIEditSnapshot: (doc: Node) => FolioAIEditSnapshot; | ||
| //#endregion | ||
| export { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText }; | ||
| export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText }; |
@@ -10,2 +10,13 @@ import { deriveBlockId } from "../types/block-id.js"; | ||
| }; | ||
| const createFolioAITextRangeHandle = ({ blockId, text, startOffset, endOffset }) => { | ||
| if (blockId.length === 0 || !Number.isInteger(startOffset) || !Number.isInteger(endOffset) || startOffset < 0 || endOffset <= startOffset || endOffset > text.length) return null; | ||
| return { | ||
| type: "textRange", | ||
| story: "main", | ||
| blockId, | ||
| startOffset, | ||
| endOffset, | ||
| selectedTextHash: hashFolioAIBlockText(text.slice(startOffset, endOffset)) | ||
| }; | ||
| }; | ||
| const createFolioAIEditSnapshot = (doc) => { | ||
@@ -196,2 +207,2 @@ const draftBlocks = []; | ||
| //#endregion | ||
| export { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText }; | ||
| export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText }; |
@@ -48,3 +48,24 @@ //#region src/ai-edits/types.d.ts | ||
| }; | ||
| type FolioAIEditPrecondition = { | ||
| blockTextHash: string; | ||
| }; | ||
| /** | ||
| * A serializable range over the visible, post-tracked-changes text of one | ||
| * block. Offsets are zero-based UTF-16 boundaries, matching JavaScript string | ||
| * slicing; `selectedTextHash` makes a shifted or changed selection fail stale. | ||
| */ | ||
| type FolioAITextRangeHandle = { | ||
| type: "textRange"; | ||
| story: "main"; | ||
| blockId: string; | ||
| startOffset: number; | ||
| endOffset: number; | ||
| selectedTextHash: string; | ||
| }; | ||
| type FolioAIInlineFormatting = { | ||
| bold?: boolean; | ||
| italic?: boolean; | ||
| underline?: boolean; | ||
| }; | ||
| /** | ||
| * A party in an `insertSignatureTable` op. Mirrors the | ||
@@ -60,3 +81,5 @@ * `signatureTable` helper in `docx-core/legal-source/compile.ts`: | ||
| }; | ||
| type FolioAIEditOperation = FolioAIEditReviewMeta & ({ | ||
| type FolioAIEditOperation = FolioAIEditReviewMeta & { | ||
| precondition?: FolioAIEditPrecondition; | ||
| } & ({ | ||
| id: string; | ||
@@ -70,2 +93,18 @@ type: "replaceInBlock"; | ||
| id: string; | ||
| type: "replaceRange"; | ||
| range: FolioAITextRangeHandle; | ||
| replace: string; | ||
| comment?: FolioAIComment; | ||
| } | { | ||
| id: string; | ||
| type: "commentOnRange"; | ||
| range: FolioAITextRangeHandle; | ||
| comment: FolioAIComment; | ||
| } | { | ||
| id: string; | ||
| type: "formatRange"; | ||
| range: FolioAITextRangeHandle; | ||
| formatting: FolioAIInlineFormatting; | ||
| } | { | ||
| id: string; | ||
| type: "insertAfterBlock" | "insertBeforeBlock"; | ||
@@ -122,3 +161,3 @@ blockId: string; | ||
| type FolioAIEditApplyMode = "direct" | "tracked-changes"; | ||
| type FolioAIEditSkipReason = "missingBlock" | "changedBlock" | "ambiguousFind" | "missingFind" | "unsupportedBlock" | "emptyOperation" | ||
| type FolioAIEditSkipReason = "missingBlock" | "changedBlock" | "ambiguousFind" | "missingFind" | "unsupportedBlock" | "unsupportedMode" | "atomicBatchRejected" | "preconditionFailed" | "staleRange" | "emptyOperation" | ||
| /** | ||
@@ -160,2 +199,2 @@ * The operation would not change the document — find equals | ||
| //#endregion | ||
| export { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty }; | ||
| export { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAIInlineFormatting, FolioAISignatureParty, FolioAITextRangeHandle }; |
@@ -1,4 +0,5 @@ | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "../ai-edits/types.js"; | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "../ai-edits/types.js"; | ||
| import { applyFolioAIEditOperations } from "../ai-edits/apply.js"; | ||
| import { document_d_exports } from "../types/document.js"; | ||
| import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "../ai-edits/snapshot.js"; | ||
@@ -29,2 +30,2 @@ import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "../types/block-id.js"; | ||
| type Document = document_d_exports.Document; | ||
| export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocxCompatibility, type EmbeddedFont, type EmbeddedFontParts, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type ImageMeta, type ImageRef, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; | ||
| export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocxCompatibility, type EmbeddedFont, type EmbeddedFontParts, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type ImageMeta, type ImageRef, InvalidFolioDocumentOperationBatchError, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; |
@@ -0,4 +1,8 @@ | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "../types/block-id.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "../ai-edits/snapshot.js"; | ||
| import { diffWordSegments } from "../ai-edits/word-diff.js"; | ||
| import { applyFolioAIEditOperations } from "../ai-edits/apply.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js"; | ||
| import { createEmptyDocument } from "../utils/createDocument.js"; | ||
| import { createDocx } from "../docx/rezip.js"; | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "../types/block-id.js"; | ||
| import { DEFAULT_AI_SUGGESTION_PRESETS } from "../ai-suggestions/types.js"; | ||
@@ -8,5 +12,2 @@ import { buildPositionalText } from "../ai-suggestions/text-positions.js"; | ||
| import { applySuggestions } from "../ai-suggestions/apply.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "../ai-edits/snapshot.js"; | ||
| import { diffWordSegments } from "../ai-edits/word-diff.js"; | ||
| import { applyFolioAIEditOperations } from "../ai-edits/apply.js"; | ||
| import { setAISuggestionsMeta, setFocusedSuggestionMeta } from "../prosemirror/plugins/aiSuggestionDecorations.js"; | ||
@@ -25,2 +26,2 @@ import { getGoogleFontsEnabled, setGoogleFontsEnabled } from "../utils/fontResolver.js"; | ||
| import { extractEmbeddedFonts, getEmbeddedFontFaces } from "../fonts/embeddedFonts.js"; | ||
| export { DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; | ||
| export { DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; |
@@ -48,2 +48,3 @@ import { document_d_exports } from "../types/document.js"; | ||
| defaultTabStop: number | undefined; | ||
| mirrorMargins: boolean; | ||
| styles: document_d_exports.StyleDefinitions | null | undefined; | ||
@@ -50,0 +51,0 @@ layout: Layout | null; |
@@ -5,3 +5,3 @@ import "../layout-engine/types.js"; | ||
| import { toFlowBlocks } from "../layout-bridge/convert/toFlowBlocks.js"; | ||
| import { recordLayoutComplete, recordLayoutError, recordLayoutPhase } from "../layout-engine/layoutInstrumentation.js"; | ||
| import { recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart } from "../layout-engine/layoutInstrumentation.js"; | ||
| import { buildBookmarkPageMap } from "../fields/bookmarkPages.js"; | ||
@@ -13,21 +13,13 @@ import { buildBookmarkText } from "../fields/bookmarkText.js"; | ||
| import { installCanvasMeasureProvider } from "../layout-engine/measure/measureContainer.js"; | ||
| import { buildFootnoteContentMap, collectFootnoteRefs } from "../layout-bridge/convert/footnoteLayout.js"; | ||
| import { buildFootnoteContentMap, collectEndnoteRefs, collectFootnoteRefs, computeNoteDisplayNumbers, remapNoteMarkerText } from "../layout-bridge/convert/footnoteLayout.js"; | ||
| import { applyTemplatePreviewToBlocks } from "../layout-bridge/convert/templatePreviewFlow.js"; | ||
| import { layoutDocument } from "../layout-engine/index.js"; | ||
| import { measureBlocks, measureSingleBlockWithoutFloatingZones } from "../layout-engine/measure/measureBlocks.js"; | ||
| import { computeFirstPageHeaderFooterMarginExtender, computeHeaderFooterMarginExtender, extendSectionBreakMargins } from "../paged-layout/headerFooterMargins.js"; | ||
| import { tryBuildIncrementalMeasures } from "../paged-layout/incrementalMeasure.js"; | ||
| import { computePerBlockMeasureInputs } from "../paged-layout/sectionBlockWidths.js"; | ||
| import { twipsToPixels } from "../paged-layout/sectionGeometry.js"; | ||
| import { getMargins, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js"; | ||
| import { getDocumentWatermark } from "../watermark/index.js"; | ||
| //#region src/controller/layoutPipeline.ts | ||
| function pageMarginsEqual(left, right) { | ||
| return left.top === right.top && left.right === right.right && left.bottom === right.bottom && left.left === right.left && left.header === right.header && left.footer === right.footer; | ||
| } | ||
| function optionalPageMarginsEqual(left, right) { | ||
| if (left === void 0 || right === void 0) return left === right; | ||
| return pageMarginsEqual(left, right); | ||
| } | ||
| function runLayoutPipeline(deps, state, options = {}) { | ||
| const { contentWidth, columns, pageSize, margins, pageGap, syncCoordinator, headerContent, footerContent, firstPageHeaderContent, firstPageFooterContent, headerContentRId, footerContentRId, firstPageHeaderContentRId, firstPageFooterContentRId, sectionHeaderFooterRefs, theme: _theme, sectionProperties, document, defaultTabStop, styles, layout, hfPMs, painter, pagesContainer, session, renderHfFromContentOrPm, renderHeaderFooterContentByRId, documentFontsAreLoaded, buildFootnoteRenderItems, describeInvalidHighlightMarks, emptyTemplatePreviewEntries: EMPTY_TEMPLATE_PREVIEW_ENTRIES } = deps; | ||
| const { contentWidth, columns, pageSize, margins, pageGap, syncCoordinator, headerContent, footerContent, firstPageHeaderContent, firstPageFooterContent, headerContentRId, footerContentRId, firstPageHeaderContentRId, firstPageFooterContentRId, sectionHeaderFooterRefs, theme: _theme, sectionProperties, document, defaultTabStop, mirrorMargins, styles, layout, hfPMs, painter, pagesContainer, session, renderHfFromContentOrPm, renderHeaderFooterContentByRId, documentFontsAreLoaded, buildFootnoteRenderItems, describeInvalidHighlightMarks, emptyTemplatePreviewEntries: EMPTY_TEMPLATE_PREVIEW_ENTRIES } = deps; | ||
| let outcome = {}; | ||
@@ -42,2 +34,3 @@ let pendingArtifacts; | ||
| const currentEpoch = syncCoordinator.getStateSeq(); | ||
| recordLayoutStart(reason); | ||
| syncCoordinator.onLayoutStart(); | ||
@@ -67,2 +60,10 @@ try { | ||
| const hasFootnotes = footnoteRefs.length > 0 && documentFootnotes !== void 0; | ||
| const footnoteDisplayNumbers = documentFootnotes ? computeNoteDisplayNumbers(documentFootnotes, footnoteRefs.map((ref) => ref.footnoteId)) : void 0; | ||
| const documentEndnotes = document?.package.endnotes; | ||
| const endnoteDisplayNumbers = documentEndnotes ? computeNoteDisplayNumbers(documentEndnotes, collectEndnoteRefs(newBlocks).map((ref) => ref.endnoteId)) : void 0; | ||
| newBlocks = remapNoteMarkerText(newBlocks, { | ||
| ...footnoteDisplayNumbers ? { footnoteNumbers: footnoteDisplayNumbers } : {}, | ||
| ...endnoteDisplayNumbers ? { endnoteNumbers: endnoteDisplayNumbers } : {} | ||
| }); | ||
| outcome.blocks = newBlocks; | ||
| const hfMetricsHeader = { | ||
@@ -97,64 +98,10 @@ section: "header", | ||
| let footerContentByRId = renderHeaderFooterContentByRId(document?.package.footers, hfPMs, contentWidth, hfMetricsFooter, hfOptions); | ||
| const hfExtenderContent = { | ||
| headerContent: headerContentForRender, | ||
| footerContent: footerContentForRender, | ||
| firstPageHeaderContent: firstPageHeaderForRender, | ||
| firstPageFooterContent: firstPageFooterForRender | ||
| }; | ||
| const buildSectionHfExtenderContent = () => { | ||
| if (!sectionHeaderFooterRefs) return; | ||
| return sectionHeaderFooterRefs.map((refs) => ({ | ||
| headerContent: pickSectionHeaderFooterContent(headerContentByRId, refs.headerDefault, refs.evenAndOddHeaders === true ? refs.headerEven : void 0), | ||
| footerContent: pickSectionHeaderFooterContent(footerContentByRId, refs.footerDefault, refs.evenAndOddHeaders === true ? refs.footerEven : void 0), | ||
| firstPageHeaderContent: refs.titlePg === true ? headerContentByRId?.get(refs.headerFirst ?? "") : void 0, | ||
| firstPageFooterContent: refs.titlePg === true ? footerContentByRId?.get(refs.footerFirst ?? "") : void 0 | ||
| })); | ||
| }; | ||
| const hfWarn = (msg) => { | ||
| console.warn(`[PagedEditor] ${msg}`); | ||
| }; | ||
| const extendForHfOverflow = computeHeaderFooterMarginExtender({ | ||
| ...hfExtenderContent, | ||
| pageSize, | ||
| warn: hfWarn | ||
| }); | ||
| const extendForFirstPage = computeFirstPageHeaderFooterMarginExtender({ | ||
| ...hfExtenderContent, | ||
| pageSize, | ||
| warn: hfWarn | ||
| }); | ||
| let effectiveMargins = extendForHfOverflow(margins); | ||
| let effectiveFirstPageMargins = hasTitlePg ? extendForFirstPage(margins) : void 0; | ||
| const sectionBreaks = newBlocks.filter((block) => block.kind === "sectionBreak"); | ||
| const originalSectionBreakMargins = /* @__PURE__ */ new Map(); | ||
| for (const sectionBreak of sectionBreaks) originalSectionBreakMargins.set(sectionBreak.id, sectionBreak.margins ? { ...sectionBreak.margins } : void 0); | ||
| const restoreSectionBreakMargins = () => { | ||
| for (const sectionBreak of sectionBreaks) { | ||
| const originalMargins = originalSectionBreakMargins.get(sectionBreak.id); | ||
| if (originalMargins === void 0) { | ||
| delete sectionBreak.margins; | ||
| continue; | ||
| } | ||
| sectionBreak.margins = { ...originalMargins }; | ||
| } | ||
| }; | ||
| const applySectionBreakMargins = (content, bodyMargins) => { | ||
| restoreSectionBreakMargins(); | ||
| extendSectionBreakMargins(sectionBreaks, { | ||
| content, | ||
| sectionContent: buildSectionHfExtenderContent(), | ||
| bodyPageSize: pageSize, | ||
| bodyMargins, | ||
| warn: hfWarn | ||
| }); | ||
| }; | ||
| applySectionBreakMargins(hfExtenderContent, effectiveMargins); | ||
| recordPhaseDuration("header-footer", phaseStartedAt); | ||
| phaseStartedAt = performance.now(); | ||
| let bodyLayoutConfig = { | ||
| const bodyLayoutConfig = { | ||
| pageSize, | ||
| margins: effectiveMargins | ||
| margins | ||
| }; | ||
| if (columns !== void 0) bodyLayoutConfig.columns = columns; | ||
| let blockMeasureInputs = computePerBlockMeasureInputs({ | ||
| const blockMeasureInputs = computePerBlockMeasureInputs({ | ||
| blocks: newBlocks, | ||
@@ -164,3 +111,3 @@ bodyConfig: bodyLayoutConfig, | ||
| }); | ||
| let blockWidths = blockMeasureInputs.widths; | ||
| const blockWidths = blockMeasureInputs.widths; | ||
| const previousArtifacts = session.artifacts; | ||
@@ -192,9 +139,21 @@ const incrementalResult = options.dirtyRange && !options.forceFull && previousArtifacts ? tryBuildIncrementalMeasures({ | ||
| const bodyBreakType = sectionProperties?.sectionStart; | ||
| const buildLayoutOpts = (nextMargins, nextFirstPageMargins) => { | ||
| const buildLayoutOpts = () => { | ||
| const nextLayoutOpts = { | ||
| pageSize, | ||
| margins: nextMargins, | ||
| pageGap | ||
| margins, | ||
| pageGap, | ||
| mirrorMargins | ||
| }; | ||
| if (nextFirstPageMargins !== void 0) nextLayoutOpts.firstPageMargins = nextFirstPageMargins; | ||
| if (hasTitlePg && firstPageHeaderForRender) { | ||
| const headerBottom = (margins.header ?? 0) + (firstPageHeaderForRender.marginPushBottom ?? firstPageHeaderForRender.height); | ||
| if (headerBottom > margins.top) nextLayoutOpts.firstPageMargins = { | ||
| ...margins, | ||
| top: headerBottom | ||
| }; | ||
| } | ||
| const finalSection = document?.package.document.sections?.at(-1); | ||
| if (finalSection) { | ||
| nextLayoutOpts.finalPageSize = getPageSize(finalSection.properties); | ||
| nextLayoutOpts.finalMargins = getMargins(finalSection.properties); | ||
| } | ||
| if (columns !== void 0) nextLayoutOpts.columns = columns; | ||
@@ -205,3 +164,3 @@ if (bodyBreakType !== void 0) nextLayoutOpts.bodyBreakType = bodyBreakType; | ||
| }; | ||
| const layoutOpts = buildLayoutOpts(effectiveMargins, effectiveFirstPageMargins); | ||
| const layoutOpts = buildLayoutOpts(); | ||
| let layoutOptsUsed = layoutOpts; | ||
@@ -303,53 +262,4 @@ if (hasFootnotes) { | ||
| footerContentByRId = renderHeaderFooterContentByRId(document?.package.footers, hfPMs, contentWidth, hfMetricsFooter, finalHfOptions); | ||
| return { | ||
| headerContent: headerContentForRender, | ||
| footerContent: footerContentForRender, | ||
| firstPageHeaderContent: firstPageHeaderForRender, | ||
| firstPageFooterContent: firstPageFooterForRender | ||
| }; | ||
| }; | ||
| const MAX_HEADER_FOOTER_STABILIZATION_PASSES = 3; | ||
| for (let pass = 0; pass < MAX_HEADER_FOOTER_STABILIZATION_PASSES; pass++) { | ||
| const finalHfExtenderContent = rebuildHeaderFooterForLayout(); | ||
| const finalEffectiveMargins = computeHeaderFooterMarginExtender({ | ||
| ...finalHfExtenderContent, | ||
| pageSize, | ||
| warn: hfWarn | ||
| })(margins); | ||
| const finalEffectiveFirstPageMargins = hasTitlePg ? computeFirstPageHeaderFooterMarginExtender({ | ||
| ...finalHfExtenderContent, | ||
| pageSize, | ||
| warn: hfWarn | ||
| })(margins) : void 0; | ||
| if (!pageMarginsEqual(effectiveMargins, finalEffectiveMargins) || !optionalPageMarginsEqual(effectiveFirstPageMargins, finalEffectiveFirstPageMargins)) { | ||
| effectiveMargins = finalEffectiveMargins; | ||
| effectiveFirstPageMargins = finalEffectiveFirstPageMargins; | ||
| applySectionBreakMargins(finalHfExtenderContent, effectiveMargins); | ||
| bodyLayoutConfig = { | ||
| pageSize, | ||
| margins: effectiveMargins | ||
| }; | ||
| if (columns !== void 0) bodyLayoutConfig.columns = columns; | ||
| blockMeasureInputs = computePerBlockMeasureInputs({ | ||
| blocks: newBlocks, | ||
| bodyConfig: bodyLayoutConfig, | ||
| finalConfig: bodyLayoutConfig | ||
| }); | ||
| blockWidths = blockMeasureInputs.widths; | ||
| newMeasures = measureBlocks(newBlocks, blockWidths, blockMeasureInputs.marginTops, { | ||
| pageHeight: blockMeasureInputs.pageHeights, | ||
| marginBottom: blockMeasureInputs.marginBottoms | ||
| }); | ||
| pendingArtifacts = { | ||
| blocks: newBlocks, | ||
| blockWidths, | ||
| measures: newMeasures | ||
| }; | ||
| outcome.measures = newMeasures; | ||
| relayoutWithCurrentMeasures(buildLayoutOpts(effectiveMargins, effectiveFirstPageMargins)); | ||
| stabilizeFieldWidths(); | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
| rebuildHeaderFooterForLayout(); | ||
| outcome.layout = newLayout; | ||
@@ -424,15 +334,3 @@ recordPhaseDuration("layout-document", phaseStartedAt); | ||
| } | ||
| function pickSectionHeaderFooterContent(byRId, defaultRId, evenRId) { | ||
| const defaultContent = defaultRId ? byRId?.get(defaultRId) : void 0; | ||
| const evenContent = evenRId ? byRId?.get(evenRId) : void 0; | ||
| if (!defaultContent) return evenContent; | ||
| if (!evenContent) return defaultContent; | ||
| return headerFooterMarginPushHeight(evenContent) > headerFooterMarginPushHeight(defaultContent) ? evenContent : defaultContent; | ||
| } | ||
| function headerFooterMarginPushHeight(content) { | ||
| const bottom = content.marginPushBottom ?? content.visualBottom ?? content.height; | ||
| const top = content.marginPushTop ?? content.visualTop ?? 0; | ||
| return Math.max(bottom - top, content.height); | ||
| } | ||
| //#endregion | ||
| export { runLayoutPipeline }; |
@@ -21,3 +21,4 @@ import { LayoutRunReason } from "../layout-engine/layoutInstrumentation.js"; | ||
| runLayout: RunLayout<TState>; /** Quiet-window debounce before an interactive layout pass. */ | ||
| debounceMs: number; /** Hard cap on latency from the first edit in a burst. */ | ||
| debounceMs: number; /** Run the first edit after a max-delay-sized idle period on the next frame. */ | ||
| leadingFrame?: boolean; /** Hard cap on latency from the first edit in a burst. */ | ||
| maxDelayMs: number; | ||
@@ -24,0 +25,0 @@ /** |
@@ -12,2 +12,3 @@ import { mergeDirtyRanges } from "../paged-layout/incrementalMeasure.js"; | ||
| const clock = config.clock; | ||
| let lastRunAt = null; | ||
| let pending = null; | ||
@@ -21,2 +22,3 @@ const flushPending = () => { | ||
| if (!latest) return; | ||
| lastRunAt = clock.now(); | ||
| const options = { reason: "transaction" }; | ||
@@ -50,2 +52,6 @@ if (latest.dirtyRange) options.dirtyRange = latest.dirtyRange; | ||
| pending = next; | ||
| if (config.leadingFrame && (lastRunAt === null || clock.now() - lastRunAt >= config.maxDelayMs)) { | ||
| flushPending(); | ||
| return; | ||
| } | ||
| armTimer(next); | ||
@@ -52,0 +58,0 @@ }, |
@@ -60,15 +60,26 @@ import { elementToXml, findChild, findDeep, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js"; | ||
| if (numId === 0) return; | ||
| if (!listCounters.has(numId)) listCounters.set(numId, Array.from({ length: 9 }).fill(0)); | ||
| if (!listCounters.has(numId)) listCounters.set(numId, Array.from({ length: 9 }).fill(NaN)); | ||
| const counters = listCounters.get(numId); | ||
| if (!counters) return; | ||
| const abstractNumId = numbering.getAbstractNumId(numId); | ||
| const styleNumbering = paragraph.formatting?.numPrFromStyle; | ||
| if (abstractNumId !== null && styleNumbering) { | ||
| const latestAbstractCounters = abstractCounters.get(abstractNumId); | ||
| if (latestAbstractCounters) for (let i = 0; i < counters.length; i += 1) counters[i] = latestAbstractCounters[i] ?? NaN; | ||
| } | ||
| if (abstractNumId !== null && level > 0) { | ||
| const latestAbstractCounters = abstractCounters.get(abstractNumId); | ||
| const missingParentCounters = counters.slice(0, level).every((value) => value === 0); | ||
| if (latestAbstractCounters && missingParentCounters) for (let i = 0; i < level; i += 1) counters[i] = latestAbstractCounters[i] ?? 0; | ||
| if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) { | ||
| const latestCounter = latestAbstractCounters?.[i]; | ||
| counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : numbering.getLevel(numId, i)?.start ?? 1; | ||
| } | ||
| } | ||
| counters[level] = (counters[level] || 0) + 1; | ||
| for (let i = level + 1; i < counters.length; i += 1) counters[i] = 0; | ||
| if (Number.isNaN(counters[level])) counters[level] = (numbering.getLevel(numId, level)?.start ?? 1) - 1; | ||
| counters[level] = (counters[level] ?? 0) + 1; | ||
| for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN; | ||
| const childAdvances = listRendering.implicitChildLevelAdvances ?? 0; | ||
| if (childAdvances > 0 && level + 1 < counters.length) counters[level + 1] = (counters[level + 1] ?? 0) + childAdvances; | ||
| if (childAdvances > 0 && level + 1 < counters.length) { | ||
| const childCounter = counters[level + 1]; | ||
| counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances; | ||
| } | ||
| if (abstractNumId !== null) abstractCounters.set(abstractNumId, [...counters]); | ||
@@ -94,3 +105,2 @@ const pattern = listRendering.marker; | ||
| const enrichParagraphTextBoxes = (paragraph, paraXml, styles, theme, numbering, rels, media) => { | ||
| if (paragraph.content.length === 0) return; | ||
| const xmlChildren = getChildElements(paraXml); | ||
@@ -97,0 +107,0 @@ let parsedIndex = 0; |
@@ -127,3 +127,4 @@ import { bytesEqual, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le } from "./cryptoBytes.js"; | ||
| const finalizeDerivedKey = async (passwordHash, blockKey, hashName, keyBits) => { | ||
| return (await digest(webHashName(hashName), joinBytes([passwordHash, blockKey]))).subarray(0, keyBits / 8); | ||
| const algorithm = webHashName(hashName); | ||
| return (await digest(algorithm, joinBytes([passwordHash, blockKey]))).subarray(0, keyBits / 8); | ||
| }; | ||
@@ -130,0 +131,0 @@ const segmentIv = async (packageSalt, segmentIndex, hashName, blockBytes) => { |
@@ -89,3 +89,4 @@ import { joinBytes } from "./cryptoBytes.js"; | ||
| if (chain === SECTOR_END || chain === SECTOR_FREE) break; | ||
| const view = viewOf(loadSector(file, header, chain)); | ||
| const sector = loadSector(file, header, chain); | ||
| const view = viewOf(sector); | ||
| const slotsPerSector = header.sectorBytes / 4 - 1; | ||
@@ -104,3 +105,4 @@ for (let slot = 0; slot < slotsPerSector; slot++) { | ||
| for (const fatSectorId of loadFatSectorIds(file, header)) { | ||
| const view = viewOf(loadSector(file, header, fatSectorId)); | ||
| const sector = loadSector(file, header, fatSectorId); | ||
| const view = viewOf(sector); | ||
| const entries = header.sectorBytes / 4; | ||
@@ -107,0 +109,0 @@ for (let i = 0; i < entries; i++) table.push(u32(view, i * 4)); |
@@ -77,18 +77,2 @@ import { document_d_exports } from "../types/document.js"; | ||
| /** | ||
| * Get footnote number for display (excluding separators) | ||
| * @param footnote - The footnote to get the number for | ||
| * @param footnoteMap - The footnote map | ||
| * @param startNumber - Starting number (default 1) | ||
| * @returns The display number, or null for separator footnotes | ||
| */ | ||
| declare function getFootnoteDisplayNumber(footnote: document_d_exports.Footnote, footnoteMap: FootnoteMap, startNumber?: number): number | null; | ||
| /** | ||
| * Get endnote number for display (excluding separators) | ||
| * @param endnote - The endnote to get the number for | ||
| * @param endnoteMap - The endnote map | ||
| * @param startNumber - Starting number (default 1) | ||
| * @returns The display number, or null for separator endnotes | ||
| */ | ||
| declare function getEndnoteDisplayNumber(endnote: document_d_exports.Endnote, endnoteMap: EndnoteMap, startNumber?: number): number | null; | ||
| /** | ||
| * Create an empty footnote map | ||
@@ -110,2 +94,2 @@ */ | ||
| //#endregion | ||
| export { EndnoteMap, FootnoteMap, createEmptyEndnoteMap, createEmptyFootnoteMap, getEndnoteDisplayNumber, getEndnoteText, getFootnoteDisplayNumber, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote, mergeEndnoteMaps, mergeFootnoteMaps, parseEndnoteProperties, parseEndnotes, parseFootnoteProperties, parseFootnotes }; | ||
| export { EndnoteMap, FootnoteMap, createEmptyEndnoteMap, createEmptyFootnoteMap, getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote, mergeEndnoteMaps, mergeFootnoteMaps, parseEndnoteProperties, parseEndnotes, parseFootnoteProperties, parseFootnotes }; |
@@ -208,28 +208,2 @@ import { findChild, findChildren, getAttributes, getChildElements, getLocalName, parseXml } from "./xmlParser.js"; | ||
| /** | ||
| * Get footnote number for display (excluding separators) | ||
| * @param footnote - The footnote to get the number for | ||
| * @param footnoteMap - The footnote map | ||
| * @param startNumber - Starting number (default 1) | ||
| * @returns The display number, or null for separator footnotes | ||
| */ | ||
| function getFootnoteDisplayNumber(footnote, footnoteMap, startNumber = 1) { | ||
| if (isSeparatorFootnote(footnote)) return null; | ||
| const index = footnoteMap.getNormalFootnotes().findIndex((fn) => fn.id === footnote.id); | ||
| if (index === -1) return null; | ||
| return startNumber + index; | ||
| } | ||
| /** | ||
| * Get endnote number for display (excluding separators) | ||
| * @param endnote - The endnote to get the number for | ||
| * @param endnoteMap - The endnote map | ||
| * @param startNumber - Starting number (default 1) | ||
| * @returns The display number, or null for separator endnotes | ||
| */ | ||
| function getEndnoteDisplayNumber(endnote, endnoteMap, startNumber = 1) { | ||
| if (isSeparatorEndnote(endnote)) return null; | ||
| const index = endnoteMap.getNormalEndnotes().findIndex((en) => en.id === endnote.id); | ||
| if (index === -1) return null; | ||
| return startNumber + index; | ||
| } | ||
| /** | ||
| * Create an empty footnote map | ||
@@ -271,2 +245,2 @@ */ | ||
| //#endregion | ||
| export { createEmptyEndnoteMap, createEmptyFootnoteMap, getEndnoteDisplayNumber, getEndnoteText, getFootnoteDisplayNumber, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote, mergeEndnoteMaps, mergeFootnoteMaps, parseEndnoteProperties, parseEndnotes, parseFootnoteProperties, parseFootnotes }; | ||
| export { createEmptyEndnoteMap, createEmptyFootnoteMap, getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote, mergeEndnoteMaps, mergeFootnoteMaps, parseEndnoteProperties, parseEndnotes, parseFootnoteProperties, parseFootnotes }; |
@@ -42,3 +42,5 @@ import { document_d_exports } from "../types/document.js"; | ||
| ilvl?: number; | ||
| }, numbering: NumberingMap): document_d_exports.ListRendering | null; | ||
| }, numbering: NumberingMap): (document_d_exports.ListRendering & { | ||
| levelStarts: number[]; | ||
| }) | null; | ||
| /** | ||
@@ -45,0 +47,0 @@ * Format a number according to the specified format |
@@ -584,3 +584,8 @@ import { findChild, findChildren, getAttribute, parseBooleanElement, parseNumericAttribute, parseXmlDocument } from "./xmlParser.js"; | ||
| const levelNumFmts = []; | ||
| for (let i = 0; i <= ilvl; i += 1) levelNumFmts.push(level.isLgl ? "decimal" : numbering.getLevel(numId, i)?.numFmt ?? "decimal"); | ||
| const levelStarts = []; | ||
| for (let i = 0; i <= ilvl; i += 1) { | ||
| const listLevel = numbering.getLevel(numId, i); | ||
| levelNumFmts.push(level.isLgl ? "decimal" : listLevel?.numFmt ?? "decimal"); | ||
| levelStarts.push(listLevel?.start ?? 1); | ||
| } | ||
| const instance = numbering.getInstance(numId); | ||
@@ -594,3 +599,4 @@ const overrideForLevel = instance?.levelOverrides?.find((override) => override.ilvl === ilvl); | ||
| numFmt: level.isLgl ? "decimal" : level.numFmt, | ||
| levelNumFmts | ||
| levelNumFmts, | ||
| levelStarts | ||
| }; | ||
@@ -597,0 +603,0 @@ if (level.isLgl) rendering.isLegal = true; |
@@ -439,3 +439,3 @@ import { elementToXml, findChild, findChildren, getAttribute, getChildElements, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js"; | ||
| const outcome = visit(node); | ||
| if (outcome === "forced") return true; | ||
| if (outcome === "forced") return sawRenderedPageBreak; | ||
| return outcome === "visible" && sawRenderedPageBreak; | ||
@@ -953,3 +953,8 @@ } | ||
| const levelNumFmts = []; | ||
| for (let levelIndex = 0; levelIndex <= ilvl; levelIndex += 1) levelNumFmts.push(level.isLgl ? "decimal" : numbering.getLevel(numId, levelIndex)?.numFmt ?? "decimal"); | ||
| const levelStarts = []; | ||
| for (let levelIndex = 0; levelIndex <= ilvl; levelIndex += 1) { | ||
| const listLevel = numbering.getLevel(numId, levelIndex); | ||
| levelNumFmts.push(level.isLgl ? "decimal" : listLevel?.numFmt ?? "decimal"); | ||
| levelStarts.push(listLevel?.start ?? 1); | ||
| } | ||
| const listRendering = { | ||
@@ -960,3 +965,4 @@ level: ilvl, | ||
| isBullet: level.numFmt === "bullet", | ||
| levelNumFmts | ||
| levelNumFmts, | ||
| levelStarts | ||
| }; | ||
@@ -963,0 +969,0 @@ const instance = numbering.getInstance(numId); |
| import { cloneWithXmlnsDeclarations, elementToXml, findChild, findChildren, getAttribute, getChildElements, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js"; | ||
| import { EmphasisMarkSchema, FontThemeSchema, HighlightColorSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js"; | ||
| import { parseImage } from "./imageParser.js"; | ||
| import { parseGroupDrawing } from "./groupDrawingParser.js"; | ||
| import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js"; | ||
@@ -445,2 +446,8 @@ import { parseVmlImageContent } from "./vmlImageParser.js"; | ||
| function parseDrawingContent(element, rels, media) { | ||
| const groupImage = parseGroupDrawing(element); | ||
| if (groupImage) return { | ||
| type: "drawing", | ||
| image: groupImage, | ||
| rawXml: elementToXml(element) | ||
| }; | ||
| if (shouldPreserveRawShapeDrawing(element)) return { | ||
@@ -554,3 +561,3 @@ type: "drawing", | ||
| if (innerDrawing) { | ||
| if (innerDrawing.type === "drawing" && !innerDrawing.image.src) innerDrawing.rawXml = elementToXml(child); | ||
| if (innerDrawing.type === "drawing" && (innerDrawing.rawXml !== void 0 || !innerDrawing.image.src)) innerDrawing.rawXml = elementToXml(child); | ||
| contents.push(innerDrawing); | ||
@@ -557,0 +564,0 @@ } |
@@ -53,7 +53,4 @@ //#region src/docx/selectiveXmlPatch.ts | ||
| }; | ||
| pos = tagEnd + 1; | ||
| } else { | ||
| depth++; | ||
| pos = tagEnd + 1; | ||
| } | ||
| } else depth++; | ||
| pos = tagEnd + 1; | ||
| } else pos = tagStart + 1; | ||
@@ -230,7 +227,4 @@ } else if (xml.startsWith("</w:p>", tagStart)) { | ||
| }; | ||
| pos = tagEnd + 1; | ||
| } else { | ||
| depth++; | ||
| pos = tagEnd + 1; | ||
| } | ||
| } else depth++; | ||
| pos = tagEnd + 1; | ||
| } else if (xml.startsWith(closeTag, tagStart)) { | ||
@@ -237,0 +231,0 @@ depth--; |
@@ -90,5 +90,7 @@ import { escapeXml, intAttr } from "./xmlUtils.js"; | ||
| function serializeNumberingXml(numbering) { | ||
| return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="${W_NS}">${numbering.abstractNums.map(serializeAbstractNum).join("")}${numbering.nums.map(serializeNum).join("")}</w:numbering>`; | ||
| const abstractNums = numbering.abstractNums.map(serializeAbstractNum).join(""); | ||
| const nums = numbering.nums.map(serializeNum).join(""); | ||
| return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="${W_NS}">${abstractNums}${nums}</w:numbering>`; | ||
| } | ||
| //#endregion | ||
| export { serializeNumberingXml }; |
| import { document_d_exports } from "../types/document.js"; | ||
| //#region src/docx/settingsParser.d.ts | ||
| type FolioDocumentSettings = document_d_exports.DocumentSettings & { | ||
| /** Swap left/right section margins on even physical pages. */mirrorMargins?: boolean; | ||
| }; | ||
| /** OOXML default per §17.6.13 when `w:defaultTabStop` is absent. */ | ||
| declare const DEFAULT_TAB_STOP_TWIPS = 720; | ||
| declare function parseSettings(xml: string | null): document_d_exports.DocumentSettings; | ||
| type DocumentSettings = document_d_exports.DocumentSettings; | ||
| export { DEFAULT_TAB_STOP_TWIPS, type DocumentSettings, parseSettings }; | ||
| declare function parseSettings(xml: string | null): FolioDocumentSettings; | ||
| //#endregion | ||
| export { DEFAULT_TAB_STOP_TWIPS, FolioDocumentSettings, parseSettings }; |
@@ -16,2 +16,4 @@ import { findChild, getAttribute, parseBooleanElement, parseXmlDocument } from "./xmlParser.js"; | ||
| if (evenAndOddHeaders && parseBooleanElement(evenAndOddHeaders)) settings.evenAndOddHeaders = true; | ||
| const mirrorMargins = root ? findChild(root, "w", "mirrorMargins") : null; | ||
| if (mirrorMargins && parseBooleanElement(mirrorMargins)) settings.mirrorMargins = true; | ||
| const themeFontLangEl = root ? findChild(root, "w", "themeFontLang") : null; | ||
@@ -18,0 +20,0 @@ const eastAsiaLang = themeFontLangEl ? getAttribute(themeFontLangEl, "w", "eastAsia") || void 0 : void 0; |
@@ -34,2 +34,3 @@ import { XMLBuilder, XMLParser } from "fast-xml-parser"; | ||
| ignorePiTags: true, | ||
| jPath: false, | ||
| processEntities: true, | ||
@@ -69,3 +70,3 @@ htmlEntities: true | ||
| const attrs = node[ATTR_KEY]; | ||
| for (const key of Object.keys(node)) { | ||
| for (const key in node) { | ||
| if (key === ATTR_KEY) continue; | ||
@@ -77,4 +78,8 @@ const children = node[key]; | ||
| }; | ||
| if (attrs && Object.keys(attrs).length > 0) element.attributes = attrs; | ||
| if (children.length > 0) element.elements = children.map(fxpNodeToElement); | ||
| if (attrs) element.attributes = attrs; | ||
| if (children.length > 0) { | ||
| const elements = []; | ||
| for (const child of children) elements.push(fxpNodeToElement(child)); | ||
| element.elements = elements; | ||
| } | ||
| return element; | ||
@@ -90,3 +95,5 @@ } | ||
| function fxpToRootElement(nodes) { | ||
| return { elements: nodes.map(fxpNodeToElement) }; | ||
| const elements = []; | ||
| for (const node of nodes) elements.push(fxpNodeToElement(node)); | ||
| return { elements }; | ||
| } | ||
@@ -93,0 +100,0 @@ /** |
+3
-2
@@ -1,4 +0,5 @@ | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./ai-edits/types.js"; | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./ai-edits/types.js"; | ||
| import { applyFolioAIEditOperations } from "./ai-edits/apply.js"; | ||
| import { document_d_exports } from "./types/document.js"; | ||
| import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js"; | ||
@@ -29,2 +30,2 @@ import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| type Document = document_d_exports.Document; | ||
| export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocxCompatibility, type EmbeddedFont, type EmbeddedFontParts, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type ImageMeta, type ImageRef, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; | ||
| export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocxCompatibility, type EmbeddedFont, type EmbeddedFontParts, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type ImageMeta, type ImageRef, InvalidFolioDocumentOperationBatchError, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; |
+6
-5
@@ -0,4 +1,8 @@ | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js"; | ||
| import { diffWordSegments } from "./ai-edits/word-diff.js"; | ||
| import { applyFolioAIEditOperations } from "./ai-edits/apply.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js"; | ||
| import { createEmptyDocument } from "./utils/createDocument.js"; | ||
| import { createDocx } from "./docx/rezip.js"; | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| import { DEFAULT_AI_SUGGESTION_PRESETS } from "./ai-suggestions/types.js"; | ||
@@ -8,5 +12,2 @@ import { buildPositionalText } from "./ai-suggestions/text-positions.js"; | ||
| import { applySuggestions } from "./ai-suggestions/apply.js"; | ||
| import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js"; | ||
| import { diffWordSegments } from "./ai-edits/word-diff.js"; | ||
| import { applyFolioAIEditOperations } from "./ai-edits/apply.js"; | ||
| import { setAISuggestionsMeta, setFocusedSuggestionMeta } from "./prosemirror/plugins/aiSuggestionDecorations.js"; | ||
@@ -25,2 +26,2 @@ import { getGoogleFontsEnabled, setGoogleFontsEnabled } from "./utils/fontResolver.js"; | ||
| import { extractEmbeddedFonts, getEmbeddedFontFaces } from "./fonts/embeddedFonts.js"; | ||
| export { DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; | ||
| export { DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult }; |
@@ -27,2 +27,38 @@ import { document_d_exports } from "../../types/document.js"; | ||
| /** | ||
| * Scan FlowBlocks for runs with endnoteRefId set, in document order. | ||
| * Same recursive walk as `collectFootnoteRefs`. | ||
| */ | ||
| declare function collectEndnoteRefs(blocks: FlowBlock[]): { | ||
| endnoteId: number; | ||
| pmPos: number; | ||
| }[]; | ||
| type NumberableNote = { | ||
| id: number; | ||
| noteType?: document_d_exports.Footnote["noteType"]; | ||
| }; | ||
| /** | ||
| * Assign sequential display numbers (1, 2, 3, …) to notes in order of first | ||
| * reference. Word numbers footnotes/endnotes by reference order, not by their | ||
| * `w:id` values, which may be non-contiguous or out of document order. Only | ||
| * `normal` notes are numbered; separator/continuation notes and refs to | ||
| * missing notes consume no number. | ||
| */ | ||
| declare function computeNoteDisplayNumbers(notes: readonly NumberableNote[], refNoteIds: readonly number[]): Map<number, number>; | ||
| type NoteDisplayNumberMaps = { | ||
| /** footnote `w:id` → sequential display number */footnoteNumbers?: ReadonlyMap<number, number>; /** endnote `w:id` → sequential display number */ | ||
| endnoteNumbers?: ReadonlyMap<number, number>; | ||
| }; | ||
| /** | ||
| * Rewrite body reference-marker run text from the raw `w:id` (which the PM | ||
| * doc stores as the marker text; the save path serializes from the mark | ||
| * attrs, never from this text) to the sequential display number, so the | ||
| * inline marker matches the number rendered in the note area. Must run | ||
| * before measurement so marker widths match the painted digits. Untouched | ||
| * blocks/runs are returned by reference so the painter's fingerprinting and | ||
| * the incremental measure path see them unchanged. Runs keep their PM range, | ||
| * so click-to-position still resolves into the marker (same contract as the | ||
| * template preview substitution). | ||
| */ | ||
| declare function remapNoteMarkerText(blocks: FlowBlock[], maps: NoteDisplayNumberMaps): FlowBlock[]; | ||
| /** | ||
| * After layout, determine which footnotes appear on which pages. | ||
@@ -58,2 +94,2 @@ * Checks each page's fragments to see if any footnoteRef PM positions fall within. | ||
| //#endregion | ||
| export { ConvertFootnoteOptions, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, MeasureBlocksFn, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectFootnoteRefs, convertFootnoteToContent, mapFootnotesToPages }; | ||
| export { ConvertFootnoteOptions, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, MeasureBlocksFn, NoteDisplayNumberMaps, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectEndnoteRefs, collectFootnoteRefs, computeNoteDisplayNumbers, convertFootnoteToContent, mapFootnotesToPages, remapNoteMarkerText }; |
@@ -19,10 +19,29 @@ import { DEFAULT_TEXTBOX_MARGINS, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT } from "../../layout-engine/types.js"; | ||
| function collectFootnoteRefs(blocks) { | ||
| return collectNoteRefs(blocks, "footnoteRefId").map(({ noteId, pmPos }) => ({ | ||
| footnoteId: noteId, | ||
| pmPos | ||
| })); | ||
| } | ||
| /** | ||
| * Scan FlowBlocks for runs with endnoteRefId set, in document order. | ||
| * Same recursive walk as `collectFootnoteRefs`. | ||
| */ | ||
| function collectEndnoteRefs(blocks) { | ||
| return collectNoteRefs(blocks, "endnoteRefId").map(({ noteId, pmPos }) => ({ | ||
| endnoteId: noteId, | ||
| pmPos | ||
| })); | ||
| } | ||
| function collectNoteRefs(blocks, idKey) { | ||
| const refs = []; | ||
| const walk = (containerBlocks) => { | ||
| for (const block of containerBlocks) if (block.kind === "paragraph") { | ||
| for (const run of block.runs) if (run.kind === "text" && run.footnoteRefId !== void 0) refs.push({ | ||
| footnoteId: run.footnoteRefId, | ||
| for (const block of containerBlocks) if (block.kind === "paragraph") for (const run of block.runs) { | ||
| if (run.kind !== "text") continue; | ||
| const noteId = run[idKey]; | ||
| if (noteId !== void 0) refs.push({ | ||
| noteId, | ||
| pmPos: run.pmStart ?? 0 | ||
| }); | ||
| } else if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks); | ||
| } | ||
| else if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks); | ||
| else if (block.kind === "textBox") walk(block.content); | ||
@@ -34,2 +53,112 @@ }; | ||
| /** | ||
| * Assign sequential display numbers (1, 2, 3, …) to notes in order of first | ||
| * reference. Word numbers footnotes/endnotes by reference order, not by their | ||
| * `w:id` values, which may be non-contiguous or out of document order. Only | ||
| * `normal` notes are numbered; separator/continuation notes and refs to | ||
| * missing notes consume no number. | ||
| */ | ||
| function computeNoteDisplayNumbers(notes, refNoteIds) { | ||
| const numberable = /* @__PURE__ */ new Set(); | ||
| for (const note of notes) if (note.noteType === "normal") numberable.add(note.id); | ||
| const displayNumbers = /* @__PURE__ */ new Map(); | ||
| let nextNumber = 1; | ||
| for (const noteId of refNoteIds) { | ||
| if (displayNumbers.has(noteId) || !numberable.has(noteId)) continue; | ||
| displayNumbers.set(noteId, nextNumber); | ||
| nextNumber++; | ||
| } | ||
| return displayNumbers; | ||
| } | ||
| /** | ||
| * Rewrite body reference-marker run text from the raw `w:id` (which the PM | ||
| * doc stores as the marker text; the save path serializes from the mark | ||
| * attrs, never from this text) to the sequential display number, so the | ||
| * inline marker matches the number rendered in the note area. Must run | ||
| * before measurement so marker widths match the painted digits. Untouched | ||
| * blocks/runs are returned by reference so the painter's fingerprinting and | ||
| * the incremental measure path see them unchanged. Runs keep their PM range, | ||
| * so click-to-position still resolves into the marker (same contract as the | ||
| * template preview substitution). | ||
| */ | ||
| function remapNoteMarkerText(blocks, maps) { | ||
| if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.endnoteNumbers?.size ?? 0) === 0) return blocks; | ||
| let changed = false; | ||
| const next = blocks.map((block) => { | ||
| const remapped = remapNoteMarkerBlock(block, maps); | ||
| changed ||= remapped !== block; | ||
| return remapped; | ||
| }); | ||
| return changed ? next : blocks; | ||
| } | ||
| function remapNoteMarkerBlock(block, maps) { | ||
| if (block.kind === "paragraph") return remapNoteMarkerParagraph(block, maps); | ||
| if (block.kind === "table") { | ||
| let tableChanged = false; | ||
| const rows = block.rows.map((row) => { | ||
| let rowChanged = false; | ||
| const cells = row.cells.map((cell) => { | ||
| let cellChanged = false; | ||
| const cellBlocks = cell.blocks.map((cellBlock) => { | ||
| const remapped = remapNoteMarkerBlock(cellBlock, maps); | ||
| cellChanged ||= remapped !== cellBlock; | ||
| return remapped; | ||
| }); | ||
| return cellChanged ? { | ||
| ...cell, | ||
| blocks: cellBlocks | ||
| } : cell; | ||
| }); | ||
| rowChanged = cells.some((cell, index) => cell !== row.cells[index]); | ||
| tableChanged ||= rowChanged; | ||
| return rowChanged ? { | ||
| ...row, | ||
| cells | ||
| } : row; | ||
| }); | ||
| return tableChanged ? { | ||
| ...block, | ||
| rows | ||
| } : block; | ||
| } | ||
| if (block.kind === "textBox") { | ||
| let boxChanged = false; | ||
| const content = block.content.map((paragraph) => { | ||
| const remapped = remapNoteMarkerParagraph(paragraph, maps); | ||
| boxChanged ||= remapped !== paragraph; | ||
| return remapped; | ||
| }); | ||
| return boxChanged ? { | ||
| ...block, | ||
| content | ||
| } : block; | ||
| } | ||
| return block; | ||
| } | ||
| function remapNoteMarkerParagraph(block, maps) { | ||
| let changed = false; | ||
| const runs = block.runs.map((run) => { | ||
| const remapped = remapNoteMarkerRun(run, maps); | ||
| changed ||= remapped !== run; | ||
| return remapped; | ||
| }); | ||
| return changed ? { | ||
| ...block, | ||
| runs | ||
| } : block; | ||
| } | ||
| function remapNoteMarkerRun(run, maps) { | ||
| if (run.kind !== "text") return run; | ||
| const displayNumber = getRunDisplayNumber(run, maps); | ||
| if (displayNumber === void 0) return run; | ||
| const text = String(displayNumber); | ||
| return run.text === text ? run : { | ||
| ...run, | ||
| text | ||
| }; | ||
| } | ||
| function getRunDisplayNumber(run, maps) { | ||
| if (run.footnoteRefId !== void 0) return maps.footnoteNumbers?.get(run.footnoteRefId); | ||
| if (run.endnoteRefId !== void 0) return maps.endnoteNumbers?.get(run.endnoteRefId); | ||
| } | ||
| /** | ||
| * After layout, determine which footnotes appear on which pages. | ||
@@ -284,12 +413,7 @@ * Checks each page's fragments to see if any footnoteRef PM positions fall within. | ||
| for (const fn of footnotes) if (fn.noteType === "normal") footnoteById.set(fn.id, fn); | ||
| let displayNumber = 1; | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| for (const ref of footnoteRefs) { | ||
| if (seen.has(ref.footnoteId)) continue; | ||
| seen.add(ref.footnoteId); | ||
| const footnote = footnoteById.get(ref.footnoteId); | ||
| const displayNumbers = computeNoteDisplayNumbers(footnotes, footnoteRefs.map((ref) => ref.footnoteId)); | ||
| for (const [footnoteId, displayNumber] of displayNumbers) { | ||
| const footnote = footnoteById.get(footnoteId); | ||
| if (!footnote) continue; | ||
| const content = convertFootnoteToContent(footnote, displayNumber, contentWidth, options); | ||
| contentMap.set(ref.footnoteId, content); | ||
| displayNumber++; | ||
| contentMap.set(footnoteId, convertFootnoteToContent(footnote, displayNumber, contentWidth, options)); | ||
| } | ||
@@ -319,2 +443,2 @@ return contentMap; | ||
| //#endregion | ||
| export { FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectFootnoteRefs, convertFootnoteToContent, mapFootnotesToPages }; | ||
| export { FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectEndnoteRefs, collectFootnoteRefs, computeNoteDisplayNumbers, convertFootnoteToContent, mapFootnotesToPages, remapNoteMarkerText }; |
@@ -23,9 +23,7 @@ import { document_d_exports } from "../../types/document.js"; | ||
| /** | ||
| * Compute the header/footer bounds used by `computeHeaderFooterMarginExtender` | ||
| * to push body margins clear of HF overflow. Excludes anchored/floating objects | ||
| * (full-page letterheads, watermarks): Word positions them on the page instead | ||
| * of in the header/footer flow, so they must not reserve body push-down. Keeping | ||
| * them in `visualBottom` is still correct for the renderer (and for the | ||
| * page-hash invalidation signal), but the margin extender needs the flow-only | ||
| * extent. | ||
| * Compute flow-only header/footer bounds, excluding anchored/floating objects | ||
| * such as full-page letterheads and watermarks. Word positions those objects on | ||
| * the page independently from header/footer flow. Keeping them in | ||
| * `visualBottom` is still correct for rendering and page-hash invalidation; | ||
| * these bounds separately describe the in-flow portion for API consumers. | ||
| */ | ||
@@ -32,0 +30,0 @@ declare function calculateHeaderFooterMarginPushBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): { |
@@ -228,9 +228,7 @@ import { emuToPixels } from "../../utils/units.js"; | ||
| /** | ||
| * Compute the header/footer bounds used by `computeHeaderFooterMarginExtender` | ||
| * to push body margins clear of HF overflow. Excludes anchored/floating objects | ||
| * (full-page letterheads, watermarks): Word positions them on the page instead | ||
| * of in the header/footer flow, so they must not reserve body push-down. Keeping | ||
| * them in `visualBottom` is still correct for the renderer (and for the | ||
| * page-hash invalidation signal), but the margin extender needs the flow-only | ||
| * extent. | ||
| * Compute flow-only header/footer bounds, excluding anchored/floating objects | ||
| * such as full-page letterheads and watermarks. Word positions those objects on | ||
| * the page independently from header/footer flow. Keeping them in | ||
| * `visualBottom` is still correct for rendering and page-hash invalidation; | ||
| * these bounds separately describe the in-flow portion for API consumers. | ||
| */ | ||
@@ -237,0 +235,0 @@ function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, metrics) { |
@@ -8,2 +8,3 @@ import { NUMBER_FORMAT_VALUES } from "../../types/documentEnumValues.js"; | ||
| import { convertBulletToUnicode } from "../../docx/bulletMarkers.js"; | ||
| import { setTextBoxGroupId } from "../../layout-engine/textBoxGroup.js"; | ||
| import { directionIsRtl } from "../../prosemirror/paragraphDirection.js"; | ||
@@ -104,3 +105,3 @@ import { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectMathAttrs, expectParagraphAttrs, expectRunFormattingOverrideMarkAttrs, expectRunShadingMarkAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs } from "../../prosemirror/attrs/index.js"; | ||
| function formatCounter(value, format) { | ||
| if (value <= 0) return ""; | ||
| if (!Number.isFinite(value)) return ""; | ||
| switch (format) { | ||
@@ -124,3 +125,5 @@ case "upperRoman": return toRoman(value, true); | ||
| if (index < 0) return ""; | ||
| const formatted = formatCounter(counters[index] ?? 0, forceDecimal ? "decimal" : levelFormats?.[index]); | ||
| const counter = counters[index]; | ||
| if (counter === void 0 || Number.isNaN(counter)) return ""; | ||
| const formatted = formatCounter(counter, forceDecimal ? "decimal" : levelFormats?.[index]); | ||
| return formatted ? `${formatted}${punct}` : ""; | ||
@@ -149,8 +152,10 @@ }); | ||
| const level = pmAttrs.numPr?.ilvl ?? 0; | ||
| const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => 0); | ||
| const counters = listCounters.get(numId) ?? Array.from({ length: 9 }, () => NaN); | ||
| const abstractNumId = pmAttrs.listAbstractNumId; | ||
| if (abstractNumId !== void 0 && level > 0) { | ||
| const latestAbstractCounters = abstractCounters.get(abstractNumId); | ||
| const missingParentCounters = counters.slice(0, level).every((value) => value === 0); | ||
| if (latestAbstractCounters && missingParentCounters) for (let i = 0; i < level; i += 1) counters[i] = latestAbstractCounters[i] ?? 0; | ||
| if (level > 0) { | ||
| const latestAbstractCounters = abstractNumId === void 0 ? void 0 : abstractCounters.get(abstractNumId); | ||
| if (counters.slice(0, level).every(Number.isNaN)) for (let i = 0; i < level; i += 1) { | ||
| const latestCounter = latestAbstractCounters?.[i]; | ||
| counters[i] = latestCounter !== void 0 && !Number.isNaN(latestCounter) ? latestCounter : pmAttrs.listLevelStarts?.[i] ?? 1; | ||
| } | ||
| } | ||
@@ -161,7 +166,11 @@ const seenKey = `${numId}:${level}`; | ||
| if (pmAttrs.listStartOverride != null) counters[level] = pmAttrs.listStartOverride - 1; | ||
| else if (Number.isNaN(counters[level])) counters[level] = (pmAttrs.listLevelStarts?.[level] ?? 1) - 1; | ||
| } | ||
| counters[level] = (counters[level] ?? 0) + 1; | ||
| for (let i = level + 1; i < counters.length; i += 1) counters[i] = 0; | ||
| for (let i = level + 1; i < counters.length; i += 1) counters[i] = NaN; | ||
| const childAdvances = pmAttrs.listImplicitChildLevelAdvances ?? 0; | ||
| if (childAdvances > 0 && level + 1 < counters.length) counters[level + 1] = (counters[level + 1] ?? 0) + childAdvances; | ||
| if (childAdvances > 0 && level + 1 < counters.length) { | ||
| const childCounter = counters[level + 1]; | ||
| counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances; | ||
| } | ||
| listCounters.set(numId, counters); | ||
@@ -242,3 +251,3 @@ if (abstractNumId !== void 0) abstractCounters.set(abstractNumId, [...counters]); | ||
| const attrs = expectCharacterSpacingMarkAttrs(mark); | ||
| if (attrs.spacing !== void 0 && attrs.spacing !== 0) formatting.letterSpacing = twipsToPixels(attrs.spacing); | ||
| if (attrs.spacing !== void 0) formatting.letterSpacing = twipsToPixels(attrs.spacing); | ||
| if (attrs.position !== void 0 && attrs.position !== 0) formatting.positionPx = halfPointsToPixels(attrs.position); | ||
@@ -345,6 +354,8 @@ if (attrs.scale !== void 0 && attrs.scale !== 100) formatting.horizontalScale = attrs.scale; | ||
| function mergeRunFormatting(paraDefaults, formatting) { | ||
| return { | ||
| const merged = { | ||
| ...paraDefaults, | ||
| ...markDefaultBlackTextColorSource(formatting, paraDefaults) | ||
| }; | ||
| if (merged.letterSpacing === 0) delete merged.letterSpacing; | ||
| return merged; | ||
| } | ||
@@ -495,5 +506,6 @@ function applyRunFormattingOverrides(formatting, attrs) { | ||
| if (child.type.name === "tab") { | ||
| const formatting = extractRunFormatting(child.marks, theme); | ||
| const run = { | ||
| kind: "tab", | ||
| ...mergeRunFormatting(paraDefaults, extractRunFormatting(child.marks, theme)), | ||
| ...mergeRunFormatting(paraDefaults, formatting), | ||
| pmStart: childPos, | ||
@@ -507,3 +519,4 @@ pmEnd: childPos + child.nodeSize | ||
| const attrs = expectImageAttrs(child); | ||
| const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, _options.pageContentHeight); | ||
| if (!attrs.src) return; | ||
| const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, _options.pageContentHeight); | ||
| const trackedFmt = extractRunFormatting(child.marks, theme); | ||
@@ -625,2 +638,4 @@ const run = buildImageRun(attrs, constrained, childPos, childPos + child.nodeSize, trackedFmt); | ||
| if (listLevelNumFmts) attrs.listLevelNumFmts = listLevelNumFmts; | ||
| const listLevelStarts = previousFormatting["listLevelStarts"]; | ||
| if (Array.isArray(listLevelStarts) && listLevelStarts.every((value) => typeof value === "number")) attrs.listLevelStarts = listLevelStarts; | ||
| const listAbstractNumId = previousFormatting["listAbstractNumId"]; | ||
@@ -682,5 +697,6 @@ if (typeof listAbstractNumId === "number") attrs.listAbstractNumId = listAbstractNumId; | ||
| const pmSpacingExplicit = pmAttrs.spacingExplicit; | ||
| const spacingFromDocDefaults = pmAttrs.spacingFromDocDefaults; | ||
| const explicit = {}; | ||
| if (autoBefore || pmSpacingExplicit?.before) explicit.before = true; | ||
| if (autoAfter || pmSpacingExplicit?.after) explicit.after = true; | ||
| if (autoBefore || pmSpacingExplicit?.before || spacingFromDocDefaults?.before) explicit.before = true; | ||
| if (autoAfter || pmSpacingExplicit?.after || spacingFromDocDefaults?.after) explicit.after = true; | ||
| if (explicit.before !== void 0 || explicit.after !== void 0) attrs.spacingExplicit = explicit; | ||
@@ -744,2 +760,3 @@ if (typeof lineSpacing === "number") if (pmAttrs.lineSpacingRule === "exact" || pmAttrs.lineSpacingRule === "atLeast") { | ||
| if (pmAttrs.pageBreakBefore) attrs.pageBreakBefore = true; | ||
| if (pmAttrs.renderedPageBreakBefore) attrs.renderedPageBreakBefore = true; | ||
| if (pmAttrs.keepNext) attrs.keepNext = true; | ||
@@ -824,2 +841,13 @@ if (pmAttrs.keepLines) attrs.keepLines = true; | ||
| const attrs = convertParagraphAttrs(pmAttrs, options.theme, options.listCounters, options.listAbstractCounters, options.listSeenNumIds, options.defaultTabStopTwips, options.originalListCounters, options.originalListAbstractCounters, options.originalListSeenNumIds); | ||
| const defaultTextFormatting = pmAttrs.defaultTextFormatting; | ||
| if (runs.length === 0) { | ||
| const paragraphMarkFormatting = pmAttrs._originalFormatting?.runProperties; | ||
| if (paragraphMarkFormatting?.fontSize !== void 0) attrs.defaultFontSize = paragraphMarkFormatting.fontSize / 2; | ||
| const paragraphMarkFontFamily = paragraphMarkFormatting?.fontFamily?.ascii ?? paragraphMarkFormatting?.fontFamily?.hAnsi; | ||
| if (paragraphMarkFontFamily) attrs.defaultFontFamily = paragraphMarkFontFamily; | ||
| } | ||
| if (runs.length === 0 && defaultTextFormatting?.hidden === true) { | ||
| attrs.suppressEmptyParagraphHeight = true; | ||
| if (attrs.listMarker !== void 0) attrs.listMarkerHidden = true; | ||
| } | ||
| const bookmarkNames = pmAttrs.bookmarks?.map((b) => b.name); | ||
@@ -909,2 +937,7 @@ return { | ||
| }); | ||
| const trailingBlock = blocks.at(-1); | ||
| if (blocks.length > 1 && trailingBlock?.kind === "paragraph" && trailingBlock.runs.every((run) => run.kind === "text" && run.text.length === 0)) trailingBlock.attrs = { | ||
| ...trailingBlock.attrs, | ||
| suppressEmptyParagraphHeight: true | ||
| }; | ||
| const attrs = expectTableCellAttrs(node); | ||
@@ -1010,2 +1043,3 @@ const margins = attrs.margins; | ||
| if (widthType !== void 0) tableBlock.widthType = widthType; | ||
| if (originalFormatting?.layout !== void 0) tableBlock.layout = originalFormatting.layout; | ||
| if (justification) tableBlock.justification = justification; | ||
@@ -1024,3 +1058,3 @@ if (indentPx !== void 0) tableBlock.indent = indentPx; | ||
| const shouldAnchor = wrapType === "behind" || wrapType === "inFront"; | ||
| const constrained = constrainImageToPage(attrs.width || 100, attrs.height || 100, pageContentHeight); | ||
| const constrained = constrainImageToPage(attrs.width ?? 100, attrs.height ?? 100, pageContentHeight); | ||
| const imgBlock = { | ||
@@ -1094,2 +1128,3 @@ kind: "image", | ||
| if (attrs.position !== void 0) textBox.position = attrs.position; | ||
| if (attrs._docxGroupId !== void 0) setTextBoxGroupId(textBox, attrs._docxGroupId); | ||
| return textBox; | ||
@@ -1255,4 +1290,4 @@ } | ||
| case "horizontalRule": | ||
| case "pageBreak": | ||
| trackedPush({ | ||
| case "pageBreak": { | ||
| const pb = { | ||
| kind: "pageBreak", | ||
@@ -1262,4 +1297,6 @@ id: nextBlockId(), | ||
| pmEnd: pos + node.nodeSize | ||
| }); | ||
| }; | ||
| trackedPush(pb); | ||
| break; | ||
| } | ||
| default: break; | ||
@@ -1266,0 +1303,0 @@ } |
@@ -1,2 +0,2 @@ | ||
| import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "./types.js"; | ||
| import { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js"; | ||
| import { PageState, Paginator, PaginatorOptions, createPaginator } from "./paginator.js"; | ||
@@ -23,5 +23,6 @@ import { KeepNextChain, calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js"; | ||
| * | ||
| * When two consecutive paragraph blocks both have `contextualSpacing: true` | ||
| * and share the same `styleId`, the spaceAfter of the first paragraph and | ||
| * the spaceBefore of the second paragraph are suppressed (set to 0). | ||
| * Contextual spacing applies independently to each paragraph: suppress the | ||
| * current paragraph's spaceAfter when it opts in, and the next paragraph's | ||
| * spaceBefore when it opts in, provided both paragraphs share a style. Two | ||
| * absent style ids both refer to the document's default paragraph style. | ||
| * | ||
@@ -46,2 +47,2 @@ * This mutates the block attrs in-place before layout runs. | ||
| //#endregion | ||
| export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak }; | ||
| export { BlockId, BorderStyle, type BreakDecision, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, type KeepNextChain, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, type PageState, type Paginator, type PaginatorOptions, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, SectionLayoutConfig, type SectionState, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak, tableColumnsArePinned }; |
@@ -0,3 +1,4 @@ | ||
| import { emuToPixels } from "../utils/units.js"; | ||
| import { measuredLineAdvance } from "./lineFlow.js"; | ||
| import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun } from "./types.js"; | ||
| import { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned } from "./types.js"; | ||
| import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js"; | ||
@@ -54,21 +55,38 @@ import { createPaginator } from "./paginator.js"; | ||
| const r = block.runs[0]; | ||
| return r?.kind === "text" && (r.text ?? "") === ""; | ||
| return r?.kind === "text" && r.text === ""; | ||
| } | ||
| function isPaginationEmptyParagraph(block) { | ||
| return block.runs.every((run) => { | ||
| if (run.kind === "text") return run.text.trim().length === 0; | ||
| return run.kind === "tab" || run.kind === "lineBreak"; | ||
| }); | ||
| } | ||
| function pageHasVisibleBodyContent(page, blocksById) { | ||
| for (const fragment of page.fragments) { | ||
| if (fragment.kind !== "paragraph") return true; | ||
| const block = blocksById.get(String(fragment.blockId)); | ||
| if (block?.kind !== "paragraph" || !isPaginationEmptyParagraph(block)) return true; | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * Get spacing before a paragraph block. Empty paragraphs whose | ||
| * `before` was inherited from a paragraph style (not set inline) collapse | ||
| * to zero — Word fidelity for incidental empty separators. | ||
| * `before` came only from the implicit default paragraph style collapse to | ||
| * zero. An explicit `w:pStyle` selection is authored paragraph formatting, | ||
| * so Word keeps the selected style's spacing even when the paragraph is empty. | ||
| */ | ||
| function getSpacingBefore(block) { | ||
| const value = block.attrs?.spacing?.before ?? 0; | ||
| if (isEmptyParagraph(block) && !block.attrs?.spacingExplicit?.before) return 0; | ||
| if (value === 0) return 0; | ||
| if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.before) return 0; | ||
| return value; | ||
| } | ||
| /** | ||
| * Get spacing after a paragraph block. Same empty-paragraph collapse rule | ||
| * as `getSpacingBefore`. | ||
| * Get spacing after a paragraph block. Same implicit-default-style collapse | ||
| * rule as `getSpacingBefore`. | ||
| */ | ||
| function getSpacingAfter(block) { | ||
| const value = block.attrs?.spacing?.after ?? 0; | ||
| if (isEmptyParagraph(block) && !block.attrs?.spacingExplicit?.after) return 0; | ||
| if (value === 0) return 0; | ||
| if (isEmptyParagraph(block) && !block.attrs?.styleId && !block.attrs?.spacingExplicit?.after) return 0; | ||
| return value; | ||
@@ -82,5 +100,6 @@ } | ||
| * | ||
| * When two consecutive paragraph blocks both have `contextualSpacing: true` | ||
| * and share the same `styleId`, the spaceAfter of the first paragraph and | ||
| * the spaceBefore of the second paragraph are suppressed (set to 0). | ||
| * Contextual spacing applies independently to each paragraph: suppress the | ||
| * current paragraph's spaceAfter when it opts in, and the next paragraph's | ||
| * spaceBefore when it opts in, provided both paragraphs share a style. Two | ||
| * absent style ids both refer to the document's default paragraph style. | ||
| * | ||
@@ -96,12 +115,11 @@ * This mutates the block attrs in-place before layout runs. | ||
| const nextAttrs = next.attrs; | ||
| if (currAttrs?.contextualSpacing && nextAttrs?.contextualSpacing && currAttrs.styleId && currAttrs.styleId === nextAttrs.styleId) { | ||
| if (currAttrs.spacing) currAttrs.spacing = { | ||
| ...currAttrs.spacing, | ||
| after: 0 | ||
| }; | ||
| if (nextAttrs.spacing) nextAttrs.spacing = { | ||
| ...nextAttrs.spacing, | ||
| before: 0 | ||
| }; | ||
| } | ||
| if (currAttrs?.styleId !== nextAttrs?.styleId) continue; | ||
| if (currAttrs?.contextualSpacing && currAttrs.spacing) currAttrs.spacing = { | ||
| ...currAttrs.spacing, | ||
| after: 0 | ||
| }; | ||
| if (nextAttrs?.contextualSpacing && nextAttrs.spacing) nextAttrs.spacing = { | ||
| ...nextAttrs.spacing, | ||
| before: 0 | ||
| }; | ||
| } | ||
@@ -151,2 +169,3 @@ for (const block of blocks) if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) applyContextualSpacing(cell.blocks); | ||
| margins: initialConfig.margins, | ||
| ...options.mirrorMargins === true ? { mirrorMargins: true } : {}, | ||
| ...options.firstPageMargins !== void 0 ? { firstPageMargins: options.firstPageMargins } : {}, | ||
@@ -160,2 +179,4 @@ columns: initialConfig.columns ?? DEFAULT_COLUMNS, | ||
| const midChainIndices = getMidChainIndices(keepNextChains); | ||
| const blocksById = /* @__PURE__ */ new Map(); | ||
| for (const block of blocks) blocksById.set(String(block.id), block); | ||
| let sectionIdx = 0; | ||
@@ -165,13 +186,19 @@ let activeSectionMarginTop = initialConfig.margins.top; | ||
| let activeSectionMarginBottom = initialConfig.margins.bottom; | ||
| let naturalPageAdvanceSinceRenderedBreak = false; | ||
| for (let i = 0; i < blocks.length; i++) { | ||
| const block = blocks[i]; | ||
| const measure = measures[i]; | ||
| const hasRenderedPageBreak = block.kind === "paragraph" && block.attrs?.renderedPageBreakBefore === true; | ||
| if (hasPageBreakBefore(block)) paginator.forcePageBreak(); | ||
| else if (hasRenderedPageBreak) { | ||
| const state = paginator.getCurrentState(); | ||
| if (!(block.kind === "paragraph" && isPaginationEmptyParagraph(block) && naturalPageAdvanceSinceRenderedBreak) && pageHasVisibleBodyContent(state.page, blocksById)) paginator.forcePageBreak(); | ||
| } | ||
| if (hasRenderedPageBreak || hasPageBreakBefore(block)) naturalPageAdvanceSinceRenderedBreak = false; | ||
| const chain = keepNextChains.get(i); | ||
| if (chain && !midChainIndices.has(i)) { | ||
| const chainHeight = calculateChainHeight(chain, blocks, measures); | ||
| const state = paginator.getCurrentState(); | ||
| const availableHeight = paginator.getAvailableHeight(); | ||
| if (chainHeight <= state.contentBottom - state.topMargin && chainHeight > availableHeight && state.page.fragments.length > 0) paginator.forcePageBreak(); | ||
| paginator.ensureFits(chainHeight); | ||
| } | ||
| const pageBeforeBlockLayout = paginator.getCurrentState().page.number; | ||
| switch (block.kind) { | ||
@@ -213,2 +240,5 @@ case "paragraph": | ||
| } | ||
| const isVisibleBodyBlock = block.kind === "paragraph" ? !isEmptyParagraph(block) : block.kind !== "pageBreak" && block.kind !== "columnBreak" && block.kind !== "sectionBreak"; | ||
| if (block.kind === "pageBreak" || block.kind === "columnBreak" || block.kind === "sectionBreak") naturalPageAdvanceSinceRenderedBreak = false; | ||
| else if (isVisibleBodyBlock && paginator.getCurrentState().page.number > pageBeforeBlockLayout) naturalPageAdvanceSinceRenderedBreak = true; | ||
| } | ||
@@ -407,3 +437,6 @@ if (paginator.pages.length === 0) paginator.getCurrentState(); | ||
| else if (block.justification === "right") x = x + paginator.columnWidth - measure.totalWidth; | ||
| else if (block.indent) x += block.indent; | ||
| else if (block.indent !== void 0) { | ||
| const leadingCellMargin = block.rows[0]?.cells[0]?.padding?.left ?? 0; | ||
| x += block.indent - leadingCellMargin; | ||
| } | ||
| return x; | ||
@@ -542,2 +575,3 @@ }; | ||
| if (floating?.vertAnchor === "page") baseY = 0; | ||
| else if (floating?.vertAnchor === "text") baseY = state.cursorY; | ||
| let x = paginator.getColumnX(state.columnIndex); | ||
@@ -661,2 +695,31 @@ if (floating?.tblpX !== void 0) x = baseX + floating.tblpX; | ||
| } | ||
| if (block.position !== void 0) { | ||
| const state = paginator.getCurrentState(); | ||
| const horizontal = block.position.horizontal; | ||
| const x = horizontal ? bandFragmentX(horizontal, { | ||
| pageWidth: state.page.size.w, | ||
| marginLeft: state.page.margins.left, | ||
| marginRight: state.page.margins.right, | ||
| boxWidth: measure.width | ||
| }) : paginator.getColumnX(state.columnIndex); | ||
| const vertical = block.position.vertical; | ||
| const y = isPageFrameRelativeAnchor(vertical?.relativeTo) ? state.topMargin + bandTopContentY(vertical, { | ||
| pageHeight: sectionPageHeight, | ||
| marginTop: sectionMarginTop, | ||
| marginBottom: sectionMarginBottom, | ||
| boxHeight: measure.height | ||
| }) : state.cursorY + emuToPixels(vertical?.posOffset ?? 0); | ||
| const fragment = { | ||
| kind: "textBox", | ||
| blockId: block.id, | ||
| x, | ||
| y, | ||
| width: measure.width, | ||
| height: measure.height, | ||
| ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {}, | ||
| ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {} | ||
| }; | ||
| state.page.fragments.push(fragment); | ||
| return; | ||
| } | ||
| const state = paginator.ensureFits(measure.height); | ||
@@ -728,2 +791,2 @@ const fragment = { | ||
| //#endregion | ||
| export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak }; | ||
| export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyContextualSpacing, applyPendingToActive, assertExhaustiveFlowBlock, calculateChainHeight, collectSectionConfigs, computeKeepNextChains, createInitialSectionState, createPaginator, findPageIndexContainingPmPos, floatingTextBoxReservesBand, floatingTextBoxWrapsText, getEffectiveColumns, getEffectiveMargins, getEffectivePageSize, getHeaderRowsHeight, getMidChainIndices, hasKeepLines, hasPageBreakBefore, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, layoutDocument, scheduleSectionBreak, tableColumnsArePinned }; |
@@ -5,7 +5,8 @@ import { FlowBlock, Measure } from "./types.js"; | ||
| /** | ||
| * A chain of consecutive keepNext paragraphs. | ||
| * A chain of keepNext-linked paragraphs, including empty separators Word | ||
| * carries through to the next paragraph with visible content. | ||
| */ | ||
| type KeepNextChain = { | ||
| /** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last paragraph in the chain. */ | ||
| endIndex: number; /** All paragraph indices in the chain. */ | ||
| /** Index of the first paragraph in the chain. */startIndex: number; /** Index of the last keepNext or pass-through empty member. */ | ||
| endIndex: number; /** All keepNext and pass-through empty paragraph indices in the chain. */ | ||
| memberIndices: number[]; /** Index of the anchor paragraph (first non-keepNext after chain), or -1 if none. */ | ||
@@ -17,5 +18,5 @@ anchorIndex: number; | ||
| * | ||
| * A keepNext chain is a sequence of consecutive paragraphs with keepNext=true, | ||
| * followed by an anchor paragraph (the first non-keepNext paragraph). | ||
| * The entire chain must stay on the same page as the anchor's first line. | ||
| * A keepNext chain starts with a paragraph whose keepNext=true and continues | ||
| * through further keepNext paragraphs and structural empty separators. The | ||
| * first visible non-keepNext paragraph is its anchor. | ||
| * | ||
@@ -26,5 +27,9 @@ * Returns a map from chain start index to chain info. | ||
| /** | ||
| * Calculate the total height needed to keep a chain together. | ||
| * Calculate the height needed to keep consecutive paragraph boundaries from | ||
| * breaking across pages. | ||
| * | ||
| * Includes all chain members plus the first line of the anchor paragraph. | ||
| * Single-line and `keepLines` members are indivisible, so the reservation must | ||
| * continue through them to the anchor's first line. A multi-line member without | ||
| * `keepLines` may itself split; only its first line is needed to satisfy the | ||
| * preceding member's `keepNext`, and the chain can stop there. | ||
| */ | ||
@@ -31,0 +36,0 @@ declare function calculateChainHeight(chain: KeepNextChain, blocks: FlowBlock[], measures: Measure[]): number; |
| //#region src/layout-engine/keep-together.ts | ||
| function isVisuallyEmptyParagraph(block) { | ||
| if (block.runs.length === 0) return true; | ||
| if (block.runs.length !== 1) return false; | ||
| const run = block.runs.at(0); | ||
| return run?.kind === "text" && run.text === ""; | ||
| } | ||
| /** | ||
| * Pre-scan blocks to find all keepNext chains. | ||
| * | ||
| * A keepNext chain is a sequence of consecutive paragraphs with keepNext=true, | ||
| * followed by an anchor paragraph (the first non-keepNext paragraph). | ||
| * The entire chain must stay on the same page as the anchor's first line. | ||
| * A keepNext chain starts with a paragraph whose keepNext=true and continues | ||
| * through further keepNext paragraphs and structural empty separators. The | ||
| * first visible non-keepNext paragraph is its anchor. | ||
| * | ||
@@ -25,3 +31,4 @@ * Returns a map from chain start index to chain info. | ||
| if (nextBlock.kind !== "paragraph") break; | ||
| if (nextBlock.attrs?.keepNext) { | ||
| const nextPara = nextBlock; | ||
| if (nextPara.attrs?.keepNext || isVisuallyEmptyParagraph(nextPara)) { | ||
| memberIndices.push(j); | ||
@@ -48,27 +55,35 @@ endIndex = j; | ||
| /** | ||
| * Calculate the total height needed to keep a chain together. | ||
| * Calculate the height needed to keep consecutive paragraph boundaries from | ||
| * breaking across pages. | ||
| * | ||
| * Includes all chain members plus the first line of the anchor paragraph. | ||
| * Single-line and `keepLines` members are indivisible, so the reservation must | ||
| * continue through them to the anchor's first line. A multi-line member without | ||
| * `keepLines` may itself split; only its first line is needed to satisfy the | ||
| * preceding member's `keepNext`, and the chain can stop there. | ||
| */ | ||
| function calculateChainHeight(chain, blocks, measures) { | ||
| let totalHeight = 0; | ||
| for (const memberIndex of chain.memberIndices) { | ||
| const block = blocks[memberIndex]; | ||
| const measure = measures[memberIndex]; | ||
| if (!block || !measure || block.kind !== "paragraph" || measure.kind !== "paragraph") continue; | ||
| const para = block; | ||
| const paraMeasure = measure; | ||
| const spacingBefore = para.attrs?.spacing?.before ?? 0; | ||
| totalHeight += spacingBefore; | ||
| totalHeight += paraMeasure.totalHeight; | ||
| const spacingAfter = para.attrs?.spacing?.after ?? 0; | ||
| totalHeight += spacingAfter; | ||
| const firstMemberIndex = chain.memberIndices.at(0); | ||
| if (firstMemberIndex === void 0) return 0; | ||
| const firstBlock = blocks[firstMemberIndex]; | ||
| const firstMeasure = measures[firstMemberIndex]; | ||
| if (firstBlock?.kind !== "paragraph" || firstMeasure?.kind !== "paragraph") return 0; | ||
| let totalHeight = (firstBlock.attrs?.spacing?.before ?? 0) + firstMeasure.totalHeight; | ||
| let trailingSpacing = firstBlock.attrs?.spacing?.after ?? 0; | ||
| const successorIndices = [...chain.memberIndices.slice(1)]; | ||
| if (chain.anchorIndex !== -1) successorIndices.push(chain.anchorIndex); | ||
| for (let index = 0; index < successorIndices.length; index++) { | ||
| const successorIndex = successorIndices[index]; | ||
| if (successorIndex === void 0) continue; | ||
| const successorBlock = blocks[successorIndex]; | ||
| const successorMeasure = measures[successorIndex]; | ||
| if (successorBlock?.kind !== "paragraph" || successorMeasure?.kind !== "paragraph") return totalHeight; | ||
| totalHeight += Math.max(trailingSpacing, successorBlock.attrs?.spacing?.before ?? 0); | ||
| const firstLine = successorMeasure.lines.at(0); | ||
| if (!firstLine) return totalHeight; | ||
| const isAnchor = index === successorIndices.length - 1 && chain.anchorIndex !== -1; | ||
| const isSplittable = successorMeasure.lines.length > 1 && !successorBlock.attrs?.keepLines; | ||
| if (isAnchor || isSplittable) return totalHeight + firstLine.lineHeight; | ||
| totalHeight += successorMeasure.totalHeight; | ||
| trailingSpacing = successorBlock.attrs?.spacing?.after ?? 0; | ||
| } | ||
| if (chain.anchorIndex !== -1) { | ||
| const anchorMeasure = measures[chain.anchorIndex]; | ||
| if (anchorMeasure?.kind === "paragraph") { | ||
| const anchorPara = anchorMeasure; | ||
| if (anchorPara.lines.length > 0) totalHeight += anchorPara.lines[0].lineHeight; | ||
| } | ||
| } | ||
| return totalHeight; | ||
@@ -75,0 +90,0 @@ } |
@@ -8,3 +8,8 @@ import { FlowBlock } from "./types.js"; | ||
| type HiddenEditorPhase = "editor-state" | "editor-view" | "to-prose-doc" | "update-state"; | ||
| type DocumentLoadPhase = "docx-parse"; | ||
| type LayoutInstrumentation = { | ||
| onDocumentLoadPhase?: (event: { | ||
| durationMs: number; | ||
| phase: DocumentLoadPhase; | ||
| }) => void; | ||
| onHiddenEditorPhase?: (event: { | ||
@@ -25,2 +30,5 @@ durationMs: number; | ||
| }) => void; | ||
| onLayoutStart?: (event: { | ||
| reason: LayoutRunReason; | ||
| }) => void; | ||
| onLayoutPhase?: (event: { | ||
@@ -47,2 +55,3 @@ durationMs: number; | ||
| declare function recordLayoutComplete(reason?: LayoutRunReason): void; | ||
| declare function recordLayoutStart(reason: LayoutRunReason): void; | ||
| declare function recordLayoutError(reason: LayoutRunReason, error: unknown): void; | ||
@@ -52,3 +61,4 @@ declare function recordLayoutPhase(reason: LayoutRunReason, phase: LayoutPhase, durationMs: number): void; | ||
| declare function recordHiddenEditorPhase(reason: HiddenEditorStateReason, phase: HiddenEditorPhase, durationMs: number): void; | ||
| declare function recordDocumentLoadPhase(phase: DocumentLoadPhase, durationMs: number): void; | ||
| //#endregion | ||
| export { HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordMeasureBlock, recordMeasureBlockError }; | ||
| export { DocumentLoadPhase, HiddenEditorPhase, HiddenEditorStateReason, LayoutInstrumentation, LayoutPhase, LayoutRunReason, recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError }; |
@@ -18,2 +18,5 @@ //#region src/layout-engine/layoutInstrumentation.ts | ||
| } | ||
| function recordLayoutStart(reason) { | ||
| globalThis.__folioLayoutInstrumentation?.onLayoutStart?.({ reason }); | ||
| } | ||
| function recordLayoutError(reason, error) { | ||
@@ -43,3 +46,9 @@ const message = error instanceof Error ? error.message : String(error); | ||
| } | ||
| function recordDocumentLoadPhase(phase, durationMs) { | ||
| globalThis.__folioLayoutInstrumentation?.onDocumentLoadPhase?.({ | ||
| durationMs, | ||
| phase | ||
| }); | ||
| } | ||
| //#endregion | ||
| export { recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordMeasureBlock, recordMeasureBlockError }; | ||
| export { recordDocumentLoadPhase, recordHiddenEditorPhase, recordHiddenEditorStateCreate, recordLayoutComplete, recordLayoutError, recordLayoutPhase, recordLayoutStart, recordMeasureBlock, recordMeasureBlockError }; |
@@ -1,4 +0,5 @@ | ||
| import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand } from "../types.js"; | ||
| import { DEFAULT_TEXTBOX_MARGINS, floatingTextBoxReservesBand, tableColumnsArePinned } from "../types.js"; | ||
| import { findClearLineY, measureParagraph } from "./measureParagraph.js"; | ||
| import { getCachedParagraphMeasure, setCachedParagraphMeasure } from "./cache.js"; | ||
| import { getTextBoxGroupId } from "../textBoxGroup.js"; | ||
| import { recordMeasureBlock, recordMeasureBlockError } from "../layoutInstrumentation.js"; | ||
@@ -83,4 +84,4 @@ import { bandTopContentY, isPageFrameRelativeAnchor } from "../textBoxFlow.js"; | ||
| } | ||
| function getTableCellVerticalBorderHeight(cell) { | ||
| return (cell?.borders?.top?.width ?? 0) + (cell?.borders?.bottom?.width ?? 0); | ||
| function getTableCellVerticalBorderHeight(cell, isFirstRow) { | ||
| return (isFirstRow ? cell?.borders?.top?.width ?? 0 : 0) + (cell?.borders?.bottom?.width ?? 0); | ||
| } | ||
@@ -122,2 +123,3 @@ function measureTableBlock(tableBlock, contentWidth, fieldValues) { | ||
| } | ||
| const columnsPinned = tableColumnsArePinned(tableBlock); | ||
| const rows = tableBlock.rows.map((row, rowIdx) => { | ||
@@ -138,3 +140,3 @@ let columnIndex = 0; | ||
| const cellContentWidth = Math.max(1, cellWidth - padLeft - padRight); | ||
| const measureWidth = cell.noWrap ? NO_WRAP_MEASURE_WIDTH : cellContentWidth; | ||
| const measureWidth = cell.noWrap === true && !columnsPinned ? NO_WRAP_MEASURE_WIDTH : cellContentWidth; | ||
| const cellMeasure = { | ||
@@ -166,2 +168,3 @@ blocks: cell.blocks.map((b) => measureBlock(b, measureWidth, void 0, void 0, fieldValues)), | ||
| let maxCellHeightWithBorders = 0; | ||
| let maxBorderHeight = 0; | ||
| for (let cellIdx = 0; cellIdx < row.cells.length; cellIdx++) { | ||
@@ -180,3 +183,5 @@ const cell = row.cells[cellIdx]; | ||
| cell.height += padTop + padBottom; | ||
| maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + getTableCellVerticalBorderHeight(sourceCell)); | ||
| const borderHeight = getTableCellVerticalBorderHeight(sourceCell, rowIdx === 0); | ||
| maxCellHeightWithBorders = Math.max(maxCellHeightWithBorders, cell.height + borderHeight); | ||
| maxBorderHeight = Math.max(maxBorderHeight, borderHeight); | ||
| } | ||
@@ -186,3 +191,3 @@ const explicitHeight = sourceRow?.height; | ||
| if (explicitHeight && heightRule === "exact") row.height = explicitHeight; | ||
| else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight); | ||
| else if (explicitHeight) row.height = Math.max(maxCellHeightWithBorders, explicitHeight + maxBorderHeight); | ||
| else row.height = maxCellHeightWithBorders; | ||
@@ -206,2 +211,3 @@ } | ||
| const zones = []; | ||
| const textBoxGroupAnchors = /* @__PURE__ */ new Map(); | ||
| const defaultMarginTop = Array.isArray(marginTop) ? marginTop[0] ?? 0 : marginTop; | ||
@@ -296,5 +302,4 @@ const pageHeightInput = pageGeometry?.pageHeight ?? 0; | ||
| const tb = block; | ||
| if (!floatingTextBoxReservesBand(tb)) continue; | ||
| if (!floatingTextBoxReservesBand(tb) || tb.position === void 0) continue; | ||
| const v = tb.position?.vertical; | ||
| if (!isPageFrameRelativeAnchor(v?.relativeTo)) continue; | ||
| const height = measureTextBoxBlock(tb).height; | ||
@@ -304,3 +309,4 @@ const distTop = tb.distTop ?? 0; | ||
| const blockMarginTop = perBlockNumberValue(marginTop, blockIndex, defaultMarginTop); | ||
| const rawTop = bandTopContentY(v, { | ||
| const pageFrameRelative = isPageFrameRelativeAnchor(v?.relativeTo); | ||
| const rawTop = pageFrameRelative ? bandTopContentY(v, { | ||
| pageHeight: perBlockNumberValue(pageHeightInput, blockIndex, defaultPageHeight), | ||
@@ -310,5 +316,8 @@ marginTop: blockMarginTop, | ||
| boxHeight: height | ||
| }); | ||
| }) : emuToPixels(v?.posOffset ?? 0); | ||
| const bottomY = rawTop + height + distBottom; | ||
| if (bottomY <= 0) continue; | ||
| const groupId = getTextBoxGroupId(tb); | ||
| const anchorBlockIndex = groupId ? textBoxGroupAnchors.get(groupId) ?? blockIndex : blockIndex; | ||
| if (groupId && !textBoxGroupAnchors.has(groupId)) textBoxGroupAnchors.set(groupId, blockIndex); | ||
| zones.push({ | ||
@@ -319,4 +328,4 @@ leftMargin: 0, | ||
| bottomY, | ||
| anchorBlockIndex: blockIndex, | ||
| isMarginRelative: true, | ||
| anchorBlockIndex, | ||
| ...pageFrameRelative ? { isMarginRelative: true } : {}, | ||
| fullWidthBlock: true | ||
@@ -381,10 +390,11 @@ }); | ||
| /** | ||
| * `true` for a section break that starts a new page (`nextPage`/`evenPage`/ | ||
| * `oddPage`, or an unspecified type, which the layout treats as `nextPage`). | ||
| * A `continuous` break stays on the current page, so it does not open a fresh | ||
| * page frame. Used by `measureBlocks` to reset the running Y for a page-pinned | ||
| * band that lands right after the break. eigenpal #694. | ||
| * `true` for a section break that explicitly starts a new page | ||
| * (`nextPage`/`evenPage`/`oddPage`). An absent type follows | ||
| * `scheduleSectionBreak` and stays continuous, so measurement must retain any | ||
| * active exclusion zones across it. Used by `measureBlocks` to reset the | ||
| * running Y for a page-pinned band that lands right after the break. | ||
| * eigenpal #694. | ||
| */ | ||
| function isNextPageSectionBreak(block) { | ||
| return block.kind === "sectionBreak" && block.type !== "continuous"; | ||
| return block.kind === "sectionBreak" && block.type !== void 0 && block.type !== "continuous"; | ||
| } | ||
@@ -391,0 +401,0 @@ /** |
@@ -25,4 +25,6 @@ import { getFontMetrics, measureRun, measureTextWidth } from "./measureProvider.js"; | ||
| const JUSTIFY_SHRINK_TOLERANCE_RATIO = .016; | ||
| const JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO = .017; | ||
| const JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO = .025; | ||
| const JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO = .021; | ||
| const DEFAULT_LIST_HANGING_INDENT_PX = 24; | ||
| const ALL_CAPS_RATIO_THRESHOLD = .8; | ||
@@ -274,4 +276,12 @@ /** | ||
| } | ||
| function canClampTabToRightEdge(alignment, currentLineWidth) { | ||
| if (alignment === "start" || alignment === "default") return currentLineWidth > WIDTH_TOLERANCE; | ||
| function hasPriorTabOnLine(runs, tabIndex) { | ||
| for (let i = tabIndex - 1; i >= 0; i--) { | ||
| const prior = runs[i]; | ||
| if (!prior || isLineBreakRun(prior) || isImageRun(prior) && isBlockLayoutImageRun(prior)) break; | ||
| if (isTabRun(prior)) return true; | ||
| } | ||
| return false; | ||
| } | ||
| function canClampTabToRightEdge(alignment, currentLineWidth, hasPriorTab, followingWidth, availableWidth) { | ||
| if (alignment === "start" || alignment === "default") return currentLineWidth > WIDTH_TOLERANCE && (hasPriorTab || followingWidth <= availableWidth); | ||
| return true; | ||
@@ -324,6 +334,14 @@ } | ||
| } | ||
| function justifyShrinkToleranceRatio(block) { | ||
| function justifyShrinkToleranceRatio(block, isFirstLine, regularSpaceCount, nonBreakingSpaceCount) { | ||
| if (block.attrs?.listMarker !== void 0) { | ||
| if ((block.attrs.indent?.hanging ?? 0) <= DEFAULT_LIST_HANGING_INDENT_PX) return JUSTIFY_SHRINK_TOLERANCE_RATIO; | ||
| if (isFirstLine) return JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO; | ||
| const totalSpaces = regularSpaceCount + nonBreakingSpaceCount; | ||
| if (totalSpaces === 0) return JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO; | ||
| return Math.max(JUSTIFY_SHRINK_TOLERANCE_RATIO, JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO * (regularSpaceCount / totalSpaces)); | ||
| } | ||
| const hasTabStops = (block.attrs?.tabs?.length ?? 0) > 0; | ||
| const hasTabRuns = block.runs.some(isTabRun); | ||
| if (hasTabStops || hasTabRuns) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO; | ||
| if (!isFirstLine && hasTabRuns && !hasTabStops) return JUSTIFY_LITERAL_TAB_CONTINUATION_SHRINK_TOLERANCE_RATIO; | ||
| if ((isFirstLine || hasTabStops) && (hasTabStops || hasTabRuns)) return (block.attrs?.indent?.firstLine ?? 0) === 0 ? JUSTIFY_HANGING_TAB_SHRINK_TOLERANCE_RATIO : JUSTIFY_SHRINK_TOLERANCE_RATIO; | ||
| if (uppercaseLetterRatio(block.runs.map((run) => isTextRun(run) ? run.text ?? "" : "").join("")) > ALL_CAPS_RATIO_THRESHOLD) return JUSTIFY_SHRINK_TOLERANCE_RATIO; | ||
@@ -418,3 +436,2 @@ return JUSTIFY_PROSE_SHRINK_TOLERANCE_RATIO; | ||
| const isJustifiedParagraph = attrs?.alignment === "justify"; | ||
| const justifyToleranceRatio = justifyShrinkToleranceRatio(block); | ||
| const floatingZones = options?.floatingZones; | ||
@@ -513,2 +530,5 @@ const paragraphYOffset = options?.paragraphYOffset ?? 0; | ||
| width: 0, | ||
| trailingWhitespaceWidth: 0, | ||
| regularSpaceCount: 0, | ||
| nonBreakingSpaceCount: 0, | ||
| maxFontSize: DEFAULT_FONT_SIZE, | ||
@@ -560,3 +580,3 @@ maxFontMetrics: null, | ||
| toChar: currentLine.toChar, | ||
| width: currentLine.width, | ||
| width: Math.max(0, currentLine.width - currentLine.trailingWhitespaceWidth), | ||
| ...finalTypography | ||
@@ -581,2 +601,3 @@ }; | ||
| const floatingMargins = getFloatingMargins(cumulativeHeight, estimatedLineHeight, floatingZones, paragraphYOffset); | ||
| const adjustedWidth = Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth)); | ||
| currentLine = { | ||
@@ -588,2 +609,5 @@ fromRun: runIndex, | ||
| width: 0, | ||
| trailingWhitespaceWidth: 0, | ||
| regularSpaceCount: 0, | ||
| nonBreakingSpaceCount: 0, | ||
| maxFontSize: DEFAULT_FONT_SIZE, | ||
@@ -593,3 +617,3 @@ maxFontMetrics: null, | ||
| maxMathHeightPx: 0, | ||
| availableWidth: Math.max(1, getFloatingAvailableWidth(floatingMargins, bodyContentWidth)), | ||
| availableWidth: adjustedWidth, | ||
| leftOffset: floatingMargins.leftMargin, | ||
@@ -632,2 +656,3 @@ rightOffset: floatingMargins.rightMargin, | ||
| ...attrs?.tabs !== void 0 ? { explicitStops: attrs.tabs } : {}, | ||
| ...attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: attrs.defaultTabStopTwips } : {}, | ||
| leftIndent: pixelsToTwips(indentLeft) | ||
@@ -640,3 +665,3 @@ }, { | ||
| const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset; | ||
| if (!hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth); | ||
| if (!hasFollowingTabOnLine(runs, runIndex) && canClampTabToRightEdge(tabResult.alignment, currentLine.width, hasPriorTabOnLine(runs, runIndex), followingWidth, currentLine.availableWidth) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth); | ||
| if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) { | ||
@@ -647,2 +672,3 @@ startNewLine(runIndex, 0); | ||
| currentLine.width += tabWidth; | ||
| currentLine.trailingWhitespaceWidth = 0; | ||
| currentLine.toRun = runIndex; | ||
@@ -677,2 +703,3 @@ currentLine.toChar = 1; | ||
| currentLine.width += imageWidth; | ||
| currentLine.trailingWhitespaceWidth = 0; | ||
| currentLine.toRun = runIndex; | ||
@@ -692,2 +719,3 @@ currentLine.toChar = 1; | ||
| currentLine.width += fieldWidth; | ||
| currentLine.trailingWhitespaceWidth = 0; | ||
| currentLine.toRun = runIndex; | ||
@@ -713,2 +741,3 @@ currentLine.toChar = 1; | ||
| currentLine.width += mathWidth; | ||
| currentLine.trailingWhitespaceWidth = 0; | ||
| currentLine.toRun = runIndex; | ||
@@ -738,9 +767,13 @@ currentLine.toChar = 1; | ||
| const word = text.slice(charIndex, nextBreak); | ||
| const wordWidth = measureTextWidth(word, style); | ||
| const widthTolerance = isJustifiedParagraph ? Math.max(WIDTH_TOLERANCE, currentLine.availableWidth * justifyToleranceRatio) : WIDTH_TOLERANCE; | ||
| const measuredWord = trimTrailingSpacesAndTabs(word); | ||
| const wordWidth = measureTextWidth(measuredWord, style); | ||
| const fullWordWidth = measureTextWidth(word, style); | ||
| const regularSpaces = measuredWord.split(" ").length - 1; | ||
| const nonBreakingSpaces = measuredWord.split("\xA0").length - 1; | ||
| const widthTolerance = isJustifiedParagraph ? Math.max(WIDTH_TOLERANCE, currentLine.availableWidth * justifyShrinkToleranceRatio(block, lines.length === 0, currentLine.regularSpaceCount + regularSpaces, currentLine.nonBreakingSpaceCount + nonBreakingSpaces)) : WIDTH_TOLERANCE; | ||
| if (wordWidth > currentLine.availableWidth + widthTolerance) { | ||
| let chunkStart = 0; | ||
| while (chunkStart < word.length) { | ||
| while (chunkStart < measuredWord.length) { | ||
| const spaceLeft = currentLine.availableWidth - currentLine.width + WIDTH_TOLERANCE; | ||
| let bestEnd = findMaxFittingLength(word.slice(chunkStart), style, spaceLeft); | ||
| let bestEnd = findMaxFittingLength(measuredWord.slice(chunkStart), style, spaceLeft); | ||
| if (bestEnd === 0) { | ||
@@ -755,8 +788,12 @@ if (currentLine.width > 0) { | ||
| const chunkEnd = chunkStart + bestEnd; | ||
| const chunkWidth = measureTextWidth(word.slice(chunkStart, chunkEnd), style); | ||
| const chunk = measuredWord.slice(chunkStart, chunkEnd); | ||
| const chunkWidth = measureTextWidth(chunk, style); | ||
| currentLine.width += chunkWidth; | ||
| currentLine.trailingWhitespaceWidth = chunkWidth - measureTextWidth(trimTrailingSpacesAndTabs(chunk), style); | ||
| currentLine.regularSpaceCount += chunk.split(" ").length - 1; | ||
| currentLine.nonBreakingSpaceCount += chunk.split("\xA0").length - 1; | ||
| currentLine.toRun = runIndex; | ||
| currentLine.toChar = charIndex + chunkEnd; | ||
| chunkStart = chunkEnd; | ||
| if (chunkStart < word.length) { | ||
| if (chunkStart < measuredWord.length) { | ||
| startNewLine(runIndex, charIndex + chunkStart); | ||
@@ -766,2 +803,8 @@ updateMaxFont(lineHeightStyle); | ||
| } | ||
| const trailingWhitespaceWidth = fullWordWidth - wordWidth; | ||
| currentLine.width += trailingWhitespaceWidth; | ||
| currentLine.trailingWhitespaceWidth = trailingWhitespaceWidth; | ||
| currentLine.regularSpaceCount += word.split(" ").length - 1; | ||
| currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1; | ||
| currentLine.toChar = nextBreak; | ||
| charIndex = nextBreak; | ||
@@ -776,3 +819,6 @@ continue; | ||
| } | ||
| currentLine.width += wordWidth; | ||
| currentLine.width += fullWordWidth; | ||
| currentLine.trailingWhitespaceWidth = fullWordWidth - wordWidth; | ||
| currentLine.regularSpaceCount += word.split(" ").length - 1; | ||
| currentLine.nonBreakingSpaceCount += word.split("\xA0").length - 1; | ||
| currentLine.toRun = runIndex; | ||
@@ -779,0 +825,0 @@ currentLine.toChar = nextBreak; |
@@ -30,3 +30,4 @@ import { ColumnLayout, FOOTNOTE_SEPARATOR_HEIGHT, Fragment, Page, PageHeaderFooterRefs, PageMargins } from "./types.js"; | ||
| }; /** Page margins. */ | ||
| margins: PageMargins; | ||
| margins: PageMargins; /** Swap left/right margins on even physical pages. */ | ||
| mirrorMargins?: boolean; | ||
| /** | ||
@@ -33,0 +34,0 @@ * Margins applied only to the very first page (page 1) of the paginator. |
@@ -49,3 +49,3 @@ import { FOOTNOTE_SEPARATOR_HEIGHT } from "./types.js"; | ||
| if (pendingPageSize || pendingMargins) return false; | ||
| return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, margins); | ||
| return arePageSizesEqual(state.page.size, pageSize) && areMarginsEqual(state.page.margins, getPageMargins(state.page.number)); | ||
| } | ||
@@ -66,3 +66,9 @@ if (getContentHeight() <= 0) panic("Paginator: page size and margins yield no content area"); | ||
| function getPageMargins(pageNumber) { | ||
| return pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins }; | ||
| const pageMargins = pageNumber === 1 && options.firstPageMargins ? { ...options.firstPageMargins } : { ...margins }; | ||
| if (options.mirrorMargins === true && pageNumber % 2 === 0) return { | ||
| ...pageMargins, | ||
| left: pageMargins.right, | ||
| right: pageMargins.left | ||
| }; | ||
| return pageMargins; | ||
| } | ||
@@ -74,3 +80,3 @@ let columnRegionTop = margins.top; | ||
| function getColumnX(columnIndex) { | ||
| return margins.left + columnIndex * (columnWidth + columns.gap); | ||
| return (states.at(-1)?.page.margins.left ?? getPageMargins(1).left) + columnIndex * (columnWidth + columns.gap); | ||
| } | ||
@@ -77,0 +83,0 @@ /** |
@@ -6,3 +6,3 @@ import { OutlineStyleAttr } from "../types/documentEnumValues.js"; | ||
| declare namespace types_d_exports { | ||
| export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun }; | ||
| export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned }; | ||
| } | ||
@@ -308,3 +308,4 @@ /** | ||
| widowControl?: boolean; | ||
| pageBreakBefore?: boolean; | ||
| pageBreakBefore?: boolean; /** Word's cached pagination hint (`w:lastRenderedPageBreak`). */ | ||
| renderedPageBreakBefore?: boolean; | ||
| styleId?: string; | ||
@@ -464,3 +465,10 @@ contextualSpacing?: boolean; | ||
| width?: number; /** Table width type ('auto', 'pct', 'dxa', 'nil'). */ | ||
| widthType?: TableWidthType; /** Table horizontal alignment */ | ||
| widthType?: TableWidthType; | ||
| /** | ||
| * Table layout algorithm (`w:tblLayout`). "fixed" pins columns to their grid | ||
| * widths; "autofit" (the default when absent) lets Word size columns to | ||
| * content. Consulted alongside `widthType` to decide whether `w:noWrap` cells | ||
| * can be honored (see `tableColumnsArePinned`). | ||
| */ | ||
| layout?: "fixed" | "autofit"; /** Table horizontal alignment */ | ||
| justification?: "left" | "center" | "right"; /** Table indent from left margin (in pixels, from w:tblInd) */ | ||
@@ -909,3 +917,4 @@ indent?: number; /** Right-to-left column order (w:bidiVisual): logical column 0 paints on the right. */ | ||
| titlePage?: boolean; /** Whether section has different even/odd headers/footers. */ | ||
| evenAndOddHeaders?: boolean; /** Per-page footnote reserved heights (pageNumber → height in pixels). */ | ||
| evenAndOddHeaders?: boolean; /** Swap left/right margins on even physical pages. */ | ||
| mirrorMargins?: boolean; /** Per-page footnote reserved heights (pageNumber → height in pixels). */ | ||
| footnoteReservedHeights?: Map<number, number>; | ||
@@ -977,6 +986,4 @@ /** | ||
| /** | ||
| * Flow-only bounds used by `computeHeaderFooterMarginExtender` to push | ||
| * body margins clear of HF overflow. Excludes `behindDoc` images that | ||
| * the renderer paints behind body content; including them would reserve | ||
| * a full-page letterhead as body push-down. | ||
| * Flow-only bounds for consumers that need to distinguish normal | ||
| * header/footer flow from floating or `behindDoc` artwork. | ||
| */ | ||
@@ -1039,3 +1046,17 @@ marginPushTop?: number; | ||
| declare function floatingTextBoxReservesBand(block: TextBoxFlowAttrs): boolean; | ||
| /** | ||
| * A table's columns are pinned when the layout engine cannot widen a column to | ||
| * fit content that overflows it: either the layout is explicitly fixed | ||
| * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/ | ||
| * `pct`) is fully consumed by the grid. | ||
| * | ||
| * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word | ||
| * can only honor it by widening the column. When the columns are pinned it | ||
| * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table | ||
| * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column | ||
| * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement | ||
| * and the painter both consult this so their line count and `white-space` agree. | ||
| */ | ||
| declare function tableColumnsArePinned(table: TableBlock): boolean; | ||
| //#endregion | ||
| export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_d_exports }; | ||
| export { BlockId, BorderStyle, CellBorderSpec, CellBorders, ColumnBreakBlock, ColumnBreakMeasure, ColumnLayout, DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, DocumentPosition, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, FieldRun, FloatingTablePosition, FlowBlock, FootnoteContent, Fragment, FragmentBase, HeaderFooterContent, HeaderFooterContentHeights, HeaderFooterLayout, HitTestResult, HyperlinkInfo, ImageBlock, ImageFragment, ImageMeasure, ImageRun, ImageRunPosition, Layout, LayoutOptions, LineBreakRun, ListNumPr, MathRun, Measure, MeasuredLine, Page, PageBreakBlock, PageBreakMeasure, PageHeaderFooterRefs, PageMargins, ParagraphAttrs, ParagraphBlock, ParagraphBorders, ParagraphFragment, ParagraphIndent, ParagraphMeasure, ParagraphSpacing, Run, RunFormatting, SdtGroup, SectionBreakBlock, SectionBreakMeasure, TabAlignment, TabRun, TabStop, TableBlock, TableCell, TableCellMeasure, TableFragment, TableMeasure, TableRow, TableRowMeasure, TextBoxBlock, TextBoxFlowAttrs, TextBoxFragment, TextBoxMeasure, TextRun, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_d_exports }; |
@@ -14,3 +14,4 @@ import { __exportAll } from "../_virtual/_rolldown/runtime.js"; | ||
| isFloatingTextBoxBlock: () => isFloatingTextBoxBlock, | ||
| isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun | ||
| isTextWrappingFloatingImageRun: () => isTextWrappingFloatingImageRun, | ||
| tableColumnsArePinned: () => tableColumnsArePinned | ||
| }); | ||
@@ -100,3 +101,20 @@ /** Default internal margins for text boxes (OOXML defaults in pixels) */ | ||
| } | ||
| /** | ||
| * A table's columns are pinned when the layout engine cannot widen a column to | ||
| * fit content that overflows it: either the layout is explicitly fixed | ||
| * (`w:tblLayout w:type="fixed"`) or an explicit total width (`w:tblW` `dxa`/ | ||
| * `pct`) is fully consumed by the grid. | ||
| * | ||
| * In OOXML, `w:noWrap` (§17.4.30) asks Word not to soft-wrap a cell, but Word | ||
| * can only honor it by widening the column. When the columns are pinned it | ||
| * cannot, so overflowing content wraps despite `w:noWrap`. An auto-width table | ||
| * (`w:tblW` `auto`/`nil`/absent with autofit layout) lets Word widen the column | ||
| * instead, so there `w:noWrap` genuinely keeps content on one line. Measurement | ||
| * and the painter both consult this so their line count and `white-space` agree. | ||
| */ | ||
| function tableColumnsArePinned(table) { | ||
| if (table.layout === "fixed") return true; | ||
| return table.widthType === "dxa" || table.widthType === "pct"; | ||
| } | ||
| //#endregion | ||
| export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, types_exports }; | ||
| export { DEFAULT_TEXTBOX_MARGINS, DEFAULT_TEXTBOX_WIDTH, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, floatingTextBoxReservesBand, floatingTextBoxWrapsText, isFloatingImageRun, isFloatingTextBoxBlock, isTextWrappingFloatingImageRun, tableColumnsArePinned, types_exports }; |
@@ -45,2 +45,16 @@ import { document_d_exports } from "../types/document.js"; | ||
| }; | ||
| type FloatingTableBandOptions = { | ||
| contentX: number; | ||
| tableWidth: number; | ||
| contentWidth: number; | ||
| distLeft: number; | ||
| distRight: number; | ||
| }; | ||
| declare const floatingTableReservesBand: ({ | ||
| contentX, | ||
| tableWidth, | ||
| contentWidth, | ||
| distLeft, | ||
| distRight | ||
| }: FloatingTableBandOptions) => boolean; | ||
| /** | ||
@@ -280,2 +294,2 @@ * CSS class names for page elements | ||
| //#endregion | ||
| export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft }; | ||
| export { AnchoredImagePosition, FootnoteRenderItem, type HeaderFooterContent, HeaderFooterLayoutInfo, PAGE_CLASS_NAMES, PAINTER_PAINTED_EVENT, PageFloatingImage, PageGeometry, PainterPaintedDetail, type RenderContext, RenderPageOptions, applySectionHeaderFooterOptions, calculateFootnoteAreaRenderHeight, computePageFingerprint, emuToPixels, findPageShellForPmPos, floatingTableReservesBand, getDefaultPageFontFamily, isFloatingImageRun, renderAllPagesNow, renderFloatingImagesLayer, renderFootnoteArea, renderPage, renderPages, resolveAnchoredImagePosition, resolveHeaderFooterFloatLeft }; |
@@ -333,4 +333,4 @@ import { parseXmlDocument } from "../docx/xmlParser.js"; | ||
| } | ||
| function canClampTabToRightEdge(alignment, hasPriorRenderedContent) { | ||
| if (alignment === "start" || alignment === "default") return hasPriorRenderedContent; | ||
| function canClampTabToRightEdge(alignment, hasPriorRenderedContent, hasPriorTab, isLastLine) { | ||
| if (alignment === "start" || alignment === "default") return hasPriorRenderedContent && (hasPriorTab || isLastLine); | ||
| return true; | ||
@@ -909,5 +909,9 @@ } | ||
| } | ||
| const hasTabRuns = runsForLine.some(isTabRun); | ||
| if (alignment === "justify" && options) { | ||
| if (!options.isLastLine || options.paragraphEndsWithLineBreak) { | ||
| const overfullPx = line.width - options.availableWidth; | ||
| const firstLineIndentPx = options.isFirstLine ? options.firstLineIndentPx ?? 0 : 0; | ||
| const firstLineHangingPx = Math.max(0, -firstLineIndentPx); | ||
| const justifyCapacityPx = options.availableWidth + firstLineHangingPx; | ||
| const overfullPx = line.width - justifyCapacityPx; | ||
| const shrinkableSpaces = countShrinkableSpaces(runsForLine); | ||
@@ -918,2 +922,6 @@ if (overfullPx > RIGHT_EDGE_EPSILON_PX && shrinkableSpaces > 0) { | ||
| lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`; | ||
| } else if (hasTabRuns && shrinkableSpaces > 0 && -overfullPx > RIGHT_EDGE_EPSILON_PX) { | ||
| lineEl.style.textAlign = "left"; | ||
| lineEl.style.textAlignLast = "auto"; | ||
| lineEl.style.wordSpacing = `${-overfullPx / shrinkableSpaces}px`; | ||
| } else { | ||
@@ -923,3 +931,4 @@ lineEl.style.textAlign = "justify"; | ||
| } | ||
| lineEl.style.width = `${options.availableWidth}px`; | ||
| const listFirstLineOffset = options.isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden ? options.firstLineIndentPx ?? 0 : 0; | ||
| lineEl.style.width = `${options.availableWidth - listFirstLineOffset}px`; | ||
| } | ||
@@ -929,3 +938,2 @@ } | ||
| lineEl.style.overflow = "visible"; | ||
| const hasTabRuns = runsForLine.some(isTabRun); | ||
| let tabContext; | ||
@@ -938,2 +946,3 @@ const measureText = createTextMeasurer(doc); | ||
| ...explicitStops !== void 0 ? { explicitStops } : {}, | ||
| ...block.attrs?.defaultTabStopTwips !== void 0 ? { defaultTabInterval: block.attrs.defaultTabStopTwips } : {}, | ||
| leftIndent: leftIndentTwips | ||
@@ -1001,3 +1010,3 @@ }; | ||
| let tabWidth = tabResult.width; | ||
| if (lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck); | ||
| if (lineRightEdgeX !== void 0 && canClampTabToRightEdge(tabResult.alignment, i > 0, runsForLine.slice(0, i).some(isTabRun), options?.isLastLine === true) && currentX + tabWidth + followingWidthForCheck > lineRightEdgeX) tabWidth = Math.max(1, lineRightEdgeX - currentX - followingWidthForCheck); | ||
| const tabEl = renderTabRun(run, doc, tabWidth, tabResult.leader); | ||
@@ -1250,3 +1259,3 @@ lineEl.append(tabEl); | ||
| } else if (indentLeft > 0) lineEl.style.paddingLeft = `${indentLeft}px`; | ||
| else if (hasHanging) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`; | ||
| else if (hasHanging && indentLeft === 0) lineEl.style.paddingLeft = `${indent.hanging ?? 0}px`; | ||
| if (indentRight > 0) lineEl.style.paddingRight = `${indentRight}px`; | ||
@@ -1270,3 +1279,4 @@ if (isFirstLine && block.attrs?.listMarker && !block.attrs.listMarkerHidden) { | ||
| const marker = renderListMarker(block.attrs.listMarker, getListMarkerInlineWidth(block), doc, markerFontFamily, markerFontSize, block.attrs.listMarkerRevision, block.attrs.listMarkerSecondSlotOffsetTwips); | ||
| if (markerStart < 0 && indentLeft > 0) marker.style.marginLeft = `${markerStart}px`; | ||
| const markerMarginLeft = markerStart - Math.min(indentLeft, 0); | ||
| if (markerMarginLeft < 0 && indentLeft !== 0) marker.style.marginLeft = `${markerMarginLeft}px`; | ||
| lineEl.prepend(marker); | ||
@@ -1273,0 +1283,0 @@ } |
@@ -0,1 +1,2 @@ | ||
| import { tableColumnsArePinned } from "../layout-engine/types.js"; | ||
| import { measureParagraph } from "../layout-engine/measure/measureParagraph.js"; | ||
@@ -140,2 +141,3 @@ import { getAutomaticTextColorForBackground } from "./documentColors.js"; | ||
| const spanningCells = /* @__PURE__ */ new Map(); | ||
| const columnsPinned = tableColumnsArePinned(block); | ||
| let y = 0; | ||
@@ -150,3 +152,3 @@ for (let rowIndex = 0; rowIndex < block.rows.length; rowIndex++) { | ||
| } | ||
| const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, void 0, block.bidi); | ||
| const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, void 0, block.bidi, columnsPinned); | ||
| tableEl.append(rowEl); | ||
@@ -174,3 +176,3 @@ y += rowMeasure.height; | ||
| */ | ||
| function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, context, doc) { | ||
| function renderTableCell(cell, cellMeasure, x, rowHeight, borderFlags, columnsPinned, context, doc) { | ||
| const cellEl = doc.createElement("div"); | ||
@@ -201,3 +203,3 @@ cellEl.className = TABLE_CLASS_NAMES.cell; | ||
| } | ||
| if (cell.noWrap) cellEl.style.whiteSpace = "nowrap"; | ||
| if (cell.noWrap && !columnsPinned) cellEl.style.whiteSpace = "nowrap"; | ||
| if (cell.verticalAlign) { | ||
@@ -233,3 +235,3 @@ cellEl.style.display = "flex"; | ||
| */ | ||
| function renderTableRow(row, rowMeasure, rowIndex, y, columnWidths, totalRows, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, bidi = false) { | ||
| function renderTableRow(row, rowMeasure, rowIndex, y, columnWidths, totalRows, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, bidi = false, columnsPinned = false) { | ||
| const rowEl = doc.createElement("div"); | ||
@@ -278,3 +280,3 @@ rowEl.className = TABLE_CLASS_NAMES.row; | ||
| isLastCol: bidi ? atLogicalStart : atLogicalEnd | ||
| }, context, doc); | ||
| }, columnsPinned, context, doc); | ||
| cellEl.dataset["cellIndex"] = String(cellIndex); | ||
@@ -357,2 +359,3 @@ cellEl.dataset["columnIndex"] = String(columnIndex); | ||
| const spanningCells = /* @__PURE__ */ new Map(); | ||
| const columnsPinned = tableColumnsArePinned(block); | ||
| const headerRowCount = fragment.headerRowCount ?? 0; | ||
@@ -368,3 +371,3 @@ let y = 0; | ||
| } | ||
| const rowEl = renderTableRow(hdrRow, hdrRowMeasure, hdrIdx, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, hdrIdx === 0, block.bidi); | ||
| const rowEl = renderTableRow(hdrRow, hdrRowMeasure, hdrIdx, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, hdrIdx === 0, block.bidi, columnsPinned); | ||
| rowEl.dataset["repeatedHeader"] = "true"; | ||
@@ -397,3 +400,3 @@ tableEl.append(rowEl); | ||
| const isFirstRowInFragment = headerRowCount > 0 && fragment.continuesFromPrev ? false : fragment.continuesFromPrev && rowIndex === fragment.fromRow; | ||
| const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, block.bidi); | ||
| const rowEl = renderTableRow(row, rowMeasure, rowIndex, y, measure.columnWidths, block.rows.length, context, doc, spanningCells, rowYPositions, isFirstRowInFragment, block.bidi, columnsPinned); | ||
| contentParent.append(rowEl); | ||
@@ -400,0 +403,0 @@ y += rowMeasure.height; |
| import { resetAuthorColors } from "../utils/authorColors.js"; | ||
| import { loadFontsWithMapping } from "../utils/fontLoader.js"; | ||
| import { parseDocx } from "../docx/parser.js"; | ||
| import { recordDocumentLoadPhase } from "../layout-engine/layoutInstrumentation.js"; | ||
| import { inspectDocxCompatibility } from "../docx/compatibility.js"; | ||
@@ -51,7 +52,13 @@ //#region src/managers/DocumentLoaderManager.ts | ||
| try { | ||
| const doc = await parseDocx(buffer, { | ||
| detectVariables: false, | ||
| preloadFonts: false, | ||
| password: options.password | ||
| }); | ||
| const parseStartedAt = performance.now(); | ||
| let doc; | ||
| try { | ||
| doc = await parseDocx(buffer, { | ||
| detectVariables: false, | ||
| preloadFonts: false, | ||
| password: options.password | ||
| }); | ||
| } finally { | ||
| recordDocumentLoadPhase("docx-parse", performance.now() - parseStartedAt); | ||
| } | ||
| if (this.loadGeneration !== generation) return; | ||
@@ -58,0 +65,0 @@ this.loadParsedDocument(doc); |
@@ -0,3 +1,3 @@ | ||
| import { addColumn, addRow, createTableContext, deleteColumn, deleteRow, getColumnCount, mergeCells, splitCell } from "../utils/tableOperations.js"; | ||
| import { Subscribable } from "./Subscribable.js"; | ||
| import { addColumn, addRow, createTableContext, deleteColumn, deleteRow, getColumnCount, mergeCells, splitCell } from "../utils/tableOperations.js"; | ||
| //#region src/managers/TableSelectionManager.ts | ||
@@ -4,0 +4,0 @@ /** Data attributes for table elements in the rendered DOM */ |
+1
-1
@@ -0,4 +1,4 @@ | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| import { BORDER_STYLE_VALUES, CONDITIONAL_STYLE_TYPE_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FLOATING_TABLE_X_SPEC_VALUES, FLOATING_TABLE_Y_SPEC_VALUES, FONT_THEME_VALUES, FRAME_WRAP_VALUES, FRAME_X_ALIGN_VALUES, FRAME_Y_ALIGN_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LEVEL_SUFFIX_VALUES, LINE_SPACING_RULE_VALUES, NUMBER_FORMAT_VALUES, OUTLINE_STYLE_ATTR_VALUES, OUTLINE_STYLE_CSS_ALIASES, OUTLINE_STYLE_CSS_ALIAS_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, SHAPE_OUTLINE_STYLE_VALUES, SHAPE_TYPE_VALUES, STYLE_TYPE_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "./types/documentEnumValues.js"; | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| import { types_exports } from "./layout-engine/types.js"; | ||
| export { BORDER_STYLE_VALUES, CONDITIONAL_STYLE_TYPE_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FLOATING_TABLE_X_SPEC_VALUES, FLOATING_TABLE_Y_SPEC_VALUES, FONT_THEME_VALUES, FRAME_WRAP_VALUES, FRAME_X_ALIGN_VALUES, FRAME_Y_ALIGN_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LEVEL_SUFFIX_VALUES, LINE_SPACING_RULE_VALUES, types_exports as Layout, NUMBER_FORMAT_VALUES, OUTLINE_STYLE_ATTR_VALUES, OUTLINE_STYLE_CSS_ALIASES, OUTLINE_STYLE_CSS_ALIAS_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, SHAPE_OUTLINE_STYLE_VALUES, SHAPE_TYPE_VALUES, STYLE_TYPE_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES, deriveBlockId, getFolioParaIdFromBlockId, getSequentialFolioBlockIdIndex, isFolioBlockId, isSequentialFolioBlockId }; |
@@ -177,2 +177,3 @@ import { FIELD_TYPE_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LINE_SPACING_RULE_VALUES, OUTLINE_STYLE_ATTR_VALUES, PARAGRAPH_ALIGNMENT_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_EFFECT_VALUES, THEME_COLOR_SLOT_VALUES, UNDERLINE_STYLE_VALUES } from "../../types/documentEnumValues.js"; | ||
| optionalStringArray(attrs, "listLevelNumFmts", "paragraph.attrs.listLevelNumFmts", issues); | ||
| optionalNumberArray(attrs, "listLevelStarts", "paragraph.attrs.listLevelStarts", issues); | ||
| optionalNumber(attrs, "listAbstractNumId", "paragraph.attrs.listAbstractNumId", issues); | ||
@@ -190,2 +191,3 @@ optionalBorderMap(attrs, "borders", "paragraph.attrs.borders", issues, [ | ||
| optionalRecord(attrs, "spacingExplicit", "paragraph.attrs.spacingExplicit", issues); | ||
| optionalRecord(attrs, "spacingFromDocDefaults", "paragraph.attrs.spacingFromDocDefaults", issues); | ||
| optionalTextFormatting(attrs, "defaultTextFormatting", "paragraph.attrs.defaultTextFormatting", issues); | ||
@@ -192,0 +194,0 @@ optionalRecord(attrs, "numPr", "paragraph.attrs.numPr", issues); |
@@ -0,1 +1,2 @@ | ||
| import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js"; | ||
| //#region src/prosemirror/commands/comments.ts | ||
@@ -62,19 +63,45 @@ /** | ||
| if (op) pPrMarkOps.push(op); | ||
| const boundaryCovered = rangeCoversParagraphBoundary(from, to, pos, node); | ||
| let nextAttrs = null; | ||
| const propertyChanges = node.attrs["_propertyChanges"]; | ||
| if (Array.isArray(propertyChanges) && propertyChanges.length > 0 && rangeCoversParagraphBoundary(from, to, pos, node)) { | ||
| if (Array.isArray(propertyChanges) && propertyChanges.length > 0 && boundaryCovered) { | ||
| const matches = propertyChanges.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id)); | ||
| if (matches.length > 0) { | ||
| const remaining = propertyChanges.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id))); | ||
| const nextAttrs = { | ||
| nextAttrs = { | ||
| ...node.attrs, | ||
| _propertyChanges: remaining.length > 0 ? remaining : null | ||
| }; | ||
| if (mode === "reject") { | ||
| for (const change of matches.toReversed()) if (change.previousFormatting) for (const [key, val] of Object.entries(change.previousFormatting)) nextAttrs[key] = val; | ||
| if (mode === "reject") for (const change of matches.toReversed()) { | ||
| Object.assign(nextAttrs, paragraphRejectAttrPatch(change.previousFormatting)); | ||
| nextAttrs["_originalFormatting"] = paragraphRejectOriginalFormatting(change.previousFormatting, node.attrs["_originalFormatting"]); | ||
| } | ||
| tr.setNodeMarkup(pos, void 0, nextAttrs); | ||
| } | ||
| } | ||
| const sectionProperties = node.attrs["_sectionProperties"]; | ||
| const sectionChanges = sectionProperties?.propertyChanges; | ||
| if (sectionProperties && Array.isArray(sectionChanges) && sectionChanges.length > 0 && boundaryCovered) { | ||
| const matches = sectionChanges.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id)); | ||
| if (matches.length > 0) { | ||
| const remaining = sectionChanges.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id))); | ||
| let restored = { ...sectionProperties }; | ||
| if (mode === "reject") for (const change of matches.toReversed()) restored = sectionRejectProperties(restored, change.previousProperties); | ||
| delete restored.propertyChanges; | ||
| if (remaining.length > 0) restored.propertyChanges = remaining; | ||
| nextAttrs = nextAttrs ?? { ...node.attrs }; | ||
| nextAttrs["_sectionProperties"] = restored; | ||
| nextAttrs["sectionBreakType"] = sectionBreakTypeFromSectionStart(restored.sectionStart); | ||
| } | ||
| } | ||
| if (nextAttrs) tr.setNodeMarkup(pos, void 0, nextAttrs); | ||
| return true; | ||
| } | ||
| const tableChangeAttrName = TABLE_PROPERTY_CHANGE_ATTR_BY_NODE[node.type.name]; | ||
| if (tableChangeAttrName !== void 0) { | ||
| if (rangeCoversNode(from, to, pos, node)) { | ||
| const nextAttrs = resolveTablePropertyChangeAttrs(node, tableChangeAttrName, mode, revisionSet); | ||
| if (nextAttrs) tr.setNodeMarkup(pos, void 0, nextAttrs); | ||
| } | ||
| return true; | ||
| } | ||
| if (!node.isInline) return true; | ||
@@ -128,2 +155,45 @@ const nodeEnd = pos + node.nodeSize; | ||
| } | ||
| /** Whether [from, to] fully covers the node — the range property-change cards | ||
| * and accept-all / reject-all sweeps supply for table-level records. */ | ||
| function rangeCoversNode(from, to, pos, node) { | ||
| return from <= pos && to >= pos + node.nodeSize; | ||
| } | ||
| const SECTION_BREAK_TYPE_VALUES = [ | ||
| "nextPage", | ||
| "continuous", | ||
| "oddPage", | ||
| "evenPage" | ||
| ]; | ||
| function sectionBreakTypeFromSectionStart(sectionStart) { | ||
| return SECTION_BREAK_TYPE_VALUES.find((value) => value === sectionStart) ?? null; | ||
| } | ||
| /** PM node type name → the attr its tracked property-change records live on. */ | ||
| const TABLE_PROPERTY_CHANGE_ATTR_BY_NODE = { | ||
| table: "tblPrChange", | ||
| tableRow: "trPrChange", | ||
| tableCell: "tcPrChange", | ||
| tableHeader: "tcPrChange" | ||
| }; | ||
| /** | ||
| * Resolve the tracked property-change records on one table / row / cell node. | ||
| * Accept keeps the live formatting and clears the matched records; reject | ||
| * additionally restores the stored previous formatting wholesale (the change | ||
| * element stores the complete old property set — see propertyChangeScope.ts). | ||
| * Returns the next attrs, or `null` when no record matches. | ||
| */ | ||
| function resolveTablePropertyChangeAttrs(node, attrName, mode, revisionSet) { | ||
| const changes = node.attrs[attrName]; | ||
| if (!Array.isArray(changes) || changes.length === 0) return null; | ||
| const matches = changes.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id)); | ||
| if (matches.length === 0) return null; | ||
| const remaining = changes.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id))); | ||
| const nextAttrs = { | ||
| ...node.attrs, | ||
| [attrName]: remaining.length > 0 ? remaining : null | ||
| }; | ||
| if (mode === "reject") for (const change of matches.toReversed()) if (attrName === "tblPrChange") Object.assign(nextAttrs, tableRejectAttrPatch(change.previousFormatting)); | ||
| else if (attrName === "trPrChange") Object.assign(nextAttrs, tableRowRejectAttrPatch(change.previousFormatting)); | ||
| else Object.assign(nextAttrs, tableCellRejectAttrPatch(change.previousFormatting, node.attrs["_originalFormatting"])); | ||
| return nextAttrs; | ||
| } | ||
| function isPPrMarkAttr(value) { | ||
@@ -130,0 +200,0 @@ if (typeof value !== "object" || value === null) return false; |
@@ -42,3 +42,4 @@ import { expectHyperlinkMarkAttrs } from "../attrs/index.js"; | ||
| if (!linkMark) return true; | ||
| const targetRange = rangeAtCursor(collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark)), $from.pos); | ||
| const ranges = collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark)); | ||
| const targetRange = rangeAtCursor(ranges, $from.pos); | ||
| if (!targetRange) return false; | ||
@@ -73,3 +74,4 @@ const { tooltip } = expectHyperlinkMarkAttrs(linkMark); | ||
| if (!linkMark) return false; | ||
| const targetRange = rangeAtCursor(collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark)), $from.pos); | ||
| const ranges = collectHyperlinkRanges($from.parent, $from.start(), hlType, hyperlinkHref(linkMark)); | ||
| const targetRange = rangeAtCursor(ranges, $from.pos); | ||
| if (!targetRange) return false; | ||
@@ -76,0 +78,0 @@ const tr = view.state.tr.removeMark(targetRange.start, targetRange.end, hlType); |
@@ -127,3 +127,4 @@ import { IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES } from "../../types/documentEnumValues.js"; | ||
| if (!node) return false; | ||
| const transform = computeImageTransform(expectImageAttrs(node).transform ?? "", action); | ||
| const currentTransform = expectImageAttrs(node).transform ?? ""; | ||
| const transform = computeImageTransform(currentTransform, action); | ||
| const tr = view.state.tr.setNodeMarkup(pos, void 0, mergeImageAttrs(node, { transform })); | ||
@@ -130,0 +131,0 @@ view.dispatch(tr.scrollIntoView()); |
@@ -8,7 +8,7 @@ import { acceptAIEditRevision, acceptAllChanges, acceptChange, addCommentMark, findAIEditRevisionRange, findNextChange, findPreviousChange, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark } from "./comments.js"; | ||
| import { getHyperlinkAttrs, getSelectedText, isHyperlinkActive } from "../extensions/marks/HyperlinkExtension.js"; | ||
| import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./table.js"; | ||
| import { clearFontFamily, clearFontSize, clearHighlight, clearTextColor, insertHyperlink, removeHyperlink, setFontFamily, setFontSize, setHighlight, setHyperlink, setTextColor, setUnderlineStyle, toggleBold, toggleItalic, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline } from "./formatting.js"; | ||
| import { addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyStyle, clearStyle, decreaseIndent, decreaseListLevel, doubleSpacing, generateTOC, increaseIndent, increaseListLevel, oneAndHalfSpacing, removeList, removeTabStop, setAlignment, setIndentFirstLine, setIndentLeft, setIndentRight, setLineSpacing, setLtr, setRtl, setSpaceAfter, setSpaceBefore, singleSpacing, toggleBidi, toggleBulletList, toggleNumberedList } from "./paragraph.js"; | ||
| import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./table.js"; | ||
| import { PAINTABLE_MARK_NAMES, applyFormatMarks, captureFormatMarks } from "./formatPainter.js"; | ||
| import { insertPageBreak } from "./pageBreak.js"; | ||
| export { CLIPBOARD_READ_ERROR_EVENT, PAINTABLE_MARK_NAMES, acceptAIEditRevision, acceptAllChanges, acceptChange, addColumnLeft, addColumnRight, addCommentMark, addRowAbove, addRowBelow, addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyFormatMarks, applyStyle, applyTableStyle, autoFitContents, buildPlainTextSlice, captureFormatMarks, clearFontFamily, clearFontSize, clearFormatting, clearHighlight, clearStyle, clearTextColor, createRemoveMarkCommand, createSetMarkCommand, decreaseIndent, decreaseListLevel, deleteColumn, deleteRow, deleteTable, distributeColumns, doubleSpacing, findAIEditRevisionRange, findNextChange, findPreviousChange, generateTOC, getHyperlinkAttrs, getListInfo, getMarkAttr, getParagraphAlignment, getParagraphBidi, getSelectedText, getStyleId, getTableContext, increaseIndent, increaseListLevel, insertHyperlink, insertPageBreak, insertTable, isHyperlinkActive, isInList, isInTableCell as isInTable, isMarkActive, mergeCells, oneAndHalfSpacing, pasteWithoutFormatting, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark, removeHyperlink, removeList, removeTabStop, removeTableBorders, selectColumn, selectRow, selectTable, setAlignment, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setFontFamily, setFontSize, setHighlight, setHyperlink, setIndentFirstLine, setIndentLeft, setIndentRight, setInsideTableBorders, setLineSpacing, setLtr, setOutsideTableBorders, setRowHeight, setRtl, setSpaceAfter, setSpaceBefore, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, setTextColor, setUnderlineStyle, singleSpacing, splitCell, toggleBidi, toggleBold, toggleBulletList, toggleHeaderRow, toggleItalic, toggleNoWrap, toggleNumberedList, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline }; |
@@ -496,9 +496,8 @@ import { OUTLINE_STYLE_CSS_ALIASES } from "../../types/documentEnumValues.js"; | ||
| const linkKey = getLinkKey(linkMark); | ||
| if (currentHyperlink && currentHyperlinkKey === linkKey) addNodeToHyperlink(currentHyperlink, node); | ||
| else { | ||
| if (currentHyperlink && currentHyperlinkKey === linkKey) {} else { | ||
| flushCurrentInline(); | ||
| currentHyperlink = createHyperlink(linkMark); | ||
| currentHyperlinkKey = linkKey; | ||
| addNodeToHyperlink(currentHyperlink, node); | ||
| } | ||
| addNodeToHyperlink(currentHyperlink, node); | ||
| return; | ||
@@ -1240,2 +1239,3 @@ } | ||
| if (attrs.columnWidths) minTable.columnWidths = attrs.columnWidths; | ||
| restoreTablePropertyChanges(minTable, attrs); | ||
| return minTable; | ||
@@ -1250,4 +1250,13 @@ } | ||
| if (formatting) table.formatting = formatting; | ||
| restoreTablePropertyChanges(table, attrs); | ||
| return table; | ||
| } | ||
| /** | ||
| * Restore `w:tblPrChange` entries that PM carried opaquely on the table attrs | ||
| * (same rationale as the paragraph `_propertyChanges` attr): they must survive | ||
| * an edit so the saved DOCX keeps the tracked property-change history. | ||
| */ | ||
| function restoreTablePropertyChanges(table, attrs) { | ||
| if (Array.isArray(attrs.tblPrChange) && attrs.tblPrChange.length > 0) table.propertyChanges = [...attrs.tblPrChange]; | ||
| } | ||
| function convertPMTableRows(node, documentCounts) { | ||
@@ -1375,2 +1384,3 @@ const rows = []; | ||
| if (rowFormatting) row.formatting = rowFormatting; | ||
| if (Array.isArray(attrs.trPrChange) && attrs.trPrChange.length > 0) row.propertyChanges = [...attrs.trPrChange]; | ||
| return row; | ||
@@ -1437,2 +1447,3 @@ } | ||
| if (cellFormatting) cell.formatting = cellFormatting; | ||
| if (Array.isArray(attrs.tcPrChange) && attrs.tcPrChange.length > 0) cell.propertyChanges = [...attrs.tcPrChange]; | ||
| return cell; | ||
@@ -1439,0 +1450,0 @@ } |
@@ -188,2 +188,3 @@ import { paragraphToStyle } from "../../../utils/formatToStyle.js"; | ||
| spacingExplicit: { default: null }, | ||
| spacingFromDocDefaults: { default: null }, | ||
| indentLeft: { default: null }, | ||
@@ -207,2 +208,3 @@ indentRight: { default: null }, | ||
| listLevelNumFmts: { default: null }, | ||
| listLevelStarts: { default: null }, | ||
| listAbstractNumId: { default: null }, | ||
@@ -209,0 +211,0 @@ listStartOverride: { default: null }, |
@@ -0,1 +1,2 @@ | ||
| import { PPR_CHANGE_SCOPED_ATTR_KEYS } from "../../commands/propertyChangeScope.js"; | ||
| import { Priority } from "../types.js"; | ||
@@ -38,2 +39,3 @@ import { createExtension } from "../create.js"; | ||
| "listLevelNumFmts", | ||
| "listLevelStarts", | ||
| "listAbstractNumId", | ||
@@ -44,2 +46,6 @@ "listStartOverride" | ||
| const previousFormatting = {}; | ||
| for (const key of PPR_CHANGE_SCOPED_ATTR_KEYS) { | ||
| const value = attrs[key]; | ||
| if (value != null) previousFormatting[key] = value; | ||
| } | ||
| for (const key of LIST_FORMATTING_ATTRS) previousFormatting[key] = attrs[key] ?? null; | ||
@@ -46,0 +52,0 @@ return previousFormatting; |
@@ -47,3 +47,4 @@ //#region src/prosemirror/findReplaceSelection.ts | ||
| } | ||
| if (!walkBlocks(cell, rowPos + 1 + cellOffset + 1)) return false; | ||
| const cellPos = rowPos + 1 + cellOffset; | ||
| if (!walkBlocks(cell, cellPos + 1)) return false; | ||
| cellOffset += cell.nodeSize; | ||
@@ -50,0 +51,0 @@ } |
@@ -14,6 +14,6 @@ import { assertValidProseMirrorDocument, formatProseMirrorDocumentIssues, validateProseMirrorDocument } from "./validation.js"; | ||
| import { createEmptyDoc, toProseDoc } from "./conversion/toProseDoc.js"; | ||
| import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./commands/table.js"; | ||
| import { extractSelectionState } from "./selectionState.js"; | ||
| import { clearFontFamily, clearFontSize, clearHighlight, clearTextColor, insertHyperlink, removeHyperlink, setFontFamily, setFontSize, setHighlight, setHyperlink, setTextColor, toggleBold, toggleItalic, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline } from "./commands/formatting.js"; | ||
| import { addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyStyle, clearStyle, decreaseIndent, decreaseListLevel, generateTOC, increaseIndent, increaseListLevel, removeList, removeTabStop, setAlignment, setIndentFirstLine, setIndentLeft, setIndentRight, setLineSpacing, setLtr, setRtl, toggleBidi, toggleBulletList, toggleNumberedList } from "./commands/paragraph.js"; | ||
| import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./commands/table.js"; | ||
| import { PAINTABLE_MARK_NAMES, applyFormatMarks, captureFormatMarks } from "./commands/formatPainter.js"; | ||
@@ -20,0 +20,0 @@ import { insertPageBreak } from "./commands/pageBreak.js"; |
@@ -0,3 +1,3 @@ | ||
| import { generateTOC } from "./commands/paragraph.js"; | ||
| import { insertTable } from "./commands/table.js"; | ||
| import { generateTOC } from "./commands/paragraph.js"; | ||
| import { insertPageBreak } from "./commands/pageBreak.js"; | ||
@@ -4,0 +4,0 @@ import { insertImageFromFile } from "./commands/image.js"; |
@@ -21,3 +21,4 @@ import { document_d_exports } from "../../types/document.js"; | ||
| lineSpacingRule?: document_d_exports.LineSpacingRule; | ||
| spacingExplicit?: SpacingExplicit; | ||
| spacingExplicit?: SpacingExplicit; /** Layout provenance: document defaults survive on empty paragraphs. */ | ||
| spacingFromDocDefaults?: SpacingExplicit; | ||
| indentLeft?: number; | ||
@@ -69,3 +70,4 @@ indentRight?: number; | ||
| listMarkerSecondSlotOffsetTwips?: number; /** Number format for each level used by multi-level marker templates. */ | ||
| listLevelNumFmts?: document_d_exports.NumberFormat[]; /** Abstract numbering ID shared by numbering instances. */ | ||
| listLevelNumFmts?: document_d_exports.NumberFormat[]; /** Initial counter for each level used by multi-level marker templates. */ | ||
| listLevelStarts?: number[]; /** Abstract numbering ID shared by numbering instances. */ | ||
| listAbstractNumId?: number; /** Numbering start override for this numId/level. */ | ||
@@ -351,3 +353,4 @@ listStartOverride?: number; | ||
| borders?: document_d_exports.TableBorders; /** Original table formatting from DOCX for lossless round-trip serialization */ | ||
| _originalFormatting?: document_d_exports.TableFormatting; | ||
| _originalFormatting?: document_d_exports.TableFormatting; /** Tracked table property changes (w:tblPrChange) for round-trip + accept/reject */ | ||
| tblPrChange?: document_d_exports.TablePropertyChange[]; | ||
| }; | ||
@@ -362,3 +365,4 @@ /** | ||
| hidden?: boolean; /** Original row formatting from DOCX for lossless round-trip serialization */ | ||
| _originalFormatting?: document_d_exports.TableRowFormatting; | ||
| _originalFormatting?: document_d_exports.TableRowFormatting; /** Tracked row property changes (w:trPrChange) for round-trip + accept/reject */ | ||
| trPrChange?: document_d_exports.TableRowPropertyChange[]; | ||
| }; | ||
@@ -390,3 +394,4 @@ /** | ||
| }; /** Original cell formatting from DOCX for lossless round-trip serialization */ | ||
| _originalFormatting?: document_d_exports.TableCellFormatting; /** Preserve a DOCX vMerge restart even when PM cannot model it as a rowspan. */ | ||
| _originalFormatting?: document_d_exports.TableCellFormatting; /** Tracked cell property changes (w:tcPrChange) for round-trip + accept/reject */ | ||
| tcPrChange?: document_d_exports.TableCellPropertyChange[]; /** Preserve a DOCX vMerge restart even when PM cannot model it as a rowspan. */ | ||
| _preserveVMergeRestart?: boolean; /** Original DOCX vMerge continuation cells skipped into this PM rowspan. */ | ||
@@ -393,0 +398,0 @@ _docxVMergeContinuationCells?: document_d_exports.TableCell[]; |
@@ -94,2 +94,3 @@ import { computeListRendering } from "../../docx/numberingParser.js"; | ||
| listLevelNumFmts: rendering?.levelNumFmts ?? null, | ||
| listLevelStarts: rendering?.levelStarts ?? null, | ||
| listAbstractNumId: rendering?.abstractNumId ?? null, | ||
@@ -96,0 +97,0 @@ listStartOverride: rendering?.startOverride ?? null |
@@ -8,2 +8,4 @@ //#region src/prosemirror/utils/tabCalculator.ts | ||
| const PIXELS_PER_INCH = 96; | ||
| /** Ignore sub-twip round trips when the cursor already occupies a tab stop. */ | ||
| const TAB_POSITION_EPSILON_TWIPS = .01; | ||
| /** | ||
@@ -48,3 +50,3 @@ * Convert twips to pixels | ||
| } | ||
| let pos = maxExplicit > 0 ? Math.max(maxExplicit, leftIndent) : leftIndent; | ||
| let pos = Math.floor(Math.max(maxExplicit, leftIndent) / defaultTabInterval) * defaultTabInterval; | ||
| const limitPos = leftIndent + 14400; | ||
@@ -89,3 +91,3 @@ while (pos < limitPos) { | ||
| const currentXTwips = pixelsToTwips(currentXPx); | ||
| const nextStop = computeTabStops(context).find((stop) => stop.pos > currentXTwips); | ||
| const nextStop = computeTabStops(context).find((stop) => stop.pos > currentXTwips + TAB_POSITION_EPSILON_TWIPS); | ||
| if (!nextStop) { | ||
@@ -92,0 +94,0 @@ const defaultTabPx = twipsToPixels(defaultTabInterval); |
+7
-3
@@ -1,4 +0,6 @@ | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./ai-edits/types.js"; | ||
| import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditSnapshot, FolioAIInlineFormatting, FolioAITextRangeHandle } from "./ai-edits/types.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js"; | ||
| import { FolioReviewChange, FolioReviewChangeKind } from "./ai-edits/read.js"; | ||
| import { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioReviewChangeFilter, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js"; | ||
| import { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyDocumentOperationsOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocxReviewer, FolioDocxReviewerOptions, FolioReviewChangeFilter, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js"; | ||
| import { createFolioAITextRangeHandle } from "./ai-edits/snapshot.js"; | ||
| import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
@@ -8,2 +10,4 @@ import { CreateEmptyDocumentOptions, createEmptyDocument } from "./utils/createDocument.js"; | ||
| import { CreateCommentReplyInput, replyToComment } from "./docx/replyToComment.js"; | ||
| export { type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, type DeriveBlockIdInput, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditSnapshot, type FolioApplyOperationsOptions, type FolioBlockId, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, applyFolioAIEditsToBuffer, createDocx, createEmptyDocument, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId, replyToComment }; | ||
| import { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, generateRedlineDocx } from "./redline.js"; | ||
| import { FolioBlockDiff, FolioFormatProperty, FolioVersionDiff, FolioVersionDiffSegment, compareDocxVersions } from "./version-comparison.js"; | ||
| export { type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, type DeriveBlockIdInput, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditSnapshot, type FolioAIInlineFormatting, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsOptions, type FolioApplyOperationsOptions, type FolioBlockDiff, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioFormatProperty, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, type FolioVersionDiff, type FolioVersionDiffSegment, type GenerateRedlineDocxOptions, type GenerateRedlineDocxResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioAIEditsToBuffer, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, deriveBlockId, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch, replyToComment }; |
+6
-2
@@ -0,6 +1,10 @@ | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| import { createFolioAITextRangeHandle } from "./ai-edits/snapshot.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js"; | ||
| import { createEmptyDocument } from "./utils/createDocument.js"; | ||
| import { createDocx } from "./docx/rezip.js"; | ||
| import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js"; | ||
| import { replyToComment } from "./docx/replyToComment.js"; | ||
| import { FolioDocxReviewer, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js"; | ||
| export { FolioDocxReviewer, applyFolioAIEditsToBuffer, createDocx, createEmptyDocument, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId, replyToComment }; | ||
| import { compareDocxVersions } from "./version-comparison.js"; | ||
| import { generateRedlineDocx } from "./redline.js"; | ||
| export { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocxReviewer, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioAIEditsToBuffer, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, deriveBlockId, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch, replyToComment }; |
@@ -89,3 +89,3 @@ //#region src/utils/clipboard.ts | ||
| } catch (error) { | ||
| onError?.(error); | ||
| onError?.(error instanceof Error ? error : new Error(String(error))); | ||
| return false; | ||
@@ -102,3 +102,3 @@ } | ||
| } catch (error) { | ||
| onError?.(error); | ||
| onError?.(error instanceof Error ? error : new Error(String(error))); | ||
| return false; | ||
@@ -175,3 +175,3 @@ } | ||
| } catch (error) { | ||
| onError?.(error); | ||
| onError?.(error instanceof Error ? error : new Error(String(error))); | ||
| return null; | ||
@@ -178,0 +178,0 @@ } |
@@ -20,2 +20,4 @@ //#region src/utils/fontLoader.ts | ||
| const FONT_MAPPING = { | ||
| Aptos: "Source Sans 3", | ||
| "Aptos Display": "Source Sans 3", | ||
| Calibri: "Carlito", | ||
@@ -42,2 +44,3 @@ Cambria: "Caladea", | ||
| "Cousine", | ||
| "Source Sans 3", | ||
| "Calibri", | ||
@@ -44,0 +47,0 @@ "Cambria", |
@@ -47,2 +47,7 @@ //#region src/utils/fontResolver.ts | ||
| }; | ||
| const APTOS_MEASURED_LINE_HEIGHT = { | ||
| source: "measured", | ||
| ratio: 1.2207, | ||
| note: "Measured against real Word: 11pt Aptos body, non-grid section, renders at 13.43pt line pitch." | ||
| }; | ||
| /** | ||
@@ -63,2 +68,26 @@ * Mapping of common DOCX fonts to Google Fonts equivalents | ||
| const FONT_MAPPINGS = { | ||
| aptos: { | ||
| googleFont: "Source Sans 3", | ||
| category: "sans-serif", | ||
| fallbackStack: [ | ||
| "Aptos", | ||
| "Source Sans 3", | ||
| "Arial", | ||
| "Helvetica", | ||
| "sans-serif" | ||
| ], | ||
| singleLineRatio: singleLineRatioOf(APTOS_MEASURED_LINE_HEIGHT) | ||
| }, | ||
| "aptos display": { | ||
| googleFont: "Source Sans 3", | ||
| category: "sans-serif", | ||
| fallbackStack: [ | ||
| "Aptos Display", | ||
| "Source Sans 3", | ||
| "Arial", | ||
| "Helvetica", | ||
| "sans-serif" | ||
| ], | ||
| singleLineRatio: singleLineRatioOf(APTOS_MEASURED_LINE_HEIGHT) | ||
| }, | ||
| calibri: { | ||
@@ -218,2 +247,19 @@ googleFont: "Carlito", | ||
| }, | ||
| montserrat: { | ||
| googleFont: "Montserrat", | ||
| category: "sans-serif", | ||
| fallbackStack: [ | ||
| "Montserrat", | ||
| "Arial", | ||
| "Helvetica", | ||
| "sans-serif" | ||
| ], | ||
| singleLineRatio: singleLineRatioOf({ | ||
| source: "hhea", | ||
| hheaAscent: 968, | ||
| hheaDescent: -251, | ||
| hheaLineGap: 0, | ||
| unitsPerEm: 1e3 | ||
| }) | ||
| }, | ||
| "trebuchet ms": { | ||
@@ -765,3 +811,4 @@ googleFont: "Fira Sans", | ||
| } | ||
| const defaultFallback = DEFAULT_FALLBACKS[detectFontCategory(docxFontName)]; | ||
| const category = detectFontCategory(docxFontName); | ||
| const defaultFallback = DEFAULT_FALLBACKS[category]; | ||
| return { | ||
@@ -768,0 +815,0 @@ googleFont: null, |
+3
-3
| { | ||
| "name": "@stll/folio-core", | ||
| "version": "0.4.0", | ||
| "version": "0.5.0", | ||
| "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.", | ||
@@ -104,3 +104,3 @@ "keywords": [ | ||
| "pack:dry-run": "bun pm pack --dry-run", | ||
| "typecheck": "tsgo --noEmit -p tsconfig.build.json", | ||
| "typecheck": "bun ../../scripts/tsc-native.ts --noEmit -p tsconfig.build.json", | ||
| "test": "bun test src", | ||
@@ -130,3 +130,3 @@ "test:property": "bun test $(grep -rlF 'fc.assert' src --include='*.test.ts')", | ||
| "utif2": "^4.1.0", | ||
| "valibot": "1.4.1", | ||
| "valibot": "1.4.2", | ||
| "y-prosemirror": "^1.3.7", | ||
@@ -133,0 +133,0 @@ "yjs": "^13.6.31" |
| import { HeaderFooterContent, PageMargins, SectionBreakBlock } from "../layout-engine/types.js"; | ||
| //#region src/paged-layout/headerFooterMargins.d.ts | ||
| type EffectiveHeaderFooterMarginsInput = { | ||
| margins: PageMargins; | ||
| headerContent?: HeaderFooterContent | undefined; | ||
| footerContent?: HeaderFooterContent | undefined; | ||
| firstPageHeaderContent?: HeaderFooterContent | undefined; | ||
| firstPageFooterContent?: HeaderFooterContent | undefined; | ||
| pageSize?: { | ||
| w: number; | ||
| h: number; | ||
| } | undefined; | ||
| warn?: ((message: string) => void) | undefined; | ||
| }; | ||
| /** | ||
| * A function that extends a `PageMargins` to clear the HF overflow computed | ||
| * from a given set of header/footer content. Returned by | ||
| * `computeHeaderFooterMarginExtender` and applied at every margins site: | ||
| * the body fallback, `finalMargins`, and per-section `sectionBreak.margins`. | ||
| * | ||
| * Eigenpal #400 — pre-PR the extension only applied to the body fallback, | ||
| * so a section break carrying its own `sb.margins` from `<w:sectPr>` | ||
| * silently overrode the extension and the footer rendered on top of body | ||
| * text. | ||
| */ | ||
| type PageMarginsExtender = (margins: PageMargins) => PageMargins; | ||
| declare function computeHeaderFooterMarginExtender(input: Omit<EffectiveHeaderFooterMarginsInput, "margins">): PageMarginsExtender; | ||
| /** | ||
| * Like `computeHeaderFooterMarginExtender` but also accounts for the | ||
| * first-page header/footer content. Apply this only to the margins used | ||
| * for page 1 of a `<w:titlePg/>`-enabled section. | ||
| */ | ||
| declare function computeFirstPageHeaderFooterMarginExtender(input: Omit<EffectiveHeaderFooterMarginsInput, "margins">): PageMarginsExtender; | ||
| declare function computeEffectiveHeaderFooterMargins({ | ||
| margins, | ||
| headerContent, | ||
| footerContent, | ||
| firstPageHeaderContent, | ||
| firstPageFooterContent, | ||
| pageSize, | ||
| warn | ||
| }: EffectiveHeaderFooterMarginsInput): PageMargins; | ||
| /** Rendered header/footer content shared by every extender on a page. */ | ||
| type HeaderFooterExtenderContent = Omit<EffectiveHeaderFooterMarginsInput, "margins" | "pageSize" | "warn">; | ||
| type ExtendSectionBreakMarginsInput = { | ||
| content: HeaderFooterExtenderContent; | ||
| sectionContent?: HeaderFooterExtenderContent[] | undefined; /** Body page size and effective margins — the inheritance seed. */ | ||
| bodyPageSize: { | ||
| w: number; | ||
| h: number; | ||
| }; | ||
| bodyMargins: PageMargins; | ||
| warn?: ((message: string) => void) | undefined; | ||
| }; | ||
| /** | ||
| * Extend every section break's margins to clear the same header/footer | ||
| * overflow the body margins do (eigenpal #400), so an overflowing footer | ||
| * never re-overlaps body text on a later section. | ||
| * | ||
| * The walk mirrors `collectSectionConfigs`: a break that omits its own | ||
| * `pageSize` or `margins` inherits the previous section's. Each break's | ||
| * authored-or-inherited margins are extended against the section's *own* | ||
| * resolved page height, so a taller page never keeps a smaller page's | ||
| * clamped reservation (and vice versa). Materializes `margins` on every | ||
| * non-inheriting break in place. | ||
| */ | ||
| declare function extendSectionBreakMargins(sectionBreaks: SectionBreakBlock[], { | ||
| content, | ||
| sectionContent, | ||
| bodyPageSize, | ||
| bodyMargins, | ||
| warn | ||
| }: ExtendSectionBreakMarginsInput): void; | ||
| //#endregion | ||
| export { HeaderFooterExtenderContent, PageMarginsExtender, computeEffectiveHeaderFooterMargins, computeFirstPageHeaderFooterMarginExtender, computeHeaderFooterMarginExtender, extendSectionBreakMargins }; |
| //#region src/paged-layout/headerFooterMargins.ts | ||
| /** | ||
| * Floor on the body content area, in pixels. Header/footer overflow can | ||
| * never shrink the content band below this, so a degenerate document | ||
| * (a header taller than the page) still renders some body text. | ||
| */ | ||
| const MIN_CONTENT_HEIGHT_PX = 24; | ||
| function headerHeight(content) { | ||
| if (!content) return 0; | ||
| return content.marginPushBottom ?? content.visualBottom ?? content.height; | ||
| } | ||
| function footerHeight(content) { | ||
| if (!content) return 0; | ||
| const bottom = content.marginPushBottom ?? content.visualBottom ?? content.height; | ||
| const top = content.marginPushTop ?? content.visualTop ?? 0; | ||
| return Math.max(bottom - top, content.height); | ||
| } | ||
| /** | ||
| * Build a function that, applied to any `PageMargins`, extends `top`/`bottom` | ||
| * to clear the same header/footer content the body's effective margins do. | ||
| * Returns the identity function when no extension is needed. | ||
| * | ||
| * Header/footer distances are taken from the *given* margins (the source | ||
| * paragraph's section margins), not the body's, so each section's authored | ||
| * `w:header`/`w:footer` distances are honored. | ||
| */ | ||
| /** | ||
| * Build an extender that pushes body margins clear of HF content. | ||
| * | ||
| * `mode === "default"` ignores `firstPageHeaderContent` / | ||
| * `firstPageFooterContent`. The first-page H/F only renders on page 1 of | ||
| * a `<w:titlePg/>`-enabled section, so margins for pages 2+ within the | ||
| * same section must NOT be extended for first-page overflow — extending | ||
| * them would push body content down on every page even though only page | ||
| * 1 actually carries the overflowing header. This produced visible | ||
| * regressions on NVCA-style first-page-header docs, where pages 2+ | ||
| * inherited page 1's title-page header reservation. | ||
| * | ||
| * `mode === "firstPage"` uses the larger of the default and first-page | ||
| * H/F heights — applied only to the first-page margins of a titlePg | ||
| * section. | ||
| */ | ||
| function buildExtender({ headerContent, footerContent, firstPageHeaderContent, firstPageFooterContent, pageSize, warn, mode }) { | ||
| const headerContentHeight = mode === "firstPage" ? Math.max(headerHeight(headerContent), headerHeight(firstPageHeaderContent)) : headerHeight(headerContent); | ||
| const footerContentHeight = mode === "firstPage" ? Math.max(footerHeight(footerContent), footerHeight(firstPageFooterContent)) : footerHeight(footerContent); | ||
| return (margins) => { | ||
| const headerDistance = margins.header ?? 48; | ||
| const footerDistance = margins.footer ?? 48; | ||
| const availableHeaderSpace = margins.top - headerDistance; | ||
| const availableFooterSpace = margins.bottom - footerDistance; | ||
| const maxMargins = pageSize ? Math.max(0, pageSize.h - MIN_CONTENT_HEIGHT_PX) : void 0; | ||
| const fitsContent = headerContentHeight <= availableHeaderSpace && footerContentHeight <= availableFooterSpace; | ||
| const fitsPage = maxMargins === void 0 || margins.top + margins.bottom <= maxMargins; | ||
| if (fitsContent && fitsPage) return margins; | ||
| const out = { ...margins }; | ||
| if (headerContentHeight > availableHeaderSpace) out.top = Math.max(margins.top, headerDistance + headerContentHeight); | ||
| if (footerContentHeight > availableFooterSpace) out.bottom = Math.max(margins.bottom, footerDistance + footerContentHeight); | ||
| if (pageSize && maxMargins !== void 0 && out.top + out.bottom > maxMargins) { | ||
| if (warn) warn(`header/footer content exceeds page height; clamping margins to preserve a content area. pageHeight=${Math.round(pageSize.h)} top=${Math.round(out.top)} bottom=${Math.round(out.bottom)}`); | ||
| out.bottom = Math.max(0, Math.min(out.bottom, maxMargins - out.top)); | ||
| if (out.top + out.bottom > maxMargins) out.top = Math.max(0, maxMargins - out.bottom); | ||
| } | ||
| return out; | ||
| }; | ||
| } | ||
| function computeHeaderFooterMarginExtender(input) { | ||
| return buildExtender({ | ||
| ...input, | ||
| mode: "default" | ||
| }); | ||
| } | ||
| /** | ||
| * Like `computeHeaderFooterMarginExtender` but also accounts for the | ||
| * first-page header/footer content. Apply this only to the margins used | ||
| * for page 1 of a `<w:titlePg/>`-enabled section. | ||
| */ | ||
| function computeFirstPageHeaderFooterMarginExtender(input) { | ||
| return buildExtender({ | ||
| ...input, | ||
| mode: "firstPage" | ||
| }); | ||
| } | ||
| function computeEffectiveHeaderFooterMargins({ margins, headerContent, footerContent, firstPageHeaderContent, firstPageFooterContent, pageSize, warn }) { | ||
| return computeHeaderFooterMarginExtender({ | ||
| headerContent, | ||
| footerContent, | ||
| firstPageHeaderContent, | ||
| firstPageFooterContent, | ||
| pageSize, | ||
| warn | ||
| })(margins); | ||
| } | ||
| /** | ||
| * Extend every section break's margins to clear the same header/footer | ||
| * overflow the body margins do (eigenpal #400), so an overflowing footer | ||
| * never re-overlaps body text on a later section. | ||
| * | ||
| * The walk mirrors `collectSectionConfigs`: a break that omits its own | ||
| * `pageSize` or `margins` inherits the previous section's. Each break's | ||
| * authored-or-inherited margins are extended against the section's *own* | ||
| * resolved page height, so a taller page never keeps a smaller page's | ||
| * clamped reservation (and vice versa). Materializes `margins` on every | ||
| * non-inheriting break in place. | ||
| */ | ||
| function extendSectionBreakMargins(sectionBreaks, { content, sectionContent, bodyPageSize, bodyMargins, warn }) { | ||
| let pageSize = bodyPageSize; | ||
| let margins = bodyMargins; | ||
| for (let index = 0; index < sectionBreaks.length; index++) { | ||
| const sb = sectionBreaks[index]; | ||
| if (!sb) continue; | ||
| if (!sb.pageSize && !sb.margins) continue; | ||
| pageSize = sb.pageSize ?? pageSize; | ||
| margins = computeHeaderFooterMarginExtender({ | ||
| ...sectionContent?.[index + 1] ?? content, | ||
| pageSize, | ||
| warn | ||
| })(sb.margins ?? margins); | ||
| sb.margins = margins; | ||
| } | ||
| } | ||
| //#endregion | ||
| export { computeEffectiveHeaderFooterMargins, computeFirstPageHeaderFooterMarginExtender, computeHeaderFooterMarginExtender, extendSectionBreakMargins }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
4123600
3.42%781
2.36%101963
3.19%+ Added
Updated