@inshapardaz/likhari-converters 0.1.16 → 0.1.18-dev.124

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;AA4UD,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,45 @@ 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
+ const POETRY_LAYOUTS = new Set(['single', 'two-column']);
255
+ const POETRY_ALIGNS = new Set(['justify', 'left', 'right', 'start']);
256
+ function convertPoetry(el) {
257
+ const rawLayout = el.getAttribute('data-likhari-poetry-layout') ?? 'single';
258
+ const layout = POETRY_LAYOUTS.has(rawLayout) ? rawLayout : 'single';
259
+ const rawAlign = el.getAttribute('data-likhari-poetry-align') ?? 'justify';
260
+ const align = POETRY_ALIGNS.has(rawAlign) ? rawAlign : 'justify';
261
+ if (layout === 'two-column') {
262
+ const containerEl = Array.from(el.children).find((c) => c.hasAttribute('data-likhari-layout-container'));
263
+ const container = containerEl
264
+ ? convertLayout(containerEl)
265
+ : elementBase('layout-container', [elementBase('layout-item', [paragraph()]), elementBase('layout-item', [paragraph()])], {
266
+ templateColumns: 'repeat(2, 1fr)',
267
+ });
268
+ return elementBase('poetry-couplet', [container], { layout, align });
269
+ }
270
+ const children = flowBlocks(el);
271
+ return elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], { layout, align });
272
+ }
273
+ function convertFootnoteList(el) {
274
+ const items = [];
275
+ Array.from(el.children).forEach((child) => {
276
+ if (child.tagName.toLowerCase() !== 'li')
277
+ return;
278
+ const li = child;
279
+ const footnoteId = li.getAttribute('data-likhari-footnote-item') ?? (li.getAttribute('id') ?? '').replace(/^fn-/, '');
280
+ if (!footnoteId)
281
+ return;
282
+ // Drop the "↩" backlink our own serializer appends — it's a generated
283
+ // affordance, not part of the note's content.
284
+ const clone = li.cloneNode(true);
285
+ Array.from(clone.querySelectorAll('a'))
286
+ .filter((a) => a.getAttribute('href') === `#fnref-${footnoteId}`)
287
+ .forEach((a) => a.remove());
288
+ const children = flowBlocks(clone);
289
+ items.push(elementBase('footnote-item', children.length > 0 ? children : [paragraph()], { footnoteId }));
290
+ });
291
+ return elementBase('footnote-list', items);
292
+ }
250
293
  function convertImage(img, caption) {
251
294
  const src = safeImageSrc(img.getAttribute('src') ?? '');
252
295
  if (!src)
@@ -292,7 +335,7 @@ function convertBlock(el) {
292
335
  }
293
336
  case 'ul':
294
337
  case 'ol':
295
- return [convertList(el)];
338
+ return el.hasAttribute('data-likhari-footnote-list') ? [convertFootnoteList(el)] : [convertList(el)];
296
339
  case 'li':
297
340
  return convertListItem(el, false).length > 0 ? [elementBase('list', convertListItem(el, false), { listType: 'bullet', start: 1, tag: 'ul' })] : [];
298
341
  case 'table':
@@ -317,6 +360,8 @@ function convertBlock(el) {
317
360
  return [{ type: 'page-break', version: 1 }];
318
361
  if (el.hasAttribute('data-likhari-layout-container'))
319
362
  return [convertLayout(el)];
363
+ if (el.hasAttribute('data-likhari-poetry-layout'))
364
+ return [convertPoetry(el)];
320
365
  return flowBlocks(el);
321
366
  case 'figcaption':
322
367
  return [];
@@ -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;AAuID,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CA+CtE;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)`;
@@ -95,6 +115,12 @@ function layoutToHtml(node, ctx) {
95
115
  .join('');
96
116
  return `<div data-likhari-layout-container style="display: grid; grid-template-columns: ${escapeAttr(templateColumns)}">${columns}</div>`;
97
117
  }
118
+ function poetryToHtml(node, ctx) {
119
+ const layout = node.layout === 'two-column' ? 'two-column' : 'single';
120
+ const align = typeof node.align === 'string' ? node.align : 'justify';
121
+ const inner = (node.children ?? []).map((child) => nodeToHtml(child, ctx)).join('');
122
+ return `<div data-likhari-poetry-layout="${layout}" data-likhari-poetry-align="${align}" style="text-align: ${escapeAttr(align)}">${inner}</div>`;
123
+ }
98
124
  function imageToHtml(node, ctx) {
99
125
  const rawSrc = String(node.src ?? '');
100
126
  const src = ctx?.resolveImageUrl ? ctx.resolveImageUrl(rawSrc) : rawSrc;
@@ -143,11 +169,19 @@ export function nodeToHtml(node, ctx) {
143
169
  return tableToHtml(node, ctx);
144
170
  case 'layout-container':
145
171
  return layoutToHtml(node, ctx);
172
+ case 'footnote-reference':
173
+ return footnoteReferenceToHtml(node);
174
+ case 'footnote-list':
175
+ return footnoteListToHtml(node, ctx);
176
+ case 'poetry-couplet':
177
+ return poetryToHtml(node, ctx);
146
178
  default:
147
179
  // Unknown node (e.g. a future feature): keep its content, drop the wrapper.
148
180
  return inlineChildren(node, ctx);
149
181
  }
150
182
  }
151
183
  export function serializeHtml(state, ctx) {
152
- return rootChildren(state).map((node) => nodeToHtml(node, ctx)).join('\n');
184
+ const blocks = rootChildren(state);
185
+ footnoteOrder = collectFootnoteOrder(blocks);
186
+ return blocks.map((node) => nodeToHtml(node, ctx)).join('\n');
153
187
  }
@@ -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;AAwTjD,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') {
@@ -234,6 +237,13 @@ function blocks(nodes, extra = {}) {
234
237
  out.push(elementBase('layout-container', columns, { templateColumns: `repeat(${columns.length}, 1fr)` }));
235
238
  }
236
239
  }
240
+ else if (node.name === 'poetry') {
241
+ const attrs = node.attributes ?? {};
242
+ const layout = attrs.layout === 'two-column' ? 'two-column' : 'single';
243
+ const align = ALIGNMENTS.has(attrs.align ?? '') ? attrs.align : 'justify';
244
+ const children = blocks(node.children ?? []);
245
+ out.push(elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], { layout, align }));
246
+ }
237
247
  else {
238
248
  // Unknown containers (including a stray "column" outside "columns")
239
249
  // are transparent: their content is kept.
@@ -290,8 +300,23 @@ function restoreUnknownDirectives(node, source) {
290
300
  return c;
291
301
  });
292
302
  }
303
+ /** Footnote definitions are always top-level in CommonMark/GFM (never nested
304
+ * inside a list/quote/etc.), wherever in the source they appear — so unlike
305
+ * everything else `blocks()` handles, they're pulled out before the normal
306
+ * recursive walk and collected into the one footnote-list our editor model
307
+ * expects, instead of staying scattered where the source happened to put them. */
308
+ function extractFootnoteList(topLevel) {
309
+ const defs = topLevel.filter((node) => node.type === 'footnoteDefinition');
310
+ if (defs.length === 0)
311
+ return null;
312
+ const items = defs.map((def) => elementBase('footnote-item', blocks(def.children ?? []), { footnoteId: def.identifier ?? '' }));
313
+ return elementBase('footnote-list', items);
314
+ }
293
315
  export function parseMarkdown(input, _ctx) {
294
316
  const tree = processor.parse(input);
295
317
  restoreUnknownDirectives(tree, input);
296
- return makeState(blocks(tree.children ?? []));
318
+ const topLevel = tree.children ?? [];
319
+ const footnoteList = extractFootnoteList(topLevel);
320
+ const body = blocks(topLevel.filter((node) => node.type !== 'footnoteDefinition'));
321
+ return makeState(footnoteList ? [...body, footnoteList] : body);
297
322
  }
@@ -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;AAoID,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));
@@ -241,6 +246,12 @@ function layoutToMd(node, ctx) {
241
246
  const columns = items.map((item) => directive('containerDirective', 'column', {}, (item.children ?? []).flatMap((child) => blockToMd(child, ctx))));
242
247
  return directive('containerDirective', 'columns', { count: String(items.length) }, columns);
243
248
  }
249
+ function poetryToMd(node, ctx) {
250
+ const layout = node.layout === 'two-column' ? 'two-column' : 'single';
251
+ const align = typeof node.align === 'string' ? node.align : 'justify';
252
+ const inner = (node.children ?? []).flatMap((child) => blockToMd(child, ctx));
253
+ return directive('containerDirective', 'poetry', { layout, align }, inner);
254
+ }
244
255
  function tableToMd(node, ctx) {
245
256
  const rows = (node.children ?? []).map((row) => ({
246
257
  type: 'tableRow',
@@ -284,6 +295,15 @@ function blockToMd(node, ctx) {
284
295
  return [tableToMd(node, ctx)];
285
296
  case 'layout-container':
286
297
  return [layoutToMd(node, ctx)];
298
+ case 'poetry-couplet':
299
+ return [poetryToMd(node, ctx)];
300
+ case 'footnote-list':
301
+ return (node.children ?? []).map((item) => ({
302
+ type: 'footnoteDefinition',
303
+ identifier: String(item.footnoteId ?? ''),
304
+ label: String(item.footnoteId ?? ''),
305
+ children: (item.children ?? []).flatMap((child) => blockToMd(child, ctx)),
306
+ }));
287
307
  default:
288
308
  // Fallback rule (dialect spec §6): unknown blocks keep their text as paragraphs.
289
309
  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;AAuFhD,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,59 @@ 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
+ }
30
+ // Linearized per the fidelity matrix ("Poetry layout variants ⚠️ linearized"):
31
+ // the two misras, one per line, regardless of single/two-column layout —
32
+ // the columns primitive a two-column couplet is built on otherwise has no
33
+ // per-node case and would fall through to running the lines together.
34
+ if (node.type === 'poetry-couplet') {
35
+ const children = withChildren.children ?? [];
36
+ const misraHolders = children[0]?.type === 'layout-container' ? (children[0].children ?? []) : children;
37
+ return misraHolders.map((child) => nodeToText(child, order)).join('\n');
38
+ }
23
39
  if (node.type === 'list' && withChildren.children) {
24
40
  const listType = node.listType ?? 'bullet';
25
41
  return withChildren.children
26
42
  .map((child, i) => {
27
43
  const prefix = LIST_ITEM_PREFIX[listType]?.(i) ?? '- ';
28
- return prefix + nodeToText(child, i);
44
+ return prefix + nodeToText(child, order, i);
29
45
  })
30
46
  .join('\n');
31
47
  }
32
48
  if (withChildren.children) {
33
- return withChildren.children.map((child) => nodeToText(child, listItemIndex)).join('');
49
+ return withChildren.children.map((child) => nodeToText(child, order, listItemIndex)).join('');
34
50
  }
35
51
  return '';
36
52
  }
37
- function blockToText(node) {
38
- return nodeToText(node);
53
+ function blockToText(node, order) {
54
+ return nodeToText(node, order);
55
+ }
56
+ /** `[n] note text` per footnote, in display-number order — the "trailing
57
+ * list" half of the fidelity matrix's plain-text footnote entry. */
58
+ function footnoteListToText(node, order) {
59
+ const items = node.children ?? [];
60
+ return items
61
+ .map((item) => {
62
+ const id = String(item.footnoteId ?? '');
63
+ return `[${order.get(id) ?? '?'}] ${nodeToText(item, order)}`;
64
+ })
65
+ .join('\n');
39
66
  }
40
67
  export const plainTextConverter = {
41
68
  id: 'plain-text',
42
69
  serialize(editorState) {
43
70
  const root = editorState.root;
44
- const blocks = (root.children ?? []).map(blockToText);
71
+ const children = root.children ?? [];
72
+ const order = collectFootnoteOrder(children);
73
+ const footnoteLists = children.filter((n) => n.type === 'footnote-list');
74
+ const blocks = children.filter((n) => n.type !== 'footnote-list').map((n) => blockToText(n, order));
75
+ if (footnoteLists.length > 0)
76
+ blocks.push(footnoteLists.map((n) => footnoteListToText(n, order)).join('\n'));
45
77
  return blocks.join('\n\n');
46
78
  },
47
79
  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-dev.124",
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": {