tldraw
Version:
A tiny little drawing editor.
169 lines (151 loc) • 4.64 kB
text/typescript
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
}