@inshapardaz/likhari-converters 0.1.18-dev.156 → 0.1.18-dev.162

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;AAsWD,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;AAkCjD,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;AAoVD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAMvF"}
@@ -1,3 +1,4 @@
1
+ import { layoutOrDefault, scaleOrDefault } from '../shared/poetry.js';
1
2
  import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, INDENT_PX, elementBase, makeState, paragraph, textNode, } from '../shared/serialized.js';
2
3
  const ALIGNMENTS = new Set(['left', 'right', 'center', 'justify', 'start', 'end']);
3
4
  const TAG_FORMATS = {
@@ -251,43 +252,30 @@ function convertLayout(container) {
251
252
  const templateColumns = container.style.gridTemplateColumns || `repeat(${Math.max(items.length, 2)}, 1fr)`;
252
253
  return elementBase('layout-container', items.length > 0 ? items : [elementBase('layout-item', [paragraph()])], { templateColumns });
253
254
  }
254
- const POETRY_LAYOUTS = new Set(['single', 'two-column', 'alternating']);
255
- const POETRY_ALIGNS = new Set(['justify', 'left', 'right', 'start']);
256
- function convertPoetryCouplet(el) {
257
- const centered = el.getAttribute('data-likhari-poetry-couplet-centered') === 'true';
258
- const children = flowBlocks(el);
259
- return elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], { centered });
260
- }
261
255
  function convertPoetry(el) {
262
- const rawLayout = el.getAttribute('data-likhari-poetry-layout') ?? 'single';
263
- const layout = POETRY_LAYOUTS.has(rawLayout) ? rawLayout : 'single';
264
- const rawAlign = el.getAttribute('data-likhari-poetry-align') ?? 'justify';
265
- const align = POETRY_ALIGNS.has(rawAlign) ? rawAlign : 'justify';
266
- // Couplets may mix wrapper types within one block — a two-column layout
267
- // row (data-likhari-layout-container) and a centered single couplet
268
- // (data-likhari-poetry-couplet) can sit side by side — grab all of them,
269
- // not just the first, or every couplet past the first silently disappears.
270
- const couplets = Array.from(el.children)
271
- .map((c) => {
272
- if (c.hasAttribute('data-likhari-layout-container'))
273
- return convertLayout(c);
274
- if (c.hasAttribute('data-likhari-poetry-couplet'))
275
- return convertPoetryCouplet(c);
276
- return null;
277
- })
278
- .filter((n) => n !== null);
279
- if (couplets.length > 0)
280
- return elementBase('poetry-block', couplets, { layout, align });
281
- // Fallback: legacy markup with no explicit couplet wrappers at all.
256
+ const layout = layoutOrDefault(el.getAttribute('data-likhari-poetry-layout'));
257
+ const settings = {
258
+ layout,
259
+ spacing: scaleOrDefault(el.getAttribute('data-likhari-poetry-spacing')),
260
+ gutter: scaleOrDefault(el.getAttribute('data-likhari-poetry-gutter')),
261
+ stagger: scaleOrDefault(el.getAttribute('data-likhari-poetry-stagger')),
262
+ ...(Number(el.getAttribute('data-likhari-poetry-width')) > 0 ? { width: Number(el.getAttribute('data-likhari-poetry-width')) } : {}),
263
+ };
282
264
  if (layout === 'two-column') {
283
- return elementBase('poetry-block', [
284
- elementBase('layout-container', [elementBase('layout-item', [paragraph()]), elementBase('layout-item', [paragraph()])], {
285
- templateColumns: 'repeat(2, 1fr)',
286
- }),
287
- ], { layout, align });
265
+ // One LayoutContainerNode row per couplet — grab all of them, not just
266
+ // the first, or every couplet past the first silently disappears.
267
+ const containerEls = Array.from(el.children).filter((c) => c.hasAttribute('data-likhari-layout-container'));
268
+ const containers = containerEls.length > 0
269
+ ? containerEls.map((c) => convertLayout(c))
270
+ : [
271
+ elementBase('layout-container', [elementBase('layout-item', [paragraph()]), elementBase('layout-item', [paragraph()])], {
272
+ templateColumns: 'repeat(2, 1fr)',
273
+ }),
274
+ ];
275
+ return elementBase('poetry-couplet', containers, settings);
288
276
  }
289
277
  const children = flowBlocks(el);
290
- return elementBase('poetry-block', [elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], { centered: false })], { layout, align });
278
+ return elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], settings);
291
279
  }
