@agentshouse/mdmodel 0.1.0-alpha.3 → 0.1.0-alpha.4

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 (68) hide show
  1. package/README.md +36 -3
  2. package/dist/document.d.ts +2 -1
  3. package/dist/document.d.ts.map +1 -1
  4. package/dist/document.js +71 -1
  5. package/dist/document.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/links.d.ts +7 -1
  11. package/dist/links.d.ts.map +1 -1
  12. package/dist/links.js +259 -48
  13. package/dist/links.js.map +1 -1
  14. package/dist/paths.d.ts +1 -0
  15. package/dist/paths.d.ts.map +1 -1
  16. package/dist/paths.js +5 -0
  17. package/dist/paths.js.map +1 -1
  18. package/dist/types.d.ts +8 -0
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/types.js.map +1 -1
  21. package/dist/validate.js +3 -3
  22. package/dist/validate.js.map +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/dist/version.js.map +1 -1
  26. package/dist/yaml.d.ts +1 -0
  27. package/dist/yaml.d.ts.map +1 -1
  28. package/dist/yaml.js +5 -3
  29. package/dist/yaml.js.map +1 -1
  30. package/fixtures/README.md +4 -0
  31. package/fixtures/cases/bracketed-link-definition/context.json +1 -0
  32. package/fixtures/cases/bracketed-link-definition/expected.json +1 -0
  33. package/fixtures/cases/bracketed-link-definition/library/item.md +10 -0
  34. package/fixtures/cases/bracketed-link-definition/library/types/task.md +7 -0
  35. package/fixtures/cases/duplicate-link-definition/context.json +1 -0
  36. package/fixtures/cases/duplicate-link-definition/expected.json +1 -0
  37. package/fixtures/cases/duplicate-link-definition/library/item.md +11 -0
  38. package/fixtures/cases/duplicate-link-definition/library/types/task.md +7 -0
  39. package/fixtures/cases/fenced-link-definition/context.json +1 -0
  40. package/fixtures/cases/fenced-link-definition/expected.json +1 -0
  41. package/fixtures/cases/fenced-link-definition/library/item.md +12 -0
  42. package/fixtures/cases/fenced-link-definition/library/types/task.md +7 -0
  43. package/fixtures/cases/interrupted-link-definition/context.json +1 -0
  44. package/fixtures/cases/interrupted-link-definition/expected.json +1 -0
  45. package/fixtures/cases/interrupted-link-definition/library/notes/draft.md +8 -0
  46. package/fixtures/cases/interrupted-link-definition/library/types/note.md +3 -0
  47. package/fixtures/cases/long-fence-link-definition/context.json +1 -0
  48. package/fixtures/cases/long-fence-link-definition/expected.json +1 -0
  49. package/fixtures/cases/long-fence-link-definition/library/notes/draft.md +9 -0
  50. package/fixtures/cases/long-fence-link-definition/library/types/note.md +3 -0
  51. package/fixtures/cases/paragraph-link-definition/context.json +5 -0
  52. package/fixtures/cases/paragraph-link-definition/expected.json +1 -0
  53. package/fixtures/cases/paragraph-link-definition/library/notes/draft.md +10 -0
  54. package/fixtures/cases/paragraph-link-definition/library/types/note.md +3 -0
  55. package/fixtures/cases/quoted-link-definition/context.json +1 -0
  56. package/fixtures/cases/quoted-link-definition/expected.json +6 -0
  57. package/fixtures/cases/quoted-link-definition/library/notes/draft.md +7 -0
  58. package/fixtures/cases/quoted-link-definition/library/types/note.md +3 -0
  59. package/fixtures/cases/truncated-link-definition/context.json +1 -0
  60. package/fixtures/cases/truncated-link-definition/expected.json +6 -0
  61. package/fixtures/cases/truncated-link-definition/library/notes/draft.md +7 -0
  62. package/fixtures/cases/truncated-link-definition/library/notes/other.md +5 -0
  63. package/fixtures/cases/truncated-link-definition/library/types/note.md +3 -0
  64. package/fixtures/cases/wide-item-fence-link/context.json +1 -0
  65. package/fixtures/cases/wide-item-fence-link/expected.json +1 -0
  66. package/fixtures/cases/wide-item-fence-link/library/notes/draft.md +7 -0
  67. package/fixtures/cases/wide-item-fence-link/library/types/note.md +3 -0
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -23,6 +23,7 @@ Package releases are published after the User confirms.
23
23
  ```ts
24
24
  import {
25
25
  apply,
26
+ links,
26
27
  MARKDOWN_SYNTAX_PROFILE,
27
28
  parse,
28
29
  serialize,
@@ -47,6 +48,15 @@ if (parsed.ok) {
47
48
  prior: 'Hello',
48
49
  });
49
50
  if (next.outcome === 'applied') serialize(next.document);
51
+ for (const link of links(parsed.document, 'tasks/two.md')) {
52
+ if (link.target !== 'data/export.csv') continue;
53
+ const moved = apply(parsed.document, {
54
+ kind: 'link',
55
+ href: link.href,
56
+ to: '../files/export.csv',
57
+ });
58
+ if (moved.outcome === 'applied') serialize(moved.document);
59
+ }
50
60
  }
51
61
  ```
52
62
 
@@ -65,9 +75,32 @@ section, the violated rule, and `VERSION`. Every caller operation that validates
65
75
  a resulting collection supplies that collection's context to this same
66
76
  `validate` function. `parse`, `apply`, and `serialize` operate on a single text
67
77
  Document; they do not validate collection membership or consume binary bytes.
