@inshapardaz/likhari-converters 0.1.0

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,8 @@
1
+ import type { FormatConverter } from '../types.js';
2
+ /**
3
+ * Placeholder — HTML import/export is Phase 2 work (lexical-editor-spec.md
4
+ * §13). Registered now so FormatRegistry's shape is stable and callers get a
5
+ * clear error instead of a missing format.
6
+ */
7
+ export declare const htmlConverter: FormatConverter;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/html/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,eAQ3B,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Placeholder — HTML import/export is Phase 2 work (lexical-editor-spec.md
3
+ * §13). Registered now so FormatRegistry's shape is stable and callers get a
4
+ * clear error instead of a missing format.
5
+ */
6
+ export const htmlConverter = {
7
+ id: 'html',
8
+ serialize() {
9
+ throw new Error('@inshapardaz/likhari-converters: HTML export is not implemented yet (Phase 2, see docs/lexical-editor-spec.md §13).');
10
+ },
11
+ parse() {
12
+ throw new Error('@inshapardaz/likhari-converters: HTML import is not implemented yet (Phase 2, see docs/lexical-editor-spec.md §13).');
13
+ },
14
+ };
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+ export * from './registry.js';
3
+ export { plainTextConverter } from './plain-text/index.js';
4
+ export { lexicalJsonConverter } from './lexical-json/index.js';
5
+ export { markdownConverter } from './markdown/index.js';
6
+ export { htmlConverter } from './html/index.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './types.js';
2
+ export * from './registry.js';
3
+ export { plainTextConverter } from './plain-text/index.js';
4
+ export { lexicalJsonConverter } from './lexical-json/index.js';
5
+ export { markdownConverter } from './markdown/index.js';
6
+ export { htmlConverter } from './html/index.js';
@@ -0,0 +1,8 @@
1
+ import type { FormatConverter } from '../types.js';
2
+ /**
3
+ * The native/lossless format — the only one that round-trips 100% of state
4
+ * (custom nodes, exact attributes). Used for autosave and internal storage
5
+ * (lexical-editor-spec.md §2).
6
+ */
7
+ export declare const lexicalJsonConverter: FormatConverter;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lexical-json/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,eAUlC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The native/lossless format — the only one that round-trips 100% of state
3
+ * (custom nodes, exact attributes). Used for autosave and internal storage
4
+ * (lexical-editor-spec.md §2).
5
+ */
6
+ export const lexicalJsonConverter = {
7
+ id: 'lexical-json',
8
+ serialize(editorState) {
9
+ return JSON.stringify(editorState);
10
+ },
11
+ parse(input) {
12
+ return JSON.parse(input);
13
+ },
14
+ };
@@ -0,0 +1,9 @@
1
+ import type { FormatConverter } from '../types.js';
2
+ /**
3
+ * Placeholder — the extended Markdown dialect (lexical-editor-spec.md §2.1)
4
+ * is Phase 2 work (§13) and needs its own spec doc + sign-off before the
5
+ * converter is built against it. Registered now so FormatRegistry's shape
6
+ * is stable and callers get a clear error instead of a missing format.
7
+ */
8
+ export declare const markdownConverter: FormatConverter;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/markdown/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,eAY/B,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Placeholder — the extended Markdown dialect (lexical-editor-spec.md §2.1)
3
+ * is Phase 2 work (§13) and needs its own spec doc + sign-off before the
4
+ * converter is built against it. Registered now so FormatRegistry's shape
5
+ * is stable and callers get a clear error instead of a missing format.
6
+ */
7
+ export const markdownConverter = {
8
+ id: 'markdown',
9
+ serialize() {
10
+ throw new Error('@inshapardaz/likhari-converters: the extended Markdown dialect is not implemented yet (Phase 2, see docs/lexical-editor-spec.md §2.1 and §13).');
11
+ },
12
+ parse() {
13
+ throw new Error('@inshapardaz/likhari-converters: the extended Markdown dialect is not implemented yet (Phase 2, see docs/lexical-editor-spec.md §2.1 and §13).');
14
+ },
15
+ };
@@ -0,0 +1,3 @@
1
+ import type { FormatConverter } from '../types.js';
2
+ export declare const plainTextConverter: FormatConverter;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;AAkDhD,eAAO,MAAM,kBAAkB,EAAE,eAkDhC,CAAC"}
@@ -0,0 +1,81 @@
1
+ const LIST_ITEM_PREFIX = {
2
+ bullet: () => '- ',
3
+ number: (index) => `${index + 1}. `,
4
+ check: () => '- ',
5
+ };
6
+ /**
7
+ * Walks a SerializedEditorState tree and produces its plain-text projection.
8
+ * Per the fidelity matrix (lexical-editor-spec.md §2.2), plain text strips
9
+ * all formatting — this is a documented, exhaustive mapping per node type,
10
+ * not an ad hoc `.textContent` walk.
11
+ */
12
+ function nodeToText(node, listItemIndex = 0) {
13
+ const withChildren = node;
14
+ if (node.type === 'text' || node.type === 'linebreak') {
15
+ return node.text ?? (node.type === 'linebreak' ? '\n' : '');
16
+ }
17
+ if (node.type === 'list' && withChildren.children) {
18
+ const listType = node.listType ?? 'bullet';
19
+ return withChildren.children
20
+ .map((child, i) => {
21
+ const prefix = LIST_ITEM_PREFIX[listType]?.(i) ?? '- ';
22
+ return prefix + nodeToText(child, i);
23
+ })
24
+ .join('\n');
25
+ }
26
+ if (withChildren.children) {
27
+ return withChildren.children.map((child) => nodeToText(child, listItemIndex)).join('');
28
+ }
29
+ return '';
30
+ }
31
+ function blockToText(node) {
32
+ return nodeToText(node);
33
+ }
34
+ export const plainTextConverter = {
35
+ id: 'plain-text',
36
+ serialize(editorState) {
37
+ const root = editorState.root;
38
+ const blocks = (root.children ?? []).map(blockToText);
39
+ return blocks.join('\n\n');
40
+ },
41
+ parse(input) {
42
+ const paragraphs = input.split(/\r?\n\r?\n/);
43
+ const children = paragraphs.map((paragraph) => ({
44
+ type: 'paragraph',
45
+ children: paragraph
46
+ ? [
47
+ {
48
+ type: 'text',
49
+ text: paragraph.replace(/\r?\n/g, '\n'),
50
+ detail: 0,
51
+ format: 0,
52
+ mode: 'normal',
53
+ style: '',
54
+ version: 1,
55
+ },
56
+ ]
57
+ : [],
58
+ direction: 'ltr',
59
+ format: '',
60
+ indent: 0,
61
+ version: 1,
62
+ }));
63
+ return {
64
+ root: {
65
+ type: 'root',
66
+ children: children.length > 0 ? children : [{
67
+ type: 'paragraph',
68
+ children: [],
69
+ direction: 'ltr',
70
+ format: '',
71
+ indent: 0,
72
+ version: 1,
73
+ }],
74
+ direction: 'ltr',
75
+ format: '',
76
+ indent: 0,
77
+ version: 1,
78
+ },
79
+ };
80
+ },
81
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/plain-text/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { plainTextConverter } from './index.js';
3
+ import { lexicalJsonConverter } from '../lexical-json/index.js';
4
+ describe('plainTextConverter', () => {
5
+ it('joins paragraph blocks with a blank line', () => {
6
+ const state = plainTextConverter.parse('Hello world\n\nSecond paragraph');
7
+ const text = plainTextConverter.serialize(state);
8
+ expect(text).toBe('Hello world\n\nSecond paragraph');
9
+ });
10
+ it('round-trips through lexical-json unchanged', () => {
11
+ const state = plainTextConverter.parse('One\n\nTwo\n\nThree');
12
+ const json = lexicalJsonConverter.serialize(state);
13
+ const restored = lexicalJsonConverter.parse(json);
14
+ expect(plainTextConverter.serialize(restored)).toBe('One\n\nTwo\n\nThree');
15
+ });
16
+ it('produces a single empty paragraph for empty input', () => {
17
+ const state = plainTextConverter.parse('');
18
+ expect(state.root.children).toHaveLength(1);
19
+ expect(plainTextConverter.serialize(state)).toBe('');
20
+ });
21
+ });
@@ -0,0 +1,15 @@
1
+ import type { FormatConverter, FormatId, ConverterContext } from './types.js';
2
+ /**
3
+ * Maps every FormatId to its converter pair (architecture doc §8). The
4
+ * public API's getContent(format)/setContent(str, format) is a thin dispatch
5
+ * over this — adding a future format doesn't touch the editor component.
6
+ */
7
+ export declare class FormatRegistry {
8
+ private readonly converters;
9
+ constructor(converters: FormatConverter[]);
10
+ get(format: FormatId): FormatConverter;
11
+ serialize(format: FormatId, editorState: Parameters<FormatConverter['serialize']>[0], ctx?: ConverterContext): string;
12
+ parse(format: FormatId, input: string, ctx?: ConverterContext): import("lexical").SerializedEditorState<import("lexical").SerializedLexicalNode>;
13
+ }
14
+ export declare const defaultFormatRegistry: FormatRegistry;
15
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAM3E;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwC;gBAEvD,UAAU,EAAE,eAAe,EAAE;IAMzC,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe;IAQtC,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM;IAIrH,KAAK,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,gBAAgB;CAG9D;AAED,eAAO,MAAM,qBAAqB,gBAKhC,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { plainTextConverter } from './plain-text/index.js';
2
+ import { lexicalJsonConverter } from './lexical-json/index.js';
3
+ import { markdownConverter } from './markdown/index.js';
4
+ import { htmlConverter } from './html/index.js';
5
+ /**
6
+ * Maps every FormatId to its converter pair (architecture doc §8). The
7
+ * public API's getContent(format)/setContent(str, format) is a thin dispatch
8
+ * over this — adding a future format doesn't touch the editor component.
9
+ */
10
+ export class FormatRegistry {
11
+ constructor(converters) {
12
+ this.converters = new Map();
13
+ for (const converter of converters) {
14
+ this.converters.set(converter.id, converter);
15
+ }
16
+ }
17
+ get(format) {
18
+ const converter = this.converters.get(format);
19
+ if (!converter) {
20
+ throw new Error(`@inshapardaz/likhari-converters: no converter registered for format "${format}".`);
21
+ }
22
+ return converter;
23
+ }
24
+ serialize(format, editorState, ctx) {
25
+ return this.get(format).serialize(editorState, ctx);
26
+ }
27
+ parse(format, input, ctx) {
28
+ return this.get(format).parse(input, ctx);
29
+ }
30
+ }
31
+ export const defaultFormatRegistry = new FormatRegistry([
32
+ plainTextConverter,
33
+ lexicalJsonConverter,
34
+ markdownConverter,
35
+ htmlConverter,
36
+ ]);
@@ -0,0 +1,19 @@
1
+ import type { SerializedEditorState } from 'lexical';
2
+ /** editor-architecture-design.md §8 / §9 — the four interchange formats. */
3
+ export type FormatId = 'markdown' | 'html' | 'plain-text' | 'lexical-json';
4
+ /**
5
+ * Carries cross-cutting data a converter may need beyond the EditorState tree
6
+ * itself — the out-of-band footnote registry (architecture doc §3.3) and a
7
+ * host-supplied image URL resolver — without coupling converters to the
8
+ * editor or React.
9
+ */
10
+ export interface ConverterContext {
11
+ footnoteRegistry?: Record<string, string>;
12
+ resolveImageUrl?: (src: string) => string;
13
+ }
14
+ export interface FormatConverter {
15
+ readonly id: FormatId;
16
+ serialize(editorState: SerializedEditorState, ctx?: ConverterContext): string;
17
+ parse(input: string, ctx?: ConverterContext): SerializedEditorState;
18
+ }
19
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD,4EAA4E;AAC5E,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,GAAG,cAAc,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,SAAS,CAAC,WAAW,EAAE,qBAAqB,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IAC9E,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,gBAAgB,GAAG,qBAAqB,CAAC;CACrE"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@inshapardaz/likhari-converters",
3
+ "version": "0.1.0",
4
+ "description": "Format transformers (Markdown/HTML/plain-text/Lexical JSON) against the canonical Lexical EditorState, isolated from UI.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/inshapardaz/likhari"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "sideEffects": false,
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs ./dist",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "dependencies": {
32
+ "lexical": "^0.20.0"
33
+ }
34
+ }