292
280
  function convertFootnoteList(el) {
293
281
  const items = [];
@@ -304,6 +292,8 @@ function convertFootnoteList(el) {
304
292
  Array.from(clone.querySelectorAll('a'))
305
293
  .filter((a) => a.getAttribute('href') === `#fnref-${footnoteId}`)
306
294
  .forEach((a) => a.remove());
295
+ // Likewise the "[n]" marker our serializer writes in front of the note.
296
+ clone.querySelectorAll('[data-likhari-footnote-number]').forEach((n) => n.remove());
307
297
  const children = flowBlocks(clone);
308
298
  items.push(elementBase('footnote-item', children.length > 0 ? children : [paragraph()], { footnoteId }));
309
299
  });
@@ -4,5 +4,7 @@ import { type SNode } from '../shared/serialized.js';
4
4
  export declare function escapeHtml(value: string): string;
5
5
  export declare function escapeAttr(value: string): string;
6
6
  export declare function nodeToHtml(node: SNode, ctx?: ConverterContext): string;
7
+ /** Lets another converter (Markdown's HTML-table fallback) number footnotes the same way. */
8
+ export declare function setFootnoteOrder(blocks: SNode[]): void;
7
9
  export declare function serializeHtml(state: SerializedEditorState, ctx?: ConverterContext): string;
8
10
  //# sourceMappingURL=serialize.d.ts.map
@@ -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,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;AA8ID,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAiDtE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAI1F"}
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;AAIjD,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;AAiOD,wBAAgB,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CA+CtE;AAED,6FAA6F;AAC7F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAEtD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAI1F"}
@@ -1,3 +1,4 @@
1
+ import { POETRY_CENTERED_WIDTH, POETRY_COUPLET_GAP, POETRY_GUTTER_PX, POETRY_RULE, POETRY_STAGGER_WIDTH, layoutOrDefault, scaleOrDefault, } from '../shared/poetry.js';
1
2
  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
3
  export function escapeHtml(value) {
3
4
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -69,6 +70,13 @@ function listItemToHtml(item, listType, ctx) {
69
70
  const checked = listType === 'check' ? ` data-checked="${item.checked ? 'true' : 'false'}"` : '';
70
71
  return `<li${blockAttrs(item, checked)}>${inlineChildren(item, ctx)}</li>`;
71
72
  }
73
+ /** Table and footnote styling matching editor.css, written inline so exported HTML keeps it. */
74
+ const TABLE_BORDER = '#cfcac0';
75
+ const TABLE_HEADER_BG = '#f1efe9';
76
+ const TABLE_STYLE = 'border-collapse: collapse; margin: 1em 0; width: 100%';
77
+ const CELL_STYLE = `border: 1px solid ${TABLE_BORDER}; padding: 6px 10px; min-width: 72px; vertical-align: top`;
78
+ const HEADER_CELL_STYLE = `background: ${TABLE_HEADER_BG}; font-weight: 600; text-align: start`;
79
+ const FOOTNOTE_LIST_STYLE = `list-style: none; margin: 2em 0 0 0; padding: 0.75em 0 0 1.5em; border-top: 1px solid ${TABLE_BORDER}; font-size: 0.9em`;
72
80
  function tableToHtml(node, ctx) {
73
81
  const rows = (node.children ?? [])
74
82
  .map((row) => {
@@ -79,13 +87,14 @@ function tableToHtml(node, ctx) {
79
87
  const span = (Number(cell.colSpan) > 1 ? ` colspan="${Number(cell.colSpan)}"` : '') +
80
88
  (Number(cell.rowSpan) > 1 ? ` rowspan="${Number(cell.rowSpan)}"` : '');
81
89
  const content = (cell.children ?? []).map((child) => nodeToHtml(child, ctx)).join('');
82
- return `<${tag}${span}>${content}</${tag}>`;
90
+ const style = header ? `${CELL_STYLE}; ${HEADER_CELL_STYLE}` : CELL_STYLE;
91
+ return `<${tag}${span} style="${escapeAttr(style)}">${content}</${tag}>`;
83
92
  })
84
93
  .join('');
85
94
  return `<tr>${cells}</tr>`;
86
95
  })
87
96
  .join('');
88
- return `<table><tbody>${rows}</tbody></table>`;
97
+ return `<table style="${escapeAttr(TABLE_STYLE)}"><tbody>${rows}</tbody></table>`;
89
98
  }
