@platecms/delta-cast 1.0.0 → 1.2.0

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,277 @@
1
+ import { Node, Literal as UnistLiteral, Parent as UnistParent } from "unist";
2
+
3
+ export type Content = BlockContent | CasSuggestion | InlineContent;
4
+
5
+ export type BlockContent = BlockContentMap[keyof BlockContentMap];
6
+ export type InlineContent = InlineContentMap[keyof InlineContentMap];
7
+
8
+ export interface BlockContentMap {
9
+ paragraph: Paragraph;
10
+ heading: Heading;
11
+ list: List;
12
+ // List item should not be used anywhere except in the list's children
13
+ listItem: ListItem;
14
+ blockquote: Blockquote;
15
+ code: Code;
16
+ }
17
+
18
+ export interface InlineContentMap {
19
+ text: Text;
20
+ bold: Bold;
21
+ italic: Italic;
22
+ underline: Underline;
23
+ strikethrough: Strikethrough;
24
+ inlineCode: InlineCode;
25
+ highlight: Highlight;
26
+ contentValue: InterpolatedContentValue;
27
+ externalLink: ExternalLink;
28
+ internalLink: InternalLink;
29
+ casSuggestion: CasSuggestion;
30
+ contentValueSuggestion: ContentValueSuggestion;
31
+ }
32
+
33
+ export interface Parent extends UnistParent {
34
+ children: Content[];
35
+ }
36
+
37
+ export interface Literal extends UnistLiteral {
38
+ value: string;
39
+ }
40
+
41
+ /*
42
+ ** Specific CAST Definitions
43
+ */
44
+
45
+ /**
46
+ * The root node of a CAST document. The unist tree starts here. Each CAST document has one root node.
47
+ *
48
+ * ```json
49
+ * {
50
+ * "type": "root",
51
+ * "children": [
52
+ * ...
53
+ * ]
54
+ * }
55
+ */
56
+ export interface Root extends Parent {
57
+ type: "root";
58
+ }
59
+
60
+ /*
61
+ ** Block Content
62
+ */
63
+ export interface Paragraph extends Parent {
64
+ type: "paragraph";
65
+
66
+ /**
67
+ * The children of a paragraph are inline content.
68
+ */
69
+ children: InlineContent[];
70
+ }
71
+
72
+ export interface Heading extends Parent {
73
+ type: "heading";
74
+
75
+ /**
76
+ * The level of a heading is a number between 1 and 6.
77
+ */
78
+ level: 1 | 2 | 3 | 4 | 5 | 6;
79
+
80
+ /**
81
+ * The children of a heading are inline content.
82
+ */
83
+ children: InlineContent[];
84
+ }
85
+
86
+ export interface List extends Parent {
87
+ type: "list";
88
+
89
+ children: ListItem[];
90
+
91
+ /**
92
+ * Whether the list is ordered or not.
93
+ */
94
+ ordered: boolean;
95
+
96
+ /**
97
+ * The start of an ordered list.
98
+ */
99
+ start?: number;
100
+ }
101
+
102
+ export interface ListItem extends Parent {
103
+ type: "listItem";
104
+
105
+ /**
106
+ * The children of a list item are inline content.
107
+ */
108
+ children: InlineContent[];
109
+ }
110
+
111
+ export interface Blockquote extends Parent {
112
+ type: "blockquote";
113
+
114
+ /**
115
+ * The children of a blockquote are inline content.
116
+ */
117
+ children: InlineContent[];
118
+ }
119
+
120
+ export interface Code extends Parent {
121
+ type: "code";
122
+
123
+ /**
124
+ * The language of the code block.
125
+ */
126
+ lang?: string;
127
+
128
+ children: Text[];
129
+ }
130
+
131
+ /*
132
+ ** Inline Content
133
+ */
134
+ export interface Text extends Literal {
135
+ type: "text";
136
+ }
137
+
138
+ export interface Bold extends Parent {
139
+ type: "bold";
140
+
141
+ /**
142
+ * The children of a bold node are inline content.
143
+ */
144
+ children: InlineContent[];
145
+ }
146
+
147
+ export interface Italic extends Parent {
148
+ type: "italic";
149
+
150
+ /**
151
+ * The children of an italic node are inline content.
152
+ */
153
+ children: InlineContent[];
154
+ }
155
+
156
+ export interface Underline extends Parent {
157
+ type: "underline";
158
+
159
+ /**
160
+ * The children of an underline node are inline content.
161
+ */
162
+ children: InlineContent[];
163
+ }
164
+
165
+ export interface Strikethrough extends Parent {
166
+ type: "strikethrough";
167
+
168
+ /**
169
+ * The children of a strikethrough node are inline content.
170
+ */
171
+ children: InlineContent[];
172
+ }
173
+
174
+ export interface InlineCode extends Literal {
175
+ type: "inlineCode";
176
+ }
177
+
178
+ export interface Highlight extends Parent {
179
+ type: "highlight";
180
+
181
+ /**
182
+ * The children of a highlight node are inline content
183
+ */
184
+ children: InlineContent[];
185
+ }
186
+
187
+ /**
188
+ * Represents an interpolated Content Value. Interpolated values are values that are stored elsewhere in the database
189
+ * and inserted into the document at request-time.
190
+ */
191
+ export interface InterpolatedContentValue extends Node {
192
+ type: "contentValue";
193
+
194
+ /**
195
+ * The PRN (identifier) of the referenced Content Value.
196
+ */
197
+ prn: string;
198
+
199
+ /**
200
+ * The nested CAST tree of the referenced Content Value.
201
+ */
202
+ value?: Root;
203
+ }
204
+
205
+ export interface ContentValueSuggestion extends Node {
206
+ type: "contentValueSuggestion";
207
+
208
+ /**
209
+ * The PRN (identifier) of the referenced Content Value.
210
+ */
211
+ prn: string;
212
+
213
+ /**
214
+ * The nested CAST tree of the referenced Content Value.
215
+ */
216
+ value?: Root;
217
+ }
218
+
219
+ export interface ExternalLink extends Parent {
220
+ type: "externalLink";
221
+
222
+ children: Text[];
223
+
224
+ /**
225
+ * The URL of the link.
226
+ */
227
+ url: string;
228
+
229
+ target?: "_parent" | "_top" | "blank" | "self";
230
+ }
231
+
232
+ export interface InternalLink extends Parent {
233
+ type: "internalLink";
234
+
235
+ children: Text[];
236
+
237
+ /**
238
+ * The PRN (identifier) of the referenced Path Part or Grid Placement.
239
+ */
240
+ prn: string;
241
+
242
+ /**
243
+ * The URL of the link. Consists of the channel and the path part. Optionally includes a grid placement reference (e.g. #anchor-name).
244
+ * Even though the URL is marked as optional, it is always defined when retrieving an internal link.
245
+ * It is not required when saving an internal link.
246
+ */
247
+ url?: string;
248
+
249
+ target?: "_parent" | "_top" | "blank" | "self";
250
+ }
251
+
252
+ /**
253
+ * A CAS suggestion node.
254
+ */
255
+ export interface CasSuggestion extends Node {
256
+ type: "casSuggestion";
257
+
258
+ /**
259
+ * The original content that is suggested to be replaced.
260
+ */
261
+ original: Content[];
262
+
263
+ /**
264
+ * The suggested replacement content.
265
+ */
266
+ suggested: Content[];
267
+
268
+ /**
269
+ * A message explaining the suggestion.
270
+ */
271
+ message: string;
272
+
273
+ /**
274
+ * The method or algorithm that created the suggestion (e.g. "database; reusable")
275
+ */
276
+ method: string;
277
+ }
@@ -0,0 +1,395 @@
1
+ import { validateCast } from "./validate-cast";
2
+ import {
3
+ Blockquote,
4
+ Bold,
5
+ CasSuggestion,
6
+ Code,
7
+ Content,
8
+ ExternalLink,
9
+ Heading,
10
+ Highlight,
11
+ InlineCode,
12
+ InlineContent,
13
+ InternalLink,
14
+ InterpolatedContentValue,
15
+ Italic,
16
+ List,
17
+ ListItem,
18
+ Paragraph,
19
+ Root,
20
+ Strikethrough,
21
+ Text,
22
+ Underline,
23
+ } from "./schemas/schema";
24
+ import { InvalidCastError } from "./invalid-cast.error";
25
+
26
+ describe("validateCast", () => {
27
+ describe("validates correct CAST trees successfully", () => {
28
+ it.each([
29
+ [
30
+ "valid paragraph",
31
+ { type: "paragraph", children: [{ type: "text", value: "Hello, world!" }] } satisfies Paragraph,
32
+ ],
33
+ [
34
+ "valid heading",
35
+ { type: "heading", level: 1, children: [{ type: "text", value: "Heading" }] } satisfies Heading,
36
+ ],
37
+ [
38
+ "valid list",
39
+ {
40
+ type: "list",
41
+ children: [{ type: "listItem", children: [{ type: "text", value: "Item" }] }],
42
+ ordered: false,
43
+ } satisfies List,
44
+ ],
45
+ [
46
+ "valid code block",
47
+ {
48
+ type: "code",
49
+ lang: "javascript",
50
+ children: [{ type: "text", value: "console.log('Hello, world!');" }],
51
+ } satisfies Code,
52
+ ],
53
+ [
54
+ "valid interpolated content value",
55
+ { type: "contentValue", prn: "prn:plate-dev:1234:cms:test:abcd" } satisfies InterpolatedContentValue,
56
+ ],
57
+ [
58
+ "valid external link",
59
+ {
60
+ type: "externalLink",
61
+ url: "https://www.google.com",
62
+ children: [{ type: "text", value: "External link" }],
63
+ } satisfies ExternalLink,
64
+ ],
65
+ [
66
+ "valid internal link",
67
+ {
68
+ type: "internalLink",
69
+ url: "https://www.hotet.getplate.rocks/blogs",
70
+ prn: "prn:plate-dev:1234:cms:test:abcd",
71
+ children: [{ type: "text", value: "Internal link" }],
72
+ } satisfies InternalLink,
73
+ ],
74
+ [
75
+ "valid internal link with anchor",
76
+ {
77
+ type: "internalLink",
78
+ url: "https://www.hotet.getplate.rocks/blogs#123",
79
+ prn: "prn:plate-dev:1234:cms:test:abcd",
80
+ children: [{ type: "text", value: "Internal link" }],
81
+ } satisfies InternalLink,
82
+ ],
83
+ ["valid text", { type: "text", value: "Hello, world!" } satisfies Text],
84
+ ["valid bold", { type: "bold", children: [{ type: "text", value: "Bold text" }] } satisfies Bold],
85
+ ["valid italic", { type: "italic", children: [{ type: "text", value: "Italic text" }] } satisfies Italic],
86
+ [
87
+ "valid underline",
88
+ { type: "underline", children: [{ type: "text", value: "Underline text" }] } satisfies Underline,
89
+ ],
90
+ [
91
+ "valid strikethrough",
92
+ { type: "strikethrough", children: [{ type: "text", value: "Strikethrough text" }] } satisfies Strikethrough,
93
+ ],
94
+ ["valid inline code", { type: "inlineCode", value: "code" } satisfies InlineCode],
95
+ [
96
+ "valid highlight",
97
+ { type: "highlight", children: [{ type: "text", value: "Highlight text" }] } satisfies Highlight,
98
+ ],
99
+ [
100
+ "valid blockquote",
101
+ { type: "blockquote", children: [{ type: "text", value: "Blockquote text" }] } satisfies Blockquote,
102
+ ],
103
+ [
104
+ "valid cas suggestion",
105
+ {
106
+ type: "casSuggestion",
107
+ original: [{ type: "text", value: "Original text" }],
108
+ suggested: [{ type: "text", value: "Suggested text" }],
109
+ message: "This is a suggestion",
110
+ method: "database; reusable",
111
+ } satisfies CasSuggestion,
112
+ ],
113
+ ])("validates a %s", (_, input) => {
114
+ const root: Root = { type: "root", children: [input as Content] };
115
+ expect(() => validateCast(root)).not.toThrow();
116
+ });
117
+ });
118
+
119
+ describe("validates incorrect CAST trees successfully", () => {
120
+ it.each([
121
+ ["root not an object", { type: "root", children: null } as unknown as Root],
122
+ ["root type not 'root'", { type: "notRoot", children: [] } as unknown as Root],
123
+ ["root children array has null", { type: "root", children: [null] }],
124
+ ["root children not an array", { type: "root", children: "notAnArray" } as unknown as Root],
125
+ ["invalid Content type", { type: "root", children: [{ type: "invalid" }] } as unknown as Root],
126
+ [
127
+ "invalid paragraph",
128
+ {
129
+ type: "root",
130
+ children: [{ type: "paragraph", children: [{ type: "text", value: null as unknown as string }] }],
131
+ } satisfies Root,
132
+ ],
133
+ [
134
+ "invalid heading",
135
+ {
136
+ type: "root",
137
+ children: [
138
+ {
139
+ type: "heading",
140
+ level: "one" as unknown as 1,
141
+ children: [{ type: "text", value: "Invalid heading level" }],
142
+ },
143
+ ],
144
+ } satisfies Root,
145
+ ],
146
+ [
147
+ "invalid list",
148
+ {
149
+ type: "root",
150
+ children: [
151
+ {
152
+ type: "list",
153
+ ordered: false,
154
+ children: [{ type: "listItem", children: null as unknown as InlineContent[] }],
155
+ },
156
+ ],
157
+ } satisfies Root,
158
+ ],
159
+ [
160
+ "invalid code block",
161
+ {
162
+ type: "root",
163
+ children: [
164
+ { type: "code", lang: 123 as unknown as string, children: [{ type: "text", value: "// my code" }] },
165
+ ],
166
+ } satisfies Root,
167
+ ],
168
+ [
169
+ "invalid interpolated content value",
170
+ { type: "root", children: [{ type: "contentValue", prn: "invalid-prn" }] } satisfies Root,
171
+ ],
172
+ [
173
+ "invalid external link",
174
+ {
175
+ type: "root",
176
+ children: [{ type: "externalLink", url: "invalid-url", children: null as unknown as Text[] }],
177
+ } satisfies Root,
178
+ ],
179
+ [
180
+ "invalid internal link",
181
+ {
182
+ type: "root",
183
+ children: [
184
+ {
185
+ type: "internalLink",
186
+ url: "invalid-url",
187
+ children: null as unknown as Text[],
188
+ prn: "invalid-prn",
189
+ },
190
+ ],
191
+ } satisfies Root,
192
+ ],
193
+ [
194
+ "invalid internal anchor link",
195
+ {
196
+ type: "root",
197
+ children: [
198
+ {
199
+ type: "internalLink",
200
+ url: "invalid-url",
201
+ children: null as unknown as Text[],
202
+ prn: "invalid-prn",
203
+ },
204
+ ],
205
+ } satisfies Root,
206
+ ],
207
+ ["invalid text", { type: "root", children: [{ type: "text", value: null as unknown as string }] } satisfies Root],
208
+ [
209
+ "invalid bold",
210
+ {
211
+ type: "root",
212
+ children: [{ type: "bold", children: [{ type: "text", value: null as unknown as string }] }],
213
+ } satisfies Root,
214
+ ],
215
+ [
216
+ "invalid italic",
217
+ {
218
+ type: "root",
219
+ children: [{ type: "italic", children: [{ type: "text", value: undefined as unknown as string }] }],
220
+ } satisfies Root,
221
+ ],
222
+ [
223
+ "invalid underline",
224
+ {
225
+ type: "root",
226
+ children: [{ type: "underline", children: [{ type: "text", value: null as unknown as string }] }],
227
+ } satisfies Root,
228
+ ],
229
+ [
230
+ "invalid strikethrough",
231
+ {
232
+ type: "root",
233
+ children: [{ type: "strikethrough", children: [{ type: "text", value: null as unknown as string }] }],
234
+ } satisfies Root,
235
+ ],
236
+ [
237
+ "invalid inline code",
238
+ { type: "root", children: [{ type: "inlineCode", value: null as unknown as string }] } satisfies Root,
239
+ ],
240
+ [
241
+ "invalid highlight",
242
+ {
243
+ type: "root",
244
+ children: [{ type: "highlight", children: [{ type: "text", value: null as unknown as string }] }],
245
+ } satisfies Root,
246
+ ],
247
+ [
248
+ "invalid blockquote",
249
+ {
250
+ type: "root",
251
+ children: [{ type: "blockquote", children: [{ type: "text", value: null as unknown as string }] }],
252
+ } satisfies Root,
253
+ ],
254
+ [
255
+ "invalid list item",
256
+ {
257
+ type: "root",
258
+ children: [{ type: "listItem", children: [{ type: "text", value: null as unknown as string }] }],
259
+ } satisfies Root,
260
+ ],
261
+ [
262
+ "paragraph children not an array",
263
+ {
264
+ type: "root",
265
+ children: [{ type: "paragraph", children: "notAnArray" as unknown as InlineContent[] }],
266
+ } satisfies Root,
267
+ ],
268
+ [
269
+ "heading children not an array",
270
+ {
271
+ type: "root",
272
+ children: [{ type: "heading", level: 1, children: "notAnArray" as unknown as InlineContent[] }],
273
+ } satisfies Root,
274
+ ],
275
+ [
276
+ "list children not an array",
277
+ {
278
+ type: "root",
279
+ children: [{ type: "list", ordered: true, children: "notAnArray" as unknown as ListItem[] }],
280
+ } satisfies Root,
281
+ ],
282
+ [
283
+ "bold children not an array",
284
+ {
285
+ type: "root",
286
+ children: [{ type: "bold", children: "notAnArray" as unknown as InlineContent[] }],
287
+ } satisfies Root,
288
+ ],
289
+ [
290
+ "italic children not an array",
291
+ {
292
+ type: "root",
293
+ children: [{ type: "italic", children: "notAnArray" as unknown as InlineContent[] }],
294
+ } satisfies Root,
295
+ ],
296
+ [
297
+ "underline children not an array",
298
+ {
299
+ type: "root",
300
+ children: [{ type: "underline", children: "notAnArray" as unknown as InlineContent[] }],
301
+ } satisfies Root,
302
+ ],
303
+ [
304
+ "strikethrough children not an array",
305
+ {
306
+ type: "root",
307
+ children: [{ type: "strikethrough", children: "notAnArray" as unknown as InlineContent[] }],
308
+ } satisfies Root,
309
+ ],
310
+ [
311
+ "highlight children not an array",
312
+ {
313
+ type: "root",
314
+ children: [{ type: "highlight", children: "notAnArray" as unknown as InlineContent[] }],
315
+ } satisfies Root,
316
+ ],
317
+ [
318
+ "blockquote children not an array",
319
+ {
320
+ type: "root",
321
+ children: [{ type: "blockquote", children: "notAnArray" as unknown as InlineContent[] }],
322
+ } satisfies Root,
323
+ ],
324
+ [
325
+ "listItem children not an array",
326
+ {
327
+ type: "root",
328
+ children: [{ type: "listItem", children: "notAnArray" as unknown as InlineContent[] }],
329
+ } satisfies Root,
330
+ ],
331
+ [
332
+ "invalid cas suggestion - original not an array",
333
+ {
334
+ type: "root",
335
+ children: [
336
+ {
337
+ type: "casSuggestion",
338
+ original: "notAnArray" as unknown as InlineContent[],
339
+ suggested: [{ type: "text", value: "Suggested text" }],
340
+ message: "This is a suggestion",
341
+ method: "database; reusable",
342
+ },
343
+ ],
344
+ } satisfies Root,
345
+ ],
346
+ [
347
+ "invalid cas suggestion - suggested not an array",
348
+ {
349
+ type: "root",
350
+ children: [
351
+ {
352
+ type: "casSuggestion",
353
+ original: [{ type: "text", value: "Original text" }],
354
+ suggested: "notAnArray" as unknown as InlineContent[],
355
+ message: "This is a suggestion",
356
+ method: "database; reusable",
357
+ },
358
+ ],
359
+ } satisfies Root,
360
+ ],
361
+ [
362
+ "invalid cas suggestion - message not a string",
363
+ {
364
+ type: "root",
365
+ children: [
366
+ {
367
+ type: "casSuggestion",
368
+ original: [{ type: "text", value: "Original text" }],
369
+ suggested: [{ type: "text", value: "Suggested text" }],
370
+ message: null as unknown as string,
371
+ method: "database; reusable",
372
+ },
373
+ ],
374
+ } satisfies Root,
375
+ ],
376
+ [
377
+ "invalid cas suggestion - method not a string",
378
+ {
379
+ type: "root",
380
+ children: [
381
+ {
382
+ type: "casSuggestion",
383
+ original: [{ type: "text", value: "Original text" }],
384
+ suggested: [{ type: "text", value: "Suggested text" }],
385
+ message: "This is a suggestion",
386
+ method: 123 as unknown as string,
387
+ },
388
+ ],
389
+ } satisfies Root,
390
+ ],
391
+ ])("throws an error for %s", (_, input) => {
392
+ expect(() => validateCast(input)).toThrow(InvalidCastError);
393
+ });
394
+ });
395
+ });