@inshapardaz/likhari-converters 0.1.16 → 0.1.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/html/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAiCjD,kEAAkE;AAClE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIlD;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKvD;AA6RD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAMvF"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/html/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAiCjD,kEAAkE;AAClE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIlD;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKvD;AAoTD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAMvF"}
@@ -92,6 +92,10 @@ function collectInline(node, state, out, preserveWs) {
92
92
  out.push({ type: 'linebreak', version: 1 });
93
93
  return;
94
94
  }
95
+ if (tag === 'sup' && el.hasAttribute('data-likhari-footnote-ref')) {
96
+ out.push({ type: 'footnote-reference', version: 1, footnoteId: el.getAttribute('data-likhari-footnote-ref') ?? '' });
97
+ return;
98
+ }
95
99
  if (tag === 'a') {
96
100
  const url = safeUrl(el.getAttribute('href') ?? '');
97
101
  const children = [];
@@ -247,6 +251,26 @@ function convertLayout(container) {
247
251
  const templateColumns = container.style.gridTemplateColumns || `repeat(${Math.max(items.length, 2)}, 1fr)`;
248
252
  return elementBase('layout-container', items.length > 0 ? items : [elementBase('layout-item', [paragraph()])], { templateColumns });
249
253
  }
254
+ function convertFootnoteList(el) {
255
+ const items = [];
256
+ Array.from(el.children).forEach((child) => {
257
+ if (child.tagName.toLowerCase() !== 'li')
258
+ return;
259
+ const li = child;
260
+ const footnoteId = li.getAttribute('data-likhari-footnote-item') ?? (li.getAttribute('id') ?? '').replace(/^fn-/, '');
261
+ if (!footnoteId)
262
+ return;
263
+ // Drop the "↩" backlink our own serializer appends — it's a generated
264
+ // affordance, not part of the note's content.
265
+ const clone = li.cloneNode(true);
266
+ Array.from(clone.querySelectorAll('a'))
267
+ .filter((a) => a.getAttribute('href') === `#fnref-${footnoteId}`)
268
+ .forEach((a) => a.remove());
269
+ const children = flowBlocks(clone);
270
+ items.push(elementBase('footnote-item', children.length > 0 ? children : [paragraph()], { footnoteId }));
271
+ });
272
+ return elementBase('footnote-list', items);
273
+ }
250
274
  function convertImage(img, caption) {
251
275
  const src = safeImageSrc(img.getAttribute('src') ?? '');
252
276
  if (!src)
@@ -292,7 +316,7 @@ function convertBlock(el) {
292
316
  }
293
317
  case 'ul':
294
318
  case 'ol':
295
- return [convertList(el)];
319
+ return el.hasAttribute('data-likhari-footnote-list') ? [convertFootnoteList(el)] : [convertList(el)];
296
320
  case 'li':
297
321
  return convertListItem(el, false).length > 0 ? [elementBase('list', convertListItem(el, false), { listType: 'bullet', start: 1, tag: 'ul' })] : [];
298
322
  case 'table':
@@ -1 +1 @@
1
- {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/html/serialize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAE4E,KAAK,KAAK,EAC5F,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAyGD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAyCtE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAE1F"}
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/html/serialize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAEkG,KAAK,KAAK,EAClH,MAAM,sBAAsB,CAAC;AAE9B,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAgID,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CA6CtE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAI1F"}
@@ -1,4 +1,4 @@
1
- import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, INDENT_PX, rootChildren, } from '../shared/serialized.js';
1
+ import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, INDENT_PX, collectFootnoteOrder, rootChildren, } from '../shared/serialized.js';
2
2
  export function escapeHtml(value) {
3
3
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
4
4
  }
@@ -87,6 +87,26 @@ function tableToHtml(node, ctx) {
87
87
  .join('');
88
88
  return `<table><tbody>${rows}</tbody></table>`;
89
89
  }
90
+ /** Set once per serializeHtml() call and read by the footnote cases below —
91
+ * avoids threading a numbering map through every recursive nodeToHtml call
92
+ * for what is otherwise a leaf concern of two node types. Safe because
93
+ * serialization is synchronous and this module is never re-entered mid-call. */
94
+ let footnoteOrder = new Map();
95
+ function footnoteReferenceToHtml(node) {
96
+ const id = String(node.footnoteId ?? '');
97
+ const n = footnoteOrder.get(id) ?? '?';
98
+ return `<sup data-likhari-footnote-ref="${escapeAttr(id)}"><a href="#fn-${escapeAttr(id)}" id="fnref-${escapeAttr(id)}">${n}</a></sup>`;
99
+ }
100
+ function footnoteListToHtml(node, ctx) {
101
+ const items = (node.children ?? [])
102
+ .map((item) => {
103
+ const id = String(item.footnoteId ?? '');
104
+ const body = (item.children ?? []).map((child) => nodeToHtml(child, ctx)).join('');
105
+ return `<li id="fn-${escapeAttr(id)}" data-likhari-footnote-item="${escapeAttr(id)}">${body} <a href="#fnref-${escapeAttr(id)}">↩</a></li>`;
106
+ })
107
+ .join('');
108
+ return `<ol data-likhari-footnote-list>${items}</ol>`;
109
+ }
90
110
  function layoutToHtml(node, ctx) {
91
111
  const items = node.children ?? [];
92
112
  const templateColumns = typeof node.templateColumns === 'string' ? node.templateColumns : `repeat(${items.length}, 1fr)`;
@@ -143,11 +163,17 @@ export function nodeToHtml(node, ctx) {
143
163
  return tableToHtml(node, ctx);
144
164
  case 'layout-container':
145
165
  return layoutToHtml(node, ctx);
166
+ case 'footnote-reference':
167
+ return footnoteReferenceToHtml(node);
168
+ case 'footnote-list':
169
+ return footnoteListToHtml(node, ctx);
146
170
  default:
147
171
  // Unknown node (e.g. a future feature): keep its content, drop the wrapper.
148
172
  return inlineChildren(node, ctx);
149
173
  }
150
174
  }
151
175
  export function serializeHtml(state, ctx) {
152
- return rootChildren(state).map((node) => nodeToHtml(node, ctx)).join('\n');
176
+ const blocks = rootChildren(state);
177
+ footnoteOrder = collectFootnoteOrder(blocks);
178
+ return blocks.map((node) => nodeToHtml(node, ctx)).join('\n');
153
179
  }
@@ -16,6 +16,11 @@ export interface MdNode {
16
16
  name?: string;
17
17
  attributes?: Record<string, string | null | undefined> | null;
18
18
  align?: Array<'left' | 'right' | 'center' | null> | null;
19
+ /** GFM footnotes (remark-gfm): `identifier` is normalized/lowercased by
20
+ * remark itself, `label` keeps the original casing. We only ever use
21
+ * plain numerals for both, so the distinction doesn't matter here. */
22
+ identifier?: string;
23
+ label?: string;
19
24
  }
20
25
  export type Attrs = Record<string, string>;
21
26
  export declare function directive(type: 'textDirective' | 'leafDirective' | 'containerDirective', name: string, attributes: Attrs, children?: MdNode[]): MdNode;
@@ -1 +1 @@
1
- {"version":3,"file":"mdast.d.ts","sourceRoot":"","sources":["../../src/markdown/mdast.ts"],"names":[],"mappings":"AAAA;qFACqF;AACrF,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IAC9D,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,GAAG,oBAAoB,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,GAAE,MAAM,EAAO,GAAG,MAAM,CAE1J"}
1
+ {"version":3,"file":"mdast.d.ts","sourceRoot":"","sources":["../../src/markdown/mdast.ts"],"names":[],"mappings":"AAAA;qFACqF;AACrF,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;IAC9D,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD;;0EAEsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C,wBAAgB,SAAS,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,GAAG,oBAAoB,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,GAAE,MAAM,EAAO,GAAG,MAAM,CAE1J"}
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/markdown/parse.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAiSjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAI3F"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/markdown/parse.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAkTjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAO3F"}
@@ -64,6 +64,9 @@ function inline(nodes, state, out) {
64
64
  out.push(elementBase('link', children, { url, rel: null, target: null, title: node.title ?? null }));
65
65
  break;
66
66
  }