90
99
  /** Set once per serializeHtml() call and read by the footnote cases below —
91
100
  * avoids threading a numbering map through every recursive nodeToHtml call
@@ -102,10 +111,11 @@ function footnoteListToHtml(node, ctx) {
102
111
  .map((item) => {
103
112
  const id = String(item.footnoteId ?? '');
104
113
  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>`;
114
+ const number = `<span data-likhari-footnote-number style="font-weight: 600">[${footnoteOrder.get(id) ?? '?'}] </span>`;
115
+ return `<li id="fn-${escapeAttr(id)}" data-likhari-footnote-item="${escapeAttr(id)}" style="margin: 0.3em 0">${number}${body} <a href="#fnref-${escapeAttr(id)}">↩</a></li>`;
106
116
  })
107
117
  .join('');
108
- return `<ol data-likhari-footnote-list>${items}</ol>`;
118
+ return `<ol data-likhari-footnote-list style="${escapeAttr(FOOTNOTE_LIST_STYLE)}">${items}</ol>`;
109
119
  }
110
120
  function layoutToHtml(node, ctx) {
111
121
  const items = node.children ?? [];
@@ -115,17 +125,91 @@ function layoutToHtml(node, ctx) {
115
125
  .join('');
116
126
  return `<div data-likhari-layout-container style="display: grid; grid-template-columns: ${escapeAttr(templateColumns)}">${columns}</div>`;
117
127
  }
118
- function poetryToHtml(node, ctx) {
119
- const layout = node.layout === 'two-column' || node.layout === 'alternating' ? node.layout : '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>`;
128
+ function firstDirection(nodes) {
129
+ for (const node of nodes) {
130
+ if (node.direction === 'rtl' || node.direction === 'ltr')
131
+ return node.direction;
132
+ const inner = firstDirection(node.children ?? []);
133
+ if (inner)
134
+ return inner;
135
+ }
136
+ return 'ltr';
123
137
  }
124
- function poetryCoupletToHtml(node, ctx) {
125
- const centered = node.centered === true;
126
- const inner = (node.children ?? []).map((child) => nodeToHtml(child, ctx)).join('');
127
- const centeredAttr = centered ? ' data-likhari-poetry-couplet-centered="true"' : '';
128
- return `<div data-likhari-poetry-couplet${centeredAttr}>${inner}</div>`;
138
+ /** One misra line; styles are inline so the exported page keeps the editor's look. */
139
+ function poetryLineToHtml(line, extraStyle, ctx) {
140
+ const dir = line.direction === 'rtl' || line.direction === 'ltr' ? ` dir="${line.direction}"` : '';
141
+ const style = ['margin: 0', extraStyle].filter(Boolean).join('; ');
142
+ return `<p${dir} style="${escapeAttr(style)}">${inlineChildren(line, ctx)}</p>`;
143
+ }
144
+ function poetryToHtml(node, ctx) {
145
+ const layout = layoutOrDefault(node.layout);
146
+ const spacing = scaleOrDefault(node.spacing);
147
+ const gutter = scaleOrDefault(node.gutter);
148
+ const stagger = scaleOrDefault(node.stagger);
149
+ const width = typeof node.width === 'number' && node.width > 0 ? node.width : undefined;
150
+ const children = node.children ?? [];
151
+ const dir = firstDirection(children);
152
+ const gap = POETRY_COUPLET_GAP[spacing];
153
+ const separator = `border-top: ${POETRY_RULE}; margin-top: ${gap}; padding-top: calc(${gap} * 0.6)`;
154
+ let body;
155
+ if (layout === 'two-column') {
156
+ body = children
157
+ .map((row, r) => {
158
+ const items = row.children ?? [];
159
+ const centered = items.length === 1;
160
+ const templateColumns = typeof row.templateColumns === 'string' ? row.templateColumns : `repeat(${Math.max(items.length, 1)}, 1fr)`;
161
+ const rowStyle = [
162
+ 'display: grid',
163
+ 'gap: 0',
164
+ `grid-template-columns: ${templateColumns}`,
165
+ 'margin: 0',
166
+ ...(r > 0 ? [separator] : []),
167
+ ...(centered ? [`width: ${POETRY_CENTERED_WIDTH}`, 'margin-inline: auto'] : []),
168
+ ].join('; ');
169
+ const px = POETRY_GUTTER_PX[gutter];
170
+ const cells = items
171
+ .map((item, i) => {
172
+ const cellStyle = centered
173
+ ? 'padding-inline: 0'
174
+ : i === 0
175
+ ? `padding-inline-end: ${px}px`
176
+ : `border-inline-start: ${POETRY_RULE}; padding-inline-start: ${px}px`;
177
+ const lines = (item.children ?? []).map((line) => poetryLineToHtml(line, '', ctx)).join('');
178
+ return `<div data-likhari-layout-item style="${escapeAttr(cellStyle)}">${lines}</div>`;
179
+ })
180
+ .join('');
181
+ return `<div data-likhari-layout-container style="${escapeAttr(rowStyle)}">${cells}</div>`;
182
+ })
183
+ .join('');
184
+ }
185
+ else {
186
+ body = children
187
+ .map((line, i) => {
188
+ const couplet = Math.floor(i / 2);
189
+ const parts = [];
190
+ if (i % 2 === 0 && couplet > 0)
191
+ parts.push(separator);
192
+ if (layout === 'staggered') {
193
+ parts.push(`width: ${POETRY_STAGGER_WIDTH[stagger]}`);
194
+ parts.push(couplet % 2 === 0 ? 'margin-inline-end: auto' : 'margin-inline-start: auto');
195
+ }
196
+ return poetryLineToHtml(line, parts.join('; '), ctx);
197
+ })
198
+ .join('');
199
+ }
200
+ const rtl = dir === 'rtl';
201
+ const blockStyle = [
202
+ 'box-sizing: border-box',
203
+ 'margin: 0 auto 1.4em auto',
204
+ `max-width: ${width !== undefined ? `${width}px` : '70%'}`,
205
+ 'padding: 10px 16px',
206
+ 'text-align: justify',
207
+ 'text-align-last: justify',
208
+ `font-size: ${rtl ? '20px' : '15px'}`,
209
+ `line-height: ${rtl ? '2.1' : '1.6'}`,
210
+ ].join('; ');
211
+ const widthAttr = width !== undefined ? ` data-likhari-poetry-width="${width}"` : '';
212
+ return `<div data-likhari-poetry-layout="${layout}" data-likhari-poetry-spacing="${spacing}" data-likhari-poetry-gutter="${gutter}" data-likhari-poetry-stagger="${stagger}"${widthAttr} dir="${dir}" style="${escapeAttr(blockStyle)}">${body}</div>`;
129
213
  }
