@paroicms/tiptap-editor-plugin 1.0.2 → 1.0.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/admin-ui-plugin/dist/admin-ui-plugin.mjs +81 -73
- package/backend/dist/extensions/custom-link.js +38 -0
- package/backend/dist/extensions/media-node.js +6 -1
- package/backend/dist/index.js +0 -2
- package/backend/dist/tiptap-json.js +7 -45
- package/package.json +4 -4
- package/backend/dist/clean-tiptap-json.js +0 -53
- package/backend/dist/tiptap-types.js +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Link from "@tiptap/extension-link";
|
|
2
|
+
/**
|
|
3
|
+
* Custom Link extension for backend rendering.
|
|
4
|
+
* - Renders only href and target from stored JSON (no rel, no class)
|
|
5
|
+
*/
|
|
6
|
+
export const CustomLink = Link.extend({
|
|
7
|
+
addOptions() {
|
|
8
|
+
return {
|
|
9
|
+
...this.parent?.(),
|
|
10
|
+
HTMLAttributes: {},
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
addAttributes() {
|
|
14
|
+
return {
|
|
15
|
+
href: {
|
|
16
|
+
default: undefined,
|
|
17
|
+
},
|
|
18
|
+
target: {
|
|
19
|
+
default: undefined,
|
|
20
|
+
},
|
|
21
|
+
rel: {
|
|
22
|
+
default: undefined,
|
|
23
|
+
},
|
|
24
|
+
class: {
|
|
25
|
+
default: undefined,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
renderHTML({ HTMLAttributes }) {
|
|
30
|
+
const attrs = {
|
|
31
|
+
href: HTMLAttributes.href || "",
|
|
32
|
+
};
|
|
33
|
+
if (HTMLAttributes.target) {
|
|
34
|
+
attrs.target = HTMLAttributes.target;
|
|
35
|
+
}
|
|
36
|
+
return ["a", attrs, 0];
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isResizeRule } from "@paroicms/public-anywhere-lib";
|
|
2
2
|
import { Node } from "@tiptap/core";
|
|
3
3
|
import { type } from "arktype";
|
|
4
|
+
import { shouldOpenInBlankTab } from "../url-blank-tab.js";
|
|
4
5
|
export const MediaNodeInputAT = type({
|
|
5
6
|
mediaId: "string",
|
|
6
7
|
resizeRule: "string",
|
|
@@ -99,7 +100,11 @@ export function createMediaNodeBackend(service, mediaMap) {
|
|
|
99
100
|
];
|
|
100
101
|
const captionSpec = caption ? ["figcaption", { class: "Fig-caption" }, caption] : null;
|
|
101
102
|
if (href) {
|
|
102
|
-
const
|
|
103
|
+
const linkAttrs = { href };
|
|
104
|
+
if (shouldOpenInBlankTab(service.pluginService, href)) {
|
|
105
|
+
linkAttrs.target = "_blank";
|
|
106
|
+
}
|
|
107
|
+
const linkSpec = ["a", linkAttrs, imgSpec];
|
|
103
108
|
const children = [linkSpec, captionSpec].filter(Boolean);
|
|
104
109
|
return ["figure", figAttrs, ...children];
|
|
105
110
|
}
|
package/backend/dist/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import { isJsonFieldValue, isObj } from "@paroicms/public-anywhere-lib";
|
|
|
2
2
|
import { makeStylesheetLinkAsyncTag } from "@paroicms/public-server-lib";
|
|
3
3
|
import { esmDirName, extractPackageNameAndVersionSync } from "@paroicms/script-lib";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { cleanTiptapJson } from "./clean-tiptap-json.js";
|
|
6
5
|
import { normalizeTypographyInTiptapJson } from "./normalize-typography-in-tiptap.js";
|
|
7
6
|
import { convertTiptapJsonToHtml, convertTiptapJsonToPlainText } from "./tiptap-json.js";
|
|
8
7
|
const projectDir = dirname(esmDirName(import.meta.url));
|
|
@@ -43,7 +42,6 @@ const plugin = {
|
|
|
43
42
|
if (fieldType.normalizeTypography) {
|
|
44
43
|
newJson = await normalizeTypographyInTiptapJson(service, newJson, language);
|
|
45
44
|
}
|
|
46
|
-
newJson = cleanTiptapJson(newJson);
|
|
47
45
|
return { j: newJson };
|
|
48
46
|
});
|
|
49
47
|
service.registerHeadTags(({ state }) => {
|
|
@@ -8,12 +8,12 @@ import TextAlign from "@tiptap/extension-text-align";
|
|
|
8
8
|
import { TextStyle } from "@tiptap/extension-text-style";
|
|
9
9
|
import StarterKit from "@tiptap/starter-kit";
|
|
10
10
|
import { renderToHTMLString } from "@tiptap/static-renderer";
|
|
11
|
+
import { CustomLink } from "./extensions/custom-link.js";
|
|
11
12
|
import { createHtmlSnippetNodeBackend } from "./extensions/html-snippet-node.js";
|
|
12
13
|
import { createInternalLinkNodeBackend, preloadInternalLinkDocuments, } from "./extensions/internal-link-node.js";
|
|
13
14
|
import { createMediaNodeBackend, preloadMediaImages, } from "./extensions/media-node.js";
|
|
14
15
|
import { createObfuscateMarkBackend, createObfuscateMarkRenderer, } from "./extensions/obfuscate-mark.js";
|
|
15
16
|
import { createPlatformVideoNodeBackend } from "./extensions/platform-video-node.js";
|
|
16
|
-
import { shouldOpenInBlankTab } from "./url-blank-tab.js";
|
|
17
17
|
const TextSize = Mark.create({
|
|
18
18
|
name: "textSize",
|
|
19
19
|
addAttributes() {
|
|
@@ -50,57 +50,17 @@ const TextSize = Mark.create({
|
|
|
50
50
|
},
|
|
51
51
|
});
|
|
52
52
|
export async function convertTiptapJsonToHtml(service, json, options) {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const usedLanguages = collectCodeBlockLanguages(processedJson);
|
|
53
|
+
const mediaMap = await preloadMediaImages(service, json, options);
|
|
54
|
+
const documentMap = await preloadInternalLinkDocuments(service, json, options);
|
|
55
|
+
const usedLanguages = collectCodeBlockLanguages(json);
|
|
57
56
|
if (usedLanguages.size > 0) {
|
|
58
57
|
service.setRenderState("paTiptapEditor:languages", Array.from(usedLanguages));
|
|
59
58
|
}
|
|
60
|
-
return renderTiptapToHtml(
|
|
59
|
+
return renderTiptapToHtml(json, service, mediaMap, documentMap, {
|
|
61
60
|
includeObfuscateMarkMapping: true,
|
|
62
61
|
renderingOptions: options,
|
|
63
62
|
});
|
|
64
63
|
}
|
|
65
|
-
function addTargetBlankToLinks(service, json) {
|
|
66
|
-
if (!json.content)
|
|
67
|
-
return json;
|
|
68
|
-
function processNodes(nodes) {
|
|
69
|
-
return nodes.map((node) => {
|
|
70
|
-
let processedNode = node;
|
|
71
|
-
if (node.marks) {
|
|
72
|
-
processedNode = {
|
|
73
|
-
...processedNode,
|
|
74
|
-
marks: node.marks.map((mark) => {
|
|
75
|
-
if (mark.type === "link" && mark.attrs?.href) {
|
|
76
|
-
if (shouldOpenInBlankTab(service.pluginService, mark.attrs.href)) {
|
|
77
|
-
return {
|
|
78
|
-
...mark,
|
|
79
|
-
attrs: {
|
|
80
|
-
...mark.attrs,
|
|
81
|
-
target: "_blank",
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return mark;
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
if (node.content) {
|
|
91
|
-
return {
|
|
92
|
-
...processedNode,
|
|
93
|
-
content: processNodes(node.content),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
return processedNode;
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
...json,
|
|
101
|
-
content: processNodes(json.content),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
64
|
export function convertTiptapJsonToPlainText(service, json) {
|
|
105
65
|
const cleanedJson = removeObfuscatedText(json);
|
|
106
66
|
const emptyMediaMap = new Map();
|
|
@@ -162,7 +122,9 @@ function renderTiptapToHtml(json, service, mediaMap, documentMap, options) {
|
|
|
162
122
|
StarterKit.configure({
|
|
163
123
|
code: hasCodeInConf ? false : undefined,
|
|
164
124
|
codeBlock: hasCodeInConf ? false : undefined,
|
|
125
|
+
link: false, // Disable default Link to use CustomLink instead
|
|
165
126
|
}),
|
|
127
|
+
CustomLink,
|
|
166
128
|
...(hasCodeInConf ? [Code, CodeBlockLowlight] : []),
|
|
167
129
|
TextAlign.configure({
|
|
168
130
|
types: ["heading", "paragraph"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paroicms/tiptap-editor-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Tiptap editor plugin for ParoiCMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"paroicms",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"test:watch": "vitest"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@paroicms/script-lib": "0.3.
|
|
32
|
+
"@paroicms/script-lib": "0.3.4",
|
|
33
33
|
"@tiptap/core": "~3.7.2",
|
|
34
34
|
"@tiptap/extension-code": "~3.7.2",
|
|
35
35
|
"@tiptap/extension-code-block-lowlight": "~3.7.2",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@paroicms/public-server-lib": "0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@paroicms/public-anywhere-lib": "0.37.
|
|
50
|
-
"@paroicms/public-server-lib": "0.47.
|
|
49
|
+
"@paroicms/public-anywhere-lib": "0.37.2",
|
|
50
|
+
"@paroicms/public-server-lib": "0.47.3",
|
|
51
51
|
"@types/node": "~24.8.1",
|
|
52
52
|
"rimraf": "~6.0.1",
|
|
53
53
|
"sass": "~1.93.2",
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Remove empty attrs objects and marks with null values from Tiptap JSON to reduce storage size.
|
|
3
|
-
* When no attributes are set, the attrs object is omitted entirely.
|
|
4
|
-
* Marks with null or undefined values in their attributes are removed.
|
|
5
|
-
*/
|
|
6
|
-
export function cleanTiptapJson(json) {
|
|
7
|
-
function cleanNode(node) {
|
|
8
|
-
const cleaned = {
|
|
9
|
-
type: node.type,
|
|
10
|
-
};
|
|
11
|
-
// Only include attrs if it has properties with non-null values
|
|
12
|
-
if (node.attrs && Object.keys(node.attrs).length > 0) {
|
|
13
|
-
const cleanedAttrs = {};
|
|
14
|
-
for (const [key, value] of Object.entries(node.attrs)) {
|
|
15
|
-
if (value !== null && value !== undefined) {
|
|
16
|
-
cleanedAttrs[key] = value;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
if (Object.keys(cleanedAttrs).length > 0) {
|
|
20
|
-
cleaned.attrs = cleanedAttrs;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
// Recursively clean child nodes
|
|
24
|
-
if (node.content) {
|
|
25
|
-
cleaned.content = node.content.map(cleanNode);
|
|
26
|
-
}
|
|
27
|
-
// Clean marks: remove marks with null/undefined values or empty attrs
|
|
28
|
-
if (node.marks) {
|
|
29
|
-
const cleanedMarks = node.marks.filter((mark) => {
|
|
30
|
-
// Keep marks without attrs
|
|
31
|
-
if (!mark.attrs)
|
|
32
|
-
return true;
|
|
33
|
-
// Remove marks with empty attrs object
|
|
34
|
-
if (Object.keys(mark.attrs).length === 0)
|
|
35
|
-
return false;
|
|
36
|
-
// Remove marks where all attribute values are null or undefined
|
|
37
|
-
const hasValidValue = Object.values(mark.attrs).some((value) => value !== null && value !== undefined);
|
|
38
|
-
return hasValidValue;
|
|
39
|
-
});
|
|
40
|
-
if (cleanedMarks.length > 0) {
|
|
41
|
-
cleaned.marks = cleanedMarks;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (node.text !== undefined) {
|
|
45
|
-
cleaned.text = node.text;
|
|
46
|
-
}
|
|
47
|
-
return cleaned;
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
type: "doc",
|
|
51
|
-
content: json.content?.map(cleanNode),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|