@stll/folio-core 0.57.0 → 0.58.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.
Files changed (39) hide show
  1. package/dist/ai-edits/apply.js +77 -8
  2. package/dist/controller/canonicalClipboard.d.ts +3 -1
  3. package/dist/controller/canonicalClipboard.js +24 -12
  4. package/dist/controller/canonicalComposition.d.ts +13 -4
  5. package/dist/controller/canonicalComposition.js +82 -9
  6. package/dist/controller/canonicalInput.d.ts +1 -1
  7. package/dist/controller/canonicalInput.js +4 -3
  8. package/dist/controller/canonicalSelection.js +10 -1
  9. package/dist/controller/canonicalSession.d.ts +1 -1
  10. package/dist/controller/canonicalSession.js +59 -23
  11. package/dist/controller/canonicalStoryEditor.d.ts +1 -1
  12. package/dist/controller/canonicalStoryEditor.js +1 -1
  13. package/dist/controller/canonicalStructure.js +8 -3
  14. package/dist/controller/hiddenEditorApi.js +1 -1
  15. package/dist/controller/hiddenEditorManager.js +1 -1
  16. package/dist/docx/listRendering.d.ts +18 -0
  17. package/dist/{prosemirror/conversion/listRenderingDefinition.js → docx/listRendering.js} +17 -3
  18. package/dist/docx/server/materializeYjsDocx.js +1 -1
  19. package/dist/markdown/internals.js +7 -0
  20. package/dist/markdown/renderParagraph.js +11 -12
  21. package/dist/markdown/renderRuns.js +2 -2
  22. package/dist/markdown/types.d.ts +3 -0
  23. package/dist/prosemirror/attrs/index.js +4 -0
  24. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  25. package/dist/prosemirror/conversion/fromProseDoc.js +128 -27
  26. package/dist/prosemirror/conversion/toProseDoc.js +10 -23
  27. package/dist/prosemirror/extensions/nodes/ImageExtension.js +8 -1
  28. package/dist/prosemirror/imagePictureNames.d.ts +12 -0
  29. package/dist/prosemirror/imagePictureNames.js +20 -0
  30. package/dist/prosemirror/paragraphFormattingProvenance.js +1 -0
  31. package/dist/prosemirror/runStyleFormatting.d.ts +72 -2
  32. package/dist/prosemirror/runStyleFormatting.js +18 -1
  33. package/dist/prosemirror/schema/nodes.d.ts +1 -0
  34. package/dist/prosemirror/styles/styleResolver.d.ts +4 -1
  35. package/dist/prosemirror/styles/styleResolver.js +32 -11
  36. package/dist/types/canonicalCapabilities.d.ts +15 -1
  37. package/dist/types/canonicalCapabilities.js +15 -1
  38. package/package.json +2 -2
  39. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +0 -6
@@ -12,8 +12,8 @@ import { mapTocSelection } from "./canonicalTocSelection.js";
12
12
  import { Result, TaggedError, panic } from "better-result";
13
13
  import { hasIllegalXmlCharacters } from "@stll/docx-core";
14
14
  import { Fragment } from "prosemirror-model";
