@agentshouse/mdmodel 0.1.0-cc9fa0498a7f7 → 0.1.0-cd90851746292

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 (59) hide show
  1. package/API.md +231 -225
  2. package/README.md +16 -19
  3. package/dist/blocks.d.ts.map +1 -1
  4. package/dist/blocks.js +35 -20
  5. package/dist/condition.d.ts +41 -11
  6. package/dist/condition.d.ts.map +1 -1
  7. package/dist/condition.js +223 -87
  8. package/dist/definition.d.ts +17 -1
  9. package/dist/definition.d.ts.map +1 -1
  10. package/dist/definition.js +319 -43
  11. package/dist/divide.d.ts.map +1 -1
  12. package/dist/divide.js +27 -17
  13. package/dist/document.d.ts +6 -3
  14. package/dist/document.d.ts.map +1 -1
  15. package/dist/document.js +326 -176
  16. package/dist/index.d.ts +13 -9
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -6
  19. package/dist/links.d.ts +3 -0
  20. package/dist/links.d.ts.map +1 -1
  21. package/dist/links.js +108 -51
  22. package/dist/paths.d.ts +10 -1
  23. package/dist/paths.d.ts.map +1 -1
  24. package/dist/paths.js +118 -12
  25. package/dist/pattern.d.ts +10 -0
  26. package/dist/pattern.d.ts.map +1 -0
  27. package/dist/pattern.js +355 -0
  28. package/dist/project.d.ts +1 -0
  29. package/dist/project.d.ts.map +1 -1
  30. package/dist/project.js +38 -15
  31. package/dist/references.d.ts +2 -2
  32. package/dist/references.d.ts.map +1 -1
  33. package/dist/references.js +6 -1
  34. package/dist/schema.d.ts +1 -0
  35. package/dist/schema.d.ts.map +1 -1
  36. package/dist/schema.js +55 -4
  37. package/dist/types.d.ts +50 -2
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/validate.d.ts +30 -2
  40. package/dist/validate.d.ts.map +1 -1
  41. package/dist/validate.js +317 -114
  42. package/dist/yaml.d.ts.map +1 -1
  43. package/dist/yaml.js +66 -21
  44. package/fixtures/README.md +10 -5
  45. package/fixtures/cases/conditional-section/context.json +1 -0
  46. package/fixtures/cases/conditional-section/expected.json +6 -0
  47. package/fixtures/cases/conditional-section/library/questions/hosting.md +9 -0
  48. package/fixtures/cases/conditional-section/library/questions/payments.md +12 -0
  49. package/fixtures/cases/conditional-section/schema.yaml +14 -0
  50. package/fixtures/cases/cross-room-link/context.json +1 -0
  51. package/fixtures/cases/cross-room-link/expected.json +1 -0
  52. package/fixtures/cases/cross-room-link/library/clients/acme.md +6 -0
  53. package/fixtures/cases/cross-room-link/schema.yaml +3 -0
  54. package/fixtures/cases/cross-room-reference/context.json +1 -0
  55. package/fixtures/cases/cross-room-reference/expected.json +1 -0
  56. package/fixtures/cases/cross-room-reference/library/clients/acme.md +9 -0
  57. package/fixtures/cases/cross-room-reference/schema.yaml +9 -0
  58. package/fixtures/division.json +26 -26
  59. package/package.json +10 -3
package/dist/document.js CHANGED
@@ -1,24 +1,35 @@
1
1
  import { ARRAY_INDEX, pointerSegments } from './definition.js';
2
- import { hrefOccurrences } from './links.js';
3
- import { relative, retarget, SHELL_ROOMS, shellTarget, target } from './paths.js';
2
+ import { destinations, hrefOccurrences, linkInventory, } from './links.js';
3
+ import { destinationPath, relative, retarget, roomPath, roomTarget, SHELL_ROOMS, shellTarget, target, } from './paths.js';
4
4
  import { references } from './references.js';
5
5
  import { headingSpans, sectionsRecord } from './sections.js';
6
6
  import { refusal } from './refuse.js';
7
7
  import { bodyStartAfterYaml, bodyStartOf, equalValue, extractFrontmatter, frontmatterValue, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError, } from './yaml.js';
