Document inherited members in reference (#3956)

Our reference docs don't currently include members inherited through the
`extends` keyword. These extended items are barely referenced at all -
you have to find them in the signature.

This diff adds a clearer note to the docs saying which type has been
extended, and if possible brings the extended items through onto the
current documentation page (with a note saying where they're from)


![image](https://github.com/tldraw/tldraw/assets/1489520/0349252d-e8bc-406b-bf47-636da424ebe0)


### Change Type

- [x] `docs` — Changes to the documentation, examples, or templates.
- [x] `improvement` — Improving existing features
This commit is contained in:
alex 2024-06-17 15:47:22 +01:00 committed by GitHub
parent 12aea7ed68
commit c4b9ea30f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 312 additions and 188 deletions

View file

@ -1,4 +1,5 @@
import classNames from 'classnames' import classNames from 'classnames'
import Link from 'next/link'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { Icon } from '../Icon' import { Icon } from '../Icon'
@ -35,32 +36,56 @@ export function TitleWithSourceLink({
source, source,
large, large,
tags, tags,
inherited,
}: { }: {
children: ReactNode children: ReactNode
source?: string | null source?: string | null
large?: boolean large?: boolean
tags?: string[] tags?: string[]
inherited?: { name: string; link: string }
}) { }) {
return ( return (
<div <>
className={classNames( <div
'article__title-with-source-link', className={classNames(
large && 'article__title-with-source-link--large' 'article__title-with-source-link',
)} large && 'article__title-with-source-link--large'
>
{children}
<div className="article__title-with-source-link__meta">
{tags?.map((tag) => <Tag key={tag}>{tag}</Tag>)}
{source && (
<a href={source} target="_blank" rel="noopener noreferrer" title="Source code">
<Icon icon="code" />
</a>
)} )}
>
{children}
<div className="article__title-with-source-link__meta">
{tags?.map((tag) => (
<Tag key={tag} tag={tag}>
{tag}
</Tag>
))}
{source && (
<a
href={source}
target="_blank"
rel="noopener noreferrer"
title="Source code"
className="article__title-with-source-link__source"
>
<Icon icon="code" />
</a>
)}
</div>
</div> </div>
</div> {inherited && (
<div className="article__title-with-source-link__from">
from{' '}
<code className="hljs">
<Link href={inherited.link} className="code-link">
{inherited.name}
</Link>
</code>
</div>
)}
</>
) )
} }
export function Tag({ children }: { children: string }) { export function Tag({ children, tag }: { children: ReactNode; tag: string }) {
return <span className={classNames(`article__tag`, `article__tag--${children}`)}>{children}</span> return <span className={classNames(`article__tag`, `article__tag--${tag}`)}>{children}</span>
} }

View file

@ -85,4 +85,6 @@ export async function createApiMarkdown() {
) )
sectionsJson.push(apiInputSection) sectionsJson.push(apiInputSection)
fs.writeFileSync(sectionsJsonPath, JSON.stringify(sectionsJson, null, '\t') + '\n') fs.writeFileSync(sectionsJsonPath, JSON.stringify(sectionsJson, null, '\t') + '\n')
model.throwEncounteredErrors()
} }

View file

