@portabletext/block-tools 2.0.7 → 3.0.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.
@@ -1,9 +1,3 @@
1
- import type {
2
- ArraySchemaType,
3
- PortableTextBlock,
4
- PortableTextObject,
5
- PortableTextTextBlock,
6
- } from '@sanity/types'
7
1
  import {flatten} from 'lodash'
8
2
  import type {
9
3
  ArbitraryTypedObject,
@@ -13,10 +7,14 @@ import type {
13
7
  PlaceholderDecorator,
14
8
  TypedObject,
15
9
  } from '../types'
16
- import {findBlockType} from '../util/findBlockType'
10
+ import {
11
+ isTextBlock,
12
+ type PortableTextBlock,
13
+ type PortableTextObject,
14
+ } from '../types.portable-text'
15
+ import type {PortableTextSchema} from '../util/portable-text-schema'
17
16
  import {resolveJsType} from '../util/resolveJsType'
18
17
  import {
19
- createRuleOptions,
20
18
  defaultParseHtml,
21
19
  ensureRootIsBlocks,
22
20
  flattenNestedBlocks,
@@ -36,7 +34,7 @@ import {createRules} from './rules'
36
34
  *
37
35
  */
38
36
  export default class HtmlDeserializer {
39
- blockContentType: ArraySchemaType
37
+ schema: PortableTextSchema
40
38
  rules: DeserializerRule[]
41
39
  parseHtml: (html: string) => HTMLElement
42
40
  _markDefs: PortableTextObject[] = []
@@ -48,20 +46,16 @@ export default class HtmlDeserializer {
48
46
  * @param options - Options for the deserialization process
49
47
  */
50
48
  constructor(
51
- blockContentType: ArraySchemaType,
49
+ schema: PortableTextSchema,
52
50
  options: HtmlDeserializerOptions = {},
53
51
  ) {
54
52
  const {rules = [], unstable_whitespaceOnPasteMode = 'preserve'} = options
55
- if (!blockContentType) {
56
- throw new Error("Parameter 'blockContentType' is required")
57
- }
58
- const standardRules = createRules(blockContentType, {
59
- ...createRuleOptions(blockContentType),
53
+ const standardRules = createRules(schema, {
60
54
  keyGenerator: options.keyGenerator,
61
55
  })
56
+ this.schema = schema
62
57
  this.rules = [...rules, ...standardRules]
63
58
  const parseHtml = options.parseHtml || defaultParseHtml()
64
- this.blockContentType = blockContentType
65
59
  this.parseHtml = (html) => {
66
60
  const doc = preprocess(html, parseHtml, {unstable_whitespaceOnPasteMode})
67
61
  return doc.body
@@ -81,16 +75,16 @@ export default class HtmlDeserializer {
81
75
  const children = Array.from(fragment.childNodes) as HTMLElement[]
82
76
  // Ensure that there are no blocks within blocks, and trim whitespace
83
77
  const blocks = trimWhitespace(
78
+ this.schema,
84
79
  flattenNestedBlocks(
85
- ensureRootIsBlocks(this.deserializeElements(children)),
80
+ this.schema,
81
+ ensureRootIsBlocks(this.schema, this.deserializeElements(children)),
86
82
  ),
87
83
  )
88
84
 
89
85
  if (this._markDefs.length > 0) {
90
86
  blocks
91
- .filter(
92
- (block): block is PortableTextTextBlock => block._type === 'block',
93
- )
87
+ .filter((block) => isTextBlock(this.schema, block))
94
88
  .forEach((block) => {
95
89
  block.markDefs = block.markDefs || []
96
90
  block.markDefs = block.markDefs.concat(
@@ -103,15 +97,9 @@ export default class HtmlDeserializer {
103
97
  })
104
98
  }
105
99
 
106
- // Set back the potentially hoisted block type
107
- const type = this.blockContentType.of.find(findBlockType)
108
- if (!type) {
109
- return blocks
110
- }
111
-
112
100
  return blocks.map((block) => {
113
101
  if (block._type === 'block') {
114
- block._type = type.name
102
+ block._type = this.schema.block.name
115
103
  }
116
104
  return block
117
105
  })
@@ -1,4 +1,3 @@
1
- import type {ArraySchemaType} from '@sanity/types'
2
1
  import {
3
2
  BLOCK_DEFAULT_STYLE,
4
3
  DEFAULT_BLOCK,
@@ -7,7 +6,8 @@ import {
7
6
  HTML_HEADER_TAGS,
8
7
  HTML_LIST_CONTAINER_TAGS,
9
8
  } from '../../constants'
10
- import type {BlockEnabledFeatures, DeserializerRule} from '../../types'
9
+ import type {DeserializerRule} from '../../types'
10
+ import type {PortableTextSchema} from '../../util/portable-text-schema'
11
11
  import {isElement, tagName} from '../helpers'
12
12
 
13
13
  const LIST_CONTAINER_TAGS = Object.keys(HTML_LIST_CONTAINER_TAGS)
@@ -81,21 +81,20 @@ const blocks: Record<string, {style: string} | undefined> = {
81
81
  ...HTML_HEADER_TAGS,
82
82
  }
83
83
 
84
- function getBlockStyle(el: Node, enabledBlockStyles: string[]): string {
84
+ function getBlockStyle(schema: PortableTextSchema, el: Node): string {
85
85
  const childTag = tagName(el.firstChild)
86
86
  const block = childTag && blocks[childTag]
87
87
  if (!block) {
88
88
  return BLOCK_DEFAULT_STYLE
89
89
  }
90
- if (!enabledBlockStyles.includes(block.style)) {
90
+ if (!schema.styles.some((style) => style.name === block.style)) {
91
91
  return BLOCK_DEFAULT_STYLE
92
92
  }
93
93
  return block.style
94
94
  }
95
95
 
96
96
  export default function createGDocsRules(
97
- _blockContentType: ArraySchemaType,
98
- options: BlockEnabledFeatures,
97
+ schema: PortableTextSchema,
99
98
  ): DeserializerRule[] {
100
99
  return [
101
100
  {
@@ -130,7 +129,7 @@ export default function createGDocsRules(
130
129
  ...DEFAULT_BLOCK,
131
130
  listItem: getListItemStyle(el),
132
131
  level: getListItemLevel(el),
133
- style: getBlockStyle(el, options.enabledBlockStyles),
132
+ style: getBlockStyle(schema, el),
134
133
  children: next(el.firstChild?.childNodes || []),
135
134
  }
136
135
  }
@@ -1,4 +1,3 @@
1
- import type {ArraySchemaType, TypedObject} from '@sanity/types'
2
1
  import {
3
2
  DEFAULT_BLOCK,
4
3
  DEFAULT_SPAN,
@@ -10,27 +9,34 @@ import {
10
9
  HTML_SPAN_TAGS,
11
10
  type PartialBlock,
12
11
  } from '../../constants'
13
- import type {BlockEnabledFeatures, DeserializerRule} from '../../types'
12
+ import type {DeserializerRule} from '../../types'
13
+ import type {PortableTextSchema} from '../../util/portable-text-schema'
14
14
  import {keyGenerator} from '../../util/randomKey'
15
15
  import {isElement, tagName} from '../helpers'
16
16
  import {whitespaceTextNodeRule} from './whitespace-text-node'
17
17
 
18
18
  export function resolveListItem(
19
+ schema: PortableTextSchema,
19
20
  listNodeTagName: string,
20
- enabledListTypes: string[],
21
21
  ): string | undefined {
22
- if (listNodeTagName === 'ul' && enabledListTypes.includes('bullet')) {
22
+ if (
23
+ listNodeTagName === 'ul' &&
24
+ schema.lists.some((list) => list.name === 'bullet')
25
+ ) {
23
26
  return 'bullet'
24
27
  }
25
- if (listNodeTagName === 'ol' && enabledListTypes.includes('number')) {
28
+ if (
29
+ listNodeTagName === 'ol' &&
30
+ schema.lists.some((list) => list.name === 'number')
31
+ ) {
26
32
  return 'number'
27
33
  }
28
34
  return undefined
29
35
  }
30
36
 
31
37
  export default function createHTMLRules(
32
- _blockContentType: ArraySchemaType,
33
- options: BlockEnabledFeatures & {keyGenerator?: () => string},
38
+ schema: PortableTextSchema,
39
+ options: {keyGenerator?: () => string},
34
40
  ): DeserializerRule[] {
35
41
  return [
36
42
  whitespaceTextNodeRule,
@@ -41,7 +47,9 @@ export default function createHTMLRules(
41
47
  return undefined
42
48
  }
43
49
 
44
- const isCodeEnabled = options.enabledBlockStyles.includes('code')
50
+ const isCodeEnabled = schema.styles.some(
51
+ (style) => style.name === 'code',
52
+ )
45
53
 
46
54
  return {
47
55
  _type: 'block',
@@ -134,8 +142,9 @@ export default function createHTMLRules(
134
142
  if (el.parentNode && tagName(el.parentNode) === 'li') {
135
143
  return next(el.childNodes)
136
144
  }
145
+ const blockStyle = block.style
137
146
  // If style is not supported, return a defaultBlockType
138
- if (!options.enabledBlockStyles.includes(block.style)) {
147
+ if (!schema.styles.some((style) => style.name === blockStyle)) {
139
148
  block = DEFAULT_BLOCK
140
149
  }
141
150
  return {
@@ -194,10 +203,7 @@ export default function createHTMLRules(
194
203
  ) {
195
204
  return undefined
196
205
  }
197
- const enabledListItem = resolveListItem(
198
- parentTag,
199
- options.enabledListTypes,
200
- )
206
+ const enabledListItem = resolveListItem(schema, parentTag)
201
207
  // If the list item style is not supported, return a new default block
202
208
  if (!enabledListItem) {
203
209
  return block({_type: 'block', children: next(el.childNodes)})
@@ -212,7 +218,12 @@ export default function createHTMLRules(
212
218
  {
213
219
  deserialize(el, next) {
214
220
  const decorator = HTML_DECORATOR_TAGS[tagName(el) || '']
215
- if (!decorator || !options.enabledSpanDecorators.includes(decorator)) {
221
+ if (
222
+ !decorator ||
223
+ !schema.decorators.some(
224
+ (decoratorType) => decoratorType.name === decorator,
225
+ )
226
+ ) {
216
227
  return undefined
217
228
  }
218
229
  return {
@@ -228,23 +239,23 @@ export default function createHTMLRules(
228
239
  if (tagName(el) !== 'a') {
229
240
  return undefined
230
241
  }
231
- const linkEnabled = options.enabledBlockAnnotations.includes('link')
242
+ const linkEnabled = schema.annotations.some(
243
+ (annotation) => annotation.name === 'link',
244
+ )
232
245
  const href = isElement(el) && el.getAttribute('href')
233
246
  if (!href) {
234
247
  return next(el.childNodes)
235
248
  }
236
- let markDef: TypedObject | undefined
237
249
  if (linkEnabled) {
238
- markDef = {
239
- _key: options.keyGenerator
240
- ? options.keyGenerator()
241
- : keyGenerator(),
242
- _type: 'link',
243
- href: href,
244
- }
245
250
  return {
246
251
  _type: '__annotation',
247
- markDef: markDef,
252
+ markDef: {
253
+ _key: options.keyGenerator
254
+ ? options.keyGenerator()
255
+ : keyGenerator(),
256
+ _type: 'link',
257
+ href: href,
258
+ },
248
259
  children: next(el.childNodes),
249
260
  }
250
261
  }
@@ -1,18 +1,18 @@
1
- import type {ArraySchemaType} from '@sanity/types'
2
- import type {BlockEnabledFeatures, DeserializerRule} from '../../types'
1
+ import type {DeserializerRule} from '../../types'
2
+ import type {PortableTextSchema} from '../../util/portable-text-schema'
3
3
  import createGDocsRules from './gdocs'
4
4
  import createHTMLRules from './html'
5
5
  import createNotionRules from './notion'
6
6
  import createWordRules from './word'
7
7
 
8
8
  export function createRules(
9
- blockContentType: ArraySchemaType,
10
- options: BlockEnabledFeatures & {keyGenerator?: () => string},
9
+ schema: PortableTextSchema,
10
+ options: {keyGenerator?: () => string},
11
11
  ): DeserializerRule[] {
12
12
  return [
13
13
  ...createWordRules(),
14
- ...createNotionRules(blockContentType),
15
- ...createGDocsRules(blockContentType, options),
16
- ...createHTMLRules(blockContentType, options),
14
+ ...createNotionRules(),
15
+ ...createGDocsRules(schema),
16
+ ...createHTMLRules(schema, options),
17
17
  ]
18
18
  }
@@ -1,4 +1,3 @@
1
- import type {ArraySchemaType} from '@sanity/types'
2
1
  import {DEFAULT_SPAN} from '../../constants'
3
2
  import type {DeserializerRule} from '../../types'
4
3
  import {isElement, tagName} from '../helpers'
@@ -28,9 +27,7 @@ function isNotion(el: Node): boolean {
28
27
  return isElement(el) && Boolean(el.getAttribute('data-is-notion'))
29
28
  }
30
29
 
31
- export default function createNotionRules(
32
- _blockContentType: ArraySchemaType,
33
- ): DeserializerRule[] {
30
+ export default function createNotionRules(): DeserializerRule[] {
34
31
  return [
35
32
  {
36
33
  deserialize(el) {
package/src/index.ts CHANGED
@@ -1,12 +1,9 @@
1
- import type {ArraySchemaType, PortableTextTextBlock} from '@sanity/types'
1
+ import type {ArraySchemaType} from '@sanity/types'
2
2
  import HtmlDeserializer from './HtmlDeserializer'
3
- import type {
4
- BlockContentFeatures,
5
- HtmlDeserializerOptions,
6
- TypedObject,
7
- } from './types'
8
- import blockContentTypeFeatures from './util/blockContentTypeFeatures'
3
+ import type {HtmlDeserializerOptions, TypedObject} from './types'
4
+ import type {PortableTextTextBlock} from './types.portable-text'
9
5
  import {normalizeBlock} from './util/normalizeBlock'
6
+ import {getPortableTextSchema} from './util/portable-text-schema'
10
7
 
11
8
  /**
12
9
  * Convert HTML to blocks respecting the block content type's schema
@@ -22,33 +19,21 @@ export function htmlToBlocks(
22
19
  blockContentType: ArraySchemaType,
23
20
  options: HtmlDeserializerOptions = {},
24
21
  ): (TypedObject | PortableTextTextBlock)[] {
25
- const deserializer = new HtmlDeserializer(blockContentType, options)
22
+ const schema = getPortableTextSchema(blockContentType)
23
+ const deserializer = new HtmlDeserializer(schema, options)
26
24
  return deserializer
27
25
  .deserialize(html)
28
26
  .map((block) => normalizeBlock(block, {keyGenerator: options.keyGenerator}))
29
27
  }
30
28
 
31
- /**
32
- * Normalize and extract features of an schema type containing a block type
33
- *
34
- * @param blockContentType - Schema type for the block type
35
- * @returns Returns the featureset of a compiled block content type.
36
- * @public
37
- */
38
- export function getBlockContentFeatures(
39
- blockContentType: ArraySchemaType,
40
- ): BlockContentFeatures {
41
- return blockContentTypeFeatures(blockContentType)
42
- }
43
-
29
+ export type {ArbitraryTypedObject, DeserializerRule, HtmlParser} from './types'
44
30
  export type {
45
- ArbitraryTypedObject,
46
- BlockEditorSchemaProps,
47
- DeserializerRule,
48
- HtmlParser,
49
- ResolvedAnnotationType,
50
- } from './types'
31
+ PortableTextBlock,
32
+ PortableTextObject,
33
+ PortableTextSpan,
34
+ PortableTextTextBlock,
35
+ } from './types.portable-text'
51
36
  export type {BlockNormalizationOptions} from './util/normalizeBlock'
52
37
  export {randomKey} from './util/randomKey'
53
38
  export {normalizeBlock}
54
- export type {BlockContentFeatures, HtmlDeserializerOptions, TypedObject}
39
+ export type {HtmlDeserializerOptions, TypedObject}
@@ -0,0 +1,79 @@
1
+ import {isArbitraryTypedObject} from './types'
2
+ import type {PortableTextSchema} from './util/portable-text-schema'
3
+
4
+ /**
5
+ * @public
6
+ */
7
+ export type PortableTextBlock = PortableTextTextBlock | PortableTextObject
8
+
9
+ /**
10
+ * @public
11
+ */
12
+ export interface PortableTextTextBlock<
13
+ TChild = PortableTextSpan | PortableTextObject,
14
+ > {
15
+ _type: string
16
+ _key: string
17
+ children: TChild[]
18
+ markDefs?: PortableTextObject[]
19
+ listItem?: string
20
+ style?: string
21
+ level?: number
22
+ }
23
+
24
+ export function isTextBlock(
25
+ schema: PortableTextSchema,
26
+ block: unknown,
27
+ ): block is PortableTextTextBlock {
28
+ if (!isArbitraryTypedObject(block)) {
29
+ return false
30
+ }
31
+
32
+ if (block._type !== schema.block.name) {
33
+ return false
34
+ }
35
+
36
+ if (!Array.isArray(block.children)) {
37
+ return false
38
+ }
39
+
40
+ return true
41
+ }
42
+
43
+ /**
44
+ * @public
45
+ */
46
+ export interface PortableTextSpan {
47
+ _key: string
48
+ _type: 'span'
49
+ text: string
50
+ marks?: string[]
51
+ }
52
+
53
+ export function isSpan(
54
+ schema: PortableTextSchema,
55
+ child: unknown,
56
+ ): child is PortableTextSpan {
57
+ if (!isArbitraryTypedObject(child)) {
58
+ return false
59
+ }
60
+
61
+ if (child._type !== schema.span.name) {
62
+ return false
63
+ }
64
+
65
+ if (typeof child.text !== 'string') {
66
+ return false
67
+ }
68
+
69
+ return true
70
+ }
71
+
72
+ /**
73
+ * @public
74
+ */
75
+ export interface PortableTextObject {
76
+ _type: string
77
+ _key: string
78
+ [other: string]: unknown
79
+ }
package/src/types.ts CHANGED
@@ -1,47 +1,4 @@
1
- import type {
2
- ArraySchemaType,
3
- I18nTitledListValue,
4
- ObjectSchemaType,
5
- PortableTextObject,
6
- SpanSchemaType,
7
- TitledListValue,
8
- } from '@sanity/types'
9
- import type {ComponentType} from 'react'
10
-
11
- /**
12
- * @public
13
- */
14
- export interface BlockContentFeatures {
15
- styles: TitledListValue<string>[]
16
- decorators: TitledListValue<string>[]
17
- annotations: ResolvedAnnotationType[]
18
- lists: I18nTitledListValue<string>[]
19
- types: {
20
- block: ArraySchemaType
21
- span: SpanSchemaType
22
- inlineObjects: ObjectSchemaType[]
23
- blockObjects: ObjectSchemaType[]
24
- }
25
- }
26
-
27
- /**
28
- * @beta
29
- */
30
- export interface BlockEditorSchemaProps {
31
- icon?: string | ComponentType
32
- render?: ComponentType
33
- }
34
-
35
- /**
36
- * @public
37
- */
38
- export interface ResolvedAnnotationType {
39
- blockEditor?: BlockEditorSchemaProps
40
- title: string | undefined
41
- value: string
42
- type: ObjectSchemaType
43
- icon: ComponentType | undefined
44
- }
1
+ import type {PortableTextObject} from './types.portable-text'
45
2
 
46
3
  /**
47
4
  * @public
@@ -58,6 +15,16 @@ export interface ArbitraryTypedObject extends TypedObject {
58
15
  [key: string]: unknown
59
16
  }
60
17
 
18
+ export function isArbitraryTypedObject(
19
+ object: unknown,
20
+ ): object is ArbitraryTypedObject {
21
+ return isRecord(object) && typeof object._type === 'string'
22
+ }
23
+
24
+ function isRecord(value: unknown): value is Record<string, unknown> {
25
+ return !!value && (typeof value === 'object' || typeof value === 'function')
26
+ }
27
+
61
28
  export interface MinimalSpan {
62
29
  _type: 'span'
63
30
  _key?: string
@@ -106,9 +73,6 @@ export interface HtmlDeserializerOptions {
106
73
  unstable_whitespaceOnPasteMode?: WhiteSpacePasteMode
107
74
  }
108
75
 
109
- /**
110
- * @public
111
- */
112
76
  export interface HtmlPreprocessorOptions {
113
77
  unstable_whitespaceOnPasteMode?: WhiteSpacePasteMode
114
78
  }
@@ -128,13 +92,3 @@ export interface DeserializerRule {
128
92
  },
129
93
  ) => TypedObject | TypedObject[] | undefined
130
94
  }
131
-
132
- /**
133
- * @public
134
- */
135
- export interface BlockEnabledFeatures {
136
- enabledBlockStyles: string[]
137
- enabledSpanDecorators: string[]
138
- enabledListTypes: string[]
139
- enabledBlockAnnotations: string[]
140
- }
@@ -1,10 +1,11 @@
1
+ import {isEqual} from 'lodash'
2
+ import type {TypedObject} from '../types'
1
3
  import {
2
- isPortableTextSpan,
4
+ isSpan,
3
5
  type PortableTextSpan,
4
6
  type PortableTextTextBlock,
5
- } from '@sanity/types'
6
- import {isEqual} from 'lodash'
7
- import type {TypedObject} from '../types'
7
+ } from '../types.portable-text'
8
+ import type {PortableTextSchema} from './portable-text-schema'
8
9
  import {keyGenerator} from './randomKey'
9
10
 
10
11
  /**
@@ -53,6 +54,19 @@ export function normalizeBlock(
53
54
  > & {
54
55
  _key: string
55
56
  } {
57
+ const schema: PortableTextSchema = {
58
+ block: {
59
+ name: options.blockTypeName || 'block',
60
+ },
61
+ span: {
62
+ name: 'span',
63
+ },
64
+ styles: [],
65
+ lists: [],
66
+ decorators: [],
67
+ annotations: [],
68
+ }
69
+
56
70
  if (node._type !== (options.blockTypeName || 'block')) {
57
71
  return '_key' in node
58
72
  ? (node as TypedObject & {_key: string})
@@ -99,8 +113,8 @@ export function normalizeBlock(
99
113
  const previousChild = acc[acc.length - 1]
100
114
  if (
101
115
  previousChild &&
102
- isPortableTextSpan(child) &&
103
- isPortableTextSpan(previousChild) &&
116
+ isSpan(schema, child) &&
117
+ isSpan(schema, previousChild) &&
104
118
  isEqual(previousChild.marks, child.marks)
105
119
  ) {
106
120
  if (
@@ -129,7 +143,7 @@ export function normalizeBlock(
129
143
  ? options.keyGenerator()
130
144
  : keyGenerator()
131
145
 
132
- if (isPortableTextSpan(child)) {
146
+ if (isSpan(schema, child)) {
133
147
  if (!child.marks) {
134
148
  child.marks = []
135
149
  } else if (allowedDecorators) {