tldraw/scripts/lib/docs/docs-utils.ts

238 lines
6 KiB
TypeScript
Raw Normal View History

import { ApiItem, ApiItemKind, ApiModel } from '@microsoft/api-extractor-model'
2023-04-25 11:01:25 +00:00
import {
DocCodeSpan,
DocEscapedText,
DocFencedCode,
DocLinkTag,
2023-04-25 11:01:25 +00:00
DocNode,
DocParagraph,
DocPlainText,
DocSection,
DocSoftBreak,
} from '@microsoft/tsdoc'
import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
2023-04-25 11:01:25 +00:00
import prettier from 'prettier'
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(/-$/, '')
}
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 {
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()
}
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)
} 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
}
}
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
}