68
- `apply` sets one typed field, including a list field as a whole, or replaces
69
- one declared section body against that value's prior content. A field change
70
- does not reformat the rest of the file.
78
+ `apply` sets one typed field, including a list field as a whole, replaces
79
+ one declared section body against that value's prior content, or rewrites one
80
+ Markdown link destination. A field change does not reformat the rest of the file.
81
+
82
+ `links(document, file)` is the inventory of that Document's ordinary link and
83
+ image destinations that resolve to a collection Path, each with the destination
84
+ `href` exactly as written and the `target` Path it resolves to relative to
85
+ `file`. A `?query` or `#fragment` the destination carries is part of `href` and
86
+ not part of `target`. Entries are ordered by the position of the destination
87
+ text a rewrite would change, and one destination written the same way appears
88
+ once however many times it occurs. A destination that the syntax profile does not read as a
89
+ link, one that is empty, a bare fragment, or carries a URL scheme, and one that
90
+ leaves the collection are not in the inventory. Frontmatter field values,
91
+ including typed references, are not link destinations and are never enumerated.
92
+
93
+ `apply(document, { kind: 'link', href, to })` rewrites every body occurrence of
94
+ that written destination and changes nothing else, so a caller that moves a Path
95
+ rewrites its referring Documents without a second Markdown parser. `to` is the
96
+ Path the destination must resolve to instead; the `?query` or `#fragment` the
97
+ written destination carried is not part of that Path and is kept. A Document
98
+ that does not carry the destination is `stale`. A reference definition is
99
+ rewritten once however many uses name it. The destination is
100
+ written in the form the syntax profile reads back: bare when it carries no
101
+ whitespace, does not open with `<`, and balances its parentheses, otherwise
102
+ inside angle brackets, which the original form is kept in when it still fits. A
103
+ destination that neither form can carry is refused as `unwritable-link`.
71
104
 
72
105
  ## Fixtures
73
106
 
@@ -1,5 +1,6 @@
1
- import type { ApplyResult, Change, Document, ParseResult } from './types.js';
1
+ import type { ApplyResult, Change, Document, Link, ParseResult } from './types.js';
2
2
  export declare function parse(content: string, file?: string): ParseResult;
3
+ export declare function links(document: Document, file: string): readonly Link[];
3
4
  export declare function apply(document: Document, change: Change): ApplyResult;
4
5
  export declare function serialize(document: Document): string;
5
6
  //# sourceMappingURL=document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAmD7E,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,WAAW,CA0C7D;AAkDD,wBAAgB,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAWrE;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEpD"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAoDnF,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAK,GAAG,WAAW,CA0C7D;AAiGD,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,CAYrE;AAED,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEpD"}
package/dist/document.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { hrefOccurrences } from './links.js';
2
+ import { classifyHref, hrefSuffix } from './paths.js';
1
3
  import { headingSpans, sectionsRecord } from './sections.js';
2
4
  import { refusal } from './refuse.js';
3
- import { bodyStartAfterYaml, equalValue, extractFrontmatter, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError } from './yaml.js';
5
+ import { bodyStartAfterYaml, bodyStartOf, equalValue, extractFrontmatter, jsValue, lineEnding, mapEntry, parseYamlMapping, serializeJs, serializeKey, YamlError } from './yaml.js';
4
6
  const internals = new WeakMap();
