@inshapardaz/likhari-converters 0.1.45-dev.338 → 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.
- package/dist/convert/clean.d.ts +10 -0
- package/dist/convert/clean.d.ts.map +1 -0
- package/dist/convert/clean.js +104 -0
- package/dist/convert/index.d.ts +34 -0
- package/dist/convert/index.d.ts.map +1 -0
- package/dist/convert/index.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inshapardaz/likhari-converters",
|
|
3
|
-
"version": "0.1.45-dev.
|
|
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": {
|