67
+ case 'footnoteReference':
68
+ out.push({ type: 'footnote-reference', version: 1, footnoteId: node.identifier ?? '' });
69
+ break;
67
70
  case 'textDirective': {
68
71
  const name = node.name ?? '';
69
72
  if (name === 'span') {
@@ -290,8 +293,23 @@ function restoreUnknownDirectives(node, source) {
290
293
  return c;
291
294
  });
292
295
  }
296
+ /** Footnote definitions are always top-level in CommonMark/GFM (never nested
297
+ * inside a list/quote/etc.), wherever in the source they appear — so unlike
298
+ * everything else `blocks()` handles, they're pulled out before the normal
299
+ * recursive walk and collected into the one footnote-list our editor model
300
+ * expects, instead of staying scattered where the source happened to put them. */
301
+ function extractFootnoteList(topLevel) {
302
+ const defs = topLevel.filter((node) => node.type === 'footnoteDefinition');
303
+ if (defs.length === 0)
304
+ return null;
305
+ const items = defs.map((def) => elementBase('footnote-item', blocks(def.children ?? []), { footnoteId: def.identifier ?? '' }));
306
+ return elementBase('footnote-list', items);
307
+ }
293
308
  export function parseMarkdown(input, _ctx) {
294
309
  const tree = processor.parse(input);
295
310
  restoreUnknownDirectives(tree, input);
296
- return makeState(blocks(tree.children ?? []));
311
+ const topLevel = tree.children ?? [];
312
+ const footnoteList = extractFootnoteList(topLevel);
313
+ const body = blocks(topLevel.filter((node) => node.type !== 'footnoteDefinition'));
314
+ return makeState(footnoteList ? [...body, footnoteList] : body);
297
315
  }