15
- import { DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, commentDocumentIssue, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
16
- import { AllSelection, TextSelection } from "prosemirror-state";
15
+ import { DOCUMENT_OP_REFUSAL_REASONS, DOCUMENT_OP_TYPES, OP_STORIES, applyDocumentOps, combineEdits, commentDocumentIssue, compileEditorIntent, createEditorIntentIdAllocator, documentStories, editorParagraphGroups, findStoryBody, normalizeForOps, packageParagraphIds, sameStory, validateOpsDocument } from "@stll/docx-core/ops";
16
+ import { AllSelection, NodeSelection, TextSelection } from "prosemirror-state";
17
17
  //#region src/controller/canonicalSession.ts
18
18
  var CanonicalSessionError = class extends TaggedError("CanonicalSessionError") {};
19
19
  const refuse = (message) => Result.err(new CanonicalSessionError({
@@ -183,12 +183,21 @@ var CanonicalProjection = class {
183
183
  return Result.ok(paragraph.start + gap.position);
184
184
  }
185
185
  selectionAt(state) {
186
- if (!(state.selection instanceof TextSelection) && !(state.selection instanceof AllSelection)) return refuse("Canonical input requires a text selection.");
187
- const range = canonicalSelectionRange(state);
188
- const type = state.selection instanceof AllSelection ? "all" : "text";
189
- const anchor = this.addressAt(type === "all" ? range.from : state.selection.anchor);
186
+ const selection = state.selection;
187
+ let type;
188
+ let anchorPosition = selection.anchor;
189
+ let headPosition = selection.head;
190
+ if (selection instanceof TextSelection) type = "text";
191
+ else if (selection instanceof NodeSelection && selection.node.isInline && selection.node.isAtom && selection.node.type.name !== "noteMarker") type = "inlineNode";
192
+ else if (selection instanceof AllSelection) {
193
+ type = "all";
194
+ const range = canonicalSelectionRange(state);
195
+ anchorPosition = range.from;
196
+ headPosition = range.to;
197
+ } else return refuse("Canonical input requires text, an inline atom, or the whole document selection.");
198
+ const anchor = this.addressAt(anchorPosition);
190
199
  if (anchor.isErr()) return anchor;
191
- const head = this.addressAt(type === "all" ? range.to : state.selection.head);
200
+ const head = this.addressAt(headPosition);
192
201
  if (head.isErr()) return head;
193
202
  return Result.ok({
194
203
  type,
@@ -294,6 +303,7 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
294
303
  const paragraphs = [];
295
304
  let failure;
296
305
  converted.value.forEach((node, offset, index) => {
306
+ if (failure !== void 0) return;
297
307
  const source = body.content.at(index);
298
308
  if (source?.type !== "paragraph" || source.paraId === void 0) {
299
309
  failure = new CanonicalSessionError({
@@ -305,7 +315,7 @@ const project = ({ document, styles, story = OP_STORIES.MAIN }) => {
305
315
  }
306
316
  if (node.type.name !== "paragraph" || node.attrs["paraId"] !== source.paraId) {
307
317
  failure = new CanonicalSessionError({
308
- gap: CANONICAL_GAP.dispatch,
318
+ gap: CANONICAL_GAP.storyContentProjection,
309
319
  reason: "refused",
310
320
  message: "The projection changed paragraph structure."
311
321
  });
@@ -595,9 +605,13 @@ var CanonicalSession = class {
595
605
  if (!Number.isFinite(time)) return refuse("The input time is invalid.");
596
606
  if (from > to || hasIllegalXmlCharacters(text) || /[\t\r\n]/u.test(text)) return refuse("Canonical input accepts a well-formed text replacement.");
597
607
  if (from === to && text.length === 0) return noChange("The input makes no text change.");
598
- const start = projection.inputAddressAt(from);
608
+ const replacesAll = state.selection instanceof AllSelection && from === state.selection.from && to === state.selection.to;
609
+ const range = canonicalSelectionRange(state);
610
+ const inputFrom = replacesAll ? range.from : from;
611
+ const inputTo = replacesAll ? range.to : to;
612
+ const start = projection.inputAddressAt(inputFrom);
599
613
  if (start.isErr()) return start;
600
- const end = projection.inputAddressAt(to);
614
+ const end = projection.inputAddressAt(inputTo);
601
615
  if (end.isErr()) return end;
602
616
  const preSelection = projection.selectionAt(state);
603
617
  if (preSelection.isErr()) return preSelection;
@@ -695,7 +709,14 @@ var CanonicalSession = class {
695
709
  mode: this.intentMode(document, intent),
696
710
  firstBlockId: this.nextBlockId
697
711
  });
698
- if (compiled.isErr()) return refuse(compiled.error.message);
712
+ if (compiled.isErr()) {
713
+ if (compiled.error.reason === DOCUMENT_OP_REFUSAL_REASONS.UNTRACKABLE && (intent.type === "setHyperlink" || intent.type === "removeHyperlink" || intent.type === "insertHyperlink")) return Result.err(new CanonicalSessionError({
714
+ gap: CANONICAL_GAP.trackedHyperlinkResolution,
715
+ reason: "refused",
716
+ message: compiled.error.message
717
+ }));
718
+ return refuse(compiled.error.message);
719
+ }
699
720
  const applied = applyDocumentOps(document, compiled.value.ops);
700
721
  if (applied.isErr()) return refuse(applied.error.message);
701
722
  document = applied.value.document;
@@ -981,8 +1002,11 @@ var CanonicalSession = class {
981
1002
  document: applied.value.document,
982
1003
  previous: this.currentDocument
983
1004
  });
984
- const unsupported = unsupportedSeedReason(applied.value.document, changed);
985
- if (unsupported !== null) return refuse(unsupported);
1005
+ if (unsupportedSeedReason(applied.value.document, changed) !== null) return Result.err(new CanonicalSessionError({
1006
+ gap: CANONICAL_GAP.storyContentProjection,
1007
+ reason: "refused",
1008
+ message: "The operations produce unsupported canonical story content."
1009
+ }));
986
1010
  preservePropertySources({
987
1011
  target: applied.value.document,
988
1012
  source: this.currentDocument,
@@ -1037,15 +1061,17 @@ var CanonicalSession = class {
1037
1061
  transaction.replaceWith(from, to, Fragment.fromArray(replacement));
1038
1062
  }
1039
1063
  if (!transaction.doc.eq(projected.value.doc)) transaction.replaceWith(0, transaction.doc.content.size, projected.value.doc.content);
1040
- if (sameStory(selection.anchor.story, projectedStory) && sameStory(selection.head.story, projectedStory)) restoreCanonicalSelection({
1041
- transaction,
1042
- projection: projected.value,
1043
- selection,
1044
- unavailable: {
1045
- type: "near",
1046
- anchor: state.selection.anchor
1047
- }
1048
- });
1064
+ if (sameStory(selection.anchor.story, projectedStory) && sameStory(selection.head.story, projectedStory)) {
1065
+ if (!restoreCanonicalSelection({
1066
+ transaction,
1067
+ projection: projected.value,
1068
+ selection,
1069
+ unavailable: {
1070
+ type: "near",
1071
+ anchor: state.selection.anchor
1072
+ }
1073
+ })) return refuse("Canonical history cannot restore the selected inline atom.");
1074
+ }
1049
1075
  transaction.setMeta(CANONICAL_PROJECTION_META, {
1050
1076
  type: "canonical",
1051
1077
  origin,
@@ -1104,6 +1130,11 @@ var CanonicalSession = class {
1104
1130
  }
1105
1131
  };
1106
1132
  const createCanonicalSession = (document, styles) => {
1133
+ if (documentStories(document).some((story) => findStoryBody(document, story)?.content.some((block) => block.type === "table"))) return Result.err(new CanonicalSessionError({
1134
+ gap: CANONICAL_GAP.tableActivation,
1135
+ reason: "refused",
1136
+ message: "Canonical sessions cannot activate documents containing tables."
1137
+ }));
1107
1138
  const unsupported = unsupportedSeedReason(document);
1108
1139
  if (unsupported !== null) return refuse(unsupported);
1109
1140
  const owned = cloneDocumentWithParagraphPropertySources(document);
@@ -1192,7 +1223,12 @@ const isCanonicalJoinBoundary = (state, direction) => {
1192
1223
  /** Native character deletion consumes one whole visible grapheme. */
1193
1224
  const deletionRange = (state, direction) => {
1194
1225
  const { selection } = state;
1195
- if (!(selection instanceof TextSelection)) return refuse("Canonical deletion requires a text selection.");
1226
+ if (selection instanceof NodeSelection && selection.node.isInline && selection.node.isAtom && selection.node.type.name !== "noteMarker") return Result.ok({
1227
+ from: selection.from,
1228
+ to: selection.to
1229
+ });
1230
+ if (selection instanceof AllSelection) return Result.ok(canonicalSelectionRange(state));
1231
+ if (!(selection instanceof TextSelection)) return refuse("Canonical deletion requires text, an inline atom, or the whole document selection.");
1196
1232
  if (!selection.empty) return Result.ok({
1197
1233
  from: selection.from,
1198
1234
  to: selection.to
@@ -25,7 +25,7 @@ declare const createCanonicalStoryEditor: ({ story, getView, getApi, enabled, on
25
25
  beforeinput: (view: EditorView, event: InputEvent) => boolean;
26
26
  mousedown: (view: EditorView) => boolean;
27
27
  compositionstart: (view: EditorView) => boolean;
28
- compositionend: (view: EditorView) => boolean;
28
+ compositionend: (view: EditorView, event: Event) => boolean;
29
29
  input: (view: EditorView) => boolean;
30
30
  blur: (view: EditorView) => boolean;
31
31
  paste: (view: EditorView, event: ClipboardEvent) => boolean;
@@ -93,7 +93,7 @@ const createCanonicalStoryEditor = ({ story, getView, getApi, enabled, onRefusal
93
93
  beforeinput: (view, event) => enabled() && boundary.handleDOMEvents.beforeinput(view, event),
94
94
  mousedown: (view) => enabled() && boundary.handleDOMEvents.mousedown(view),
95
95
  compositionstart: (view) => enabled() && boundary.handleDOMEvents.compositionstart(view),
96
- compositionend: (view) => enabled() && boundary.handleDOMEvents.compositionend(view),
96
+ compositionend: (view, event) => enabled() && boundary.handleDOMEvents.compositionend(view, event),
97
97
  input: (view) => enabled() && boundary.handleDOMEvents.input(view),
98
98
  blur: (view) => enabled() && boundary.handleDOMEvents.blur(view),
99
99
  paste: (view, event) => enabled() && boundary.handleDOMEvents.paste(view, event),
@@ -15,6 +15,11 @@ const refuse = (message) => Result.err(new CanonicalSessionError({
15
15
  message,
16
16
  reason: "refused"
17
17
  }));
18
+ const noChange = (message) => Result.err(new CanonicalSessionError({
19
+ gap: CANONICAL_GAP.commands,
20
+ message,
21
+ reason: "noChange"
22
+ }));
18
23
  const selectedParagraphs = (session, state) => {
19
24
  const ids = /* @__PURE__ */ new Set();
20
25
  state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
@@ -218,7 +223,7 @@ const prepareCanonicalCommands = (session, state, commands) => {
218
223
  offset: 0
219
224
  };
220
225
  const level = (numPr.ilvl ?? 0) + (command.direction === "increase" ? 1 : -1);
221
- if (level > 8) return refuse("The list is already at its deepest level.");
226
+ if (level > 8) return noChange("The list is already at its deepest level.");
222
227
  intents.push({
223
228
  type: "formatParagraph",
224
229
  at,
@@ -234,7 +239,7 @@ const prepareCanonicalCommands = (session, state, commands) => {
234
239
  const source = paragraphs.at(0);
235
240
  const numPr = source?.formatting?.numPr;
236
241
  const definitions = session.document.package.numbering;
237
- if (source === void 0 || numPr?.kind !== "reference" || definitions === void 0) return refuse("Numbering changes require a list paragraph.");
242
+ if (source === void 0 || numPr?.kind !== "reference" || definitions === void 0) return noChange("Numbering changes require a list paragraph.");
238
243
  const body = session.document.package.document.content;
239
244
  const index = body.findIndex((item) => item === source);
240
245
  const affected = body.slice(index).filter((item) => item.type === "paragraph" && item.formatting?.numPr?.kind === "reference" && item.formatting.numPr.numId === numPr.numId);
@@ -244,7 +249,7 @@ const prepareCanonicalCommands = (session, state, commands) => {
244
249
  const map = getCachedNumberingMap(definitions);
245
250
  const bullet = numberingLevelUsesBulletMarker(map.getLevel(numPr.numId, numPr.ilvl ?? 0));
246
251
  const earlier = body.slice(0, index).findLast((item) => item.type === "paragraph" && item.formatting?.numPr?.kind === "reference" && item.formatting.numPr.numId !== numPr.numId && numberingLevelUsesBulletMarker(map.getLevel(item.formatting.numPr.numId, item.formatting.numPr.ilvl ?? 0)) === bullet);
247
- if (earlier?.type !== "paragraph" || earlier.formatting?.numPr?.kind !== "reference") return refuse("There is no preceding compatible list.");
252
+ if (earlier?.type !== "paragraph" || earlier.formatting?.numPr?.kind !== "reference") return noChange("There is no preceding compatible list.");
248
253
  targetId = earlier.formatting.numPr.numId;
249
254
  target = {
250
255
  type: "existing",
@@ -9,7 +9,7 @@ import { redo, undo } from "prosemirror-history";
9
9
  */
10
10
  const stateToDocument = (state, originalDoc) => {
11
11
  if (!originalDoc) return null;
12
- return fromProseDoc(state.doc, originalDoc);
12
+ return fromProseDoc(state.doc, originalDoc, { stylesheetSource: { type: "package" } });
13
13
  };
14
14
  const createHiddenEditorApi = (deps) => {
15
15
  const dispatchTr = (view, tr) => {
@@ -688,7 +688,7 @@ const createHiddenEditorManager = (deps) => {
688
688
  blur: (pmView) => editorSession.type === "canonical" ? input.handleDOMEvents.blur(pmView) : false,
689
689
  ...createHiddenEditorClipboardHandlers(deps),
690
690
  compositionstart: (pmView) => editorSession.type === "canonical" ? input.handleDOMEvents.compositionstart(pmView) : false,
691
- compositionend: (pmView) => editorSession.type === "canonical" ? input.handleDOMEvents.compositionend(pmView) : false,
691
+ compositionend: (pmView, event) => editorSession.type === "canonical" ? input.handleDOMEvents.compositionend(pmView, event) : false,
692
692
  input: (pmView) => editorSession.type === "canonical" ? input.handleDOMEvents.input(pmView) : false,
693
693
  paste: (pmView, event) => {
694
694
  if (editorSession.type === "canonical" && !deps.getReadOnly()) return input.handleDOMEvents.paste(pmView, event);
@@ -0,0 +1,18 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { NumberingMap } from "./numberingParser.js";
3
+ //#region src/docx/listRendering.d.ts
4
+ /** Compare scalar fields directly; only nested records need canonical ordering. */
5
+ declare const listRenderingDefinitionsMatch: (left: document_d_exports.ListRendering, right: document_d_exports.ListRendering) => boolean;
6
+ type ListRenderingDefinition = {
7
+ rendering: document_d_exports.ListRendering | null;
8
+ nextSlotOffset: number | undefined;
9
+ };
10
+ /** Resolve numbering facts independently of any paragraph's cached counters. */
11
+ declare const resolveListRenderingDefinition: (numPr: {
12
+ numId: number;
13
+ ilvl?: number;
14
+ }, numbering: NumberingMap | undefined) => ListRenderingDefinition;
15
+ /** Keep content-derived counter/folded-field facts only while their definition is current. */
16
+ declare const resolveCachedListRendering: (cached: document_d_exports.ListRendering | undefined, { rendering, nextSlotOffset }: ListRenderingDefinition) => document_d_exports.ListRendering | null;
17
+ //#endregion
18
+ export { ListRenderingDefinition, listRenderingDefinitionsMatch, resolveCachedListRendering, resolveListRenderingDefinition };
@@ -1,5 +1,6 @@
1
- import { canonicalJson } from "../../utils/canonicalJson.js";
2
- //#region src/prosemirror/conversion/listRenderingDefinition.ts
1
+ import { canonicalJson } from "../utils/canonicalJson.js";
2
+ import { computeListRendering } from "./numberingParser.js";
3
+ //#region src/docx/listRendering.ts
3
4
  const DEFINITION_FIELDS = Object.values({
4
5
  marker: {
5
6
  key: "marker",
@@ -93,5 +94,18 @@ const listRenderingDefinitionsMatch = (left, right) => {
93
94
  }
94
95
  return true;
95
96
  };
97
+ /** Resolve numbering facts independently of any paragraph's cached counters. */
98
+ const resolveListRenderingDefinition = (numPr, numbering) => {
99
+ const nextLevel = numbering?.getLevel(numPr.numId, (numPr.ilvl ?? 0) + 1);
100
+ return {
101
+ rendering: numbering === void 0 ? null : computeListRendering(numPr, numbering),
102
+ nextSlotOffset: nextLevel?.pPr?.hangingIndent === true && nextLevel.pPr.indentFirstLine !== void 0 && nextLevel.pPr.indentFirstLine < 0 ? -nextLevel.pPr.indentFirstLine : void 0
103
+ };
104
+ };
105
+ /** Keep content-derived counter/folded-field facts only while their definition is current. */
106
+ const resolveCachedListRendering = (cached, { rendering, nextSlotOffset }) => {
107
+ if (rendering === null) return null;
108
+ return cached !== void 0 && listRenderingDefinitionsMatch(cached, rendering) && (cached.markerSecondSlotOffsetTwips === void 0 || cached.markerSecondSlotOffsetTwips === nextSlotOffset) ? cached : rendering;
109
+ };
96
110
  //#endregion
97
- export { listRenderingDefinitionsMatch };
111
+ export { listRenderingDefinitionsMatch, resolveCachedListRendering, resolveListRenderingDefinition };
@@ -82,7 +82,7 @@ const materializeYjsDocx = async ({ sourceDocx, yjsUpdate }) => {
82
82
  const proseMirrorDocument = readProseMirrorDocument(yjsUpdate);
83
83
  const baseDocument = await parseDocx(sourceDocx, { preloadFonts: false });
84
84
  const converted = Result.try({
85
- try: () => fromProseDoc(proseMirrorDocument, baseDocument),
85
+ try: () => fromProseDoc(proseMirrorDocument, baseDocument, { stylesheetSource: { type: "package" } }),
86
86
  catch: (cause) => cause instanceof ParagraphPropertySourceValidationError ? new FolioYjsDocxMaterializationError({
87
87
  code: "source_mismatch",
88
88
  message: "The collaboration snapshot belongs to a different source document.",
@@ -1,5 +1,6 @@
1
1
  import { createBuiltInStyleIndex } from "../docx/builtInStyles.js";
2
2
  import { createListLabelCounter } from "../prosemirror/listLabels.js";
3
+ import { createStyleResolver, resolveStyleParagraphNumbering } from "../prosemirror/styles/styleResolver.js";
3
4
  import { createNoteReferenceNumbering, noteReferenceMarker, noteReferenceToken } from "../utils/noteReferenceLabels.js";
4
5
  //#region src/markdown/internals.ts
5
6
  /**
@@ -18,7 +19,13 @@ import { createNoteReferenceNumbering, noteReferenceMarker, noteReferenceToken }
18
19
  * make the render quadratic.
19
20
  */
20
21
  function newContext(opts = {}, styles) {
22
+ const resolver = createStyleResolver(styles);
23
+ const numbering = /* @__PURE__ */ new Map();
21
24
  return {
25
+ paragraphNumbering: (styleId) => {
26
+ if (!numbering.has(styleId)) numbering.set(styleId, resolveStyleParagraphNumbering(resolver, styleId));
27
+ return numbering.get(styleId);
28
+ },
22
29
  builtInStyles: createBuiltInStyleIndex(styles?.styles ?? [], styles?.docDefaults),
23
30
  opts: {
24
31
  annotations: opts.annotations ?? "html",
@@ -1,4 +1,7 @@
1
1
  import { isQuoteStyle, resolveHeadingLevel } from "../docx/builtInStyles.js";
2
+ import { resolveCachedListRendering, resolveListRenderingDefinition } from "../docx/listRendering.js";
3
+ import { getCachedNumberingMap } from "../docx/numberingParser.js";
4
+ import { mergeParagraphNumbering } from "../docx/numberingReference.js";
2
5
  import { listLabelAttrsFromRendering } from "../prosemirror/listLabels.js";
3
6
  import { inlineBreaksToHtml } from "./escape.js";
4
7
  import { renderParagraphInline } from "./renderRuns.js";
@@ -32,7 +35,12 @@ function renderParagraph(ctx, pkg, para) {
32
35
  * does not define as prose, as every other reader reads them.
33
36
  */
34
37
  function renderParagraphBlock(ctx, pkg, para) {
35
- const label = ctx.nextListLabel(listLabelAttrs(para));
38
+ const numPrFromStyle = ctx.paragraphNumbering(para.formatting?.styleId) ?? para.formatting?.numPrFromStyle;
39
+ const numPr = mergeParagraphNumbering(numPrFromStyle, para.formatting?.numPr);
40
+ let list = para.listRendering;
41
+ if (numPr?.kind === "none" || pkg?.numbering !== void 0 && numPr === void 0) list = void 0;
42
+ else if (pkg?.numbering !== void 0 && numPr?.kind === "reference") list = resolveCachedListRendering(para.listRendering, resolveListRenderingDefinition(numPr, getCachedNumberingMap(pkg.numbering))) ?? void 0;
43
+ const label = ctx.nextListLabel(list ? listLabelAttrsFromRendering(list, numPrFromStyle) : UNNUMBERED);
36
44
  const inline = renderParagraphInline(ctx, pkg, para.content, para.paraId);
37
45
  const styleId = para.formatting?.styleId;
38
46
  const headingLevel = markdownHeadingLevel(ctx, para);
@@ -49,8 +57,8 @@ function renderParagraphBlock(ctx, pkg, para) {
49
57
  isListItem: false
50
58
  };
51
59
  }
52
- if (para.listRendering && label !== void 0) return {
53
- markdown: renderListItem(ctx, para.listRendering, label, inline),
60
+ if (list && label !== void 0) return {
61
+ markdown: renderListItem(ctx, list, label, inline),
54
62
  isListItem: true
55
63
  };
56
64
  ctx.listIndentWidths = [];
@@ -72,15 +80,6 @@ function markdownHeadingLevel(ctx, para) {
72
80
  }
73
81
  const UNNUMBERED = Object.freeze({});
74
82
  /**
75
- * The attrs the page's list counter reads, projected from the paragraph the
76
- * way the editor projects them (`toProseDoc`). A paragraph without a list
77
- * rendering counts as unnumbered.
78
- */
79
- function listLabelAttrs(para) {
80
- const list = para.listRendering;
81
- return list ? listLabelAttrsFromRendering(list, para.formatting?.numPrFromStyle) : UNNUMBERED;
82
- }
83
- /**
84
83
  * A plain paragraph whose visible text begins with markdown block syntax (e.g.
85
84
  * `# Not a heading`, `- value`, `1. value`, `> quote`) would be reclassified as
86
85
  * a heading/list/blockquote on re-parse, even though Word carries no matching
@@ -1,5 +1,5 @@
1
1
  import { getHyperlinkRuns } from "../docx/hyperlinkParser.js";
2
- import { RELATIONSHIP_TYPES, resolveRelationshipIdOfType } from "../docx/relsParser.js";
2
+ import { RELATIONSHIP_TYPES, resolveRelationshipIdOfType, resolveRelativePath } from "../docx/relsParser.js";
3
3
  import { decodeOoxmlSymbolCharacter } from "../utils/ooxmlSymbol.js";
4
4
  import { wrapComment, wrapDeletion, wrapInsertion, wrapMoveFrom, wrapMoveTo } from "./annotations.js";
5
5
  import { escapeAltText, escapeInline, escapeLinkUrl } from "./escape.js";
@@ -226,7 +226,7 @@ function renderRunContent(ctx, pkg, content, paraId) {
226
226
  case "noteMarker": break;
227
227
  case "drawing": {
228
228
  const ref = resolveRelationshipIdOfType(pkg?.relationships, item.image.rId, RELATIONSHIP_TYPES.image);
229
- const media = ref.status === "resolved" ? pkg?.media?.get(ref.relationship.target) : void 0;
229
+ const media = ref.status === "resolved" ? pkg?.media?.get(resolveRelativePath("word/document.xml", ref.relationship.target)) : void 0;
230
230
  if (media) {
231
231
  const reg = registerImage(ctx, media, item.image, paraId);
232
232
  const alt = reg.alt ? escapeAltText(reg.alt) : "";
@@ -1,3 +1,4 @@
1
+ import { document_d_exports } from "../types/document.js";
1
2
  import { NoteReferenceKind, NoteReferenceNumbering } from "../utils/noteReferenceLabels.js";
2
3
  import { BuiltInStyleIndex } from "../docx/builtInStyles.js";
3
4
  import { ListLabelCounter } from "../prosemirror/listLabels.js";
@@ -140,6 +141,8 @@ type RenderContext = {
140
141
  nextListLabel: ListLabelCounter;
141
142
  /** The document's styles indexed by the built-in they are; classifies headings and quotes. */
142
143
  builtInStyles: BuiltInStyleIndex;
144
+ /** Authoritative paragraph numbering inherited through the style cascade. */
145
+ paragraphNumbering: (styleId: string | undefined) => document_d_exports.ParagraphFormatting["numPr"];
143
146
  /**
144
147
  * Active native Markdown ancestor marker widths, indexed by OOXML level.
145
148
  * Missing levels contribute no columns. Prose, headings and container
@@ -8,6 +8,7 @@ import { attrsRecord, attrsResult, expectAttrs, expectCachedNodeAttrs, expectNod
8
8
  import { COUNTER_FORMAT_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FONT_HINT_VALUES, FONT_THEME_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, POSITIONAL_TAB_ALIGNMENT_VALUES, POSITIONAL_TAB_LEADER_VALUES, POSITIONAL_TAB_RELATIVE_TO_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_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_DIRECTION_VALUES, TEXT_EFFECT_VALUES, UNDERLINE_STYLE_VALUES } from "../../types/documentEnumValues.js";
9
9
  import { canonicalJson } from "../../utils/canonicalJson.js";
10
10
  import { normalizeHorizontalScalePercent } from "../../utils/horizontalScale.js";
11
+ import { PICTURE_NAME_DOM_ATTRIBUTES } from "../imagePictureNames.js";
11
12
  import { isNoteReferenceOccurrenceId } from "../noteReferenceOccurrenceId.js";
12
13
  import { paragraphNumberingFromAttrValue } from "../numberingAttr.js";
13
14
  import { outlineLevelFromAttrValue } from "../outlineLevelAttr.js";
@@ -489,6 +490,9 @@ const readImageAttrs = (node) => {
489
490
  expectNodeType(node, "image", issues);
490
491
  requiredString(attrs, "src", "image.attrs.src", issues);
491
492
  optionalString(attrs, "docPrName", "image.attrs.docPrName", issues);
493
+ optionalNestedRecord(attrs, "pictureNames", "image.attrs.pictureNames", issues, (names, path, nestedIssues) => {
494
+ for (const key of Object.keys(PICTURE_NAME_DOM_ATTRIBUTES)) optionalString(names, key, `${path}.${key}`, nestedIssues);
495
+ });
492
496
  optionalString(attrs, "alt", "image.attrs.alt", issues);
493
497
  optionalString(attrs, "title", "image.attrs.title", issues);
494
498
  optionalString(attrs, "rId", "image.attrs.rId", issues);
@@ -16,11 +16,18 @@ import { Node } from "prosemirror-model";
16
16
  type ProjectionReuse = "none" | "matched";
17
17
  /** How the conversion treats records the editor did not change. */
18
18
  type FromProseDocOptions = {
19
+ /** The stylesheet authority used to construct the projection. */
20
+ stylesheetSource: {
21
+ type: "package";
22
+ } | {
23
+ type: "supplied";
24
+ styles: document_d_exports.StyleDefinitions;
25
+ };
19
26
  /** Defaults to `"none"`, the only value implemented. */
20
27
  reuse?: ProjectionReuse;
21
28
  };
22
29
  /** Convert a ProseMirror document to the document model. */
23
- declare function fromProseDoc(pmDoc: Node, baseDocument?: document_d_exports.Document, { reuse }?: FromProseDocOptions): document_d_exports.Document;
30
+ declare function fromProseDoc(pmDoc: Node, baseDocument: document_d_exports.Document | undefined, { stylesheetSource, reuse }: FromProseDocOptions): document_d_exports.Document;
24
31
  declare function tableAttrsToFormatting(attrs: TableAttrs): document_d_exports.TableFormatting | undefined;
25
32
  /**
26
33
  * Convert ProseMirror table row attrs to TableRowFormatting