@asd20/ui-next 2.14.0 → 2.15.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.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.15.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.14.1...ui-next-v2.15.0) (2026-07-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* align TipTap document validation ([3e0d95c](https://github.com/academydistrict20/asd20-ui-next/commit/3e0d95cf040a1b1e1d0b22a6ff6b03d635e0c019))
|
|
9
|
+
* preserve TipTap rendering semantics ([3fcec8e](https://github.com/academydistrict20/asd20-ui-next/commit/3fcec8ea4c2c34418d876ea17e4939c73fe32855))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* add TipTap message body dual reader ([1552256](https://github.com/academydistrict20/asd20-ui-next/commit/1552256630fde93026c9667b9854ccf9cd288914))
|
|
15
|
+
|
|
16
|
+
## [2.14.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.14.0...ui-next-v2.14.1) (2026-07-28)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* align multiselect control height ([63b8e84](https://github.com/academydistrict20/asd20-ui-next/commit/63b8e841f558f2df7dc3063ff8b144bc77415167))
|
|
22
|
+
|
|
3
23
|
# [2.14.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.13.3...ui-next-v2.14.0) (2026-07-28)
|
|
4
24
|
|
|
5
25
|
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<div
|
|
3
3
|
v-if="renderNodes.length"
|
|
4
4
|
class="asd20-rich-body-content"
|
|
5
|
+
:data-message-document-status="renderModel.status"
|
|
5
6
|
@click="onContentClick"
|
|
6
7
|
>
|
|
7
8
|
<template v-for="(node, index) in renderNodes">
|
|
@@ -40,76 +41,10 @@ import Asd20BodyAccordion from '../../molecules/Asd20BodyAccordion'
|
|
|
40
41
|
import Asd20TableauEmbed from '../../molecules/Asd20TableauEmbed'
|
|
41
42
|
import { navigateInternalHref } from '../../../helpers/linkPolicy'
|
|
42
43
|
import { applyRichListColumnExceptions } from '../../../helpers/richBodyListColumns'
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function containsBodyBlockPlaceholder(html) {
|
|
48
|
-
if (!html) return false
|
|
49
|
-
return new RegExp(BODY_BLOCK_PLACEHOLDER_PATTERN).test(html)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function buildRenderNodes(html, blockMap) {
|
|
53
|
-
const nodes = []
|
|
54
|
-
const placeholderPattern = new RegExp(BODY_BLOCK_PLACEHOLDER_PATTERN)
|
|
55
|
-
let lastIndex = 0
|
|
56
|
-
let match
|
|
57
|
-
|
|
58
|
-
while ((match = placeholderPattern.exec(html))) {
|
|
59
|
-
const [fullMatch, , blockType, , blockId] = match
|
|
60
|
-
const htmlBefore = html.slice(lastIndex, match.index)
|
|
61
|
-
if (htmlBefore) {
|
|
62
|
-
nodes.push({
|
|
63
|
-
type: 'html',
|
|
64
|
-
html: htmlBefore,
|
|
65
|
-
})
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const block = blockMap[blockId]
|
|
69
|
-
if (block && block.type === blockType) {
|
|
70
|
-
nodes.push({
|
|
71
|
-
type: blockType,
|
|
72
|
-
block,
|
|
73
|
-
})
|
|
74
|
-
} else {
|
|
75
|
-
nodes.push({
|
|
76
|
-
type: 'html',
|
|
77
|
-
html: fullMatch,
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
lastIndex = match.index + fullMatch.length
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const htmlAfter = html.slice(lastIndex)
|
|
85
|
-
if (htmlAfter) {
|
|
86
|
-
nodes.push({
|
|
87
|
-
type: 'html',
|
|
88
|
-
html: htmlAfter,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return nodes
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function parseBodyBlocks(bodyJson) {
|
|
96
|
-
if (!bodyJson) return []
|
|
97
|
-
|
|
98
|
-
let parsed = bodyJson
|
|
99
|
-
if (typeof bodyJson === 'string') {
|
|
100
|
-
try {
|
|
101
|
-
parsed = JSON.parse(bodyJson)
|
|
102
|
-
} catch (error) {
|
|
103
|
-
return []
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.blocks)) {
|
|
108
|
-
return []
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return parsed.blocks.filter(block => block && typeof block === 'object')
|
|
112
|
-
}
|
|
44
|
+
import {
|
|
45
|
+
buildRichBodyRenderModel,
|
|
46
|
+
containsBodyBlockPlaceholder,
|
|
47
|
+
} from './messageDocumentRenderer'
|
|
113
48
|
|
|
114
49
|
export default {
|
|
115
50
|
name: 'Asd20RichBodyContent',
|
|
@@ -126,22 +61,24 @@ export default {
|
|
|
126
61
|
}),
|
|
127
62
|
|
|
128
63
|
computed: {
|
|
129
|
-
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}, {})
|
|
64
|
+
renderModel() {
|
|
65
|
+
return buildRichBodyRenderModel({
|
|
66
|
+
bodyContent: this.bodyContent,
|
|
67
|
+
bodyJson: this.bodyJson,
|
|
68
|
+
})
|
|
135
69
|
},
|
|
136
70
|
renderNodes() {
|
|
137
71
|
const html = this.bodyContent || ''
|
|
138
|
-
if (!html) return []
|
|
139
72
|
// Keep SSR and the initial client render on the same raw HTML markup,
|
|
140
73
|
// then enhance block placeholders after hydration.
|
|
141
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
this.renderModel.status === 'legacy' &&
|
|
76
|
+
!this.hasMounted &&
|
|
77
|
+
containsBodyBlockPlaceholder(html)
|
|
78
|
+
) {
|
|
142
79
|
return [{ type: 'html', html }]
|
|
143
80
|
}
|
|
144
|
-
return
|
|
81
|
+
return this.renderModel.nodes
|
|
145
82
|
},
|
|
146
83
|
},
|
|
147
84
|
|
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
const MESSAGE_DOCUMENT_SCHEMA_VERSION = 1
|
|
2
|
+
|
|
3
|
+
export const BODY_JSON_FORMAT = Object.freeze({
|
|
4
|
+
EMPTY: 'empty',
|
|
5
|
+
INVALID: 'invalid',
|
|
6
|
+
LEGACY_BLOCKS: 'legacy-blocks',
|
|
7
|
+
TIPTAP: 'tiptap',
|
|
8
|
+
UNKNOWN: 'unknown',
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const MESSAGE_DOCUMENT_STATUS = Object.freeze({
|
|
12
|
+
LEGACY: 'legacy',
|
|
13
|
+
SUPPORTED: 'supported',
|
|
14
|
+
UNSUPPORTED: 'unsupported',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const LEGACY_PLACEHOLDER_PATTERN =
|
|
18
|
+
/<div\b(?=[^>]*\bdata-cc-block=(['"])([^'"]+)\1)(?=[^>]*\bdata-block-id=(['"])([^'"]+)\3)[^>]*><\/div>/gi
|
|
19
|
+
|
|
20
|
+
const INLINE_NODE_TYPES = new Set(['text', 'hardBreak'])
|
|
21
|
+
const STANDARD_BLOCK_NODE_TYPES = new Set([
|
|
22
|
+
'paragraph',
|
|
23
|
+
'heading',
|
|
24
|
+
'blockquote',
|
|
25
|
+
'codeBlock',
|
|
26
|
+
'horizontalRule',
|
|
27
|
+
'bulletList',
|
|
28
|
+
'orderedList',
|
|
29
|
+
'image',
|
|
30
|
+
'videoEmbed',
|
|
31
|
+
'table',
|
|
32
|
+
])
|
|
33
|
+
const TOP_LEVEL_NODE_TYPES = new Set([
|
|
34
|
+
...STANDARD_BLOCK_NODE_TYPES,
|
|
35
|
+
'accordionBlock',
|
|
36
|
+
'tableauBlock',
|
|
37
|
+
])
|
|
38
|
+
const KNOWN_NODE_TYPES = new Set([
|
|
39
|
+
'doc',
|
|
40
|
+
...TOP_LEVEL_NODE_TYPES,
|
|
41
|
+
...INLINE_NODE_TYPES,
|
|
42
|
+
'listItem',
|
|
43
|
+
'tableRow',
|
|
44
|
+
'tableCell',
|
|
45
|
+
'tableHeader',
|
|
46
|
+
'accordionItem',
|
|
47
|
+
'legacyRecovery',
|
|
48
|
+
])
|
|
49
|
+
const MARK_TYPES = new Set([
|
|
50
|
+
'bold',
|
|
51
|
+
'italic',
|
|
52
|
+
'strike',
|
|
53
|
+
'code',
|
|
54
|
+
'link',
|
|
55
|
+
'subscript',
|
|
56
|
+
'superscript',
|
|
57
|
+
])
|
|
58
|
+
const MARK_RENDER_ORDER = Object.freeze({
|
|
59
|
+
link: 0,
|
|
60
|
+
bold: 1,
|
|
61
|
+
code: 2,
|
|
62
|
+
italic: 3,
|
|
63
|
+
strike: 4,
|
|
64
|
+
subscript: 5,
|
|
65
|
+
superscript: 6,
|
|
66
|
+
})
|
|
67
|
+
const NODE_ATTRIBUTE_NAMES = Object.freeze({
|
|
68
|
+
doc: ['schemaVersion'],
|
|
69
|
+
text: [],
|
|
70
|
+
hardBreak: [],
|
|
71
|
+
paragraph: [],
|
|
72
|
+
heading: ['level'],
|
|
73
|
+
blockquote: [],
|
|
74
|
+
codeBlock: ['language'],
|
|
75
|
+
horizontalRule: [],
|
|
76
|
+
bulletList: [],
|
|
77
|
+
orderedList: ['start', 'type'],
|
|
78
|
+
listItem: [],
|
|
79
|
+
image: ['src', 'alt', 'title', 'width', 'height'],
|
|
80
|
+
videoEmbed: ['src', 'title', 'descriptiveText'],
|
|
81
|
+
table: [],
|
|
82
|
+
tableRow: [],
|
|
83
|
+
tableCell: ['colspan', 'rowspan', 'colwidth', 'align'],
|
|
84
|
+
tableHeader: ['colspan', 'rowspan', 'colwidth', 'align'],
|
|
85
|
+
tableauBlock: [
|
|
86
|
+
'blockId',
|
|
87
|
+
'src',
|
|
88
|
+
'height',
|
|
89
|
+
'toolbar',
|
|
90
|
+
'hideTabs',
|
|
91
|
+
'descriptiveText',
|
|
92
|
+
],
|
|
93
|
+
accordionBlock: ['blockId'],
|
|
94
|
+
accordionItem: ['itemId', 'heading', 'opened'],
|
|
95
|
+
legacyRecovery: ['reason', 'blockId', 'blockType', 'rawHtml', 'payloadJson'],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function isRecord(value) {
|
|
99
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseBodyJson(value) {
|
|
103
|
+
if (
|
|
104
|
+
value === null ||
|
|
105
|
+
value === undefined ||
|
|
106
|
+
(typeof value === 'string' && value.trim() === '')
|
|
107
|
+
) {
|
|
108
|
+
return { parsed: null, error: null }
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== 'string') return { parsed: value, error: null }
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
return { parsed: JSON.parse(value), error: null }
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return { parsed: null, error }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function detectBodyJsonFormat(value) {
|
|
120
|
+
const { parsed, error } = parseBodyJson(value)
|
|
121
|
+
|
|
122
|
+
if (error) {
|
|
123
|
+
return {
|
|
124
|
+
format: BODY_JSON_FORMAT.INVALID,
|
|
125
|
+
value: null,
|
|
126
|
+
reason: 'bodyJson is not valid JSON.',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (parsed === null) {
|
|
130
|
+
return { format: BODY_JSON_FORMAT.EMPTY, value: null, reason: '' }
|
|
131
|
+
}
|
|
132
|
+
if (!isRecord(parsed)) {
|
|
133
|
+
return {
|
|
134
|
+
format: BODY_JSON_FORMAT.UNKNOWN,
|
|
135
|
+
value: parsed,
|
|
136
|
+
reason: 'bodyJson must contain an object.',
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (parsed.type === 'doc') {
|
|
140
|
+
return { format: BODY_JSON_FORMAT.TIPTAP, value: parsed, reason: '' }
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(parsed.blocks)) {
|
|
143
|
+
return {
|
|
144
|
+
format: BODY_JSON_FORMAT.LEGACY_BLOCKS,
|
|
145
|
+
value: parsed,
|
|
146
|
+
reason: parsed.version === 1 ? '' : 'Legacy bodyJson must use version 1.',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
format: BODY_JSON_FORMAT.UNKNOWN,
|
|
151
|
+
value: parsed,
|
|
152
|
+
reason: 'bodyJson is neither a TipTap document nor a legacy block sidecar.',
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function issue(code, path, detail = '') {
|
|
157
|
+
return { code, path, detail }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateAttributeNames(node, path) {
|
|
161
|
+
const allowedNames = new Set(NODE_ATTRIBUTE_NAMES[node.type] || [])
|
|
162
|
+
const unknownName = Object.keys(node.attrs || {}).find(
|
|
163
|
+
name => !allowedNames.has(name)
|
|
164
|
+
)
|
|
165
|
+
return unknownName ? issue('unknown-attribute', path, unknownName) : null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateStringAttribute(node, name, path, required = false) {
|
|
169
|
+
const value = node.attrs?.[name]
|
|
170
|
+
if (value === undefined || value === null) {
|
|
171
|
+
return required ? issue('malformed-attribute', path, name) : null
|
|
172
|
+
}
|
|
173
|
+
return typeof value === 'string'
|
|
174
|
+
? null
|
|
175
|
+
: issue('malformed-attribute', path, name)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isSafeResource(value, allowHash = false) {
|
|
179
|
+
if (typeof value !== 'string' || !value.trim()) return false
|
|
180
|
+
if (
|
|
181
|
+
value.startsWith('/') ||
|
|
182
|
+
value.startsWith('?') ||
|
|
183
|
+
value.startsWith('./') ||
|
|
184
|
+
value.startsWith('../') ||
|
|
185
|
+
(allowHash && value.startsWith('#'))
|
|
186
|
+
) {
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const url = new URL(value, 'https://ui-next.invalid')
|
|
192
|
+
return ['http:', 'https:', 'mailto:', 'tel:'].includes(url.protocol)
|
|
193
|
+
} catch (_error) {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateMarks(marks, path) {
|
|
199
|
+
if (marks === undefined) return null
|
|
200
|
+
if (!Array.isArray(marks)) return issue('malformed-marks', path)
|
|
201
|
+
|
|
202
|
+
const seenTypes = new Set()
|
|
203
|
+
for (let index = 0; index < marks.length; index += 1) {
|
|
204
|
+
const mark = marks[index]
|
|
205
|
+
const markPath = `${path}.marks[${index}]`
|
|
206
|
+
if (!isRecord(mark) || !MARK_TYPES.has(mark.type)) {
|
|
207
|
+
return issue('unsupported-mark', markPath, mark?.type || 'unknown')
|
|
208
|
+
}
|
|
209
|
+
if (mark.attrs !== undefined && !isRecord(mark.attrs)) {
|
|
210
|
+
return issue('malformed-mark-attributes', markPath)
|
|
211
|
+
}
|
|
212
|
+
if (seenTypes.has(mark.type)) {
|
|
213
|
+
return issue('duplicate-mark', markPath, mark.type)
|
|
214
|
+
}
|
|
215
|
+
seenTypes.add(mark.type)
|
|
216
|
+
if (mark.type === 'link') {
|
|
217
|
+
const href = mark.attrs?.href
|
|
218
|
+
if (!isSafeResource(href, true)) {
|
|
219
|
+
return issue('unsafe-link', markPath, href || '')
|
|
220
|
+
}
|
|
221
|
+
const unknownName = Object.keys(mark.attrs || {}).find(
|
|
222
|
+
name => !['href', 'target', 'rel', 'class', 'title'].includes(name)
|
|
223
|
+
)
|
|
224
|
+
if (unknownName) {
|
|
225
|
+
return issue('unknown-mark-attribute', markPath, unknownName)
|
|
226
|
+
}
|
|
227
|
+
for (const name of ['target', 'rel', 'class', 'title']) {
|
|
228
|
+
const value = mark.attrs?.[name]
|
|
229
|
+
if (
|
|
230
|
+
value !== undefined &&
|
|
231
|
+
value !== null &&
|
|
232
|
+
typeof value !== 'string'
|
|
233
|
+
) {
|
|
234
|
+
return issue('malformed-mark-attributes', markPath, name)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else if (Object.keys(mark.attrs || {}).length) {
|
|
238
|
+
return issue('unknown-mark-attribute', markPath)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validateChildren(node, path, allowedTypes, required = false) {
|
|
245
|
+
if (node.content === undefined) {
|
|
246
|
+
return required ? issue('malformed-content', path) : null
|
|
247
|
+
}
|
|
248
|
+
if (!Array.isArray(node.content) || (required && !node.content.length)) {
|
|
249
|
+
return issue('malformed-content', path)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (let index = 0; index < node.content.length; index += 1) {
|
|
253
|
+
const child = node.content[index]
|
|
254
|
+
const childPath = `${path}.content[${index}]`
|
|
255
|
+
if (!isRecord(child)) return issue('malformed-node', childPath)
|
|
256
|
+
const childIssue = validateNode(child, childPath)
|
|
257
|
+
if (childIssue) return childIssue
|
|
258
|
+
if (!allowedTypes.has(child.type)) {
|
|
259
|
+
return issue('invalid-child-node', childPath, child.type)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function validateAtom(node, path) {
|
|
266
|
+
if (
|
|
267
|
+
node.content !== undefined &&
|
|
268
|
+
(!Array.isArray(node.content) || node.content.length)
|
|
269
|
+
) {
|
|
270
|
+
return issue('malformed-content', path)
|
|
271
|
+
}
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validateNode(node, path) {
|
|
276
|
+
if (!isRecord(node) || typeof node.type !== 'string') {
|
|
277
|
+
return issue('malformed-node', path)
|
|
278
|
+
}
|
|
279
|
+
if (node.type === 'legacyRecovery') return issue('recovery-node', path)
|
|
280
|
+
if (!KNOWN_NODE_TYPES.has(node.type)) {
|
|
281
|
+
return issue('unknown-node', path, node.type)
|
|
282
|
+
}
|
|
283
|
+
if (node.attrs !== undefined && !isRecord(node.attrs)) {
|
|
284
|
+
return issue('malformed-attributes', path)
|
|
285
|
+
}
|
|
286
|
+
const attributeIssue = validateAttributeNames(node, path)
|
|
287
|
+
if (attributeIssue) return attributeIssue
|
|
288
|
+
|
|
289
|
+
if (node.type === 'doc') {
|
|
290
|
+
if (node.attrs?.schemaVersion !== MESSAGE_DOCUMENT_SCHEMA_VERSION) {
|
|
291
|
+
return issue(
|
|
292
|
+
'unsupported-schema-version',
|
|
293
|
+
path,
|
|
294
|
+
String(node.attrs?.schemaVersion ?? 'missing')
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
return validateChildren(node, path, TOP_LEVEL_NODE_TYPES, true)
|
|
298
|
+
}
|
|
299
|
+
if (node.type === 'text') {
|
|
300
|
+
if (
|
|
301
|
+
typeof node.text !== 'string' ||
|
|
302
|
+
node.text.length === 0 ||
|
|
303
|
+
node.content !== undefined
|
|
304
|
+
) {
|
|
305
|
+
return issue('malformed-text', path)
|
|
306
|
+
}
|
|
307
|
+
return validateMarks(node.marks, path)
|
|
308
|
+
}
|
|
309
|
+
if (node.type === 'hardBreak') {
|
|
310
|
+
return validateMarks(node.marks, path) || validateAtom(node, path)
|
|
311
|
+
}
|
|
312
|
+
if (node.marks !== undefined) return issue('invalid-node-marks', path)
|
|
313
|
+
|
|
314
|
+
if (node.type === 'paragraph') {
|
|
315
|
+
return node.content === undefined
|
|
316
|
+
? null
|
|
317
|
+
: validateChildren(node, path, INLINE_NODE_TYPES)
|
|
318
|
+
}
|
|
319
|
+
if (node.type === 'heading') {
|
|
320
|
+
const level = node.attrs?.level
|
|
321
|
+
if (!Number.isInteger(level) || level < 1 || level > 6) {
|
|
322
|
+
return issue('malformed-attribute', path, 'level')
|
|
323
|
+
}
|
|
324
|
+
return node.content === undefined
|
|
325
|
+
? null
|
|
326
|
+
: validateChildren(node, path, INLINE_NODE_TYPES)
|
|
327
|
+
}
|
|
328
|
+
if (node.type === 'horizontalRule') {
|
|
329
|
+
return validateAtom(node, path)
|
|
330
|
+
}
|
|
331
|
+
if (node.type === 'codeBlock') {
|
|
332
|
+
const languageIssue = validateStringAttribute(node, 'language', path)
|
|
333
|
+
if (languageIssue) return languageIssue
|
|
334
|
+
const contentIssue =
|
|
335
|
+
node.content === undefined
|
|
336
|
+
? null
|
|
337
|
+
: validateChildren(node, path, new Set(['text']))
|
|
338
|
+
if (contentIssue) return contentIssue
|
|
339
|
+
return (node.content || []).some(child => child.marks?.length)
|
|
340
|
+
? issue('invalid-code-block-marks', path)
|
|
341
|
+
: null
|
|
342
|
+
}
|
|
343
|
+
if (node.type === 'blockquote') {
|
|
344
|
+
return validateChildren(node, path, STANDARD_BLOCK_NODE_TYPES, true)
|
|
345
|
+
}
|
|
346
|
+
if (node.type === 'bulletList' || node.type === 'orderedList') {
|
|
347
|
+
if (
|
|
348
|
+
node.type === 'orderedList' &&
|
|
349
|
+
node.attrs?.start !== undefined &&
|
|
350
|
+
(!Number.isInteger(node.attrs.start) || node.attrs.start < 1)
|
|
351
|
+
) {
|
|
352
|
+
return issue('malformed-attribute', path, 'start')
|
|
353
|
+
}
|
|
354
|
+
if (
|
|
355
|
+
node.type === 'orderedList' &&
|
|
356
|
+
node.attrs?.type !== undefined &&
|
|
357
|
+
node.attrs.type !== null &&
|
|
358
|
+
!['1', 'a', 'A', 'i', 'I'].includes(node.attrs.type)
|
|
359
|
+
) {
|
|
360
|
+
return issue('malformed-attribute', path, 'type')
|
|
361
|
+
}
|
|
362
|
+
return validateChildren(node, path, new Set(['listItem']), true)
|
|
363
|
+
}
|
|
364
|
+
if (node.type === 'listItem') {
|
|
365
|
+
const contentIssue = validateChildren(
|
|
366
|
+
node,
|
|
367
|
+
path,
|
|
368
|
+
STANDARD_BLOCK_NODE_TYPES,
|
|
369
|
+
true
|
|
370
|
+
)
|
|
371
|
+
if (contentIssue) return contentIssue
|
|
372
|
+
return node.content[0].type === 'paragraph'
|
|
373
|
+
? null
|
|
374
|
+
: issue('invalid-child-node', `${path}.content[0]`, node.content[0].type)
|
|
375
|
+
}
|
|
376
|
+
if (node.type === 'image') {
|
|
377
|
+
const srcIssue = validateStringAttribute(node, 'src', path)
|
|
378
|
+
if (srcIssue || (node.attrs?.src && !isSafeResource(node.attrs.src))) {
|
|
379
|
+
return srcIssue || issue('unsafe-resource', path, 'src')
|
|
380
|
+
}
|
|
381
|
+
for (const name of ['alt', 'title']) {
|
|
382
|
+
const attrIssue = validateStringAttribute(node, name, path)
|
|
383
|
+
if (attrIssue) return attrIssue
|
|
384
|
+
}
|
|
385
|
+
for (const name of ['width', 'height']) {
|
|
386
|
+
const value = node.attrs?.[name]
|
|
387
|
+
if (
|
|
388
|
+
value !== undefined &&
|
|
389
|
+
value !== null &&
|
|
390
|
+
(!Number.isFinite(value) || value <= 0)
|
|
391
|
+
) {
|
|
392
|
+
return issue('malformed-attribute', path, name)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return validateAtom(node, path)
|
|
396
|
+
}
|
|
397
|
+
if (node.type === 'videoEmbed') {
|
|
398
|
+
const srcIssue = validateStringAttribute(node, 'src', path)
|
|
399
|
+
if (srcIssue || (node.attrs?.src && !isSafeResource(node.attrs.src))) {
|
|
400
|
+
return srcIssue || issue('unsafe-resource', path, 'src')
|
|
401
|
+
}
|
|
402
|
+
for (const name of ['title', 'descriptiveText']) {
|
|
403
|
+
const attrIssue = validateStringAttribute(node, name, path)
|
|
404
|
+
if (attrIssue) return attrIssue
|
|
405
|
+
}
|
|
406
|
+
return validateAtom(node, path)
|
|
407
|
+
}
|
|
408
|
+
if (node.type === 'table') {
|
|
409
|
+
return validateChildren(node, path, new Set(['tableRow']), true)
|
|
410
|
+
}
|
|
411
|
+
if (node.type === 'tableRow') {
|
|
412
|
+
return validateChildren(
|
|
413
|
+
node,
|
|
414
|
+
path,
|
|
415
|
+
new Set(['tableCell', 'tableHeader']),
|
|
416
|
+
true
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
if (node.type === 'tableCell' || node.type === 'tableHeader') {
|
|
420
|
+
const colspan = node.attrs?.colspan ?? 1
|
|
421
|
+
const rowspan = node.attrs?.rowspan ?? 1
|
|
422
|
+
const colwidth = node.attrs?.colwidth
|
|
423
|
+
const align = node.attrs?.align
|
|
424
|
+
if (!Number.isInteger(colspan) || colspan < 1) {
|
|
425
|
+
return issue('malformed-attribute', path, 'colspan')
|
|
426
|
+
}
|
|
427
|
+
if (!Number.isInteger(rowspan) || rowspan < 1) {
|
|
428
|
+
return issue('malformed-attribute', path, 'rowspan')
|
|
429
|
+
}
|
|
430
|
+
if (
|
|
431
|
+
colwidth !== undefined &&
|
|
432
|
+
colwidth !== null &&
|
|
433
|
+
(!Array.isArray(colwidth) ||
|
|
434
|
+
colwidth.length !== colspan ||
|
|
435
|
+
colwidth.some(width => !Number.isFinite(width) || width <= 0))
|
|
436
|
+
) {
|
|
437
|
+
return issue('malformed-attribute', path, 'colwidth')
|
|
438
|
+
}
|
|
439
|
+
if (
|
|
440
|
+
align !== undefined &&
|
|
441
|
+
align !== null &&
|
|
442
|
+
!['left', 'center', 'right'].includes(align)
|
|
443
|
+
) {
|
|
444
|
+
return issue('malformed-attribute', path, 'align')
|
|
445
|
+
}
|
|
446
|
+
return validateChildren(node, path, STANDARD_BLOCK_NODE_TYPES, true)
|
|
447
|
+
}
|
|
448
|
+
if (node.type === 'tableauBlock') {
|
|
449
|
+
for (const name of ['blockId', 'src', 'toolbar', 'descriptiveText']) {
|
|
450
|
+
const attrIssue = validateStringAttribute(node, name, path, false)
|
|
451
|
+
if (attrIssue) return attrIssue
|
|
452
|
+
}
|
|
453
|
+
if (node.attrs?.src && !isSafeResource(node.attrs.src)) {
|
|
454
|
+
return issue('unsafe-resource', path, 'src')
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
node.attrs?.height !== undefined &&
|
|
458
|
+
(!Number.isFinite(Number(node.attrs.height)) ||
|
|
459
|
+
Number(node.attrs.height) <= 0)
|
|
460
|
+
) {
|
|
461
|
+
return issue('malformed-attribute', path, 'height')
|
|
462
|
+
}
|
|
463
|
+
if (
|
|
464
|
+
node.attrs?.hideTabs !== undefined &&
|
|
465
|
+
typeof node.attrs.hideTabs !== 'boolean'
|
|
466
|
+
) {
|
|
467
|
+
return issue('malformed-attribute', path, 'hideTabs')
|
|
468
|
+
}
|
|
469
|
+
return validateAtom(node, path)
|
|
470
|
+
}
|
|
471
|
+
if (node.type === 'accordionBlock') {
|
|
472
|
+
const blockIdIssue = validateStringAttribute(node, 'blockId', path)
|
|
473
|
+
return (
|
|
474
|
+
blockIdIssue ||
|
|
475
|
+
validateChildren(node, path, new Set(['accordionItem']), true)
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
if (node.type === 'accordionItem') {
|
|
479
|
+
for (const name of ['itemId', 'heading']) {
|
|
480
|
+
const attrIssue = validateStringAttribute(node, name, path)
|
|
481
|
+
if (attrIssue) return attrIssue
|
|
482
|
+
}
|
|
483
|
+
if (
|
|
484
|
+
node.attrs?.opened !== undefined &&
|
|
485
|
+
typeof node.attrs.opened !== 'boolean'
|
|
486
|
+
) {
|
|
487
|
+
return issue('malformed-attribute', path, 'opened')
|
|
488
|
+
}
|
|
489
|
+
if (!Array.isArray(node.content) || !node.content.length) {
|
|
490
|
+
return issue('malformed-content', path)
|
|
491
|
+
}
|
|
492
|
+
const tableauChildren = node.content.filter(
|
|
493
|
+
child => child?.type === 'tableauBlock'
|
|
494
|
+
)
|
|
495
|
+
if (
|
|
496
|
+
tableauChildren.length &&
|
|
497
|
+
(tableauChildren.length !== 1 || node.content.length !== 1)
|
|
498
|
+
) {
|
|
499
|
+
return issue('unsupported-accordion-content', path)
|
|
500
|
+
}
|
|
501
|
+
return validateChildren(
|
|
502
|
+
node,
|
|
503
|
+
path,
|
|
504
|
+
tableauChildren.length
|
|
505
|
+
? new Set(['tableauBlock'])
|
|
506
|
+
: STANDARD_BLOCK_NODE_TYPES,
|
|
507
|
+
true
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
return issue('unknown-node', path, node.type)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function escapeHtml(value = '') {
|
|
514
|
+
return String(value)
|
|
515
|
+
.replace(/&/g, '&')
|
|
516
|
+
.replace(/</g, '<')
|
|
517
|
+
.replace(/>/g, '>')
|
|
518
|
+
.replace(/"/g, '"')
|
|
519
|
+
.replace(/'/g, ''')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function attribute(name, value, preserveEmpty = false) {
|
|
523
|
+
return value === undefined ||
|
|
524
|
+
value === null ||
|
|
525
|
+
(!preserveEmpty && value === '')
|
|
526
|
+
? ''
|
|
527
|
+
: ` ${name}="${escapeHtml(value)}"`
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function renderMarkedText(node) {
|
|
531
|
+
let html = escapeHtml(node.text)
|
|
532
|
+
const marks = [...(node.marks || [])].sort(
|
|
533
|
+
(left, right) =>
|
|
534
|
+
MARK_RENDER_ORDER[left.type] - MARK_RENDER_ORDER[right.type]
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
for (const mark of marks.reverse()) {
|
|
538
|
+
if (mark.type === 'bold') html = `<strong>${html}</strong>`
|
|
539
|
+
else if (mark.type === 'italic') html = `<em>${html}</em>`
|
|
540
|
+
else if (mark.type === 'strike') html = `<s>${html}</s>`
|
|
541
|
+
else if (mark.type === 'code') html = `<code>${html}</code>`
|
|
542
|
+
else if (mark.type === 'subscript') html = `<sub>${html}</sub>`
|
|
543
|
+
else if (mark.type === 'superscript') html = `<sup>${html}</sup>`
|
|
544
|
+
else if (mark.type === 'link') {
|
|
545
|
+
html = `<a${attribute('href', mark.attrs.href)}${attribute(
|
|
546
|
+
'target',
|
|
547
|
+
mark.attrs.target
|
|
548
|
+
)}${attribute('rel', mark.attrs.rel)}${attribute(
|
|
549
|
+
'class',
|
|
550
|
+
mark.attrs.class
|
|
551
|
+
)}${attribute('title', mark.attrs.title)}>${html}</a>`
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return html
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function renderChildren(node) {
|
|
558
|
+
return (node.content || []).map(renderStandardNode).join('')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function renderTableCell(node, tagName) {
|
|
562
|
+
const colspan = node.attrs?.colspan ?? 1
|
|
563
|
+
const rowspan = node.attrs?.rowspan ?? 1
|
|
564
|
+
const colwidth = node.attrs?.colwidth
|
|
565
|
+
const align = node.attrs?.align
|
|
566
|
+
return `<${tagName}${attribute('colspan', colspan)}${attribute(
|
|
567
|
+
'rowspan',
|
|
568
|
+
rowspan
|
|
569
|
+
)}${
|
|
570
|
+
Array.isArray(colwidth) ? attribute('colwidth', colwidth.join(',')) : ''
|
|
571
|
+
}${align ? attribute('style', `text-align: ${align}`) : ''}>${renderChildren(
|
|
572
|
+
node
|
|
573
|
+
)}</${tagName}>`
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function renderTable(node) {
|
|
577
|
+
const minimumWidth = 25
|
|
578
|
+
const firstRow = node.content[0]
|
|
579
|
+
const columns = firstRow.content.flatMap(cell => {
|
|
580
|
+
const colspan = cell.attrs?.colspan ?? 1
|
|
581
|
+
const widths = cell.attrs?.colwidth
|
|
582
|
+
return Array.from({ length: colspan }, (_value, index) => widths?.[index])
|
|
583
|
+
})
|
|
584
|
+
const fixedWidth = columns.every(Boolean)
|
|
585
|
+
const totalWidth = columns.reduce(
|
|
586
|
+
(sum, width) => sum + (width || minimumWidth),
|
|
587
|
+
0
|
|
588
|
+
)
|
|
589
|
+
const tableStyle = fixedWidth
|
|
590
|
+
? `width: ${totalWidth}px`
|
|
591
|
+
: `min-width: ${totalWidth}px`
|
|
592
|
+
const colgroup = columns
|
|
593
|
+
.map(width =>
|
|
594
|
+
width
|
|
595
|
+
? `<col style="width: ${width}px;">`
|
|
596
|
+
: `<col style="min-width: ${minimumWidth}px;">`
|
|
597
|
+
)
|
|
598
|
+
.join('')
|
|
599
|
+
return `<table${attribute(
|
|
600
|
+
'style',
|
|
601
|
+
tableStyle
|
|
602
|
+
)}><colgroup>${colgroup}</colgroup><tbody>${renderChildren(
|
|
603
|
+
node
|
|
604
|
+
)}</tbody></table>`
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function renderStandardNode(node) {
|
|
608
|
+
if (node.type === 'text') return renderMarkedText(node)
|
|
609
|
+
if (node.type === 'hardBreak') return '<br>'
|
|
610
|
+
if (node.type === 'paragraph') return `<p>${renderChildren(node)}</p>`
|
|
611
|
+
if (node.type === 'heading') {
|
|
612
|
+
return `<h${node.attrs.level}>${renderChildren(node)}</h${
|
|
613
|
+
node.attrs.level
|
|
614
|
+
}>`
|
|
615
|
+
}
|
|
616
|
+
if (node.type === 'blockquote') {
|
|
617
|
+
return `<blockquote>${renderChildren(node)}</blockquote>`
|
|
618
|
+
}
|
|
619
|
+
if (node.type === 'codeBlock') {
|
|
620
|
+
const languageClass = node.attrs?.language
|
|
621
|
+
? attribute('class', `language-${node.attrs.language}`)
|
|
622
|
+
: ''
|
|
623
|
+
return `<pre><code${languageClass}>${renderChildren(node)}</code></pre>`
|
|
624
|
+
}
|
|
625
|
+
if (node.type === 'horizontalRule') return '<hr>'
|
|
626
|
+
if (node.type === 'bulletList') return `<ul>${renderChildren(node)}</ul>`
|
|
627
|
+
if (node.type === 'orderedList') {
|
|
628
|
+
const start = node.attrs?.start
|
|
629
|
+
const type = node.attrs?.type
|
|
630
|
+
return `<ol${start && start !== 1 ? attribute('start', start) : ''}${
|
|
631
|
+
type && type !== '1' ? attribute('type', type) : ''
|
|
632
|
+
}>${renderChildren(node)}</ol>`
|
|
633
|
+
}
|
|
634
|
+
if (node.type === 'listItem') return `<li>${renderChildren(node)}</li>`
|
|
635
|
+
if (node.type === 'image') {
|
|
636
|
+
return `<img${attribute('src', node.attrs.src)}${attribute(
|
|
637
|
+
'alt',
|
|
638
|
+
node.attrs.alt,
|
|
639
|
+
true
|
|
640
|
+
)}${attribute('title', node.attrs.title)}${attribute(
|
|
641
|
+
'width',
|
|
642
|
+
node.attrs.width
|
|
643
|
+
)}${attribute('height', node.attrs.height)}>`
|
|
644
|
+
}
|
|
645
|
+
if (node.type === 'videoEmbed') {
|
|
646
|
+
return `<div data-cc-node="video"${attribute(
|
|
647
|
+
'data-src',
|
|
648
|
+
node.attrs.src
|
|
649
|
+
)}${attribute('data-title', node.attrs.title)}${attribute(
|
|
650
|
+
'data-descriptive-text',
|
|
651
|
+
node.attrs.descriptiveText
|
|
652
|
+
)}><iframe${attribute('src', node.attrs.src)}${attribute(
|
|
653
|
+
'title',
|
|
654
|
+
node.attrs.title || 'Embedded video'
|
|
655
|
+
)}></iframe></div>`
|
|
656
|
+
}
|
|
657
|
+
if (node.type === 'table') return renderTable(node)
|
|
658
|
+
if (node.type === 'tableRow') return `<tr>${renderChildren(node)}</tr>`
|
|
659
|
+
if (node.type === 'tableCell') return renderTableCell(node, 'td')
|
|
660
|
+
if (node.type === 'tableHeader') return renderTableCell(node, 'th')
|
|
661
|
+
return ''
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function buildLegacyRenderNodes(bodyHtml, blocks) {
|
|
665
|
+
const blockMap = new Map(
|
|
666
|
+
blocks.filter(isRecord).map(block => [block.id, block])
|
|
667
|
+
)
|
|
668
|
+
const nodes = []
|
|
669
|
+
const pattern = new RegExp(LEGACY_PLACEHOLDER_PATTERN)
|
|
670
|
+
let lastIndex = 0
|
|
671
|
+
let match
|
|
672
|
+
|
|
673
|
+
while ((match = pattern.exec(bodyHtml))) {
|
|
674
|
+
if (match.index > lastIndex) {
|
|
675
|
+
nodes.push({ type: 'html', html: bodyHtml.slice(lastIndex, match.index) })
|
|
676
|
+
}
|
|
677
|
+
const block = blockMap.get(match[4])
|
|
678
|
+
nodes.push(
|
|
679
|
+
block && block.type === match[2]
|
|
680
|
+
? { type: block.type, block }
|
|
681
|
+
: { type: 'html', html: match[0] }
|
|
682
|
+
)
|
|
683
|
+
lastIndex = match.index + match[0].length
|
|
684
|
+
}
|
|
685
|
+
if (lastIndex < bodyHtml.length) {
|
|
686
|
+
nodes.push({ type: 'html', html: bodyHtml.slice(lastIndex) })
|
|
687
|
+
}
|
|
688
|
+
return nodes
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function accordionToRenderBlock(node) {
|
|
692
|
+
return {
|
|
693
|
+
id: node.attrs?.blockId || '',
|
|
694
|
+
type: 'accordion',
|
|
695
|
+
items: node.content.map(item => {
|
|
696
|
+
const isTableau = item.content[0]?.type === 'tableauBlock'
|
|
697
|
+
return isTableau
|
|
698
|
+
? {
|
|
699
|
+
id: item.attrs?.itemId || '',
|
|
700
|
+
heading: item.attrs?.heading || '',
|
|
701
|
+
opened: Boolean(item.attrs?.opened),
|
|
702
|
+
contentType: 'tableau',
|
|
703
|
+
tableau: { ...item.content[0].attrs },
|
|
704
|
+
}
|
|
705
|
+
: {
|
|
706
|
+
id: item.attrs?.itemId || '',
|
|
707
|
+
heading: item.attrs?.heading || '',
|
|
708
|
+
opened: Boolean(item.attrs?.opened),
|
|
709
|
+
contentType: 'text',
|
|
710
|
+
bodyHtml: item.content.map(renderStandardNode).join(''),
|
|
711
|
+
}
|
|
712
|
+
}),
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function buildTiptapRenderNodes(messageDocument) {
|
|
717
|
+
const nodes = []
|
|
718
|
+
let standardContent = []
|
|
719
|
+
const flushStandardContent = () => {
|
|
720
|
+
if (!standardContent.length) return
|
|
721
|
+
nodes.push({
|
|
722
|
+
type: 'html',
|
|
723
|
+
html: standardContent.map(renderStandardNode).join(''),
|
|
724
|
+
})
|
|
725
|
+
standardContent = []
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
messageDocument.content.forEach(node => {
|
|
729
|
+
if (node.type === 'accordionBlock') {
|
|
730
|
+
flushStandardContent()
|
|
731
|
+
nodes.push({ type: 'accordion', block: accordionToRenderBlock(node) })
|
|
732
|
+
} else if (node.type === 'tableauBlock') {
|
|
733
|
+
flushStandardContent()
|
|
734
|
+
nodes.push({
|
|
735
|
+
type: 'tableau',
|
|
736
|
+
block: {
|
|
737
|
+
id: node.attrs?.blockId || '',
|
|
738
|
+
type: 'tableau',
|
|
739
|
+
...node.attrs,
|
|
740
|
+
},
|
|
741
|
+
})
|
|
742
|
+
} else {
|
|
743
|
+
standardContent.push(node)
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
flushStandardContent()
|
|
747
|
+
return nodes
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function unsupportedModel(detected, bodyContent, validationIssue) {
|
|
751
|
+
return {
|
|
752
|
+
format: detected.format,
|
|
753
|
+
status: MESSAGE_DOCUMENT_STATUS.UNSUPPORTED,
|
|
754
|
+
reason:
|
|
755
|
+
validationIssue ||
|
|
756
|
+
issue('unsupported-body-json', 'bodyJson', detected.reason),
|
|
757
|
+
blocks: [],
|
|
758
|
+
nodes: bodyContent ? [{ type: 'html', html: bodyContent }] : [],
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function containsBodyBlockPlaceholder(html) {
|
|
763
|
+
if (!html) return false
|
|
764
|
+
return new RegExp(LEGACY_PLACEHOLDER_PATTERN).test(html)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export function buildRichBodyRenderModel({
|
|
768
|
+
bodyContent = '',
|
|
769
|
+
bodyJson = '',
|
|
770
|
+
} = {}) {
|
|
771
|
+
const detected = detectBodyJsonFormat(bodyJson)
|
|
772
|
+
|
|
773
|
+
if (detected.format === BODY_JSON_FORMAT.EMPTY) {
|
|
774
|
+
return {
|
|
775
|
+
format: BODY_JSON_FORMAT.EMPTY,
|
|
776
|
+
status: MESSAGE_DOCUMENT_STATUS.LEGACY,
|
|
777
|
+
reason: null,
|
|
778
|
+
blocks: [],
|
|
779
|
+
nodes: buildLegacyRenderNodes(bodyContent, []),
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (detected.format === BODY_JSON_FORMAT.LEGACY_BLOCKS) {
|
|
783
|
+
if (detected.reason) return unsupportedModel(detected, bodyContent)
|
|
784
|
+
const blocks = detected.value.blocks.filter(isRecord)
|
|
785
|
+
return {
|
|
786
|
+
format: BODY_JSON_FORMAT.LEGACY_BLOCKS,
|
|
787
|
+
status: MESSAGE_DOCUMENT_STATUS.LEGACY,
|
|
788
|
+
reason: null,
|
|
789
|
+
blocks,
|
|
790
|
+
nodes: buildLegacyRenderNodes(bodyContent, blocks),
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (detected.format !== BODY_JSON_FORMAT.TIPTAP) {
|
|
794
|
+
return unsupportedModel(detected, bodyContent)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const validationIssue = validateNode(detected.value, 'bodyJson')
|
|
798
|
+
if (validationIssue) {
|
|
799
|
+
return unsupportedModel(detected, bodyContent, validationIssue)
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
format: BODY_JSON_FORMAT.TIPTAP,
|
|
803
|
+
status: MESSAGE_DOCUMENT_STATUS.SUPPORTED,
|
|
804
|
+
reason: null,
|
|
805
|
+
blocks: [],
|
|
806
|
+
nodes: buildTiptapRenderNodes(detected.value),
|
|
807
|
+
}
|
|
808
|
+
}
|