5
7
  function refuse(file, rule, location) {
6
8
  return location === undefined ? refusal(file, rule) : refusal(file, rule, {
@@ -133,7 +135,75 @@ function applySection(document, name, value, prior) {
133
135
  document: parsed.document
134
136
  };
135
137
  }
138
+ function fitsBare(to) {
139
+ if (to === '' || to.startsWith('<')) return false;
140
+ let depth = 0;
141
+ for (const character of to){
142
+ if (/[\s\u0000-\u001f\u007f]/.test(character)) return false;
143
+ if (character === '(') depth += 1;
144
+ else if (character === ')') {
145
+ if (depth === 0) return false;
146
+ depth -= 1;
147
+ }
148
+ }
149
+ return depth === 0;
150
+ }
151
+ function fitsBrackets(to) {
152
+ return to !== '' && !/[<>\n\r]/.test(to);
153
+ }
154
+ function destinationText(to, bracketed) {
155
+ if (fitsBare(to)) return bracketed && fitsBrackets(to) ? `<${to}>` : to;
156
+ if (fitsBrackets(to)) return `<${to}>`;
157
+ return null;
158
+ }
159
+ function applyLink(document, href, to) {
160
+ const content = document.content;
161
+ const bodyStart = bodyStartOf(content);
162
+ const matches = hrefOccurrences(content.slice(bodyStart)).filter((occurrence)=>occurrence.href === href);
163
+ if (matches.length === 0) return {
164
+ outcome: 'stale',
165
+ document
166
+ };
167
+ const written = to + hrefSuffix(href);
168
+ let rewritten = '';
169
+ let cursor = 0;
170
+ for (const match of matches){
171
+ const text = destinationText(written, match.bracketed);
172
+ if (text === null) return {
173
+ outcome: 'refuse',
174
+ refusal: refuse('', 'unwritable-link', written)
175
+ };
176
+ rewritten += content.slice(cursor, bodyStart + match.start);
177
+ rewritten += text;
178
+ cursor = bodyStart + match.end;
179
+ }
180
+ const parsed = parse(rewritten + content.slice(cursor));
181
+ if (!parsed.ok) return {
182
+ outcome: 'refuse',
183
+ refusal: parsed.refusal
184
+ };
185
+ return {
186
+ outcome: 'applied',
187
+ document: parsed.document
188
+ };
189
+ }
190
+ export function links(document, file) {
191
+ const found = [];
192
+ const seen = new Set();
193
+ for (const occurrence of hrefOccurrences(document.content.slice(bodyStartOf(document.content)))){
194
+ if (seen.has(occurrence.href)) continue;
195
+ const resolved = classifyHref(file, occurrence.href);
196
+ if (resolved.kind !== 'path') continue;
197
+ seen.add(occurrence.href);
198
+ found.push({
199
+ href: occurrence.href,
200
+ target: resolved.path
201
+ });
202
+ }
203
+ return found;
204
+ }
136
205
  export function apply(document, change) {
206
+ if (change.kind === 'link') return applyLink(document, change.href, change.to);
137
207
  try {
138
208
  if (change.kind === 'field') return applyField(document, change.name, change.value, change.prior);
139
209
  return applySection(document, change.name, change.value, change.prior);
@@ -1 +1 @@
1
- {"version":3,"sources":["/workspace/packages/mdmodel/src/document.ts"],"sourcesContent":["import { headingSpans, sectionsRecord } from './sections.js';\nimport { refusal } from './refuse.js';\nimport type { ApplyResult, Change, Document, ParseResult } from './types.js';\nimport {\n bodyStartAfterYaml,\n equalValue,\n extractFrontmatter,\n jsValue,\n lineEnding,\n mapEntry,\n parseYamlMapping,\n serializeJs,\n serializeKey,\n YamlError,\n type YamlMap,\n} from './yaml.js';\n\ntype Internal = {\n readonly root: YamlMap | null;\n readonly yamlEnd: number;\n readonly bodyStart: number;\n};\n\nconst internals = new WeakMap<Document, Internal>();\n\nfunction refuse(file: string, rule: string, location?: string) {\n return location === undefined ? refusal(file, rule) : refusal(file, rule, { location });\n}\n\nfunction freezeDocument(\n content: string,\n type: string | undefined,\n fields: Record<string, unknown>,\n sections: Record<string, string>,\n internal: Internal,\n): Document {\n const document: Document = Object.freeze({\n content,\n type,\n fields: Object.freeze(fields),\n sections: Object.freeze(sections),\n });\n internals.set(document, internal);\n return document;\n}\n\nfunction declaredType(fields: Record<string, unknown>): string | undefined {\n const value = fields.type;\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'string' || value === '') return undefined;\n return value;\n}\n\nexport function parse(content: string, file = ''): ParseResult {\n const fence = extractFrontmatter(content);\n if (fence.kind === 'none') {\n const spans = headingSpans(content, 0);\n return {\n ok: true,\n document: freezeDocument(content, undefined, {}, sectionsRecord(spans, content), {\n root: null,\n yamlEnd: 0,\n bodyStart: 0,\n }),\n };\n }\n if (fence.kind === 'unclosed') return { ok: false, refusal: refuse(file, 'frontmatter-fence') };\n try {\n const root = parseYamlMapping(content, fence.yamlStart, fence.yamlEnd);\n const fields = jsValue(root);\n if (fields === null || typeof fields !== 'object' || Array.isArray(fields)) {\n return { ok: false, refusal: refuse(file, 'frontmatter-fence') };\n }\n const bodyStart = bodyStartAfterYaml(content, fence.yamlEnd);\n const spans = headingSpans(content, bodyStart);\n const record = fields as Record<string, unknown>;\n return {\n ok: true,\n document: freezeDocument(\n content,\n declaredType(record),\n record,\n sectionsRecord(spans, content),\n {\n root,\n yamlEnd: fence.yamlEnd,\n bodyStart,\n },\n ),\n };\n } catch (error) {\n if (error instanceof YamlError)\n return { ok: false, refusal: refuse(file, 'frontmatter-fence') };\n throw error;\n }\n}\n\nfunction splice(content: string, start: number, end: number, insert: string): string {\n return content.slice(0, start) + insert + content.slice(end);\n}\n\nfunction applyField(document: Document, name: string, value: unknown, prior: unknown): ApplyResult {\n const internal = internals.get(document);\n if (internal === undefined || internal.root === null) {\n return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };\n }\n if (!equalValue(document.fields[name], prior)) {\n return { outcome: 'stale', document };\n }\n let nextContent: string;\n const existing = mapEntry(internal.root, name);\n if (existing !== undefined) {\n nextContent = splice(\n document.content,\n existing.value.start,\n existing.value.end,\n serializeJs(value),\n );\n } else {\n const nl = lineEnding(document.content);\n const line = `${serializeKey(name)}: ${serializeJs(value)}${nl}`;\n const before = document.content.slice(0, internal.yamlEnd);\n const prefix = before.endsWith('\\n') || before.endsWith('\\r\\n') ? '' : nl;\n nextContent = splice(document.content, internal.yamlEnd, internal.yamlEnd, `${prefix}${line}`);\n }\n const parsed = parse(nextContent);\n if (!parsed.ok) return { outcome: 'refuse', refusal: parsed.refusal };\n return { outcome: 'applied', document: parsed.document };\n}\n\nfunction applySection(document: Document, name: string, value: string, prior: string): ApplyResult {\n const internal = internals.get(document);\n if (internal === undefined)\n return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };\n const spans = headingSpans(document.content, internal.bodyStart);\n const span = spans.find((item) => item.name === name);\n if (span === undefined)\n return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };\n const current = document.content.slice(span.bodyStart, span.bodyEnd);\n if (current !== prior) return { outcome: 'stale', document };\n const parsed = parse(splice(document.content, span.bodyStart, span.bodyEnd, value));\n if (!parsed.ok) return { outcome: 'refuse', refusal: parsed.refusal };\n return { outcome: 'applied', document: parsed.document };\n}\n\nexport function apply(document: Document, change: Change): ApplyResult {\n try {\n if (change.kind === 'field')\n return applyField(document, change.name, change.value, change.prior);\n return applySection(document, change.name, change.value, change.prior);\n } catch (error) {\n if (error instanceof YamlError) {\n return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', change.name) };\n }\n throw error;\n }\n}\n\nexport function serialize(document: Document): string {\n return document.content;\n}\n"],"names":["headingSpans","sectionsRecord","refusal","bodyStartAfterYaml","equalValue","extractFrontmatter","jsValue","lineEnding","mapEntry","parseYamlMapping","serializeJs","serializeKey","YamlError","internals","WeakMap","refuse","file","rule","location","undefined","freezeDocument","content","type","fields","sections","internal","document","Object","freeze","set","declaredType","value","parse","fence","kind","spans","ok","root","yamlEnd","bodyStart","yamlStart","Array","isArray","record","error","splice","start","end","insert","slice","applyField","name","prior","get","outcome","nextContent","existing","nl","line","before","prefix","endsWith","parsed","applySection","span","find","item","current","bodyEnd","apply","change","serialize"],"mappings":"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"}
1
+ {"version":3,"sources":["/workspace/packages/mdmodel/src/document.ts"],"sourcesContent":["import { hrefOccurrences } from './links.js';\nimport { classifyHref, hrefSuffix } from './paths.js';\nimport { headingSpans, sectionsRecord } from './sections.js';\nimport { refusal } from './refuse.js';\nimport type { ApplyResult, Change, Document, Link, ParseResult } from './types.js';\nimport {\n bodyStartAfterYaml,\n bodyStartOf,\n equalValue,\n extractFrontmatter,\n jsValue,\n lineEnding,\n mapEntry,\n parseYamlMapping,\n serializeJs,\n serializeKey,\n YamlError,\n type YamlMap,\n} from './yaml.js';\n\ntype Internal = {\n readonly root: YamlMap | null;\n readonly yamlEnd: number;\n readonly bodyStart: number;\n};\n\nconst internals = new WeakMap<Document, Internal>();\n\nfunction refuse(file: string, rule: string, location?: string) {\n return location === undefined ? refusal(file, rule) : refusal(file, rule, { location });\n}\n\nfunction freezeDocument(\n content: string,\n type: string | undefined,\n fields: Record<string, unknown>,\n sections: Record<string, string>,\n internal: Internal,\n): Document {\n const document: Document = Object.freeze({\n content,\n type,\n fields: Object.freeze(fields),\n sections: Object.freeze(sections),\n });\n internals.set(document, internal);\n return document;\n}\n\nfunction declaredType(fields: Record<string, unknown>): string | undefined {\n const value = fields.type;\n if (value === undefined || value === null) return undefined;\n if (typeof value !== 'string' || value === '') return undefined;\n return value;\n}\n\nexport function parse(content: string, file = ''): ParseResult {\n const fence = extractFrontmatter(content);\n if (fence.kind === 'none') {\n const spans = headingSpans(content, 0);\n return {\n ok: true,\n document: freezeDocument(content, undefined, {}, sectionsRecord(spans, content), {\n root: null,\n yamlEnd: 0,\n bodyStart: 0,\n }),\n };\n }\n if (fence.kind === 'unclosed') return { ok: false, refusal: refuse(file, 'frontmatter-fence') };\n try {\n const root = parseYamlMapping(content, fence.yamlStart, fence.yamlEnd);\n const fields = jsValue(root);\n if (fields === null || typeof fields !== 'object' || Array.isArray(fields)) {\n return { ok: false, refusal: refuse(file, 'frontmatter-fence') };\n }\n const bodyStart = bodyStartAfterYaml(content, fence.yamlEnd);\n const spans = headingSpans(content, bodyStart);\n const record = fields as Record<string, unknown>;\n return {\n ok: true,\n document: freezeDocument(\n content,\n declaredType(record),\n record,\n sectionsRecord(spans, content),\n {\n root,\n yamlEnd: fence.yamlEnd,\n bodyStart,\n },\n ),\n };\n } catch (error) {\n if (error instanceof YamlError)\n return { ok: false, refusal: refuse(file, 'frontmatter-fence') };\n throw error;\n }\n}\n\nfunction splice(content: string, start: number, end: number, insert: string): string {\n return content.slice(0, start) + insert + content.slice(end);\n}\n\nfunction applyField(document: Document, name: string, value: unknown, prior: unknown): ApplyResult {\n const internal = internals.get(document);\n if (internal === undefined || internal.root === null) {\n return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', name) };\n }\n if (!equalValue(document.fields[name], prior)) {\n return { outcome: 'stale', document };\n }\n let nextContent: string;\n const existing = mapEntry(internal.root, name);\n if (existing !== undefined) {\n nextContent = splice(\n document.content,\n existing.value.start,\n existing.value.end,\n serializeJs(value),\n );\n } else {\n const nl = lineEnding(document.content);\n const line = `${serializeKey(name)}: ${serializeJs(value)}${nl}`;\n const before = document.content.slice(0, internal.yamlEnd);\n const prefix = before.endsWith('\\n') || before.endsWith('\\r\\n') ? '' : nl;\n nextContent = splice(document.content, internal.yamlEnd, internal.yamlEnd, `${prefix}${line}`);\n }\n const parsed = parse(nextContent);\n if (!parsed.ok) return { outcome: 'refuse', refusal: parsed.refusal };\n return { outcome: 'applied', document: parsed.document };\n}\n\nfunction applySection(document: Document, name: string, value: string, prior: string): ApplyResult {\n const internal = internals.get(document);\n if (internal === undefined)\n return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };\n const spans = headingSpans(document.content, internal.bodyStart);\n const span = spans.find((item) => item.name === name);\n if (span === undefined)\n return { outcome: 'refuse', refusal: refuse('', 'required-section', name) };\n const current = document.content.slice(span.bodyStart, span.bodyEnd);\n if (current !== prior) return { outcome: 'stale', document };\n const parsed = parse(splice(document.content, span.bodyStart, span.bodyEnd, value));\n if (!parsed.ok) return { outcome: 'refuse', refusal: parsed.refusal };\n return { outcome: 'applied', document: parsed.document };\n}\n\nfunction fitsBare(to: string): boolean {\n if (to === '' || to.startsWith('<')) return false;\n let depth = 0;\n for (const character of to) {\n if (/[\\s\\u0000-\\u001f\\u007f]/.test(character)) return false;\n if (character === '(') depth += 1;\n else if (character === ')') {\n if (depth === 0) return false;\n depth -= 1;\n }\n }\n return depth === 0;\n}\n\nfunction fitsBrackets(to: string): boolean {\n return to !== '' && !/[<>\\n\\r]/.test(to);\n}\n\nfunction destinationText(to: string, bracketed: boolean): string | null {\n if (fitsBare(to)) return bracketed && fitsBrackets(to) ? `<${to}>` : to;\n if (fitsBrackets(to)) return `<${to}>`;\n return null;\n}\n\nfunction applyLink(document: Document, href: string, to: string): ApplyResult {\n const content = document.content;\n const bodyStart = bodyStartOf(content);\n const matches = hrefOccurrences(content.slice(bodyStart)).filter(\n (occurrence) => occurrence.href === href,\n );\n if (matches.length === 0) return { outcome: 'stale', document };\n const written = to + hrefSuffix(href);\n let rewritten = '';\n let cursor = 0;\n for (const match of matches) {\n const text = destinationText(written, match.bracketed);\n if (text === null)\n return { outcome: 'refuse', refusal: refuse('', 'unwritable-link', written) };\n rewritten += content.slice(cursor, bodyStart + match.start);\n rewritten += text;\n cursor = bodyStart + match.end;\n }\n const parsed = parse(rewritten + content.slice(cursor));\n if (!parsed.ok) return { outcome: 'refuse', refusal: parsed.refusal };\n return { outcome: 'applied', document: parsed.document };\n}\n\nexport function links(document: Document, file: string): readonly Link[] {\n const found: Link[] = [];\n const seen = new Set<string>();\n for (const occurrence of hrefOccurrences(document.content.slice(bodyStartOf(document.content)))) {\n if (seen.has(occurrence.href)) continue;\n const resolved = classifyHref(file, occurrence.href);\n if (resolved.kind !== 'path') continue;\n seen.add(occurrence.href);\n found.push({ href: occurrence.href, target: resolved.path });\n }\n return found;\n}\n\nexport function apply(document: Document, change: Change): ApplyResult {\n if (change.kind === 'link') return applyLink(document, change.href, change.to);\n try {\n if (change.kind === 'field')\n return applyField(document, change.name, change.value, change.prior);\n return applySection(document, change.name, change.value, change.prior);\n } catch (error) {\n if (error instanceof YamlError) {\n return { outcome: 'refuse', refusal: refuse('', 'frontmatter-fence', change.name) };\n }\n throw error;\n }\n}\n\nexport function serialize(document: Document): string {\n return document.content;\n}\n"],"names":["hrefOccurrences","classifyHref","hrefSuffix","headingSpans","sectionsRecord","refusal","bodyStartAfterYaml","bodyStartOf","equalValue","extractFrontmatter","jsValue","lineEnding","mapEntry","parseYamlMapping","serializeJs","serializeKey","YamlError","internals","WeakMap","refuse","file","rule","location","undefined","freezeDocument","content","type","fields","sections","internal","document","Object","freeze","set","declaredType","value","parse","fence","kind","spans","ok","root","yamlEnd","bodyStart","yamlStart","Array","isArray","record","error","splice","start","end","insert","slice","applyField","name","prior","get","outcome","nextContent","existing","nl","line","before","prefix","endsWith","parsed","applySection","span","find","item","current","bodyEnd","fitsBare","to","startsWith","depth","character","test","fitsBrackets","destinationText","bracketed","applyLink","href","matches","filter","occurrence","length","written","rewritten","cursor","match","text","links","found","seen","Set","has","resolved","add","push","target","path","apply","change","serialize"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { apply, parse, serialize } from './document.js';
1
+ export { apply, links, parse, serialize } from './document.js';
2
2
  export { MARKDOWN_SYNTAX_PROFILE } from './profile.js';
3
- export type { ApplyResult, Change, Document, Library, ParseResult, Refusal, Validation, ValidationContext, } from './types.js';
3
+ export type { ApplyResult, Change, Document, Library, Link, ParseResult, Refusal, Validation, ValidationContext, } from './types.js';
4
4
  export { validate } from './validate.js';
5
5
  export { VERSION } from './version.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,WAAW,EACX,OAAO,EACP,UAAU,EACV,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,WAAW,EACX,OAAO,EACP,UAAU,EACV,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { apply, parse, serialize } from './document.js';
1
+ export { apply, links, parse, serialize } from './document.js';
2
2
  export { MARKDOWN_SYNTAX_PROFILE } from './profile.js';
3
3
  export { validate } from './validate.js';
4
4
  export { VERSION } from './version.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["/workspace/packages/mdmodel/src/index.ts"],"sourcesContent":["export { apply, parse, serialize } from './document.js';\nexport { MARKDOWN_SYNTAX_PROFILE } from './profile.js';\nexport type {\n ApplyResult,\n Change,\n Document,\n Library,\n ParseResult,\n Refusal,\n Validation,\n ValidationContext,\n} from './types.js';\nexport { validate } from './validate.js';\nexport { VERSION } from './version.js';\n"],"names":["apply","parse","serialize","MARKDOWN_SYNTAX_PROFILE","validate","VERSION"],"mappings":"AAAA,SAASA,KAAK,EAAEC,KAAK,EAAEC,SAAS,QAAQ,gBAAgB;AACxD,SAASC,uBAAuB,QAAQ,eAAe;AAWvD,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,OAAO,QAAQ,eAAe"}
1
+ {"version":3,"sources":["/workspace/packages/mdmodel/src/index.ts"],"sourcesContent":["export { apply, links, parse, serialize } from './document.js';\nexport { MARKDOWN_SYNTAX_PROFILE } from './profile.js';\nexport type {\n ApplyResult,\n Change,\n Document,\n Library,\n Link,\n ParseResult,\n Refusal,\n Validation,\n ValidationContext,\n} from './types.js';\nexport { validate } from './validate.js';\nexport { VERSION } from './version.js';\n"],"names":["apply","links","parse","serialize","MARKDOWN_SYNTAX_PROFILE","validate","VERSION"],"mappings":"AAAA,SAASA,KAAK,EAAEC,KAAK,EAAEC,KAAK,EAAEC,SAAS,QAAQ,gBAAgB;AAC/D,SAASC,uBAAuB,QAAQ,eAAe;AAYvD,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,OAAO,QAAQ,eAAe"}
package/dist/links.d.ts CHANGED
@@ -1,2 +1,8 @@
1
- export declare function extractRelativeHrefs(body: string): string[];
1
+ export type HrefOccurrence = {
2
+ readonly href: string;
3
+ readonly start: number;
4
+ readonly end: number;
5
+ readonly bracketed: boolean;
6
+ };
7
+ export declare function hrefOccurrences(body: string): readonly HrefOccurrence[];
2
8
  //# sourceMappingURL=links.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAoHA,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA4B3D"}
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AA4VA,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,cAAc,EAAE,CA2BvE"}
package/dist/links.js CHANGED
@@ -1,17 +1,127 @@
1
- function atLineStart(text, index) {
2
- if (index === 0) return true;
3
- const prev = text[index - 1];
4
- return prev === '\n' || prev === '\r';
1
+ const TAB_STOP = 4;
2
+ const CLOSING_FENCE_INDENT = 3;
3
+ const HEADING = /^#{1,6}(?:[ \t]|$)/;
4
+ const BREAK = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
5
+ const SETEXT = /^(?:=+|-+)[ \t]*$/;
6
+ const LABEL = /^\[((?:[^\\\]\n]|\\.)+)\]:[ \t]*/;
7
+ const DESTINATION = /^(?:(<[^<>\n\r]*>)|([^\s<][^\s]*))/d;
8
+ const TITLE = /^(?:"[^"\n]*"|'[^'\n]*'|\([^()\n]*\))[ \t]*$/;
9
+ function endOfLine(text, from) {
10
+ const nl = text.indexOf('\n', from);
11
+ return nl === -1 ? text.length : nl;
5
12
  }
6
- function skipFence(text, index) {
13
+ function afterLine(text, end) {
14
+ return end < text.length ? end + 1 : text.length;
15
+ }
16
+ function itemMarker(text, index, end) {
7
17
  let i = index;
8
- while(i < text.length && (text[i] === ' ' || text[i] === '\t'))i += 1;
9
- const marker = text.startsWith('```', i) ? '```' : text.startsWith('~~~', i) ? '~~~' : null;
10
- if (marker === null) return index + 1;
11
- i += marker.length;
12
- const close = text.indexOf(marker, i);
13
- if (close === -1) return text.length;
14
- return close + marker.length;
18
+ const bullet = text[i];
19
+ if (bullet === '-' || bullet === '*' || bullet === '+') i += 1;
20
+ else {
21
+ let digits = 0;
22
+ while(digits < 9 && i + digits < end && text[i + digits] >= '0' && text[i + digits] <= '9')digits += 1;
23
+ const delimiter = text[i + digits];
24
+ if (digits === 0 || delimiter !== '.' && delimiter !== ')') return null;
25
+ i += digits + 1;
26
+ }
27
+ if (i >= end) return i;
28
+ if (text[i] !== ' ' && text[i] !== '\t') return null;
29
+ return i + 1;
30
+ }
31
+ function readLine(text, start, end) {
32
+ let i = start;
33
+ let depth = 0;
34
+ let item = false;
35
+ let inner = start;
36
+ for(;;){
37
+ let spaces = 0;
38
+ while(spaces < 3 && i < end && text[i] === ' '){
39
+ i += 1;
40
+ spaces += 1;
41
+ }
42
+ if (i < end && text[i] === '>') {
43
+ depth += 1;
44
+ i += 1;
45
+ if (i < end && text[i] === ' ') i += 1;
46
+ inner = i;
47
+ continue;
48
+ }
49
+ const after = i < end ? itemMarker(text, i, end) : null;
50
+ if (after === null) break;
51
+ item = true;
52
+ i = after;
53
+ }
54
+ return {
55
+ start,
56
+ end,
57
+ content: i,
58
+ depth,
59
+ item,
60
+ inner
61
+ };
62
+ }
63
+ function widthOf(text, from, to) {
64
+ let width = 0;
65
+ for(let i = from; i < to; i += 1)width += text[i] === '\t' ? TAB_STOP - width % TAB_STOP : 1;
66
+ return width;
67
+ }
68
+ function afterIndent(text, line) {
69
+ let i = line.inner;
70
+ while(i < line.end && (text[i] === ' ' || text[i] === '\t'))i += 1;
71
+ return i;
72
+ }
73
+ function indentWidth(text, line) {
74
+ return widthOf(text, line.inner, afterIndent(text, line));
75
+ }
76
+ function lineText(text, line) {
77
+ const raw = text.slice(line.content, line.end);
78
+ return raw.endsWith('\r') ? raw.slice(0, -1) : raw;
79
+ }
80
+ function fenceRun(text, at, end) {
81
+ const char = text[at];
82
+ if (char !== '`' && char !== '~') return null;
83
+ let length = 0;
84
+ while(at + length < end && text[at + length] === char)length += 1;
85
+ return length < 3 ? null : {
86
+ char,
87
+ length
88
+ };
89
+ }
90
+ function fenceAt(text, line) {
91
+ const run = fenceRun(text, line.content, line.end);
92
+ if (run === null) return null;
93
+ return {
94
+ char: run.char,
95
+ length: run.length,
96
+ depth: line.depth,
97
+ column: line.item ? widthOf(text, line.inner, line.content) : 0,
98
+ item: line.item
99
+ };
100
+ }
101
+ function closesFence(text, line, open) {
102
+ if (line.depth !== open.depth || line.item) return false;
103
+ const indent = indentWidth(text, line);
104
+ if (indent < open.column || indent > open.column + CLOSING_FENCE_INDENT) return false;
105
+ const at = afterIndent(text, line);
106
+ const run = fenceRun(text, at, line.end);
107
+ if (run === null || run.char !== open.char || run.length < open.length) return false;
108
+ return text.slice(at + run.length, line.end).trim() === '';
109
+ }
110
+ function leavesContainer(text, line, open) {
111
+ if (line.depth < open.depth) return true;
112
+ if (!open.item || lineText(text, line).trim() === '') return false;
113
+ return indentWidth(text, line) < open.column;
114
+ }
115
+ function fenceSpan(text, line, open) {
116
+ let cursor = afterLine(text, line.end);
117
+ while(cursor < text.length){
118
+ const end = endOfLine(text, cursor);
119
+ const next = readLine(text, cursor, end);
120
+ if (leavesContainer(text, next, open)) return cursor;
121
+ if (closesFence(text, next, open)) return afterLine(text, end);
122
+ cursor = afterLine(text, end);
123
+ }
124
+ return text.length;
15
125
  }
16
126
  function skipInlineCode(text, index) {
17
127
  let ticks = 0;
@@ -40,11 +150,16 @@ function skipTitle(text, start) {
40
150
  }
41
151
  function destination(text, start) {
42
152
  let i = skipLinkSpace(text, start + 1);
43
- let href;
153
+ let occurrence;
44
154
  if (text[i] === '<') {
45
155
  const close = text.indexOf('>', i + 1);
46
156
  if (close === -1) return null;
47
- href = text.slice(i + 1, close);
157
+ occurrence = {
158
+ href: text.slice(i + 1, close),
159
+ start: i,
160
+ end: close + 1,
161
+ bracketed: true
162
+ };
48
163
  i = close + 1;
49
164
  } else {
50
165
  const from = i;
@@ -59,7 +174,12 @@ function destination(text, start) {
59
174
  }
60
175
  i += 1;
61
176
  }
62
- href = text.slice(from, i);
177
+ occurrence = {
178
+ href: text.slice(from, i),
179
+ start: from,
180
+ end: i,
181
+ bracketed: false
182
+ };
63
183
  }
64
184
  i = skipLinkSpace(text, i);
65
185
  const afterTitle = skipTitle(text, i);
@@ -67,76 +187,167 @@ function destination(text, start) {
67
187
  i = skipLinkSpace(text, afterTitle);
68
188
  if (text[i] !== ')') return null;
69
189
  return {
70
- href,
190
+ occurrence,
71
191
  end: i + 1
72
192
  };
73
193
  }
74
- function takeLink(text, index, hrefs, defs) {
75
- let i = index + 1;
194
+ function labelEnd(text, start) {
195
+ let i = start;
76
196
  let depth = 1;
77
197
  while(i < text.length && depth > 0){
78
198
  const ch = text[i];
199
+ if (ch === '\\') {
200
+ i += 2;
201
+ continue;
202
+ }
79
203
  if (ch === '[') depth += 1;
80
204
  else if (ch === ']') depth -= 1;
81
- else if (ch === '\n' || ch === '\r') return index + 1;
205
+ else if (ch === '\n' || ch === '\r') return -1;
82
206
  i += 1;
83
207
  }
84
- if (depth !== 0) return index + 1;
85
- const label = text.slice(index + 1, i - 1);
208
+ return depth === 0 ? i : -1;
209
+ }
210
+ function takeLink(text, index, found, defs) {
211
+ const end = labelEnd(text, index + 1);
212
+ if (end === -1) return index + 1;
213
+ const label = text.slice(index + 1, end - 1);
214
+ let i = end;
86
215
  if (text[i] === '(') {
87
216
  const dest = destination(text, i);
88
217
  if (dest === null) return index + 1;
89
- hrefs.push(dest.href);
218
+ found.push(dest.occurrence);
90
219
  return dest.end;
91
220
  }
92
221
  if (text[i] === '[') {
93
- const close = text.indexOf(']', i + 1);
222
+ const close = labelEnd(text, i + 1);
94
223
  if (close === -1) return index + 1;
95
- const id = (text.slice(i + 1, close) || label).trim().toLowerCase();
96
- const href = defs.get(id);
97
- if (href !== undefined) hrefs.push(href);
98
- return close + 1;
224
+ const id = (text.slice(i + 1, close - 1) || label).trim().toLowerCase();
225
+ const defined = defs.get(id);
226
+ if (defined !== undefined) found.push(defined);
227
+ return close;
99
228
  }
100
229
  return i;
101
230
  }
102
- function collectDefinitions(text) {
103
- const defs = new Map();
104
- const pattern = /^ {0,3}\[([^\]]+)\]:[ \t]+<?([^\s>]+)>?/gm;
105
- for (const match of text.matchAll(pattern)){
106
- const id = match[1]?.trim().toLowerCase();
107
- const href = match[2];
108
- if (id !== undefined && href !== undefined) defs.set(id, href);
231
+ function definitionDestination(text, from, to) {
232
+ let i = from;
233
+ while(i < to && (text[i] === ' ' || text[i] === '\t'))i += 1;
234
+ const match = DESTINATION.exec(text.slice(i, to));
235
+ if (match === null) return null;
236
+ const bracketed = match[1];
237
+ const span = match.indices?.[1] ?? match.indices?.[2];
238
+ const href = bracketed === undefined ? match[2] : bracketed.slice(1, -1);
239
+ if (href === undefined || span === undefined) return null;
240
+ return {
241
+ href,
242
+ start: i + span[0],
243
+ end: i + span[1],
244
+ bracketed: bracketed !== undefined
245
+ };
246
+ }
247
+ function definitionAt(text, line) {
248
+ const head = LABEL.exec(lineText(text, line));
249
+ if (head === null) return null;
250
+ const id = head[1]?.trim().toLowerCase();
251
+ if (id === undefined || id === '') return null;
252
+ const from = line.content + head[0].length;
253
+ let occurrence = definitionDestination(text, from, line.end);
254
+ let next = afterLine(text, line.end);
255
+ if (occurrence === null) {
256
+ if (text.slice(from, line.end).trim() !== '' || next >= text.length) return null;
257
+ const end = endOfLine(text, next);
258
+ occurrence = definitionDestination(text, readLine(text, next, end).content, end);
259
+ if (occurrence === null) return null;
260
+ next = afterLine(text, end);
261
+ }
262
+ if (next < text.length) {
263
+ const end = endOfLine(text, next);
264
+ if (TITLE.test(lineText(text, readLine(text, next, end)))) next = afterLine(text, end);
265
+ }
266
+ return {
267
+ id,
268
+ occurrence,
269
+ next
270
+ };
271
+ }
272
+ function endsBlock(content) {
273
+ return HEADING.test(content) || BREAK.test(content) || SETEXT.test(content);
274
+ }
275
+ function scanBlocks(text) {
276
+ const fences = [];
277
+ const definitions = new Map();
278
+ let paragraph = false;
279
+ let depth = 0;
280
+ let i = 0;
281
+ while(i < text.length){
282
+ const line = readLine(text, i, endOfLine(text, i));
283
+ if (line.depth !== depth || line.item) paragraph = false;
284
+ depth = line.depth;
285
+ const open = fenceAt(text, line);
286
+ if (open !== null) {
287
+ const stop = fenceSpan(text, line, open);
288
+ fences.push([
289
+ i,
290
+ stop
291
+ ]);
292
+ paragraph = false;
293
+ i = stop;
294
+ continue;
295
+ }
296
+ const content = lineText(text, line);
297
+ if (content.trim() === '') {
298
+ paragraph = false;
299
+ i = afterLine(text, line.end);
300
+ continue;
301
+ }
302
+ const definition = paragraph ? null : definitionAt(text, line);
303
+ if (definition !== null) {
304
+ if (!definitions.has(definition.id)) definitions.set(definition.id, definition.occurrence);
305
+ i = definition.next;
306
+ continue;
307
+ }
308
+ paragraph = !endsBlock(content);
309
+ i = afterLine(text, line.end);
109
310
  }
110
- return defs;
311
+ return {
312
+ fences,
313
+ definitions
314
+ };
111
315
  }
112
- export function extractRelativeHrefs(body) {
113
- const defs = collectDefinitions(body);
114
- const hrefs = [];
316
+ function fenceEndAt(spans, index) {
317
+ for (const [start, end] of spans){
318
+ if (index >= start && index < end) return end;
319
+ }
320
+ return undefined;
321
+ }
322
+ export function hrefOccurrences(body) {
323
+ const { fences, definitions } = scanBlocks(body);
324
+ const found = [];
115
325
  let i = 0;
116
326
  while(i < body.length){
117
- if (atLineStart(body, i) || i > 0 && (body[i - 1] === '\n' || body[i - 1] === '\r')) {
118
- let j = i;
119
- while(body[j] === ' ' && j - i < 3)j += 1;
120
- if (body.startsWith('```', j) || body.startsWith('~~~', j)) {
121
- i = skipFence(body, i);
122
- continue;
123
- }
327
+ const fenceEnd = fenceEndAt(fences, i);
328
+ if (fenceEnd !== undefined) {
329
+ i = fenceEnd;
330
+ continue;
124
331
  }
125
332
  if (body[i] === '`') {
126
333
  i = skipInlineCode(body, i);
127
334
  continue;
128
335
  }
129
336
  if (body[i] === '!' && body[i + 1] === '[') {
130
- i = takeLink(body, i + 1, hrefs, defs);
337
+ i = takeLink(body, i + 1, found, definitions);
131
338
  continue;
132
339
  }
133
340
  if (body[i] === '[') {
134
- i = takeLink(body, i, hrefs, defs);
341
+ i = takeLink(body, i, found, definitions);
135
342
  continue;
136
343
  }
137
344
  i += 1;
138
345
  }
139
- return hrefs;
346
+ const byStart = new Map();
347
+ for (const occurrence of found)byStart.set(occurrence.start, occurrence);
348
+ return [
349
+ ...byStart.values()
350
+ ].sort((left, right)=>left.start - right.start);
140
351
  }
141
352
 
142
353
  //# sourceMappingURL=links.js.map