@inshapardaz/likhari-converters 0.1.45-dev.337 → 0.1.45-dev.339

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.
@@ -0,0 +1,10 @@
1
+ import type { SerializedEditorState } from 'lexical';
2
+ /**
3
+ * Tidies a document in place, the way the editor's "clean up text" does and a little further:
4
+ * Unicode NFC (Arabic-script text can store the same letters in different code points), runs of
5
+ * spaces or tabs collapsed to one, text runs with the same formatting merged, empty runs removed,
6
+ * and stray spaces at the start and end of each block trimmed. Text with the code format is left
7
+ * exactly as written. Structure, empty paragraphs and everything that is not text are untouched.
8
+ */
9
+ export declare function cleanState(state: SerializedEditorState): SerializedEditorState;
10
+ //# sourceMappingURL=clean.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clean.d.ts","sourceRoot":"","sources":["../../src/convert/clean.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAyFrD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,qBAAqB,GAAG,qBAAqB,CAG9E"}
@@ -0,0 +1,104 @@
1
+ import { FORMAT_CODE, rootChildren } from '../shared/serialized.js';
2
+ /** Runs of two or more spaces or tabs: the same rule as the editor's own text cleanup. */
3
+ const REPEATED_SPACE = /[ \t]{2,}/g;
4
+ const isText = (node) => node.type === 'text';
5
+ const isCode = (node) => (Number(node.format ?? 0) & FORMAT_CODE) !== 0;
6
+ /** Two text runs that look the same, so can be one run. */
7
+ function sameLook(a, b) {
8
+ return isText(a) && isText(b) && a.format === b.format && a.style === b.style && a.mode === b.mode && a.detail === b.detail;
9
+ }
10
+ /** Drops spaces from the end of the last kept text run (not code), removing the run if nothing is left. */
11
+ function trimEnd(tidy) {
12
+ const last = tidy[tidy.length - 1];
13
+ if (!last || !isText(last) || isCode(last))
14
+ return;
15
+ last.text = String(last.text).replace(/[ \t]+$/, '');
16
+ if (last.text === '')
17
+ tidy.pop();
18
+ }
19
+ /** The text a run of nodes ends with, looking into a trailing link. */
20
+ function endsWithSpace(nodes) {
21
+ const last = nodes[nodes.length - 1];
22
+ if (!last)
23
+ return false;
24
+ if (last.children && !last.children.some((c) => c.children))
25
+ return endsWithSpace(last.children);
26
+ return isText(last) && !isCode(last) && /[ \t]$/.test(String(last.text ?? ''));
27
+ }
28
+ /**
29
+ * Cleans the inline content of one element: merge look-alike runs, tidy spaces, drop empty runs.
30
+ * `trimEdges` trims the start and end of a block; `afterSpace` says the text before these children
31
+ * already ended in a space, so a space they start with is a double one.
32
+ */
33
+ function cleanInline(children, trimEdges, afterSpace = false) {
34
+ // Merge neighbours that look the same, so a space split across two runs is seen as one.
35
+ const merged = [];
36
+ for (const child of children) {
37
+ const last = merged[merged.length - 1];
38
+ if (last && sameLook(last, child))
39
+ last.text = String(last.text ?? '') + String(child.text ?? '');
40
+ else
41
+ merged.push(child);
42
+ }
43
+ let previousEndsWithSpace = trimEdges || afterSpace; // at the start of a block a space is a stray one
44
+ const tidy = [];
45
+ for (const node of merged) {
46
+ if (!isText(node)) {
47
+ if (node.type === 'link' || node.type === 'autolink') {
48
+ // A link's text is cleaned in place, but a space doubled across its edge is still a double space.
49
+ node.children = cleanInline(node.children ?? [], false, previousEndsWithSpace);
50
+ previousEndsWithSpace = endsWithSpace(node.children);
51
+ }
52
+ else if (node.type === 'linebreak') {
53
+ trimEnd(tidy); // spaces at the end of a line
54
+ previousEndsWithSpace = true;
55
+ }
56
+ else if (node.children) {
57
+ // A nested block, a list inside a list item: the text before it ends there.
58
+ trimEnd(tidy);
59
+ cleanNode(node);
60
+ previousEndsWithSpace = true;
61
+ }
62
+ else {
63
+ previousEndsWithSpace = node.type === 'tab';
64
+ }
65
+ tidy.push(node);
66
+ continue;
67
+ }
68
+ let text = String(node.text ?? '').normalize('NFC');
69
+ if (!isCode(node)) {
70
+ text = text.replace(REPEATED_SPACE, ' ');
71
+ if (previousEndsWithSpace)
72
+ text = text.replace(/^[ \t]+/, '');
73
+ }
74
+ if (text === '')
75
+ continue;
76
+ node.text = text;
77
+ previousEndsWithSpace = !isCode(node) && /[ \t]$/.test(text);
78
+ tidy.push(node);
79
+ }
80
+ if (trimEdges)
81
+ trimEnd(tidy);
82
+ return tidy;
83
+ }
84
+ function cleanNode(node) {
85
+ const children = node.children;
86
+ if (!children)
87
+ return;
88
+ const hasInline = children.some((child) => isText(child) || child.type === 'linebreak' || child.type === 'tab' || child.type === 'link' || child.type === 'autolink');
89
+ if (hasInline)
90
+ node.children = cleanInline(children, true);
91
+ else
92
+ children.forEach(cleanNode);
93
+ }
94
+ /**
95
+ * Tidies a document in place, the way the editor's "clean up text" does and a little further:
96
+ * Unicode NFC (Arabic-script text can store the same letters in different code points), runs of
97
+ * spaces or tabs collapsed to one, text runs with the same formatting merged, empty runs removed,
98
+ * and stray spaces at the start and end of each block trimmed. Text with the code format is left
99
+ * exactly as written. Structure, empty paragraphs and everything that is not text are untouched.
100
+ */
101
+ export function cleanState(state) {
102
+ rootChildren(state).forEach(cleanNode);
103
+ return state;
104
+ }
@@ -0,0 +1,34 @@
1
+ import type { SerializedEditorState } from 'lexical';
2
+ import { type FormatRegistry } from '../registry.js';
3
+ import type { ConverterContext, FormatId } from '../types.js';
4
+ import { cleanState } from './clean.js';
5
+ export { cleanState };
6
+ export interface ConvertContentOptions {
7
+ /** The format `content` is in. */
8
+ inputFormat: FormatId;
9
+ /** The format to write. */
10
+ outputFormat: FormatId;
11
+ /**
12
+ * Tidy the text on the way (see `cleanState`): Unicode NFC, repeated spaces collapsed, stray
13
+ * spaces at the edges of blocks trimmed, look-alike runs merged. On by default; pass `false`
14
+ * to convert exactly what is there.
15
+ */
16
+ clean?: boolean;
17
+ /** Passed to the parse and the serialize (image URL resolver and the like). */
18
+ context?: ConverterContext;
19
+ /** Defaults to the registry with all four built-in formats. */
20
+ registry?: FormatRegistry;
21
+ }
22
+ /** Reads `content` and returns it as Lexical state, cleaned unless `clean` is false. */
23
+ export declare function convertContentToState(content: string, options: Omit<ConvertContentOptions, 'outputFormat'>): SerializedEditorState;
24
+ /**
25
+ * Converts a document from one format to another through the editor's own parsers and
26
+ * serializers, so the result is exactly what the editor would save in that format. Needs no
27
+ * editor or DOM for Markdown, Lexical JSON and plain text (HTML input uses `DOMParser`).
28
+ *
29
+ * Going to a format that holds less than the input (rich text to plain text, a table to
30
+ * Markdown, ...) degrades the way the format's fidelity matrix says; see
31
+ * `docs/lexical-editor-spec.md` and `docs/markdown-dialect.md`.
32
+ */
33
+ export declare function convertContent(content: string, options: ConvertContentOptions): string;
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/convert/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EAAyB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,MAAM,WAAW,qBAAqB;IACpC,kCAAkC;IAClC,WAAW,EAAE,QAAQ,CAAC;IACtB,2BAA2B;IAC3B,YAAY,EAAE,QAAQ,CAAC;IACvB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED,wFAAwF;AACxF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,cAAc,CAAC,GAAG,qBAAqB,CAUlI;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAGtF"}
@@ -0,0 +1,29 @@
1
+ import { defaultFormatRegistry } from '../registry.js';
2
+ import { cleanState } from './clean.js';
3
+ export { cleanState };
4
+ /** Reads `content` and returns it as Lexical state, cleaned unless `clean` is false. */
5
+ export function convertContentToState(content, options) {
6
+ const { inputFormat, clean = true, context, registry = defaultFormatRegistry } = options;
7
+ let state;
8
+ try {
9
+ state = registry.parse(inputFormat, content, context);
10
+ }
11
+ catch (error) {
12
+ const reason = error instanceof Error ? error.message : String(error);
13
+ throw new Error(`convertContent: the input is not valid ${inputFormat}: ${reason}`);
14
+ }
15
+ return clean ? cleanState(state) : state;
16
+ }
17
+ /**
18
+ * Converts a document from one format to another through the editor's own parsers and
19
+ * serializers, so the result is exactly what the editor would save in that format. Needs no
20
+ * editor or DOM for Markdown, Lexical JSON and plain text (HTML input uses `DOMParser`).
21
+ *
22
+ * Going to a format that holds less than the input (rich text to plain text, a table to
23
+ * Markdown, ...) degrades the way the format's fidelity matrix says; see
24
+ * `docs/lexical-editor-spec.md` and `docs/markdown-dialect.md`.
25
+ */
26
+ export function convertContent(content, options) {
27
+ const registry = options.registry ?? defaultFormatRegistry;
28
+ return registry.serialize(options.outputFormat, convertContentToState(content, options), options.context);
29
+ }
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export { lexicalJsonConverter } from './lexical-json/index.js';
5
5
  export { markdownConverter } from './markdown/index.js';