8
- const internals = new WeakMap();
8
+ const HELD = Symbol('held');
9
+ function heldOf(document) {
10
+ return document[HELD];
11
+ }
12
+ export function inventoryOf(document) {
13
+ const held = heldOf(document);
14
+ if (held?.inventory !== undefined)
15
+ return held.inventory;
16
+ const inventory = linkInventory(document.content.slice(bodyStartOf(document.content)));
17
+ if (held !== undefined)
18
+ held.inventory = inventory;
19
+ return inventory;
20
+ }
9
21
  function refuse(file, rule, location) {
10
22
  return location === undefined ? refusal(file, rule) : refusal(file, rule, { location });
11
23
  }
12
24
  function freezeDocument(content, type, fields, preamble, sections, internal) {
13
- const document = Object.freeze({
25
+ const held = { internal, inventory: undefined };
26
+ return Object.freeze(Object.defineProperty({
14
27
  content,
15
28
  type,
16
29
  fields: Object.freeze(fields),
17
30
  preamble,
18
31
  sections: Object.freeze(sections),
19
- });
20
- internals.set(document, internal);
21
- return document;
32
+ }, HELD, { value: held }));
22
33
  }
23
34
  function declaredType(fields) {
24
35
  const value = fields.type;
@@ -28,19 +39,10 @@ function declaredType(fields) {
28
39
  return undefined;
29
40
  return value;
30
41
  }
31
- export function parse(content, file = '') {
42
+ function opened(content, file) {
32
43
  const fence = extractFrontmatter(content);
33
- if (fence.kind === 'none') {
34
- const spans = headingSpans(content, 0);
35
- return {
36
- ok: true,
37
- document: freezeDocument(content, undefined, {}, content.slice(0, spans[0]?.headingStart), sectionsRecord(spans, content), {
38
- root: null,
39
- yamlEnd: 0,
40
- bodyStart: 0,
41
- }),
42
- };
43
- }
44
+ if (fence.kind === 'none')
45
+ return { ok: true, root: null, fields: {}, yamlEnd: 0 };
44
46
  if (fence.kind === 'unclosed')
45
47
  return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
46
48
  try {
@@ -49,17 +51,7 @@ export function parse(content, file = '') {
49
51
  if (fields === null || typeof fields !== 'object' || Array.isArray(fields)) {
50
52
  return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
51
53
  }
52
- const bodyStart = bodyStartAfterYaml(content, fence.yamlEnd);
53
- const spans = headingSpans(content, bodyStart);
54
- const record = fields;
55
- return {
56
- ok: true,
57
- document: freezeDocument(content, declaredType(record), record, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), {
58
- root,
59
- yamlEnd: fence.yamlEnd,
60
- bodyStart,
61
- }),
62
- };
54
+ return { ok: true, root, fields: fields, yamlEnd: fence.yamlEnd };
63
55
  }
64
56
  catch (error) {
65
57
  if (error instanceof YamlError)
@@ -67,71 +59,29 @@ export function parse(content, file = '') {
67
59
  throw error;
68
60
  }
69
61
  }
70
- function splice(content, start, end, insert) {
71
- return content.slice(0, start) + insert + content.slice(end);
72
- }
73
- function applyField(document, name, value, prior) {
74
- const internal = internals.get(document);
75
- if (internal === undefined || internal.root === null) {
76
- return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };
77
- }
78
- if (!equalValue(document.fields[name], prior)) {
79
- return { outcome: 'stale', document };
80
- }
81
- if (!frontmatterValue(value)) {
82
- return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };
83
- }
84
- let nextContent;
85
- const existing = mapEntry(internal.root, name);
86
- if (existing !== undefined) {
87
- const held = existing.value;
88
- const inline = held.start > existing.after &&
89
- !/[\r\n]/.test(document.content.slice(existing.after, held.start));
90
- nextContent = inline
91
- ? splice(document.content, held.start, held.end, serializeJs(value))
92
- : splice(document.content, existing.after, held.end, ` ${serializeJs(value)}`);
93
- }
94
- else if (document.content[internal.root.start] === '{') {
95
- nextContent = splice(document.content, internal.root.end - 1, internal.root.end - 1, `${internal.root.entries.length === 0 ? '' : ', '}${serializeKey(name)}: ${serializeJs(value)}`);
96
- }
97
- else {
98
- const nl = lineEnding(document.content);
99
- const lineStart = document.content.lastIndexOf('\n', internal.root.start - 1) + 1;
100
- const indent = document.content.slice(lineStart, internal.root.start);
101
- const line = `${indent}${serializeKey(name)}: ${serializeJs(value)}${nl}`;
102
- const before = document.content.slice(0, internal.yamlEnd);
103
- const prefix = before.endsWith('\n') || before.endsWith('\r\n') ? '' : nl;
104
- nextContent = splice(document.content, internal.yamlEnd, internal.yamlEnd, `${prefix}${line}`);
105
- }
106
- const parsed = parse(nextContent);
107
- if (!parsed.ok)
108
- return { outcome: 'refuse', refusal: parsed.refusal };
109
- return { outcome: 'applied', document: parsed.document };
110
- }
111
- function applySection(document, name, value, prior) {
112
- const internal = internals.get(document);
113
- if (internal === undefined)
114
- return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };
115
- const spans = headingSpans(document.content, internal.bodyStart);
116
- const span = spans.find((item) => item.name === name);
117
- if (span === undefined)
118
- return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };
119
- const current = document.content.slice(span.bodyStart, span.bodyEnd);
120
- if (current !== prior)
121
- return { outcome: 'stale', document };
122
- const parsed = parse(splice(document.content, span.bodyStart, span.bodyEnd, value));
123
- if (!parsed.ok)
124
- return { outcome: 'refuse', refusal: parsed.refusal };
125
- return { outcome: 'applied', document: parsed.document };
62
+ export function parseFrontmatter(content, file = '') {
63
+ const read = opened(content, file);
64
+ if (!read.ok)
65
+ return read;
66
+ return {
67
+ ok: true,
68
+ frontmatter: Object.freeze({
69
+ content,
70
+ type: declaredType(read.fields),
71
+ fields: Object.freeze(read.fields),
72
+ }),
73
+ };
126
74
  }
