2023-06-24 14:01:02 +00:00
|
|
|
import { ApiItem, ApiItemKind, ApiModel } from '@microsoft/api-extractor-model'
|
2023-04-25 11:01:25 +00:00
|
|
|
import {
|
|
|
|
DocCodeSpan,
|
|
|
|
DocEscapedText,
|
|
|
|
DocFencedCode,
|
2023-06-24 14:01:02 +00:00
|
|
|
DocLinkTag,
|
2023-04-25 11:01:25 +00:00
|
|
|
DocNode,
|
|
|
|
DocParagraph,
|
|
|
|
DocPlainText,
|
|
|
|
DocSection,
|
|
|
|
DocSoftBreak,
|
|
|
|
} from '@microsoft/tsdoc'
|
2023-06-24 14:01:02 +00:00
|
|
|
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
|
2023-04-25 11:01:25 +00:00
|
|
|
import prettier from 'prettier'
|
|
|
|
|
2023-06-24 14:01:02 +00:00
|
|
|
function isOnParentPage(itemKind: ApiItemKind) {
|
|
|
|
switch (itemKind) {
|
|
|
|
case ApiItemKind.CallSignature:
|
|
|
|
case ApiItemKind.Class:
|
|
|
|
case ApiItemKind.EntryPoint:
|
|
|
|
case ApiItemKind.Enum:
|
|
|
|
case ApiItemKind.Function:
|
|
|
|
case ApiItemKind.Interface:
|
|
|
|
case ApiItemKind.Model:
|
|
|
|
case ApiItemKind.Namespace:
|
|
|
|
case ApiItemKind.Package:
|
|
|
|
case ApiItemKind.TypeAlias:
|
|
|
|
case ApiItemKind.Variable:
|
|
|
|
case ApiItemKind.None:
|
|
|
|
return false
|
|
|
|
case ApiItemKind.Constructor:
|
|
|
|
case ApiItemKind.ConstructSignature:
|
|
|
|
case ApiItemKind.EnumMember:
|
|
|
|
case ApiItemKind.Method:
|
|
|
|
case ApiItemKind.MethodSignature:
|
|
|
|
case ApiItemKind.Property:
|
|
|
|
case ApiItemKind.PropertySignature:
|
|
|
|
case ApiItemKind.IndexSignature:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
exhaustiveSwitchError(itemKind)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function sanitizeReference(reference: string) {
|
|
|
|
return reference
|
|
|
|
.replace(/[!:()#.[\]]/g, '-')
|
2023-04-25 11:01:25 +00:00
|
|
|
.replace(/-+/g, '-')
|
|
|
|
.replace(/^-/, '')
|
|
|
|
.replace(/\/-/, '/')
|
|
|
|
.replace(/-$/, '')
|
|
|
|
}
|
|
|
|
|
2023-06-24 14:01:02 +00:00
|
|
|
export function getSlug(item: ApiItem): string {
|
|
|
|
return sanitizeReference(item.canonicalReference.toString().replace(/^@tldraw\/[^!]+!/, ''))
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getPath(item: ApiItem): string {
|
|
|
|
if (isOnParentPage(item.kind)) {
|
|
|
|
const parentPath = getPath(assertExists(item.parent))
|
|
|
|
const childSlug = getSlug(item)
|
|
|
|
return `${parentPath}#${childSlug}`
|
|
|
|
}
|
|
|
|
return sanitizeReference(item.canonicalReference.toString().replace(/^@tldraw\/([^!]+)/, '$1/'))
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const prettierConfigPromise = prettier.resolveConfig(__dirname)
|
|
|
|
const languages: { [tag: string]: string | undefined } = {
|
|
|
|
ts: 'typescript',
|
|
|
|
tsx: 'typescript',
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function formatWithPrettier(
|
|
|
|
code: string,
|
|
|
|
{
|
|
|
|
languageTag,
|
|
|
|
// roughly the width of our code blocks on a desktop
|
|
|
|
printWidth = 80,
|
|
|
|
}: { languageTag?: string; printWidth?: number } = {}
|
|
|
|
) {
|
|
|
|
const language = languages[languageTag || 'ts']
|
|
|
|
if (!language) {
|
|
|
|
throw new Error(`Unknown language: ${languageTag}`)
|
|
|
|
}
|
|
|
|
const prettierConfig = await prettierConfigPromise
|
|
|
|
const formattedCode = prettier.format(code, {
|
|
|
|
...prettierConfig,
|
|
|
|
parser: language,
|
|
|
|
printWidth,
|
|
|
|
tabWidth: 2,
|
|
|
|
useTabs: false,
|
|
|
|
})
|
|
|
|
|
|
|
|
return formattedCode.trimEnd()
|
|
|
|
}
|
|
|
|
|
|
|
|
export class MarkdownWriter {
|
2023-06-24 14:01:02 +00:00
|
|
|
static async docNodeToMarkdown(apiContext: ApiItem, docNode: DocNode) {
|
|
|
|
const writer = new MarkdownWriter(apiContext)
|
2023-04-25 11:01:25 +00:00
|
|
|
await writer.writeDocNode(docNode)
|
|
|
|
return writer.toString()
|
|
|
|
}
|
|
|
|
|
2023-06-24 14:01:02 +00:00
|
|
|
private constructor(private readonly apiContext: ApiItem) {}
|
|
|
|
|
2023-04-25 11:01:25 +00:00
|
|
|
private result = ''
|
|
|
|
|
|
|
|
write(...parts: string[]): this {
|
|
|
|
this.result += parts.join('')
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
endsWith(str: string) {
|
|
|
|
return this.result.endsWith(str)
|
|
|
|
}
|
|
|
|
|
|
|
|
writeIfNeeded(str: string): this {
|
|
|
|
if (!this.endsWith(str)) {
|
|
|
|
this.write(str)
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
async writeDocNode(docNode: DocNode) {
|
|
|
|
if (docNode instanceof DocPlainText) {
|
|
|
|
this.write(docNode.text)
|
|
|
|
} else if (docNode instanceof DocSection || docNode instanceof DocParagraph) {
|
|
|
|
await this.writeDocNodes(docNode.nodes)
|
|
|
|
this.writeIfNeeded('\n\n')
|
|
|
|
} else if (docNode instanceof DocSoftBreak) {
|
|
|
|
this.writeIfNeeded('\n')
|
|
|
|
} else if (docNode instanceof DocCodeSpan) {
|
|
|
|
this.write('`', docNode.code, '`')
|
|
|
|
} else if (docNode instanceof DocFencedCode) {
|
|
|
|
this.writeIfNeeded('\n').write(
|
|
|
|
'```',
|
|
|
|
docNode.language,
|
|
|
|
'\n',
|
|
|
|
await formatWithPrettier(docNode.code, { languageTag: docNode.language }),
|
|
|
|
'\n',
|
|
|
|
'```\n'
|
|
|
|
)
|
|
|
|
} else if (docNode instanceof DocEscapedText) {
|
|
|
|
this.write(docNode.encodedText)
|
2023-06-24 14:01:02 +00:00
|
|
|
} else if (docNode instanceof DocLinkTag) {
|
|
|
|
if (docNode.urlDestination) {
|
|
|
|
this.write(
|
|
|
|
'[',
|
|
|
|
docNode.linkText ?? docNode.urlDestination,
|
|
|
|
'](',
|
|
|
|
docNode.urlDestination,
|
|
|
|
')'
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
assert(docNode.codeDestination)
|
|
|
|
const apiModel = getTopLevelModel(this.apiContext)
|
|
|
|
const refResult = apiModel.resolveDeclarationReference(
|
|
|
|
docNode.codeDestination,
|
|
|
|
this.apiContext
|
|
|
|
)
|
|
|
|
|
|
|
|
if (refResult.errorMessage) {
|
|
|
|
throw new Error(refResult.errorMessage)
|
|
|
|
}
|
|
|
|
const linkedItem = assertExists(refResult.resolvedApiItem)
|
|
|
|
const path = getPath(linkedItem)
|
|
|
|
|
|
|
|
this.write(
|
|
|
|
'[',
|
|
|
|
docNode.linkText ?? getDefaultReferenceText(linkedItem),
|
|
|
|
'](/gen/',
|
|
|
|
path,
|
|
|
|
')'
|
|
|
|
)
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
} else {
|
|
|
|
throw new Error(`Unknown docNode kind: ${docNode.kind}`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async writeDocNodes(docNodes: readonly DocNode[]) {
|
|
|
|
for (const docNode of docNodes) {
|
|
|
|
await this.writeDocNode(docNode)
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
toString() {
|
|
|
|
return this.result
|
|
|
|
}
|
|
|
|
}
|
2023-06-24 14:01:02 +00:00
|
|
|
|
|
|
|
function getDefaultReferenceText(item: ApiItem): string {
|
|
|
|
function parentPrefix(str: string, sep = '.'): string {
|
|
|
|
if (!item.parent) return str
|
|
|
|
return `${getDefaultReferenceText(item.parent)}${sep}${str}`
|
|
|
|
}
|
|
|
|
switch (item.kind) {
|
|
|
|
case ApiItemKind.CallSignature:
|
|
|
|
return parentPrefix(`${item.displayName}()`)
|
|
|
|
case ApiItemKind.Constructor:
|
|
|
|
case ApiItemKind.ConstructSignature: {
|
|
|
|
const parent = assertExists(item.parent)
|
|
|
|
return `new ${getDefaultReferenceText(parent)}()`
|
|
|
|
}
|
|
|
|
case ApiItemKind.EnumMember:
|
|
|
|
case ApiItemKind.Method:
|
|
|
|
case ApiItemKind.MethodSignature:
|
|
|
|
case ApiItemKind.Property:
|
|
|
|
case ApiItemKind.PropertySignature:
|
|
|
|
return parentPrefix(item.displayName)
|
|
|
|
case ApiItemKind.IndexSignature:
|
|
|
|
return parentPrefix(`[${item.displayName}]`, '')
|
|
|
|
case ApiItemKind.Class:
|
|
|
|
case ApiItemKind.EntryPoint:
|
|
|
|
case ApiItemKind.Enum:
|
|
|
|
case ApiItemKind.Function:
|
|
|
|
case ApiItemKind.Interface:
|
|
|
|
case ApiItemKind.Model:
|
|
|
|
case ApiItemKind.Namespace:
|
|
|
|
case ApiItemKind.Package:
|
|
|
|
case ApiItemKind.TypeAlias:
|
|
|
|
case ApiItemKind.Variable:
|
|
|
|
case ApiItemKind.None:
|
|
|
|
return item.displayName
|
|
|
|
default:
|
|
|
|
exhaustiveSwitchError(item.kind)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getTopLevelModel(item: ApiItem): ApiModel {
|
|
|
|
const model = assertExists(item.getAssociatedModel())
|
|
|
|
if (model.parent) {
|
|
|
|
return getTopLevelModel(model.parent)
|
|
|
|
}
|
|
|
|
return model
|
|
|
|
}
|