@hcengineering/text-ydoc 0.7.1

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,220 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var import_core = require("@hcengineering/core");
5
+ var import_text = require("@hcengineering/text");
6
+ var import_y_prosemirror = require("y-prosemirror");
7
+ var import_fast_equals = require("fast-equals");
8
+ var import_yjs = require("yjs");
9
+ var import_ydoc = require("../ydoc");
10
+ expect.extend({
11
+ toEqualMarkup(received, expected) {
12
+ const pass = received === expected || (0, import_fast_equals.deepEqual)(JSON.parse(received), JSON.parse(expected));
13
+ return {
14
+ message: /* @__PURE__ */ __name(() => pass ? `Expected markup strings NOT to be equal:
15
+ Expected: ${expected}
16
+ Received: ${received}` : `Expected markup strings to be equal:
17
+ Expected: ${expected}
18
+ Received: ${received}`, "message"),
19
+ pass
20
+ };
21
+ },
22
+ toEqualYdoc(received, expected) {
23
+ const expectedJSON = expected.toJSON();
24
+ const receivedJSON = received.toJSON();
25
+ const pass = (0, import_fast_equals.deepEqual)(expectedJSON, receivedJSON);
26
+ return {
27
+ message: /* @__PURE__ */ __name(() => pass ? `Expected yjs documents NOT to be equal:
28
+ Expected: ${JSON.stringify(expectedJSON)}
29
+ Received: ${JSON.stringify(receivedJSON)}` : `Expected yjs documents to be equal:
30
+ Expected: ${JSON.stringify(expectedJSON)}
31
+ Received: ${JSON.stringify(receivedJSON)}`, "message"),
32
+ pass
33
+ };
34
+ }
35
+ });
36
+ function referenceMarkupToYDoc(markup, field) {
37
+ const ydoc = new import_yjs.Doc({ guid: (0, import_core.generateId)() });
38
+ const fragment = ydoc.getXmlFragment(field);
39
+ (0, import_y_prosemirror.prosemirrorToYXmlFragment)((0, import_text.jsonToPmNode)((0, import_text.markupToJSON)(markup)), fragment);
40
+ return ydoc;
41
+ }
42
+ __name(referenceMarkupToYDoc, "referenceMarkupToYDoc");
43
+ function referenceYDocToMarkup(ydoc, field) {
44
+ const json = (0, import_y_prosemirror.yDocToProsemirrorJSON)(ydoc, field);
45
+ return (0, import_text.jsonToMarkup)(json);
46
+ }
47
+ __name(referenceYDocToMarkup, "referenceYDocToMarkup");
48
+ const markups = [
49
+ {
50
+ name: "text",
51
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}'
52
+ },
53
+ {
54
+ name: "text with bold mark",
55
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello world"}]}]}'
56
+ },
57
+ {
58
+ name: "separate paragraphs with bold mark",
59
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello"}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}'
60
+ },
61
+ {
62
+ name: "mixed text and text with bold mark",
63
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}'
64
+ },
65
+ {
66
+ name: "mixed text with italic and text with bold and italic marks",
67
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}},{"type":"italic","attrs":{}}],"text":"world"}]}]}'
68
+ },
69
+ {
70
+ name: "text with link and italic marks",
71
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","text":"hello world","marks":[{"type":"link","attrs":{"href":"http://example.com","target":"_blank","rel":"noopener noreferrer","class":"cursor-pointer"}},{"type":"italic","attrs":{}}]}]}]}'
72
+ },
73
+ {
74
+ name: "image",
75
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}'
76
+ },
77
+ {
78
+ name: "table with formatting inside",
79
+ markup: '{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"1"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"2"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"codeBlock","content":[{"type":"text","text":"3"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","text":"4"}]}]}]}]}]}'
80
+ },
81
+ {
82
+ name: "non-overlapping marks",
83
+ markup: `{
84
+ "type": "doc",
85
+ "content": [
86
+ {
87
+ "type": "paragraph",
88
+ "content": [
89
+ {
90
+ "type": "text",
91
+ "text": "xy",
92
+ "marks": [
93
+ {
94
+ "type": "inline-comment",
95
+ "attrs": {
96
+ "thread": "1"
97
+ }
98
+ }
99
+ ]
100
+ },
101
+ {
102
+ "type": "text",
103
+ "text": "z",
104
+ "marks": [
105
+ {
106
+ "type": "inline-comment",
107
+ "attrs": {
108
+ "thread": "2"
109
+ }
110
+ }
111
+ ]
112
+ }
113
+ ]
114
+ }
115
+ ]
116
+ }`,
117
+ skipYdocCompare: true
118
+ },
119
+ {
120
+ name: "overlapping marks",
121
+ markup: `{
122
+ "type": "doc",
123
+ "content": [
124
+ {
125
+ "type": "paragraph",
126
+ "content": [
127
+ {
128
+ "type": "text",
129
+ "text": "x",
130
+ "marks": [
131
+ {
132
+ "type": "inline-comment",
133
+ "attrs": {
134
+ "thread": "1"
135
+ }
136
+ }
137
+ ]
138
+ },
139
+ {
140
+ "type": "text",
141
+ "text": "y",
142
+ "marks": [
143
+ {
144
+ "type": "inline-comment",
145
+ "attrs": {
146
+ "thread": "1"
147
+ }
148
+ },
149
+ {
150
+ "type": "inline-comment",
151
+ "attrs": {
152
+ "thread": "2"
153
+ }
154
+ }
155
+ ]
156
+ },
157
+ {
158
+ "type": "text",
159
+ "text": "z",
160
+ "marks": [
161
+ {
162
+ "type": "inline-comment",
163
+ "attrs": {
164
+ "thread": "2"
165
+ }
166
+ }
167
+ ]
168
+ }
169
+ ]
170
+ }
171
+ ]
172
+ }`,
173
+ skipYdocCompare: true
174
+ }
175
+ ];
176
+ describe("markupToYDoc", () => {
177
+ describe.each(markups)("compare with reference", ({ name, markup, skipYdocCompare }) => {
178
+ if (skipYdocCompare !== true) {
179
+ it(name, () => {
180
+ const expected = referenceMarkupToYDoc(markup, "test");
181
+ const actual = (0, import_ydoc.markupToYDoc)(markup, "test");
182
+ expect(actual).toEqualYdoc(expected);
183
+ });
184
+ }
185
+ });
186
+ describe.each(markups)("converts markup to ydoc and back", ({ name, markup }) => {
187
+ it(name, () => {
188
+ const ydoc = (0, import_ydoc.markupToYDoc)(markup, "test");
189
+ const actual = (0, import_ydoc.yDocToMarkup)(ydoc, "test");
190
+ expect(actual).toEqualMarkup(markup);
191
+ });
192
+ });
193
+ });
194
+ describe("yDocToMarkup", () => {
195
+ describe.each(markups)("compare with original", ({ name, markup }) => {
196
+ it(name, () => {
197
+ const ydoc = referenceMarkupToYDoc(markup, "test");
198
+ const actual = (0, import_ydoc.yDocToMarkup)(ydoc, "test");
199
+ expect(actual).toEqualMarkup(markup);
200
+ });
201
+ });
202
+ describe.each(markups)("compare with reference", ({ name, markup }) => {
203
+ it(name, () => {
204
+ const ydoc = referenceMarkupToYDoc(markup, "test");
205
+ const expected = referenceYDocToMarkup(ydoc, "test");
206
+ const actual = (0, import_ydoc.yDocToMarkup)(ydoc, "test");
207
+ expect(actual).toEqualMarkup(expected);
208
+ });
209
+ });
210
+ describe.each(markups)("converts ydoc to markup and back", ({ name, markup, skipYdocCompare }) => {
211
+ if (skipYdocCompare !== true) {
212
+ it(name, () => {
213
+ const expected = referenceMarkupToYDoc(markup, "test");
214
+ const actual = referenceMarkupToYDoc((0, import_ydoc.yDocToMarkup)(expected, "test"), "test");
215
+ expect(actual).toEqualYdoc(expected);
216
+ });
217
+ }
218
+ });
219
+ });
220
+ //# sourceMappingURL=ydoc.test.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/__tests__/ydoc.test.ts"],
4
+ "sourcesContent": ["//\n// Copyright \u00A9 2024 Hardcore Engineering Inc.\n//\n// Licensed under the Eclipse Public License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License. You may\n// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n//\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nimport { type Markup, generateId } from '@hcengineering/core'\nimport { type MarkupNode, jsonToMarkup, jsonToPmNode, markupToJSON } from '@hcengineering/text'\nimport { prosemirrorToYXmlFragment, yDocToProsemirrorJSON } from 'y-prosemirror'\nimport { deepEqual } from 'fast-equals'\nimport { Doc as YDoc } from 'yjs'\nimport { markupToYDoc, yDocToMarkup } from '../ydoc'\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace jest {\n interface Matchers<R> {\n toEqualMarkup: (expected: string) => R\n toEqualYdoc: (expected: YDoc) => R\n }\n }\n}\n\nexpect.extend({\n toEqualMarkup (received: string, expected: string) {\n const pass = received === expected || deepEqual(JSON.parse(received), JSON.parse(expected))\n return {\n message: () =>\n pass\n ? `Expected markup strings NOT to be equal:\\n Expected: ${expected}\\n Received: ${received}`\n : `Expected markup strings to be equal:\\n Expected: ${expected}\\n Received: ${received}`,\n pass\n }\n },\n\n toEqualYdoc (received: YDoc, expected: YDoc) {\n const expectedJSON = expected.toJSON()\n const receivedJSON = received.toJSON()\n\n const pass = deepEqual(expectedJSON, receivedJSON)\n return {\n message: () =>\n pass\n ? `Expected yjs documents NOT to be equal:\\n Expected: ${JSON.stringify(expectedJSON)}\\n Received: ${JSON.stringify(receivedJSON)}`\n : `Expected yjs documents to be equal:\\n Expected: ${JSON.stringify(expectedJSON)}\\n Received: ${JSON.stringify(receivedJSON)}`,\n pass\n }\n }\n})\n\nfunction referenceMarkupToYDoc (markup: Markup, field: string): YDoc {\n const ydoc = new YDoc({ guid: generateId() })\n const fragment = ydoc.getXmlFragment(field)\n prosemirrorToYXmlFragment(jsonToPmNode(markupToJSON(markup)), fragment)\n return ydoc\n}\n\nfunction referenceYDocToMarkup (ydoc: YDoc, field: string): Markup {\n const json = yDocToProsemirrorJSON(ydoc, field)\n return jsonToMarkup(json as MarkupNode)\n}\n\nconst markups: Array<{ name: string, markup: Markup, skipYdocCompare?: boolean }> = [\n {\n name: 'text',\n markup: '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"hello world\"}]}]}'\n },\n {\n name: 'text with bold mark',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"marks\":[{\"type\":\"bold\",\"attrs\":{}}],\"text\":\"hello world\"}]}]}'\n },\n {\n name: 'separate paragraphs with bold mark',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"marks\":[{\"type\":\"bold\",\"attrs\":{}}],\"text\":\"hello\"}]},{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"marks\":[{\"type\":\"bold\",\"attrs\":{}}],\"text\":\"world\"}]}]}'\n },\n {\n name: 'mixed text and text with bold mark',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"hello \"},{\"type\":\"text\",\"marks\":[{\"type\":\"bold\",\"attrs\":{}}],\"text\":\"world\"}]}]}'\n },\n {\n name: 'mixed text with italic and text with bold and italic marks',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"marks\":[{\"type\":\"italic\",\"attrs\":{}}],\"text\":\"hello \"},{\"type\":\"text\",\"marks\":[{\"type\":\"bold\",\"attrs\":{}},{\"type\":\"italic\",\"attrs\":{}}],\"text\":\"world\"}]}]}'\n },\n {\n name: 'text with link and italic marks',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"hello \"},{\"type\":\"text\",\"text\":\"hello world\",\"marks\":[{\"type\":\"link\",\"attrs\":{\"href\":\"http://example.com\",\"target\":\"_blank\",\"rel\":\"noopener noreferrer\",\"class\":\"cursor-pointer\"}},{\"type\":\"italic\",\"attrs\":{}}]}]}]}'\n },\n {\n name: 'image',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"image\",\"attrs\":{\"src\":\"http://example.com/image.jpg\",\"alt\":\"image\"}}]}]}'\n },\n {\n name: 'table with formatting inside',\n markup:\n '{\"type\":\"doc\",\"content\":[{\"type\":\"table\",\"content\":[{\"type\":\"tableRow\",\"content\":[{\"type\":\"tableCell\",\"attrs\":{\"colspan\":1,\"rowspan\":1},\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"marks\":[{\"type\":\"bold\",\"attrs\":{}}],\"text\":\"1\"}]}]},{\"type\":\"tableCell\",\"attrs\":{\"colspan\":1,\"rowspan\":1},\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"marks\":[{\"type\":\"italic\",\"attrs\":{}}],\"text\":\"2\"}]}]}]},{\"type\":\"tableRow\",\"content\":[{\"type\":\"tableCell\",\"attrs\":{\"colspan\":1,\"rowspan\":1},\"content\":[{\"type\":\"codeBlock\",\"content\":[{\"type\":\"text\",\"text\":\"3\"}]}]},{\"type\":\"tableCell\",\"attrs\":{\"colspan\":1,\"rowspan\":1},\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"4\"}]}]}]}]}]}'\n },\n {\n name: 'non-overlapping marks',\n markup: `{\n \"type\": \"doc\",\n \"content\": [\n {\n \"type\": \"paragraph\",\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"xy\",\n \"marks\": [\n {\n \"type\": \"inline-comment\",\n \"attrs\": {\n \"thread\": \"1\"\n }\n }\n ]\n },\n {\n \"type\": \"text\",\n \"text\": \"z\",\n \"marks\": [\n {\n \"type\": \"inline-comment\",\n \"attrs\": {\n \"thread\": \"2\"\n }\n }\n ]\n }\n ]\n }\n ]\n}`,\n skipYdocCompare: true\n },\n {\n name: 'overlapping marks',\n markup: `{\n \"type\": \"doc\",\n \"content\": [\n {\n \"type\": \"paragraph\",\n \"content\": [\n {\n \"type\": \"text\",\n \"text\": \"x\",\n \"marks\": [\n {\n \"type\": \"inline-comment\",\n \"attrs\": {\n \"thread\": \"1\"\n }\n }\n ]\n },\n {\n \"type\": \"text\",\n \"text\": \"y\",\n \"marks\": [\n {\n \"type\": \"inline-comment\",\n \"attrs\": {\n \"thread\": \"1\"\n }\n },\n {\n \"type\": \"inline-comment\",\n \"attrs\": {\n \"thread\": \"2\"\n }\n }\n ]\n },\n {\n \"type\": \"text\",\n \"text\": \"z\",\n \"marks\": [\n {\n \"type\": \"inline-comment\",\n \"attrs\": {\n \"thread\": \"2\"\n }\n }\n ]\n }\n ]\n }\n ]\n}`,\n skipYdocCompare: true\n }\n]\n\ndescribe('markupToYDoc', () => {\n describe.each(markups)('compare with reference', ({ name, markup, skipYdocCompare }) => {\n if (skipYdocCompare !== true) {\n it(name, () => {\n const expected = referenceMarkupToYDoc(markup, 'test')\n const actual = markupToYDoc(markup, 'test')\n expect(actual).toEqualYdoc(expected)\n })\n }\n })\n\n describe.each(markups)('converts markup to ydoc and back', ({ name, markup }) => {\n it(name, () => {\n const ydoc = markupToYDoc(markup, 'test')\n const actual = yDocToMarkup(ydoc, 'test')\n\n expect(actual).toEqualMarkup(markup)\n })\n })\n})\n\ndescribe('yDocToMarkup', () => {\n describe.each(markups)('compare with original', ({ name, markup }) => {\n it(name, () => {\n const ydoc = referenceMarkupToYDoc(markup, 'test')\n const actual = yDocToMarkup(ydoc, 'test')\n\n expect(actual).toEqualMarkup(markup)\n })\n })\n\n describe.each(markups)('compare with reference', ({ name, markup }) => {\n it(name, () => {\n const ydoc = referenceMarkupToYDoc(markup, 'test')\n const expected = referenceYDocToMarkup(ydoc, 'test')\n const actual = yDocToMarkup(ydoc, 'test')\n\n expect(actual).toEqualMarkup(expected)\n })\n })\n\n describe.each(markups)('converts ydoc to markup and back', ({ name, markup, skipYdocCompare }) => {\n if (skipYdocCompare !== true) {\n it(name, () => {\n const expected = referenceMarkupToYDoc(markup, 'test')\n const actual = referenceMarkupToYDoc(yDocToMarkup(expected, 'test'), 'test')\n\n expect(actual).toEqualYdoc(expected)\n })\n }\n })\n})\n"],
5
+ "mappings": "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",
6
+ "names": ["YDoc"]
7
+ }
package/lib/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
15
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
16
+ var index_exports = {};
17
+ module.exports = __toCommonJS(index_exports);
18
+ __reExport(index_exports, require("./ydoc"), module.exports);
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["//\n// Copyright \u00A9 2023 Hardcore Engineering Inc.\n//\n// Licensed under the Eclipse Public License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License. You may\n// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n//\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nexport * from './ydoc'\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;AAAA;AAAA;AAeA,0BAAc,mBAfd;",
6
+ "names": []
7
+ }
package/lib/ydoc.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var ydoc_exports = {};
21
+ __export(ydoc_exports, {
22
+ jsonToYDoc: () => jsonToYDoc,
23
+ markupToYDoc: () => markupToYDoc,
24
+ yDocToMarkup: () => yDocToMarkup
25
+ });
26
+ module.exports = __toCommonJS(ydoc_exports);
27
+ var import_core = require("@hcengineering/core");
28
+ var import_text = require("@hcengineering/text");
29
+ var import_text_core = require("@hcengineering/text-core");
30
+ var import_yjs = require("yjs");
31
+ function markupToYDoc(markup, field) {
32
+ return jsonToYDoc((0, import_text.markupToJSON)(markup), field);
33
+ }
34
+ __name(markupToYDoc, "markupToYDoc");
35
+ function jsonToYDoc(json, field) {
36
+ const ydoc = new import_yjs.Doc({ guid: (0, import_core.generateId)() });
37
+ const fragment = ydoc.getXmlFragment(field);
38
+ const nodes = json.type === "doc" ? json.content ?? [] : [json];
39
+ nodes.map((p) => nodeToXmlElement(fragment, p));
40
+ return ydoc;
41
+ }
42
+ __name(jsonToYDoc, "jsonToYDoc");
43
+ function nodeToXmlElement(parent, node) {
44
+ const elem = node.type === "text" ? new import_yjs.XmlText() : new import_yjs.XmlElement(node.type);
45
+ parent.push([elem]);
46
+ if (elem instanceof import_yjs.XmlElement) {
47
+ if (node.content !== void 0 && node.content.length > 0) {
48
+ node.content.map((p) => nodeToXmlElement(elem, p));
49
+ }
50
+ } else if (elem instanceof import_yjs.XmlText) {
51
+ const attributes = {};
52
+ if (node.marks !== void 0) {
53
+ const attrCount = /* @__PURE__ */ new Map();
54
+ node.marks.forEach((mark) => {
55
+ attrCount.set(mark.type, (attrCount.get(mark.type) ?? 0) + 1);
56
+ });
57
+ node.marks.forEach((mark) => {
58
+ const count = attrCount.get(mark.type) ?? 0;
59
+ const type = count > 1 ? `${mark.type}--${(0, import_text_core.hashAttrs)(mark.attrs)}` : mark.type;
60
+ attributes[type] = mark.attrs ?? {};
61
+ });
62
+ }
63
+ elem.applyDelta([
64
+ {
65
+ insert: node.text ?? "",
66
+ attributes
67
+ }
68
+ ]);
69
+ }
70
+ if (node.attrs !== void 0) {
71
+ Object.entries(node.attrs).forEach(([key, value]) => {
72
+ elem.setAttribute(key, value);
73
+ });
74
+ }
75
+ return elem;
76
+ }
77
+ __name(nodeToXmlElement, "nodeToXmlElement");
78
+ function yDocToMarkup(ydoc, field) {
79
+ const fragment = ydoc.getXmlFragment(field);
80
+ const json = xmlFragmentToNode(fragment);
81
+ return (0, import_text.jsonToMarkup)({ type: import_text.MarkupNodeType.doc, content: json });
82
+ }
83
+ __name(yDocToMarkup, "yDocToMarkup");
84
+ function xmlFragmentToNode(fragment) {
85
+ const result = [];
86
+ for (let i = 0; i < fragment.length; i++) {
87
+ const item = fragment.get(i);
88
+ if (item instanceof import_yjs.XmlElement) {
89
+ const node = {
90
+ type: item.nodeName
91
+ };
92
+ const attrs = item.getAttributes();
93
+ if (Object.keys(attrs).length > 0) {
94
+ node.attrs = attrs;
95
+ }
96
+ if (item.length > 0) {
97
+ node.content = xmlFragmentToNode(item);
98
+ }
99
+ result.push(node);
100
+ } else if (item instanceof import_yjs.XmlText) {
101
+ const delta = item.toDelta();
102
+ for (const op of delta) {
103
+ const textNode = {
104
+ type: import_text.MarkupNodeType.text,
105
+ text: op.insert
106
+ };
107
+ if (op.attributes != null) {
108
+ textNode.marks = Object.entries(op.attributes).map(([type, attrs]) => ({
109
+ type: (0, import_text_core.stripHash)(type),
110
+ attrs
111
+ }));
112
+ }
113
+ result.push(textNode);
114
+ }
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+ __name(xmlFragmentToNode, "xmlFragmentToNode");
120
+ //# sourceMappingURL=ydoc.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/ydoc.ts"],
4
+ "sourcesContent": ["//\n// Copyright \u00A9 2025 Hardcore Engineering Inc.\n//\n// Licensed under the Eclipse Public License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License. You may\n// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n//\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n\nimport { generateId, Markup } from '@hcengineering/core'\nimport { jsonToMarkup, MarkupMarkType, MarkupNodeType, markupToJSON, type MarkupNode } from '@hcengineering/text'\nimport { hashAttrs, stripHash } from '@hcengineering/text-core'\nimport { Doc as YDoc, XmlElement as YXmlElement, XmlFragment as YXmlFragment, XmlText as YXmlText } from 'yjs'\n\n/**\n * Convert Markup to Y.Doc\n *\n * @public\n */\nexport function markupToYDoc (markup: Markup, field: string): YDoc {\n return jsonToYDoc(markupToJSON(markup), field)\n}\n\n/**\n * Convert Markup JSON to Y.Doc\n *\n * @public\n */\nexport function jsonToYDoc (json: MarkupNode, field: string): YDoc {\n const ydoc = new YDoc({ guid: generateId() })\n const fragment = ydoc.getXmlFragment(field)\n\n const nodes = json.type === 'doc' ? (json.content ?? []) : [json]\n nodes.map((p) => nodeToXmlElement(fragment, p))\n\n return ydoc\n}\n\nfunction nodeToXmlElement (parent: YXmlFragment, node: MarkupNode): YXmlElement | YXmlText {\n const elem = node.type === 'text' ? new YXmlText() : new YXmlElement(node.type)\n parent.push([elem])\n\n if (elem instanceof YXmlElement) {\n if (node.content !== undefined && node.content.length > 0) {\n node.content.map((p) => nodeToXmlElement(elem, p))\n }\n } else if (elem instanceof YXmlText) {\n // https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js#L777\n const attributes: Record<string, any> = {}\n if (node.marks !== undefined) {\n const attrCount = new Map<string, number>()\n node.marks.forEach((mark) => {\n attrCount.set(mark.type, (attrCount.get(mark.type) ?? 0) + 1)\n })\n\n node.marks.forEach((mark) => {\n const count = attrCount.get(mark.type) ?? 0\n const type = count > 1 ? `${mark.type}--${hashAttrs(mark.attrs)}` : mark.type\n attributes[type] = mark.attrs ?? {}\n })\n }\n\n elem.applyDelta([\n {\n insert: node.text ?? '',\n attributes\n }\n ])\n }\n\n if (node.attrs !== undefined) {\n Object.entries(node.attrs).forEach(([key, value]) => {\n elem.setAttribute(key, value)\n })\n }\n\n return elem\n}\n\n/**\n * Convert Y.Doc to Markup\n *\n * @public\n */\nexport function yDocToMarkup (ydoc: YDoc, field: string): Markup {\n const fragment = ydoc.getXmlFragment(field)\n const json = xmlFragmentToNode(fragment)\n return jsonToMarkup({ type: MarkupNodeType.doc, content: json })\n}\n\nfunction xmlFragmentToNode (fragment: YXmlFragment): MarkupNode[] {\n const result: MarkupNode[] = []\n\n for (let i = 0; i < fragment.length; i++) {\n const item = fragment.get(i)\n if (item instanceof YXmlElement) {\n const node: MarkupNode = {\n type: item.nodeName as MarkupNodeType\n }\n\n // Handle attributes\n const attrs = item.getAttributes()\n if (Object.keys(attrs).length > 0) {\n node.attrs = attrs\n }\n\n // Handle content\n if (item.length > 0) {\n node.content = xmlFragmentToNode(item)\n }\n\n result.push(node)\n } else if (item instanceof YXmlText) {\n // Handle text with marks\n const delta = item.toDelta()\n for (const op of delta) {\n const textNode: MarkupNode = {\n type: MarkupNodeType.text,\n text: op.insert\n }\n\n // Convert attributes to marks\n if (op.attributes != null) {\n textNode.marks = Object.entries(op.attributes).map(([type, attrs]) => ({\n type: stripHash(type) as MarkupMarkType,\n attrs: attrs as Record<string, any>\n }))\n }\n\n result.push(textNode)\n }\n }\n }\n\n return result\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["YDoc", "YXmlText", "YXmlElement"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@hcengineering/text-ydoc",
3
+ "version": "0.7.1",
4
+ "main": "lib/index.js",
5
+ "svelte": "src/index.ts",
6
+ "types": "types/index.d.ts",
7
+ "files": [
8
+ "lib/**/*",
9
+ "types/**/*",
10
+ "src/**/*",
11
+ "!src/**/__test__/**",
12
+ "tsconfig.json"
13
+ ],
14
+ "author": "Anticrm Platform Contributors",
15
+ "license": "EPL-2.0",
16
+ "devDependencies": {
17
+ "@hcengineering/platform-rig": "^0.7.9",
18
+ "@typescript-eslint/eslint-plugin": "^6.11.0",
19
+ "eslint-plugin-import": "^2.26.0",
20
+ "eslint-plugin-promise": "^6.1.1",
21
+ "eslint-plugin-n": "^15.4.0",
22
+ "eslint": "^8.54.0",
23
+ "@typescript-eslint/parser": "^6.11.0",
24
+ "eslint-config-standard-with-typescript": "^40.0.0",
25
+ "prettier": "^3.1.0",
26
+ "typescript": "^5.8.3",
27
+ "jest": "^29.7.0",
28
+ "ts-jest": "^29.1.1",
29
+ "@types/jest": "^29.5.5",
30
+ "jest-environment-jsdom": "^29.7.0",
31
+ "fast-equals": "^5.2.2",
32
+ "y-prosemirror": "^1.3.7"
33
+ },
34
+ "dependencies": {
35
+ "@hcengineering/core": "^0.7.1",
36
+ "@hcengineering/text": "^0.7.1",
37
+ "@hcengineering/text-core": "^0.7.1",
38
+ "yjs": "^13.6.27",
39
+ "y-protocols": "^1.0.6"
40
+ },
41
+ "repository": "https://github.com/hcengineering/huly.core",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "exports": {
46
+ ".": {
47
+ "types": "./types/index.d.ts",
48
+ "require": "./lib/index.js",
49
+ "import": "./lib/index.js"
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "compile",
54
+ "test": "jest --passWithNoTests --silent",
55
+ "build:watch": "compile",
56
+ "format": "format src",
57
+ "_phase:build": "compile transpile src",
58
+ "_phase:test": "jest --passWithNoTests --silent",
59
+ "_phase:format": "format src",
60
+ "_phase:validate": "compile validate"
61
+ }
62
+ }
@@ -0,0 +1,259 @@
1
+ //
2
+ // Copyright © 2024 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ import { type Markup, generateId } from '@hcengineering/core'
17
+ import { type MarkupNode, jsonToMarkup, jsonToPmNode, markupToJSON } from '@hcengineering/text'
18
+ import { prosemirrorToYXmlFragment, yDocToProsemirrorJSON } from 'y-prosemirror'
19
+ import { deepEqual } from 'fast-equals'
20
+ import { Doc as YDoc } from 'yjs'
21
+ import { markupToYDoc, yDocToMarkup } from '../ydoc'
22
+
23
+ declare global {
24
+ // eslint-disable-next-line @typescript-eslint/no-namespace
25
+ namespace jest {
26
+ interface Matchers<R> {
27
+ toEqualMarkup: (expected: string) => R
28
+ toEqualYdoc: (expected: YDoc) => R
29
+ }
30
+ }
31
+ }
32
+
33
+ expect.extend({
34
+ toEqualMarkup (received: string, expected: string) {
35
+ const pass = received === expected || deepEqual(JSON.parse(received), JSON.parse(expected))
36
+ return {
37
+ message: () =>
38
+ pass
39
+ ? `Expected markup strings NOT to be equal:\n Expected: ${expected}\n Received: ${received}`
40
+ : `Expected markup strings to be equal:\n Expected: ${expected}\n Received: ${received}`,
41
+ pass
42
+ }
43
+ },
44
+
45
+ toEqualYdoc (received: YDoc, expected: YDoc) {
46
+ const expectedJSON = expected.toJSON()
47
+ const receivedJSON = received.toJSON()
48
+
49
+ const pass = deepEqual(expectedJSON, receivedJSON)
50
+ return {
51
+ message: () =>
52
+ pass
53
+ ? `Expected yjs documents NOT to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}`
54
+ : `Expected yjs documents to be equal:\n Expected: ${JSON.stringify(expectedJSON)}\n Received: ${JSON.stringify(receivedJSON)}`,
55
+ pass
56
+ }
57
+ }
58
+ })
59
+
60
+ function referenceMarkupToYDoc (markup: Markup, field: string): YDoc {
61
+ const ydoc = new YDoc({ guid: generateId() })
62
+ const fragment = ydoc.getXmlFragment(field)
63
+ prosemirrorToYXmlFragment(jsonToPmNode(markupToJSON(markup)), fragment)
64
+ return ydoc
65
+ }
66
+
67
+ function referenceYDocToMarkup (ydoc: YDoc, field: string): Markup {
68
+ const json = yDocToProsemirrorJSON(ydoc, field)
69
+ return jsonToMarkup(json as MarkupNode)
70
+ }
71
+
72
+ const markups: Array<{ name: string, markup: Markup, skipYdocCompare?: boolean }> = [
73
+ {
74
+ name: 'text',
75
+ markup: '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello world"}]}]}'
76
+ },
77
+ {
78
+ name: 'text with bold mark',
79
+ markup:
80
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello world"}]}]}'
81
+ },
82
+ {
83
+ name: 'separate paragraphs with bold mark',
84
+ markup:
85
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"hello"}]},{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}'
86
+ },
87
+ {
88
+ name: 'mixed text and text with bold mark',
89
+ markup:
90
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"world"}]}]}'
91
+ },
92
+ {
93
+ name: 'mixed text with italic and text with bold and italic marks',
94
+ markup:
95
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"hello "},{"type":"text","marks":[{"type":"bold","attrs":{}},{"type":"italic","attrs":{}}],"text":"world"}]}]}'
96
+ },
97
+ {
98
+ name: 'text with link and italic marks',
99
+ markup:
100
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello "},{"type":"text","text":"hello world","marks":[{"type":"link","attrs":{"href":"http://example.com","target":"_blank","rel":"noopener noreferrer","class":"cursor-pointer"}},{"type":"italic","attrs":{}}]}]}]}'
101
+ },
102
+ {
103
+ name: 'image',
104
+ markup:
105
+ '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"image","attrs":{"src":"http://example.com/image.jpg","alt":"image"}}]}]}'
106
+ },
107
+ {
108
+ name: 'table with formatting inside',
109
+ markup:
110
+ '{"type":"doc","content":[{"type":"table","content":[{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"bold","attrs":{}}],"text":"1"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"2"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"codeBlock","content":[{"type":"text","text":"3"}]}]},{"type":"tableCell","attrs":{"colspan":1,"rowspan":1},"content":[{"type":"paragraph","content":[{"type":"text","text":"4"}]}]}]}]}]}'
111
+ },
112
+ {
113
+ name: 'non-overlapping marks',
114
+ markup: `{
115
+ "type": "doc",
116
+ "content": [
117
+ {
118
+ "type": "paragraph",
119
+ "content": [
120
+ {
121
+ "type": "text",
122
+ "text": "xy",
123
+ "marks": [
124
+ {
125
+ "type": "inline-comment",
126
+ "attrs": {
127
+ "thread": "1"
128
+ }
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ "type": "text",
134
+ "text": "z",
135
+ "marks": [
136
+ {
137
+ "type": "inline-comment",
138
+ "attrs": {
139
+ "thread": "2"
140
+ }
141
+ }
142
+ ]
143
+ }
144
+ ]
145
+ }
146
+ ]
147
+ }`,
148
+ skipYdocCompare: true
149
+ },
150
+ {
151
+ name: 'overlapping marks',
152
+ markup: `{
153
+ "type": "doc",
154
+ "content": [
155
+ {
156
+ "type": "paragraph",
157
+ "content": [
158
+ {
159
+ "type": "text",
160
+ "text": "x",
161
+ "marks": [
162
+ {
163
+ "type": "inline-comment",
164
+ "attrs": {
165
+ "thread": "1"
166
+ }
167
+ }
168
+ ]
169
+ },
170
+ {
171
+ "type": "text",
172
+ "text": "y",
173
+ "marks": [
174
+ {
175
+ "type": "inline-comment",
176
+ "attrs": {
177
+ "thread": "1"
178
+ }
179
+ },
180
+ {
181
+ "type": "inline-comment",
182
+ "attrs": {
183
+ "thread": "2"
184
+ }
185
+ }
186
+ ]
187
+ },
188
+ {
189
+ "type": "text",
190
+ "text": "z",
191
+ "marks": [
192
+ {
193
+ "type": "inline-comment",
194
+ "attrs": {
195
+ "thread": "2"
196
+ }
197
+ }
198
+ ]
199
+ }
200
+ ]
201
+ }
202
+ ]
203
+ }`,
204
+ skipYdocCompare: true
205
+ }
206
+ ]
207
+
208
+ describe('markupToYDoc', () => {
209
+ describe.each(markups)('compare with reference', ({ name, markup, skipYdocCompare }) => {
210
+ if (skipYdocCompare !== true) {
211
+ it(name, () => {
212
+ const expected = referenceMarkupToYDoc(markup, 'test')
213
+ const actual = markupToYDoc(markup, 'test')
214
+ expect(actual).toEqualYdoc(expected)
215
+ })
216
+ }
217
+ })
218
+
219
+ describe.each(markups)('converts markup to ydoc and back', ({ name, markup }) => {
220
+ it(name, () => {
221
+ const ydoc = markupToYDoc(markup, 'test')
222
+ const actual = yDocToMarkup(ydoc, 'test')
223
+
224
+ expect(actual).toEqualMarkup(markup)
225
+ })
226
+ })
227
+ })
228
+
229
+ describe('yDocToMarkup', () => {
230
+ describe.each(markups)('compare with original', ({ name, markup }) => {
231
+ it(name, () => {
232
+ const ydoc = referenceMarkupToYDoc(markup, 'test')
233
+ const actual = yDocToMarkup(ydoc, 'test')
234
+
235
+ expect(actual).toEqualMarkup(markup)
236
+ })
237
+ })
238
+
239
+ describe.each(markups)('compare with reference', ({ name, markup }) => {
240
+ it(name, () => {
241
+ const ydoc = referenceMarkupToYDoc(markup, 'test')
242
+ const expected = referenceYDocToMarkup(ydoc, 'test')
243
+ const actual = yDocToMarkup(ydoc, 'test')
244
+
245
+ expect(actual).toEqualMarkup(expected)
246
+ })
247
+ })
248
+
249
+ describe.each(markups)('converts ydoc to markup and back', ({ name, markup, skipYdocCompare }) => {
250
+ if (skipYdocCompare !== true) {
251
+ it(name, () => {
252
+ const expected = referenceMarkupToYDoc(markup, 'test')
253
+ const actual = referenceMarkupToYDoc(yDocToMarkup(expected, 'test'), 'test')
254
+
255
+ expect(actual).toEqualYdoc(expected)
256
+ })
257
+ }
258
+ })
259
+ })
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright © 2023 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ export * from './ydoc'
package/src/ydoc.ts ADDED
@@ -0,0 +1,142 @@
1
+ //
2
+ // Copyright © 2025 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ import { generateId, Markup } from '@hcengineering/core'
17
+ import { jsonToMarkup, MarkupMarkType, MarkupNodeType, markupToJSON, type MarkupNode } from '@hcengineering/text'
18
+ import { hashAttrs, stripHash } from '@hcengineering/text-core'
19
+ import { Doc as YDoc, XmlElement as YXmlElement, XmlFragment as YXmlFragment, XmlText as YXmlText } from 'yjs'
20
+
21
+ /**
22
+ * Convert Markup to Y.Doc
23
+ *
24
+ * @public
25
+ */
26
+ export function markupToYDoc (markup: Markup, field: string): YDoc {
27
+ return jsonToYDoc(markupToJSON(markup), field)
28
+ }
29
+
30
+ /**
31
+ * Convert Markup JSON to Y.Doc
32
+ *
33
+ * @public
34
+ */
35
+ export function jsonToYDoc (json: MarkupNode, field: string): YDoc {
36
+ const ydoc = new YDoc({ guid: generateId() })
37
+ const fragment = ydoc.getXmlFragment(field)
38
+
39
+ const nodes = json.type === 'doc' ? (json.content ?? []) : [json]
40
+ nodes.map((p) => nodeToXmlElement(fragment, p))
41
+
42
+ return ydoc
43
+ }
44
+
45
+ function nodeToXmlElement (parent: YXmlFragment, node: MarkupNode): YXmlElement | YXmlText {
46
+ const elem = node.type === 'text' ? new YXmlText() : new YXmlElement(node.type)
47
+ parent.push([elem])
48
+
49
+ if (elem instanceof YXmlElement) {
50
+ if (node.content !== undefined && node.content.length > 0) {
51
+ node.content.map((p) => nodeToXmlElement(elem, p))
52
+ }
53
+ } else if (elem instanceof YXmlText) {
54
+ // https://github.com/yjs/y-prosemirror/blob/master/src/plugins/sync-plugin.js#L777
55
+ const attributes: Record<string, any> = {}
56
+ if (node.marks !== undefined) {
57
+ const attrCount = new Map<string, number>()
58
+ node.marks.forEach((mark) => {
59
+ attrCount.set(mark.type, (attrCount.get(mark.type) ?? 0) + 1)
60
+ })
61
+
62
+ node.marks.forEach((mark) => {
63
+ const count = attrCount.get(mark.type) ?? 0
64
+ const type = count > 1 ? `${mark.type}--${hashAttrs(mark.attrs)}` : mark.type
65
+ attributes[type] = mark.attrs ?? {}
66
+ })
67
+ }
68
+
69
+ elem.applyDelta([
70
+ {
71
+ insert: node.text ?? '',
72
+ attributes
73
+ }
74
+ ])
75
+ }
76
+
77
+ if (node.attrs !== undefined) {
78
+ Object.entries(node.attrs).forEach(([key, value]) => {
79
+ elem.setAttribute(key, value)
80
+ })
81
+ }
82
+
83
+ return elem
84
+ }
85
+
86
+ /**
87
+ * Convert Y.Doc to Markup
88
+ *
89
+ * @public
90
+ */
91
+ export function yDocToMarkup (ydoc: YDoc, field: string): Markup {
92
+ const fragment = ydoc.getXmlFragment(field)
93
+ const json = xmlFragmentToNode(fragment)
94
+ return jsonToMarkup({ type: MarkupNodeType.doc, content: json })
95
+ }
96
+
97
+ function xmlFragmentToNode (fragment: YXmlFragment): MarkupNode[] {
98
+ const result: MarkupNode[] = []
99
+
100
+ for (let i = 0; i < fragment.length; i++) {
101
+ const item = fragment.get(i)
102
+ if (item instanceof YXmlElement) {
103
+ const node: MarkupNode = {
104
+ type: item.nodeName as MarkupNodeType
105
+ }
106
+
107
+ // Handle attributes
108
+ const attrs = item.getAttributes()
109
+ if (Object.keys(attrs).length > 0) {
110
+ node.attrs = attrs
111
+ }
112
+
113
+ // Handle content
114
+ if (item.length > 0) {
115
+ node.content = xmlFragmentToNode(item)
116
+ }
117
+
118
+ result.push(node)
119
+ } else if (item instanceof YXmlText) {
120
+ // Handle text with marks
121
+ const delta = item.toDelta()
122
+ for (const op of delta) {
123
+ const textNode: MarkupNode = {
124
+ type: MarkupNodeType.text,
125
+ text: op.insert
126
+ }
127
+
128
+ // Convert attributes to marks
129
+ if (op.attributes != null) {
130
+ textNode.marks = Object.entries(op.attributes).map(([type, attrs]) => ({
131
+ type: stripHash(type) as MarkupMarkType,
132
+ attrs: attrs as Record<string, any>
133
+ }))
134
+ }
135
+
136
+ result.push(textNode)
137
+ }
138
+ }
139
+ }
140
+
141
+ return result
142
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
3
+
4
+ "compilerOptions": {
5
+ "rootDir": "./src",
6
+ "outDir": "./lib",
7
+ "declarationDir": "./types",
8
+ "tsBuildInfoFile": ".build/build.tsbuildinfo"
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["node_modules", "lib", "dist", "types", "bundle"]
12
+ }
@@ -0,0 +1,10 @@
1
+ import { Doc as YDoc } from 'yjs';
2
+ declare global {
3
+ namespace jest {
4
+ interface Matchers<R> {
5
+ toEqualMarkup: (expected: string) => R;
6
+ toEqualYdoc: (expected: YDoc) => R;
7
+ }
8
+ }
9
+ }
10
+ //# sourceMappingURL=ydoc.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ydoc.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ydoc.test.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,GAAG,IAAI,IAAI,EAAE,MAAM,KAAK,CAAA;AAGjC,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,IAAI,CAAC;QACb,UAAU,QAAQ,CAAC,CAAC;YAClB,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC,CAAA;YACtC,WAAW,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,CAAC,CAAA;SACnC;KACF;CACF"}
@@ -0,0 +1,2 @@
1
+ export * from './ydoc';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,cAAc,QAAQ,CAAA"}
@@ -0,0 +1,22 @@
1
+ import { Markup } from '@hcengineering/core';
2
+ import { type MarkupNode } from '@hcengineering/text';
3
+ import { Doc as YDoc } from 'yjs';
4
+ /**
5
+ * Convert Markup to Y.Doc
6
+ *
7
+ * @public
8
+ */
9
+ export declare function markupToYDoc(markup: Markup, field: string): YDoc;
10
+ /**
11
+ * Convert Markup JSON to Y.Doc
12
+ *
13
+ * @public
14
+ */
15
+ export declare function jsonToYDoc(json: MarkupNode, field: string): YDoc;
16
+ /**
17
+ * Convert Y.Doc to Markup
18
+ *
19
+ * @public
20
+ */
21
+ export declare function yDocToMarkup(ydoc: YDoc, field: string): Markup;
22
+ //# sourceMappingURL=ydoc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ydoc.d.ts","sourceRoot":"","sources":["../src/ydoc.ts"],"names":[],"mappings":"AAeA,OAAO,EAAc,MAAM,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EAA8D,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEjH,OAAO,EAAE,GAAG,IAAI,IAAI,EAA+E,MAAM,KAAK,CAAA;AAE9G;;;;GAIG;AACH,wBAAgB,YAAY,CAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQjE;AA2CD;;;;GAIG;AACH,wBAAgB,YAAY,CAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI/D"}