127
- function applyPreamble(document, value, prior) {
128
- if (document.preamble !== prior)
129
- return { outcome: 'stale', document };
130
- const start = bodyStartOf(document.content);
131
- const parsed = parse(splice(document.content, start, start + document.preamble.length, value));
132
- if (!parsed.ok)
133
- return { outcome: 'refuse', refusal: parsed.refusal };
134
- return { outcome: 'applied', document: parsed.document };
75
+ export function parse(content, file = '') {
76
+ const read = opened(content, file);
77
+ if (!read.ok)
78
+ return read;
79
+ const bodyStart = read.root === null ? 0 : bodyStartAfterYaml(content, read.yamlEnd);
80
+ const spans = headingSpans(content, bodyStart);
81
+ return {
82
+ ok: true,
83
+ document: freezeDocument(content, declaredType(read.fields), read.fields, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), { root: read.root, yamlEnd: read.yamlEnd, bodyStart }),
84
+ };
135
85
  }
136
86
  function fitsBare(to) {
137
87
  if (to === '' || to.startsWith('<'))
@@ -160,58 +110,34 @@ function destinationText(to, bracketed) {
160
110
  return `<${to}>`;
161
111
  return null;
162
112
  }
163
- function relinked(content, bodyStart, href, to) {
164
- const written = retarget(href, to);
165
- let rewritten = '';
166
- let cursor = 0;
167
- for (const match of hrefOccurrences(content.slice(bodyStart))) {
168
- if (match.href !== href)
169
- continue;
170
- const text = destinationText(written, match.bracketed);
171
- if (text === null)
172
- return refuse('', 'unwritable-link', written);
173
- rewritten += content.slice(cursor, bodyStart + match.start);
174
- rewritten += text;
175
- cursor = bodyStart + match.end;
176
- }
177
- return rewritten + content.slice(cursor);
178
- }
179
- function shellRelinked(content, bodyStart, file, handle) {
180
- const hrefs = new Set(hrefOccurrences(content.slice(bodyStart)).map(({ href }) => href));
181
- let held = content;
182
- for (const href of hrefs) {
113
+ function shellRelinked(body, file, handle) {
114
+ const occurrences = hrefOccurrences(body);
115
+ const written = new Map();
116
+ for (const href of new Set(occurrences.map((occurrence) => occurrence.href))) {
183
117
  const path = shellTarget(href, handle);
184
118
  if (path === null)
185
119
  continue;
186
- const text = relinked(held, bodyStart, href, relative(file, path));
187
- if (typeof text !== 'string')
188
- return text;
189
- held = text;
120
+ const to = retarget(href, relative(file, path));
121
+ if (destinationText(to, false) === null)
122
+ return refuse('', 'unwritable-link', to);
123
+ written.set(href, to);
190
124
  }
191
- return held;
192
- }
193
- function reparsed(document, content) {
194
- if (typeof content !== 'string')
195
- return { outcome: 'refuse', refusal: content };
196
- if (content === document.content)
197
- return { outcome: 'applied', document };
198
- const parsed = parse(content);
199
- if (!parsed.ok)
200
- return { outcome: 'refuse', refusal: parsed.refusal };
201
- return { outcome: 'applied', document: parsed.document };
202
- }
203
- function applyLink(document, href, to) {
204
- const content = document.content;
205
- const bodyStart = bodyStartOf(content);
206
- if (!hrefOccurrences(content.slice(bodyStart)).some((occurrence) => occurrence.href === href))
207
- return { outcome: 'stale', document };
208
- return reparsed(document, relinked(content, bodyStart, href, to));
125
+ let rewritten = '';
126
+ let cursor = 0;
127
+ for (const match of occurrences) {
128
+ const to = written.get(match.href);
129
+ if (to === undefined)
130
+ continue;
131
+ rewritten += body.slice(cursor, match.start) + destinationText(to, match.bracketed);
132
+ cursor = match.end;
133
+ }
134
+ return rewritten + body.slice(cursor);
209
135
  }
210
- function nodeAt(root, pointer) {
136
+ function nodeAt(root, pointer, entryOf = mapEntry) {
211
137
  let node = root;
212
138
  for (const key of pointerSegments(pointer)) {
213
139
  if (node?.kind === 'map')
214
- node = mapEntry(node, key)?.value;
140
+ node = entryOf(node, key)?.value;
215
141
  else if (node?.kind === 'seq' && ARRAY_INDEX.test(key))
216
142
  node = node.items[Number(key)];
217
143
  else
@@ -226,37 +152,36 @@ function scalarText(source, value) {
226
152
  return JSON.stringify(value);
227
153
  return serializeJs(value);
228
154
  }
229
- function applyReference(document, pointer, value, prior) {
230
- const root = internals.get(document)?.root;
231
- const node = root === null || root === undefined ? undefined : nodeAt(root, pointer);
232
- if (node?.kind !== 'scalar' || node.value !== prior)
233
- return { outcome: 'stale', document };
234
- const text = scalarText(document.content[node.start], value);
235
- const parsed = parse(splice(document.content, node.start, node.end, text));
236
- if (!parsed.ok)
237
- return { outcome: 'refuse', refusal: parsed.refusal };
238
- return { outcome: 'applied', document: parsed.document };
239
- }
240
- export function relativize(document, file, handle, definition) {
241
- let held = document;
155
+ export function relativize(document, file, handle, definition, kept = new Set()) {
156
+ const relativized = (href) => {
157
+ const path = shellTarget(href, handle);
158
+ return path === null || kept.has(`${SHELL_ROOMS}${handle}/${path}`) ? null : path;
159
+ };
160
+ const changes = [];
242
161
  for (const { pointer, value } of definition === undefined
243
162
  ? []
244
163
  : references(document, definition)) {
245
- const path = shellTarget(value, handle);
246
- if (path === null)
247
- continue;
248
- const result = applyReference(held, pointer, retarget(value, relative(file, path)), value);
249
- if (result.outcome !== 'applied')
250
- return result;
251
- held = result.document;
164
+ const path = relativized(value);
165
+ if (path !== null)
166
+ changes.push({
167
+ kind: 'reference',
168
+ pointer,
169
+ value: retarget(value, relative(file, path)),
170
+ prior: value,
171
+ });
172
+ }
173
+ for (const href of new Set(destinations(inventoryOf(document)).map(({ href }) => href))) {
174
+ const path = relativized(href);
175
+ if (path !== null)
176
+ changes.push({ kind: 'link', href, to: relative(file, path) });
252
177
  }
253
- return reparsed(held, shellRelinked(held.content, bodyStartOf(held.content), file, handle));
178
+ return apply(document, changes);
254
179
  }
255
180
  export function relativizeBody(body, file, handle) {
256
- const relinkedBody = shellRelinked(body, 0, file, handle);
181
+ const relinkedBody = shellRelinked(body, file, handle);
257
182
  if (typeof relinkedBody !== 'string')
258
183
  return { outcome: 'refuse', refusal: relinkedBody };
259
- const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => href.startsWith(SHELL_ROOMS));
184
+ const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => destinationPath(href)?.startsWith(SHELL_ROOMS) === true && roomTarget(href) === null);
260
185
  if (elsewhere !== undefined)
261
186
  return { outcome: 'refuse', refusal: refuse(file, 'unresolved-link', elsewhere.href) };
262
187
  return { outcome: 'applied', body: relinkedBody };
@@ -265,11 +190,11 @@ export function relinkBody(body, file, moves) {
265
190
  let rewritten = '';
266
191
  let cursor = 0;
267
192
  for (const match of hrefOccurrences(body)) {
268
- const from = target(file, match.href);
193
+ const from = target(file, match.href) ?? roomPath(match.href);
269
194
  const to = from === null ? undefined : moves.get(from);
270
195
  if (to === undefined)
271
196
  continue;
272
- const written = retarget(match.href, relative(file, to));
197
+ const written = retarget(match.href, from.startsWith(SHELL_ROOMS) || to.startsWith(SHELL_ROOMS) ? to : relative(file, to));
273
198
  const text = destinationText(written, match.bracketed);
274
199
  if (text === null)
275
200
  return { outcome: 'refuse', refusal: refuse(file, 'unwritable-link', written) };
@@ -278,10 +203,10 @@ export function relinkBody(body, file, moves) {
278
203
  }
279
204
  return { outcome: 'applied', body: rewritten + body.slice(cursor) };
280
205
  }
281
- function linksIn(text, file) {
206
+ function linksIn(occurrences, file) {
282
207
  const found = [];
283
208
  const seen = new Set();
284
- for (const occurrence of hrefOccurrences(text)) {
209
+ for (const occurrence of occurrences) {
285
210
  if (seen.has(occurrence.href))
286
211
  continue;
287
212
  const resolved = target(file, occurrence.href);
@@ -293,25 +218,250 @@ function linksIn(text, file) {
293
218
  return found;
294
219
  }
295
220
  export function links(document, file) {
296
- return linksIn(document.content.slice(bodyStartOf(document.content)), file);
221
+ return linksIn(destinations(inventoryOf(document)), file);
297
222
  }
298
223
  export function bodyLinks(body, file) {
299
- return linksIn(body, file);
224
+ return linksIn(hrefOccurrences(body), file);
300
225
  }
301
- export function apply(document, change) {
226
+ function internalOf(document) {
227
+ const held = heldOf(document);
228
+ if (held !== undefined)
229
+ return held.internal;
230
+ const read = opened(document.content, '');
231
+ if (!read.ok)
232
+ return read.refusal;
233
+ return {
234
+ root: read.root,
235
+ yamlEnd: read.yamlEnd,
236
+ bodyStart: read.root === null ? 0 : bodyStartAfterYaml(document.content, read.yamlEnd),
237
+ };
238
+ }
239
+ function partOf(change) {
240
+ if (change.kind === 'field' || change.kind === 'section')
241
+ return `${change.kind} ${change.name}`;
302
242
  if (change.kind === 'link')
303
- return applyLink(document, change.href, change.to);
243
+ return `link ${change.href}`;
304
244
  if (change.kind === 'reference')
305
- return applyReference(document, change.pointer, change.value, change.prior);
306
- if (change.kind === 'preamble')
307
- return applyPreamble(document, change.value, change.prior);
308
- if (change.kind === 'field')
309
- return applyField(document, change.name, change.value, change.prior);
310
- return applySection(document, change.name, change.value, change.prior);
245
+ return `reference ${change.pointer}`;
246
+ return 'preamble';
247
+ }
248
+ function planner(document, internal) {
249
+ const content = document.content;
250
+ const keyed = new Map();
251
+ let spans;
252
+ let occurrences;
253
+ let ending;
254
+ let added = 0;
255
+ const entryOf = (map, key) => {
256
+ let index = keyed.get(map);
257
+ if (index === undefined) {
258
+ index = new Map();
259
+ for (const entry of map.entries)
260
+ if (!index.has(entry.key))
261
+ index.set(entry.key, entry);
262
+ keyed.set(map, index);
263
+ }
264
+ return index.get(key);
265
+ };
266
+ const field = (name, value, prior, at) => {
267
+ const root = internal.root;
268
+ if (root === null)
269
+ return refuse('', 'frontmatter-fence', name);
270
+ if (!equalValue(document.fields[name], prior))
271
+ return 'stale';
272
+ if (!frontmatterValue(value))
273
+ return refuse('', 'frontmatter-fence', name);
274
+ const existing = entryOf(root, name);
275
+ if (existing !== undefined) {
276
+ const held = existing.value;
277
+ const inline = held.start > existing.after && !/[\r\n]/.test(content.slice(existing.after, held.start));
278
+ return inline
279
+ ? [{ start: held.start, end: held.end, text: serializeJs(value), at }]
280
+ : [{ start: existing.after, end: held.end, text: ` ${serializeJs(value)}`, at }];
281
+ }
282
+ const entry = `${serializeKey(name)}: ${serializeJs(value)}`;
283
+ added += 1;
284
+ if (content[root.start] === '{') {
285
+ const separator = root.entries.length === 0 && added === 1 ? '' : ', ';
286
+ return [{ start: root.end - 1, end: root.end - 1, text: `${separator}${entry}`, at }];
287
+ }
288
+ const lineStart = content.lastIndexOf('\n', root.start - 1) + 1;
289
+ ending ??= lineEnding(content);
290
+ const line = `${content.slice(lineStart, root.start)}${entry}${ending}`;
291
+ return [{ start: internal.yamlEnd, end: internal.yamlEnd, text: line, at, entry: true }];
292
+ };
293
+ const section = (name, value, prior, at) => {
294
+ if (spans === undefined) {
295
+ spans = new Map();
296
+ for (const span of headingSpans(content, internal.bodyStart))
297
+ if (!spans.has(span.name))
298
+ spans.set(span.name, span);
299
+ }
300
+ const span = spans.get(name);
301
+ if (span === undefined)
302
+ return refuse('', 'required-section', name);
303
+ if (content.slice(span.bodyStart, span.bodyEnd) !== prior)
304
+ return 'stale';
305
+ return [{ start: span.bodyStart, end: span.bodyEnd, text: value, at }];
306
+ };
307
+ const link = (href, to, at) => {
308
+ if (occurrences === undefined) {
309
+ occurrences = new Map();
310
+ for (const occurrence of destinations(inventoryOf(document))) {
311
+ const same = occurrences.get(occurrence.href);
312
+ if (same === undefined)
313
+ occurrences.set(occurrence.href, [occurrence]);
314
+ else
315
+ same.push(occurrence);
316
+ }
317
+ }
318
+ const found = occurrences.get(href);
319
+ if (found === undefined)
320
+ return 'stale';
321
+ const written = retarget(href, to);
322
+ if (destinationText(written, false) === null)
323
+ return refuse('', 'unwritable-link', written);
324
+ return found.map((occurrence) => ({
325
+ start: internal.bodyStart + occurrence.start,
326
+ end: internal.bodyStart + occurrence.end,
327
+ text: destinationText(written, occurrence.bracketed),
328
+ at,
329
+ }));
330
+ };
331
+ const reference = (pointer, value, prior, at) => {
332
+ const node = internal.root === null ? undefined : nodeAt(internal.root, pointer, entryOf);
333
+ if (node?.kind !== 'scalar' || node.value !== prior)
334
+ return 'stale';
335
+ return [
336
+ { start: node.start, end: node.end, text: scalarText(content[node.start], value), at },
337
+ ];
338
+ };
339
+ return (change, at) => {
340
+ if (change.kind === 'field')
341
+ return field(change.name, change.value, change.prior, at);
342
+ if (change.kind === 'section')
343
+ return section(change.name, change.value, change.prior, at);
344
+ if (change.kind === 'link')
345
+ return link(change.href, change.to, at);
346
+ if (change.kind === 'reference')
347
+ return reference(change.pointer, change.value, change.prior, at);
348
+ if (document.preamble !== change.prior)
349
+ return 'stale';
350
+ const start = internal.bodyStart;
351
+ return [{ start, end: start + document.preamble.length, text: change.value, at }];
352
+ };
353
+ }
354
+ function overlapping(edits, stale) {
355
+ let reach = -1;
356
+ let owner = -1;
357
+ for (const edit of edits) {
358
+ if (edit.start < reach)
359
+ stale.add(Math.max(edit.at, owner));
360
+ if (edit.end > reach) {
361
+ reach = edit.end;
362
+ owner = edit.at;
363
+ }
364
+ }
365
+ }
366
+ function firstHeadings(content, bodyStart) {
367
+ const first = new Map();
368
+ for (const span of headingSpans(content, bodyStart)) {
369
+ if (!first.has(span.name))
370
+ first.set(span.name, span.headingStart);
371
+ }
372
+ return first;
373
+ }
374
+ function hidden(before, bodyStart, after, shifts) {
375
+ const placed = firstHeadings(after, bodyStartOf(after));
376
+ let shift = 0;
377
+ let index = 0;
378
+ for (const [name, at] of firstHeadings(before, bodyStart)) {
379
+ while (index < shifts.length && shifts[index][0] <= at) {
380
+ shift += shifts[index][1];
381
+ index += 1;
382
+ }
383
+ if (placed.get(name) !== at + shift)
384
+ return name;
385
+ }
386
+ return undefined;
387
+ }
388
+ function spliced(source, edits) {
389
+ const shifts = [];
390
+ const pieces = [];
391
+ let cursor = 0;
392
+ let entered = false;
393
+ let ended = false;
394
+ for (const edit of edits) {
395
+ const kept = source.slice(cursor, edit.start);
396
+ pieces.push(kept);
397
+ if (kept !== '')
398
+ ended = kept.endsWith('\n');
399
+ let text = edit.text;
400
+ if (edit.entry === true && !entered) {
401
+ entered = true;
402
+ if (!ended)
403
+ text = `${lineEnding(source)}${text}`;
404
+ }
405
+ pieces.push(text);
406
+ if (text !== '')
407
+ ended = text.endsWith('\n');
408
+ shifts.push([edit.start, text.length - (edit.end - edit.start)]);
409
+ cursor = edit.end;
410
+ }
411
+ pieces.push(source.slice(cursor));
412
+ return { content: pieces.join(''), shifts };
413
+ }
414
+ export function apply(document, changes) {
415
+ const internal = internalOf(document);
416
+ if (!('bodyStart' in internal))
417
+ return { outcome: 'refuse', refusal: internal };
418
+ const plan = planner(document, internal);
419
+ const edits = [];
420
+ const stale = new Set();
421
+ const named = new Set();
422
+ for (const [at, change] of changes.entries()) {
423
+ const planned = plan(change, at);
424
+ if (planned === 'stale') {
425
+ stale.add(at);
426
+ continue;
427
+ }
428
+ if (!Array.isArray(planned))
429
+ return { outcome: 'refuse', refusal: planned };
430
+ const part = partOf(change);
431
+ if (named.has(part)) {
432
+ stale.add(at);
433
+ continue;
434
+ }
435
+ named.add(part);
436
+ for (const edit of planned)
437
+ edits.push(edit);
438
+ }
439
+ edits.sort((left, right) => left.start - right.start || left.end - right.end || left.at - right.at);
440
+ overlapping(edits, stale);
441
+ if (stale.size > 0)
442
+ return { outcome: 'stale', document, stale: [...stale].sort((left, right) => left - right) };
443
+ const source = document.content;
444
+ const { content } = spliced(source, edits);
445
+ if (content === source)
446
+ return { outcome: 'applied', document };
447
+ const replaced = edits.filter((edit) => {
448
+ const kind = changes[edit.at].kind;
449
+ return kind === 'section' || kind === 'preamble';
450
+ });
451
+ if (replaced.length > 0) {
452
+ const checked = spliced(source, replaced);
453
+ const name = hidden(source, internal.bodyStart, checked.content, checked.shifts);
454
+ if (name !== undefined)
455
+ return { outcome: 'refuse', refusal: refuse('', 'overlapping-section', name) };
456
+ }
457
+ const parsed = parse(content);
458
+ if (!parsed.ok)
459
+ return { outcome: 'refuse', refusal: parsed.refusal };
460
+ return { outcome: 'applied', document: parsed.document };
311
461
  }
312
462
  export function serialize(document) {
313
463
  return document.content;
314
464
  }
315
465
  export function frontmatter(document) {
316
- return internals.get(document)?.root ?? null;
466
+ return heldOf(document)?.internal.root ?? null;
317
467
  }
package/dist/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
- export { dated, datePointers, factKind, facts, fires, holds, instant, itemFires, pointed, prosePaths, WRITE_DOORS, } from './condition.js';
2
- export type { Actor, Condition, ConditionScope, ConditionTest, ConditionValue, CronCondition, DateCondition, DocumentCondition, DocumentEvent, DocumentFacts, Fact, FactKind, ItemCondition, ItemFacts, Resolved, WriteDoor, } from './condition.js';
1
+ export { ACTOR_PATHS, actorOperators, changeFields, CONDITION_OPERATORS, dated, datePointers, DOCUMENT_PATHS, factKind, facts, fires, holds, instant, ITEM_PATHS, itemFires, itemKind, itemOperators, memberPointers, members, MESSAGE_PATHS, messageFires, messageOperators, notificationFires, pointed, prosePaths, testOperators, TEXT_FIELD, valuedChange, WRITE_DOORS, } from './condition.js';
2
+ export type { Actor, Condition, ConditionOperator, ConditionScope, ConditionTest, ConditionValue, CronCondition, DateCondition, DocumentCondition, DocumentEvent, DocumentFacts, Fact, FactKind, ItemCondition, ItemFacts, MessageCondition, MessageFacts, NotificationCondition, Resolved, WriteDoor, } from './condition.js';
3
+ export { headingLines } from './blocks.js';
4
+ export type { HeadingLine } from './blocks.js';
3
5
  export { divide } from './divide.js';
4
- export { apply, bodyLinks, links, parse, relativize, relativizeBody, relinkBody, serialize, } from './document.js';
6
+ export { apply, bodyLinks, links, parse, parseFrontmatter, relativize, relativizeBody, relinkBody, serialize, } from './document.js';
5
7
  export { HOUSE_FIELDS, MEMBER_FIELD, RECEIVED_FIELD, SITE_FIELD, TYPE_FIELD } from './fields.js';
6
- export { relative, retarget, target } from './paths.js';
8
+ export { relative, retarget, roomPath, roomTarget, target } from './paths.js';
9
+ export type { RoomTarget } from './paths.js';
7
10
  export { MARKDOWN_SYNTAX_PROFILE } from './profile.js';
8
11
  export { project } from './project.js';
9
12
  export { references } from './references.js';
10
- export { ARRAY_INDEX, ATTACHMENT, crossedFacts, escapePointer, itemFacts, memberFacts, pointerFacts, pointerSegments, } from './definition.js';
11
- export type { Definition, JsonType, PatternFacts, PointerFacts, SectionSpec, Shape, ValueFacts, } from './definition.js';
12
- export type { ApplyResult, BodyResult, Change, Division, DivisionContext, Document, Library, ModelVerification, Link, ParseResult, Projection, ProjectionContext, Reference, Refusal, Span, Validation, ValidationContext, VerifiedModel, } from './types.js';
13
- export { validate, verify } from './validate.js';
13
+ export { ARRAY_INDEX, ATTACHMENT, crossedFacts, escapePointer, itemFacts, JSON_POINTER, memberFacts, pointerFacts, pointerSegments, } from './definition.js';
14
+ export type { Definition, FieldRules, JsonType, PatternFacts, PointerFacts, RuleValue, SectionSpec, Shape, Transition, ValueFacts, } from './definition.js';
15
+ export type { ApplyResult, BodyResult, Change, Division, DivisionContext, Document, DocumentCheck, Frontmatter, FrontmatterResult, Library, ModelVerification, Link, ParseResult, Projection, ProjectionContext, Reached, Reference, Refusal, RoomReference, Span, Validation, ValidationContext, ValidationStep, VerifiedModel, } from './types.js';
16
+ export { assess, bodyRoomLinks, checkDocument, resolves, roomReferences, sectionReferences, validate, validating, verdicts, verify, } from './validate.js';
17
+ export type { Assessment, Verdicts } from './validate.js';
14
18
  export { VERSION } from './version.js';
15
- export { equalValue, frontmatterValue } from './yaml.js';
19
+ export { bodyAfterFrontmatter, equalValue, frontmatterValue } from './yaml.js';
16
20
  export type { FrontmatterValue } from './yaml.js';
17
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,YAAY,EACZ,QAAQ,EACR,KAAK,EACL,KAAK,EACL,KAAK,EACL,OAAO,EACP,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,KAAK,EACL,SAAS,EACT,cAAc,EACd,aAAa,EACb,cAAc,EACd,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,aAAa,EACb,IAAI,EACJ,QAAQ,EACR,aAAa,EACb,SAAS,EACT,QAAQ,EACR,SAAS,GACV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EACL,KAAK,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,UAAU,EACV,cAAc,EACd,UAAU,EACV,SAAS,GACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,WAAW,EACX,UAAU,EACV,YAAY,EACZ,aAAa,EACb,SAAS,EACT,WAAW,EACX,YAAY,EACZ,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,UAAU,GACX,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,WAAW,EACX,UAAU,EACV,MAAM,EACN,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,OAAO,EACP,iBAAiB,EACjB,IAAI,EACJ,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,IAAI,EACJ,UAAU,EACV,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACzD,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}