@portabletext/block-tools 3.2.1 → 3.3.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/lib/index.cjs +140 -22
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +48 -2
- package/lib/index.d.ts +48 -2
- package/lib/index.js +139 -21
- package/lib/index.js.map +1 -1
- package/package.json +4 -4
- package/src/HtmlDeserializer/flatten-nested-blocks.test.ts +140 -0
- package/src/HtmlDeserializer/helpers.ts +113 -7
- package/src/HtmlDeserializer/index.ts +9 -2
- package/src/HtmlDeserializer/rules/gdocs.ts +9 -1
- package/src/HtmlDeserializer/rules/html.ts +76 -1
- package/src/HtmlDeserializer/rules/index.ts +2 -1
- package/src/index.ts +2 -2
- package/src/schema-matchers.ts +41 -0
- package/src/types.ts +6 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {compileSchema, defineSchema} from '@portabletext/schema'
|
|
2
|
+
import {describe, expect, test} from 'vitest'
|
|
3
|
+
import {createTestKeyGenerator} from '../../test/test-key-generator'
|
|
4
|
+
import {flattenNestedBlocks} from './helpers'
|
|
5
|
+
|
|
6
|
+
describe(flattenNestedBlocks.name, () => {
|
|
7
|
+
test('splitting text block', () => {
|
|
8
|
+
const keyGenerator = createTestKeyGenerator('k')
|
|
9
|
+
const schema = compileSchema(
|
|
10
|
+
defineSchema({
|
|
11
|
+
styles: [{name: 'h1'}],
|
|
12
|
+
annotations: [{name: 'link'}],
|
|
13
|
+
blockObjects: [{name: 'image'}],
|
|
14
|
+
}),
|
|
15
|
+
)
|
|
16
|
+
const blockKey = keyGenerator()
|
|
17
|
+
const fooKey = keyGenerator()
|
|
18
|
+
const imageKey = keyGenerator()
|
|
19
|
+
const barKey = keyGenerator()
|
|
20
|
+
const linkKey = keyGenerator()
|
|
21
|
+
|
|
22
|
+
expect(
|
|
23
|
+
flattenNestedBlocks({schema, keyGenerator}, [
|
|
24
|
+
{
|
|
25
|
+
_key: blockKey,
|
|
26
|
+
_type: 'block',
|
|
27
|
+
children: [
|
|
28
|
+
{
|
|
29
|
+
_key: fooKey,
|
|
30
|
+
_type: 'span',
|
|
31
|
+
text: 'foo',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
_key: imageKey,
|
|
35
|
+
_type: 'image',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
_key: barKey,
|
|
39
|
+
_type: 'span',
|
|
40
|
+
text: 'bar',
|
|
41
|
+
marks: [linkKey],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
style: 'h1',
|
|
45
|
+
markDefs: [
|
|
46
|
+
{
|
|
47
|
+
_key: linkKey,
|
|
48
|
+
_type: 'link',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
]),
|
|
53
|
+
).toEqual([
|
|
54
|
+
{
|
|
55
|
+
_key: blockKey,
|
|
56
|
+
_type: 'block',
|
|
57
|
+
children: [{_key: fooKey, _type: 'span', text: 'foo'}],
|
|
58
|
+
style: 'h1',
|
|
59
|
+
markDefs: [{_key: linkKey, _type: 'link'}],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
_key: imageKey,
|
|
63
|
+
_type: 'image',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
_key: blockKey,
|
|
67
|
+
_type: 'block',
|
|
68
|
+
children: [
|
|
69
|
+
{_key: barKey, _type: 'span', text: 'bar', marks: [linkKey]},
|
|
70
|
+
],
|
|
71
|
+
style: 'h1',
|
|
72
|
+
markDefs: [{_key: linkKey, _type: 'link'}],
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('splitting text block with __block', () => {
|
|
78
|
+
const keyGenerator = createTestKeyGenerator('k')
|
|
79
|
+
const schema = compileSchema(
|
|
80
|
+
defineSchema({
|
|
81
|
+
blockObjects: [{name: 'image'}],
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
const blockKey = keyGenerator()
|
|
85
|
+
const fooKey = keyGenerator()
|
|
86
|
+
const imageKey = keyGenerator()
|
|
87
|
+
const barKey = keyGenerator()
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
flattenNestedBlocks({schema, keyGenerator}, [
|
|
91
|
+
{
|
|
92
|
+
_key: blockKey,
|
|
93
|
+
_type: 'block',
|
|
94
|
+
children: [
|
|
95
|
+
{
|
|
96
|
+
_key: fooKey,
|
|
97
|
+
_type: 'span',
|
|
98
|
+
text: 'foo',
|
|
99
|
+
marks: [],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
_type: '__block',
|
|
103
|
+
block: {
|
|
104
|
+
_key: imageKey,
|
|
105
|
+
_type: 'image',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
_key: barKey,
|
|
110
|
+
_type: 'span',
|
|
111
|
+
text: 'bar',
|
|
112
|
+
marks: [],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
style: 'normal',
|
|
116
|
+
markDefs: [],
|
|
117
|
+
},
|
|
118
|
+
]),
|
|
119
|
+
).toEqual([
|
|
120
|
+
{
|
|
121
|
+
_key: blockKey,
|
|
122
|
+
_type: 'block',
|
|
123
|
+
children: [{_key: fooKey, _type: 'span', text: 'foo', marks: []}],
|
|
124
|
+
style: 'normal',
|
|
125
|
+
markDefs: [],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
_key: imageKey,
|
|
129
|
+
_type: 'image',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
_key: blockKey,
|
|
133
|
+
_type: 'block',
|
|
134
|
+
children: [{_key: barKey, _type: 'span', text: 'bar', marks: []}],
|
|
135
|
+
style: 'normal',
|
|
136
|
+
markDefs: [],
|
|
137
|
+
},
|
|
138
|
+
])
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -3,6 +3,7 @@ import {vercelStegaClean} from '@vercel/stega'
|
|
|
3
3
|
import {isEqual} from 'lodash'
|
|
4
4
|
import {DEFAULT_BLOCK} from '../constants'
|
|
5
5
|
import type {
|
|
6
|
+
ArbitraryTypedObject,
|
|
6
7
|
HtmlParser,
|
|
7
8
|
HtmlPreprocessorOptions,
|
|
8
9
|
MinimalBlock,
|
|
@@ -12,8 +13,10 @@ import type {
|
|
|
12
13
|
TypedObject,
|
|
13
14
|
} from '../types'
|
|
14
15
|
import {
|
|
16
|
+
isSpan,
|
|
15
17
|
isTextBlock,
|
|
16
18
|
type PortableTextObject,
|
|
19
|
+
type PortableTextSpan,
|
|
17
20
|
type PortableTextTextBlock,
|
|
18
21
|
} from '../types.portable-text'
|
|
19
22
|
import {resolveJsType} from '../util/resolveJsType'
|
|
@@ -70,18 +73,121 @@ export function defaultParseHtml(): HtmlParser {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
export function flattenNestedBlocks(
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
context: {
|
|
77
|
+
schema: Schema
|
|
78
|
+
keyGenerator: () => string
|
|
79
|
+
},
|
|
80
|
+
blocks: Array<ArbitraryTypedObject>,
|
|
75
81
|
): TypedObject[] {
|
|
76
82
|
let depth = 0
|
|
77
83
|
const flattened: TypedObject[] = []
|
|
84
|
+
|
|
78
85
|
const traverse = (nodes: TypedObject[]) => {
|
|
79
86
|
const toRemove: TypedObject[] = []
|
|
80
87
|
nodes.forEach((node) => {
|
|
81
88
|
if (depth === 0) {
|
|
82
|
-
|
|
89
|
+
//Only apply splitting logic if we have block objects defined in the schema
|
|
90
|
+
if (
|
|
91
|
+
context.schema.blockObjects.length > 0 &&
|
|
92
|
+
isTextBlock(context.schema, node)
|
|
93
|
+
) {
|
|
94
|
+
const hasBlockObjects = node.children.some((child) => {
|
|
95
|
+
const knownBlockObject = context.schema.blockObjects.some(
|
|
96
|
+
(blockObject) => blockObject.name === child._type,
|
|
97
|
+
)
|
|
98
|
+
return knownBlockObject
|
|
99
|
+
})
|
|
100
|
+
const hasBlocks = node.children.some(
|
|
101
|
+
(child) => child._type === '__block',
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (hasBlockObjects || hasBlocks) {
|
|
105
|
+
// Split the block when it contains block objects
|
|
106
|
+
const splitChildren = node.children.reduce(
|
|
107
|
+
(slices, child) => {
|
|
108
|
+
const knownInlineObject = context.schema.inlineObjects.some(
|
|
109
|
+
(inlineObject) => inlineObject.name === child._type,
|
|
110
|
+
)
|
|
111
|
+
const knownBlockObject = context.schema.blockObjects.some(
|
|
112
|
+
(blockObject) => blockObject.name === child._type,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const lastSlice = slices.pop()
|
|
116
|
+
|
|
117
|
+
if (!isSpan(context.schema, child) && !knownInlineObject) {
|
|
118
|
+
if (knownBlockObject) {
|
|
119
|
+
return [
|
|
120
|
+
...slices,
|
|
121
|
+
...(lastSlice ? [lastSlice] : []),
|
|
122
|
+
{type: 'block object' as const, block: child},
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (child._type === '__block') {
|
|
128
|
+
return [
|
|
129
|
+
...slices,
|
|
130
|
+
...(lastSlice ? [lastSlice] : []),
|
|
131
|
+
{
|
|
132
|
+
type: 'block object' as const,
|
|
133
|
+
block: (child as any).block,
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (lastSlice) {
|
|
139
|
+
if (lastSlice.type === 'children') {
|
|
140
|
+
return [
|
|
141
|
+
...slices,
|
|
142
|
+
{
|
|
143
|
+
type: 'children' as const,
|
|
144
|
+
children: [...lastSlice.children, child],
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
...slices,
|
|
152
|
+
...(lastSlice ? [lastSlice] : []),
|
|
153
|
+
{type: 'children' as const, children: [child]},
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
[] as Array<
|
|
157
|
+
| {
|
|
158
|
+
type: 'children'
|
|
159
|
+
children: Array<PortableTextSpan | PortableTextObject>
|
|
160
|
+
}
|
|
161
|
+
| {type: 'block object'; block: PortableTextObject}
|
|
162
|
+
>,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// Process each slice
|
|
166
|
+
splitChildren.forEach((slice) => {
|
|
167
|
+
if (slice.type === 'block object') {
|
|
168
|
+
// Add the block object directly
|
|
169
|
+
flattened.push(slice.block)
|
|
170
|
+
} else if (slice.children.length > 0) {
|
|
171
|
+
// Create a new text block with the remaining children
|
|
172
|
+
const newBlock = {
|
|
173
|
+
...node,
|
|
174
|
+
children: slice.children,
|
|
175
|
+
}
|
|
176
|
+
flattened.push(newBlock)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
return
|
|
180
|
+
} else {
|
|
181
|
+
// No block objects, add the block as is
|
|
182
|
+
flattened.push(node)
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
//Not a text block or no block objects in schema, add directly
|
|
186
|
+
flattened.push(node)
|
|
187
|
+
}
|
|
83
188
|
}
|
|
84
|
-
|
|
189
|
+
|
|
190
|
+
if (isTextBlock(context.schema, node)) {
|
|
85
191
|
if (depth > 0) {
|
|
86
192
|
toRemove.push(node)
|
|
87
193
|
flattened.push(node)
|
|
@@ -187,8 +293,8 @@ export function trimWhitespace(
|
|
|
187
293
|
|
|
188
294
|
export function ensureRootIsBlocks(
|
|
189
295
|
schema: Schema,
|
|
190
|
-
blocks:
|
|
191
|
-
):
|
|
296
|
+
blocks: Array<ArbitraryTypedObject>,
|
|
297
|
+
): ArbitraryTypedObject[] {
|
|
192
298
|
return blocks.reduce((memo, node, i, original) => {
|
|
193
299
|
if (node._type === 'block') {
|
|
194
300
|
memo.push(node)
|
|
@@ -217,7 +323,7 @@ export function ensureRootIsBlocks(
|
|
|
217
323
|
|
|
218
324
|
memo.push(block)
|
|
219
325
|
return memo
|
|
220
|
-
}, [] as
|
|
326
|
+
}, [] as ArbitraryTypedObject[])
|
|
221
327
|
}
|
|
222
328
|
|
|
223
329
|
export function isNodeList(node: unknown): node is NodeList {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type PortableTextBlock,
|
|
14
14
|
type PortableTextObject,
|
|
15
15
|
} from '../types.portable-text'
|
|
16
|
+
import {keyGenerator} from '../util/randomKey'
|
|
16
17
|
import {resolveJsType} from '../util/resolveJsType'
|
|
17
18
|
import {
|
|
18
19
|
defaultParseHtml,
|
|
@@ -34,6 +35,7 @@ import {createRules} from './rules'
|
|
|
34
35
|
*
|
|
35
36
|
*/
|
|
36
37
|
export default class HtmlDeserializer {
|
|
38
|
+
keyGenerator: () => string
|
|
37
39
|
schema: Schema
|
|
38
40
|
rules: DeserializerRule[]
|
|
39
41
|
parseHtml: (html: string) => HTMLElement
|
|
@@ -49,8 +51,10 @@ export default class HtmlDeserializer {
|
|
|
49
51
|
const {rules = [], unstable_whitespaceOnPasteMode = 'preserve'} = options
|
|
50
52
|
const standardRules = createRules(schema, {
|
|
51
53
|
keyGenerator: options.keyGenerator,
|
|
54
|
+
matchers: options.matchers,
|
|
52
55
|
})
|
|
53
56
|
this.schema = schema
|
|
57
|
+
this.keyGenerator = options.keyGenerator ?? keyGenerator
|
|
54
58
|
this.rules = [...rules, ...standardRules]
|
|
55
59
|
const parseHtml = options.parseHtml || defaultParseHtml()
|
|
56
60
|
this.parseHtml = (html) => {
|
|
@@ -74,8 +78,11 @@ export default class HtmlDeserializer {
|
|
|
74
78
|
const blocks = trimWhitespace(
|
|
75
79
|
this.schema,
|
|
76
80
|
flattenNestedBlocks(
|
|
77
|
-
this.schema,
|
|
78
|
-
ensureRootIsBlocks(
|
|
81
|
+
{schema: this.schema, keyGenerator: this.keyGenerator},
|
|
82
|
+
ensureRootIsBlocks(
|
|
83
|
+
this.schema,
|
|
84
|
+
this.deserializeElements(children) as Array<ArbitraryTypedObject>,
|
|
85
|
+
),
|
|
79
86
|
),
|
|
80
87
|
)
|
|
81
88
|
|
|
@@ -96,8 +96,16 @@ function getBlockStyle(schema: Schema, el: Node): string {
|
|
|
96
96
|
export default function createGDocsRules(schema: Schema): DeserializerRule[] {
|
|
97
97
|
return [
|
|
98
98
|
{
|
|
99
|
-
deserialize(el) {
|
|
99
|
+
deserialize(el, next) {
|
|
100
100
|
if (isElement(el) && tagName(el) === 'span' && isGoogleDocs(el)) {
|
|
101
|
+
if (!el.textContent) {
|
|
102
|
+
if (!el.previousSibling && !el.nextSibling) {
|
|
103
|
+
el.setAttribute('data-lonely-child', 'true')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return next(el.childNodes)
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
const span = {
|
|
102
110
|
...DEFAULT_SPAN,
|
|
103
111
|
marks: [] as string[],
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
HTML_SPAN_TAGS,
|
|
11
11
|
type PartialBlock,
|
|
12
12
|
} from '../../constants'
|
|
13
|
+
import type {SchemaMatchers} from '../../schema-matchers'
|
|
13
14
|
import type {DeserializerRule} from '../../types'
|
|
14
15
|
import {keyGenerator} from '../../util/randomKey'
|
|
15
16
|
import {isElement, tagName} from '../helpers'
|
|
@@ -36,7 +37,7 @@ export function resolveListItem(
|
|
|
36
37
|
|
|
37
38
|
export default function createHTMLRules(
|
|
38
39
|
schema: Schema,
|
|
39
|
-
options: {keyGenerator?: () => string},
|
|
40
|
+
options: {keyGenerator?: () => string; matchers?: SchemaMatchers},
|
|
40
41
|
): DeserializerRule[] {
|
|
41
42
|
return [
|
|
42
43
|
whitespaceTextNodeRule,
|
|
@@ -265,5 +266,79 @@ export default function createHTMLRules(
|
|
|
265
266
|
)
|
|
266
267
|
},
|
|
267
268
|
},
|
|
269
|
+
{
|
|
270
|
+
deserialize(el) {
|
|
271
|
+
if (isElement(el) && tagName(el) === 'img') {
|
|
272
|
+
const src = el.getAttribute('src') ?? undefined
|
|
273
|
+
const alt = el.getAttribute('alt') ?? undefined
|
|
274
|
+
|
|
275
|
+
const props = Object.fromEntries(
|
|
276
|
+
Array.from(el.attributes).map((attr) => [attr.name, attr.value]),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
const ancestorOfLonelyChild =
|
|
280
|
+
el?.parentElement?.parentElement?.getAttribute('data-lonely-child')
|
|
281
|
+
const ancestorOfListItem = el.closest('li') !== null
|
|
282
|
+
|
|
283
|
+
if (ancestorOfLonelyChild && !ancestorOfListItem) {
|
|
284
|
+
const image = options.matchers?.image?.({
|
|
285
|
+
context: {
|
|
286
|
+
schema,
|
|
287
|
+
keyGenerator: options.keyGenerator ?? keyGenerator,
|
|
288
|
+
},
|
|
289
|
+
props: {
|
|
290
|
+
...props,
|
|
291
|
+
...(src ? {src} : {}),
|
|
292
|
+
...(alt ? {alt} : {}),
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
if (image) {
|
|
297
|
+
return {
|
|
298
|
+
_type: '__block',
|
|
299
|
+
block: image,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const inlineImage = options.matchers?.inlineImage?.({
|
|
305
|
+
context: {
|
|
306
|
+
schema,
|
|
307
|
+
keyGenerator: options.keyGenerator ?? keyGenerator,
|
|
308
|
+
},
|
|
309
|
+
props: {
|
|
310
|
+
...props,
|
|
311
|
+
...(src ? {src} : {}),
|
|
312
|
+
...(alt ? {alt} : {}),
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
if (inlineImage) {
|
|
317
|
+
return inlineImage
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const image = options.matchers?.image?.({
|
|
321
|
+
context: {
|
|
322
|
+
schema,
|
|
323
|
+
keyGenerator: options.keyGenerator ?? keyGenerator,
|
|
324
|
+
},
|
|
325
|
+
props: {
|
|
326
|
+
...props,
|
|
327
|
+
...(src ? {src} : {}),
|
|
328
|
+
...(alt ? {alt} : {}),
|
|
329
|
+
},
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
if (image) {
|
|
333
|
+
return {
|
|
334
|
+
_type: '__block',
|
|
335
|
+
block: image,
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return undefined
|
|
341
|
+
},
|
|
342
|
+
},
|
|
268
343
|
]
|
|
269
344
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {Schema} from '@portabletext/schema'
|
|
2
|
+
import type {SchemaMatchers} from '../../schema-matchers'
|
|
2
3
|
import type {DeserializerRule} from '../../types'
|
|
3
4
|
import createGDocsRules from './gdocs'
|
|
4
5
|
import createHTMLRules from './html'
|
|
@@ -7,7 +8,7 @@ import createWordRules from './word'
|
|
|
7
8
|
|
|
8
9
|
export function createRules(
|
|
9
10
|
schema: Schema,
|
|
10
|
-
options: {keyGenerator?: () => string},
|
|
11
|
+
options: {keyGenerator?: () => string; matchers?: SchemaMatchers},
|
|
11
12
|
): DeserializerRule[] {
|
|
12
13
|
return [
|
|
13
14
|
...createWordRules(),
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type {Schema} from '@portabletext/schema'
|
|
|
3
3
|
import type {ArraySchemaType} from '@sanity/types'
|
|
4
4
|
import HtmlDeserializer from './HtmlDeserializer'
|
|
5
5
|
import type {HtmlDeserializerOptions, TypedObject} from './types'
|
|
6
|
-
import type {PortableTextTextBlock} from './types.portable-text'
|
|
7
6
|
import {normalizeBlock} from './util/normalizeBlock'
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -19,7 +18,7 @@ export function htmlToBlocks(
|
|
|
19
18
|
html: string,
|
|
20
19
|
schemaType: ArraySchemaType | Schema,
|
|
21
20
|
options: HtmlDeserializerOptions = {},
|
|
22
|
-
)
|
|
21
|
+
) {
|
|
23
22
|
const schema = isSanitySchema(schemaType)
|
|
24
23
|
? sanitySchemaToPortableTextSchema(schemaType)
|
|
25
24
|
: schemaType
|
|
@@ -30,6 +29,7 @@ export function htmlToBlocks(
|
|
|
30
29
|
.map((block) => normalizeBlock(block, {keyGenerator: options.keyGenerator}))
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
export type {ImageSchemaMatcher, SchemaMatchers} from './schema-matchers'
|
|
33
33
|
export type {ArbitraryTypedObject, DeserializerRule, HtmlParser} from './types'
|
|
34
34
|
export type {
|
|
35
35
|
PortableTextBlock,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type {Schema} from '@portabletext/schema'
|
|
2
|
+
import type {ArbitraryTypedObject} from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Use the current `Schema` as well as the potential element props to determine
|
|
6
|
+
* what Portable Text Object to use to represent the element.
|
|
7
|
+
*/
|
|
8
|
+
type ObjectSchemaMatcher<TProps extends Record<string, unknown>> = ({
|
|
9
|
+
context,
|
|
10
|
+
props,
|
|
11
|
+
}: {
|
|
12
|
+
context: {schema: Schema; keyGenerator: () => string}
|
|
13
|
+
props: TProps
|
|
14
|
+
}) => ArbitraryTypedObject | undefined
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Use the current `Schema` as well as the potential img element props to
|
|
18
|
+
* determine what Portable Text Object to use to represent the image.
|
|
19
|
+
* @beta
|
|
20
|
+
*/
|
|
21
|
+
export type ImageSchemaMatcher = ObjectSchemaMatcher<{
|
|
22
|
+
src?: string
|
|
23
|
+
alt?: string
|
|
24
|
+
[key: string]: string | undefined
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @beta
|
|
29
|
+
*/
|
|
30
|
+
export type SchemaMatchers = {
|
|
31
|
+
/**
|
|
32
|
+
* Called whenever the HTML parsing encounters an `<img>` element that is
|
|
33
|
+
* inferred to be a block element.
|
|
34
|
+
*/
|
|
35
|
+
image?: ImageSchemaMatcher
|
|
36
|
+
/**
|
|
37
|
+
* Called whenever the HTML parsing encounters an `<img>` element that is
|
|
38
|
+
* inferred to be an inline element.
|
|
39
|
+
*/
|
|
40
|
+
inlineImage?: ImageSchemaMatcher
|
|
41
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {SchemaMatchers} from './schema-matchers'
|
|
1
2
|
import type {PortableTextObject} from './types.portable-text'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -71,6 +72,11 @@ export interface HtmlDeserializerOptions {
|
|
|
71
72
|
rules?: DeserializerRule[]
|
|
72
73
|
parseHtml?: HtmlParser
|
|
73
74
|
unstable_whitespaceOnPasteMode?: WhiteSpacePasteMode
|
|
75
|
+
/**
|
|
76
|
+
* Custom schema matchers to use when deserializing HTML to Portable Text.
|
|
77
|
+
* @beta
|
|
78
|
+
*/
|
|
79
|
+
matchers?: SchemaMatchers
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
export interface HtmlPreprocessorOptions {
|