@portone/docx-editor 0.6.1 → 0.6.2

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.
@@ -8,7 +8,12 @@ import {
8
8
  } from "../ooxml/xml.js";
9
9
  import { visitPreservedFragments } from "../schema/preservedFragments.js";
10
10
  import { unattributedCommentAuthors } from "../schema/protection.js";
11
- import { HEADER_FOOTER_KINDS } from "../schema/stories.js";
11
+ import {
12
+ HEADER_FOOTER_KINDS,
13
+ NOTE_KINDS,
14
+ splitStoryKey,
15
+ storyKey
16
+ } from "../schema/stories.js";
12
17
  import {
13
18
  commentReferencesIn,
14
19
  commentsChanged,
@@ -23,12 +28,10 @@ import {
23
28
  } from "./comments/parts.js";
24
29
  import { unrecordedAuthors } from "./comments/people.js";
25
30
  import { currentCommentBodies } from "./comments/writing.js";
26
- import {
27
- identityProblems,
28
- identityProblemsInStories
29
- } from "./identities.js";
31
+ import { identityProblems, identityProblemsInStories } from "./identities.js";
30
32
  import { insertedImageSrcs } from "./media.js";
31
33
  import { canDefineNewList, newNumIds, startedLists } from "./newLists.js";
34
+ import { firstNoteReferences } from "./notes/references.js";
32
35
  import { CONTENT_TYPES_PATH } from "./packageParts.js";
33
36
  import { STORY_ENTRIES_PARTS, STORY_WRITINGS } from "./partPlanners.js";
34
37
  import { lostOriginal } from "./serializePreserved.js";
@@ -37,75 +40,110 @@ import {
37
40
  sessionOf
38
41
  } from "./session.js";
39
42
  import {
43
+ rewrittenStories,
40
44
  storyChangesOf,
41
45
  storyEntriesOf,
42
46
  storyEntriesProblems,
43
47
  unwrittenStoryChanges
44
48
  } from "./storyParts.js";
45
- var bookmarkPairs = {
46
- name: "bookmarkPairs",
47
- check(doc, session) {
48
- const problems = [];
49
- const open = /* @__PURE__ */ new Map();
50
- const used = /* @__PURE__ */ new Set();
51
- visitPreservedFragments(doc, (node, pos, xml) => {
52
- const source = xml ?? originalBlock(node, session)?.xml ?? null;
53
- if (source === null) return;
54
- if (!source.includes("bookmark")) return;
55
- let root;
56
- try {
57
- root = parseXml(
58
- session.documentPrefix + source + session.documentSuffix
59
- ).documentElement;
60
- } catch {
61
- problems.push({
49
+ function found(scope, pos, problem) {
50
+ return scope.story === null ? { ...problem, pos } : problem;
51
+ }
52
+ function overScopes(name, check) {
53
+ return {
54
+ name,
55
+ check: ({ scopes, session }) => scopes.flatMap((scope) => check(scope, session))
56
+ };
57
+ }
58
+ var bookmarkPairs = overScopes("bookmarkPairs", (scope, session) => {
59
+ const problems = [];
60
+ const open = /* @__PURE__ */ new Map();
61
+ const used = /* @__PURE__ */ new Set();
62
+ visitPreservedFragments(scope.doc, (node, pos, xml) => {
63
+ const source = xml ?? originalBlock(node, session)?.xml ?? null;
64
+ if (source === null) return;
65
+ if (!source.includes("bookmark")) return;
66
+ let root;
67
+ try {
68
+ root = parseXml(
69
+ session.documentPrefix + source + session.documentSuffix
70
+ ).documentElement;
71
+ } catch {
72
+ problems.push(
73
+ found(scope, pos, {
62
74
  code: "malformed-xml",
63
75
  message: "preserved bookmark XML could not be parsed",
64
- pos
65
- });
66
- return;
67
- }
68
- for (const marker of Array.from(root.getElementsByTagName("*"))) {
69
- if (marker.namespaceURI !== W_NS || marker.localName !== "bookmarkStart" && marker.localName !== "bookmarkEnd")
70
- continue;
71
- const kind = marker.localName.slice("bookmark".length);
72
- const value = attributeByLocalName(marker, "id");
73
- if (value === null || value === "") {
74
- problems.push({
76
+ reason: { kind: "unreadable-preserved-xml", story: scope.story }
77
+ })
78
+ );
79
+ return;
80
+ }
81
+ for (const marker of Array.from(root.getElementsByTagName("*"))) {
82
+ if (marker.namespaceURI !== W_NS || marker.localName !== "bookmarkStart" && marker.localName !== "bookmarkEnd")
83
+ continue;
84
+ const kind = marker.localName.slice("bookmark".length);
85
+ const value = attributeByLocalName(marker, "id");
86
+ if (value === null || value === "") {
87
+ problems.push(
88
+ found(scope, pos, {
75
89
  code: "malformed-xml",
76
90
  message: `a bookmark${kind} has no id`,
77
- pos
78
- });
79
- } else if (kind === "Start") {
80
- if (used.has(value)) {
81
- problems.push({
91
+ reason: {
92
+ kind: "unnamed-bookmark",
93
+ marker: kind === "Start" ? "start" : "end",
94
+ story: scope.story
95
+ }
96
+ })
97
+ );
98
+ } else if (kind === "Start") {
99
+ if (used.has(value)) {
100
+ problems.push(
101
+ found(scope, pos, {
82
102
  code: "malformed-xml",
83
103
  message: `bookmark ${value} has more than one start marker`,
84
- pos
85
- });
86
- } else {
87
- open.set(value, pos);
88
- used.add(value);
89
- }
90
- } else if (!open.delete(value)) {
91
- problems.push({
104
+ reason: {
105
+ kind: "repeated-bookmark-start",
106
+ id: value,
107
+ story: scope.story
108
+ }
109
+ })
110
+ );
111
+ } else {
112
+ open.set(value, pos);
113
+ used.add(value);
114
+ }
115
+ } else if (!open.delete(value)) {
116
+ problems.push(
117
+ found(scope, pos, {
92
118
  code: "malformed-xml",
93
119
  message: `bookmark ${value} ends without an earlier start marker`,
94
- pos
95
- });
96
- }
120
+ reason: {
121
+ kind: "unmatched-bookmark",
122
+ id: value,
123
+ marker: "end",
124
+ story: scope.story
125
+ }
126
+ })
127
+ );
97
128
  }
98
- });
99
- for (const [id, pos] of open) {
100
- problems.push({
129
+ }
130
+ });
131
+ for (const [id, pos] of open) {
132
+ problems.push(
133
+ found(scope, pos, {
101
134
  code: "malformed-xml",
102
135
  message: `bookmark ${id} has no end marker`,
103
- pos
104
- });
105
- }
106
- return problems;
136
+ reason: {
137
+ kind: "unmatched-bookmark",
138
+ id,
139
+ marker: "start",
140
+ story: scope.story
141
+ }
142
+ })
143
+ );
107
144
  }
108
- };
145
+ return problems;
146
+ });
109
147
  function mergeReachesPastLastRow(table) {
110
148
  let overlong = false;
111
149
  table.forEach((row, _offset, at) => {
@@ -117,135 +155,190 @@ function mergeReachesPastLastRow(table) {
117
155
  });
118
156
  return overlong;
119
157
  }
120
- var tableGrids = {
121
- name: "tableGrids",
122
- check(doc) {
123
- const problems = [];
124
- doc.descendants((node, pos) => {
125
- if (node.type.name !== "table") return true;
126
- if (mergeReachesPastLastRow(node)) {
127
- problems.push({
158
+ var tableGrids = overScopes("tableGrids", (scope) => {
159
+ const problems = [];
160
+ scope.doc.descendants((node, pos) => {
161
+ if (node.type.name !== "table") return true;
162
+ if (mergeReachesPastLastRow(node)) {
163
+ problems.push(
164
+ found(scope, pos, {
128
165
  code: "invalid-table",
129
166
  message: "a vertical merge in the table reaches past the last row",
130
- pos
131
- });
132
- }
133
- return true;
134
- });
135
- return problems;
136
- }
137
- };
167
+ reason: { kind: "vertical-merge-past-table", story: scope.story }
168
+ })
169
+ );
170
+ }
171
+ return true;
172
+ });
173
+ return problems;
174
+ });
138
175
  var CARRIES_ITS_XML = /* @__PURE__ */ new Set([
139
176
  "rawInline",
140
177
  "rawRunContent"
141
178
  ]);
142
- function lostOriginalOf(node, session) {
179
+ function lostOriginalOf(node, session, story) {
143
180
  if (CARRIES_ITS_XML.has(node.type.name)) {
144
- return typeof node.attrs.xml === "string" ? null : "a preserved element has lost its original XML";
181
+ return typeof node.attrs.xml === "string" ? null : {
182
+ message: "a preserved element has lost its original XML",
183
+ reason: {
184
+ kind: "lost-preserved-xml",
185
+ node: node.type.name,
186
+ story
187
+ }
188
+ };
145
189
  }
146
190
  if (node.type.isInGroup("preserved")) {
147
191
  if (typeof node.attrs.xml === "string") return null;
148
- return originalBlock(node, session) ? null : lostOriginal(node, session);
192
+ return originalBlock(node, session) ? null : lostOriginal(node, session, story);
149
193
  }
150
194
  return null;
151
195
  }
152
- var preservedOriginals = {
153
- name: "preservedOriginals",
154
- check(doc, session) {
196
+ var preservedOriginals = overScopes(
197
+ "preservedOriginals",
198
+ (scope, session) => {
155
199
  const problems = [];
156
- doc.descendants((node, pos) => {
157
- const message = lostOriginalOf(node, session);
158
- if (message !== null)
159
- problems.push({ code: "lost-original", message, pos });
200
+ scope.doc.descendants((node, pos) => {
201
+ const lost = lostOriginalOf(node, session, scope.story);
202
+ if (lost !== null) {
203
+ problems.push(found(scope, pos, { code: "lost-original", ...lost }));
204
+ }
160
205
  return true;
161
206
  });
162
207
  return problems;
163
208
  }
164
- };
209
+ );
165
210
  var uniqueIdentities = {
166
211
  name: "uniqueIdentities",
167
- check(doc, session) {
212
+ check({ doc, session }) {
168
213
  const parts = [
169
214
  ...HEADER_FOOTER_KINDS.flatMap(
170
- (kind) => storyChangesOf(doc, session, kind)
171
- ).flatMap(
172
- (change) => change.change === "edited" ? [[{ story: change.current, frozen: false }]] : []
215
+ (kind) => storyChangesOf(doc, session, kind).flatMap(
216
+ (change) => change.change === "edited" ? [
217
+ [
218
+ {
219
+ key: change.imported.key,
220
+ story: change.current,
221
+ frozen: false
222
+ }
223
+ ]
224
+ ] : []
225
+ )
173
226
  ),
174
227
  ...STORY_ENTRIES_PARTS.map((part) => storyEntriesOf(part, doc, session))
175
228
  ];
176
229
  return [
177
- ...identityProblems(doc),
178
- ...parts.flatMap((entries) => identityProblemsInStories(entries)).map(({ code, message }) => ({ code, message }))
230
+ ...identityProblems(doc).map(
231
+ ({ code, message, node, pos }) => ({
232
+ code,
233
+ message,
234
+ reason: { kind: "duplicate-preserved-block", node, story: null },
235
+ pos
236
+ })
237
+ ),
238
+ ...parts.flatMap(
239
+ (entries) => identityProblemsInStories(entries).map(
240
+ ({ code, message, node, story }) => {
241
+ const entry = entries[story];
242
+ return {
243
+ code,
244
+ message,
245
+ reason: {
246
+ kind: "duplicate-preserved-block",
247
+ node,
248
+ story: entry === void 0 ? null : splitStoryKey(entry.key)
249
+ }
250
+ };
251
+ }
252
+ )
253
+ )
179
254
  ];
180
255
  }
181
256
  };
182
257
  var storiesHaveWriters = {
183
258
  name: "storiesHaveWriters",
184
- check(doc, session) {
259
+ check({ doc, session }) {
185
260
  return unwrittenStoryChanges(STORY_WRITINGS, doc, session).map(
186
261
  ({ key, change }) => ({
187
262
  code: "unsupported-content",
188
- message: `the ${key} story was ${change}, and no part writer carries that into the file`
263
+ message: `the ${key} story was ${change}, and no part writer carries that into the file`,
264
+ reason: {
265
+ kind: "unwritten-story-change",
266
+ story: splitStoryKey(key),
267
+ change
268
+ }
189
269
  })
190
270
  );
191
271
  }
192
272
  };
273
+ function undefinedListProblems(scope, undefinedIds) {
274
+ const problems = [];
275
+ scope.doc.descendants((node, pos) => {
276
+ if (node.type.name !== "paragraph") return true;
277
+ const numId = toParagraphFormat(node.attrs.format)?.numbering?.numId;
278
+ if (numId === void 0 || !undefinedIds.delete(numId)) return false;
279
+ problems.push(
280
+ found(scope, pos, {
281
+ code: "unsupported-content",
282
+ message: `the list numbered ${numId} has no definition to be written`,
283
+ reason: { kind: "undefined-list", numId, story: scope.story }
284
+ })
285
+ );
286
+ return false;
287
+ });
288
+ return problems;
289
+ }
193
290
  var listDefinitions = {
194
291
  name: "listDefinitions",
195
- check(doc, session) {
292
+ check({ doc, session, scopes }) {
196
293
  const undefinedIds = new Set(startedLists(doc, session).unregistered);
197
294
  if (undefinedIds.size === 0) return [];
198
- const problems = [];
199
- doc.descendants((node, pos) => {
200
- if (node.type.name !== "paragraph") return true;
201
- const numId = toParagraphFormat(node.attrs.format)?.numbering?.numId;
202
- if (numId === void 0 || !undefinedIds.delete(numId)) return false;
203
- problems.push({
204
- code: "unsupported-content",
205
- message: `the list numbered ${numId} has no definition to be written`,
206
- pos
207
- });
208
- return false;
209
- });
210
- return problems;
295
+ return scopes.flatMap(
296
+ (scope) => undefinedListProblems(scope, undefinedIds)
297
+ );
211
298
  }
212
299
  };
213
- function addsCommentsPart(doc, session) {
300
+ function addedCommentsPart(doc, session) {
214
301
  const bodyChanged = commentsChanged(doc, session);
215
302
  const threadChanged = extensionsChanged(doc, session);
216
- if (!bodyChanged && !threadChanged) return false;
217
- if (commentsPart.pathIn(session) === null || session.comments.xml === null)
218
- return true;
303
+ if (!bodyChanged && !threadChanged) return null;
304
+ if (commentsPart.pathIn(session) === null || session.comments.xml === null) {
305
+ return "comments";
306
+ }
219
307
  const references = commentReferencesIn(doc);
220
- if (threadChanged && (references.size > 0 || session.comments.extendedPartPath !== null) && (commentsExtendedPart.pathIn(session) === null || session.comments.extendedXml === null))
221
- return true;
308
+ if (threadChanged && (references.size > 0 || session.comments.extendedPartPath !== null) && (commentsExtendedPart.pathIn(session) === null || session.comments.extendedXml === null)) {
309
+ return "commentsExtended";
310
+ }
222
311
  return bodyChanged && (peoplePart.pathIn(session) === null || session.comments.people.xml === null) && unrecordedAuthors(
223
312
  currentCommentBodies(references).values(),
224
313
  session.comments.people,
225
314
  unattributedCommentAuthors(session.comments.ordered)
226
- ).size > 0;
315
+ ).size > 0 ? "people" : null;
227
316
  }
228
317
  var mediaContentTypes = {
229
318
  name: "mediaContentTypes",
230
- check(doc, session) {
319
+ check({ doc, session }) {
231
320
  if (session.parts.has(CONTENT_TYPES_PATH)) return [];
232
321
  const problems = [];
233
322
  if (insertedImageSrcs(doc).length > 0) {
234
323
  problems.push({
235
324
  code: "missing-content-types",
236
- message: `cannot add an image to a package that has no ${CONTENT_TYPES_PATH}`
325
+ message: `cannot add an image to a package that has no ${CONTENT_TYPES_PATH}`,
326
+ reason: { kind: "missing-content-types", part: "media" }
237
327
  });
238
328
  }
239
329
  if (!canDefineNewList(session) && newNumIds(doc, session).length > 0) {
240
330
  problems.push({
241
331
  code: "missing-content-types",
242
- message: `cannot add a part to a package that has no ${CONTENT_TYPES_PATH}`
332
+ message: `cannot add a part to a package that has no ${CONTENT_TYPES_PATH}`,
333
+ reason: { kind: "missing-content-types", part: "numbering" }
243
334
  });
244
335
  }
245
- if (addsCommentsPart(doc, session)) {
336
+ const comments = addedCommentsPart(doc, session);
337
+ if (comments !== null) {
246
338
  problems.push({
247
339
  code: "missing-content-types",
248
- message: `cannot add a part to a package that has no ${CONTENT_TYPES_PATH}`
340
+ message: `cannot add a part to a package that has no ${CONTENT_TYPES_PATH}`,
341
+ reason: { kind: "missing-content-types", part: comments }
249
342
  });
250
343
  }
251
344
  return problems;
@@ -253,28 +346,63 @@ var mediaContentTypes = {
253
346
  };
254
347
  var commentPartRoots = {
255
348
  name: "commentPartRoots",
256
- check(doc, session) {
349
+ check({ doc, session }) {
257
350
  const problems = [];
258
351
  const { xml, extendedXml, extendedPartPath } = session.comments;
259
352
  if (xml !== null && commentsChanged(doc, session)) {
260
353
  const problem = commentsRootProblem(xml);
261
- if (problem !== null)
262
- problems.push({ code: "malformed-xml", message: problem });
354
+ if (problem !== null) {
355
+ problems.push({
356
+ code: "malformed-xml",
357
+ message: problem,
358
+ reason: { kind: "unwritable-part-root", part: "comments" }
359
+ });
360
+ }
263
361
  }
264
362
  if (extendedXml !== null && extensionsChanged(doc, session) && (commentReferencesIn(doc).size > 0 || extendedPartPath !== null)) {
265
363
  const problem = extensionsRootProblem(extendedXml);
266
- if (problem !== null)
267
- problems.push({ code: "malformed-xml", message: problem });
364
+ if (problem !== null) {
365
+ problems.push({
366
+ code: "malformed-xml",
367
+ message: problem,
368
+ reason: { kind: "unwritable-part-root", part: "commentsExtended" }
369
+ });
370
+ }
268
371
  }
269
372
  return problems;
270
373
  }
271
374
  };
272
375
  var notePartRoots = {
273
376
  name: "notePartRoots",
274
- check: (doc, session) => STORY_ENTRIES_PARTS.flatMap(
377
+ check: ({ doc, session }) => STORY_ENTRIES_PARTS.flatMap(
275
378
  (part) => storyEntriesProblems(part, doc, session)
276
379
  )
277
380
  };
381
+ function noteKeyIn(reason) {
382
+ const story = "story" in reason ? reason.story : null;
383
+ if (story === null) return null;
384
+ const kind = NOTE_KINDS.find((candidate) => candidate === story.kind);
385
+ return kind === void 0 ? null : storyKey(kind, story.id);
386
+ }
387
+ function atNoteReferences(problems, doc) {
388
+ let references = null;
389
+ const placed = (key) => {
390
+ references ??= firstNoteReferences(
391
+ doc,
392
+ new Set(
393
+ problems.flatMap(
394
+ ({ pos, reason }) => pos === void 0 ? noteKeyIn(reason) ?? [] : []
395
+ )
396
+ )
397
+ );
398
+ return references.get(key);
399
+ };
400
+ return problems.map((problem) => {
401
+ const key = problem.pos === void 0 ? noteKeyIn(problem.reason) : null;
402
+ const pos = key === null ? void 0 : placed(key);
403
+ return pos === void 0 ? problem : { ...problem, pos };
404
+ });
405
+ }
278
406
  var EXPORT_INVARIANTS = [
279
407
  bookmarkPairs,
280
408
  tableGrids,
@@ -286,12 +414,27 @@ var EXPORT_INVARIANTS = [
286
414
  commentPartRoots,
287
415
  notePartRoots
288
416
  ];
417
+ function exportScopes(doc, session) {
418
+ return [
419
+ { doc, story: null },
420
+ ...rewrittenStories(STORY_WRITINGS, doc, session).map(({ key, story }) => ({
421
+ doc: story,
422
+ story: splitStoryKey(key)
423
+ }))
424
+ ];
425
+ }
289
426
  var answered = /* @__PURE__ */ new WeakMap();
290
427
  function problemsOf(doc, session) {
291
428
  const known = answered.get(doc);
292
429
  if (known && known.session === session) return known.problems;
293
- const problems = EXPORT_INVARIANTS.flatMap(
294
- (invariant) => invariant.check(doc, session)
430
+ const subject = {
431
+ doc,
432
+ session,
433
+ scopes: exportScopes(doc, session)
434
+ };
435
+ const problems = atNoteReferences(
436
+ EXPORT_INVARIANTS.flatMap((invariant) => invariant.check(subject)),
437
+ doc
295
438
  );
296
439
  answered.set(doc, { session, problems });
297
440
  return problems;
@@ -3,6 +3,7 @@ import { toParagraphFormat } from "../model/format.js";
3
3
  import { NEW_LISTS_ATTR, newListsOf } from "../numbering/listRegistry.js";
4
4
  import { parseNumbering } from "../numbering/parseNumbering.js";
5
5
  import { R_NS } from "../ooxml/xml.js";
6
+ import { asStoryKey, storiesOf, storyNodeOf } from "../schema/stories.js";
6
7
  import { CONTENT_TYPES_PATH } from "./packageParts.js";
7
8
  var NUMBERING_REL_TYPE = `${R_NS}/numbering`;
8
9
  var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
@@ -15,20 +16,28 @@ function collectNumIds(node, into) {
15
16
  };
16
17
  if (visit(node)) node.descendants(visit);
17
18
  }
18
- function numIdsIn(node) {
19
+ function numIdsNow(doc) {
19
20
  const used = /* @__PURE__ */ new Set();
20
- collectNumIds(node, used);
21
+ collectNumIds(doc, used);
22
+ for (const key of Object.keys(storiesOf(doc))) {
23
+ const story = asStoryKey(key);
24
+ const node = story === null ? null : storyNodeOf(doc, story);
25
+ if (node !== null) collectNumIds(node, used);
26
+ }
21
27
  return used;
22
28
  }
23
29
  function numIdsAtOpen(session) {
24
30
  const used = /* @__PURE__ */ new Set();
25
31
  for (const block of session.blocks) collectNumIds(block.node, used);
32
+ for (const story of session.stories.values()) {
33
+ for (const block of story.blocks) collectNumIds(block.node, used);
34
+ }
26
35
  return used;
27
36
  }
28
37
  function newNumIds(doc, session) {
29
38
  const defined = parseNumbering(session.numberingXml).lists;
30
39
  const atOpen = numIdsAtOpen(session);
31
- return Array.from(numIdsIn(doc)).filter((numId) => !defined.has(numId) && !atOpen.has(numId)).sort((a, b) => a - b);
40
+ return Array.from(numIdsNow(doc)).filter((numId) => !defined.has(numId) && !atOpen.has(numId)).sort((a, b) => a - b);
32
41
  }
33
42
  function startedLists(doc, session) {
34
43
  const registered = newListsOf(doc.attrs[NEW_LISTS_ATTR]);
@@ -9,19 +9,12 @@ import { sectionsOf } from "../sections.js";
9
9
  import {
10
10
  withNoteProps
11
11
  } from "./reading.js";
12
+ import { eachNoteReference } from "./references.js";
12
13
  var MAX_LABEL_CHARS = 64;
13
14
  function referencesBySection(doc) {
14
15
  const found = [];
15
- doc.forEach((block, offset, index) => {
16
- block.descendants((node, pos) => {
17
- if (node.type.name === "noteReference") {
18
- found.push({
19
- block: index,
20
- reference: { pos: offset + 1 + pos, node }
21
- });
22
- }
23
- return true;
24
- });
16
+ eachNoteReference(doc, ({ node, pos, block }) => {
17
+ found.push({ block, reference: { pos, node } });
25
18
  });
26
19
  if (found.length === 0) return [];
27
20
  const sections = sectionsOf(doc);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Where the body refers to its notes.
3
+ *
4
+ * Three readers ask: the notes writers collect the ids each part is referred to by, the numbering
5
+ * counts the references section by section (`./numbering`), and the export invariants look up the
6
+ * first reference to one note, which is where a problem about a note's own text is reported
7
+ * (`docx/invariants`). One walk here is what keeps the three reading the same nodes in the same
8
+ * order.
9
+ */
10
+ import type { Node as PMNode } from "prosemirror-model";
11
+ import { type NoteKey } from "../../schema/stories";
12
+ /** One reference the body makes to a note */
13
+ export interface PlacedNoteReference {
14
+ readonly node: PMNode;
15
+ /** Where the reference stands in the document */
16
+ readonly pos: number;
17
+ /** The top-level block it stands in, by index, which is what a section covers a run of */
18
+ readonly block: number;
19
+ /** The note it names, and null where it names no kind of note or carries no id */
20
+ readonly key: NoteKey | null;
21
+ }
22
+ /**
23
+ * Every `noteReference` of the body in document order, until `visit` answers false.
24
+ *
25
+ * Only the document node is walked: WordprocessingML puts a reference in the body and a note's own
26
+ * text in a part of its own, so a side story holds none.
27
+ */
28
+ export declare function eachNoteReference(doc: PMNode, visit: (reference: PlacedNoteReference) => boolean | void): void;
29
+ /**
30
+ * Where the body first refers to each of these notes, and nothing for one it refers to nowhere.
31
+ * The walk stops as soon as every note asked about has been placed.
32
+ */
33
+ export declare function firstNoteReferences(doc: PMNode, wanted: ReadonlySet<NoteKey>): ReadonlyMap<NoteKey, number>;
@@ -0,0 +1,37 @@
1
+ // src/docx/notes/references.ts
2
+ import { NOTE_KINDS, storyKey } from "../../schema/stories.js";
3
+ function keyOf(node) {
4
+ const kind = NOTE_KINDS.find((candidate) => candidate === node.attrs.kind);
5
+ const id = node.attrs.id;
6
+ return kind === void 0 || typeof id !== "string" ? null : storyKey(kind, id);
7
+ }
8
+ function eachNoteReference(doc, visit) {
9
+ let running = true;
10
+ doc.forEach((block, offset, index) => {
11
+ if (!running) return;
12
+ block.descendants((node, pos) => {
13
+ if (!running) return false;
14
+ if (node.type.name !== "noteReference") return true;
15
+ running = visit({
16
+ node,
17
+ pos: offset + 1 + pos,
18
+ block: index,
19
+ key: keyOf(node)
20
+ }) !== false;
21
+ return false;
22
+ });
23
+ });
24
+ }
25
+ function firstNoteReferences(doc, wanted) {
26
+ const first = /* @__PURE__ */ new Map();
27
+ if (wanted.size === 0) return first;
28
+ eachNoteReference(doc, ({ key, pos }) => {
29
+ if (key !== null && wanted.has(key) && !first.has(key)) first.set(key, pos);
30
+ return first.size < wanted.size;
31
+ });
32
+ return first;
33
+ }
34
+ export {
35
+ eachNoteReference,
36
+ firstNoteReferences
37
+ };