UNPKG

@portabletext/block-tools

Version:

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

184 lines (167 loc) 5.05 kB
import type {ArraySchemaType} from '@sanity/types' import { BLOCK_DEFAULT_STYLE, DEFAULT_BLOCK, DEFAULT_SPAN, HTML_BLOCK_TAGS, HTML_HEADER_TAGS, HTML_LIST_CONTAINER_TAGS, } from '../../constants' import type {BlockEnabledFeatures, DeserializerRule} from '../../types' import {isElement, tagName} from '../helpers' const LIST_CONTAINER_TAGS = Object.keys(HTML_LIST_CONTAINER_TAGS) // font-style:italic seems like the most important rule for italic / emphasis in their html function isEmphasis(el: Node): boolean { const style = isElement(el) && el.getAttribute('style') return /font-style\s*:\s*italic/.test(style || '') } // font-weight:700 seems like the most important rule for bold in their html function isStrong(el: Node): boolean { const style = isElement(el) && el.getAttribute('style') return /font-weight\s*:\s*700/.test(style || '') } // text-decoration seems like the most important rule for underline in their html function isUnderline(el: Node): boolean { if (!isElement(el) || tagName(el.parentNode) === 'a') { return false } const style = isElement(el) && el.getAttribute('style') return /text-decoration\s*:\s*underline/.test(style || '') } // text-decoration seems like the most important rule for strike-through in their html // allows for line-through regex to be more lineient to allow for other text-decoration before or after function isStrikethrough(el: Node): boolean { const style = isElement(el) && el.getAttribute('style') return /text-decoration\s*:\s*(?:.*line-through.*;)/.test(style || '') } // Check for attribute given by the gdocs preprocessor function isGoogleDocs(el: Node): boolean { return isElement(el) && Boolean(el.getAttribute('data-is-google-docs')) } function isRootNode(el: Node): boolean { return isElement(el) && Boolean(el.getAttribute('data-is-root-node')) } function getListItemStyle(el: Node): 'bullet' | 'number' | undefined { const parentTag = tagName(el.parentNode) if (parentTag && !LIST_CONTAINER_TAGS.includes(parentTag)) { return undefined } return tagName(el.parentNode) === 'ul' ? 'bullet' : 'number' } function getListItemLevel(el: Node): number { let level = 0 if (tagName(el) === 'li') { let parentNode = el.parentNode while (parentNode) { const parentTag = tagName(parentNode) if (parentTag && LIST_CONTAINER_TAGS.includes(parentTag)) { level++ } parentNode = parentNode.parentNode } } else { level = 1 } return level } const blocks: Record<string, {style: string} | undefined> = { ...HTML_BLOCK_TAGS, ...HTML_HEADER_TAGS, } function getBlockStyle(el: Node, enabledBlockStyles: string[]): string { const childTag = tagName(el.firstChild) const block = childTag && blocks[childTag] if (!block) { return BLOCK_DEFAULT_STYLE } if (!enabledBlockStyles.includes(block.style)) { return BLOCK_DEFAULT_STYLE } return block.style } export default function createGDocsRules( _blockContentType: ArraySchemaType, options: BlockEnabledFeatures, ): DeserializerRule[] { return [ { deserialize(el) { if (isElement(el) && tagName(el) === 'span' && isGoogleDocs(el)) { const span = { ...DEFAULT_SPAN, marks: [] as string[], text: el.textContent, } if (isStrong(el)) { span.marks.push('strong') } if (isUnderline(el)) { span.marks.push('underline') } if (isStrikethrough(el)) { span.marks.push('strike-through') } if (isEmphasis(el)) { span.marks.push('em') } return span } return undefined }, }, { deserialize(el, next) { if (tagName(el) === 'li' && isGoogleDocs(el)) { return { ...DEFAULT_BLOCK, listItem: getListItemStyle(el), level: getListItemLevel(el), style: getBlockStyle(el, options.enabledBlockStyles), children: next(el.firstChild?.childNodes || []), } } return undefined }, }, { deserialize(el) { if ( tagName(el) === 'br' && isGoogleDocs(el) && isElement(el) && el.classList.contains('apple-interchange-newline') ) { return { ...DEFAULT_SPAN, text: '', } } // BRs inside empty paragraphs if ( tagName(el) === 'br' && isGoogleDocs(el) && isElement(el) && el?.parentNode?.textContent === '' ) { return { ...DEFAULT_SPAN, text: '', } } // BRs on the root if ( tagName(el) === 'br' && isGoogleDocs(el) && isElement(el) && isRootNode(el) ) { return { ...DEFAULT_SPAN, text: '', } } return undefined }, }, ] }