import * as React from 'react' import { getBlockIcon, getTextContent, getPageTableOfContents, getBlockParentPage, uuidToId, getBlockCollectionId } from 'notion-utils' import * as types from 'notion-types' import { PageIcon } from './components/page-icon' import { PageTitle } from './components/page-title' import { PageAside } from './components/page-aside' import { LinkIcon } from './icons/link-icon' import { GoogleDrive } from './components/google-drive' import { Audio } from './components/audio' import { File } from './components/file' import { LazyImage } from './components/lazy-image' import { useNotionContext } from './context' import { cs, getListNumber, isUrl } from './utils' import { Text } from './components/text' import { SyncPointerBlock } from './components/sync-pointer-block' import { AssetWrapper } from './components/asset-wrapper' import { EOI } from './components/eoi' interface BlockProps { block: types.Block level: number className?: string bodyClassName?: string header?: React.ReactNode footer?: React.ReactNode pageHeader?: React.ReactNode pageFooter?: React.ReactNode pageTitle?: React.ReactNode pageAside?: React.ReactNode pageCover?: React.ReactNode hideBlockId?: boolean disableHeader?: boolean children?: React.ReactNode } // TODO: use react state instead of a global for this const tocIndentLevelCache: { [blockId: string]: number } = {} const pageCoverStyleCache: Record = {} export const Block: React.FC = (props) => { const ctx = useNotionContext() const { components, fullPage, darkMode, recordMap, mapPageUrl, mapImageUrl, showTableOfContents, minTableOfContentsItems, defaultPageIcon, defaultPageCover, defaultPageCoverPosition } = ctx const [activeSection, setActiveSection] = React.useState(null) const { block, children, level, className, bodyClassName, header, footer, pageHeader, pageFooter, pageTitle, pageAside, pageCover, hideBlockId, disableHeader } = props if (!block) { return null } // ugly hack to make viewing raw collection views work properly // e.g., 6d886ca87ab94c21a16e3b82b43a57fb if (level === 0 && block.type === 'collection_view') { ;(block as any).type = 'collection_view_page' } const blockId = hideBlockId ? 'notion-block' : `notion-block-${uuidToId(block.id)}` switch (block.type) { case 'collection_view_page': // fallthrough case 'page': if (level === 0) { const { page_icon = defaultPageIcon, page_cover = defaultPageCover, page_cover_position = defaultPageCoverPosition, page_full_width, page_small_text } = block.format || {} if (fullPage) { const properties = block.type === 'page' ? block.properties : { title: recordMap.collection[getBlockCollectionId(block, recordMap)] ?.value?.name } const coverPosition = (1 - (page_cover_position || 0.5)) * 100 const pageCoverObjectPosition = `center ${coverPosition}%` let pageCoverStyle = pageCoverStyleCache[pageCoverObjectPosition] if (!pageCoverStyle) { pageCoverStyle = pageCoverStyleCache[pageCoverObjectPosition] = { objectPosition: pageCoverObjectPosition } } const pageIcon = getBlockIcon(block, recordMap) ?? defaultPageIcon const isPageIconUrl = pageIcon && isUrl(pageIcon) const toc = getPageTableOfContents( block as types.PageBlock, recordMap ) const hasToc = showTableOfContents && toc.length >= minTableOfContentsItems const hasAside = (hasToc || pageAside) && !page_full_width const hasPageCover = pageCover || page_cover return (
{!disableHeader && } {header}
{hasPageCover && (pageCover ? ( pageCover ) : (
))}
{page_icon && ( )} {pageHeader}

{pageTitle ?? ( )}

{(block.type === 'collection_view_page' || (block.type === 'page' && block.parent_table === 'collection')) && ( )} {block.type !== 'collection_view_page' && (
{children}
{hasAside && ( )}
)} {pageFooter}
{footer}
) } else { return (
{pageHeader} {(block.type === 'collection_view_page' || (block.type === 'page' && block.parent_table === 'collection')) && ( )} {block.type !== 'collection_view_page' && children} {pageFooter}
) } } else { const blockColor = block.format?.block_color return ( ) } case 'header': // fallthrough case 'sub_header': // fallthrough case 'sub_sub_header': { if (!block.properties) return null const blockColor = block.format?.block_color const id = uuidToId(block.id) const title = getTextContent(block.properties.title) || `Notion Header ${id}` // we use a cache here because constructing the ToC is non-trivial let indentLevel = tocIndentLevelCache[block.id] let indentLevelClass: string if (indentLevel === undefined) { const page = getBlockParentPage(block, recordMap) if (page) { const toc = getPageTableOfContents(page, recordMap) const tocItem = toc.find((tocItem) => tocItem.id === block.id) if (tocItem) { indentLevel = tocItem.indentLevel tocIndentLevelCache[block.id] = indentLevel } } } if (indentLevel !== undefined) { indentLevelClass = `notion-h-indent-${indentLevel}` } const isH1 = block.type === 'header' const isH2 = block.type === 'sub_header' const isH3 = block.type === 'sub_sub_header' const classNameStr = cs( isH1 && 'notion-h notion-h1', isH2 && 'notion-h notion-h2', isH3 && 'notion-h notion-h3', blockColor && `notion-${blockColor}`, indentLevelClass, blockId ) const innerHeader = (
{!block.format?.toggleable && ( )} ) let headerBlock //page title takes the h1 so all header blocks are greater if (isH1) { headerBlock = (

{innerHeader}

) } else if (isH2) { headerBlock = (

{innerHeader}

) } else { headerBlock = (

{innerHeader}

) } if (block.format?.toggleable) { return (
{headerBlock}
{children}
) } else { return headerBlock } } case 'divider': return
case 'text': { if (!block.properties && !block.content?.length) { return (
 
) } const blockColor = block.format?.block_color return (
{block.properties?.title && ( )} {children &&
{children}
}
) } case 'bulleted_list': // fallthrough case 'numbered_list': { const wrapList = (content: React.ReactNode, start?: number) => block.type === 'bulleted_list' ? (
    {content}
) : (
    {content}
) let output: JSX.Element | null = null if (block.content) { output = ( <> {block.properties && (
  • )} {wrapList(children)} ) } else { output = block.properties ? (
  • ) : null } const isTopLevel = block.type !== recordMap.block[block.parent_id]?.value?.type const start = getListNumber(block.id, recordMap.block) return isTopLevel ? wrapList(output, start) : output } case 'embed': return case 'tweet': // fallthrough case 'maps': // fallthrough case 'pdf': // fallthrough case 'figma': // fallthrough case 'typeform': // fallthrough case 'codepen': // fallthrough case 'excalidraw': // fallthrough case 'image': // fallthrough case 'gist': // fallthrough case 'video': return case 'drive': { const properties = block.format?.drive_properties if (!properties) { //check if this drive actually needs to be embeded ex. google sheets. if (block.format?.display_source) { return } } return ( ) } case 'audio': return (