From f51f6cc7eb38c6e04b05c13c74c4034e4fd54f2c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Sun, 1 Dec 2024 10:51:01 +0100 Subject: [PATCH] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8=20ad?= =?UTF-8?q?d=20ability=20to=20export=20to=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../block/__tests__/Fragment-export.spec.ts | 182 ++++++++++++++---- .../export/{toJsonMl.ts => export.ts} | 6 + src/json-ml/toHtml.ts | 20 +- 3 files changed, 162 insertions(+), 46 deletions(-) rename src/json-crdt-extensions/peritext/export/{toJsonMl.ts => export.ts} (77%) diff --git a/src/json-crdt-extensions/peritext/block/__tests__/Fragment-export.spec.ts b/src/json-crdt-extensions/peritext/block/__tests__/Fragment-export.spec.ts index 6a36dd9b07..a7edc7ad35 100644 --- a/src/json-crdt-extensions/peritext/block/__tests__/Fragment-export.spec.ts +++ b/src/json-crdt-extensions/peritext/block/__tests__/Fragment-export.spec.ts @@ -1,50 +1,150 @@ import {type Kit, runAlphabetKitTestSuite} from '../../__tests__/setup'; -import {toJsonMl} from '../../export/toJsonMl'; +import {toHtml, toJsonMl} from '../../export/export'; import {CommonSliceType} from '../../slice'; const runTests = (setup: () => Kit) => { - test('can export two paragraphs', () => { - const {editor, peritext} = setup(); - editor.cursor.setAt(10); - editor.saved.insMarker(CommonSliceType.p); - peritext.refresh(); - const fragment = peritext.fragment(peritext.rangeAt(4, 10)); - fragment.refresh(); - const html = toJsonMl(fragment.toJson()); - expect(html).toEqual([ - '', - null, - ['p', null, 'efghij'], - ['p', null, 'klm'], - ]); + describe('JSON-ML', () => { + test('can export two paragraphs', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const html = toJsonMl(fragment.toJson()); + expect(html).toEqual([ + '', + null, + ['p', null, 'efghij'], + ['p', null, 'klm'], + ]); + }); + + test('can export two paragraphs with inline formatting', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + editor.cursor.setAt(6, 2); + editor.saved.insOverwrite(CommonSliceType.b); + editor.cursor.setAt(7, 2); + editor.saved.insOverwrite(CommonSliceType.i); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const html = toJsonMl(fragment.toJson()); + expect(html).toEqual([ + '', + null, + ['p', null, + 'ef', + ['b', null, 'g'], + ['i', null, + ['b', null, 'h'], + ], + ['i', null, 'i'], + 'j', + ], + ['p', null, 'klm'], + ]); + }); }); - test('can export two paragraphs with inline formatting', () => { - const {editor, peritext} = setup(); - editor.cursor.setAt(10); - editor.saved.insMarker(CommonSliceType.p); - editor.cursor.setAt(6, 2); - editor.saved.insOverwrite(CommonSliceType.b); - editor.cursor.setAt(7, 2); - editor.saved.insOverwrite(CommonSliceType.i); - peritext.refresh(); - const fragment = peritext.fragment(peritext.rangeAt(4, 10)); - fragment.refresh(); - const html = toJsonMl(fragment.toJson()); - expect(html).toEqual([ - '', - null, - ['p', null, - 'ef', - ['b', null, 'g'], - ['i', null, - ['b', null, 'h'], - ], - ['i', null, 'i'], - 'j', - ], - ['p', null, 'klm'], - ]); + describe('HTML', () => { + test('can export two paragraphs', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const html = toHtml(fragment.toJson()); + expect(html).toBe('

efghij

klm

'); + }); + + test('can export two paragraphs (formatted)', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const html = toHtml(fragment.toJson(), ' '); + expect(html).toBe('

efghij

\n

klm

'); + }); + + test('can export two paragraphs (formatted and wrapped in
)', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const json = fragment.toJson(); + json[0] = 'div'; + const html = toHtml(json, ' '); + expect(html).toBe('
\n

efghij

\n

klm

\n
'); + }); + + test('can export two paragraphs with inline formatting', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + editor.cursor.setAt(6, 2); + editor.saved.insOverwrite(CommonSliceType.b); + editor.cursor.setAt(7, 2); + editor.saved.insOverwrite(CommonSliceType.i); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const json = fragment.toJson(); + const html = toHtml(json, ''); + expect(html).toEqual('

efghij

klm

'); + }); + + test('can export two paragraphs with inline formatting (formatted)', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + editor.cursor.setAt(6, 2); + editor.saved.insOverwrite(CommonSliceType.b); + editor.cursor.setAt(7, 2); + editor.saved.insOverwrite(CommonSliceType.i); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const json = fragment.toJson(); + const html = toHtml(json, ' '); + expect(html).toEqual('

\n ef\n g\n \n h\n \n i\n j\n

\n

klm

'); + }); + + test('can export two paragraphs with inline formatting (formatted, wrapped in
)', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(CommonSliceType.p); + editor.cursor.setAt(6, 2); + editor.saved.insOverwrite(CommonSliceType.b); + editor.cursor.setAt(7, 2); + editor.saved.insOverwrite(CommonSliceType.i); + peritext.refresh(); + const fragment = peritext.fragment(peritext.rangeAt(4, 10)); + fragment.refresh(); + const json = fragment.toJson(); + json[0] = 'div'; + const html = toHtml(json, ' '); + expect('\n' + html).toEqual(` +
+

+ ef + g + + h + + i + j +

+

klm

+
`); + }); }); }; diff --git a/src/json-crdt-extensions/peritext/export/toJsonMl.ts b/src/json-crdt-extensions/peritext/export/export.ts similarity index 77% rename from src/json-crdt-extensions/peritext/export/toJsonMl.ts rename to src/json-crdt-extensions/peritext/export/export.ts index bfc5acd664..67a10e81b7 100644 --- a/src/json-crdt-extensions/peritext/export/toJsonMl.ts +++ b/src/json-crdt-extensions/peritext/export/export.ts @@ -1,4 +1,5 @@ import {SliceTypeName} from "../slice"; +import {toHtml as _toHtml} from "../../../json-ml/toHtml"; import type {JsonMlNode} from "../../../json-ml"; import type {PeritextMlNode} from "../block/types"; @@ -13,3 +14,8 @@ export const toJsonMl = (json: PeritextMlNode): JsonMlNode => { for (let i = 0; i < length; i++) htmlNode.push(toJsonMl(children[i])); return htmlNode; }; + +export const toHtml = (json: PeritextMlNode, tab?: string): string => { + const jsonml = toJsonMl(json); + return _toHtml(jsonml, tab); +}; diff --git a/src/json-ml/toHtml.ts b/src/json-ml/toHtml.ts index fa299eb77a..019c2a64a3 100644 --- a/src/json-ml/toHtml.ts +++ b/src/json-ml/toHtml.ts @@ -8,13 +8,23 @@ export const toHtml = (node: JsonMlNode, tab: string = '', ident: string = ''): if (typeof node === 'string') return ident + escapeText(node); const [tag, attrs, ...children] = node; const childrenLength = children.length; - let attrStr = ''; + const isFragment = !tag; + const childrenIdent = ident + (isFragment ? '' : tab); + const doIdent = !!tab; let childrenStr = ''; - const childrenIdent = ident + tab; - for (let i = 0; i < childrenLength; i++) childrenStr += toHtml(children[i], tab, childrenIdent) + (tab ? '\n' : ''); - if (!tag) return childrenStr; + let textOnlyChildren = true; + for (let i = 0; i < childrenLength; i++) if (typeof children[i] !== 'string') { + textOnlyChildren = false; + break; + } + if (textOnlyChildren) for (let i = 0; i < childrenLength; i++) + childrenStr += escapeText(children[i] as string); + else for (let i = 0; i < childrenLength; i++) + childrenStr += (doIdent ? ((!isFragment || i) ? '\n' : '') : '') + toHtml(children[i], tab, childrenIdent); + if (isFragment) return childrenStr; + let attrStr = ''; if (attrs) for (const key in attrs) attrStr += ' ' + key + '="' + escapeAttr(attrs[key] + '') + '"'; const htmlHead = '<' + tag + attrStr; return ident + - (childrenStr ? (htmlHead + '>' + (tab ? '\n' : '') + childrenStr + ident + '') : htmlHead + ' />'); + (childrenStr ? (htmlHead + '>' + childrenStr + ((doIdent && !textOnlyChildren) ? '\n' + ident : '') + '') : htmlHead + ' />'); };