UNPKG

@portabletext/block-tools

Version:

Can format HTML, Slate JSON or Sanity block array into any other format.

1 lines 90.8 kB
{"version":3,"file":"index.cjs","sources":["../src/util/findBlockType.ts","../src/util/resolveJsType.ts","../../../node_modules/.pnpm/@vercel+stega@0.1.2/node_modules/@vercel/stega/dist/index.mjs","../src/constants.ts","../src/util/blockContentTypeFeatures.ts","../src/HtmlDeserializer/preprocessors/xpathResult.ts","../src/HtmlDeserializer/preprocessors/gdocs.ts","../src/HtmlDeserializer/preprocessors/html.ts","../src/HtmlDeserializer/preprocessors/notion.ts","../src/HtmlDeserializer/preprocessors/whitespace.ts","../src/HtmlDeserializer/preprocessors/word.ts","../src/HtmlDeserializer/preprocessors/index.ts","../src/HtmlDeserializer/helpers.ts","../src/HtmlDeserializer/rules/gdocs.ts","../src/util/randomKey.ts","../src/HtmlDeserializer/rules/whitespace-text-node.ts","../src/HtmlDeserializer/rules/html.ts","../src/HtmlDeserializer/rules/notion.ts","../src/HtmlDeserializer/rules/word.ts","../src/HtmlDeserializer/rules/index.ts","../src/HtmlDeserializer/index.ts","../src/util/normalizeBlock.ts","../src/index.ts"],"sourcesContent":["import type {BlockSchemaType, SchemaType} from '@sanity/types'\n\nexport function findBlockType(type: SchemaType): type is BlockSchemaType {\n if (type.type) {\n return findBlockType(type.type)\n }\n\n if (type.name === 'block') {\n return true\n }\n\n return false\n}\n","const objectToString = Object.prototype.toString\n\n// Copied from https://github.com/ForbesLindesay/type-of\n// but inlined to have fine grained control\nexport function resolveJsType(val: unknown) {\n switch (objectToString.call(val)) {\n case '[object Function]':\n return 'function'\n case '[object Date]':\n return 'date'\n case '[object RegExp]':\n return 'regexp'\n case '[object Arguments]':\n return 'arguments'\n case '[object Array]':\n return 'array'\n case '[object String]':\n return 'string'\n default:\n }\n\n if (val === null) {\n return 'null'\n }\n\n if (val === undefined) {\n return 'undefined'\n }\n\n if (\n val &&\n typeof val === 'object' &&\n 'nodeType' in val &&\n (val as {nodeType: unknown}).nodeType === 1\n ) {\n return 'element'\n }\n\n if (val === Object(val)) {\n return 'object'\n }\n\n return typeof val\n}\n","var s={0:8203,1:8204,2:8205,3:8290,4:8291,5:8288,6:65279,7:8289,8:119155,9:119156,a:119157,b:119158,c:119159,d:119160,e:119161,f:119162},c={0:8203,1:8204,2:8205,3:65279},u=new Array(4).fill(String.fromCodePoint(c[0])).join(\"\"),m=String.fromCharCode(0);function E(t){let e=JSON.stringify(t);return`${u}${Array.from(e).map(r=>{let n=r.charCodeAt(0);if(n>255)throw new Error(`Only ASCII edit info can be encoded. Error attempting to encode ${e} on character ${r} (${n})`);return Array.from(n.toString(4).padStart(4,\"0\")).map(o=>String.fromCodePoint(c[o])).join(\"\")}).join(\"\")}`}function y(t){let e=JSON.stringify(t);return Array.from(e).map(r=>{let n=r.charCodeAt(0);if(n>255)throw new Error(`Only ASCII edit info can be encoded. Error attempting to encode ${e} on character ${r} (${n})`);return Array.from(n.toString(16).padStart(2,\"0\")).map(o=>String.fromCodePoint(s[o])).join(\"\")}).join(\"\")}function I(t){return!Number.isNaN(Number(t))||/[a-z]/i.test(t)&&!/\\d+(?:[-:\\/]\\d+){2}(?:T\\d+(?:[-:\\/]\\d+){1,2}(\\.\\d+)?Z?)?/.test(t)?!1:Boolean(Date.parse(t))}function T(t){try{new URL(t,t.startsWith(\"/\")?\"https://acme.com\":void 0)}catch{return!1}return!0}function C(t,e,r=\"auto\"){return r===!0||r===\"auto\"&&(I(t)||T(t))?t:`${t}${E(e)}`}var x=Object.fromEntries(Object.entries(c).map(t=>t.reverse())),g=Object.fromEntries(Object.entries(s).map(t=>t.reverse())),S=`${Object.values(s).map(t=>`\\\\u{${t.toString(16)}}`).join(\"\")}`,f=new RegExp(`[${S}]{4,}`,\"gu\");function G(t){let e=t.match(f);if(!!e)return h(e[0],!0)[0]}function $(t){let e=t.match(f);if(!!e)return e.map(r=>h(r)).flat()}function h(t,e=!1){let r=Array.from(t);if(r.length%2===0){if(r.length%4||!t.startsWith(u))return A(r,e)}else throw new Error(\"Encoded data has invalid length\");let n=[];for(let o=r.length*.25;o--;){let p=r.slice(o*4,o*4+4).map(d=>x[d.codePointAt(0)]).join(\"\");n.unshift(String.fromCharCode(parseInt(p,4)))}if(e){n.shift();let o=n.indexOf(m);return o===-1&&(o=n.length),[JSON.parse(n.slice(0,o).join(\"\"))]}return n.join(\"\").split(m).filter(Boolean).map(o=>JSON.parse(o))}function A(t,e){var d;let r=[];for(let i=t.length*.5;i--;){let a=`${g[t[i*2].codePointAt(0)]}${g[t[i*2+1].codePointAt(0)]}`;r.unshift(String.fromCharCode(parseInt(a,16)))}let n=[],o=[r.join(\"\")],p=10;for(;o.length;){let i=o.shift();try{if(n.push(JSON.parse(i)),e)return n}catch(a){if(!p--)throw a;let l=+((d=a.message.match(/\\sposition\\s(\\d+)$/))==null?void 0:d[1]);if(!l)throw a;o.unshift(i.substring(0,l),i.substring(l))}}return n}function _(t){var e;return{cleaned:t.replace(f,\"\"),encoded:((e=t.match(f))==null?void 0:e[0])||\"\"}}function O(t){return t&&JSON.parse(_(JSON.stringify(t)).cleaned)}export{f as VERCEL_STEGA_REGEX,y as legacyStegaEncode,O as vercelStegaClean,C as vercelStegaCombine,G as vercelStegaDecode,$ as vercelStegaDecodeAll,E as vercelStegaEncode,_ as vercelStegaSplit};\n","import {uniq} from 'lodash'\n\nexport interface PartialBlock {\n _type: string\n markDefs: string[]\n style: string\n level?: number\n listItem?: string\n}\n\nexport const PRESERVE_WHITESPACE_TAGS = ['pre', 'textarea', 'code']\n\nexport const BLOCK_DEFAULT_STYLE = 'normal'\n\nexport const DEFAULT_BLOCK: PartialBlock = Object.freeze({\n _type: 'block',\n markDefs: [],\n style: BLOCK_DEFAULT_STYLE,\n})\n\nexport const DEFAULT_SPAN = Object.freeze({\n _type: 'span',\n marks: [] as string[],\n})\n\nexport const HTML_BLOCK_TAGS = {\n p: DEFAULT_BLOCK,\n blockquote: {...DEFAULT_BLOCK, style: 'blockquote'} as PartialBlock,\n}\n\nexport const HTML_SPAN_TAGS = {\n span: {object: 'text'},\n}\n\nexport const HTML_LIST_CONTAINER_TAGS: Record<\n string,\n {object: null} | undefined\n> = {\n ol: {object: null},\n ul: {object: null},\n}\n\nexport const HTML_HEADER_TAGS: Record<string, PartialBlock | undefined> = {\n h1: {...DEFAULT_BLOCK, style: 'h1'},\n h2: {...DEFAULT_BLOCK, style: 'h2'},\n h3: {...DEFAULT_BLOCK, style: 'h3'},\n h4: {...DEFAULT_BLOCK, style: 'h4'},\n h5: {...DEFAULT_BLOCK, style: 'h5'},\n h6: {...DEFAULT_BLOCK, style: 'h6'},\n}\n\nexport const HTML_MISC_TAGS = {\n br: {...DEFAULT_BLOCK, style: BLOCK_DEFAULT_STYLE} as PartialBlock,\n}\n\nexport const HTML_DECORATOR_TAGS: Record<string, string | undefined> = {\n b: 'strong',\n strong: 'strong',\n\n i: 'em',\n em: 'em',\n\n u: 'underline',\n s: 'strike-through',\n strike: 'strike-through',\n del: 'strike-through',\n\n code: 'code',\n sup: 'sup',\n sub: 'sub',\n ins: 'ins',\n mark: 'mark',\n small: 'small',\n}\n\nexport const HTML_LIST_ITEM_TAGS: Record<string, PartialBlock | undefined> = {\n li: {\n ...DEFAULT_BLOCK,\n style: BLOCK_DEFAULT_STYLE,\n level: 1,\n listItem: 'bullet',\n },\n}\n\nexport const ELEMENT_MAP = {\n ...HTML_BLOCK_TAGS,\n ...HTML_SPAN_TAGS,\n ...HTML_LIST_CONTAINER_TAGS,\n ...HTML_LIST_ITEM_TAGS,\n ...HTML_HEADER_TAGS,\n ...HTML_MISC_TAGS,\n}\n\nexport const DEFAULT_SUPPORTED_STYLES = uniq(\n Object.values(ELEMENT_MAP)\n .filter((tag): tag is PartialBlock => 'style' in tag)\n .map((tag) => tag.style),\n)\n\nexport const DEFAULT_SUPPORTED_DECORATORS = uniq(\n Object.values(HTML_DECORATOR_TAGS),\n)\n\nexport const DEFAULT_SUPPORTED_ANNOTATIONS = ['link']\n","import {\n isBlockChildrenObjectField,\n isBlockListObjectField,\n isBlockSchemaType,\n isBlockStyleObjectField,\n isObjectSchemaType,\n isTitledListValue,\n type ArraySchemaType,\n type BlockSchemaType,\n type EnumListProps,\n type I18nTitledListValue,\n type ObjectSchemaType,\n type SpanSchemaType,\n type TitledListValue,\n} from '@sanity/types'\nimport type {BlockContentFeatures, ResolvedAnnotationType} from '../types'\nimport {findBlockType} from './findBlockType'\n\n// Helper method for describing a blockContentType's feature set\nexport default function blockContentFeatures(\n blockContentType: ArraySchemaType,\n): BlockContentFeatures {\n if (!blockContentType) {\n throw new Error(\"Parameter 'blockContentType' required\")\n }\n\n const blockType = blockContentType.of.find(findBlockType)\n if (!isBlockSchemaType(blockType)) {\n throw new Error(\"'block' type is not defined in this schema (required).\")\n }\n\n const ofType = blockType.fields.find(isBlockChildrenObjectField)?.type?.of\n if (!ofType) {\n throw new Error('No `of` declaration found for blocks `children` field')\n }\n\n const spanType = ofType.find(\n (member): member is SpanSchemaType => member.name === 'span',\n )\n if (!spanType) {\n throw new Error(\n 'No `span` type found in `block` schema type `children` definition',\n )\n }\n\n const inlineObjectTypes = ofType.filter(\n (inlineType): inlineType is ObjectSchemaType =>\n inlineType.name !== 'span' && isObjectSchemaType(inlineType),\n )\n\n const blockObjectTypes = blockContentType.of.filter(\n (memberType): memberType is ObjectSchemaType =>\n memberType.name !== blockType.name && isObjectSchemaType(memberType),\n )\n\n return {\n styles: resolveEnabledStyles(blockType),\n decorators: resolveEnabledDecorators(spanType),\n annotations: resolveEnabledAnnotationTypes(spanType),\n lists: resolveEnabledListItems(blockType),\n types: {\n block: blockContentType,\n span: spanType,\n inlineObjects: inlineObjectTypes,\n blockObjects: blockObjectTypes,\n },\n }\n}\n\nfunction resolveEnabledStyles(\n blockType: BlockSchemaType,\n): TitledListValue<string>[] {\n const styleField = blockType.fields.find(isBlockStyleObjectField)\n if (!styleField) {\n throw new Error(\n \"A field with name 'style' is not defined in the block type (required).\",\n )\n }\n\n const textStyles = getTitledListValuesFromEnumListOptions(\n styleField.type.options,\n )\n if (textStyles.length === 0) {\n throw new Error(\n 'The style fields need at least one style ' +\n \"defined. I.e: {title: 'Normal', value: 'normal'}.\",\n )\n }\n\n return textStyles\n}\n\nfunction resolveEnabledAnnotationTypes(\n spanType: SpanSchemaType,\n): ResolvedAnnotationType[] {\n return spanType.annotations.map((annotation) => ({\n title: annotation.title,\n type: annotation,\n value: annotation.name,\n icon: annotation.icon,\n }))\n}\n\nfunction resolveEnabledDecorators(\n spanType: SpanSchemaType,\n): TitledListValue<string>[] {\n return spanType.decorators\n}\n\nfunction resolveEnabledListItems(\n blockType: BlockSchemaType,\n): I18nTitledListValue<string>[] {\n const listField = blockType.fields.find(isBlockListObjectField)\n if (!listField) {\n throw new Error(\n \"A field with name 'list' is not defined in the block type (required).\",\n )\n }\n\n const listItems = getTitledListValuesFromEnumListOptions(\n listField.type.options,\n )\n if (!listItems) {\n throw new Error('The list field need at least to be an empty array')\n }\n\n return listItems\n}\n\nfunction getTitledListValuesFromEnumListOptions(\n options: EnumListProps<string> | undefined,\n): I18nTitledListValue<string>[] {\n const list = options ? options.list : undefined\n if (!Array.isArray(list)) {\n return []\n }\n\n return list.map((item) =>\n isTitledListValue(item) ? item : {title: item, value: item},\n )\n}\n","// We need this here if run server side\nexport const _XPathResult = {\n ANY_TYPE: 0,\n NUMBER_TYPE: 1,\n STRING_TYPE: 2,\n BOOLEAN_TYPE: 3,\n UNORDERED_NODE_ITERATOR_TYPE: 4,\n ORDERED_NODE_ITERATOR_TYPE: 5,\n UNORDERED_NODE_SNAPSHOT_TYPE: 6,\n ORDERED_NODE_SNAPSHOT_TYPE: 7,\n ANY_UNORDERED_NODE_TYPE: 8,\n FIRST_ORDERED_NODE_TYPE: 9,\n}\n","import type {HtmlPreprocessorOptions} from '../../types'\nimport {normalizeWhitespace, removeAllWhitespace, tagName} from '../helpers'\nimport {_XPathResult} from './xpathResult'\n\nexport default (\n _html: string,\n doc: Document,\n options: HtmlPreprocessorOptions,\n): Document => {\n const whitespaceOnPasteMode =\n options?.unstable_whitespaceOnPasteMode || 'preserve'\n let gDocsRootOrSiblingNode = doc\n .evaluate(\n '//*[@id and contains(@id, \"docs-internal-guid\")]',\n doc,\n null,\n _XPathResult.ORDERED_NODE_ITERATOR_TYPE,\n null,\n )\n .iterateNext()\n\n if (gDocsRootOrSiblingNode) {\n const isWrappedRootTag = tagName(gDocsRootOrSiblingNode) === 'b'\n\n // If this document isn't wrapped in a 'b' tag, then assume all siblings live on the root level\n if (!isWrappedRootTag) {\n gDocsRootOrSiblingNode = doc.body\n }\n\n switch (whitespaceOnPasteMode) {\n case 'normalize':\n // Keep only 1 empty block between content nodes\n normalizeWhitespace(gDocsRootOrSiblingNode)\n break\n case 'remove':\n // Remove all whitespace nodes\n removeAllWhitespace(gDocsRootOrSiblingNode)\n break\n default:\n break\n }\n\n // Tag every child with attribute 'is-google-docs' so that the GDocs rule-set can\n // work exclusivly on these children\n const childNodes = doc.evaluate(\n '//*',\n doc,\n null,\n _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n null,\n )\n\n for (let i = childNodes.snapshotLength - 1; i >= 0; i--) {\n const elm = childNodes.snapshotItem(i) as HTMLElement\n elm?.setAttribute('data-is-google-docs', 'true')\n\n if (\n elm?.parentElement === gDocsRootOrSiblingNode ||\n (!isWrappedRootTag && elm.parentElement === doc.body)\n ) {\n elm?.setAttribute('data-is-root-node', 'true')\n tagName(elm)\n }\n\n // Handle checkmark lists - The first child of a list item is an image with a checkmark, and the serializer\n // expects the first child to be the text node\n if (\n tagName(elm) === 'li' &&\n elm.firstChild &&\n tagName(elm?.firstChild) === 'img'\n ) {\n elm.removeChild(elm.firstChild)\n }\n }\n\n // Remove that 'b' which Google Docs wraps the HTML content in\n if (isWrappedRootTag) {\n doc.body.firstElementChild?.replaceWith(\n ...Array.from(gDocsRootOrSiblingNode.childNodes),\n )\n }\n\n return doc\n }\n return doc\n}\n","import {_XPathResult} from './xpathResult'\n\n// Remove this cruft from the document\nconst unwantedWordDocumentPaths = [\n '/html/text()',\n '/html/head/text()',\n '/html/body/text()',\n '/html/body/ul/text()',\n '/html/body/ol/text()',\n '//comment()',\n '//style',\n '//xml',\n '//script',\n '//meta',\n '//link',\n]\n\nexport default (_html: string, doc: Document): Document => {\n // Make sure text directly on the body is wrapped in spans.\n // This mimics what the browser does before putting html on the clipboard,\n // when used in a script context with JSDOM\n const bodyTextNodes = doc.evaluate(\n '/html/body/text()',\n doc,\n null,\n _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n null,\n )\n\n for (let i = bodyTextNodes.snapshotLength - 1; i >= 0; i--) {\n const node = bodyTextNodes.snapshotItem(i) as HTMLElement\n const text = node.textContent || ''\n if (text.replace(/[^\\S\\n]+$/g, '')) {\n const newNode = doc.createElement('span')\n newNode.appendChild(doc.createTextNode(text))\n node.parentNode?.replaceChild(newNode, node)\n } else {\n node.parentNode?.removeChild(node)\n }\n }\n\n const unwantedNodes = doc.evaluate(\n unwantedWordDocumentPaths.join('|'),\n doc,\n null,\n _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n null,\n )\n for (let i = unwantedNodes.snapshotLength - 1; i >= 0; i--) {\n const unwanted = unwantedNodes.snapshotItem(i)\n if (!unwanted) {\n continue\n }\n unwanted.parentNode?.removeChild(unwanted)\n }\n return doc\n}\n","import {_XPathResult} from './xpathResult'\n\nexport default (html: string, doc: Document): Document => {\n const NOTION_REGEX = /<!-- notionvc:.*?-->/g\n\n if (html.match(NOTION_REGEX)) {\n // Tag every child with attribute 'is-notion' so that the Notion rule-set can\n // work exclusivly on these children\n const childNodes = doc.evaluate(\n '//*',\n doc,\n null,\n _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n null,\n )\n\n for (let i = childNodes.snapshotLength - 1; i >= 0; i--) {\n const elm = childNodes.snapshotItem(i) as HTMLElement\n elm?.setAttribute('data-is-notion', 'true')\n }\n\n return doc\n }\n return doc\n}\n","import {PRESERVE_WHITESPACE_TAGS} from '../../constants'\nimport {_XPathResult} from './xpathResult'\n\nexport default (_: string, doc: Document): Document => {\n // Recursively process all nodes.\n function processNode(node: Node) {\n // If this is a text node and not inside a tag where whitespace should be preserved, process it.\n if (\n node.nodeType === _XPathResult.BOOLEAN_TYPE &&\n !PRESERVE_WHITESPACE_TAGS.includes(\n node.parentElement?.tagName.toLowerCase() || '',\n )\n ) {\n node.textContent =\n node.textContent\n ?.replace(/\\s\\s+/g, ' ') // Remove multiple whitespace\n .replace(/[\\r\\n]+/g, ' ') || '' // Replace newlines with spaces\n }\n // Otherwise, if this node has children, process them.\n else {\n for (let i = 0; i < node.childNodes.length; i++) {\n processNode(node.childNodes[i])\n }\n }\n }\n\n // Process all nodes starting from the root.\n processNode(doc.body)\n\n return doc\n}\n","import {_XPathResult} from './xpathResult'\n\nconst WORD_HTML_REGEX =\n /(class=\"?Mso|style=(?:\"|')[^\"]*?\\bmso-|w:WordDocument|<o:\\w+>|<\\/font>)/\n\n// xPaths for elements that will be removed from the document\nconst unwantedPaths = [\n '//o:p',\n \"//span[@style='mso-list:Ignore']\",\n \"//span[@style='mso-list: Ignore']\",\n]\n\n// xPaths for elements that needs to be remapped into other tags\nconst mappedPaths = [\n \"//p[@class='MsoTocHeading']\",\n \"//p[@class='MsoTitle']\",\n \"//p[@class='MsoToaHeading']\",\n \"//p[@class='MsoSubtitle']\",\n \"//span[@class='MsoSubtleEmphasis']\",\n \"//span[@class='MsoIntenseEmphasis']\",\n]\n\n// Which HTML element(s) to map the elements matching mappedPaths into\nconst elementMap: Record<string, string[] | undefined> = {\n MsoTocHeading: ['h3'],\n MsoTitle: ['h1'],\n MsoToaHeading: ['h2'],\n MsoSubtitle: ['h5'],\n MsoSubtleEmphasis: ['span', 'em'],\n MsoIntenseEmphasis: ['span', 'em', 'strong'],\n // Remove cruft\n}\n\nfunction isWordHtml(html: string) {\n return WORD_HTML_REGEX.test(html)\n}\n\nexport default (html: string, doc: Document): Document => {\n if (!isWordHtml(html)) {\n return doc\n }\n\n const unwantedNodes = doc.evaluate(\n unwantedPaths.join('|'),\n doc,\n (prefix) => {\n if (prefix === 'o') {\n return 'urn:schemas-microsoft-com:office:office'\n }\n return null\n },\n _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n null,\n )\n\n for (let i = unwantedNodes.snapshotLength - 1; i >= 0; i--) {\n const unwanted = unwantedNodes.snapshotItem(i)\n if (unwanted?.parentNode) {\n unwanted.parentNode.removeChild(unwanted)\n }\n }\n\n // Transform mapped elements into what they should be mapped to\n const mappedElements = doc.evaluate(\n mappedPaths.join('|'),\n doc,\n null,\n _XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,\n null,\n )\n for (let i = mappedElements.snapshotLength - 1; i >= 0; i--) {\n const mappedElm = mappedElements.snapshotItem(i) as HTMLElement\n const tags = elementMap[mappedElm.className]\n const text = doc.createTextNode(mappedElm.textContent || '')\n if (!tags) {\n continue\n }\n\n const parentElement = doc.createElement(tags[0])\n let parent = parentElement\n let child = parentElement\n tags.slice(1).forEach((tag) => {\n child = doc.createElement(tag)\n parent.appendChild(child)\n parent = child\n })\n child.appendChild(text)\n mappedElm?.parentNode?.replaceChild(parentElement, mappedElm)\n }\n\n return doc\n}\n","import preprocessGDocs from './gdocs'\nimport preprocessHTML from './html'\nimport preprocessNotion from './notion'\nimport preprocessWhitespace from './whitespace'\nimport preprocessWord from './word'\n\nexport default [\n preprocessWhitespace,\n preprocessNotion,\n preprocessWord,\n preprocessGDocs,\n preprocessHTML,\n]\n","import {\n isPortableTextTextBlock,\n type ArraySchemaType,\n type PortableTextTextBlock,\n} from '@sanity/types'\nimport {vercelStegaClean} from '@vercel/stega'\nimport {isEqual} from 'lodash'\nimport {DEFAULT_BLOCK} from '../constants'\nimport type {\n BlockEnabledFeatures,\n HtmlParser,\n HtmlPreprocessorOptions,\n MinimalBlock,\n MinimalSpan,\n PlaceholderAnnotation,\n PlaceholderDecorator,\n TypedObject,\n} from '../types'\nimport blockContentTypeFeatures from '../util/blockContentTypeFeatures'\nimport {resolveJsType} from '../util/resolveJsType'\nimport preprocessors from './preprocessors'\n\n/**\n * A utility function to create the options needed for the various rule sets,\n * based on the structure of the blockContentType\n *\n * @param blockContentType - Schema type for array containing _at least_ a block child type\n * @returns\n */\nexport function createRuleOptions(\n blockContentType: ArraySchemaType,\n): BlockEnabledFeatures {\n const features = blockContentTypeFeatures(blockContentType)\n const enabledBlockStyles = features.styles.map(\n (item) => item.value || item.title,\n )\n const enabledSpanDecorators = features.decorators.map(\n (item) => item.value || item.title,\n )\n const enabledBlockAnnotations = features.annotations.map(\n (item) => item.value || item.title || '',\n )\n const enabledListTypes = features.lists.map(\n (item) => item.value || item.title || '',\n )\n return {\n enabledBlockStyles,\n enabledSpanDecorators,\n enabledBlockAnnotations,\n enabledListTypes,\n }\n}\n\n/**\n * Utility function that always return a lowerCase version of the element.tagName\n *\n * @param el - Element to get tag name for\n * @returns Lowercase tagName for that element, or undefined if not an element\n */\nexport function tagName(el: HTMLElement | Node | null): string | undefined {\n if (el && 'tagName' in el) {\n return el.tagName.toLowerCase()\n }\n\n return undefined\n}\n\n// TODO: make this plugin-style\nexport function preprocess(\n html: string,\n parseHtml: HtmlParser,\n options: HtmlPreprocessorOptions,\n): Document {\n const cleanHTML = vercelStegaClean(html)\n const doc = parseHtml(normalizeHtmlBeforePreprocess(cleanHTML))\n preprocessors.forEach((processor) => {\n processor(cleanHTML, doc, options)\n })\n return doc\n}\n\nfunction normalizeHtmlBeforePreprocess(html: string): string {\n return html.trim()\n}\n\n/**\n * A default `parseHtml` function that returns the html using `DOMParser`.\n *\n * @returns HTML Parser based on `DOMParser`\n */\nexport function defaultParseHtml(): HtmlParser {\n if (resolveJsType(DOMParser) === 'undefined') {\n throw new Error(\n 'The native `DOMParser` global which the `Html` deserializer uses by ' +\n 'default is not present in this environment. ' +\n 'You must supply the `options.parseHtml` function instead.',\n )\n }\n return (html) => {\n return new DOMParser().parseFromString(html, 'text/html')\n }\n}\n\nexport function flattenNestedBlocks(blocks: TypedObject[]): TypedObject[] {\n let depth = 0\n const flattened: TypedObject[] = []\n const traverse = (nodes: TypedObject[]) => {\n const toRemove: TypedObject[] = []\n nodes.forEach((node) => {\n if (depth === 0) {\n flattened.push(node)\n }\n if (isPortableTextTextBlock(node)) {\n if (depth > 0) {\n toRemove.push(node)\n flattened.push(node)\n }\n depth++\n traverse(node.children)\n }\n if (node._type === '__block') {\n toRemove.push(node)\n flattened.push((node as any).block)\n }\n })\n toRemove.forEach((node) => {\n nodes.splice(nodes.indexOf(node), 1)\n })\n depth--\n }\n traverse(blocks)\n return flattened\n}\n\nfunction nextSpan(block: PortableTextTextBlock, index: number) {\n const next = block.children[index + 1]\n return next && next._type === 'span' ? next : null\n}\n\nfunction prevSpan(block: PortableTextTextBlock, index: number) {\n const prev = block.children[index - 1]\n return prev && prev._type === 'span' ? prev : null\n}\n\nfunction isWhiteSpaceChar(text: string) {\n return ['\\xa0', ' '].includes(text)\n}\n\n/**\n * NOTE: _mutates_ passed blocks!\n *\n * @param blocks - Array of blocks to trim whitespace for\n * @returns\n */\nexport function trimWhitespace(blocks: TypedObject[]): TypedObject[] {\n blocks.forEach((block) => {\n if (!isPortableTextTextBlock(block)) {\n return\n }\n\n // eslint-disable-next-line complexity\n block.children.forEach((child, index) => {\n if (!isMinimalSpan(child)) {\n return\n }\n const nextChild = nextSpan(block, index)\n const prevChild = prevSpan(block, index)\n if (index === 0) {\n child.text = child.text.replace(/^[^\\S\\n]+/g, '')\n }\n if (index === block.children.length - 1) {\n child.text = child.text.replace(/[^\\S\\n]+$/g, '')\n }\n if (\n /\\s/.test(child.text.slice(Math.max(0, child.text.length - 1))) &&\n nextChild &&\n isMinimalSpan(nextChild) &&\n /\\s/.test(nextChild.text.slice(0, 1))\n ) {\n child.text = child.text.replace(/[^\\S\\n]+$/g, '')\n }\n if (\n /\\s/.test(child.text.slice(0, 1)) &&\n prevChild &&\n isMinimalSpan(prevChild) &&\n /\\s/.test(prevChild.text.slice(Math.max(0, prevChild.text.length - 1)))\n ) {\n child.text = child.text.replace(/^[^\\S\\n]+/g, '')\n }\n if (!child.text) {\n block.children.splice(index, 1)\n }\n if (\n prevChild &&\n isEqual(prevChild.marks, child.marks) &&\n isWhiteSpaceChar(child.text)\n ) {\n prevChild.text += ' '\n block.children.splice(index, 1)\n } else if (\n nextChild &&\n isEqual(nextChild.marks, child.marks) &&\n isWhiteSpaceChar(child.text)\n ) {\n nextChild.text = ` ${nextChild.text}`\n block.children.splice(index, 1)\n }\n })\n })\n\n return blocks\n}\n\nexport function ensureRootIsBlocks(blocks: TypedObject[]): TypedObject[] {\n return blocks.reduce((memo, node, i, original) => {\n if (node._type === 'block') {\n memo.push(node)\n return memo\n }\n\n if (node._type === '__block') {\n memo.push((node as any).block)\n return memo\n }\n\n const lastBlock = memo[memo.length - 1]\n if (\n i > 0 &&\n !isPortableTextTextBlock(original[i - 1]) &&\n isPortableTextTextBlock<TypedObject>(lastBlock)\n ) {\n lastBlock.children.push(node)\n return memo\n }\n\n const block = {\n ...DEFAULT_BLOCK,\n children: [node],\n }\n\n memo.push(block)\n return memo\n }, [] as TypedObject[])\n}\n\nexport function isNodeList(node: unknown): node is NodeList {\n return Object.prototype.toString.call(node) === '[object NodeList]'\n}\n\nexport function isMinimalSpan(node: TypedObject): node is MinimalSpan {\n return node._type === 'span'\n}\n\nexport function isMinimalBlock(node: TypedObject): node is MinimalBlock {\n return node._type === 'block'\n}\n\nexport function isPlaceholderDecorator(\n node: TypedObject,\n): node is PlaceholderDecorator {\n return node._type === '__decorator'\n}\n\nexport function isPlaceholderAnnotation(\n node: TypedObject,\n): node is PlaceholderAnnotation {\n return node._type === '__annotation'\n}\n\nexport function isElement(node: Node): node is Element {\n return node.nodeType === 1\n}\n\n/**\n * Helper to normalize whitespace to only 1 empty block between content nodes\n * @param node - Root node to process\n */\nexport function normalizeWhitespace(rootNode: Node) {\n let emptyBlockCount = 0\n let lastParent = null\n const nodesToRemove: Node[] = []\n\n for (let child = rootNode.firstChild; child; child = child.nextSibling) {\n if (!isElement(child)) {\n normalizeWhitespace(child)\n emptyBlockCount = 0\n continue\n }\n\n const elm = child as HTMLElement\n\n if (isWhitespaceBlock(elm)) {\n if (lastParent && elm.parentElement === lastParent) {\n emptyBlockCount++\n if (emptyBlockCount > 1) {\n nodesToRemove.push(elm)\n }\n } else {\n // Different parent, reset counter\n emptyBlockCount = 1\n }\n\n lastParent = elm.parentElement\n } else {\n // Recurse into child nodes\n normalizeWhitespace(child)\n // Reset counter for siblings\n emptyBlockCount = 0\n }\n }\n\n // Remove marked nodes\n nodesToRemove.forEach((node) => node.parentElement?.removeChild(node))\n}\n\n/**\n * Helper to remove all whitespace nodes\n * @param node - Root node to process\n */\nexport function removeAllWhitespace(rootNode: Node) {\n const nodesToRemove: Node[] = []\n\n function collectNodesToRemove(currentNode: Node) {\n if (isElement(currentNode)) {\n const elm = currentNode as HTMLElement\n\n // Handle <br> tags that is between <p> tags\n if (\n tagName(elm) === 'br' &&\n (tagName(elm.nextElementSibling) === 'p' ||\n tagName(elm.previousElementSibling) === 'p')\n ) {\n nodesToRemove.push(elm)\n\n return\n }\n\n // Handle empty blocks\n if (\n (tagName(elm) === 'p' || tagName(elm) === 'br') &&\n elm?.firstChild?.textContent?.trim() === ''\n ) {\n nodesToRemove.push(elm)\n\n return\n }\n\n // Recursively process child nodes\n for (let child = elm.firstChild; child; child = child.nextSibling) {\n collectNodesToRemove(child)\n }\n }\n }\n\n collectNodesToRemove(rootNode)\n\n // Remove the collected nodes\n nodesToRemove.forEach((node) => node.parentElement?.removeChild(node))\n}\n\nfunction isWhitespaceBlock(elm: HTMLElement): boolean {\n return ['p', 'br'].includes(tagName(elm) || '') && !elm.textContent?.trim()\n}\n","import type {ArraySchemaType} from '@sanity/types'\nimport {\n BLOCK_DEFAULT_STYLE,\n DEFAULT_BLOCK,\n DEFAULT_SPAN,\n HTML_BLOCK_TAGS,\n HTML_HEADER_TAGS,\n HTML_LIST_CONTAINER_TAGS,\n} from '../../constants'\nimport type {BlockEnabledFeatures, DeserializerRule} from '../../types'\nimport {isElement, tagName} from '../helpers'\n\nconst LIST_CONTAINER_TAGS = Object.keys(HTML_LIST_CONTAINER_TAGS)\n\n// font-style:italic seems like the most important rule for italic / emphasis in their html\nfunction isEmphasis(el: Node): boolean {\n const style = isElement(el) && el.getAttribute('style')\n return /font-style\\s*:\\s*italic/.test(style || '')\n}\n\n// font-weight:700 seems like the most important rule for bold in their html\nfunction isStrong(el: Node): boolean {\n const style = isElement(el) && el.getAttribute('style')\n return /font-weight\\s*:\\s*700/.test(style || '')\n}\n\n// text-decoration seems like the most important rule for underline in their html\nfunction isUnderline(el: Node): boolean {\n if (!isElement(el) || tagName(el.parentNode) === 'a') {\n return false\n }\n\n const style = isElement(el) && el.getAttribute('style')\n\n return /text-decoration\\s*:\\s*underline/.test(style || '')\n}\n\n// text-decoration seems like the most important rule for strike-through in their html\n// allows for line-through regex to be more lineient to allow for other text-decoration before or after\nfunction isStrikethrough(el: Node): boolean {\n const style = isElement(el) && el.getAttribute('style')\n return /text-decoration\\s*:\\s*(?:.*line-through.*;)/.test(style || '')\n}\n\n// Check for attribute given by the gdocs preprocessor\nfunction isGoogleDocs(el: Node): boolean {\n return isElement(el) && Boolean(el.getAttribute('data-is-google-docs'))\n}\n\nfunction isRootNode(el: Node): boolean {\n return isElement(el) && Boolean(el.getAttribute('data-is-root-node'))\n}\n\nfunction getListItemStyle(el: Node): 'bullet' | 'number' | undefined {\n const parentTag = tagName(el.parentNode)\n if (parentTag && !LIST_CONTAINER_TAGS.includes(parentTag)) {\n return undefined\n }\n return tagName(el.parentNode) === 'ul' ? 'bullet' : 'number'\n}\n\nfunction getListItemLevel(el: Node): number {\n let level = 0\n if (tagName(el) === 'li') {\n let parentNode = el.parentNode\n while (parentNode) {\n const parentTag = tagName(parentNode)\n if (parentTag && LIST_CONTAINER_TAGS.includes(parentTag)) {\n level++\n }\n parentNode = parentNode.parentNode\n }\n } else {\n level = 1\n }\n return level\n}\n\nconst blocks: Record<string, {style: string} | undefined> = {\n ...HTML_BLOCK_TAGS,\n ...HTML_HEADER_TAGS,\n}\n\nfunction getBlockStyle(el: Node, enabledBlockStyles: string[]): string {\n const childTag = tagName(el.firstChild)\n const block = childTag && blocks[childTag]\n if (!block) {\n return BLOCK_DEFAULT_STYLE\n }\n if (!enabledBlockStyles.includes(block.style)) {\n return BLOCK_DEFAULT_STYLE\n }\n return block.style\n}\n\nexport default function createGDocsRules(\n _blockContentType: ArraySchemaType,\n options: BlockEnabledFeatures,\n): DeserializerRule[] {\n return [\n {\n deserialize(el) {\n if (isElement(el) && tagName(el) === 'span' && isGoogleDocs(el)) {\n const span = {\n ...DEFAULT_SPAN,\n marks: [] as string[],\n text: el.textContent,\n }\n if (isStrong(el)) {\n span.marks.push('strong')\n }\n if (isUnderline(el)) {\n span.marks.push('underline')\n }\n if (isStrikethrough(el)) {\n span.marks.push('strike-through')\n }\n if (isEmphasis(el)) {\n span.marks.push('em')\n }\n return span\n }\n return undefined\n },\n },\n {\n deserialize(el, next) {\n if (tagName(el) === 'li' && isGoogleDocs(el)) {\n return {\n ...DEFAULT_BLOCK,\n listItem: getListItemStyle(el),\n level: getListItemLevel(el),\n style: getBlockStyle(el, options.enabledBlockStyles),\n children: next(el.firstChild?.childNodes || []),\n }\n }\n return undefined\n },\n },\n {\n deserialize(el) {\n if (\n tagName(el) === 'br' &&\n isGoogleDocs(el) &&\n isElement(el) &&\n el.classList.contains('apple-interchange-newline')\n ) {\n return {\n ...DEFAULT_SPAN,\n text: '',\n }\n }\n\n // BRs inside empty paragraphs\n if (\n tagName(el) === 'br' &&\n isGoogleDocs(el) &&\n isElement(el) &&\n el?.parentNode?.textContent === ''\n ) {\n return {\n ...DEFAULT_SPAN,\n text: '',\n }\n }\n\n // BRs on the root\n if (\n tagName(el) === 'br' &&\n isGoogleDocs(el) &&\n isElement(el) &&\n isRootNode(el)\n ) {\n return {\n ...DEFAULT_SPAN,\n text: '',\n }\n }\n return undefined\n },\n },\n ]\n}\n","import getRandomValues from 'get-random-values-esm'\n\nexport function keyGenerator() {\n return randomKey(12)\n}\n\n// WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html\nfunction whatwgRNG(length = 16) {\n const rnds8 = new Uint8Array(length)\n getRandomValues(rnds8)\n return rnds8\n}\n\nconst byteToHex: string[] = []\nfor (let i = 0; i < 256; ++i) {\n byteToHex[i] = (i + 0x100).toString(16).slice(1)\n}\n\n/**\n * Generate a random key of the given length\n *\n * @param length - Length of string to generate\n * @returns A string of the given length\n * @public\n */\nexport function randomKey(length: number): string {\n return whatwgRNG(length)\n .reduce((str, n) => str + byteToHex[n], '')\n .slice(0, length)\n}\n","import {DEFAULT_SPAN} from '../../constants'\nimport type {DeserializerRule} from '../../types'\nimport {tagName} from '../helpers'\n\nexport const whitespaceTextNodeRule: DeserializerRule = {\n deserialize(node) {\n return node.nodeName === '#text' && isWhitespaceTextNode(node)\n ? {\n ...DEFAULT_SPAN,\n marks: [],\n text: (node.textContent ?? '').replace(/\\s\\s+/g, ' '),\n }\n : undefined\n },\n}\n\nfunction isWhitespaceTextNode(node: Node) {\n const isValidWhiteSpace =\n node.nodeType === 3 &&\n (node.textContent || '').replace(/[\\r\\n]/g, ' ').replace(/\\s\\s+/g, ' ') ===\n ' ' &&\n node.nextSibling &&\n node.nextSibling.nodeType !== 3 &&\n node.previousSibling &&\n node.previousSibling.nodeType !== 3\n\n return (\n (isValidWhiteSpace || node.textContent !== ' ') &&\n tagName(node.parentNode) !== 'body'\n )\n}\n","import type {ArraySchemaType, TypedObject} from '@sanity/types'\nimport {\n DEFAULT_BLOCK,\n DEFAULT_SPAN,\n HTML_BLOCK_TAGS,\n HTML_DECORATOR_TAGS,\n HTML_HEADER_TAGS,\n HTML_LIST_CONTAINER_TAGS,\n HTML_LIST_ITEM_TAGS,\n HTML_SPAN_TAGS,\n type PartialBlock,\n} from '../../constants'\nimport type {BlockEnabledFeatures, DeserializerRule} from '../../types'\nimport {keyGenerator} from '../../util/randomKey'\nimport {isElement, tagName} from '../helpers'\nimport {whitespaceTextNodeRule} from './whitespace-text-node'\n\nexport function resolveListItem(\n listNodeTagName: string,\n enabledListTypes: string[],\n): string | undefined {\n if (listNodeTagName === 'ul' && enabledListTypes.includes('bullet')) {\n return 'bullet'\n }\n if (listNodeTagName === 'ol' && enabledListTypes.includes('number')) {\n return 'number'\n }\n return undefined\n}\n\nexport default function createHTMLRules(\n _blockContentType: ArraySchemaType,\n options: BlockEnabledFeatures & {keyGenerator?: () => string},\n): DeserializerRule[] {\n return [\n whitespaceTextNodeRule,\n {\n // Pre element\n deserialize(el) {\n if (tagName(el) !== 'pre') {\n return undefined\n }\n\n const isCodeEnabled = options.enabledBlockStyles.includes('code')\n\n return {\n _type: 'block',\n style: 'normal',\n markDefs: [],\n children: [\n {\n ...DEFAULT_SPAN,\n marks: isCodeEnabled ? ['code'] : [],\n text: el.textContent || '',\n },\n ],\n }\n },\n }, // Blockquote element\n {\n deserialize(el, next) {\n if (tagName(el) !== 'blockquote') {\n return undefined\n }\n const blocks: Record<string, PartialBlock | undefined> = {\n ...HTML_BLOCK_TAGS,\n ...HTML_HEADER_TAGS,\n }\n delete blocks.blockquote\n const nonBlockquoteBlocks = Object.keys(blocks)\n\n const children: HTMLElement[] = []\n\n el.childNodes.forEach((node, index) => {\n if (!el.ownerDocument) {\n return\n }\n\n if (\n node.nodeType === 1 &&\n nonBlockquoteBlocks.includes(\n (node as Element).localName.toLowerCase(),\n )\n ) {\n const span = el.ownerDocument.createElement('span')\n\n const previousChild = children[children.length - 1]\n\n if (\n previousChild &&\n previousChild.nodeType === 3 &&\n previousChild.textContent?.trim()\n ) {\n // Only prepend line break if the previous node is a non-empty\n // text node.\n span.appendChild(el.ownerDocument.createTextNode('\\r'))\n }\n\n node.childNodes.forEach((cn) => {\n span.appendChild(cn.cloneNode(true))\n })\n\n if (index !== el.childNodes.length) {\n // Only append line break if this is not the last child\n span.appendChild(el.ownerDocument.createTextNode('\\r'))\n }\n\n children.push(span)\n } else {\n children.push(node as HTMLElement)\n }\n })\n\n return {\n _type: 'block',\n style: 'blockquote',\n markDefs: [],\n children: next(children),\n }\n },\n }, // Block elements\n {\n deserialize(el, next) {\n const blocks: Record<string, PartialBlock | undefined> = {\n ...HTML_BLOCK_TAGS,\n ...HTML_HEADER_TAGS,\n }\n const tag = tagName(el)\n let block = tag ? blocks[tag] : undefined\n if (!block) {\n return undefined\n }\n // Don't add blocks into list items\n if (el.parentNode && tagName(el.parentNode) === 'li') {\n return next(el.childNodes)\n }\n // If style is not supported, return a defaultBlockType\n if (!options.enabledBlockStyles.includes(block.style)) {\n block = DEFAULT_BLOCK\n }\n return {\n ...block,\n children: next(el.childNodes),\n }\n },\n }, // Ignore span tags\n {\n deserialize(el, next) {\n const tag = tagName(el)\n if (!tag || !(tag in HTML_SPAN_TAGS)) {\n return undefined\n }\n return next(el.childNodes)\n },\n }, // Ignore div tags\n {\n deserialize(el, next) {\n const div = tagName(el) === 'div'\n if (!div) {\n return undefined\n }\n return next(el.childNodes)\n },\n }, // Ignore list containers\n {\n deserialize(el, next) {\n const tag = tagName(el)\n if (!tag || !(tag in HTML_LIST_CONTAINER_TAGS)) {\n return undefined\n }\n return next(el.childNodes)\n },\n }, // Deal with br's\n {\n deserialize(el) {\n if (tagName(el) === 'br') {\n return {\n ...DEFAULT_SPAN,\n text: '\\n',\n }\n }\n return undefined\n },\n }, // Deal with list items\n {\n deserialize(el, next, block) {\n const tag = tagName(el)\n const listItem = tag ? HTML_LIST_ITEM_TAGS[tag] : undefined\n const parentTag = tagName(el.parentNode) || ''\n if (\n !listItem ||\n !el.parentNode ||\n !HTML_LIST_CONTAINER_TAGS[parentTag]\n ) {\n return undefined\n }\n const enabledListItem = resolveListItem(\n parentTag,\n options.enabledListTypes,\n )\n // If the list item style is not supported, return a new default block\n if (!enabledListItem) {\n return block({_type: 'block', children: next(el.childNodes)})\n }\n listItem.listItem = enabledListItem\n return {\n ...listItem,\n children: next(el.childNodes),\n }\n },\n }, // Deal with decorators - this is a limited set of known html elements that we know how to deserialize\n {\n deserialize(el, next) {\n const decorator = HTML_DECORATOR_TAGS[tagName(el) || '']\n if (!decorator || !options.enabledSpanDecorators.includes(decorator)) {\n return undefined\n }\n return {\n _type: '__decorator',\n name: decorator,\n children: next(el.childNodes),\n }\n },\n }, // Special case for hyperlinks, add annotation (if allowed by schema),\n // If not supported just write out the link text and href in plain text.\n {\n deserialize(el, next) {\n if (tagName(el) !== 'a') {\n return undefined\n }\n const linkEnabled = options.enabledBlockAnnotations.includes('link')\n const href = isElement(el) && el.getAttribute('href')\n if (!href) {\n return next(el.childNodes)\n }\n let markDef: TypedObject | undefined\n if (linkEnabled) {\n markDef = {\n _key: options.keyGenerator\n ? options.keyGenerator()\n : keyGenerator(),\n _type: 'link',\n href: href,\n }\n return {\n _type: '__annotation',\n markDef: markDef,\n children: next(el.childNodes),\n }\n }\n return (\n el.appendChild(el.ownerDocument.createTextNode(` (${href})`)) &&\n next(el.childNodes)\n )\n },\n },\n ]\n}\n","import type {ArraySchemaType} from '@sanity/types'\nimport {DEFAULT_SPAN} from '../../constants'\nimport type {DeserializerRule} from '../../types'\nimport {isElement, tagName} from '../helpers'\n\n// font-style:italic seems like the most important rule for italic / emphasis in their html\nfunction isEmphasis(el: Node): boolean {\n const style = isElement(el) && el.getAttribute('style')\n return /font-style:italic/.test(style || '')\n}\n\n// font-weight:700 or 600 seems like the most important rule for bold in their html\nfunction isStrong(el: Node): boolean {\n const style = isElement(el) && el.getAttribute('style')\n return (\n /font-weight:700/.test(style || '') || /font-weight:600/.test(style || '')\n )\n}\n\n// text-decoration seems like the most important rule for underline in their html\nfunction isUnderline(el: Node): boolean {\n const style = isElement(el) && el.getAttribute('style')\n return /text-decoration:underline/.test(style || '')\n}\n\n// Check for attribute given by the Notion preprocessor\nfunction isNotion(el: Node): boolean {\n return isElement(el) && Boolean(el.getAttribute('data-is-notion'))\n}\n\nexport default function createNotionRules(\n _blockContentType: ArraySchemaType,\n): DeserializerRule[] {\n return [\n {\n deserialize(el) {\n // Notion normally exports semantic HTML. However, if you copy a single block, the formatting will be inline styles\n // This handles a limited set of styles\n if (isElement(el) && tagName(el) === 'span' && isNotion(el)) {\n const span = {\n ...DEFAULT_SPAN,\n marks: [] as string[],\n text: el.textContent,\n }\n if (isStrong(el)) {\n span.marks.push('strong')\n }\n if (isUnderline(el)) {\n span.marks.push('underline')\n }\n if (isEmphasis(el)) {\n span.marks.push('em')\n }\n return span\n }\n return undefined\n },\n },\n ]\n}\n","import {BLOCK_DEFAULT_STYLE, DEFAULT_BLOCK} from '../../constants'\nimport type {DeserializerRule} from '../../types'\nimport {isElement, tagName} from '../helpers'\n\nfunction getListItemStyle(el: Node): string | undefined {\n const style = isElement(el) && el.getAttribute('style')\n if (!style) {\n return undefined\n }\n\n if (!style.match(/lfo\\d+/)) {\n return undefined\n }\n\n return style.match('lfo1') ? 'bullet' : 'number'\n}\n\nfunction getListItemLevel(el: Node): number | undefined {\n const style = isElement(el) && el.getAttribute('style')\n if (!style) {\n return undefined\n }\n\n const levelMatch = style.match(/level\\d+/)\n if (!levelMatch) {\n return undefined\n }\n\n const [level] = levelMatch[0].match(/\\d/) || []\n const levelNum = level ? Number.parseInt(level, 10) : 1\n return levelNum || 1\n}\n\nfunction isWordListElement(el: Node): boolean {\n return isElement(el) && el.className\n ? el.className === 'MsoListParagraphCxSpFirst' ||\n el.className === 'MsoListParagraphCxSpMiddle' ||\n el.className === 'MsoListParagraphCxSpLast'\n : false\n}\n\nexport default function createWordRules(): DeserializerRule[] {\n return [\n {\n deserialize(el, next) {\n if (tagName(el) === 'p' && isWordListElement(el)) {\n return {\n ...DEFAULT_BLOCK,\n listItem: getListItemStyle(el),\n level: getListItemLevel(el),\n style: BLOCK_DEFAULT_STYLE,\n children: next(el.childNodes),\n }\n }\n return undefined\n },\n },\n ]\n}\n","import type {ArraySchemaType} from '@sanity/types'\nimport type {BlockEnabledFeatures, DeserializerRule} from '../../types'\nimport createGDocsRules from './gdocs'\nimport createHTMLRules from './html'\nimport createNotionRules from './notion'\nimport createWordRules from './word'\n\nexport function createRules(\n blockContentType: ArraySchemaType,\n options: BlockEnabledFeatures & {keyGenerator?: () => string},\n): DeserializerRule[] {\n return [\n ...createWordRules(),\n ...createNotionRules(blockContentType),\n ...createGDocsRules(blockContentType, options),\n ...createHTMLRules(blockContentType, options),\n ]\n}\n","import type {\n ArraySchemaType,\n PortableTextBlock,\n PortableTextObject,\n PortableTextTextBlock,\n} from '@sanity/types'\nimport {flatten} from 'lodash'\nimport type {\n ArbitraryTypedObject,\n DeserializerRule,\n HtmlDeserializerOptions,\n PlaceholderAnnotation,\n PlaceholderDecorator,\n TypedObject,\n} from '../types'\nimport {findBlockType} from '../util/findBlockType'\nimport {resolveJsType} from '../util/resolveJsType'\nimport {\n createRuleOptions,\n defaultParseHtml,\n ensureRootIsBlocks,\n flattenNestedBlocks,\n isMinimalBlock,\n isMinimalSpan,\n isNodeList,\n