@stll/folio-core 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-edits/apply.js +565 -121
- package/dist/ai-edits/minimal-replacement.d.ts +64 -0
- package/dist/ai-edits/minimal-replacement.js +282 -0
- package/dist/controller/layoutPipeline.js +11 -1
- package/dist/display-list/build/paragraphPrimitives.js +63 -7
- package/dist/display-list/build/textBoxPrimitives.js +1 -1
- package/dist/display-list/build/unsupported.d.ts +2 -2
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +0 -1
- package/dist/docx/blockContentParser.js +2 -2
- package/dist/docx/bulletMarkers.d.ts +18 -2
- package/dist/docx/bulletMarkers.js +27 -2
- package/dist/docx/drawingGroupChildren.d.ts +60 -0
- package/dist/docx/drawingGroupChildren.js +176 -0
- package/dist/docx/drawingUtils.js +3 -0
- package/dist/docx/groupDrawingParser.d.ts +7 -1
- package/dist/docx/groupDrawingParser.js +123 -32
- package/dist/docx/packageParts.d.ts +45 -1
- package/dist/docx/packageParts.js +93 -2
- package/dist/docx/paraIdAttribute.d.ts +3 -2
- package/dist/docx/paragraphParser.js +91 -7
- package/dist/docx/paragraphTextBoxEnrichment.js +126 -4
- package/dist/docx/runParser.js +10 -0
- package/dist/docx/selectiveSaveFlags.d.ts +10 -3
- package/dist/docx/selectiveSaveFlags.js +1 -1
- package/dist/docx/selectiveXmlPatch.d.ts +56 -34
- package/dist/docx/selectiveXmlPatch.js +222 -115
- package/dist/docx/serializer/groupTextBoxWriteBack.d.ts +12 -0
- package/dist/docx/serializer/groupTextBoxWriteBack.js +101 -0
- package/dist/docx/serializer/paragraphSerializer.js +5 -4
- package/dist/docx/serializer/runSerializer.d.ts +5 -1
- package/dist/docx/serializer/runSerializer.js +4 -3
- package/dist/docx/shapeAlternateContent.d.ts +25 -0
- package/dist/docx/shapeAlternateContent.js +52 -0
- package/dist/docx/textBoxParser.d.ts +10 -2
- package/dist/docx/textBoxParser.js +9 -3
- package/dist/internal/headlessRevisionResolution.js +50 -13
- package/dist/layout-bridge/convert/toFlowBlocks.js +26 -4
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +33 -4
- package/dist/layout-engine/keep-together.d.ts +5 -2
- package/dist/layout-engine/keep-together.js +5 -1
- package/dist/layout-engine/measure/advanceComposition.js +49 -3
- package/dist/layout-engine/measure/measureBlocks.js +5 -3
- package/dist/layout-engine/measure/measureContainer.js +46 -3
- package/dist/layout-engine/measure/measureHelpers.js +13 -3
- package/dist/layout-engine/measure/measureParagraph.js +44 -81
- package/dist/layout-engine/measure/smallCapsCasing.d.ts +56 -0
- package/dist/layout-engine/measure/smallCapsCasing.js +88 -0
- package/dist/layout-engine/paginator.d.ts +2 -0
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphSpacing.d.ts +4 -2
- package/dist/layout-engine/paragraphSpacing.js +2 -1
- package/dist/layout-engine/tableRowBreak.d.ts +8 -1
- package/dist/layout-engine/tableRowBreak.js +22 -1
- package/dist/layout-engine/types.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +41 -4
- package/dist/layout-painter/renderTextBox.d.ts +12 -2
- package/dist/layout-painter/renderTextBox.js +26 -2
- package/dist/prosemirror/alternateContentAttrs.d.ts +9 -0
- package/dist/prosemirror/alternateContentAttrs.js +35 -0
- package/dist/prosemirror/attrs/index.js +90 -0
- package/dist/prosemirror/commands/comments.js +51 -41
- package/dist/prosemirror/contentControlRevisions.d.ts +43 -0
- package/dist/prosemirror/contentControlRevisions.js +84 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +46 -22
- package/dist/prosemirror/conversion/toProseDoc.js +42 -19
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.d.ts +7 -7
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -7
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +6 -3
- package/dist/prosemirror/extensions/nodes/SdtExtension.js +9 -2
- package/dist/prosemirror/extensions/nodes/ShapeExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +3 -0
- package/dist/prosemirror/listMarker.js +2 -2
- package/dist/prosemirror/paragraphDirection.d.ts +29 -2
- package/dist/prosemirror/paragraphDirection.js +21 -2
- package/dist/prosemirror/plugins/suggestionMode.js +37 -8
- package/dist/prosemirror/rejoinRunCarriers.d.ts +27 -0
- package/dist/prosemirror/rejoinRunCarriers.js +66 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +10 -0
- package/dist/prosemirror/runIdentityAcrossRevisions.js +90 -0
- package/dist/prosemirror/schema/nodes.d.ts +37 -5
- package/dist/types/content.d.ts +2 -2
- package/package.json +2 -2
- package/dist/layout-engine/justifiedLineFit.d.ts +0 -7
- package/dist/layout-engine/justifiedLineFit.js +0 -6
|
@@ -5,6 +5,7 @@ import { applyComplexScriptFormatting, hasComplexScriptFormatting, resolveComple
|
|
|
5
5
|
import { getHyperlinkInstanceIndex } from "../layout-engine/measure/hyperlinkInstance.js";
|
|
6
6
|
import { getListMarkerInlineWidth, getListMarkerVisualOffset, resolveListMarkerFont } from "../layout-engine/measure/listMarkerWidth.js";
|
|
7
7
|
import { DEFAULT_FONT_FAMILY, docxScriptFontSize } from "../layout-engine/measure/measureHelpers.js";
|
|
8
|
+
import { SMALL_CAPS_SCALE, smallCapsSegments } from "../layout-engine/measure/smallCapsCasing.js";
|
|
8
9
|
import { calculateTabWidth } from "../layout-engine/measure/tabCalculator.js";
|
|
9
10
|
import { FONT_KERNING_MODE, countCompressibleSpaces, getFontKerningMode, getRunFontKerningMode, toPaintedText } from "../layout-engine/measure/textMeasurementPolicy.js";
|
|
10
11
|
import { isFloatingImageRun } from "../layout-engine/types.js";
|
|
@@ -201,7 +202,6 @@ function applyRunStyles(element, run) {
|
|
|
201
202
|
}
|
|
202
203
|
if (run.letterSpacing) element.style.letterSpacing = `${run.letterSpacing}px`;
|
|
203
204
|
if (run.allCaps) element.style.textTransform = "uppercase";
|
|
204
|
-
if (run.smallCaps) element.style.fontVariant = "small-caps";
|
|
205
205
|
if (run.positionPx) element.style.verticalAlign = `${run.positionPx}px`;
|
|
206
206
|
applyHorizontalScaleTransform(element, run.horizontalScale);
|
|
207
207
|
element.style.fontKerning = getRunFontKerningMode(run, 11);
|
|
@@ -338,7 +338,8 @@ function applyPmPositions(element, pmStart, pmEnd) {
|
|
|
338
338
|
}
|
|
339
339
|
const NON_COMPRESSIBLE_WHITESPACE_PATTERN = /([^\S ]+)/gu;
|
|
340
340
|
const HAS_NON_COMPRESSIBLE_WHITESPACE_PATTERN = /[^\S ]/u;
|
|
341
|
-
|
|
341
|
+
/** Fill `host` with `text`, splitting it into word-spacing segments as needed. */
|
|
342
|
+
function fillPaintedText({ host, text, doc, contractedWordSpacing }) {
|
|
342
343
|
if (!contractedWordSpacing) {
|
|
343
344
|
host.textContent = text;
|
|
344
345
|
return;
|
|
@@ -357,6 +358,39 @@ function appendPaintedText({ host, text, doc, contractedWordSpacing }) {
|
|
|
357
358
|
host.append(segmentEl);
|
|
358
359
|
}
|
|
359
360
|
}
|
|
361
|
+
function appendPaintedText({ host, text, doc, contractedWordSpacing, smallCapsFontSizePx }) {
|
|
362
|
+
const wordSpacing = contractedWordSpacing === void 0 ? {} : { contractedWordSpacing };
|
|
363
|
+
if (smallCapsFontSizePx === void 0) {
|
|
364
|
+
fillPaintedText({
|
|
365
|
+
host,
|
|
366
|
+
text,
|
|
367
|
+
doc,
|
|
368
|
+
...wordSpacing
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const segments = smallCapsSegments(text);
|
|
373
|
+
if (!segments.some((segment) => segment.small)) {
|
|
374
|
+
fillPaintedText({
|
|
375
|
+
host,
|
|
376
|
+
text,
|
|
377
|
+
doc,
|
|
378
|
+
...wordSpacing
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
for (const segment of segments) {
|
|
383
|
+
const segmentEl = doc.createElement("span");
|
|
384
|
+
if (segment.small) segmentEl.style.fontSize = `${smallCapsFontSizePx}px`;
|
|
385
|
+
fillPaintedText({
|
|
386
|
+
host: segmentEl,
|
|
387
|
+
text: segment.text,
|
|
388
|
+
doc,
|
|
389
|
+
...wordSpacing
|
|
390
|
+
});
|
|
391
|
+
host.append(segmentEl);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
360
394
|
function renderTextRun(run, doc, options) {
|
|
361
395
|
const span = doc.createElement("span");
|
|
362
396
|
span.className = `${PARAGRAPH_CLASS_NAMES.run} ${PARAGRAPH_CLASS_NAMES.text}`;
|
|
@@ -374,6 +408,7 @@ function renderTextRun(run, doc, options) {
|
|
|
374
408
|
applyRunStyles(span, run);
|
|
375
409
|
applyPmPositions(span, run.pmStart, run.pmEnd);
|
|
376
410
|
const paintedText = toPaintedText(run.text);
|
|
411
|
+
const smallCapsFontSizePx = run.smallCaps && !run.allCaps ? fontSizePtToPx(run.fontSize || 11) * SMALL_CAPS_SCALE : void 0;
|
|
377
412
|
const visualSpaceContractionPx = options?.visualSpaceContractionPx;
|
|
378
413
|
const effectiveHorizontalScaleFactor = getHorizontalScaleFactor(run.horizontalScale) * (options?.ancestorHorizontalScaleFactor ?? 1);
|
|
379
414
|
const contractedWordSpacing = visualSpaceContractionPx === void 0 || effectiveHorizontalScaleFactor === 0 ? void 0 : `${-visualSpaceContractionPx / effectiveHorizontalScaleFactor}px`;
|
|
@@ -393,7 +428,8 @@ function renderTextRun(run, doc, options) {
|
|
|
393
428
|
host: anchor,
|
|
394
429
|
text: paintedText,
|
|
395
430
|
doc,
|
|
396
|
-
...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {}
|
|
431
|
+
...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {},
|
|
432
|
+
...smallCapsFontSizePx !== void 0 ? { smallCapsFontSizePx } : {}
|
|
397
433
|
});
|
|
398
434
|
if (!run.hyperlink.noDefaultStyle) {
|
|
399
435
|
const hyperlinkColor = getHyperlinkTextColor(run, span.style.color);
|
|
@@ -406,7 +442,8 @@ function renderTextRun(run, doc, options) {
|
|
|
406
442
|
host: span,
|
|
407
443
|
text: paintedText,
|
|
408
444
|
doc,
|
|
409
|
-
...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {}
|
|
445
|
+
...contractedWordSpacing !== void 0 ? { contractedWordSpacing } : {},
|
|
446
|
+
...smallCapsFontSizePx !== void 0 ? { smallCapsFontSizePx } : {}
|
|
410
447
|
});
|
|
411
448
|
applyWhitespaceUnderline(span, run);
|
|
412
449
|
return span;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TableBlock, TableMeasure, TextBoxBlock, TextBoxFragment, TextBoxMeasure } from "../layout-engine/types.js";
|
|
1
|
+
import { TableBlock, TableMeasure, TextBoxBlock, TextBoxFragment, TextBoxGradientFill, TextBoxMeasure } from "../layout-engine/types.js";
|
|
2
2
|
import { RenderContext } from "./renderUtils.js";
|
|
3
3
|
//#region src/layout-painter/renderTextBox.d.ts
|
|
4
4
|
/**
|
|
@@ -14,9 +14,19 @@ type RenderTextBoxFragmentOptions = {
|
|
|
14
14
|
document?: Document;
|
|
15
15
|
renderTable?: (block: TableBlock, measure: TableMeasure, context: RenderContext, document: Document, frameWidth?: number) => HTMLElement;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* The CSS angle of a DrawingML linear gradient over a `width` x `height` box.
|
|
19
|
+
*
|
|
20
|
+
* DrawingML measures `a:lin@ang` clockwise from the positive x axis; CSS
|
|
21
|
+
* measures clockwise from "to top", a quarter turn earlier. A `scaled` angle is
|
|
22
|
+
* stated in the unit square: stretching that square to the box keeps each
|
|
23
|
+
* isoline through the same corners, so the gradient runs perpendicular to the
|
|
24
|
+
* stretched isoline rather than along the stated angle.
|
|
25
|
+
*/
|
|
26
|
+
declare function cssLinearGradientAngle(fill: Pick<TextBoxGradientFill, "angle" | "scaled">, width: number, height: number): number;
|
|
17
27
|
/**
|
|
18
28
|
* Render a text box fragment to DOM
|
|
19
29
|
*/
|
|
20
30
|
declare function renderTextBoxFragment(fragment: TextBoxFragment, block: TextBoxBlock, measure: TextBoxMeasure, context: RenderContext, options?: RenderTextBoxFragmentOptions): HTMLElement;
|
|
21
31
|
//#endregion
|
|
22
|
-
export { RenderTextBoxFragmentOptions, TEXTBOX_CLASS_NAMES, renderTextBoxFragment };
|
|
32
|
+
export { RenderTextBoxFragmentOptions, TEXTBOX_CLASS_NAMES, cssLinearGradientAngle, renderTextBoxFragment };
|
|
@@ -10,7 +10,7 @@ import { panic } from "better-result";
|
|
|
10
10
|
* Text Box Renderer
|
|
11
11
|
*
|
|
12
12
|
* Renders text box fragments to DOM. Handles:
|
|
13
|
-
* - Background fill color
|
|
13
|
+
* - Background fill color or linear gradient
|
|
14
14
|
* - Border/outline
|
|
15
15
|
* - Internal padding (margins)
|
|
16
16
|
* - Block content inside the box (using pre-measured data)
|
|
@@ -20,6 +20,29 @@ import { panic } from "better-result";
|
|
|
20
20
|
*/
|
|
21
21
|
const TEXTBOX_CLASS_NAMES = { textBox: "layout-textbox" };
|
|
22
22
|
/**
|
|
23
|
+
* The CSS angle of a DrawingML linear gradient over a `width` x `height` box.
|
|
24
|
+
*
|
|
25
|
+
* DrawingML measures `a:lin@ang` clockwise from the positive x axis; CSS
|
|
26
|
+
* measures clockwise from "to top", a quarter turn earlier. A `scaled` angle is
|
|
27
|
+
* stated in the unit square: stretching that square to the box keeps each
|
|
28
|
+
* isoline through the same corners, so the gradient runs perpendicular to the
|
|
29
|
+
* stretched isoline rather than along the stated angle.
|
|
30
|
+
*/
|
|
31
|
+
function cssLinearGradientAngle(fill, width, height) {
|
|
32
|
+
const radians = fill.angle * Math.PI / 180;
|
|
33
|
+
return ((90 + (fill.scaled && width > 0 && height > 0 ? Math.atan2(width * Math.sin(radians), height * Math.cos(radians)) : radians) * 180 / Math.PI) % 360 + 360) % 360;
|
|
34
|
+
}
|
|
35
|
+
const formatCssNumber = (value) => String(Math.round(value * 1e3) / 1e3);
|
|
36
|
+
function paintGradientFill(style, fill, width, height) {
|
|
37
|
+
const [first] = fill.stops;
|
|
38
|
+
if (first === void 0) return;
|
|
39
|
+
if (fill.stops.length === 1) {
|
|
40
|
+
setAuthoredBackgroundColor(style, first.color);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
style.backgroundImage = `linear-gradient(${formatCssNumber(cssLinearGradientAngle(fill, width, height))}deg, ${fill.stops.map((stop) => `${stop.color} ${formatCssNumber(stop.offset * 100)}%`).join(", ")})`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
23
46
|
* Render a text box fragment to DOM
|
|
24
47
|
*/
|
|
25
48
|
function renderTextBoxFragment(fragment, block, measure, context, options = {}) {
|
|
@@ -32,6 +55,7 @@ function renderTextBoxFragment(fragment, block, measure, context, options = {})
|
|
|
32
55
|
containerEl.style.overflow = block.textWrap === "none" ? "visible" : "hidden";
|
|
33
56
|
containerEl.style.boxSizing = "border-box";
|
|
34
57
|
if (block.fillColor) setAuthoredBackgroundColor(containerEl.style, block.fillColor);
|
|
58
|
+
else if (block.fillGradient) paintGradientFill(containerEl.style, block.fillGradient, fragment.width, fragment.height);
|
|
35
59
|
const outlineDash = presetDashForOutlineAttr(block.outlineStyle) ?? "solid";
|
|
36
60
|
if (block.outlineWidth && block.outlineWidth > 0 && outlineDash !== "none") {
|
|
37
61
|
const color = block.outlineColor || "#000000";
|
|
@@ -93,4 +117,4 @@ function renderTextBoxFragment(fragment, block, measure, context, options = {})
|
|
|
93
117
|
return containerEl;
|
|
94
118
|
}
|
|
95
119
|
//#endregion
|
|
96
|
-
export { TEXTBOX_CLASS_NAMES, renderTextBoxFragment };
|
|
120
|
+
export { TEXTBOX_CLASS_NAMES, cssLinearGradientAngle, renderTextBoxFragment };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AlternateContentAttrs } from "./schema/nodes.js";
|
|
2
|
+
import { Node } from "prosemirror-model";
|
|
3
|
+
//#region src/prosemirror/alternateContentAttrs.d.ts
|
|
4
|
+
/** Attach a captured `mc:AlternateContent` to a freshly projected node. */
|
|
5
|
+
declare const withAlternateContent: (node: Node, xml: string | undefined) => Node;
|
|
6
|
+
/** The captured element, while the node is still the one it was attached to. */
|
|
7
|
+
declare const unchangedAlternateContentXml: (node: Node, captured: AlternateContentAttrs | undefined) => string | undefined;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { unchangedAlternateContentXml, withAlternateContent };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
2
|
+
//#region src/prosemirror/alternateContentAttrs.ts
|
|
3
|
+
const ALTERNATE_CONTENT_ATTR = "_docxAlternateContent";
|
|
4
|
+
/**
|
|
5
|
+
* What the node says about the shape: its attributes and, for a text box, its
|
|
6
|
+
* content. Marks are left out; they carry the run's properties, which sit
|
|
7
|
+
* outside `mc:AlternateContent`.
|
|
8
|
+
*
|
|
9
|
+
* The projection from the model is lossy, so the node is compared with itself
|
|
10
|
+
* as it was projected rather than the rebuilt shape with the parsed one: a
|
|
11
|
+
* round trip that changed nothing would otherwise read as an edit.
|
|
12
|
+
*/
|
|
13
|
+
const nodeFingerprint = (node) => {
|
|
14
|
+
const { [ALTERNATE_CONTENT_ATTR]: _captured, ...attrs } = node.attrs;
|
|
15
|
+
return canonicalJson({
|
|
16
|
+
attrs,
|
|
17
|
+
content: node.content.toJSON()
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
/** Attach a captured `mc:AlternateContent` to a freshly projected node. */
|
|
21
|
+
const withAlternateContent = (node, xml) => {
|
|
22
|
+
if (xml === void 0) return node;
|
|
23
|
+
const alternateContent = {
|
|
24
|
+
xml,
|
|
25
|
+
fingerprint: nodeFingerprint(node)
|
|
26
|
+
};
|
|
27
|
+
return node.type.create({
|
|
28
|
+
...node.attrs,
|
|
29
|
+
[ALTERNATE_CONTENT_ATTR]: alternateContent
|
|
30
|
+
}, node.content, node.marks);
|
|
31
|
+
};
|
|
32
|
+
/** The captured element, while the node is still the one it was attached to. */
|
|
33
|
+
const unchangedAlternateContentXml = (node, captured) => captured !== void 0 && captured.fingerprint === nodeFingerprint(node) ? captured.xml : void 0;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { unchangedAlternateContentXml, withAlternateContent };
|
|
@@ -540,6 +540,7 @@ const readFieldAttrs = (node) => {
|
|
|
540
540
|
requiredOneOf(attrs, "fieldKind", "field.attrs.fieldKind", issues, FIELD_KINDS);
|
|
541
541
|
optionalBoolean(attrs, "fldLock", "field.attrs.fldLock", issues);
|
|
542
542
|
optionalBoolean(attrs, "dirty", "field.attrs.dirty", issues);
|
|
543
|
+
optionalBoolean(attrs, "fieldResultIsFallback", "field.attrs.fieldResultIsFallback", issues);
|
|
543
544
|
return attrsResult(attrs, issues);
|
|
544
545
|
};
|
|
545
546
|
const expectFieldAttrs = (node) => expectCachedNodeAttrs(node, fieldAttrsCache, readFieldAttrs, "field attrs");
|
|
@@ -637,6 +638,7 @@ const readShapeAttrs = (node) => {
|
|
|
637
638
|
optionalNumber(attrs, "shadowOffsetY", "shape.attrs.shadowOffsetY", issues);
|
|
638
639
|
optionalString(attrs, "glowColor", "shape.attrs.glowColor", issues);
|
|
639
640
|
optionalNumber(attrs, "glowRadius", "shape.attrs.glowRadius", issues);
|
|
641
|
+
optionalAlternateContent(attrs, "shape.attrs._docxAlternateContent", issues);
|
|
640
642
|
return attrsResult(attrs, issues);
|
|
641
643
|
};
|
|
642
644
|
const expectShapeAttrs = (node) => expectCachedNodeAttrs(node, shapeAttrsCache, readShapeAttrs, "shape attrs");
|
|
@@ -654,6 +656,7 @@ const readTextBoxAttrs = (node) => {
|
|
|
654
656
|
optionalString(attrs, "alt", "textBox.attrs.alt", issues);
|
|
655
657
|
optionalString(attrs, "title", "textBox.attrs.title", issues);
|
|
656
658
|
optionalString(attrs, "fillColor", "textBox.attrs.fillColor", issues);
|
|
659
|
+
optionalGradientFill(attrs, "gradientFill", "textBox.attrs.gradientFill", issues);
|
|
657
660
|
optionalNumber(attrs, "outlineWidth", "textBox.attrs.outlineWidth", issues);
|
|
658
661
|
optionalAuthoredEmu(attrs, "_docxAuthoredEmu", "textBox.attrs._docxAuthoredEmu", issues, TEXT_BOX_AUTHORED_EMU_ATTRS);
|
|
659
662
|
optionalString(attrs, "outlineColor", "textBox.attrs.outlineColor", issues);
|
|
@@ -681,8 +684,10 @@ const readTextBoxAttrs = (node) => {
|
|
|
681
684
|
optionalEffectExtentSlots(attrs, "wrapEffectExtentSlots", "textBox.attrs.wrapEffectExtentSlots", issues);
|
|
682
685
|
optionalWrapPolygon(attrs, "wrapPolygon", "textBox.attrs.wrapPolygon", issues);
|
|
683
686
|
optionalOneOf(attrs, "_docxPlacement", "textBox.attrs._docxPlacement", issues, TEXT_BOX_DOCX_PLACEMENTS);
|
|
687
|
+
optionalAlternateContent(attrs, "textBox.attrs._docxAlternateContent", issues);
|
|
684
688
|
optionalString(attrs, "_docxGroupId", "textBox.attrs._docxGroupId", issues);
|
|
685
689
|
optionalString(attrs, "_docxAnchorId", "textBox.attrs._docxAnchorId", issues);
|
|
690
|
+
optionalDrawingGroupChild(attrs, "_docxGroupChild", "textBox.attrs._docxGroupChild", issues);
|
|
686
691
|
requiredTextBoxBodyContentState(attrs, issues);
|
|
687
692
|
optionalTextBoxTrackedChange(attrs, issues);
|
|
688
693
|
optionalTextBoxInlineSdts(attrs, issues);
|
|
@@ -1618,6 +1623,20 @@ const optionalTextBoxTrackedChange = (attrs, issues) => {
|
|
|
1618
1623
|
requiredString(info, "author", "textBox.attrs._docxTrackedChange.info.author", issues);
|
|
1619
1624
|
optionalString(info, "date", "textBox.attrs._docxTrackedChange.info.date", issues);
|
|
1620
1625
|
};
|
|
1626
|
+
/** `_docxAlternateContent`: a captured element and its node fingerprint, or absent. */
|
|
1627
|
+
const optionalAlternateContent = (attrs, path, issues) => {
|
|
1628
|
+
const value = attrs["_docxAlternateContent"];
|
|
1629
|
+
if (value === void 0 || value === null) return;
|
|
1630
|
+
if (!isRecord(value)) {
|
|
1631
|
+
issues.push({
|
|
1632
|
+
path,
|
|
1633
|
+
message: "Expected an object."
|
|
1634
|
+
});
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
requiredString(value, "xml", `${path}.xml`, issues);
|
|
1638
|
+
requiredString(value, "fingerprint", `${path}.fingerprint`, issues);
|
|
1639
|
+
};
|
|
1621
1640
|
const requiredTextBoxBodyContentState = (attrs, issues) => {
|
|
1622
1641
|
const value = attrs["_docxTextBodyContentState"];
|
|
1623
1642
|
if (!isRecord(value)) {
|
|
@@ -1667,6 +1686,7 @@ const validateSdtAttrsRecord = (attrs, path, issues) => {
|
|
|
1667
1686
|
optionalPreservedMarkup(attrs, "_preserved", `${path}._preserved`, issues);
|
|
1668
1687
|
optionalString(attrs, "rawEndPropertiesXml", `${path}.rawEndPropertiesXml`, issues);
|
|
1669
1688
|
optionalSdtEndProperties(attrs, `${path}.endProperties`, issues);
|
|
1689
|
+
optionalNumberArray(attrs, "_docxEnclosingRevisionIds", `${path}._docxEnclosingRevisionIds`, issues);
|
|
1670
1690
|
};
|
|
1671
1691
|
/**
|
|
1672
1692
|
* The attribute remainder an authored element's record carries.
|
|
@@ -2555,6 +2575,76 @@ const optionalWrapPolygon = (attrs, key, path, issues) => {
|
|
|
2555
2575
|
readWrapPolygonPoint(point, `${path}.lineTo[${index}]`, issues);
|
|
2556
2576
|
});
|
|
2557
2577
|
};
|
|
2578
|
+
/** A text box's gradient fill: the model's `ShapeFill`, and only its gradient arm. */
|
|
2579
|
+
const optionalGradientFill = (attrs, key, path, issues) => {
|
|
2580
|
+
const value = attrs[key];
|
|
2581
|
+
if (value === void 0 || value === null) return;
|
|
2582
|
+
if (!isRecord(value)) {
|
|
2583
|
+
issues.push({
|
|
2584
|
+
path,
|
|
2585
|
+
message: "Expected an object."
|
|
2586
|
+
});
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
if (value["type"] !== "gradient") issues.push({
|
|
2590
|
+
path: `${path}.type`,
|
|
2591
|
+
message: "Expected \"gradient\"."
|
|
2592
|
+
});
|
|
2593
|
+
optionalString(value, "rawXml", `${path}.rawXml`, issues);
|
|
2594
|
+
const gradient = value["gradient"];
|
|
2595
|
+
if (!isRecord(gradient)) {
|
|
2596
|
+
issues.push({
|
|
2597
|
+
path: `${path}.gradient`,
|
|
2598
|
+
message: "Expected an object."
|
|
2599
|
+
});
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
optionalOneOf(gradient, "type", `${path}.gradient.type`, issues, SHAPE_GRADIENT_TYPES);
|
|
2603
|
+
optionalNumber(gradient, "angle", `${path}.gradient.angle`, issues);
|
|
2604
|
+
optionalBoolean(gradient, "scaled", `${path}.gradient.scaled`, issues);
|
|
2605
|
+
const stops = gradient["stops"];
|
|
2606
|
+
if (!Array.isArray(stops)) {
|
|
2607
|
+
issues.push({
|
|
2608
|
+
path: `${path}.gradient.stops`,
|
|
2609
|
+
message: "Expected an array."
|
|
2610
|
+
});
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
stops.forEach((stop, index) => {
|
|
2614
|
+
const stopPath = `${path}.gradient.stops[${index}]`;
|
|
2615
|
+
if (!isRecord(stop)) {
|
|
2616
|
+
issues.push({
|
|
2617
|
+
path: stopPath,
|
|
2618
|
+
message: "Expected a gradient stop object."
|
|
2619
|
+
});
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
if (typeof stop["position"] !== "number") issues.push({
|
|
2623
|
+
path: `${stopPath}.position`,
|
|
2624
|
+
message: "Expected a number."
|
|
2625
|
+
});
|
|
2626
|
+
optionalColorValue(stop, "color", `${stopPath}.color`, issues);
|
|
2627
|
+
});
|
|
2628
|
+
};
|
|
2629
|
+
/** Where a text box sits inside a DrawingML group: its element path and two fingerprints. */
|
|
2630
|
+
const optionalDrawingGroupChild = (attrs, key, path, issues) => {
|
|
2631
|
+
const value = attrs[key];
|
|
2632
|
+
if (value === void 0 || value === null) return;
|
|
2633
|
+
if (!isRecord(value)) {
|
|
2634
|
+
issues.push({
|
|
2635
|
+
path,
|
|
2636
|
+
message: "Expected an object."
|
|
2637
|
+
});
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
const childPath = value["path"];
|
|
2641
|
+
if (!Array.isArray(childPath) || childPath.length === 0 || !childPath.every((index) => Number.isInteger(index) && index >= 0)) issues.push({
|
|
2642
|
+
path: `${path}.path`,
|
|
2643
|
+
message: "Expected a non-empty array of indices."
|
|
2644
|
+
});
|
|
2645
|
+
requiredString(value, "group", `${path}.group`, issues);
|
|
2646
|
+
requiredString(value, "content", `${path}.content`, issues);
|
|
2647
|
+
};
|
|
2558
2648
|
/** `a:hlinkClick` as captured: its own bytes, plus the target it named. */
|
|
2559
2649
|
const optionalDocPrLink = (attrs, key, path, issues) => {
|
|
2560
2650
|
const value = attrs[key];
|
|
@@ -4,11 +4,13 @@ import { appendHeadlessInlineResolution } from "../../internal/headlessRevisionR
|
|
|
4
4
|
import { stateAllowsHeadlessRevisionResolution } from "../../internal/headlessRevisionResolutionGuard.js";
|
|
5
5
|
import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
|
|
6
6
|
import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../containerFinalParagraph.js";
|
|
7
|
+
import { enclosingRevisionIds, resolutionRemovesControl, withoutResolvedEnclosures } from "../contentControlRevisions.js";
|
|
7
8
|
import { resolveParagraphDefaultTextFormatting } from "../conversion/toProseDoc.js";
|
|
8
9
|
import { markChangedParagraphRanges, markStructuralChange, markTrackedSectionEndpointRemoval } from "../extensions/features/ParagraphChangeTrackerExtension.js";
|
|
9
10
|
import "../extensions/nodes/SdtExtension.js";
|
|
10
11
|
import { getDocumentStyleResolver } from "../plugins/documentStyles.js";
|
|
11
12
|
import { setParagraphAttrsWithRebasedRunFormatting } from "../rebaseParagraphRunFormatting.js";
|
|
13
|
+
import { rejoinRunsAt } from "../rejoinRunCarriers.js";
|
|
12
14
|
import { getFolioNodeRevisionCarriers, nodePropertyRevisionSites, propertyRevisionMetadata, propertyRevisionRecords } from "../revisionCarriers.js";
|
|
13
15
|
import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
|
|
14
16
|
import { reconstructRejectedRunFormattingMarks } from "../runPropertyChangeResolution.js";
|
|
@@ -109,6 +111,8 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
|
|
|
109
111
|
if (dispatch) {
|
|
110
112
|
const tr = state.tr;
|
|
111
113
|
const deleteRanges = [];
|
|
114
|
+
/** Where resolved inline content began and ended, in `state.doc`. */
|
|
115
|
+
const resolvedBoundaries = [];
|
|
112
116
|
const pPrMarkOps = [];
|
|
113
117
|
const tableRowStructuralOps = [];
|
|
114
118
|
const tableCellStructuralOps = [];
|
|
@@ -198,15 +202,27 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
|
|
|
198
202
|
const nodeEnd = pos + node.nodeSize;
|
|
199
203
|
const rangeFrom = Math.max(from, pos);
|
|
200
204
|
const rangeTo = Math.min(to, nodeEnd);
|
|
201
|
-
if (node.type.name === "sdt"
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
if (node.type.name === "sdt") {
|
|
206
|
+
if (!rangeCoversNode(from, to, pos, node)) return true;
|
|
207
|
+
const resolvesRevision = (revisionId) => revisionSet === null || revisionSet.has(revisionId);
|
|
208
|
+
if (resolutionRemovesControl({
|
|
209
|
+
control: node,
|
|
210
|
+
removeType,
|
|
211
|
+
resolves: resolvesRevision
|
|
212
|
+
})) {
|
|
213
|
+
deleteRanges.push({
|
|
214
|
+
from: pos,
|
|
215
|
+
to: nodeEnd
|
|
216
|
+
});
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const released = withoutResolvedEnclosures(node, resolvesRevision);
|
|
220
|
+
if (released) tr.setNodeMarkup(pos, void 0, released);
|
|
221
|
+
return true;
|
|
207
222
|
}
|
|
208
223
|
const runPropertyChangeMark = node.marks.find((mark) => mark.type.name === "runPropertyChange");
|
|
209
224
|
const removesNode = removeType !== void 0 && node.marks.some((mark) => mark.type === removeType && matchesRevision(mark));
|
|
225
|
+
if (removesNode || runPropertyChangeMark !== void 0 || node.marks.some((mark) => (mark.type === insertionType || mark.type === deletionType) && matchesRevision(mark))) resolvedBoundaries.push(rangeFrom, rangeTo);
|
|
210
226
|
if (runPropertyChangeMark) resolveRunPropertyChange({
|
|
211
227
|
tr,
|
|
212
228
|
node,
|
|
@@ -386,6 +402,11 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
|
|
|
386
402
|
removedReferences: removedSectionReferences
|
|
387
403
|
});
|
|
388
404
|
if (!terminalTableDeletionIsPending(tr)) resolveTerminalTableReviewCarrier(tr, mode);
|
|
405
|
+
rejoinRunsAt({
|
|
406
|
+
tr,
|
|
407
|
+
boundaries: resolvedBoundaries.map((position) => tr.mapping.map(position)),
|
|
408
|
+
styleResolver
|
|
409
|
+
});
|
|
389
410
|
if (tr.steps.length > 0) dispatch(tr);
|
|
390
411
|
}
|
|
391
412
|
return true;
|
|
@@ -620,24 +641,6 @@ function rangeCoversNode(from, to, pos, node) {
|
|
|
620
641
|
return from <= pos && to >= pos + node.nodeSize;
|
|
621
642
|
}
|
|
622
643
|
/**
|
|
623
|
-
* Whether a revision covers everything an inline content control holds.
|
|
624
|
-
*
|
|
625
|
-
* The save leg writes such a revision around the control (`w:ins > w:sdt`),
|
|
626
|
-
* so resolving it is an operation over the control itself: a reader who
|
|
627
|
-
* rejects an inserted control gets no control, not an empty one standing
|
|
628
|
-
* where it was. A revision over part of the content is a revision over that
|
|
629
|
-
* part, and a control that holds nothing is covered by nothing.
|
|
630
|
-
*/
|
|
631
|
-
function revisionCoversWholeControl(node, removeType, matchesRevision) {
|
|
632
|
-
if (node.childCount === 0) return false;
|
|
633
|
-
for (let index = 0; index < node.childCount; index += 1) {
|
|
634
|
-
const child = node.child(index);
|
|
635
|
-
if (child.marks.some((mark) => mark.type === removeType && matchesRevision(mark))) continue;
|
|
636
|
-
if (child.type.name !== "sdt" || !revisionCoversWholeControl(child, removeType, matchesRevision)) return false;
|
|
637
|
-
}
|
|
638
|
-
return true;
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
644
|
* The attrs a reject restores for each property revision the shared node-attr
|
|
642
645
|
* pass owns. Total over those kinds, so a revision the model gains either
|
|
643
646
|
* declares a different resolution or states here what rejecting it restores.
|
|
@@ -826,6 +829,7 @@ function findAIEditRevisionRange(state, revisionIds) {
|
|
|
826
829
|
};
|
|
827
830
|
state.doc.descendants((node, pos) => {
|
|
828
831
|
for (const carrier of getFolioNodeRevisionCarriers(node, pos)) if (idSet.has(carrier.id)) includeRange(carrier.from, carrier.to);
|
|
832
|
+
if (node.type.name === "sdt" && enclosingRevisionIds(node).some((id) => idSet.has(id))) includeRange(pos, pos + node.nodeSize);
|
|
829
833
|
if (node.type.name === "tableRow") for (const attrName of ["trIns", "trDel"]) {
|
|
830
834
|
const marker = node.attrs[attrName];
|
|
831
835
|
if (isTableRowRevisionAttr(marker) && idSet.has(marker.revisionId)) {
|
|
@@ -1724,24 +1728,30 @@ function expandTrackedChangeRange(state, mark, fromHint, toHint) {
|
|
|
1724
1728
|
const carriesSameInlineMark = (node) => node?.isInline === true && node.marks.some((m) => m.eq(mark));
|
|
1725
1729
|
let from = fromHint;
|
|
1726
1730
|
let to = toHint;
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1731
|
+
const revisionId = mark.attrs["revisionId"];
|
|
1732
|
+
for (;;) {
|
|
1733
|
+
let $from = state.doc.resolve(from);
|
|
1734
|
+
let nodeBefore = $from.nodeBefore;
|
|
1735
|
+
while (carriesSameInlineMark(nodeBefore)) {
|
|
1736
|
+
from -= nodeBefore.nodeSize;
|
|
1737
|
+
$from = state.doc.resolve(from);
|
|
1738
|
+
nodeBefore = $from.nodeBefore;
|
|
1739
|
+
}
|
|
1740
|
+
let $to = state.doc.resolve(to);
|
|
1741
|
+
let nodeAfter = $to.nodeAfter;
|
|
1742
|
+
while (carriesSameInlineMark(nodeAfter)) {
|
|
1743
|
+
to += nodeAfter.nodeSize;
|
|
1744
|
+
$to = state.doc.resolve(to);
|
|
1745
|
+
nodeAfter = $to.nodeAfter;
|
|
1746
|
+
}
|
|
1747
|
+
const control = $from.parent;
|
|
1748
|
+
if (control.type.name !== "sdt" || typeof revisionId !== "number" || !enclosingRevisionIds(control).includes(revisionId) || from !== $from.start() || to !== $from.end()) return {
|
|
1749
|
+
from,
|
|
1750
|
+
to
|
|
1751
|
+
};
|
|
1752
|
+
from = $from.before();
|
|
1753
|
+
to = $from.after();
|
|
1740
1754
|
}
|
|
1741
|
-
return {
|
|
1742
|
-
from,
|
|
1743
|
-
to
|
|
1744
|
-
};
|
|
1745
1755
|
}
|
|
1746
1756
|
/**
|
|
1747
1757
|
* Find the next tracked change after the given position. Returns the full
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Transaction } from "prosemirror-state";
|
|
2
|
+
import { MarkType, Node } from "prosemirror-model";
|
|
3
|
+
//#region src/prosemirror/contentControlRevisions.d.ts
|
|
4
|
+
declare const ENCLOSING_REVISION_IDS_ATTR = "_docxEnclosingRevisionIds";
|
|
5
|
+
/** The revisions whose element encloses `control`. */
|
|
6
|
+
declare const enclosingRevisionIds: (control: Node) => readonly number[];
|
|
7
|
+
/** `control`'s attrs with `revisionId` added to its enclosing revisions. */
|
|
8
|
+
declare const withEnclosingRevision: (control: Node, revisionId: number) => Node["attrs"] | null;
|
|
9
|
+
type ResolutionRemovesControlOptions = {
|
|
10
|
+
control: Node;
|
|
11
|
+
/** The revision mark type the resolution removes content under. */
|
|
12
|
+
removeType: MarkType | undefined;
|
|
13
|
+
/** Whether the resolution resolves revision `revisionId`. */
|
|
14
|
+
resolves: (revisionId: number) => boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Whether resolving removes the control itself: a revision that encloses it is
|
|
18
|
+
* resolved in the direction that removes its content, and still covers all of
|
|
19
|
+
* it. Removing only the content leaves the control standing, emptied.
|
|
20
|
+
*/
|
|
21
|
+
declare const resolutionRemovesControl: ({ control, removeType, resolves }: ResolutionRemovesControlOptions) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* `control`'s attrs without the enclosing revisions `resolved` names, or
|
|
24
|
+
* `null` when none of them is: a resolved revision no longer exists, and an id
|
|
25
|
+
* left behind would claim whatever revision is later given that id.
|
|
26
|
+
*/
|
|
27
|
+
declare const withoutResolvedEnclosures: (control: Node, resolved: (revisionId: number) => boolean) => Node["attrs"] | null;
|
|
28
|
+
type EncloseWholeControlsOptions = {
|
|
29
|
+
tr: Transaction;
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
revisionId: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Record `revisionId` as enclosing every inline control `[from, to]` covers
|
|
36
|
+
* whole, opening and closing token included: the revision removes or adds the
|
|
37
|
+
* control, as deleting or inserting the same range directly would. A control
|
|
38
|
+
* the range covers only the content of is left alone, and its revision stays
|
|
39
|
+
* one over that content.
|
|
40
|
+
*/
|
|
41
|
+
declare const encloseWholeControls: ({ tr, from, to, revisionId }: EncloseWholeControlsOptions) => void;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ENCLOSING_REVISION_IDS_ATTR, encloseWholeControls, enclosingRevisionIds, resolutionRemovesControl, withEnclosingRevision, withoutResolvedEnclosures };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { expectTrackedChangeMarkAttrs } from "./attrs/index.js";
|
|
2
|
+
import "./extensions/nodes/SdtExtension.js";
|
|
3
|
+
//#region src/prosemirror/contentControlRevisions.ts
|
|
4
|
+
const ENCLOSING_REVISION_IDS_ATTR = "_docxEnclosingRevisionIds";
|
|
5
|
+
const isInlineControl = (node) => node.type.name === "sdt";
|
|
6
|
+
/** The revisions whose element encloses `control`. */
|
|
7
|
+
const enclosingRevisionIds = (control) => {
|
|
8
|
+
const ids = control.attrs[ENCLOSING_REVISION_IDS_ATTR];
|
|
9
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "number") : [];
|
|
10
|
+
};
|
|
11
|
+
/** `control`'s attrs with `revisionId` added to its enclosing revisions. */
|
|
12
|
+
const withEnclosingRevision = (control, revisionId) => {
|
|
13
|
+
const ids = enclosingRevisionIds(control);
|
|
14
|
+
return ids.includes(revisionId) ? null : {
|
|
15
|
+
...control.attrs,
|
|
16
|
+
[ENCLOSING_REVISION_IDS_ATTR]: [...ids, revisionId]
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const REMOVED_LAYERS = {
|
|
20
|
+
deletion: /* @__PURE__ */ new Set(["deletion", "moveFrom"]),
|
|
21
|
+
insertion: /* @__PURE__ */ new Set(["insertion", "moveTo"])
|
|
22
|
+
};
|
|
23
|
+
/** Whether `mark`, or a revision it is nested in, is `revisionId` of `removeType`'s kind. */
|
|
24
|
+
const carriesRevision = (mark, removeType, revisionId) => {
|
|
25
|
+
if (mark.type.name !== "insertion" && mark.type.name !== "deletion") return false;
|
|
26
|
+
const attrs = expectTrackedChangeMarkAttrs(mark);
|
|
27
|
+
if (mark.type === removeType && attrs.revisionId === revisionId) return true;
|
|
28
|
+
const removed = removeType.name === "deletion" ? REMOVED_LAYERS.deletion : REMOVED_LAYERS.insertion;
|
|
29
|
+
return (attrs._docxRevisionAncestors ?? []).some((layer) => layer.revisionId === revisionId && removed.has(layer.type));
|
|
30
|
+
};
|
|
31
|
+
const coveredBy = (node, covers) => {
|
|
32
|
+
if (node.childCount === 0) return false;
|
|
33
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
34
|
+
const child = node.child(index);
|
|
35
|
+
if (child.marks.some(covers)) continue;
|
|
36
|
+
if (!isInlineControl(child) || !coveredBy(child, covers)) return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Whether resolving removes the control itself: a revision that encloses it is
|
|
42
|
+
* resolved in the direction that removes its content, and still covers all of
|
|
43
|
+
* it. Removing only the content leaves the control standing, emptied.
|
|
44
|
+
*/
|
|
45
|
+
const resolutionRemovesControl = ({ control, removeType, resolves }) => {
|
|
46
|
+
if (removeType === void 0 || !isInlineControl(control)) return false;
|
|
47
|
+
return enclosingRevisionIds(control).some((revisionId) => resolves(revisionId) && coveredBy(control, (mark) => carriesRevision(mark, removeType, revisionId)));
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* `control`'s attrs without the enclosing revisions `resolved` names, or
|
|
51
|
+
* `null` when none of them is: a resolved revision no longer exists, and an id
|
|
52
|
+
* left behind would claim whatever revision is later given that id.
|
|
53
|
+
*/
|
|
54
|
+
const withoutResolvedEnclosures = (control, resolved) => {
|
|
55
|
+
const ids = enclosingRevisionIds(control);
|
|
56
|
+
const remaining = ids.filter((revisionId) => !resolved(revisionId));
|
|
57
|
+
if (remaining.length === ids.length) return null;
|
|
58
|
+
return {
|
|
59
|
+
...control.attrs,
|
|
60
|
+
[ENCLOSING_REVISION_IDS_ATTR]: remaining.length > 0 ? remaining : null
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Record `revisionId` as enclosing every inline control `[from, to]` covers
|
|
65
|
+
* whole, opening and closing token included: the revision removes or adds the
|
|
66
|
+
* control, as deleting or inserting the same range directly would. A control
|
|
67
|
+
* the range covers only the content of is left alone, and its revision stays
|
|
68
|
+
* one over that content.
|
|
69
|
+
*/
|
|
70
|
+
const encloseWholeControls = ({ tr, from, to, revisionId }) => {
|
|
71
|
+
const enclosed = [];
|
|
72
|
+
tr.doc.nodesBetween(from, to, (node, pos) => {
|
|
73
|
+
if (!isInlineControl(node) || pos < from || pos + node.nodeSize > to) return true;
|
|
74
|
+
const attrs = withEnclosingRevision(node, revisionId);
|
|
75
|
+
if (attrs) enclosed.push({
|
|
76
|
+
pos,
|
|
77
|
+
attrs
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
for (const { pos, attrs } of enclosed) tr.setNodeMarkup(pos, void 0, attrs);
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { ENCLOSING_REVISION_IDS_ATTR, encloseWholeControls, enclosingRevisionIds, resolutionRemovesControl, withEnclosingRevision, withoutResolvedEnclosures };
|