Skip to content

Commit

Permalink
feat(json-crdt-extensions): 🎸 add ability to export to HTML
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Dec 1, 2024
1 parent b3e7e5c commit f51f6cc
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -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('<p>efghij</p><p>klm</p>');
});

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('<p>efghij</p>\n<p>klm</p>');
});

test('can export two paragraphs (formatted and wrapped in <div>)', () => {
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('<div>\n <p>efghij</p>\n <p>klm</p>\n</div>');
});

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('<p>ef<b>g</b><i><b>h</b></i><i>i</i>j</p><p>klm</p>');
});

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('<p>\n ef\n <b>g</b>\n <i>\n <b>h</b>\n </i>\n <i>i</i>\n j\n</p>\n<p>klm</p>');
});

test('can export two paragraphs with inline formatting (formatted, wrapped in <div>)', () => {
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(`
<div>
<p>
ef
<b>g</b>
<i>
<b>h</b>
</i>
<i>i</i>
j
</p>
<p>klm</p>
</div>`);
});
});
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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);
};
20 changes: 15 additions & 5 deletions src/json-ml/toHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + '</' + tag + '>') : htmlHead + ' />');
(childrenStr ? (htmlHead + '>' + childrenStr + ((doIdent && !textOnlyChildren) ? '\n' + ident : '') + '</' + tag + '>') : htmlHead + ' />');
};

0 comments on commit f51f6cc

Please sign in to comment.