29044867dd
This PR adds the docs app back into the tldraw monorepo. ## Deploying We'll want to update our deploy script to update the SOURCE_SHA to the newest release sha... and then deploy the docs pulling api.json files from that release. We _could_ update the docs on every push to main, but we don't have to unless something has changed. Right now there's no automated deployments from this repo. ## Side effects To make this one work, I needed to update the lock file. This might be ok (new year new lock file), and everything builds as expected, though we may want to spend some time with our scripts to be sure that things are all good. I also updated our prettier installation, which decided to add trailing commas to every generic type. Which is, I suppose, [correct behavior](https://github.com/prettier/prettier-vscode/issues/955)? But that caused diffs in every file, which is unfortunate. ### Change Type - [x] `internal` — Any other changes that don't affect the published package[^2]
243 lines
5.9 KiB
TypeScript
243 lines
5.9 KiB
TypeScript
import { ApiItem, ApiItemKind, ApiModel } from '@microsoft/api-extractor-model'
|
|
import {
|
|
DocCodeSpan,
|
|
DocEscapedText,
|
|
DocFencedCode,
|
|
DocLinkTag,
|
|
DocNode,
|
|
DocParagraph,
|
|
DocPlainText,
|
|
DocSection,
|
|
DocSoftBreak,
|
|
} from '@microsoft/tsdoc'
|
|
import assert from 'assert'
|
|
import GithubSlugger from 'github-slugger'
|
|
|
|
const slugs = new GithubSlugger()
|
|
|
|
import path from 'path'
|
|
import prettier from 'prettier'
|
|
export const API_DIR = path.join(process.cwd(), 'api')
|
|
export const CONTENT_DIR = path.join(process.cwd(), 'content')
|
|
|
|
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:
|
|
throw Error(itemKind)
|
|
}
|
|
}
|
|
|
|
export function getSlug(item: ApiItem): string {
|
|
return slugs.slug(item.displayName, true)
|
|
}
|
|
|
|
export function getPath(item: ApiItem): string {
|
|
if (isOnParentPage(item.kind)) {
|
|
const parentPath = getPath(item.parent!)
|
|
const childSlug = getSlug(item)
|
|
return `${parentPath}#${childSlug}`
|
|
}
|
|
|
|
return item.canonicalReference
|
|
.toString()
|
|
.replace(/^@tldraw\//, '')
|
|
.replace(/:.+$/, '')
|
|
.replace(/!/g, '/')
|
|
.replace(/\./g, '-')
|
|
}
|
|
|
|
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 = await 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)
|
|
await writer.writeDocNode(docNode)
|
|
return writer.toString()
|
|
}
|
|
|
|
private constructor(private readonly apiContext: ApiItem) {}
|
|
|
|
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 = refResult.resolvedApiItem!
|
|
const path = getPath(linkedItem)
|
|
|
|
this.write(
|
|
'[',
|
|
docNode.linkText ?? getDefaultReferenceText(linkedItem),
|
|
'](/gen/',
|
|
path,
|
|
')'
|
|
)
|
|
}
|
|
} 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 = 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:
|
|
throw Error(item.kind)
|
|
}
|
|
}
|
|
|
|
function getTopLevelModel(item: ApiItem): ApiModel {
|
|
const model = item.getAssociatedModel()!
|
|
if (model.parent) {
|
|
return getTopLevelModel(model.parent)
|
|
}
|
|
return model
|
|
}
|