6
6
  export { htmlConverter } from './html/index.js';
7
7
  export { mergePages, mergePagesToState, type MergePagesOptions, type PageInput } from './merge/index.js';
8
+ export { convertContent, convertContentToState, cleanState, type ConvertContentOptions } from './convert/index.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAChG,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,UAAU,EAAE,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export { lexicalJsonConverter } from './lexical-json/index.js';
5
5
  export { markdownConverter } from './markdown/index.js';
6
6
  export { htmlConverter } from './html/index.js';
7
7
  export { mergePages, mergePagesToState } from './merge/index.js';
8
+ export { convertContent, convertContentToState, cleanState } from './convert/index.js';
@@ -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;AAmUjD,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;AAoUjD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAO3F"}
@@ -264,6 +264,8 @@ function blocks(nodes, extra = {}) {
264
264
  case 'leafDirective': {
265
265
  if (node.name === 'pagebreak')
266
266
  out.push({ type: 'page-break', version: 1 });
267
+ else if (node.name === 'blank')
268
+ out.push(paragraph());
267
269
  else if (node.name === 'figure') {
268
270
  const attrs = node.attributes ?? {};
269
271
  const src = safeImageSrc(attrs.src ?? '');
@@ -302,7 +304,7 @@ function restoreUnknownDirectives(node, source) {
302
304
  node.children = node.children.map((child) => {
303
305
  const c = child;
304
306
  const unknownText = c.type === 'textDirective' && !KNOWN_TEXT_DIRECTIVES.has(c.name ?? '');
305
- const unknownLeaf = c.type === 'leafDirective' && c.name !== 'pagebreak' && c.name !== 'figure';
307
+ const unknownLeaf = c.type === 'leafDirective' && c.name !== 'pagebreak' && c.name !== 'blank' && c.name !== 'figure';
306
308
  if ((unknownText || unknownLeaf) && c.position?.start.offset !== undefined && c.position.end.offset !== undefined) {
307
309
  const raw = source.slice(c.position.start.offset, c.position.end.offset);
308
310
  return unknownLeaf ? { type: 'paragraph', children: [{ type: 'text', value: raw }] } : { type: 'text', value: raw };
@@ -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;AA6C7D,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAWjD;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"}
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;AA6C7D,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAWjD;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,CAc9F"}
@@ -2,7 +2,7 @@ import { unified } from 'unified';
2
2
  import remarkStringify from 'remark-stringify';
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, rootChildren, } from '../shared/serialized.js';
5
+ import { FORMAT_BOLD, FORMAT_CODE, FORMAT_HIGHLIGHT, FORMAT_ITALIC, FORMAT_STRIKETHROUGH, FORMAT_SUBSCRIPT, FORMAT_SUPERSCRIPT, FORMAT_UNDERLINE, isEmptyParagraph, rootChildren, } from '../shared/serialized.js';
6
6
  import { directive } from './mdast.js';
7
7
  import { layoutOrDefault, scaleOrDefault } from '../shared/poetry.js';
8
8
  import { nodeToHtml, setFootnoteOrder } from '../html/serialize.js';
@@ -333,7 +333,14 @@ function blockToMd(node, ctx) {
333
333
  export function serializeMarkdown(state, ctx) {
334
334
  const blocks = rootChildren(state);
335
335
  setFootnoteOrder(blocks);
336
- const tree = { type: 'root', children: blocks.flatMap((n) => blockToMd(n, ctx)) };
336
+ // Markdown has no empty paragraph, so a run of them at the end of the document (which marks where a
337
+ // page's last paragraph ends, see mergePages) is written as `::blank` lines to survive a save.
338
+ let end = blocks.length;
339
+ while (end > 0 && (isEmptyParagraph(blocks[end - 1]) || blocks[end - 1].type === 'footnote-list'))
340
+ end -= 1;
341
+ // A document that is nothing but empty paragraphs is still just empty (end === 0).
342
+ const children = blocks.flatMap((n, i) => end > 0 && i >= end && isEmptyParagraph(n) ? [directive('leafDirective', 'blank', {})] : blockToMd(n, ctx));
343
+ const tree = { type: 'root', children };
337
344
  // remark-stringify consumes mdast; our MdNode is a structural subset of it.
338
345
  return String(processor.stringify(tree)).replace(/\n+$/, '\n').replace(/^\n+$/, '');
339
346
  }
@@ -11,13 +11,11 @@ export declare function endsMidSentence(block: SNode): boolean;
11
11
  * and alignment. A direction left unset on either side is compatible with anything.
12
12
  */
13
13
  export declare function canJoinParagraphs(previous: SNode | undefined, next: SNode | undefined): boolean;
14
- /** A paragraph with nothing in it: only whitespace and line breaks. */
15
- export declare function isEmptyParagraph(block: SNode | undefined): boolean;
16
14
  /**
17
- * Whether a page's text ends with an empty line, which marks the end of a paragraph. Markdown
18
- * cannot hold an empty paragraph (the parser drops it), so it is read from the raw text: the
19
- * text must end with a blank line (`\n\n`), not just a final newline. For every other format it
20
- * is a trailing empty paragraph in the parsed page.
15
+ * Whether a page's text ends with an empty line, which marks the end of a paragraph: a trailing
16
+ * empty paragraph in the parsed page (Markdown writes one as `::blank`). Markdown written by hand
17
+ * may instead just end with a blank line (`\n\n`, not only a final newline), which the parser
18
+ * would drop, so for Markdown the raw text is checked too.
21
19
  */
22
20
  export declare function endsWithBlankLine(rawFormat: string, rawText: string, blocks: SNode[]): boolean;
23
21
  /** Appends `next`'s content to `previous` (which is changed), separated by one space unless one is already there. */
@@ -1 +1 @@
1
- {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../../src/merge/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAc5D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAGrD;AAKD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,EAAE,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAM/F;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAG9F;AAKD,qHAAqH;AACrH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,CAOjE"}
1
+ {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../../src/merge/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAc9E;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAGrD;AAKD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,KAAK,GAAG,SAAS,EAAE,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAM/F;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAG9F;AAKD,qHAAqH;AACrH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,CAOjE"}
@@ -1,4 +1,4 @@
1
- import { textNode } from '../shared/serialized.js';
1
+ import { isEmptyParagraph, textNode } from '../shared/serialized.js';
2
2
  /** Marks that end a sentence, in Urdu and Latin script: ۔ ؟ ! ? . … and the CJK full stop. */
3
3
  const SENTENCE_END = /[۔؟!?.…。]/;
4
4
  /** Closing quotes and brackets that may follow the mark and still end the sentence: ” ’ " ' ) ] » */
@@ -38,20 +38,16 @@ export function canJoinParagraphs(previous, next) {
38
38
  return false;
39
39
  return alignment(previous) === alignment(next);
40
40
  }
41
- /** A paragraph with nothing in it: only whitespace and line breaks. */
42
- export function isEmptyParagraph(block) {
43
- return !!block && block.type === 'paragraph' && plainText(block).trim() === '';
44
- }
45
41
  /**
46
- * Whether a page's text ends with an empty line, which marks the end of a paragraph. Markdown
47
- * cannot hold an empty paragraph (the parser drops it), so it is read from the raw text: the
48
- * text must end with a blank line (`\n\n`), not just a final newline. For every other format it
49
- * is a trailing empty paragraph in the parsed page.
42
+ * Whether a page's text ends with an empty line, which marks the end of a paragraph: a trailing
43
+ * empty paragraph in the parsed page (Markdown writes one as `::blank`). Markdown written by hand
44
+ * may instead just end with a blank line (`\n\n`, not only a final newline), which the parser
45
+ * would drop, so for Markdown the raw text is checked too.
50
46
  */
51
47
  export function endsWithBlankLine(rawFormat, rawText, blocks) {
52
- if (rawFormat === 'markdown')
53
- return /(\r?\n[ \t]*){2,}$/.test(rawText);
54
- return isEmptyParagraph(blocks[blocks.length - 1]);
48
+ if (isEmptyParagraph(blocks[blocks.length - 1]))
49
+ return true;
50
+ return rawFormat === 'markdown' && /(\r?\n[ \t]*){2,}$/.test(rawText);
55
51
  }
56
52
  const startsWithSpace = (children) => /^\s/.test(plainText({ type: 'x', children: children.slice(0, 1) }));
57
53
  const endsWithSpace = (children) => /\s$/.test(plainText({ type: 'x', children: children.slice(-1) }));
@@ -1,6 +1,6 @@
1
1
  import { defaultFormatRegistry } from '../registry.js';
2
- import { elementBase, makeState, rootChildren } from '../shared/serialized.js';
3
- import { canJoinParagraphs, endsMidSentence, endsWithBlankLine, isEmptyParagraph, joinParagraphs } from './continuation.js';
2
+ import { elementBase, isEmptyParagraph, makeState, rootChildren } from '../shared/serialized.js';
3
+ import { canJoinParagraphs, endsMidSentence, endsWithBlankLine, joinParagraphs } from './continuation.js';
4
4
  /** Parses each page and returns the combined document as Lexical state, without serializing it. */
5
5
  export function mergePagesToState(pages, options = {}) {
6
6
  const { inputFormat = 'markdown', skipBlankPages = false, joinParagraphs: joinMode, context, registry = defaultFormatRegistry } = options;
@@ -33,4 +33,6 @@ export declare function isInlineNode(node: SNode): boolean;
33
33
  * order, among all such nodes. Walks the whole tree once so every converter
34
34
  * numbers references and their matching footnote-list entries identically. */
35
35
  export declare function collectFootnoteOrder(blocks: SNode[]): Map<string, number>;
36
+ /** A paragraph with nothing in it: no children, or only whitespace and line breaks. */
37
+ export declare function isEmptyParagraph(node: SNode | undefined): boolean;
36
38
  //# 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;AAED;;;8EAG8E;AAC9E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAUzE"}
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;AAED,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAMjE"}
@@ -45,3 +45,9 @@ export function collectFootnoteOrder(blocks) {
45
45
  blocks.forEach(visit);
46
46
  return order;
47
47
  }
48
+ /** A paragraph with nothing in it: no children, or only whitespace and line breaks. */
49
+ export function isEmptyParagraph(node) {
50
+ return (!!node &&
51
+ node.type === 'paragraph' &&
52
+ (node.children ?? []).every((child) => (child.type === 'text' ? String(child.text ?? '').trim() === '' : child.type === 'linebreak')));
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-converters",
3
- "version": "0.1.45-dev.337",
3
+ "version": "0.1.45-dev.339",
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": {