snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
98
packages/commenting/src/canvas/comment-render.ts
Normal file
98
packages/commenting/src/canvas/comment-render.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { type Extensions, generateText, JSONContent } from '@tiptap/core'
|
||||
import { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'
|
||||
import { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'
|
||||
import { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'
|
||||
|
||||
/**
|
||||
* The author name shown in a byline when no source can name an id (e.g. a deleted account, or a
|
||||
* member with no comment and no live presence). The toolkit's one generic default, applied where a
|
||||
* byline needs a name; hosts pre-empt it by resolving the id from `resolveAuthor`. Not a
|
||||
* translation key — a single English literal.
|
||||
*/
|
||||
export const UNKNOWN_AUTHOR = 'Someone'
|
||||
|
||||
/** The `CommentAuthor` fallback for an unresolvable id — just the name, no color or image.
|
||||
* Frozen: the one shared instance is handed to host render slots. */
|
||||
export const UNKNOWN_COMMENT_AUTHOR: CommentAuthor = Object.freeze({ name: UNKNOWN_AUTHOR })
|
||||
|
||||
/**
|
||||
* The comment extension set has no heading node, so a body that nonetheless contains one (e.g. a
|
||||
* record created programmatically or synced from a client with a fuller set) would make TipTap
|
||||
* throw on an unknown node type. Demote any heading to a paragraph first, so headings can never
|
||||
* render as headings and rendering never crashes.
|
||||
*/
|
||||
function demoteHeadings(node: JSONContent): JSONContent {
|
||||
const content = Array.isArray(node.content) ? node.content.map(demoteHeadings) : node.content
|
||||
if (node.type === 'heading') {
|
||||
const { attrs: _attrs, ...rest } = node
|
||||
return { ...rest, type: 'paragraph', content }
|
||||
}
|
||||
return content === node.content ? node : { ...node, content }
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a body contains a mention node. Mention text is resolved from a member id at render time,
|
||||
* so a body with one can't be cached by identity alone (the same body renders differently as names
|
||||
* change) — those bodies skip the cache and re-render each call.
|
||||
*/
|
||||
function hasMention(node: JSONContent): boolean {
|
||||
if (node.type === 'mention') return true
|
||||
return Array.isArray(node.content) ? node.content.some(hasMention) : false
|
||||
}
|
||||
|
||||
const htmlCache = new WeakMap<TLRichText, string>()
|
||||
const textCache = new WeakMap<TLRichText, string>()
|
||||
|
||||
/**
|
||||
* Render a body through the comment extension set, memoized by body identity in `cache` unless the
|
||||
* body carries a mention (see {@link hasMention}), in which case the mention extension resolving
|
||||
* `resolveName` is added and nothing is cached.
|
||||
*/
|
||||
function renderWithCommentExtensions(
|
||||
cache: WeakMap<TLRichText, string>,
|
||||
richText: TLRichText,
|
||||
resolveName: ((id: string) => string | undefined) | undefined,
|
||||
render: (doc: JSONContent, extensions: Extensions) => string
|
||||
): string {
|
||||
const mentions = hasMention(richText as JSONContent)
|
||||
if (!mentions) {
|
||||
const cached = cache.get(richText)
|
||||
if (cached !== undefined) return cached
|
||||
}
|
||||
const extensions = mentions
|
||||
? [...commentTipTapExtensions, createMentionExtension({ resolveName })]
|
||||
: commentTipTapExtensions
|
||||
const result = render(demoteHeadings(richText as JSONContent), extensions)
|
||||
if (!mentions) cache.set(richText, result)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a comment body to HTML through the limited comment extension set (no headings), so a body
|
||||
* always renders with comment formatting regardless of the host editor's rich-text config. Renders
|
||||
* through tldraw's shared helper, so it gets the same empty-paragraph fix that keeps blank lines
|
||||
* from collapsing. `resolveName` maps a member id to its current name for any @mentions.
|
||||
*/
|
||||
export function renderCommentHtml(
|
||||
richText: TLRichText,
|
||||
resolveName?: (id: string) => string | undefined
|
||||
): string {
|
||||
return renderWithCommentExtensions(htmlCache, richText, resolveName, (doc, extensions) =>
|
||||
renderHtmlFromRichTextWithExtensions(doc as TLRichText, extensions)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a comment body to plaintext through the limited comment extension set — paragraphs and
|
||||
* list items separated by newlines. Used for previews (e.g. the sidebar) where formatting is dropped.
|
||||
* `resolveName` maps a member id to its current name for any @mentions.
|
||||
*/
|
||||
export function renderCommentPlaintext(
|
||||
richText: TLRichText,
|
||||
resolveName?: (id: string) => string | undefined
|
||||
): string {
|
||||
if (isCommentEmpty(richText)) return ''
|
||||
return renderWithCommentExtensions(textCache, richText, resolveName, (doc, extensions) =>
|
||||
generateText(doc, extensions, { blockSeparator: '\n' })
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user