@cyberyozh/text-editor 0.5.2 → 0.5.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyberyozh/text-editor",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Rich text editor library based on Tiptap for CyberYozh",
5
5
  "author": "CyberYozh",
6
6
  "license": "MIT",
@@ -17,6 +17,11 @@
17
17
  "import": "./dist/render/client.mjs",
18
18
  "require": "./dist/render/client.js"
19
19
  },
20
+ "./html": {
21
+ "types": "./dist/html.d.ts",
22
+ "import": "./dist/html.mjs",
23
+ "require": "./dist/html.js"
24
+ },
20
25
  "./editor": {
21
26
  "types": "./dist/editor.d.ts",
22
27
  "import": "./dist/editor.mjs",
@@ -74,6 +79,7 @@
74
79
  "@tiptap/extension-text-align": "3.31.3",
75
80
  "@tiptap/extensions": "3.31.3",
76
81
  "@tiptap/html": "3.31.3",
82
+ "happy-dom": "^20.14.0",
77
83
  "@tiptap/pm": "3.31.3",
78
84
  "@tiptap/react": "3.31.3",
79
85
  "@tiptap/starter-kit": "3.31.3",
@@ -82,13 +88,13 @@
82
88
  "@tiptap/y-tiptap": "3.0.9",
83
89
  "emoji-picker-react": "4.19.1",
84
90
  "highlight.js": "11.12.0",
85
- "katex": "0.16.35",
91
+ "katex": "0.18.10",
86
92
  "lowlight": "3.3.0",
87
93
  "y-protocols": "1.0.7",
88
94
  "yjs": "13.6.30"
89
95
  },