130
214
  function imageToHtml(node, ctx) {
131
215
  const rawSrc = String(node.src ?? '');
@@ -179,17 +263,19 @@ export function nodeToHtml(node, ctx) {
179
263
  return footnoteReferenceToHtml(node);
180
264
  case 'footnote-list':
181
265
  return footnoteListToHtml(node, ctx);
182
- case 'poetry-block':
183
- return poetryToHtml(node, ctx);
184
266
  case 'poetry-couplet':
185
- return poetryCoupletToHtml(node, ctx);
267
+ return poetryToHtml(node, ctx);
186
268
  default:
187
269
  // Unknown node (e.g. a future feature): keep its content, drop the wrapper.
188
270
  return inlineChildren(node, ctx);
189
271
  }
190
272
  }
273
+ /** Lets another converter (Markdown's HTML-table fallback) number footnotes the same way. */
274
+ export function setFootnoteOrder(blocks) {
275
+ footnoteOrder = collectFootnoteOrder(blocks);
276
+ }
191
277
  export function serializeHtml(state, ctx) {
192
278
  const blocks = rootChildren(state);
193
- footnoteOrder = collectFootnoteOrder(blocks);
279
+ setFootnoteOrder(blocks);
194
280
  return blocks.map((node) => nodeToHtml(node, ctx)).join('\n');
195
281
  }
