@portabletext/block-tools 3.3.3 → 3.4.1

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.
@@ -1,9 +1,120 @@
1
1
  import {compileSchema, defineSchema} from '@portabletext/schema'
2
2
  import {describe, expect, test} from 'vitest'
3
3
  import {createTestKeyGenerator} from '../../test/test-key-generator'
4
- import {flattenNestedBlocks} from './helpers'
4
+ import {flattenNestedBlocks} from './flatten-nested-blocks'
5
5
 
6
6
  describe(flattenNestedBlocks.name, () => {
7
+ test('flattening text blocks', () => {
8
+ const keyGenerator = createTestKeyGenerator('k')
9
+ const schema = compileSchema(defineSchema({}))
10
+ expect(
11
+ flattenNestedBlocks({schema, keyGenerator}, [
12
+ {
13
+ _type: 'block',
14
+ children: [
15
+ {
16
+ _type: 'block',
17
+ children: [
18
+ {
19
+ _type: 'span',
20
+ marks: [],
21
+ text: 'foo',
22
+ },
23
+ ],
24
+ markDefs: [],
25
+ style: 'normal',
26
+ },
27
+ ],
28
+ markDefs: [],
29
+ style: 'normal',
30
+ },
31
+ ]),
32
+ ).toEqual([
33
+ {
34
+ _type: 'block',
35
+ children: [{_type: 'span', text: 'foo', marks: []}],
36
+ markDefs: [],
37
+ style: 'normal',
38
+ },
39
+ ])
40
+ })
41
+
42
+ test('flattening text blocks with block objects in schema', () => {
43
+ const keyGenerator = createTestKeyGenerator('k')
44
+ const schema = compileSchema(
45
+ defineSchema({blockObjects: [{name: 'image'}]}),
46
+ )
47
+ expect(
48
+ flattenNestedBlocks({schema, keyGenerator}, [
49
+ {
50
+ _type: 'block',
51
+ children: [
52
+ {
53
+ _type: 'block',
54
+ children: [
55
+ {
56
+ _type: 'span',
57
+ text: 'foo',
58
+ marks: [],
59
+ },
60
+ ],
61
+ markDefs: [],
62
+ style: 'normal',
63
+ },
64
+ ],
65
+ markDefs: [],
66
+ style: 'normal',
67
+ },
68
+ ]),
69
+ ).toEqual([
70
+ {
71
+ _type: 'block',
72
+ children: [{_type: 'span', text: 'foo', marks: []}],
73
+ markDefs: [],
74
+ style: 'normal',
75
+ },
76
+ ])
77
+ })
78
+
79
+ test('flattening text blocks with styles in schema', () => {
80
+ const keyGenerator = createTestKeyGenerator('k')
81
+ const schema = compileSchema(
82
+ defineSchema({
83
+ styles: [{name: 'h1'}],
84
+ }),
85
+ )
86
+ expect(
87
+ flattenNestedBlocks({schema, keyGenerator}, [
88
+ {
89
+ _type: 'block',
90
+ children: [
91
+ {
92
+ _type: 'block',
93
+ children: [
94
+ {
95
+ _type: 'span',
96
+ marks: [],
97
+ text: 'foo',
98
+ },
99
+ ],
100
+ markDefs: [],
101
+ style: 'normal',
102
+ },
103
+ ],
104
+ markDefs: [],
105
+ style: 'h1',
106
+ },
107
+ ]),
108
+ ).toEqual([
109
+ {
110
+ _type: 'block',
111
+ children: [{_type: 'span', text: 'foo', marks: []}],
112
+ markDefs: [],
113
+ style: 'normal',
114
+ },
115
+ ])
116
+ })
117
+
7
118
  test('splitting text block', () => {
8
119
  const keyGenerator = createTestKeyGenerator('k')
9
120
  const schema = compileSchema(
@@ -0,0 +1,174 @@
1
+ import type {Schema} from '@portabletext/schema'
2
+ import {
3
+ isSpan,
4
+ isTextBlock,
5
+ type PortableTextBlock,
6
+ type PortableTextObject,
7
+ type PortableTextSpan,
8
+ type PortableTextTextBlock,
9
+ } from '@portabletext/schema'
10
+ import {isEqual} from 'lodash'
11
+ import {
12
+ isArbitraryTypedObject,
13
+ type ArbitraryTypedObject,
14
+ type TypedObject,
15
+ } from '../types'
16
+
17
+ export function flattenNestedBlocks(
18
+ context: {
19
+ schema: Schema
20
+ keyGenerator: () => string
21
+ },
22
+ blocks: Array<ArbitraryTypedObject>,
23
+ ): TypedObject[] {
24
+ const flattened = blocks.flatMap((block) => {
25
+ if (isBlockContainer(block)) {
26
+ return flattenNestedBlocks(context, [block.block])
27
+ }
28
+
29
+ if (isTextBlock(context, block)) {
30
+ const hasBlockObjects = block.children.some((child) => {
31
+ const knownBlockObject = context.schema.blockObjects.some(
32
+ (blockObject) => blockObject.name === child._type,
33
+ )
34
+ return knownBlockObject
35
+ })
36
+ const hasBlocks = block.children.some(
37
+ (child) => child._type === '__block' || child._type === 'block',
38
+ )
39
+
40
+ if (hasBlockObjects || hasBlocks) {
41
+ const splitChildren = getSplitChildren(context, block)
42
+
43
+ if (
44
+ splitChildren.length === 1 &&
45
+ splitChildren[0].type === 'children' &&
46
+ isEqual(splitChildren[0].children, block.children)
47
+ ) {
48
+ return [block]
49
+ }
50
+
51
+ return splitChildren.flatMap((slice) => {
52
+ if (slice.type === 'block object') {
53
+ return [slice.block]
54
+ }
55
+
56
+ if (slice.type === 'block') {
57
+ return flattenNestedBlocks(context, [
58
+ slice.block as ArbitraryTypedObject,
59
+ ])
60
+ }
61
+
62
+ if (slice.children.length > 0) {
63
+ if (
64
+ slice.children.every(
65
+ (child) => isSpan(context, child) && child.text.trim() === '',
66
+ )
67
+ ) {
68
+ return []
69
+ }
70
+
71
+ return flattenNestedBlocks(context, [
72
+ {
73
+ ...block,
74
+ children: slice.children,
75
+ },
76
+ ])
77
+ }
78
+
79
+ return []
80
+ })
81
+ }
82
+
83
+ return [block]
84
+ }
85
+
86
+ return [block]
87
+ })
88
+
89
+ return flattened
90
+ }
91
+
92
+ function isBlockContainer(
93
+ block: ArbitraryTypedObject,
94
+ ): block is BlockContainer {
95
+ return block._type === '__block' && isArbitraryTypedObject(block.block)
96
+ }
97
+
98
+ type BlockContainer = {
99
+ _type: '__block'
100
+ block: ArbitraryTypedObject
101
+ }
102
+
103
+ function getSplitChildren(
104
+ context: {schema: Schema},
105
+ block: PortableTextTextBlock,
106
+ ) {
107
+ return block.children.reduce(
108
+ (slices, child) => {
109
+ const knownInlineObject = context.schema.inlineObjects.some(
110
+ (inlineObject) => inlineObject.name === child._type,
111
+ )
112
+ const knownBlockObject = context.schema.blockObjects.some(
113
+ (blockObject) => blockObject.name === child._type,
114
+ )
115
+
116
+ const lastSlice = slices.pop()
117
+
118
+ if (!isSpan(context, child) && !knownInlineObject) {
119
+ if (knownBlockObject) {
120
+ return [
121
+ ...slices,
122
+ ...(lastSlice ? [lastSlice] : []),
123
+ {type: 'block object' as const, block: child},
124
+ ]
125
+ }
126
+ }
127
+
128
+ if (child._type === '__block') {
129
+ return [
130
+ ...slices,
131
+ ...(lastSlice ? [lastSlice] : []),
132
+ {
133
+ type: 'block object' as const,
134
+ block: (child as any).block,
135
+ },
136
+ ]
137
+ }
138
+
139
+ if (child._type === 'block') {
140
+ return [
141
+ ...slices,
142
+ ...(lastSlice ? [lastSlice] : []),
143
+ {type: 'block' as const, block: child},
144
+ ]
145
+ }
146
+
147
+ if (lastSlice) {
148
+ if (lastSlice.type === 'children') {
149
+ return [
150
+ ...slices,
151
+ {
152
+ type: 'children' as const,
153
+ children: [...lastSlice.children, child],
154
+ },
155
+ ]
156
+ }
157
+ }
158
+
159
+ return [
160
+ ...slices,
161
+ ...(lastSlice ? [lastSlice] : []),
162
+ {type: 'children' as const, children: [child]},
163
+ ]
164
+ },
165
+ [] as Array<
166
+ | {
167
+ type: 'children'
168
+ children: Array<PortableTextSpan | PortableTextObject>
169
+ }
170
+ | {type: 'block object'; block: PortableTextObject}
171
+ | {type: 'block'; block: PortableTextBlock}
172
+ >,
173
+ )
174
+ }
@@ -1,4 +1,9 @@
1
1
  import type {Schema} from '@portabletext/schema'
2
+ import {
3
+ isTextBlock,
4
+ type PortableTextObject,
5
+ type PortableTextTextBlock,
6
+ } from '@portabletext/schema'
2
7
  import {vercelStegaClean} from '@vercel/stega'
3
8
  import {isEqual} from 'lodash'
4
9
  import {DEFAULT_BLOCK} from '../constants'
@@ -12,13 +17,6 @@ import type {
12
17
  PlaceholderDecorator,
13
18
  TypedObject,
14
19
  } from '../types'
15
- import {
16
- isSpan,
17
- isTextBlock,
18
- type PortableTextObject,
19
- type PortableTextSpan,
20
- type PortableTextTextBlock,
21
- } from '../types.portable-text'
22
20
  import {resolveJsType} from '../util/resolveJsType'
23
21
  import preprocessors from './preprocessors'
24
22
 
@@ -72,143 +70,6 @@ export function defaultParseHtml(): HtmlParser {
72
70
  }
73
71
  }
74
72
 
75
- export function flattenNestedBlocks(
76
- context: {
77
- schema: Schema
78
- keyGenerator: () => string
79
- },
80
- blocks: Array<ArbitraryTypedObject>,
81
- ): TypedObject[] {
82
- let depth = 0
83
- const flattened: TypedObject[] = []
84
-
85
- const traverse = (nodes: TypedObject[]) => {
86
- const toRemove: TypedObject[] = []
87
- nodes.forEach((node) => {
88
- if (depth === 0) {
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
- }
188
- }
189
-
190
- if (isTextBlock(context.schema, node)) {
191
- if (depth > 0) {
192
- toRemove.push(node)
193
- flattened.push(node)
194
- }
195
- depth++
196
- traverse(node.children)
197
- }
198
- if (node._type === '__block') {
199
- toRemove.push(node)
200
- flattened.push((node as any).block)
201
- }
202
- })
203
- toRemove.forEach((node) => {
204
- nodes.splice(nodes.indexOf(node), 1)
205
- })
206
- depth--
207
- }
208
- traverse(blocks)
209
- return flattened
210
- }
211
-
212
73
  function nextSpan(block: PortableTextTextBlock, index: number) {
213
74
  const next = block.children[index + 1]
214
75
  return next && next._type === 'span' ? next : null
@@ -234,7 +95,7 @@ export function trimWhitespace(
234
95
  blocks: TypedObject[],
235
96
  ): TypedObject[] {
236
97
  blocks.forEach((block) => {
237
- if (!isTextBlock(schema, block)) {
98
+ if (!isTextBlock({schema}, block)) {
238
99
  return
239
100
  }
240
101
 
@@ -293,27 +154,27 @@ export function trimWhitespace(
293
154
 
294
155
  export function ensureRootIsBlocks(
295
156
  schema: Schema,
296
- blocks: Array<ArbitraryTypedObject>,
157
+ objects: Array<ArbitraryTypedObject>,
297
158
  ): ArbitraryTypedObject[] {
298
- return blocks.reduce((memo, node, i, original) => {
159
+ return objects.reduce((blocks, node, i, original) => {
299
160
  if (node._type === 'block') {
300
- memo.push(node)
301
- return memo
161
+ blocks.push(node)
162
+ return blocks
302
163
  }
303
164
 
304
165
  if (node._type === '__block') {
305
- memo.push((node as any).block)
306
- return memo
166
+ blocks.push((node as any).block)
167
+ return blocks
307
168
  }
308
169
 
309
- const lastBlock = memo[memo.length - 1]
170
+ const lastBlock = blocks[blocks.length - 1]
310
171
  if (
311
172
  i > 0 &&
312
- !isTextBlock(schema, original[i - 1]) &&
313
- isTextBlock(schema, lastBlock)
173
+ !isTextBlock({schema}, original[i - 1]) &&
174
+ isTextBlock({schema}, lastBlock)
314
175
  ) {
315
176
  lastBlock.children.push(node as PortableTextObject)
316
- return memo
177
+ return blocks
317
178
  }
318
179
 
319
180
  const block = {
@@ -321,8 +182,8 @@ export function ensureRootIsBlocks(
321
182
  children: [node],
322
183
  }
323
184
 
324
- memo.push(block)
325
- return memo
185
+ blocks.push(block)
186
+ return blocks
326
187
  }, [] as ArbitraryTypedObject[])
327
188
  }
328
189
 
@@ -393,7 +254,9 @@ export function normalizeWhitespace(rootNode: Node) {
393
254
  }
394
255
 
395
256
  // Remove marked nodes
396
- nodesToRemove.forEach((node) => node.parentElement?.removeChild(node))
257
+ nodesToRemove.forEach((node) => {
258
+ node.parentElement?.removeChild(node)
259
+ })
397
260
  }
398
261
 
399
262
  /**
@@ -438,7 +301,9 @@ export function removeAllWhitespace(rootNode: Node) {
438
301
  collectNodesToRemove(rootNode)
439
302
 
440
303
  // Remove the collected nodes
441
- nodesToRemove.forEach((node) => node.parentElement?.removeChild(node))
304
+ nodesToRemove.forEach((node) => {
305
+ node.parentElement?.removeChild(node)
306
+ })
442
307
  }
443
308
 
444
309
  function isWhitespaceBlock(elm: HTMLElement): boolean {
@@ -1,4 +1,9 @@
1
1
  import type {Schema} from '@portabletext/schema'
2
+ import {
3
+ isTextBlock,
4
+ type PortableTextBlock,
5
+ type PortableTextObject,
6
+ } from '@portabletext/schema'
2
7
  import {flatten} from 'lodash'
3
8
  import type {
4
9
  ArbitraryTypedObject,
@@ -8,17 +13,12 @@ import type {
8
13
  PlaceholderDecorator,
9
14
  TypedObject,
10
15
  } from '../types'
11
- import {
12
- isTextBlock,
13
- type PortableTextBlock,
14
- type PortableTextObject,
15
- } from '../types.portable-text'
16
16
  import {keyGenerator} from '../util/randomKey'
17
17
  import {resolveJsType} from '../util/resolveJsType'
18
+ import {flattenNestedBlocks} from './flatten-nested-blocks'
18
19
  import {
19
20
  defaultParseHtml,
20
21
  ensureRootIsBlocks,
21
- flattenNestedBlocks,
22
22
  isMinimalBlock,
23
23
  isMinimalSpan,
24
24
  isNodeList,
@@ -88,7 +88,7 @@ export default class HtmlDeserializer {
88
88
 
89
89
  if (this._markDefs.length > 0) {
90
90
  blocks
91
- .filter((block) => isTextBlock(this.schema, block))
91
+ .filter((block) => isTextBlock({schema: this.schema}, block))
92
92
  .forEach((block) => {
93
93
  block.markDefs = block.markDefs || []
94
94
  block.markDefs = block.markDefs.concat(
@@ -266,6 +266,18 @@ export default function createHTMLRules(
266
266
  )
267
267
  },
268
268
  },
269
+ {
270
+ deserialize(el, next) {
271
+ if (isElement(el) && (tagName(el) === 'td' || tagName(el) === 'th')) {
272
+ return {
273
+ ...DEFAULT_BLOCK,
274
+ children: next(el.childNodes),
275
+ }
276
+ }
277
+
278
+ return undefined
279
+ },
280
+ },
269
281
  {
270
282
  deserialize(el) {
271
283
  if (isElement(el) && tagName(el) === 'img') {
package/src/index.ts CHANGED
@@ -36,7 +36,7 @@ export type {
36
36
  PortableTextObject,
37
37
  PortableTextSpan,
38
38
  PortableTextTextBlock,
39
- } from './types.portable-text'
39
+ } from '@portabletext/schema'
40
40
  export type {BlockNormalizationOptions} from './util/normalizeBlock'
41
41
  export {randomKey} from './util/randomKey'
42
42
  export {normalizeBlock}
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import type {PortableTextObject} from '@portabletext/schema'
1
2
  import type {SchemaMatchers} from './schema-matchers'
2
- import type {PortableTextObject} from './types.portable-text'
3
3
 
4
4
  /**
5
5
  * @public
@@ -1,11 +1,11 @@
1
1
  import type {Schema} from '@portabletext/schema'
2
- import {isEqual} from 'lodash'
3
- import type {TypedObject} from '../types'
4
2
  import {
5
3
  isSpan,
6
4
  type PortableTextSpan,
7
5
  type PortableTextTextBlock,
8
- } from '../types.portable-text'
6
+ } from '@portabletext/schema'
7
+ import {isEqual} from 'lodash'
8
+ import type {TypedObject} from '../types'
9
9
  import {keyGenerator} from './randomKey'
10
10
 
11
11
  /**
@@ -115,8 +115,8 @@ export function normalizeBlock(
115
115
  const previousChild = acc[acc.length - 1]
116
116
  if (
117
117
  previousChild &&
118
- isSpan(schema, child) &&
119
- isSpan(schema, previousChild) &&
118
+ isSpan({schema}, child) &&
119
+ isSpan({schema}, previousChild) &&
120
120
  isEqual(previousChild.marks, child.marks)
121
121
  ) {
122
122
  if (
@@ -145,7 +145,7 @@ export function normalizeBlock(
145
145
  ? options.keyGenerator()
146
146
  : keyGenerator()
147
147
 
148
- if (isSpan(schema, child)) {
148
+ if (isSpan({schema}, child)) {
149
149
  if (!child.marks) {
150
150
  child.marks = []
151
151
  } else if (allowedDecorators) {