@ -22,6 +22,7 @@ import {
ApiTypeAlias, ApiTypeAlias,
ApiVariable, ApiVariable,
Excerpt, Excerpt,
HeritageType,
} from '@microsoft/api-extractor-model' } from '@microsoft/api-extractor-model'
import { MarkdownWriter, formatWithPrettier, getPath, getSlug } from '../utils' import { MarkdownWriter, formatWithPrettier, getPath, getSlug } from '../utils'
@ -36,6 +37,11 @@ const date = new Intl.DateTimeFormat('en-US', {
day: '2-digit', day: '2-digit',
}).format(new Date()) }).format(new Date())
interface Member {
item: ApiItem
inheritedFrom: ApiItem | null
}
export async function getApiMarkdown( export async function getApiMarkdown(
model: TldrawApiModel, model: TldrawApiModel,
categoryName: string, categoryName: string,
@ -49,100 +55,59 @@ export async function getApiMarkdown(
const isComponent = model.isComponent(item) const isComponent = model.isComponent(item)
const componentProps = isComponent ? model.getReactPropsItem(item) : null const componentProps = isComponent ? model.getReactPropsItem(item) : null
const members = componentProps?.members ?? item.members const contents = collectMembersAndExtends(model, item)
if (members) {
const constructors = []
const properties = []
const methods = []
for (const member of members) {
switch (member.kind) {
case ApiItemKind.Constructor:
case ApiItemKind.ConstructSignature:
constructors.push(member)
break
case ApiItemKind.Variable:
case ApiItemKind.Property:
case ApiItemKind.PropertySignature:
properties.push(member)
break
case ApiItemKind.Method:
case ApiItemKind.Function:
case ApiItemKind.MethodSignature:
if (isComponent) {
properties.push(member)
} else {
methods.push(member)
}
break
case ApiItemKind.EnumMember:
case ApiItemKind.Class:
case ApiItemKind.TypeAlias:
case ApiItemKind.Interface:
// TODO: document these
break
default:
throw new Error(`Unknown member kind: ${member.kind} ${member.displayName}`)
}
}
if (contents.constructors.length) {
const constructorResult: Result = { markdown: '', keywords: [] } const constructorResult: Result = { markdown: '', keywords: [] }
for (const member of contents.constructors) {
await addMarkdownForMember(model, constructorResult, member)
addHorizontalRule(constructorResult)
}
addMarkdown(membersResult, constructorResult.markdown)
}
if (contents.properties.length || isComponent) {
const propertiesResult: Result = { markdown: '', keywords: [] } const propertiesResult: Result = { markdown: '', keywords: [] }
if (componentProps) addExtends(propertiesResult, item, contents.heritage)
for (const member of contents.properties) {
const slug = getSlug(member.item)
addMarkdown(toc, ` - [${member.item.displayName}](#${slug})\n`)
await addMarkdownForMember(model, propertiesResult, member, {
isComponentProp: isComponent,
})
addHorizontalRule(propertiesResult)
}
if (
componentProps &&
componentProps instanceof ApiDeclaredItem &&
componentProps?.kind !== 'Interface'
) {
propertiesResult.markdown += await excerptToMarkdown(componentProps, componentProps.excerpt, {
kind: componentProps.kind,
})
}
if (propertiesResult.markdown.trim()) {
addMarkdown(toc, `- [Properties](#properties)\n`)
addMarkdown(membersResult, `## Properties\n\n`)
addMarkdown(membersResult, propertiesResult.markdown)
} else if (isComponent && !componentProps) {
addMarkdown(membersResult, `## Properties\n\n`)
addMarkdown(membersResult, `This component does not take any props.\n\n`)
}
}
if (contents.methods.length) {
const methodsResult: Result = { markdown: '', keywords: [] } const methodsResult: Result = { markdown: '', keywords: [] }
addMarkdown(toc, `- [Methods](#methods)\n`)
if (constructors.length) { addMarkdown(methodsResult, `## Methods\n\n`)
for (const member of constructors) { for (const member of contents.methods) {
await addMarkdownForMember(model, constructorResult, member) const slug = getSlug(member.item)
addHorizontalRule(constructorResult) addMarkdown(toc, ` - [${member.item.displayName}](#${slug})\n`)
} await addMarkdownForMember(model, methodsResult, member)
addMarkdown(membersResult, constructorResult.markdown) addHorizontalRule(methodsResult)
}
if (properties.length || isComponent) {
if (componentProps) addExtends(propertiesResult, componentProps)
for (const member of properties) {
const slug = getSlug(member)
addMarkdown(toc, ` - [${member.displayName}](#${slug})\n`)
await addMarkdownForMember(model, propertiesResult, member, {
isComponentProp: isComponent,
})
addHorizontalRule(propertiesResult)
}
if (
componentProps &&
componentProps instanceof ApiDeclaredItem &&
componentProps?.kind !== 'Interface'
) {
propertiesResult.markdown += await excerptToMarkdown(
componentProps,
componentProps.excerpt,
{
kind: componentProps.kind,
}
)
}
if (propertiesResult.markdown.trim()) {
addMarkdown(toc, `- [Properties](#properties)\n`)
addMarkdown(membersResult, `## Properties\n\n`)
addMarkdown(membersResult, propertiesResult.markdown)
} else if (isComponent && !componentProps) {
addMarkdown(membersResult, `## Properties\n\n`)
addMarkdown(membersResult, `This component does not take any props.\n\n`)
}
}
if (methods.length) {
addMarkdown(toc, `- [Methods](#methods)\n`)
addMarkdown(methodsResult, `## Methods\n\n`)
for (const member of methods) {
const slug = getSlug(member)
addMarkdown(toc, ` - [${member.displayName}](#${slug})\n`)
await addMarkdownForMember(model, methodsResult, member)
addHorizontalRule(methodsResult)
}
addMarkdown(membersResult, methodsResult.markdown)
} }
addMarkdown(membersResult, methodsResult.markdown)
} }
await addFrontmatter(model, result, item, categoryName, j) await addFrontmatter(model, result, item, categoryName, j)
@ -153,6 +118,10 @@ export async function getApiMarkdown(
result.markdown += `</details>\n\n` result.markdown += `</details>\n\n`
} }
if (!isComponent) {
addExtends(result, item, contents.heritage)
}
await addDocComment(model, result, item) await addDocComment(model, result, item)
if (membersResult.markdown.length) { if (membersResult.markdown.length) {
@ -165,6 +134,108 @@ export async function getApiMarkdown(
/* --------------------- Helpers -------------------- */ /* --------------------- Helpers -------------------- */
function sortMembers(members: Member[]) {
return members.sort((a, b) => {
const aIsStatic = ApiStaticMixin.isBaseClassOf(a.item) && a.item.isStatic
const bIsStatic = ApiStaticMixin.isBaseClassOf(b.item) && b.item.isStatic
if (aIsStatic && !bIsStatic) return -1
if (!aIsStatic && bIsStatic) return 1
return a.item.displayName.localeCompare(b.item.displayName)
})
}
function collectMembersAndExtends(model: TldrawApiModel, item: ApiItem) {
const isComponent = model.isComponent(item)
const constructors: Member[] = []
const properties: Member[] = []
const methods: Member[] = []
const heritage: HeritageType[] = []
function visit(item: ApiItem, inheritedFrom: ApiItem | null) {
if (item.members) {
for (const member of item.members) {
switch (member.kind) {
case ApiItemKind.Constructor:
case ApiItemKind.ConstructSignature:
addMember(constructors, member, inheritedFrom)
break
case ApiItemKind.Variable:
case ApiItemKind.Property:
case ApiItemKind.PropertySignature:
addMember(properties, member, inheritedFrom)
break
case ApiItemKind.Method:
case ApiItemKind.Function:
case ApiItemKind.MethodSignature:
if (isComponent) {
addMember(properties, member, inheritedFrom)
} else {
addMember(methods, member, inheritedFrom)
}
break
case ApiItemKind.EnumMember:
case ApiItemKind.Class:
case ApiItemKind.TypeAlias:
case ApiItemKind.Interface:
// TODO: document these
break
default:
model.nonBlockingError(
member,
`Unknown member kind: ${member.kind} in ${item.displayName}`
)
break
}
}
}
if (model.isComponent(item)) {
const componentProps = model.getReactPropsItem(item)
if (componentProps) {
visit(componentProps, null)
}
}
const extendsTypes =
item instanceof ApiClass && item.extendsType
? [item.extendsType]
: item instanceof ApiInterface
? item.extendsTypes
: []
for (const extendsType of extendsTypes) {
if (!inheritedFrom) {
heritage.push(extendsType)
}
const tokens = extendsType.excerpt.spannedTokens
let extendedItem = null
if (tokens[0].kind === 'Reference') {
extendedItem = model.tryResolveToken(item, tokens[0])
}
if (extendedItem) {
visit(extendedItem, extendedItem)
}
}
}
visit(item, null)
sortMembers(constructors)
sortMembers(properties)
sortMembers(methods)
return { constructors, properties, methods, heritage }
}
function addMember(members: Member[], item: ApiItem, inheritedFrom: ApiItem | null) {
if (members.some((m) => m.item.displayName === item.displayName)) return
members.push({ item, inheritedFrom })
}
function addMarkdown(result: Result, markdown: string) { function addMarkdown(result: Result, markdown: string) {
result.markdown += markdown result.markdown += markdown
} }
@ -172,12 +243,12 @@ function addMarkdown(result: Result, markdown: string) {
async function addMarkdownForMember( async function addMarkdownForMember(
model: TldrawApiModel, model: TldrawApiModel,
result: Result, result: Result,
member: ApiItem, { item, inheritedFrom }: Member,
{ isComponentProp = false } = {} { isComponentProp = false } = {}
) { ) {
if (member.displayName.startsWith('_')) return if (item.displayName.startsWith('_')) return
addMemberNameAndMeta(result, model, member, { isComponentProp }) addMemberNameAndMeta(result, model, item, { isComponentProp, inheritedFrom })
await addDocComment(model, result, member) await addDocComment(model, result, item)
} }
async function addFrontmatter( async function addFrontmatter(
@ -251,24 +322,27 @@ function addMemberNameAndMeta(
result: Result, result: Result,
model: TldrawApiModel, model: TldrawApiModel,
item: ApiItem, item: ApiItem,
{ level = 3, isComponentProp = false } = {} {
level = 3,
isComponentProp = false,
inheritedFrom = null,
}: { level?: number; isComponentProp?: boolean; inheritedFrom?: ApiItem | null } = {}
) { ) {
const heading = `${'#'.repeat(level)} ${getItemTitle(item)}` const heading = `${'#'.repeat(level)} ${getItemTitle(item)}`
if (item instanceof ApiDeclaredItem && item.sourceLocation.fileUrl) { const inherited = inheritedFrom
const source = item.sourceLocation.fileUrl ? { name: inheritedFrom.displayName, link: `/reference/${getPath(inheritedFrom)}` }
const tags = getTags(model, item, { isComponentProp, includeKind: false }) : null
result.markdown += [
`<TitleWithSourceLink source={${JSON.stringify(source)}} tags={${JSON.stringify(tags)}}>`, const tags = getTags(model, item, { isComponentProp, includeKind: false })
'', result.markdown += [
heading, `<TitleWithSourceLink tags={${JSON.stringify(tags)}} inherited={${JSON.stringify(inherited)}}>`,
'', '',
'</TitleWithSourceLink>', heading,
'', '',
].join('\n') '</TitleWithSourceLink>',
} else { '',
result.markdown += `${heading}\n\n` ].join('\n')
}
} }
async function addDocComment(model: TldrawApiModel, result: Result, member: ApiItem) { async function addDocComment(model: TldrawApiModel, result: Result, member: ApiItem) {
@ -395,7 +469,7 @@ async function addDocComment(model: TldrawApiModel, result: Result, member: ApiI
result.markdown += '</ParametersTable>\n\n' result.markdown += '</ParametersTable>\n\n'
} }
} else { } else {
model.error(member, `Unknown member kind: ${member.kind}`) model.nonBlockingError(member, `Unknown member kind: ${member.kind}`)
} }
} }
@ -514,18 +588,11 @@ function getTags(
return tags return tags
} }
function addExtends(result: Result, item: ApiItem) { function addExtends(result: Result, item: ApiItem, heritage: HeritageType[]) {
const extendsTypes = if (!heritage.length) return
item instanceof ApiClass && item.extendsType
? [item.extendsType]
: item instanceof ApiInterface
? item.extendsTypes
: []
if (!extendsTypes.length) return
const links = {} as Record<string, string> const links = {} as Record<string, string>
for (const type of extendsTypes) { for (const type of heritage) {
for (const token of type.excerpt.spannedTokens) { for (const token of type.excerpt.spannedTokens) {
if (!token.canonicalReference) continue if (!token.canonicalReference) continue
@ -543,7 +610,7 @@ function addExtends(result: Result, item: ApiItem) {
result.markdown += [ result.markdown += [
`<CodeLinkProvider links={${JSON.stringify(links)}}>`, `<CodeLinkProvider links={${JSON.stringify(links)}}>`,
'', '',
`Extends \`${extendsTypes.map((type) => type.excerpt.text).join(', ')}\`.`, `Extends \`${heritage.map((type) => type.excerpt.text).join(', ')}\`.`,
'', '',
'</CodeLinkProvider>', '</CodeLinkProvider>',
'', '',

View file

@ -309,7 +309,7 @@ body {
margin-left: auto; margin-left: auto;
padding-left: 16px; padding-left: 16px;
} }
.article__title-with-source-link__meta > a { .article__title-with-source-link__source {
display: block; display: block;
width: 32px; width: 32px;
height: 32px; height: 32px;
@ -318,11 +318,11 @@ body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.article__title-with-source-link .article__title-with-source-link__meta > a { .article__title-with-source-link__source {
color: var(--color-text); color: var(--color-text) !important;
} }
.article__title-with-source-link > a:hover { .article__title-with-source-link__source:hover {
background-color: var(--color-tint-1); background-color: var(--color-tint-1);
} }
.article__title-with-source-link .icon { .article__title-with-source-link .icon {
@ -330,7 +330,7 @@ body {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
.article__title-with-source-link--large .article__title-with-source-link__meta > a { .article__title-with-source-link--large .article__title-with-source-link__source {
width: 42px; width: 42px;
height: 42px; height: 42px;
} }
@ -342,15 +342,21 @@ body {
display: inline-block; display: inline-block;
padding: 4px 6px; padding: 4px 6px;
border-radius: var(--border-radius-menu); border-radius: var(--border-radius-menu);
background-color: var(--color-tint-0); background-color: var(--color-tint-1);
color: var(--color-tint-5); color: var(--color-tint-5);
font-size: 12px; font-size: 12px;
margin-right: 8px; margin-right: 8px;
line-height: 1; line-height: 1;
white-space: nowrap;
} }
.article__title-with-source-link--large .article__tag { .article__title-with-source-link--large .article__tag {
font-size: 14px; font-size: 14px;
} }
.article__title-with-source-link__from {
font-size: 12px;
line-height: 1.5;
color: var(--color-tint-5);
}
/* Prev / Next Links */ /* Prev / Next Links */

View file

@ -83,8 +83,8 @@
} }
a.code-link::before { a.code-link::before {
content: ''; content: '';
width: 15px; width: 1.0714em; /* 15px when font-size is 14px */
height: 15px; height: 1.0714em;
display: inline-block; display: inline-block;
background: currentColor; background: currentColor;
-webkit-mask: url('data:image/svg+xml;utf8,<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.5 2.5H3.5C2.94772 2.5 2.5 2.94772 2.5 3.5V11.5C2.5 12.0523 2.94772 12.5 3.5 12.5H11.5C12.0523 12.5 12.5 12.0523 12.5 11.5V8.5M9.5 2.5H12.5M12.5 2.5V5.5M12.5 2.5L6.5 8.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/></svg>') -webkit-mask: url('data:image/svg+xml;utf8,<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.5 2.5H3.5C2.94772 2.5 2.5 2.94772 2.5 3.5V11.5C2.5 12.0523 2.94772 12.5 3.5 12.5H11.5C12.0523 12.5 12.5 12.0523 12.5 11.5V8.5M9.5 2.5H12.5M12.5 2.5V5.5M12.5 2.5L6.5 8.5" stroke="black" stroke-linecap="round" stroke-linejoin="round"/></svg>')
@ -93,6 +93,6 @@ a.code-link::before {
no-repeat 50% 50%; no-repeat 50% 50%;
-webkit-mask-size: cover; -webkit-mask-size: cover;
mask-size: cover; mask-size: cover;
vertical-align: -2px; vertical-align: -0.142em; /* 2px when font-size is 14px */
margin-right: 2px; margin-right: 0.142em;
} }

View file

@ -15,8 +15,9 @@ export class TldrawApiModel extends ApiModel {
private reactComponents = new Set<ApiItem>() private reactComponents = new Set<ApiItem>()
private reactComponentProps = new Set<ApiItem>() private reactComponentProps = new Set<ApiItem>()
nonBlockingErrors: Error[] = []
async preprocessReactComponents() { async preprocessReactComponents() {
const errors = []
for (const packageModel of this.members) { for (const packageModel of this.members) {
assert(packageModel instanceof ApiPackage) assert(packageModel instanceof ApiPackage)
if (packageModel.name !== 'tldraw') continue if (packageModel.name !== 'tldraw') continue
@ -37,22 +38,18 @@ export class TldrawApiModel extends ApiModel {
props.tsdocComment.summarySection props.tsdocComment.summarySection
) )
if (markdown.trim()) { if (markdown.trim()) {
this.error( this.nonBlockingError(
props, props,
"Component props should not contain documentation as it won't be included in the docs site. Add it to the component instead." "Component props should not contain documentation as it won't be included in the docs site. Add it to the component instead."
) )
} }
} }
if (props) this.reactComponentProps.add(props) if (props) this.reactComponentProps.add(props)
} catch (e) { } catch (e: any) {
errors.push(e) this.nonBlockingErrors.push(e)
} }
} }
} }
if (errors.length > 0) {
throw new Error(errors.map((e) => (e as any).message).join('\n\n'))
}
} }
resolveToken(origin: ApiItem, token: ExcerptToken) { resolveToken(origin: ApiItem, token: ExcerptToken) {
@ -63,6 +60,14 @@ export class TldrawApiModel extends ApiModel {
return apiItemResult.resolvedApiItem! return apiItemResult.resolvedApiItem!
} }
tryResolveToken(origin: ApiItem, token: ExcerptToken) {
const apiItemResult = this.resolveDeclarationReference(token.canonicalReference!, origin)
if (apiItemResult.errorMessage) {
return null
}
return apiItemResult.resolvedApiItem!
}
getReactPropsItem(component: ApiItem): ApiItem | null { getReactPropsItem(component: ApiItem): ApiItem | null {
if (component instanceof ApiFunction) { if (component instanceof ApiFunction) {
if (component.parameters.length === 0) return null if (component.parameters.length === 0) return null
@ -84,10 +89,11 @@ export class TldrawApiModel extends ApiModel {
return this.resolveToken(component, tokens[0]) return this.resolveToken(component, tokens[0])
} }
this.error( this.nonBlockingError(
component, component,
`Expected props parameter to be a simple reference. Rewrite this to use a \`${component.displayName}Props\` interface.\nFound: ${propsParam.parameterTypeExcerpt.text}` `Expected props parameter to be a simple reference. Rewrite this to use a \`${component.displayName}Props\` interface.\nFound: ${propsParam.parameterTypeExcerpt.text}`
) )
return null
} else if (component instanceof ApiVariable) { } else if (component instanceof ApiVariable) {
const tokens = component.variableTypeExcerpt.spannedTokens const tokens = component.variableTypeExcerpt.spannedTokens
if ( if (
@ -130,12 +136,16 @@ export class TldrawApiModel extends ApiModel {
return null return null
} }
this.error( this.nonBlockingError(
component, component,
`Expected a simple props interface for react component. Got: ${component.variableTypeExcerpt.text}` `Expected a simple props interface for react component. Got: ${component.variableTypeExcerpt.text}`
) )
return null
} else { } else {
this.error(component, `Unknown item kind for @react component: ${component.kind}`) this.nonBlockingError(component, `Unknown item kind for @react component: ${component.kind}`)
return null
} }
} }
@ -147,12 +157,26 @@ export class TldrawApiModel extends ApiModel {
return this.reactComponentProps.has(item) return this.reactComponentProps.has(item)
} }
error(item: ApiItem, message: string): never { private createError(item: ApiItem, message: string) {
const suffix = const suffix =
'_fileUrlPath' in item && typeof item._fileUrlPath === 'string' '_fileUrlPath' in item && typeof item._fileUrlPath === 'string'
? `\nin ${item._fileUrlPath}` ? `\nin ${item._fileUrlPath}`
: '' : ''
throw new Error(`${item.displayName}: ${message}${suffix}`) return new Error(`${item.displayName}: ${message}${suffix}`)
}
nonBlockingError(item: ApiItem, message: string) {
this.nonBlockingErrors.push(this.createError(item, message))
}
throwEncounteredErrors() {
if (this.nonBlockingErrors.length > 0) {
throw new Error(this.nonBlockingErrors.map((e) => (e as any).message).join('\n\n'))
}
}
error(item: ApiItem, message: string): never {
throw this.createError(item, message)
} }
assert(item: ApiItem, condition: unknown, message: string): asserts condition { assert(item: ApiItem, condition: unknown, message: string): asserts condition {

View file

@ -1045,13 +1045,13 @@ export class Editor extends EventEmitter<TLEventMap> {
getStateDescendant<T extends StateNode>(path: string): T | undefined; getStateDescendant<T extends StateNode>(path: string): T | undefined;
getStyleForNextShape<T>(style: StyleProp<T>): T; getStyleForNextShape<T>(style: StyleProp<T>): T;
// @deprecated (undocumented) // @deprecated (undocumented)
getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<SVGSVGElement | undefined>; getSvg(shapes: TLShape[] | TLShapeId[], opts?: TLSvgOptions): Promise<SVGSVGElement | undefined>;
getSvgElement(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<{ getSvgElement(shapes: TLShape[] | TLShapeId[], opts?: TLSvgOptions): Promise<{
height: number; height: number;
svg: SVGSVGElement; svg: SVGSVGElement;
width: number; width: number;
} | undefined>; } | undefined>;
getSvgString(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<{ getSvgString(shapes: TLShape[] | TLShapeId[], opts?: TLSvgOptions): Promise<{
height: number; height: number;
svg: string; svg: string;
width: number; width: number;
@ -3299,17 +3299,17 @@ export type TLStoreWithStatus = {
// @public (undocumented) // @public (undocumented)
export interface TLSvgOptions { export interface TLSvgOptions {
// (undocumented) // (undocumented)
background: boolean; background?: boolean;
// (undocumented) // (undocumented)
bounds: Box; bounds?: Box;
// (undocumented) // (undocumented)
darkMode?: boolean; darkMode?: boolean;
// (undocumented) // (undocumented)
padding: number; padding?: number;
// (undocumented) // (undocumented)
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']; preserveAspectRatio?: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
// (undocumented) // (undocumented)
scale: number; scale?: number;
} }
// @public (undocumented) // @public (undocumented)

View file

@ -8224,7 +8224,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
async getSvgElement(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) { async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgOptions = {}) {
const result = await getSvgJsx(this, shapes, opts) const result = await getSvgJsx(this, shapes, opts)
if (!result) return undefined if (!result) return undefined
@ -8251,7 +8251,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* *
* @public * @public
*/ */
async getSvgString(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) { async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLSvgOptions = {}) {
const result = await this.getSvgElement(shapes, opts) const result = await this.getSvgElement(shapes, opts)
if (!result) return undefined if (!result) return undefined
@ -8264,7 +8264,7 @@ export class Editor extends EventEmitter<TLEventMap> {
} }
/** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */ /** @deprecated Use {@link Editor.getSvgString} or {@link Editor.getSvgElement} instead. */
async getSvg(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) { async getSvg(shapes: TLShapeId[] | TLShape[], opts: TLSvgOptions = {}) {
const result = await this.getSvgElement(shapes, opts) const result = await this.getSvgElement(shapes, opts)
if (!result) return undefined if (!result) return undefined
return result.svg return result.svg

View file

@ -13,7 +13,7 @@ import { TLSvgOptions } from './types/misc-types'
export async function getSvgJsx( export async function getSvgJsx(
editor: Editor, editor: Editor,
shapes: TLShapeId[] | TLShape[], shapes: TLShapeId[] | TLShape[],
opts = {} as Partial<TLSvgOptions> opts: TLSvgOptions = {}
) { ) {
const ids = const ids =
typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : (shapes as TLShape[]).map((s) => s.id) typeof shapes[0] === 'string' ? (shapes as TLShapeId[]) : (shapes as TLShape[]).map((s) => s.id)

View file

@ -9,12 +9,12 @@ export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>
/** @public */ /** @public */
export interface TLSvgOptions { export interface TLSvgOptions {
bounds: Box bounds?: Box
scale: number scale?: number
background: boolean background?: boolean
padding: number padding?: number
darkMode?: boolean darkMode?: boolean
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'] preserveAspectRatio?: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
} }
/** @public */ /** @public */

View file

@ -359,7 +359,7 @@ export function ConvertToBookmarkMenuItem(): JSX_2.Element | null;
export function ConvertToEmbedMenuItem(): JSX_2.Element | null; export function ConvertToEmbedMenuItem(): JSX_2.Element | null;
// @public // @public
export function copyAs(editor: Editor, ids: TLShapeId[], format?: TLCopyType, opts?: Partial<TLSvgOptions>): Promise<void>; export function copyAs(editor: Editor, ids: TLShapeId[], format?: TLCopyType, opts?: TLSvgOptions): Promise<void>;
// @public (undocumented) // @public (undocumented)
export function CopyAsMenuGroup(): JSX_2.Element; export function CopyAsMenuGroup(): JSX_2.Element;
@ -634,7 +634,7 @@ export interface ExampleDialogProps {
} }
// @public // @public
export function exportAs(editor: Editor, ids: TLShapeId[], format: TLExportType | undefined, name: string | undefined, opts?: Partial<TLSvgOptions>): Promise<void>; export function exportAs(editor: Editor, ids: TLShapeId[], format: TLExportType | undefined, name: string | undefined, opts?: TLSvgOptions): Promise<void>;
// @public (undocumented) // @public (undocumented)
export function ExportFileContentSubMenu(): JSX_2.Element; export function ExportFileContentSubMenu(): JSX_2.Element;
@ -644,7 +644,7 @@ export function exportToBlob({ editor, ids, format, opts, }: {
editor: Editor; editor: Editor;
format: TLExportType; format: TLExportType;
ids: TLShapeId[]; ids: TLShapeId[];
opts?: Partial<TLSvgOptions>; opts?: TLSvgOptions;
}): Promise<Blob>; }): Promise<Blob>;
// @public (undocumented) // @public (undocumented)
@ -1660,7 +1660,7 @@ export function TldrawHandles({ children }: TLHandlesProps): JSX_2.Element | nul
export const TldrawImage: NamedExoticComponent<TldrawImageProps>; export const TldrawImage: NamedExoticComponent<TldrawImageProps>;
// @public (undocumented) // @public (undocumented)
export interface TldrawImageProps extends Partial<TLSvgOptions> { export interface TldrawImageProps extends TLSvgOptions {
bindingUtils?: readonly TLAnyBindingUtilConstructor[]; bindingUtils?: readonly TLAnyBindingUtilConstructor[];
format?: 'png' | 'svg'; format?: 'png' | 'svg';
pageId?: TLPageId; pageId?: TLPageId;

View file

@ -20,7 +20,7 @@ import { getSvgAsImage } from './utils/export/export'
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls' import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
/** @public */ /** @public */
export interface TldrawImageProps extends Partial<TLSvgOptions> { export interface TldrawImageProps extends TLSvgOptions {
/** /**
* The snapshot to display. * The snapshot to display.
*/ */

View file

@ -18,7 +18,7 @@ export function copyAs(
editor: Editor, editor: Editor,
ids: TLShapeId[], ids: TLShapeId[],
format: TLCopyType = 'svg', format: TLCopyType = 'svg',
opts = {} as Partial<TLSvgOptions> opts: TLSvgOptions = {}
): Promise<void> { ): Promise<void> {
// Note: it's important that this function itself isn't async and doesn't really use promises - // Note: it's important that this function itself isn't async and doesn't really use promises -
// we need to create the relevant `ClipboardItem`s and call window.navigator.clipboard.write // we need to create the relevant `ClipboardItem`s and call window.navigator.clipboard.write

View file

@ -96,7 +96,7 @@ export async function getSvgAsImage(
} }
} }
async function getSvgString(editor: Editor, ids: TLShapeId[], opts: Partial<TLSvgOptions>) { async function getSvgString(editor: Editor, ids: TLShapeId[], opts: TLSvgOptions) {
const svg = await editor.getSvgString(ids?.length ? ids : [...editor.getCurrentPageShapeIds()], { const svg = await editor.getSvgString(ids?.length ? ids : [...editor.getCurrentPageShapeIds()], {
scale: 1, scale: 1,
background: editor.getInstanceState().exportBackground, background: editor.getInstanceState().exportBackground,
@ -112,7 +112,7 @@ export async function exportToString(
editor: Editor, editor: Editor,
ids: TLShapeId[], ids: TLShapeId[],
format: 'svg' | 'json', format: 'svg' | 'json',
opts = {} as Partial<TLSvgOptions> opts: TLSvgOptions = {}
) { ) {
switch (format) { switch (format) {
case 'svg': { case 'svg': {
@ -141,12 +141,12 @@ export async function exportToBlob({
editor, editor,
ids, ids,
format, format,
opts = {} as Partial<TLSvgOptions>, opts = {},
}: { }: {
editor: Editor editor: Editor
ids: TLShapeId[] ids: TLShapeId[]
format: TLExportType format: TLExportType
opts?: Partial<TLSvgOptions> opts?: TLSvgOptions
}): Promise<Blob> { }): Promise<Blob> {
switch (format) { switch (format) {
case 'svg': case 'svg':
@ -188,7 +188,7 @@ export function exportToBlobPromise(
editor: Editor, editor: Editor,
ids: TLShapeId[], ids: TLShapeId[],
format: TLExportType, format: TLExportType,
opts = {} as Partial<TLSvgOptions> opts: TLSvgOptions = {}
): { blobPromise: Promise<Blob>; mimeType: string } { ): { blobPromise: Promise<Blob>; mimeType: string } {
return { return {
blobPromise: exportToBlob({ editor, ids, format, opts }), blobPromise: exportToBlob({ editor, ids, format, opts }),

View file

@ -20,7 +20,7 @@ export async function exportAs(
ids: TLShapeId[], ids: TLShapeId[],
format: TLExportType = 'png', format: TLExportType = 'png',
name: string | undefined, name: string | undefined,
opts = {} as Partial<TLSvgOptions> opts: TLSvgOptions = {}
) { ) {
// If we don't get name then use a predefined one // If we don't get name then use a predefined one
if (!name) { if (!name) {