@@ -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;AAkUjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAO3F"}
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;AAiUjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAO3F"}
@@ -2,8 +2,9 @@ import { unified } from 'unified';
2
2
  import remarkParse from 'remark-parse';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import remarkDirective from 'remark-directive';
5
- import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, elementBase, makeState, paragraph, textNode, } from '../shared/serialized.js';
6
- import { safeImageSrc, safeUrl } from '../html/parse.js';
5
+ import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, elementBase, makeState, paragraph, rootChildren, textNode, } from '../shared/serialized.js';
6
+ import { parseHtml, safeImageSrc, safeUrl } from '../html/parse.js';
7
+ import { layoutOrDefault, scaleOrDefault } from '../shared/poetry.js';
7
8
  const processor = unified().use(remarkParse).use(remarkGfm).use(remarkDirective);
8
9
  const ALIGNMENTS = new Set(['left', 'right', 'center', 'justify', 'start', 'end']);
9
10
  const DIRECTIVE_FORMATS = {
@@ -225,6 +226,9 @@ function blocks(nodes, extra = {}) {
225
226
  case 'table':
226
227
  out.push(tableBlock(node));
227
228
  break;
229
+ case 'html':
230
+ out.push(...rootChildren(parseHtml(node.value ?? '')).filter((n) => n.type === 'table'));
231
+ break;
228
232
  case 'containerDirective':
229
233
  if (node.name === 'columns') {
230
234
  const columns = (node.children ?? [])
@@ -239,21 +243,15 @@ function blocks(nodes, extra = {}) {
239
243
  }
240
244
  else if (node.name === 'poetry') {
241
245
  const attrs = node.attributes ?? {};
242
- const layout = attrs.layout === 'two-column' || attrs.layout === 'alternating' ? attrs.layout : 'single';
243
- const align = ALIGNMENTS.has(attrs.align ?? '') ? attrs.align : 'justify';
244
- let children = blocks(node.children ?? []);
245
- // Legacy markup (no explicit couplet/column wrappers): treat the
246
- // flat content as a single couplet, same as the import fallback.
247
- const hasWrappers = children.some((c) => c.type === 'poetry-couplet' || c.type === 'layout-container');
248
- if (!hasWrappers) {
249
- children = [elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], { centered: false })];
250
- }
251
- out.push(elementBase('poetry-block', children, { layout, align }));
252
- }
253
- else if (node.name === 'couplet') {
254
- const centered = (node.attributes ?? {}).centered === 'true';
246
+ const settings = {
247
+ layout: layoutOrDefault(attrs.layout),
248
+ spacing: scaleOrDefault(attrs.spacing),
249
+ gutter: scaleOrDefault(attrs.gutter),
250
+ stagger: scaleOrDefault(attrs.stagger),
251
+ ...(Number(attrs.width) > 0 ? { width: Number(attrs.width) } : {}),
252
+ };
255
253
  const children = blocks(node.children ?? []);
256
- out.push(elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], { centered }));
254
+ out.push(elementBase('poetry-couplet', children.length > 0 ? children : [paragraph(), paragraph()], settings));
257
255
  }
258
256
  else {
259
257
  // Unknown containers (including a stray "column" outside "columns")
@@ -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;AAqHD,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAMhE;AA4ID,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;AA2C7D,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;AAuJD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAM9F"}
@@ -4,6 +4,8 @@ import remarkGfm from 'remark-gfm';
4
4
  import remarkDirective from 'remark-directive';
5
5
  import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, rootChildren, } from '../shared/serialized.js';
6
6
  import { directive } from './mdast.js';
7
+ import { layoutOrDefault, scaleOrDefault } from '../shared/poetry.js';
8
+ import { nodeToHtml, setFootnoteOrder } from '../html/serialize.js';
7
9
  const processor = unified()
8
10
  .use(remarkGfm)
9
11
  .use(remarkDirective)
@@ -247,17 +249,28 @@ function layoutToMd(node, ctx) {
247
249
  return directive('containerDirective', 'columns', { count: String(items.length) }, columns);
248
250
  }
