UNPKG

tldraw

Version:

A tiny little drawing editor.

169 lines (151 loc) 4.64 kB
import { Extension, Extensions, generateHTML, generateJSON, generateText, JSONContent, } from '@tiptap/core' import Code from '@tiptap/extension-code' import Highlight from '@tiptap/extension-highlight' import Link from '@tiptap/extension-link' import { Node } from '@tiptap/pm/model' import StarterKit from '@tiptap/starter-kit' import { Editor, getOwnProperty, RichTextFontVisitorState, TLFontFace, TLRichText, WeakCache, } from '@tldraw/editor' import { DefaultFontFaces } from '../../shapes/shared/defaultFonts' import { TextDirection } from './textDirection' /** @public */ export const KeyboardShiftEnterTweakExtension = Extension.create({ name: 'keyboardShiftEnterHandler', addKeyboardShortcuts() { return { // We don't support soft breaks, so we just use the default enter command. 'Shift-Enter': ({ editor }) => editor.commands.enter(), } }, }) // We change the default Code to override what's in the StarterKit. // It allows for other attributes/extensions. Code.config.excludes = undefined // We want the highlighting to take precedence over bolding/italics/links // as far as rendering is concerned. Otherwise, the highlighting // looks broken up. Highlight.config.priority = 1100 /** * Default extensions for the TipTap editor. * * @public */ export const tipTapDefaultExtensions: Extensions = [ StarterKit.configure({ blockquote: false, codeBlock: false, horizontalRule: false, }), Link.configure({ openOnClick: false, autolink: true, }), Highlight, KeyboardShiftEnterTweakExtension, TextDirection, ] /** * Renders HTML from a rich text string. * * @param editor - The editor instance. * @param richText - The rich text content. * * @public */ export function renderHtmlFromRichText(editor: Editor, richText: TLRichText) { const tipTapExtensions = editor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions const html = generateHTML(richText as JSONContent, tipTapExtensions) // We replace empty paragraphs with a single line break to prevent the browser from collapsing them. return html.replaceAll('<p dir="auto"></p>', '<p><br /></p>') ?? '' } /** * Renders HTML from a rich text string for measurement. * @param editor - The editor instance. * @param richText - The rich text content. * * * @public */ export function renderHtmlFromRichTextForMeasurement(editor: Editor, richText: TLRichText) { const html = renderHtmlFromRichText(editor, richText) return `<div class="tl-rich-text">${html}</div>` } // A weak cache used to store plaintext that's been extracted from rich text. const plainTextFromRichTextCache = new WeakCache<TLRichText, string>() export function isEmptyRichText(richText: TLRichText) { if (richText.content.length === 1) { if (!(richText.content[0] as any).content) return true } return false } /** * Renders plaintext from a rich text string. * @param editor - The editor instance. * @param richText - The rich text content. * * * @public */ export function renderPlaintextFromRichText(editor: Editor, richText: TLRichText) { if (isEmptyRichText(richText)) return '' return plainTextFromRichTextCache.get(richText, () => { const tipTapExtensions = editor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions return generateText(richText as JSONContent, tipTapExtensions, { blockSeparator: '\n', }) }) } /** * Renders JSONContent from html. * @param editor - The editor instance. * @param richText - The rich text content. * * * @public */ export function renderRichTextFromHTML(editor: Editor, html: string): TLRichText { const tipTapExtensions = editor.getTextOptions().tipTapConfig?.extensions ?? tipTapDefaultExtensions return generateJSON(html, tipTapExtensions) as TLRichText } /** @public */ export function defaultAddFontsFromNode( node: Node, state: RichTextFontVisitorState, addFont: (font: TLFontFace) => void ) { for (const mark of node.marks) { if (mark.type.name === 'bold' && state.weight !== 'bold') { state = { ...state, weight: 'bold' } } if (mark.type.name === 'italic' && state.style !== 'italic') { state = { ...state, style: 'italic' } } if (mark.type.name === 'code' && state.family !== 'tldraw_mono') { state = { ...state, family: 'tldraw_mono' } } } const fontsForFamily = getOwnProperty(DefaultFontFaces, state.family) if (!fontsForFamily) return state const fontsForStyle = getOwnProperty(fontsForFamily, state.style) if (!fontsForStyle) return state const fontsForWeight = getOwnProperty(fontsForStyle, state.weight) if (!fontsForWeight) return state addFont(fontsForWeight) return state }