@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.
@@ -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 linkSpec = ["a", { href }, imgSpec];
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
  }
@@ -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 processedJson = addTargetBlankToLinks(service, json);
54
- const mediaMap = await preloadMediaImages(service, processedJson, options);
55
- const documentMap = await preloadInternalLinkDocuments(service, processedJson, options);
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(processedJson, service, mediaMap, documentMap, {
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.2",
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.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.1",
50
- "@paroicms/public-server-lib": "0.47.2",
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 {};