249
251
  function poetryToMd(node, ctx) {
250
- const layout = node.layout === 'two-column' || node.layout === 'alternating' ? node.layout : 'single';
251
- const align = typeof node.align === 'string' ? node.align : 'justify';
252
+ const attrs = { layout: layoutOrDefault(node.layout) };
253
+ for (const key of ['spacing', 'gutter', 'stagger']) {
254
+ const value = scaleOrDefault(node[key]);
255
+ if (value !== 'normal')
256
+ attrs[key] = value;
257
+ }
258
+ if (typeof node.width === 'number' && node.width > 0)
259
+ attrs.width = String(node.width);
252
260
  const inner = (node.children ?? []).flatMap((child) => blockToMd(child, ctx));
253
- return directive('containerDirective', 'poetry', { layout, align }, inner);
261
+ return directive('containerDirective', 'poetry', attrs, inner);
254
262
  }
255
- function poetryCoupletToMd(node, ctx) {
256
- const inner = (node.children ?? []).flatMap((child) => blockToMd(child, ctx));
257
- const attrs = node.centered === true ? { centered: 'true' } : {};
258
- return directive('containerDirective', 'couplet', attrs, inner);
263
+ /** Markdown tables cannot express merged cells or multi-block cells, so those
264
+ * are written as an HTML table block, read back by the HTML importer. */
265
+ function needsHtmlTable(node) {
266
+ return (node.children ?? []).some((row) => (row.children ?? []).some((cell) => Number(cell.colSpan ?? 1) > 1 ||
267
+ Number(cell.rowSpan ?? 1) > 1 ||
268
+ (cell.children ?? []).length !== 1 ||
269
+ cell.children[0].type !== 'paragraph'));
259
270
  }
260
271
  function tableToMd(node, ctx) {
272
+ if (needsHtmlTable(node))
273
+ return { type: 'html', value: nodeToHtml(node, ctx) };
261
274
  const rows = (node.children ?? []).map((row) => ({
262
275
  type: 'tableRow',
263
276
  children: (row.children ?? []).map((cell) => {
@@ -300,10 +313,8 @@ function blockToMd(node, ctx) {
300
313
  return [tableToMd(node, ctx)];
301
314
  case 'layout-container':
302
315
  return [layoutToMd(node, ctx)];
303
- case 'poetry-block':
304
- return [poetryToMd(node, ctx)];
305
316
  case 'poetry-couplet':
306
- return [poetryCoupletToMd(node, ctx)];
317
+ return [poetryToMd(node, ctx)];
307
318
  case 'footnote-list':
308
319
  return (node.children ?? []).map((item) => ({
309
320
  type: 'footnoteDefinition',
@@ -317,7 +328,9 @@ function blockToMd(node, ctx) {
317
328
  }
318
329
  }
319
330
  export function serializeMarkdown(state, ctx) {
320
- const tree = { type: 'root', children: rootChildren(state).flatMap((n) => blockToMd(n, ctx)) };
331
+ const blocks = rootChildren(state);
332
+ setFootnoteOrder(blocks);
333
+ const tree = { type: 'root', children: blocks.flatMap((n) => blockToMd(n, ctx)) };
321
334
  // remark-stringify consumes mdast; our MdNode is a structural subset of it.
322
335
  return String(processor.stringify(tree)).replace(/\n+$/, '\n').replace(/^\n+$/, '');
323
336
  }
@@ -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;AAgGhD,eAAO,MAAM,kBAAkB,EAAE,eAsDhC,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;AA4FhD,eAAO,MAAM,kBAAkB,EAAE,eAsDhC,CAAC"}
@@ -28,19 +28,15 @@ function nodeToText(node, order, listItemIndex = 0) {
28
28
  return `[${order.get(id) ?? '?'}]`;
29
29
  }
30
30
  // Linearized per the fidelity matrix ("Poetry layout variants ⚠️ linearized"):
31
- // one misra per line, regardless of single/two-column/alternating layout
32
- // and however many couplets the block holds, mixed wrapper types included
33
- // (a two-column couplet is its own layout-container row; a single or
34
- // centered couplet is its own poetry-couplet wrapper) — so each couplet
35
- // contributes its two misra lines in turn rather than only the block's
36
- // first couplet being read.
37
- if (node.type === 'poetry-block' || node.type === 'poetry-couplet') {
31
+ // one misra per line, regardless of single/two-column layout and however
32
+ // many couplets the block holds — in two-column layout each couplet is
33
+ // its own layout-container row, so each contributes its two item lines
34
+ // in turn rather than only the block's first couplet being read.
35
+ if (node.type === 'poetry-couplet') {
38
36
  const children = withChildren.children ?? [];
39
37
  const lines = children.flatMap((child) => child.type === 'layout-container'
40
38
  ? (child.children ?? []).map((item) => nodeToText(item, order))
41
- : child.type === 'poetry-couplet'
42
- ? (child.children ?? []).map((item) => nodeToText(item, order))
43
- : [nodeToText(child, order)]);
39
+ : [nodeToText(child, order)]);
44
40
  return lines.join('\n');
45
41
  }
46
42
  if (node.type === 'list' && withChildren.children) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Poetry block settings shared by the HTML and Markdown converters. The CSS
3
+ * values mirror the editor's own (packages/react/src/theme/editor.css), so an
4
+ * exported page looks the same outside the editor.
5
+ */
6
+ export declare const POETRY_LAYOUTS: readonly ["single", "two-column", "staggered"];
7
+ export type PoetryLayout = (typeof POETRY_LAYOUTS)[number];
8
+ export declare const POETRY_SCALE_NAMES: readonly ["compact", "normal", "relaxed", "loose"];
9
+ export type PoetryScaleName = (typeof POETRY_SCALE_NAMES)[number];
10
+ export declare const POETRY_COUPLET_GAP: Record<PoetryScaleName, string>;
11
+ export declare const POETRY_GUTTER_PX: Record<PoetryScaleName, number>;
12
+ export declare const POETRY_STAGGER_WIDTH: Record<PoetryScaleName, string>;
13
+ export declare const POETRY_CENTERED_WIDTH = "60%";
14
+ export declare const POETRY_RULE = "1px dashed #8a8880";
15
+ export declare function scaleOrDefault(value: unknown): PoetryScaleName;
16
+ export declare function layoutOrDefault(value: unknown): PoetryLayout;
17
+ //# sourceMappingURL=poetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"poetry.d.ts","sourceRoot":"","sources":["../../src/shared/poetry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,cAAc,gDAAiD,CAAC;AAC7E,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3D,eAAO,MAAM,kBAAkB,oDAAqD,CAAC;AACrF,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAK9D,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAK5D,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,CAKhE,CAAC;AAEF,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAC3C,eAAO,MAAM,WAAW,uBAAuB,CAAC;AAEhD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAE9D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAE5D"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Poetry block settings shared by the HTML and Markdown converters. The CSS
3
+ * values mirror the editor's own (packages/react/src/theme/editor.css), so an
4
+ * exported page looks the same outside the editor.
5
+ */
6
+ export const POETRY_LAYOUTS = ['single', 'two-column', 'staggered'];
7
+ export const POETRY_SCALE_NAMES = ['compact', 'normal', 'relaxed', 'loose'];
8
+ export const POETRY_COUPLET_GAP = {
9
+ compact: '0.4em',
10
+ normal: '1em',
11
+ relaxed: '1.8em',
12
+ loose: '2.8em',
13
+ };
14
+ export const POETRY_GUTTER_PX = {
15
+ compact: 8,
16
+ normal: 20,
17
+ relaxed: 32,
18
+ loose: 48,
19
+ };
20
+ export const POETRY_STAGGER_WIDTH = {
21
+ compact: '50%',
22
+ normal: '65%',
23
+ relaxed: '75%',
24
+ loose: '85%',
25
+ };
26
+ export const POETRY_CENTERED_WIDTH = '60%';
27
+ export const POETRY_RULE = '1px dashed #8a8880';
28
+ export function scaleOrDefault(value) {
29
+ return POETRY_SCALE_NAMES.find((name) => name === value) ?? 'normal';
30
+ }
31
+ export function layoutOrDefault(value) {
32
+ return POETRY_LAYOUTS.find((name) => name === value) ?? 'single';
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-converters",
3
- "version": "0.1.18-dev.156",
3
+ "version": "0.1.18-dev.162",
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": {