@@ -1 +1 @@
1
- {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/markdown/serialize.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAKjD,OAAO,EAAa,KAAK,KAAK,EAAe,MAAM,SAAS,CAAC;AAyC7D,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAUjD;AAiHD,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAMhE;AAoHD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAI9F"}
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/markdown/serialize.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAKjD,OAAO,EAAa,KAAK,KAAK,EAAe,MAAM,SAAS,CAAC;AAyC7D,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAUjD;AAqHD,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAMhE;AA2HD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAI9F"}
@@ -152,6 +152,11 @@ function inlineNodes(children, ctx) {
152
152
  flush();
153
153
  out.push({ type: 'link', url: String(child.url ?? ''), title: child.title ? String(child.title) : null, children: inlineNodes(child.children ?? [], ctx) });
154
154
  }
155
+ else if (child.type === 'footnote-reference') {
156
+ flush();
157
+ const id = String(child.footnoteId ?? '');
158
+ out.push({ type: 'footnoteReference', identifier: id, label: id });
159
+ }
155
160
  else if (child.children) {
156
161
  flush();
157
162
  out.push(...inlineNodes(child.children, ctx));
@@ -284,6 +289,13 @@ function blockToMd(node, ctx) {
284
289
  return [tableToMd(node, ctx)];
285
290
  case 'layout-container':
286
291
  return [layoutToMd(node, ctx)];
292
+ case 'footnote-list':
293
+ return (node.children ?? []).map((item) => ({
294
+ type: 'footnoteDefinition',
295
+ identifier: String(item.footnoteId ?? ''),
296
+ label: String(item.footnoteId ?? ''),
297
+ children: (item.children ?? []).flatMap((child) => blockToMd(child, ctx)),
298
+ }));
287
299
  default:
288
300
  // Fallback rule (dialect spec §6): unknown blocks keep their text as paragraphs.
289
301
  return (node.children ?? []).flatMap((child) => blockToMd(child, ctx));
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plain-text/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAyDhD,eAAO,MAAM,kBAAkB,EAAE,eAkDhC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plain-text/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AA6EhD,eAAO,MAAM,kBAAkB,EAAE,eAsDhC,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { collectFootnoteOrder } from '../shared/serialized.js';
1
2
  const LIST_ITEM_PREFIX = {
2
3
  bullet: () => '- ',
3
4
  number: (index) => `${index + 1}. `,
@@ -9,7 +10,7 @@ const LIST_ITEM_PREFIX = {
9
10
  * all formatting — this is a documented, exhaustive mapping per node type,
10
11
  * not an ad hoc `.textContent` walk.
11
12
  */
12
- function nodeToText(node, listItemIndex = 0) {
13
+ function nodeToText(node, order, listItemIndex = 0) {
13
14
  const withChildren = node;
14
15
  if (node.type === 'text' || node.type === 'linebreak') {
15
16
  return node.text ?? (node.type === 'linebreak' ? '\n' : '');
@@ -20,28 +21,50 @@ function nodeToText(node, listItemIndex = 0) {
20
21
  const image = node;
21
22
  return image.altText || image.caption || '';
22
23
  }
24
+ // Inline marker only — its matching note is in the trailing list (below),
25
+ // never inline, per the fidelity matrix's "inline bracketed number".
26
+ if (node.type === 'footnote-reference') {
27
+ const id = String(node.footnoteId ?? '');
28
+ return `[${order.get(id) ?? '?'}]`;
29
+ }
23
30
  if (node.type === 'list' && withChildren.children) {
24
31
  const listType = node.listType ?? 'bullet';
25
32
  return withChildren.children
26
33
  .map((child, i) => {
27
34
  const prefix = LIST_ITEM_PREFIX[listType]?.(i) ?? '- ';
28
- return prefix + nodeToText(child, i);
35
+ return prefix + nodeToText(child, order, i);
29
36
  })
30
37
  .join('\n');
31
38
  }
32
39
  if (withChildren.children) {
33
- return withChildren.children.map((child) => nodeToText(child, listItemIndex)).join('');
40
+ return withChildren.children.map((child) => nodeToText(child, order, listItemIndex)).join('');
34
41
  }
35
42
  return '';
36
43
  }
37
- function blockToText(node) {
38
- return nodeToText(node);
44
+ function blockToText(node, order) {
45
+ return nodeToText(node, order);
46
+ }
47
+ /** `[n] note text` per footnote, in display-number order — the "trailing
48
+ * list" half of the fidelity matrix's plain-text footnote entry. */
49
+ function footnoteListToText(node, order) {
50
+ const items = node.children ?? [];
51
+ return items
52
+ .map((item) => {
53
+ const id = String(item.footnoteId ?? '');
54
+ return `[${order.get(id) ?? '?'}] ${nodeToText(item, order)}`;
55
+ })
56
+ .join('\n');
39
57
  }
40
58
  export const plainTextConverter = {
41
59
  id: 'plain-text',
42
60
  serialize(editorState) {
43
61
  const root = editorState.root;
44
- const blocks = (root.children ?? []).map(blockToText);
62
+ const children = root.children ?? [];
63
+ const order = collectFootnoteOrder(children);
64
+ const footnoteLists = children.filter((n) => n.type === 'footnote-list');
65
+ const blocks = children.filter((n) => n.type !== 'footnote-list').map((n) => blockToText(n, order));
66
+ if (footnoteLists.length > 0)
67
+ blocks.push(footnoteLists.map((n) => footnoteListToText(n, order)).join('\n'));
45
68
  return blocks.join('\n\n');
46
69
  },
47
70
  parse(input) {
@@ -28,4 +28,9 @@ export declare function paragraph(children?: SNode[], extra?: Record<string, unk
28
28
  export declare function makeState(blocks: SNode[]): SerializedEditorState;
29
29
  /** Inline node types: everything else is a block. */
30
30
  export declare function isInlineNode(node: SNode): boolean;
31
+ /** Footnote reference numbers (lexical-editor-spec.md §4.10) are never
32
+ * stored — they're each `footnote-reference` node's position, in document
33
+ * order, among all such nodes. Walks the whole tree once so every converter
34
+ * numbers references and their matching footnote-list entries identically. */
35
+ export declare function collectFootnoteOrder(blocks: SNode[]): Map<string, number>;
31
36
  //# sourceMappingURL=serialized.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serialized.d.ts","sourceRoot":"","sources":["../../src/shared/serialized.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,6DAA6D;AAC7D,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;AAE7C,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,KAAK,EAAE,CAElE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAK,GAAG,KAAK,CAEpE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,KAAK,CAEvG;AAED,wBAAgB,SAAS,CAAC,QAAQ,GAAE,KAAK,EAAO,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,KAAK,CAE5F;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAKhE;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,IAAI,EAAE,KAAK,GAAG,OAAO,CAEjD"}
1
+ {"version":3,"file":"serialized.d.ts","sourceRoot":"","sources":["../../src/shared/serialized.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD;;;;GAIG;AACH,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAClC,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,6DAA6D;AAC7D,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;AAE7C,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,KAAK,EAAE,CAElE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAK,GAAG,KAAK,CAEpE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,KAAK,CAEvG;AAED,wBAAgB,SAAS,CAAC,QAAQ,GAAE,KAAK,EAAO,EAAE,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,KAAK,CAE5F;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,qBAAqB,CAKhE;AAED,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,IAAI,EAAE,KAAK,GAAG,OAAO,CAEjD;AAED;;;8EAG8E;AAC9E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzE"}
@@ -30,3 +30,18 @@ export function makeState(blocks) {
30
30
  export function isInlineNode(node) {
31
31
  return node.type === 'text' || node.type === 'linebreak' || node.type === 'tab' || node.type === 'link' || node.type === 'autolink';
32
32
  }
33
+ /** Footnote reference numbers (lexical-editor-spec.md §4.10) are never
34
+ * stored — they're each `footnote-reference` node's position, in document
35
+ * order, among all such nodes. Walks the whole tree once so every converter
36
+ * numbers references and their matching footnote-list entries identically. */
37
+ export function collectFootnoteOrder(blocks) {
38
+ const order = new Map();
39
+ const visit = (node) => {
40
+ if (node.type === 'footnote-reference' && typeof node.footnoteId === 'string' && !order.has(node.footnoteId)) {
41
+ order.set(node.footnoteId, order.size + 1);
42
+ }
43
+ (node.children ?? []).forEach(visit);
44
+ };
45
+ blocks.forEach(visit);
46
+ return order;
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-converters",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Format transformers (Markdown/HTML/plain-text/Lexical JSON) against the canonical Lexical EditorState, isolated from UI.",
5
5
  "license": "MIT",
6
6
  "repository": {