90
96
  "devDependencies": {
91
- "@changesets/cli": "^2.30.0",
97
+ "@changesets/cli": "^3.0.3",
92
98
  "@types/node": "^25.3.3",
93
99
  "cross-env": "^10.1.0",
94
100
  "eslint": "^10.0.2",
@@ -60,6 +60,12 @@ export interface TiptapProps {
60
60
  */
61
61
  lang?: SupportedLanguage;
62
62
  error?: string;
63
+ /**
64
+ * Domains (with all their subdomains) of the site the content belongs to:
65
+ * such links open in the same tab and get no rel on paste / HTML parsing.
66
+ * Read once when the editor instance is created.
67
+ */
68
+ ownDomains?: string[];
63
69
  /**
64
70
  * Domains (with all their subdomains) whose links are treated as internal:
65
71
  * rel="noopener noreferrer nofollow" is not added on paste / HTML parsing.
@@ -81,6 +87,7 @@ function TiptapContent({
81
87
  isDisabled = false,
82
88
  lang,
83
89
  error,
90
+ ownDomains,
84
91
  trustedDomains,
85
92
  }: TiptapProps) {
86
93
  const [linkDialogContext, setLinkDialogContext] = useState<LinkDialogContextProps | null>(null);
@@ -124,6 +131,7 @@ function TiptapContent({
124
131
  suggestion: createSuggestions(translations, !!imageHandlers),
125
132
  isEditable: isEditable && !disabled,
126
133
  getImageById: imageHandlers?.getImage,
134
+ ownDomains,
127
135
  trustedDomains,
128
136
  onKatexClick: (latex, pos) => {
129
137
  setKatexDialogContext({ latex, pos });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateHTML, generateJSON } from "@tiptap/html";
3
+ import StarterKit from "@tiptap/starter-kit";
4
+ import { BaseCodeBlock } from "./code-block";
5
+
6
+ const extensions = [StarterKit.configure({ codeBlock: false }), BaseCodeBlock];
7
+
8
+ const codeBlock = (language: string | null) => ({
9
+ type: "doc",
10
+ content: [
11
+ {
12
+ type: "codeBlock",
13
+ attrs: { language },
14
+ content: [{ type: "text", text: "{}" }],
15
+ },
16
+ ],
17
+ });
18
+
19
+ describe("BaseCodeBlock", () => {
20
+ it("renders the language as a class only, not as a raw attribute", () => {
21
+ const html = generateHTML(codeBlock("json"), extensions);
22
+ expect(html).toBe('<pre><code class="language-json">{}</code></pre>');
23
+ });
24
+
25
+ it("keeps the language through an HTML round-trip", () => {
26
+ const html = generateHTML(codeBlock("json"), extensions);
27
+ expect(generateJSON(html, extensions)).toEqual(codeBlock("json"));
28
+ });
29
+
30
+ it("reads the legacy data-language attribute", () => {
31
+ const json = generateJSON('<pre><code data-language="python">x</code></pre>', extensions);
32
+ expect(json.content?.[0]?.attrs?.language).toBe("python");
33
+ });
34
+
35
+ it("falls back to bash when the language is unknown", () => {
36
+ const json = generateJSON("<pre><code>x</code></pre>", extensions);
37
+ expect(json.content?.[0]?.attrs?.language).toBe("bash");
38
+ });
39
+ });
@@ -5,6 +5,13 @@ import { common, createLowlight } from "lowlight";
5
5
  const lowlight = createLowlight(common);
6
6
  lowlight.register("html", html);
7
7
 
8
+ function languageFromClass(code: Element | null, prefix: string): string | null {
9
+ const languageClass = Array.from(code?.classList ?? []).find((className) =>
10
+ className.startsWith(prefix),
11
+ );
12
+ return languageClass ? languageClass.slice(prefix.length) : null;
13
+ }
14
+
8
15
  export const BaseCodeBlock = CodeBlockLowlight.configure({
9
16
  lowlight,
10
17
  }).extend({
@@ -12,11 +19,18 @@ export const BaseCodeBlock = CodeBlockLowlight.configure({
12
19
  draggable: true,
13
20
 
14
21
  addAttributes() {
22
+ const { languageClassPrefix } = this.options;
23
+
15
24
  return {
16
25
  language: {
17
26
  default: null,
27
+ rendered: false,
18
28
  parseHTML: (element: HTMLElement) => {
19
- return element.querySelector("code")?.getAttribute("data-language") || "bash";
29
+ const code = element.querySelector("code");
30
+ const fromClass = languageClassPrefix
31
+ ? languageFromClass(code, languageClassPrefix)
32
+ : null;
33
+ return fromClass || code?.getAttribute("data-language") || "bash";
20
34
  },
21
35
  },
22
36
  };
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { isTrustedDomain } from "./link";
2
+ import { isOwnDomain, isTrustedDomain, linkAttributes } from "./link";
3
3
 
4
4
  describe("isTrustedDomain", () => {
5
5
  afterEach(() => {
@@ -58,3 +58,56 @@ describe("isTrustedDomain", () => {
58
58
  ).toBe(true);
59
59
  });
60
60
  });
61
+
62
+ describe("isOwnDomain", () => {
63
+ afterEach(() => {
64
+ vi.unstubAllGlobals();
65
+ });
66
+
67
+ it("matches the own domain and its subdomains", () => {
68
+ expect(isOwnDomain("https://cyberyozh.com/", ["cyberyozh.com"])).toBe(true);
69
+ expect(isOwnDomain("https://app.cyberyozh.com/ru/proxy/", ["cyberyozh.com"])).toBe(true);
70
+ expect(isOwnDomain("https://cyberyozh.pro/", ["cyberyozh.com"])).toBe(false);
71
+ });
72
+
73
+ it("treats the current window hostname as own regardless of the list", () => {
74
+ vi.stubGlobal("window", { location: { hostname: "app.detect.expert" } });
75
+ expect(isOwnDomain("https://app.detect.expert/page", [])).toBe(true);
76
+ });
77
+
78
+ it("returns false for unparseable urls", () => {
79
+ expect(isOwnDomain("/relative/path", ["cyberyozh.com"])).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe("linkAttributes", () => {
84
+ const options = { ownDomains: ["cyberyozh.com"], trustedDomains: ["cyberyozh.pro"] };
85
+
86
+ it("opens own links in the same tab without rel", () => {
87
+ expect(linkAttributes("https://app.cyberyozh.com/ru/catalog/", options)).toEqual({
88
+ target: "_self",
89
+ rel: null,
90
+ });
91
+ });
92
+
93
+ it("opens trusted links in a new tab without rel", () => {
94
+ expect(linkAttributes("https://cyberyozh.pro/blog/", options)).toEqual({
95
+ target: "_blank",
96
+ rel: null,
97
+ });
98
+ });
99
+
100
+ it("opens external links in a new tab with nofollow", () => {
101
+ expect(linkAttributes("https://github.com/", options)).toEqual({
102
+ target: "_blank",
103
+ rel: "noopener noreferrer nofollow",
104
+ });
105
+ });
106
+
107
+ it("keeps the previous behaviour when no domains are configured", () => {
108
+ expect(linkAttributes("https://app.cyberyozh.com/", { ownDomains: [], trustedDomains: [] })).toEqual({
109
+ target: "_blank",
110
+ rel: "noopener noreferrer nofollow",
111
+ });
112
+ });
113
+ });
@@ -4,6 +4,12 @@ import { Plugin, TextSelection } from "@tiptap/pm/state";
4
4
  import { find } from "linkifyjs";
5
5
 
6
6
  export interface ExtendedLinkOptions extends LinkOptions {
7
+ /**
8
+ * Domains (with all their subdomains) of the site the content belongs to:
9
+ * such links open in the same tab and get no rel on paste / HTML parsing.
10
+ * The current window hostname is always own.
11
+ */
12
+ ownDomains: string[];
7
13
  /**
8
14
  * Domains (with all their subdomains) whose links are treated as internal:
9
15
  * rel="noopener noreferrer nofollow" is not added on paste / HTML parsing.
@@ -12,64 +18,70 @@ export interface ExtendedLinkOptions extends LinkOptions {
12
18
  trustedDomains: string[];
13
19
  }
14
20
 
15
- const isSameDomain = (url: string): boolean => {
16
- if (typeof window === "undefined") return false;
21
+ export type LinkDomains = Pick<ExtendedLinkOptions, "ownDomains" | "trustedDomains">;
22
+
23
+ const hostnameOf = (url: string): string | null => {
17
24
  try {
18
- return new URL(url).hostname === window.location.hostname;
25
+ return new URL(url).hostname.toLowerCase();
19
26
  } catch {
20
- return false;
27
+ return null;
21
28
  }
22
29
  };
23
30
 
24
- export const isTrustedDomain = (
25
- url: string,
26
- trustedDomains: string[],
27
- ): boolean => {
28
- try {
29
- const { hostname } = new URL(url);
30
- if (
31
- typeof window !== "undefined" &&
32
- hostname === window.location.hostname
33
- ) {
34
- return true;
35
- }
36
- return trustedDomains.some((trusted) => {
37
- const domain = trusted.toLowerCase();
38
- return hostname === domain || hostname.endsWith(`.${domain}`);
39
- });
40
- } catch {
41
- return false;
42
- }
31
+ const isSameDomain = (url: string): boolean => {
32
+ if (typeof window === "undefined") return false;
33
+ return hostnameOf(url) === window.location.hostname;
34
+ };
35
+
36
+ const matchesDomains = (url: string, domains: string[]): boolean => {
37
+ const hostname = hostnameOf(url);
38
+ if (hostname === null) return false;
39
+ return domains.some((candidate) => {
40
+ const domain = candidate.toLowerCase();
41
+ return hostname === domain || hostname.endsWith(`.${domain}`);
42
+ });
43
+ };
44
+
45
+ export const isOwnDomain = (url: string, ownDomains: string[]): boolean =>
46
+ isSameDomain(url) || matchesDomains(url, ownDomains);
47
+
48
+ export const isTrustedDomain = (url: string, trustedDomains: string[]): boolean =>
49
+ isSameDomain(url) || matchesDomains(url, trustedDomains);
50
+
51
+ export const linkAttributes = (
52
+ href: string,
53
+ { ownDomains, trustedDomains }: LinkDomains,
54
+ ): { target: string; rel: string | null } => {
55
+ const own = isOwnDomain(href, ownDomains);
56
+ return {
57
+ target: own ? "_self" : "_blank",
58
+ rel: own || isTrustedDomain(href, trustedDomains) ? null : "noopener noreferrer nofollow",
59
+ };
43
60
  };
44
61
 
45
62
  export const LinkExtension = Link.extend<ExtendedLinkOptions>({
46
63
  addOptions() {
47
64
  return {
48
65
  ...(this.parent?.() ?? ({} as ExtendedLinkOptions)),
66
+ ownDomains: [] as string[],
49
67
  trustedDomains: [] as string[],
50
68
  };
51
69
  },
52
70
 
53
71
  addAttributes() {
54
- const { trustedDomains } = this.options;
72
+ const domains = this.options;
55
73
 
56
74
  return {
57
75
  ...this.parent?.(),
58
76
  target: {
59
77
  default: "_blank",
60
- parseHTML: (element) => {
61
- const href = element.getAttribute("href") ?? "";
62
- return isSameDomain(href) ? "_self" : "_blank";
63
- },
78
+ parseHTML: (element) =>
79
+ linkAttributes(element.getAttribute("href") ?? "", domains).target,
64
80
  },
65
81
  rel: {
66
82
  default: null,
67
- parseHTML: (element) => {
68
- const href = element.getAttribute("href") ?? "";
69
- return isTrustedDomain(href, trustedDomains)
70
- ? null
71
- : "noopener noreferrer nofollow";
72
- },
83
+ parseHTML: (element) =>
84
+ linkAttributes(element.getAttribute("href") ?? "", domains).rel,
73
85
  },
74
86
  };
75
87
  },
@@ -105,13 +117,7 @@ export const LinkExtension = Link.extend<ExtendedLinkOptions>({
105
117
  type: this.type,
106
118
  getAttributes: (match) => {
107
119
  const href = match.data?.href ?? "";
108
- return {
109
- href,
110
- target: isSameDomain(href) ? "_self" : "_blank",
111
- rel: isTrustedDomain(href, this.options.trustedDomains)
112
- ? null
113
- : "noopener noreferrer nofollow",
114
- };
120
+ return { href, ...linkAttributes(href, this.options) };
115
121
  },
116
122
  }),
117
123
  ];
@@ -29,6 +29,7 @@ interface GetExtensionsOptions {
29
29
  getImageById?: (fileId: string) => string | null;
30
30
  onKatexClick?: (latex: string, pos?: number) => void;
31
31
  onFigureClick?: (attrs: any, pos: number) => void;
32
+ ownDomains?: string[];
32
33
  trustedDomains?: string[];
33
34
  }
34
35
 
@@ -45,9 +46,10 @@ export function getEditorExtensions(options: GetExtensionsOptions) {
45
46
  getImageById,
46
47
  onKatexClick,
47
48
  onFigureClick,
49
+ ownDomains,
48
50
  trustedDomains,
49
51
  } = options;
50
- const sharedExtensions = getSharedExtensions({ trustedDomains });
52
+ const sharedExtensions = getSharedExtensions({ ownDomains, trustedDomains });
51
53
 
52
54
  return [
53
55
  ...sharedExtensions,
@@ -15,7 +15,10 @@ import {
15
15
  BaseVideo,
16
16
  BaseFigure,
17
17
  } from "../base";
18
- import { getSharedExtensions } from "@/utils/get-shared-extensions";
18
+ import {
19
+ getSharedExtensions,
20
+ type SharedExtensionsOptions,
21
+ } from "@/utils/get-shared-extensions";
19
22
  import {
20
23
  BaseLegacyCustomBlockquote,
21
24
  BaseLegacyPromptSE,
@@ -28,8 +31,8 @@ import {
28
31
  BaseLegacyFigure,
29
32
  } from "../base/compatibility";
30
33
 
31
- export function getRenderExtensions() {
32
- const sharedExtensions = getSharedExtensions();
34
+ export function getRenderExtensions(options: SharedExtensionsOptions = {}) {
35
+ const sharedExtensions = getSharedExtensions(options);
33
36
 
34
37
  return [
35
38
  ...sharedExtensions,
@@ -0,0 +1,154 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { htmlToTiptapContent, tiptapContentToHtml } from "./index";
3
+
4
+ const paragraph = (content: object[]) => ({
5
+ type: "paragraph",
6
+ attrs: { textAlign: null },
7
+ content,
8
+ });
9
+
10
+ const link = (href: string, target: string, rel: string | null) => ({
11
+ type: "link",
12
+ attrs: { href, target, rel, class: null, title: null },
13
+ });
14
+
15
+ const document = {
16
+ type: "doc",
17
+ content: [
18
+ paragraph([
19
+ { type: "text", text: "Read about " },
20
+ {
21
+ type: "text",
22
+ marks: [link("https://app.cyberyozh.com/ru/proxy/", "_self", null), { type: "bold" }],
23
+ text: "proxies",
24
+ },
25
+ { type: "text", text: " and " },
26
+ {
27
+ type: "text",
28
+ marks: [link("https://cyberyozh.pro/", "_blank", null)],
29
+ text: "the blog",
30
+ },
31
+ { type: "text", text: " or " },
32
+ {
33
+ type: "text",
34
+ marks: [link("https://github.com/", "_blank", "noopener noreferrer nofollow")],
35
+ text: "GitHub",
36
+ },
37
+ { type: "text", text: "." },
38
+ ]),
39
+ {
40
+ type: "callout",
41
+ attrs: { type: "info", bgColor: "#F6F0FF", emoji: "⭐", calloutType: null, callout: null },
42
+ content: [
43
+ {
44
+ type: "bulletList",
45
+ content: [
46
+ { type: "listItem", content: [paragraph([{ type: "text", text: "First" }])] },
47
+ { type: "listItem", content: [paragraph([{ type: "text", text: "Second" }])] },
48
+ ],
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ type: "table",
54
+ content: [
55
+ {
56
+ type: "tableRow",
57
+ content: [
58
+ {
59
+ type: "tableHeader",
60
+ attrs: { colspan: 1, rowspan: 1, colwidth: [200], align: null },
61
+ content: [paragraph([{ type: "text", text: "Access" }])],
62
+ },
63
+ {
64
+ type: "tableCell",
65
+ attrs: { colspan: 1, rowspan: 1, colwidth: null, align: null },
66
+ content: [paragraph([{ type: "text", text: "Manual search" }])],
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ type: "codeBlock",
74
+ attrs: { language: "json" },
75
+ content: [{ type: "text", text: '{ "ok": true }' }],
76
+ },
77
+ paragraph([
78
+ { type: "text", marks: [{ type: "strike" }], text: "struck" },
79
+ { type: "text", text: " " },
80
+ { type: "text", marks: [{ type: "highlight", attrs: { color: "#adb79e" } }], text: "marked" },
81
+ { type: "text", text: " " },
82
+ { type: "text", marks: [{ type: "copyable-text" }], text: "copyable" },
83
+ { type: "text", text: " " },
84
+ {
85
+ type: "text",
86
+ marks: [
87
+ {
88
+ type: "textStyle",
89
+ attrs: { backgroundColor: "", color: "#582f95", fontFamily: "", fontSize: "", lineHeight: "" },
90
+ },
91
+ ],
92
+ text: "colored",
93
+ },
94
+ { type: "text", text: " " },
95
+ { type: "inlineMath", attrs: { latex: "\\utilde{AB}" } },
96
+ ]),
97
+ {
98
+ type: "orderedList",
99
+ attrs: { start: 1, type: null },
100
+ content: [{ type: "listItem", content: [paragraph([{ type: "text", text: "One" }])] }],
101
+ },
102
+ { type: "blockquote", content: [paragraph([{ type: "text", text: "Quote" }])] },
103
+ {
104
+ type: "details",
105
+ attrs: { open: false },
106
+ content: [
107
+ { type: "detailsSummary", content: [{ type: "text", text: "Summary" }] },
108
+ { type: "detailsContent", content: [paragraph([{ type: "text", text: "Hidden" }])] },
109
+ ],
110
+ },
111
+ {
112
+ type: "customQuote",
113
+ content: [
114
+ { type: "customQuoteText", attrs: { open: true }, content: [{ type: "text", text: "Quote" }] },
115
+ { type: "customQuoteAuthor", content: [{ type: "text", text: "Author" }] },
116
+ { type: "customQuoteSign", content: [{ type: "text", text: "Sign" }] },
117
+ ],
118
+ },
119
+ {
120
+ type: "mythReality",
121
+ content: [
122
+ { type: "mythRealityBlock", content: [paragraph([{ type: "text", text: "Myth" }])] },
123
+ { type: "mythRealityBlock", content: [paragraph([{ type: "text", text: "Reality" }])] },
124
+ ],
125
+ },
126
+ {
127
+ type: "prompt",
128
+ content: [
129
+ { type: "promptHeader", content: [{ type: "text", text: "Prompt" }] },
130
+ { type: "promptContent", content: [{ type: "text", text: "Text of the prompt" }] },
131
+ ],
132
+ },
133
+ ],
134
+ };
135
+
136
+ const domains = { ownDomains: ["cyberyozh.com"], trustedDomains: ["cyberyozh.pro"] };
137
+
138
+ describe("html entry point", () => {
139
+ it("round-trips every node and mark of the content model", () => {
140
+ const html = tiptapContentToHtml(document, domains);
141
+ expect(htmlToTiptapContent(html, domains)).toEqual(document);
142
+ });
143
+
144
+ it("writes custom nodes as HTML the parser reads back", () => {
145
+ const html = tiptapContentToHtml(document, domains);
146
+ expect(html).toContain('<div data-callout="info"');
147
+ expect(html).toContain("<table");
148
+ expect(html).toContain('<code class="language-json">');
149
+ });
150
+
151
+ it("parses empty HTML into an empty document", () => {
152
+ expect(htmlToTiptapContent("")).toEqual({ type: "doc", content: [{ type: "paragraph", attrs: { textAlign: null } }] });
153
+ });
154
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * HTML entry point for @cyberyozh/text-editor
3
+ *
4
+ * Converts Tiptap JSON content to HTML and back with the full set of content extensions.
5
+ * Server-side only: parses with happy-dom, no React and no stylesheets, so it is safe for
6
+ * server code, scripts and tests. Broken HTML throws instead of returning an empty document.
7
+ */
8
+
9
+ import type { JSONContent } from "@tiptap/core";
10
+ import { generateHTML, generateJSON } from "@tiptap/html/server";
11
+ import { getRenderExtensions } from "../extensions/render";
12
+ import type { SharedExtensionsOptions } from "../utils/get-shared-extensions";
13
+
14
+ export type { SharedExtensionsOptions } from "../utils/get-shared-extensions";
15
+ export type { JSONContent };
16
+
17
+ export function tiptapContentToHtml(
18
+ content: JSONContent,
19
+ options: SharedExtensionsOptions = {},
20
+ ): string {
21
+ return generateHTML(content, getRenderExtensions(options));
22
+ }
23
+
24
+ export function htmlToTiptapContent(
25
+ html: string,
26
+ options: SharedExtensionsOptions = {},
27
+ ): JSONContent {
28
+ return generateJSON(html, getRenderExtensions(options));
29
+ }
@@ -17,6 +17,11 @@ import { TextStyleKit } from "@tiptap/extension-text-style";
17
17
  import { LegacyInlineLatexSE } from "@/extensions/base/compatibility";
18
18
 
19
19
  export interface SharedExtensionsOptions {
20
+ /**
21
+ * Domains (with all their subdomains) of the site the content belongs to —
22
+ * see the ownDomains option of LinkExtension.
23
+ */
24
+ ownDomains?: string[];
20
25
  /**
21
26
  * Domains (with all their subdomains) whose links are treated as internal —
22
27
  * see the trustedDomains option of LinkExtension.
@@ -35,7 +40,10 @@ export function getSharedExtensions(options: SharedExtensionsOptions = {}) {
35
40
  link: false,
36
41
  }),
37
42
  Highlight.configure({ multicolor: true }),
38
- LinkExtension.configure({ trustedDomains: options.trustedDomains ?? [] }),
43
+ LinkExtension.configure({
44
+ ownDomains: options.ownDomains ?? [],
45
+ trustedDomains: options.trustedDomains ?? [],
46
+ }),
39
47
  Superscript,
40
48
  Subscript,
41
49
  TextAlign.configure({
@@ -1,11 +1,7 @@
1
- import { generateJSON, type JSONContent } from "@tiptap/core";
2
- import {
3
- getSharedExtensions,
4
- type SharedExtensionsOptions,
5
- } from "./get-shared-extensions";
6
- import { BaseFigure } from "@/extensions/base/figure";
7
- import { TableKit } from "@tiptap/extension-table";
8
- import { BaseCodeBlock } from "@/extensions/base";
1
+ import { type JSONContent } from "@tiptap/core";
2
+ import { generateJSON } from "@tiptap/html";
3
+ import { getRenderExtensions } from "@/extensions/render";
4
+ import type { SharedExtensionsOptions } from "./get-shared-extensions";
9
5
 
10
6
  function isJSONContent(value: unknown): value is JSONContent {
11
7
  return (
@@ -39,12 +35,8 @@ export function htmlToTiptapContent(
39
35
  return { type: "doc", content: [{ type: "paragraph" }] };
40
36
  }
41
37
 
42
- const sharedExtensions = getSharedExtensions(options);
43
-
44
- const extensions = [...sharedExtensions, BaseFigure, TableKit, BaseCodeBlock];
45
-
46
38
  try {
47
- return generateJSON(content, extensions);
39
+ return generateJSON(content, getRenderExtensions(options));
48
40
  } catch (error) {
49
41
  console.error("Failed to convert HTML to Tiptap content:", error);
50
42
  return { type: "doc", content: [{ type: "paragraph" }] };