@fileverse/content-processor 0.0.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,114 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ import markdownItFootnote from 'markdown-it-footnote';
3
+ import { createDOMPurifyInstance, parseHTMLString } from './dom-setup.js';
4
+ const markdownIt = new MarkdownIt().use(markdownItFootnote);
5
+ export function stripFrontmatter(markdown) {
6
+ const fmRegex = /^---\n[\s\S]*?\n---\n*/;
7
+ return markdown.replace(fmRegex, '').replace(/^\s*\n/, '');
8
+ }
9
+ export function parseMarkdownToHTML(markdown) {
10
+ let cleanMarkdown = stripFrontmatter(markdown);
11
+ cleanMarkdown = cleanMarkdown.replace(/(\d)\*(\d)/g, '$1\\*$2');
12
+ cleanMarkdown = cleanMarkdown.replace(/(\])\*(\[)/g, '$1\\*$2');
13
+ cleanMarkdown = cleanMarkdown.replace(/(\))\*(\()/g, '$1\\*$2');
14
+ let convertedHtml = markdownIt.render(cleanMarkdown);
15
+ return convertedHtml;
16
+ }
17
+ export function processHTMLForEditor(html) {
18
+ const doc = parseHTMLString(html);
19
+ const body = doc.body;
20
+ const topLevelPs = body.querySelectorAll(':scope > p');
21
+ topLevelPs.forEach((p) => {
22
+ if (p.childNodes.length === 0 && p.textContent === '') {
23
+ p.remove();
24
+ }
25
+ });
26
+ const calloutAsides = body.querySelectorAll('aside.callout');
27
+ calloutAsides.forEach((el) => {
28
+ el.setAttribute('data-type', 'callout');
29
+ el.removeAttribute('class');
30
+ });
31
+ const callouts = body.querySelectorAll('aside[data-type="callout"]');
32
+ callouts.forEach((aside) => {
33
+ const ps = aside.querySelectorAll('p');
34
+ ps.forEach((p) => {
35
+ const isEmpty = Array.from(p.childNodes).every((node) => {
36
+ if (node.nodeType === 1) {
37
+ return node.nodeName === 'BR';
38
+ }
39
+ if (node.nodeType === 3) {
40
+ return node.textContent?.trim() === '';
41
+ }
42
+ return false;
43
+ });
44
+ if (isEmpty && p.parentNode) {
45
+ p.parentNode.removeChild(p);
46
+ }
47
+ });
48
+ });
49
+ const lists = body.getElementsByTagName('ul');
50
+ for (let i = 0; i < lists.length; i++) {
51
+ const list = lists[i];
52
+ const items = list.getElementsByTagName('li');
53
+ let isTodoList = false;
54
+ for (let j = 0; j < items.length; j++) {
55
+ const item = items[j];
56
+ const text = item.textContent || '';
57
+ const todoMatch = text.match(/^\[([ x])\]\s*(.*)/i);
58
+ if (todoMatch) {
59
+ isTodoList = true;
60
+ const isChecked = todoMatch[1].toLowerCase() === 'x';
61
+ const content = todoMatch[2];
62
+ item.setAttribute('data-type', 'taskItem');
63
+ item.setAttribute('data-checked', isChecked.toString());
64
+ item.textContent = content;
65
+ }
66
+ }
67
+ if (isTodoList) {
68
+ list.setAttribute('data-type', 'taskList');
69
+ }
70
+ }
71
+ const paragraphs = body.getElementsByTagName('p');
72
+ for (let i = paragraphs.length - 1; i >= 0; i--) {
73
+ const p = paragraphs[i];
74
+ if (p.childNodes.length === 1 && p.firstChild?.nodeName === 'IMG') {
75
+ p.parentNode?.replaceChild(p.firstChild, p);
76
+ }
77
+ if (p.childNodes.length === 1 &&
78
+ p.firstChild?.textContent?.trim() === '===') {
79
+ const pageBreakDiv = doc.createElement('div');
80
+ pageBreakDiv.setAttribute('data-type', 'page-break');
81
+ pageBreakDiv.setAttribute('data-page-break', 'true');
82
+ p.parentNode?.replaceChild(pageBreakDiv, p);
83
+ }
84
+ }
85
+ let processedHtml = body.innerHTML;
86
+ const superscriptRegex = /\^([^\s^]+)\^/g;
87
+ const subscriptRegex = /~([^\s~](?:[^~]*[^\s~])?)~/g;
88
+ const pageBreakRegex = /===\s*$/gm;
89
+ processedHtml = processedHtml.replace(superscriptRegex, '<sup data-type="sup">$1</sup>');
90
+ processedHtml = processedHtml.replace(subscriptRegex, '<sub data-type="sub">$1</sub>');
91
+ processedHtml = processedHtml.replace(pageBreakRegex, '<div data-type="page-break" data-page-break="true"></div>');
92
+ const DOMPurify = createDOMPurifyInstance();
93
+ processedHtml = DOMPurify.sanitize(processedHtml, {
94
+ ADD_TAGS: ['div', 'aside'],
95
+ ADD_ATTR: [
96
+ 'data-type',
97
+ 'data-page-break',
98
+ 'data-checked',
99
+ 'url',
100
+ 'src',
101
+ 'media-type',
102
+ 'alt',
103
+ 'title',
104
+ 'width',
105
+ 'height',
106
+ ],
107
+ });
108
+ return processedHtml;
109
+ }
110
+ export function convertMarkdownToHTML(markdown) {
111
+ const rawHtml = parseMarkdownToHTML(markdown);
112
+ return processHTMLForEditor(rawHtml);
113
+ }
114
+ //# sourceMappingURL=markdown-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-parser.js","sourceRoot":"","sources":["../src/markdown-parser.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@fileverse/content-processor",
3
+ "version": "0.0.1",
4
+ "description": "Convert markdown files to Yjs-encoded strings compatible with ddoc Tiptap editor",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsc --watch",
11
+ "test": "node --experimental-strip-types test/test.ts"
12
+ },
13
+ "keywords": [
14
+ "markdown",
15
+ "yjs",
16
+ "tiptap",
17
+ "prosemirror",
18
+ "converter"
19
+ ],
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "@tiptap/core": "^2.11.5",
23
+ "@tiptap/extension-blockquote": "^2.11.5",
24
+ "@tiptap/extension-bold": "^2.11.5",
25
+ "@tiptap/extension-bullet-list": "^2.11.5",
26
+ "@tiptap/extension-code": "^2.11.5",
27
+ "@tiptap/extension-code-block": "^2.11.5",
28
+ "@tiptap/extension-collaboration": "^2.11.5",
29
+ "@tiptap/extension-document": "^2.11.5",
30
+ "@tiptap/extension-dropcursor": "^2.11.5",
31
+ "@tiptap/extension-gapcursor": "^2.11.5",
32
+ "@tiptap/extension-hard-break": "^2.11.5",
33
+ "@tiptap/extension-heading": "^2.11.5",
34
+ "@tiptap/extension-highlight": "^2.11.5",
35
+ "@tiptap/extension-horizontal-rule": "^2.11.5",
36
+ "@tiptap/extension-image": "^2.11.5",
37
+ "@tiptap/extension-italic": "^2.11.5",
38
+ "@tiptap/extension-link": "^2.11.5",
39
+ "@tiptap/extension-list-item": "^2.11.5",
40
+ "@tiptap/extension-ordered-list": "^2.11.5",
41
+ "@tiptap/extension-paragraph": "^2.11.5",
42
+ "@tiptap/extension-strike": "^2.11.5",
43
+ "@tiptap/extension-subscript": "^2.11.5",
44
+ "@tiptap/extension-superscript": "^2.11.5",
45
+ "@tiptap/extension-table": "^2.11.5",
46
+ "@tiptap/extension-table-cell": "^2.11.5",
47
+ "@tiptap/extension-table-header": "^2.11.5",
48
+ "@tiptap/extension-table-row": "^2.11.5",
49
+ "@tiptap/extension-task-item": "^2.11.5",
50
+ "@tiptap/extension-task-list": "^2.11.5",
51
+ "@tiptap/extension-text": "^2.11.5",
52
+ "@tiptap/extension-underline": "^2.11.5",
53
+ "@tiptap/pm": "^2.11.5",
54
+ "dompurify": "^3.2.4",
55
+ "js-base64": "^3.7.7",
56
+ "jsdom": "^26.0.0",
57
+ "markdown-it": "^14.1.0",
58
+ "markdown-it-footnote": "^4.0.0",
59
+ "yjs": "^13.6.22"
60
+ },
61
+ "devDependencies": {
62
+ "@types/jsdom": "^21.1.7",
63
+ "@types/markdown-it": "^14.1.2",
64
+ "@types/markdown-it-footnote": "^3.0.4",
65
+ "@types/node": "^22.13.1",
66
+ "typescript": "^5.7.3"
67
+ }
68
+ }
@@ -0,0 +1,60 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import createDOMPurify from 'dompurify';
3
+
4
+ let domSetup = false;
5
+ let jsdomInstance: JSDOM | null = null;
6
+
7
+ export function setupDOM() {
8
+ if (domSetup) return;
9
+
10
+ jsdomInstance = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
11
+ pretendToBeVisual: true,
12
+ });
13
+
14
+ const { window } = jsdomInstance;
15
+ const { document } = window;
16
+
17
+ const g = globalThis as any;
18
+
19
+ g.window = window;
20
+ g.document = document;
21
+ g.Node = window.Node;
22
+ g.HTMLElement = window.HTMLElement;
23
+ g.Text = window.Text;
24
+ g.DOMParser = window.DOMParser;
25
+ g.Element = window.Element;
26
+ g.DocumentFragment = window.DocumentFragment;
27
+ g.getComputedStyle = window.getComputedStyle.bind(window);
28
+ g.requestAnimationFrame = (cb: () => void) => setTimeout(cb, 0);
29
+ g.cancelAnimationFrame = clearTimeout;
30
+ g.innerHeight = 768;
31
+ g.innerWidth = 1024;
32
+ g.scrollTo = () => {};
33
+ g.scrollBy = () => {};
34
+ g.MutationObserver = window.MutationObserver;
35
+ g.Range = window.Range;
36
+ g.Selection = window.Selection;
37
+ g.getSelection = () => window.getSelection();
38
+
39
+ try {
40
+ Object.defineProperty(globalThis, 'navigator', {
41
+ value: window.navigator,
42
+ writable: true,
43
+ configurable: true,
44
+ });
45
+ } catch {
46
+ // navigator may already be defined
47
+ }
48
+
49
+ domSetup = true;
50
+ }
51
+
52
+ export function createDOMPurifyInstance() {
53
+ const jsdomWindow = new JSDOM('').window;
54
+ return createDOMPurify(jsdomWindow as any);
55
+ }
56
+
57
+ export function parseHTMLString(html: string): Document {
58
+ const dom = new JSDOM(`<!DOCTYPE html><html><body>${html}</body></html>`);
59
+ return dom.window.document;
60
+ }
@@ -0,0 +1,294 @@
1
+ import { Node, mergeAttributes, Mark } from '@tiptap/core';
2
+ import { Document as TiptapDocument } from '@tiptap/extension-document';
3
+ import Paragraph from '@tiptap/extension-paragraph';
4
+ import Text from '@tiptap/extension-text';
5
+ import Bold from '@tiptap/extension-bold';
6
+ import Italic from '@tiptap/extension-italic';
7
+ import Strike from '@tiptap/extension-strike';
8
+ import Underline from '@tiptap/extension-underline';
9
+ import Code from '@tiptap/extension-code';
10
+ import CodeBlock from '@tiptap/extension-code-block';
11
+ import Heading from '@tiptap/extension-heading';
12
+ import BulletList from '@tiptap/extension-bullet-list';
13
+ import OrderedList from '@tiptap/extension-ordered-list';
14
+ import ListItem from '@tiptap/extension-list-item';
15
+ import TaskList from '@tiptap/extension-task-list';
16
+ import TaskItem from '@tiptap/extension-task-item';
17
+ import Blockquote from '@tiptap/extension-blockquote';
18
+ import HorizontalRule from '@tiptap/extension-horizontal-rule';
19
+ import HardBreak from '@tiptap/extension-hard-break';
20
+ import Link from '@tiptap/extension-link';
21
+ import Image from '@tiptap/extension-image';
22
+ import Table from '@tiptap/extension-table';
23
+ import TableRow from '@tiptap/extension-table-row';
24
+ import TableCell from '@tiptap/extension-table-cell';
25
+ import TableHeader from '@tiptap/extension-table-header';
26
+ import Highlight from '@tiptap/extension-highlight';
27
+ import Subscript from '@tiptap/extension-subscript';
28
+ import Superscript from '@tiptap/extension-superscript';
29
+ import Dropcursor from '@tiptap/extension-dropcursor';
30
+ import Gapcursor from '@tiptap/extension-gapcursor';
31
+ import Collaboration from '@tiptap/extension-collaboration';
32
+ import * as Y from 'yjs';
33
+
34
+ export const Document = TiptapDocument.extend({
35
+ content: '(dBlock|columns|pageBreak)+',
36
+ });
37
+
38
+ export const DBlock = Node.create({
39
+ name: 'dBlock',
40
+ priority: 1000,
41
+ group: 'dBlock',
42
+ content: '(block|columns)',
43
+ draggable: true,
44
+ selectable: false,
45
+ inline: false,
46
+
47
+ addAttributes() {
48
+ return {
49
+ isCorrupted: {
50
+ default: false,
51
+ },
52
+ };
53
+ },
54
+
55
+ parseHTML() {
56
+ return [{ tag: 'div[data-type="d-block"]' }];
57
+ },
58
+
59
+ renderHTML({ HTMLAttributes }) {
60
+ return [
61
+ 'div',
62
+ mergeAttributes(HTMLAttributes, { 'data-type': 'd-block' }),
63
+ 0,
64
+ ];
65
+ },
66
+ });
67
+
68
+ export const PageBreak = Node.create({
69
+ name: 'pageBreak',
70
+ group: 'pageBreak',
71
+
72
+ addOptions() {
73
+ return {
74
+ HTMLAttributes: {
75
+ style: 'page-break-after: always',
76
+ 'data-page-break': 'true',
77
+ },
78
+ };
79
+ },
80
+
81
+ parseHTML() {
82
+ return [
83
+ { tag: 'br[data-type="page-break"]' },
84
+ { tag: 'div[data-type="page-break"]' },
85
+ ];
86
+ },
87
+
88
+ renderHTML({ HTMLAttributes }) {
89
+ return ['br', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
90
+ },
91
+ });
92
+
93
+ export const Callout = Node.create({
94
+ name: 'callout',
95
+ group: 'block',
96
+ content: '(paragraph | bulletList | orderedList | taskList | block)+',
97
+ defining: true,
98
+ draggable: true,
99
+ isolating: true,
100
+
101
+ addOptions() {
102
+ return {
103
+ HTMLAttributes: {
104
+ class: 'callout',
105
+ },
106
+ };
107
+ },
108
+
109
+ addAttributes() {
110
+ return {
111
+ dataType: {
112
+ default: 'callout',
113
+ parseHTML: (element: HTMLElement) => element.getAttribute('data-type'),
114
+ renderHTML: (attributes: { dataType: string }) => ({
115
+ 'data-type': attributes.dataType,
116
+ }),
117
+ },
118
+ };
119
+ },
120
+
121
+ parseHTML() {
122
+ return [{ tag: 'aside[data-type="callout"]' }];
123
+ },
124
+
125
+ renderHTML({ HTMLAttributes }) {
126
+ return [
127
+ 'aside',
128
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
129
+ 0,
130
+ ];
131
+ },
132
+ });
133
+
134
+ export const Columns = Node.create({
135
+ name: 'columns',
136
+ group: 'columns',
137
+ content: 'column{2,}',
138
+ isolating: true,
139
+ defining: true,
140
+
141
+ parseHTML() {
142
+ return [{ tag: 'div[data-type="columns"]' }];
143
+ },
144
+
145
+ renderHTML({ HTMLAttributes }) {
146
+ return [
147
+ 'div',
148
+ mergeAttributes(HTMLAttributes, { 'data-type': 'columns' }),
149
+ 0,
150
+ ];
151
+ },
152
+ });
153
+
154
+ export const Column = Node.create({
155
+ name: 'column',
156
+ group: 'column',
157
+ content: '(block|columns)+',
158
+
159
+ parseHTML() {
160
+ return [{ tag: 'div[data-type="column"]' }];
161
+ },
162
+
163
+ renderHTML({ HTMLAttributes }) {
164
+ return [
165
+ 'div',
166
+ mergeAttributes(HTMLAttributes, { 'data-type': 'column' }),
167
+ 0,
168
+ ];
169
+ },
170
+ });
171
+
172
+ export const ResizableMedia = Node.create({
173
+ name: 'resizableMedia',
174
+ group: 'block',
175
+ atom: true,
176
+ draggable: true,
177
+
178
+ addAttributes() {
179
+ return {
180
+ src: { default: null },
181
+ 'media-type': { default: 'img' },
182
+ alt: { default: null },
183
+ title: { default: null },
184
+ width: { default: '100%' },
185
+ height: { default: 'auto' },
186
+ dataAlign: { default: 'center' },
187
+ };
188
+ },
189
+
190
+ parseHTML() {
191
+ return [
192
+ {
193
+ tag: 'img',
194
+ getAttrs: (element: HTMLElement) => ({
195
+ src: element.getAttribute('src'),
196
+ 'media-type': element.getAttribute('media-type') || 'img',
197
+ alt: element.getAttribute('alt'),
198
+ title: element.getAttribute('title'),
199
+ width: element.getAttribute('width') || '100%',
200
+ height: element.getAttribute('height') || 'auto',
201
+ }),
202
+ },
203
+ ];
204
+ },
205
+
206
+ renderHTML({ HTMLAttributes }) {
207
+ return ['img', mergeAttributes(HTMLAttributes)];
208
+ },
209
+ });
210
+
211
+ export const Footnote = Node.create({
212
+ name: 'footnote',
213
+ group: 'block',
214
+ content: 'inline*',
215
+
216
+ addAttributes() {
217
+ return {
218
+ id: { default: null },
219
+ };
220
+ },
221
+
222
+ parseHTML() {
223
+ return [{ tag: 'div[data-type="footnote"]' }];
224
+ },
225
+
226
+ renderHTML({ HTMLAttributes }) {
227
+ return [
228
+ 'div',
229
+ mergeAttributes(HTMLAttributes, { 'data-type': 'footnote' }),
230
+ 0,
231
+ ];
232
+ },
233
+ });
234
+
235
+ export const FootnoteRef = Mark.create({
236
+ name: 'footnoteRef',
237
+
238
+ addAttributes() {
239
+ return {
240
+ id: { default: null },
241
+ };
242
+ },
243
+
244
+ parseHTML() {
245
+ return [{ tag: 'sup.footnote-ref' }];
246
+ },
247
+
248
+ renderHTML({ HTMLAttributes }) {
249
+ return ['sup', mergeAttributes(HTMLAttributes, { class: 'footnote-ref' }), 0];
250
+ },
251
+ });
252
+
253
+ export function getExtensions(ydoc: Y.Doc) {
254
+ return [
255
+ Document,
256
+ DBlock,
257
+ PageBreak,
258
+ Callout,
259
+ Columns,
260
+ Column,
261
+ ResizableMedia,
262
+ Footnote,
263
+ FootnoteRef,
264
+ Paragraph,
265
+ Text,
266
+ Bold,
267
+ Italic,
268
+ Strike,
269
+ Underline,
270
+ Code,
271
+ CodeBlock,
272
+ Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
273
+ BulletList,
274
+ OrderedList,
275
+ ListItem,
276
+ TaskList,
277
+ TaskItem.configure({ nested: true }),
278
+ Blockquote,
279
+ HorizontalRule,
280
+ HardBreak,
281
+ Link.configure({ openOnClick: false }),
282
+ Image,
283
+ Table.configure({ resizable: false }),
284
+ TableRow,
285
+ TableCell,
286
+ TableHeader,
287
+ Highlight.configure({ multicolor: true }),
288
+ Subscript,
289
+ Superscript,
290
+ Dropcursor,
291
+ Gapcursor,
292
+ Collaboration.configure({ document: ydoc }),
293
+ ];
294
+ }
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { setupDOM, parseHTMLString } from './dom-setup.js';
2
+ import { getExtensions } from './extensions.js';
3
+ import { convertMarkdownToHTML } from './markdown-parser.js';
4
+ import { Editor } from '@tiptap/core';
5
+ import { DOMParser as ProseMirrorDOMParser } from '@tiptap/pm/model';
6
+ import * as Y from 'yjs';
7
+ import { fromUint8Array, toUint8Array } from 'js-base64';
8
+
9
+ setupDOM();
10
+
11
+ export interface MarkdownToYjsOptions {
12
+ wrapInDBlock?: boolean;
13
+ }
14
+
15
+ export function markdownToYjs(
16
+ markdown: string,
17
+ options: MarkdownToYjsOptions = {}
18
+ ): string {
19
+ const { wrapInDBlock = true } = options;
20
+
21
+ const ydoc = new Y.Doc();
22
+ const extensions = getExtensions(ydoc);
23
+
24
+ const editor = new Editor({
25
+ extensions,
26
+ content: '',
27
+ });
28
+
29
+ try {
30
+ const html = convertMarkdownToHTML(markdown);
31
+ const doc = parseHTMLString(html);
32
+ const domContent = doc.body;
33
+
34
+ const proseMirrorNodes = ProseMirrorDOMParser.fromSchema(
35
+ editor.schema
36
+ ).parse(domContent as unknown as Node);
37
+
38
+ if (wrapInDBlock) {
39
+ const wrappedContent: any[] = [];
40
+ proseMirrorNodes.content.forEach((node) => {
41
+ const nodeJSON = node.toJSON();
42
+ if (
43
+ nodeJSON.type === 'dBlock' ||
44
+ nodeJSON.type === 'columns' ||
45
+ nodeJSON.type === 'pageBreak'
46
+ ) {
47
+ wrappedContent.push(nodeJSON);
48
+ } else {
49
+ wrappedContent.push({
50
+ type: 'dBlock',
51
+ content: [nodeJSON],
52
+ });
53
+ }
54
+ });
55
+
56
+ if (wrappedContent.length === 0) {
57
+ wrappedContent.push({
58
+ type: 'dBlock',
59
+ content: [{ type: 'paragraph' }],
60
+ });
61
+ }
62
+
63
+ editor.commands.setContent({
64
+ type: 'doc',
65
+ content: wrappedContent,
66
+ } as any);
67
+ } else {
68
+ editor.commands.setContent(proseMirrorNodes.toJSON());
69
+ }
70
+
71
+ const yjsContent = Y.encodeStateAsUpdate(ydoc);
72
+ const result = fromUint8Array(yjsContent);
73
+
74
+ return result;
75
+ } finally {
76
+ editor.destroy();
77
+ if (!ydoc.isDestroyed) {
78
+ ydoc.destroy();
79
+ }
80
+ }
81
+ }
82
+
83
+ export function mergeYjsUpdates(updates: string[]): string {
84
+ const parsedUpdates = updates.map((update) => toUint8Array(update));
85
+ return fromUint8Array(Y.mergeUpdates(parsedUpdates));
86
+ }
87
+
88
+ export function applyYjsUpdate(
89
+ existingContent: string,
90
+ update: string
91
+ ): string {
92
+ const ydoc = new Y.Doc();
93
+ Y.applyUpdate(ydoc, toUint8Array(existingContent));
94
+ Y.applyUpdate(ydoc, toUint8Array(update));
95
+ const result = fromUint8Array(Y.encodeStateAsUpdate(ydoc));
96
+ ydoc.destroy();
97
+ return result;
98
+ }
99
+
100
+ export function isYjsEncoded(content: string): boolean {
101
+ if (typeof content !== 'string') return false;
102
+
103
+ try {
104
+ JSON.parse(content);
105
+ return false;
106
+ } catch {
107
+ try {
108
+ const decoded = toUint8Array(content);
109
+ return decoded.length > 0;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ }
115
+
116
+ export { convertMarkdownToHTML } from './markdown-parser.js';
117
+ export { stripFrontmatter } from './markdown-parser.js';