@agentshouse/mdmodel 0.1.0-c5a71fe539a49 → 0.1.0-c60b1ddb74fcc

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 (58) hide show
  1. package/API.md +220 -164
  2. package/dist/blocks.d.ts.map +1 -1
  3. package/dist/blocks.js +31 -17
  4. package/dist/condition.d.ts +19 -7
  5. package/dist/condition.d.ts.map +1 -1
  6. package/dist/condition.js +223 -117
  7. package/dist/definition.d.ts +23 -0
  8. package/dist/definition.d.ts.map +1 -1
  9. package/dist/definition.js +266 -25
  10. package/dist/document.d.ts +7 -8
  11. package/dist/document.d.ts.map +1 -1
  12. package/dist/document.js +105 -49
  13. package/dist/fields.d.ts +2 -0
  14. package/dist/fields.d.ts.map +1 -1
  15. package/dist/fields.js +7 -0
  16. package/dist/index.d.ts +11 -7
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -5
  19. package/dist/links.d.ts +1 -0
  20. package/dist/links.d.ts.map +1 -1
  21. package/dist/links.js +1 -1
  22. package/dist/paths.d.ts +1 -0
  23. package/dist/paths.d.ts.map +1 -1
  24. package/dist/paths.js +92 -12
  25. package/dist/references.d.ts +2 -2
  26. package/dist/references.d.ts.map +1 -1
  27. package/dist/schema.js +2 -2
  28. package/dist/types.d.ts +32 -1
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/validate.d.ts +6 -1
  31. package/dist/validate.d.ts.map +1 -1
  32. package/dist/validate.js +163 -109
  33. package/dist/yaml.d.ts +7 -3
  34. package/dist/yaml.d.ts.map +1 -1
  35. package/dist/yaml.js +72 -43
  36. package/fixtures/README.md +1 -1
  37. package/fixtures/cases/absent-link-dangles/context.json +1 -0
  38. package/fixtures/cases/absent-link-dangles/expected.json +1 -0
  39. package/fixtures/cases/absent-link-dangles/library/item.md +8 -0
  40. package/fixtures/cases/absent-link-dangles/schema.yaml +6 -0
  41. package/fixtures/cases/absolute-link/context.json +1 -0
  42. package/fixtures/cases/{missing-binary-image → absolute-link}/expected.json +1 -1
  43. package/fixtures/cases/absolute-link/library/item.md +8 -0
  44. package/fixtures/cases/absolute-link/schema.yaml +6 -0
  45. package/fixtures/cases/binary-image-dangles/expected.json +1 -0
  46. package/fixtures/cases/binary-link-dangles/expected.json +1 -0
  47. package/fixtures/cases/quoted-link-definition/library/notes/draft.md +1 -1
  48. package/fixtures/cases/shortcut-reference-link/library/item.md +1 -1
  49. package/fixtures/cases/truncated-link-definition/library/notes/draft.md +1 -1
  50. package/fixtures/cases/unresolved-link/library/item.md +1 -1
  51. package/package.json +4 -2
  52. package/fixtures/cases/missing-binary-link/expected.json +0 -5
  53. /package/fixtures/cases/{missing-binary-image → binary-image-dangles}/context.json +0 -0
  54. /package/fixtures/cases/{missing-binary-image → binary-image-dangles}/library/notes/draft.md +0 -0
  55. /package/fixtures/cases/{missing-binary-image → binary-image-dangles}/schema.yaml +0 -0
  56. /package/fixtures/cases/{missing-binary-link → binary-link-dangles}/context.json +0 -0
  57. /package/fixtures/cases/{missing-binary-link → binary-link-dangles}/library/notes/draft.md +0 -0
  58. /package/fixtures/cases/{missing-binary-link → binary-link-dangles}/schema.yaml +0 -0
@@ -1,4 +1,4 @@
1
- import { MEMBER_FIELD, RECEIVED_FIELD, SITE_FIELD } from './fields.js';
1
+ import { MEMBER_FIELD, RECEIVED_FIELD, SITE_FIELD, TYPE_FIELD } from './fields.js';
2
2
  import { equalValue, isRecord, jsValue, parseYamlMapping, YamlError } from './yaml.js';
