@portabletext/block-tools 2.0.8 → 3.1.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,4 @@
1
- import type {
2
- ArraySchemaType,
3
- PortableTextBlock,
4
- PortableTextObject,
5
- PortableTextTextBlock,
6
- } from '@sanity/types'
1
+ import type {Schema} from '@portabletext/schema'
7
2
  import {flatten} from 'lodash'
8
3
  import type {
9
4
  ArbitraryTypedObject,
@@ -13,10 +8,13 @@ import type {
13
8
  PlaceholderDecorator,
14
9
  TypedObject,
15
10
  } from '../types'
16
- import {findBlockType} from '../util/findBlockType'
11
+ import {
12
+ isTextBlock,
13
+ type PortableTextBlock,
14
+ type PortableTextObject,
15
+ } from '../types.portable-text'
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: Schema
40
38
  rules: DeserializerRule[]
41
39
  parseHtml: (html: string) => HTMLElement
42
40
  _markDefs: PortableTextObject[] = []
@@ -47,21 +45,14 @@ export default class HtmlDeserializer {
47
45
  * @param blockContentType - Schema type for array containing _at least_ a block child type
48
46
  * @param options - Options for the deserialization process
49
47
  */
50
- constructor(
51
- blockContentType: ArraySchemaType,
52
- options: HtmlDeserializerOptions = {},
53
- ) {
48
+ constructor(schema: Schema, options: HtmlDeserializerOptions = {}) {
54
49
  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),
50
+ const standardRules = createRules(schema, {
60
51
  keyGenerator: options.keyGenerator,
61
52
  })
53
+ this.schema = schema
62
54
  this.rules = [...rules, ...standardRules]
63
55
  const parseHtml = options.parseHtml || defaultParseHtml()
64
- this.blockContentType = blockContentType
65
56
  this.parseHtml = (html) => {
66
57
  const doc = preprocess(html, parseHtml, {unstable_whitespaceOnPasteMode})
67
58
  return doc.body
@@ -81,16 +72,16 @@ export default class HtmlDeserializer {
81
72
  const children = Array.from(fragment.childNodes) as HTMLElement[]
82
73
  // Ensure that there are no blocks within blocks, and trim whitespace
83
74
  const blocks = trimWhitespace(
75
+ this.schema,
84
76
  flattenNestedBlocks(
85
- ensureRootIsBlocks(this.deserializeElements(children)),
77
+ this.schema,
78
+ ensureRootIsBlocks(this.schema, this.deserializeElements(children)),
86
79
  ),
87
80
  )
88
81
 
89
82
  if (this._markDefs.length > 0) {
90
83
  blocks
91
- .filter(
92
- (block): block is PortableTextTextBlock => block._type === 'block',
93
- )
84
+ .filter((block) => isTextBlock(this.schema, block))
94
85
  .forEach((block) => {
95
86
  block.markDefs = block.markDefs || []
96
87
  block.markDefs = block.markDefs.concat(
@@ -103,15 +94,9 @@ export default class HtmlDeserializer {
103
94
  })
104
95
  }
105
96
 
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
97
  return blocks.map((block) => {
113
98
  if (block._type === 'block') {
114
- block._type = type.name
99
+ block._type = this.schema.block.name
115
100
  }
116
101
  return block
117
102
  })
@@ -1,4 +1,4 @@
1
- import type {ArraySchemaType} from '@sanity/types'
1
+ import type {Schema} from '@portabletext/schema'
2
2
  import {
3
3
  BLOCK_DEFAULT_STYLE,
4
4
  DEFAULT_BLOCK,
@@ -7,7 +7,7 @@ import {
7
7
  HTML_HEADER_TAGS,
8
8
  HTML_LIST_CONTAINER_TAGS,
9
9
  } from '../../constants'
10
- import type {BlockEnabledFeatures, DeserializerRule} from '../../types'
10
+ import type {DeserializerRule} from '../../types'
11
11
  import {isElement, tagName} from '../helpers'
12
12
 
13
13
  const LIST_CONTAINER_TAGS = Object.keys(HTML_LIST_CONTAINER_TAGS)
@@ -81,22 +81,19 @@ 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: Schema, 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
- export default function createGDocsRules(
97
- _blockContentType: ArraySchemaType,
98
- options: BlockEnabledFeatures,
99
- ): DeserializerRule[] {
96
+ export default function createGDocsRules(schema: Schema): DeserializerRule[] {
100
97
  return [
101
98
  {
102
99
  deserialize(el) {
@@ -130,7 +127,7 @@ export default function createGDocsRules(
130
127
  ...DEFAULT_BLOCK,
131
128
  listItem: getListItemStyle(el),
132
129
  level: getListItemLevel(el),
133
- style: getBlockStyle(el, options.enabledBlockStyles),
130
+ style: getBlockStyle(schema, el),
134
131
  children: next(el.firstChild?.childNodes || []),
135
132
  }
136
133
  }
@@ -1,4 +1,4 @@
1
- import type {ArraySchemaType, TypedObject} from '@sanity/types'
1
+ import type {Schema} from '@portabletext/schema'
2
2
  import {
3
3
  DEFAULT_BLOCK,
4
4
  DEFAULT_SPAN,
@@ -10,27 +10,33 @@ import {
10
10
  HTML_SPAN_TAGS,
11
11
  type PartialBlock,
12
12
  } from '../../constants'
13
- import type {BlockEnabledFeatures, DeserializerRule} from '../../types'
13
+ import type {DeserializerRule} from '../../types'
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: Schema,
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: Schema,
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 {Schema} from '@portabletext/schema'
2
+ import type {DeserializerRule} from '../../types'
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: Schema,
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,54 +1,49 @@
1
- import type {ArraySchemaType, PortableTextTextBlock} from '@sanity/types'
1
+ import {sanitySchemaToPortableTextSchema} from '@portabletext/sanity-bridge'
2
+ import type {Schema} from '@portabletext/schema'
3
+ import type {ArraySchemaType} from '@sanity/types'
2
4
  import HtmlDeserializer from './HtmlDeserializer'
3
- import type {
4
- BlockContentFeatures,
5
- HtmlDeserializerOptions,
6
- TypedObject,
7
- } from './types'
8
- import blockContentTypeFeatures from './util/blockContentTypeFeatures'
5
+ import type {HtmlDeserializerOptions, TypedObject} from './types'
6
+ import type {PortableTextTextBlock} from './types.portable-text'
9
7
  import {normalizeBlock} from './util/normalizeBlock'
10
8
 
11
9
  /**
12
10
  * Convert HTML to blocks respecting the block content type's schema
13
11
  *
14
12
  * @param html - The HTML to convert to blocks
15
- * @param blockContentType - A compiled version of the schema type for the block content
13
+ * @param schemaType - A compiled version of the schema type for the block content
16
14
  * @param options - Options for deserializing HTML to blocks
17
15
  * @returns Array of blocks
18
16
  * @public
19
17
  */
20
18
  export function htmlToBlocks(
21
19
  html: string,
22
- blockContentType: ArraySchemaType,
20
+ schemaType: ArraySchemaType | Schema,
23
21
  options: HtmlDeserializerOptions = {},
24
22
  ): (TypedObject | PortableTextTextBlock)[] {
25
- const deserializer = new HtmlDeserializer(blockContentType, options)
23
+ const schema = isSanitySchema(schemaType)
24
+ ? sanitySchemaToPortableTextSchema(schemaType)
25
+ : schemaType
26
+
27
+ const deserializer = new HtmlDeserializer(schema, options)
26
28
  return deserializer
27
29
  .deserialize(html)
28
30
  .map((block) => normalizeBlock(block, {keyGenerator: options.keyGenerator}))
29
31
  }
30
32
 
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
-
33
+ export type {ArbitraryTypedObject, DeserializerRule, HtmlParser} from './types'
44
34
  export type {
45
- ArbitraryTypedObject,
46
- BlockEditorSchemaProps,
47
- DeserializerRule,
48
- HtmlParser,
49
- ResolvedAnnotationType,
50
- } from './types'
35
+ PortableTextBlock,
36
+ PortableTextObject,
37
+ PortableTextSpan,
38
+ PortableTextTextBlock,
39
+ } from './types.portable-text'
51
40
  export type {BlockNormalizationOptions} from './util/normalizeBlock'
52
41
  export {randomKey} from './util/randomKey'
53
42
  export {normalizeBlock}
54
- export type {BlockContentFeatures, HtmlDeserializerOptions, TypedObject}
43
+ export type {HtmlDeserializerOptions, TypedObject}
44
+
45
+ function isSanitySchema(
46
+ schema: ArraySchemaType | Schema,
47
+ ): schema is ArraySchemaType {
48
+ return schema.hasOwnProperty('jsonType')
49
+ }
@@ -0,0 +1,79 @@
1
+ import type {Schema} from '@portabletext/schema'
2
+ import {isArbitraryTypedObject} from './types'
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: Schema,
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: Schema,
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 type {Schema} from '@portabletext/schema'
2
+ import {isEqual} from 'lodash'
3
+ import type {TypedObject} from '../types'
1
4
  import {
2
- isPortableTextSpan,
5
+ isSpan,
3
6
  type PortableTextSpan,
4
7
  type PortableTextTextBlock,
5
- } from '@sanity/types'
6
- import {isEqual} from 'lodash'
7
- import type {TypedObject} from '../types'
8
+ } from '../types.portable-text'
8
9
  import {keyGenerator} from './randomKey'
9
10
 
10
11
  /**
@@ -53,6 +54,21 @@ export function normalizeBlock(
53
54
  > & {
54
55
  _key: string
55
56
  } {
57
+ const schema: Schema = {
58
+ block: {
59
+ name: options.blockTypeName || 'block',
60
+ },
61
+ span: {
62
+ name: 'span',
63
+ },
64
+ styles: [],
65
+ lists: [],
66
+ decorators: [],
67
+ annotations: [],
68
+ blockObjects: [],
69
+ inlineObjects: [],
70
+ }
71
+
56
72
  if (node._type !== (options.blockTypeName || 'block')) {
57
73
  return '_key' in node
58
74
  ? (node as TypedObject & {_key: string})
@@ -99,8 +115,8 @@ export function normalizeBlock(
99
115
  const previousChild = acc[acc.length - 1]
100
116
  if (
101
117
  previousChild &&
102
- isPortableTextSpan(child) &&
103
- isPortableTextSpan(previousChild) &&
118
+ isSpan(schema, child) &&
119
+ isSpan(schema, previousChild) &&
104
120
  isEqual(previousChild.marks, child.marks)
105
121
  ) {
106
122
  if (
@@ -129,7 +145,7 @@ export function normalizeBlock(
129
145
  ? options.keyGenerator()
130
146
  : keyGenerator()
131
147
 
132
- if (isPortableTextSpan(child)) {
148
+ if (isSpan(schema, child)) {
133
149
  if (!child.marks) {
134
150
  child.marks = []
135
151
  } else if (allowedDecorators) {