3
3
  export const JSON_TYPES = [
4
4
  'string',
@@ -12,11 +12,23 @@ export const JSON_TYPES = [
12
12
  export const BUILT_IN_TYPE = 'any';
13
13
  export const ATTACHMENT = 'attachment';
14
14
  const SCALAR_TYPES = new Set(['string', 'number', 'integer', 'boolean']);
15
+ const COMPOSITES = ['array', 'object'];
16
+ const ANNOTATIONS = new Set([
17
+ '$comment',
18
+ 'title',
19
+ 'description',
20
+ 'default',
21
+ 'examples',
22
+ 'deprecated',
23
+ 'readOnly',
24
+ 'writeOnly',
25
+ ]);
26
+ const FIELD_RULES = ['change_roles', 'initial_roles', 'transitions'];
15
27
  const DEFS_REFERENCE = '#/$defs/';
16
28
  const UNRESOLVED = /[\u0000-\u001f\u007f%[\]\\^{|}]|\s$/u;
17
29
  export const ARRAY_INDEX = /^(?:0|[1-9][0-9]*)$/u;
18
30
  const HOUSE_KEYS = {
19
- type: { prose: false },
31
+ [TYPE_FIELD]: { prose: false },
20
32
  [SITE_FIELD]: { prose: false },
21
33
  [RECEIVED_FIELD]: { prose: false },
22
34
  };
@@ -270,24 +282,43 @@ function united(left, right) {
270
282
  return undefined;
271
283
  return [...left, ...right.filter((value) => !left.some((held) => equalValue(held, value)))];
272
284
  }
285
+ function both(left, right) {
286
+ return left !== undefined && right !== undefined ? joined(left, right) : undefined;
287
+ }
273
288
  function joined(left, right) {
274
289
  const type = left.type === right.type ? left.type : undefined;
275
290
  const format = left.format === right.format ? left.format : undefined;
276
291
  const values = united(left.values, right.values);
277
292
  const reference = united(left.reference, right.reference);
278
- const items = left.items !== undefined && right.items !== undefined
279
- ? joined(left.items, right.items)
280
- : undefined;
293
+ const items = both(left.items, right.items);
294
+ const prefix = left.prefix === undefined && right.prefix === undefined
295
+ ? undefined
296
+ : Array.from({ length: Math.max(left.prefix?.length ?? 0, right.prefix?.length ?? 0) }, (_, index) => both(itemFacts(left, index), itemFacts(right, index)) ?? { prose: false });
297
+ const properties = Object.create(null);
298
+ for (const [name, facts] of Object.entries(left.properties ?? {})) {
299
+ const shared = both(facts, right.properties?.[name]);
300
+ if (shared !== undefined)
301
+ properties[name] = shared;
302
+ }
303
+ const patterns = (left.patterns ?? []).flatMap(({ pattern, facts }) => {
304
+ const shared = both(facts, right.patterns?.find((one) => one.pattern === pattern)?.facts);
305
+ return shared === undefined ? [] : [{ pattern, facts: shared }];
306
+ });
307
+ const additional = both(left.additional, right.additional);
281
308
  return {
282
309
  ...(type === undefined ? {} : { type }),
283
310
  ...(format === undefined ? {} : { format }),
284
311
  ...(values === undefined ? {} : { values }),
285
312
  ...(reference === undefined ? {} : { reference }),
286
313
  prose: left.prose && right.prose,
314
+ ...(prefix === undefined ? {} : { prefix }),
287
315
  ...(items === undefined ? {} : { items }),
316
+ ...(Object.keys(properties).length === 0 ? {} : { properties }),
317
+ ...(patterns.length === 0 ? {} : { patterns }),
318
+ ...(additional === undefined ? {} : { additional }),
288
319
  };
289
320
  }
290
- function propertiesOf(held, defs, ancestry) {
321
+ function propertiesOf(held, variants, defs, ancestry) {
291
322
  const declared = new Map();
292
323
  for (const node of held) {
293
324
  if (!isRecord(node.properties))
@@ -298,45 +329,87 @@ function propertiesOf(held, defs, ancestry) {
298
329
  const properties = Object.create(null);
299
330
  for (const [name, schemas] of declared)
300
331
  properties[name] = factsOf(schemas, defs, ancestry);
301
- const variants = Object.create(null);
302
- for (const branch of held.flatMap(variantsOf)) {
303
- for (const [name, facts] of Object.entries(factsOf([branch], defs, ancestry).properties ?? {})) {
332
+ const conditional = Object.create(null);
333
+ for (const branch of variants.values()) {
334
+ for (const [name, facts] of Object.entries(branch.properties ?? {})) {
304
335
  if (Object.hasOwn(properties, name))
305
336
  continue;
306
- variants[name] = Object.hasOwn(variants, name) ? joined(variants[name], facts) : facts;
337
+ conditional[name] = Object.hasOwn(conditional, name)
338
+ ? joined(conditional[name], facts)
339
+ : facts;
307
340
  }
308
341
  }
309
- Object.assign(properties, variants);
342
+ for (const [name, facts] of Object.entries(conditional))
343
+ properties[name] = { ...facts, conditional: true };
310
344
  return Object.keys(properties).length === 0 ? undefined : properties;
311
345
  }
346
+ function nullOnly(facts) {
347
+ return facts.type === 'null' || (facts.values?.every((value) => value === null) ?? false);
348
+ }
349
+ function alternativesOf(held, variants) {
350
+ return held.flatMap((node) => [node.anyOf, node.oneOf].flatMap((branches) => {
351
+ if (!Array.isArray(branches))
352
+ return [];
353
+ const kept = branches
354
+ .flatMap((branch) => {
355
+ const facts = isRecord(branch) ? variants.get(branch) : undefined;
356
+ return facts === undefined ? [] : [facts];
357
+ })
358
+ .filter((facts) => !nullOnly(facts));
359
+ return kept.length === 0 ? [] : [kept];
360
+ }));
361
+ }
362
+ function inheritedOf(held) {
363
+ return held.flatMap((node) => {
364
+ const kept = Object.fromEntries(['type', 'items', 'prefixItems'].flatMap((key) => node[key] === undefined ? [] : [[key, node[key]]]));
365
+ return Object.keys(kept).length === 0 ? [] : [kept];
366
+ });
367
+ }
312
368
  function factsOf(nodes, defs, ancestry) {
313
369
  const { held, ancestry: reached } = conjoined(nodes, defs, ancestry);
314
- const listed = held.flatMap((node) => Array.isArray(node.enum) ? [node.enum] : node.const !== undefined ? [[node.const]] : []);
370
+ const inherited = inheritedOf(held);
371
+ const variants = new Map(held
372
+ .flatMap(variantsOf)
373
+ .map((branch) => [branch, factsOf([branch, ...inherited], defs, reached)]));
374
+ const unions = alternativesOf(held, variants);
375
+ const alternatives = unions.map((kept) => kept.reduce(joined));
376
+ const listed = [
377
+ ...held.flatMap((node) => Array.isArray(node.enum) ? [node.enum] : node.const !== undefined ? [[node.const]] : []),
378
+ ...alternatives.flatMap((facts) => (facts.values === undefined ? [] : [facts.values])),
379
+ ];
315
380
  const values = listed.length === 0
316
381
  ? undefined
317
382
  : listed.reduce((kept, list) => kept.filter((value) => list.some((other) => equalValue(other, value))));
318
383
  const type = firstOf(held, (node) => singleType(node.type)) ??
319
- (values === undefined ? undefined : sharedType(values));
384
+ (values === undefined ? undefined : sharedType(values)) ??
385
+ alternatives.find((facts) => facts.type !== undefined)?.type;
320
386
  const reference = firstOf(held, (node) => typeof node.reference === 'string'
321
387
  ? [node.reference]
322
388
  : Array.isArray(node.reference)
323
389
  ? node.reference
324
- : undefined);
325
- const format = firstOf(held, (node) => typeof node.format === 'string' ? node.format : undefined);
390
+ : undefined) ?? alternatives.find((facts) => facts.reference !== undefined)?.reference;
391
+ const format = firstOf(held, (node) => (typeof node.format === 'string' ? node.format : undefined)) ??
392
+ alternatives.find((facts) => facts.format !== undefined)?.format;
326
393
  const prose = type === 'string' &&
327
394
  values === undefined &&
328
395
  format === undefined &&
329
396
  reference === undefined &&
330
- held.every((node) => node.pattern === undefined);
397
+ held.every((node) => node.pattern === undefined) &&
398
+ unions.every((kept) => kept.every((facts) => facts.type !== 'string' || facts.prose));
331
399
  const listedItems = held.flatMap((node) => (node.items === undefined ? [] : [node.items]));
332
- const items = type === 'array' && listedItems.length > 0 ? factsOf(listedItems, defs, reached) : undefined;
333
- const prefix = type === 'array'
334
- ? firstOf(held, (node) => Array.isArray(node.prefixItems)
335
- ? node.prefixItems.map((schema) => factsOf([schema], defs, reached))
336
- : undefined)
337
- : undefined;
400
+ const items = type !== 'array'
401
+ ? undefined
402
+ : (alternatives.find((facts) => facts.items !== undefined)?.items ??
403
+ (listedItems.length > 0 ? factsOf(listedItems, defs, reached) : undefined));
404
+ const tuples = held.flatMap((node) => Array.isArray(node.prefixItems) ? [node.prefixItems] : []);
405
+ const prefix = type !== 'array'
406
+ ? undefined
407
+ : (alternatives.find((facts) => facts.prefix !== undefined)?.prefix ??
408
+ (tuples.length === 0
409
+ ? undefined
410
+ : Array.from({ length: Math.max(...tuples.map((tuple) => tuple.length)) }, (_, index) => factsOf(tuples.flatMap((tuple) => (index < tuple.length ? [tuple[index]] : [])), defs, reached))));
338
411
  const mapped = type === 'object' || type === undefined;
339
- const properties = mapped ? propertiesOf(held, defs, reached) : undefined;
412
+ const properties = mapped ? propertiesOf(held, variants, defs, reached) : undefined;
340
413
  const patterns = mapped
341
414
  ? held.flatMap((node) => Object.entries(isRecord(node.patternProperties) ? node.patternProperties : {}).map(([pattern, schema]) => ({ pattern, facts: factsOf([schema], defs, reached) })))
342
415
  : [];
@@ -369,6 +442,54 @@ export function memberFacts(facts, key) {
369
442
  const pattern = facts.patterns?.find((one) => new RegExp(one.pattern, 'u').test(key));
370
443
  return pattern === undefined ? facts.additional : pattern.facts;
371
444
  }
445
+ export function crossedFacts(facts) {
446
+ if (facts.type !== 'array')
447
+ return [facts];
448
+ return [...(facts.prefix ?? []), ...(facts.items === undefined ? [] : [facts.items])].flatMap(crossedFacts);
449
+ }
450
+ export function fieldFacts(definition, key) {
451
+ if (Object.hasOwn(definition.fields, key))
452
+ return definition.fields[key];
453
+ if (Object.hasOwn(HOUSE_KEYS, key))
454
+ return undefined;
455
+ return memberFacts(definition.frontmatter, key);
456
+ }
457
+ function stepped({ facts, through, conditional }, token) {
458
+ if (facts.type === 'array' && ARRAY_INDEX.test(token)) {
459
+ const item = itemFacts(facts, Number(token));
460
+ return item === undefined ? [] : [{ facts: item, through, conditional }];
461
+ }
462
+ return crossedFacts(facts).flatMap((crossed) => {
463
+ const member = memberFacts(crossed, token);
464
+ return member === undefined
465
+ ? []
466
+ : [
467
+ {
468
+ facts: member,
469
+ through: through || facts.type === 'array',
470
+ conditional: conditional || member.conditional === true,
471
+ },
472
+ ];
473
+ });
474
+ }
475
+ export function pointerFacts(definition, pointer) {
476
+ const [field, ...rest] = pointerSegments(pointer);
477
+ const declared = field === undefined ? undefined : fieldFacts(definition, field);
478
+ if (declared === undefined)
479
+ return undefined;
480
+ let reached = [
481
+ { facts: declared, through: false, conditional: declared.conditional === true },
482
+ ];
483
+ for (const token of rest)
484
+ reached = reached.flatMap((held) => stepped(held, token));
485
+ if (reached.length === 0)
486
+ return undefined;
487
+ return reached.reduce((kept, held) => ({
488
+ facts: joined(kept.facts, held.facts),
489
+ through: kept.through || held.through,
490
+ conditional: kept.conditional || held.conditional,
491
+ }));
492
+ }
372
493
  function reachedDefs(value, defs, reached) {
373
494
  if (Array.isArray(value)) {
374
495
  for (const item of value)
@@ -429,21 +550,134 @@ function deepFreeze(value) {
429
550
  }
430
551
  return value;
431
552
  }
553
+ function ruleValue(value) {
554
+ return value === null || typeof value === 'string' || typeof value === 'boolean' || finite(value);
555
+ }
556
+ function roleNames(value) {
557
+ return strings(value) && value.every((name) => name !== '');
558
+ }
559
+ function compositesOf(value) {
560
+ return Array.isArray(value) ? ['array'] : isRecord(value) ? ['object'] : [];
561
+ }
562
+ function typed(type) {
563
+ const types = typeof type === 'string' ? [type] : type;
564
+ return Array.isArray(types) ? COMPOSITES.filter((kind) => types.includes(kind)) : undefined;
565
+ }
566
+ function admittedWhole(node) {
567
+ if (node === true)
568
+ return COMPOSITES;
569
+ if (!isRecord(node) || Object.keys(node).some((key) => key !== 'type' && !ANNOTATIONS.has(key)))
570
+ return [];
571
+ return typed(node.type) ?? COMPOSITES;
572
+ }
573
+ function composites(node, defs, seen) {
574
+ if (node === false)
575
+ return [];
576
+ if (!isRecord(node))
577
+ return COMPOSITES;
578
+ const each = (nodes) => Array.isArray(nodes) ? nodes.map((one) => composites(one, defs, seen)) : [];
579
+ const target = typeof node.$ref === 'string' ? refTarget(node.$ref, defs)?.node : undefined;
580
+ const conditioned = () => {
581
+ const met = composites(node.then ?? true, defs, seen);
582
+ return COMPOSITES.filter((kind) => (composites(node.if, defs, seen).includes(kind) && met.includes(kind)) ||
583
+ composites(node.else ?? true, defs, seen).includes(kind));
584
+ };
585
+ const bounds = [
586
+ typed(node.type),
587
+ Array.isArray(node.enum) ? node.enum.flatMap(compositesOf) : undefined,
588
+ Object.hasOwn(node, 'const') ? compositesOf(node.const) : undefined,
589
+ target === undefined || seen.has(target)
590
+ ? undefined
591
+ : composites(target, defs, new Set([...seen, target])),
592
+ ...each(node.allOf),
593
+ ...[node.anyOf, node.oneOf].filter(Array.isArray).map((branches) => each(branches).flat()),
594
+ Object.hasOwn(node, 'if') ? conditioned() : undefined,
595
+ Object.hasOwn(node, 'not')
596
+ ? COMPOSITES.filter((kind) => !admittedWhole(node.not).includes(kind))
597
+ : undefined,
598
+ ];
599
+ return COMPOSITES.filter((kind) => bounds.every((bound) => bound?.includes(kind) ?? true));
600
+ }
601
+ function transitionOf(value) {
602
+ if (!isRecord(value) || !Object.hasOwn(value, 'from') || !Object.hasOwn(value, 'to'))
603
+ return undefined;
604
+ const { from, to, roles, ...rest } = value;
605
+ if (Object.keys(rest).length > 0 || !ruleValue(from) || !ruleValue(to))
606
+ return undefined;
607
+ if (roles === undefined)
608
+ return { from, to };
609
+ return roleNames(roles) ? { from, to, roles } : undefined;
610
+ }
611
+ function fieldRules(written, field, at, defs) {
612
+ const { change_roles: changeRoles, initial_roles: initialRoles, transitions } = written;
613
+ if (changeRoles !== undefined && !roleNames(changeRoles))
614
+ return refuse('change_roles', `${at}/change_roles`);
615
+ if (initialRoles !== undefined && !roleNames(initialRoles))
616
+ return refuse('initial_roles', `${at}/initial_roles`);
617
+ if (transitions === undefined)
618
+ return {
619
+ ...(changeRoles === undefined ? {} : { changeRoles }),
620
+ ...(initialRoles === undefined ? {} : { initialRoles }),
621
+ };
622
+ if (!Array.isArray(transitions) || composites(field, defs, new Set()).length > 0)
623
+ return refuse('transitions', `${at}/transitions`);
624
+ const listed = [];
625
+ for (const [index, held] of transitions.entries()) {
626
+ const transition = transitionOf(held);
627
+ if (transition === undefined)
628
+ return refuse('transitions', `${at}/transitions/${index}`);
629
+ listed.push(transition);
630
+ }
631
+ return {
632
+ ...(changeRoles === undefined ? {} : { changeRoles }),
633
+ ...(initialRoles === undefined ? {} : { initialRoles }),
634
+ transitions: listed,
635
+ };
636
+ }
637
+ function unruled(schema) {
638
+ const written = new Map();
639
+ if (!isRecord(schema.properties))
640
+ return { schema, written };
641
+ const properties = Object.create(null);
642
+ for (const [field, node] of Object.entries(schema.properties)) {
643
+ if (!isRecord(node) || !FIELD_RULES.some((keyword) => Object.hasOwn(node, keyword))) {
644
+ properties[field] = node;
645
+ continue;
646
+ }
647
+ written.set(field, node);
648
+ properties[field] = Object.fromEntries(Object.entries(node).filter(([keyword]) => !FIELD_RULES.includes(keyword)));
649
+ }
650
+ return { schema: written.size === 0 ? schema : { ...schema, properties }, written };
651
+ }
432
652
  function readType(name, value, context) {
433
653
  const at = `/${escapePointer(name)}`;
434
654
  if (!isRecord(value))
435
655
  return refuse('schema', at);
436
- const { sections: declared, member_document: mark, ...schema } = value;
656
+ const { sections: declared, member_document: mark, create_roles: createRoles, delete_roles: deleteRoles, ...ruled } = value;
437
657
  if (mark !== undefined && typeof mark !== 'boolean')
438
658
  return refuse('member_document', `${at}/member_document`);
659
+ if (createRoles !== undefined && !roleNames(createRoles))
660
+ return refuse('create_roles', `${at}/create_roles`);
661
+ if (deleteRoles !== undefined && !roleNames(deleteRoles))
662
+ return refuse('delete_roles', `${at}/delete_roles`);
439
663
  const read = declared === undefined
440
664
  ? { sections: Object.create(null) }
441
665
  : readSections(declared, `${at}/sections`, context);
442
666
  if ('failure' in read)
443
667
  return read.failure;
668
+ const { schema, written } = unruled(ruled);
444
669
  const failure = schemaFailure(schema, at, context);
445
670
  if (failure !== undefined)
446
671
  return failure;
672
+ const rules = Object.create(null);
673
+ for (const [field, keywords] of written) {
674
+ const properties = schema.properties;
675
+ const where = `${at}/properties/${escapePointer(field)}`;
676
+ const ruling = fieldRules(keywords, properties[field], where, context.defs);
677
+ if ('rule' in ruling)
678
+ return ruling;
679
+ rules[field] = ruling;
680
+ }
447
681
  const memberDocument = mark === true;
448
682
  const root = valueFacts(schema, context.defs);
449
683
  const fields = Object.assign(Object.create(null), root.properties);
@@ -451,13 +685,20 @@ function readType(name, value, context) {
451
685
  fields[MEMBER_FIELD] = { type: 'string', prose: false };
452
686
  const defs = Object.create(null);
453
687
  reachedDefs(value, context.defs, defs);
688
+ const { create_roles: _create, delete_roles: _delete, ...shaped } = value;
454
689
  return {
455
690
  name,
456
691
  memberDocument,
457
692
  frontmatter: { ...root, properties: { ...HOUSE_KEYS, ...fields } },
458
693
  fields,
459
694
  sections: read.sections,
460
- shape: { schema: value, defs },
695
+ rules,
696
+ ...(createRoles === undefined ? {} : { createRoles }),
697
+ ...(deleteRoles === undefined ? {} : { deleteRoles }),
698
+ shape: {
699
+ schema: written.size === 0 ? shaped : { ...shaped, properties: schema.properties },
700
+ defs,
701
+ },
461
702
  };
462
703
  }
463
704
  function lineAt(text, offset) {
@@ -1,17 +1,16 @@
1
1
  import { type Definition } from './definition.js';
2
- import type { ApplyResult, Change, Document, Link, ParseResult, Refusal } from './types.js';
2
+ import type { ApplyResult, BodyResult, FrontmatterResult, Change, Document, Link, LinkOccurrence, ParseResult } from './types.js';
3
3
  import { type YamlMap } from './yaml.js';
4
+ export declare function parseFrontmatter(content: string, file?: string): FrontmatterResult;
4
5
  export declare function parse(content: string, file?: string): ParseResult;
5
6
  export declare function relativize(document: Document, file: string, handle: string, definition?: Definition): ApplyResult;
6
- export declare function relativizeBody(body: string, file: string, handle: string): {
7
- readonly outcome: 'applied';
8
- readonly body: string;
9
- } | {
10
- readonly outcome: 'refuse';
11
- readonly refusal: Refusal;
12
- };
7
+ export declare function relativizeBody(body: string, file: string, handle: string): BodyResult;
8
+ export declare function relinkBody(body: string, file: string, moves: ReadonlyMap<string, string>): BodyResult;
13
9
  export declare function links(document: Document, file: string): readonly Link[];
10
+ export declare function linkOccurrences(document: Document, file: string): readonly LinkOccurrence[];
11
+ export declare function bodyLinks(body: string, file: string): readonly Link[];
14
12
  export declare function apply(document: Document, change: Change): ApplyResult;
15
13
  export declare function serialize(document: Document): string;
14
+ export declare function fieldStart(document: Document, pointer: string): number | undefined;
16
15
  export declare function frontmatter(document: Document): YamlMap | null;
17
16
  //# sourceMappingURL=document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5F,OAAO,EAYL,KAAK,OAAO,EAEb,MAAM,WAAW,CAAC;AAwCnB,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,WAAW,CAkD7D;AA2KD,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,UAAU,GACtB,WAAW,CAYb;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAEZ;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAO5D;AAED,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,IAAI,EAAE,CAWvE;AAED,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAerE;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEpD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAE9D"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhF,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,cAAc,EACd,WAAW,EAEZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAaL,KAAK,OAAO,EAEb,MAAM,WAAW,CAAC;AAmEnB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,iBAAiB,CAW9E;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,WAAW,CAgB7D;AAqMD,wBAAgB,UAAU,CACxB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,UAAU,GACtB,WAAW,CAYb;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CASrF;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,UAAU,CAeZ;AAuBD,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,IAAI,EAAE,CAEvE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,cAAc,EAAE,CAG3F;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,IAAI,EAAE,CAErE;AAED,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAOrE;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAE9D"}
package/dist/document.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { ARRAY_INDEX, pointerSegments } from './definition.js';
2
2
  import { hrefOccurrences } from './links.js';
3
- import { relative, retarget, SHELL_ROOMS, shellTarget, target } from './paths.js';
3
+ import { destinationPath, relative, retarget, 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
- import { bodyStartAfterYaml, bodyStartOf, equalValue, extractFrontmatter, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError, } from './yaml.js';
7
+ import { bodyStartAfterYaml, bodyStartOf, equalValue, extractFrontmatter, frontmatterValue, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError, } from './yaml.js';
8
8
  const internals = new WeakMap();
9
9
  function refuse(file, rule, location) {
10
10
  return location === undefined ? refusal(file, rule) : refusal(file, rule, { location });
@@ -28,19 +28,10 @@ function declaredType(fields) {
28
28
  return undefined;
29
29
  return value;
30
30
  }
31
- export function parse(content, file = '') {
31
+ function opened(content, file) {
32
32
  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
- }
33
+ if (fence.kind === 'none')
34
+ return { ok: true, root: null, fields: {}, yamlEnd: 0 };
44
35
  if (fence.kind === 'unclosed')
45
36
  return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
46
37
  try {
@@ -49,17 +40,7 @@ export function parse(content, file = '') {
49
40
  if (fields === null || typeof fields !== 'object' || Array.isArray(fields)) {
50
41
  return { ok: false, refusal: refuse(file, 'frontmatter-fence') };
51
42
  }
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
- };
43
+ return { ok: true, root, fields: fields, yamlEnd: fence.yamlEnd };
63
44
  }
64
45
  catch (error) {
65
46
  if (error instanceof YamlError)
@@ -67,6 +48,30 @@ export function parse(content, file = '') {
67
48
  throw error;
68
49
  }
69
50
  }
51
+ export function parseFrontmatter(content, file = '') {
52
+ const read = opened(content, file);
53
+ if (!read.ok)
54
+ return read;
55
+ return {
56
+ ok: true,
57
+ frontmatter: Object.freeze({
58
+ content,
59
+ type: declaredType(read.fields),
60
+ fields: Object.freeze(read.fields),
61
+ }),
62
+ };
63
+ }
64
+ export function parse(content, file = '') {
65
+ const read = opened(content, file);
66
+ if (!read.ok)
67
+ return read;
68
+ const bodyStart = read.root === null ? 0 : bodyStartAfterYaml(content, read.yamlEnd);
69
+ const spans = headingSpans(content, bodyStart);
70
+ return {
71
+ ok: true,
72
+ document: freezeDocument(content, declaredType(read.fields), read.fields, content.slice(bodyStart, spans[0]?.headingStart), sectionsRecord(spans, content), { root: read.root, yamlEnd: read.yamlEnd, bodyStart }),
73
+ };
74
+ }
70
75
  function splice(content, start, end, insert) {
71
76
  return content.slice(0, start) + insert + content.slice(end);
72
77
  }
@@ -78,10 +83,18 @@ function applyField(document, name, value, prior) {
78
83
  if (!equalValue(document.fields[name], prior)) {
79
84
  return { outcome: 'stale', document };
80
85
  }
86
+ if (!frontmatterValue(value)) {
87
+ return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };
88
+ }
81
89
  let nextContent;
82
90
  const existing = mapEntry(internal.root, name);
83
91
  if (existing !== undefined) {
84
- nextContent = splice(document.content, existing.value.start, existing.value.end, serializeJs(value));
92
+ const held = existing.value;
93
+ const inline = held.start > existing.after &&
94
+ !/[\r\n]/.test(document.content.slice(existing.after, held.start));
95
+ nextContent = inline
96
+ ? splice(document.content, held.start, held.end, serializeJs(value))
97
+ : splice(document.content, existing.after, held.end, ` ${serializeJs(value)}`);
85
98
  }
86
99
  else if (document.content[internal.root.start] === '{') {
87
100
  nextContent = splice(document.content, internal.root.end - 1, internal.root.end - 1, `${internal.root.entries.length === 0 ? '' : ', '}${serializeKey(name)}: ${serializeJs(value)}`);
@@ -111,16 +124,30 @@ function applySection(document, name, value, prior) {
111
124
  const current = document.content.slice(span.bodyStart, span.bodyEnd);
112
125
  if (current !== prior)
113
126
  return { outcome: 'stale', document };
114
- const parsed = parse(splice(document.content, span.bodyStart, span.bodyEnd, value));
115
- if (!parsed.ok)
116
- return { outcome: 'refuse', refusal: parsed.refusal };
117
- return { outcome: 'applied', document: parsed.document };
127
+ return replaced(document, internal.bodyStart, span.bodyStart, span.bodyEnd, value);
118
128
  }
119
129
  function applyPreamble(document, value, prior) {
120
130
  if (document.preamble !== prior)
121
131
  return { outcome: 'stale', document };
122
132
  const start = bodyStartOf(document.content);
123
- const parsed = parse(splice(document.content, start, start + document.preamble.length, value));
133
+ return replaced(document, start, start, start + document.preamble.length, value);
134
+ }
135
+ function firstHeadings(content, bodyStart) {
136
+ const first = new Map();
137
+ for (const span of headingSpans(content, bodyStart)) {
138
+ if (!first.has(span.name))
139
+ first.set(span.name, span.headingStart);
140
+ }
141
+ return first;
142
+ }
143
+ function replaced(document, bodyStart, start, end, value) {
144
+ const content = splice(document.content, start, end, value);
145
+ const after = firstHeadings(content, bodyStartOf(content));
146
+ for (const [name, at] of firstHeadings(document.content, bodyStart)) {
147
+ if (after.get(name) !== (at < start ? at : at + value.length - (end - start)))
148
+ return { outcome: 'refuse', refusal: refuse('', 'overlapping-section', name) };
149
+ }
150
+ const parsed = parse(content);
124
151
  if (!parsed.ok)
125
152
  return { outcome: 'refuse', refusal: parsed.refusal };
126
153
  return { outcome: 'applied', document: parsed.document };
@@ -248,25 +275,58 @@ export function relativizeBody(body, file, handle) {
248
275
  const relinkedBody = shellRelinked(body, 0, file, handle);
249
276
  if (typeof relinkedBody !== 'string')
250
277
  return { outcome: 'refuse', refusal: relinkedBody };
251
- const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => href.startsWith(SHELL_ROOMS));
278
+ const elsewhere = hrefOccurrences(relinkedBody).find(({ href }) => destinationPath(href)?.startsWith(SHELL_ROOMS) === true);
252
279
  if (elsewhere !== undefined)
253
280
  return { outcome: 'refuse', refusal: refuse(file, 'unresolved-link', elsewhere.href) };
254
281
  return { outcome: 'applied', body: relinkedBody };
255
282
  }
256
- export function links(document, file) {
283
+ export function relinkBody(body, file, moves) {
284
+ let rewritten = '';
285
+ let cursor = 0;
286
+ for (const match of hrefOccurrences(body)) {
287
+ const from = target(file, match.href);
288
+ const to = from === null ? undefined : moves.get(from);
289
+ if (to === undefined)
290
+ continue;
291
+ const written = retarget(match.href, relative(file, to));
292
+ const text = destinationText(written, match.bracketed);
293
+ if (text === null)
294
+ return { outcome: 'refuse', refusal: refuse(file, 'unwritable-link', written) };
295
+ rewritten += body.slice(cursor, match.start) + text;
296
+ cursor = match.end;
297
+ }
298
+ return { outcome: 'applied', body: rewritten + body.slice(cursor) };
299
+ }
300
+ function occurrencesIn(text, file, offset) {
301
+ const found = [];
302
+ for (const occurrence of hrefOccurrences(text)) {
303
+ const resolved = target(file, occurrence.href);
304
+ if (resolved !== null)
305
+ found.push({ href: occurrence.href, target: resolved, start: offset + occurrence.start });
306
+ }
307
+ return found;
308
+ }
309
+ function linksIn(text, file) {
257
310
  const found = [];
258
311
  const seen = new Set();
259
- for (const occurrence of hrefOccurrences(document.content.slice(bodyStartOf(document.content)))) {
312
+ for (const occurrence of occurrencesIn(text, file, 0)) {
260
313
  if (seen.has(occurrence.href))
261
314
  continue;
262
- const resolved = target(file, occurrence.href);
263
- if (resolved === null)
264
- continue;
265
315
  seen.add(occurrence.href);
266
- found.push({ href: occurrence.href, target: resolved });
316
+ found.push({ href: occurrence.href, target: occurrence.target });
267
317
  }
268
318
  return found;
269
319
  }
320
+ export function links(document, file) {
321
+ return linksIn(document.content.slice(bodyStartOf(document.content)), file);
322
+ }
323
+ export function linkOccurrences(document, file) {
324
+ const bodyStart = bodyStartOf(document.content);
325
+ return occurrencesIn(document.content.slice(bodyStart), file, bodyStart);
326
+ }
327
+ export function bodyLinks(body, file) {
328
+ return linksIn(body, file);
329
+ }
270
330
  export function apply(document, change) {
271
331
  if (change.kind === 'link')
272
332
  return applyLink(document, change.href, change.to);
@@ -274,21 +334,17 @@ export function apply(document, change) {
274
334
  return applyReference(document, change.pointer, change.value, change.prior);
275
335
  if (change.kind === 'preamble')
276
336
  return applyPreamble(document, change.value, change.prior);
277
- try {
278
- if (change.kind === 'field')
279
- return applyField(document, change.name, change.value, change.prior);
280
- return applySection(document, change.name, change.value, change.prior);
281
- }
282
- catch (error) {
283
- if (error instanceof YamlError) {
284
- return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', change.name) };
285
- }
286
- throw error;
287
- }
337
+ if (change.kind === 'field')
338
+ return applyField(document, change.name, change.value, change.prior);
339
+ return applySection(document, change.name, change.value, change.prior);
288
340
  }
289
341
  export function serialize(document) {
290
342
  return document.content;
291
343
  }
344
+ export function fieldStart(document, pointer) {
345
+ const root = internals.get(document)?.root;
346
+ return root === null || root === undefined ? undefined : nodeAt(root, pointer)?.start;
347
+ }
292
348
  export function frontmatter(document) {
293
349
  return internals.get(document)?.root ?? null;
294
350
  }