forked from canpolatlardanfurkan/api-docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTSDocReactEmitter.ts
More file actions
393 lines (342 loc) · 16.5 KB
/
TSDocReactEmitter.ts
File metadata and controls
393 lines (342 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
import * as React from "react"
import { renderToStaticMarkup } from "react-dom/server"
import {
DocNode,
DocNodeKind,
DocBlock,
DocCodeSpan,
DocComment,
DocDeclarationReference,
DocErrorText,
DocEscapedText,
DocFencedCode,
DocHtmlStartTag,
DocInlineTag,
DocLinkTag,
DocSection,
DocParagraph,
DocNodeTransforms,
DocParamBlock,
DocParamCollection,
DocPlainText,
PlainTextEmitter,
TSDocEmitter,
StringBuilder,
} from "@microsoft/tsdoc"
type CreateElementFn<T> = (tag: string | object, props?: object, children?: T | T[], ...rest: T[]) => T
type FragmentId = string | object
type LikeReact<T> = { createElement: CreateElementFn<T>; Fragment: FragmentId }
// Converts a TSDoc DocNode into an HTML string
// http://api-extractor.com/pages/tsdoc/syntax/
// https://github.com/Microsoft/tsdoc/blob/6034bee/spec/code-snippets/DeclarationReferences.ts
// https://github.com/Microsoft/tsdoc/tree/6034bee/tsdoc/src/emitters
// https://microsoft.github.io/tsdoc/#
export function renderTSDocToHTML(node: DocNode | undefined): string {
const element: React.ReactElement | null = render(node, React)
if (!element) return ""
if (!React.isValidElement(element)) throw new Error(`render() returned non-ReactElement: ${typeof element}`)
return renderToStaticMarkup(element)
}
// Converts a TSDoc DocNode into a tree, usually React but allows custom createElement and Fragment methods.
export function renderTSDoc<T>(node: DocNode | undefined, opts: LikeReact<T>): T | null {
return render(node, opts)
}
function render<T>(docNode: DocNode | undefined, { createElement, Fragment }: LikeReact<any>): T | null {
function renderNode(node: DocNode | undefined): T | null {
return render(node, { createElement, Fragment })
}
function renderNodes(docNodes: ReadonlyArray<DocNode | undefined>): T[] {
const rendered: (T | null)[] = []
let idx = 0
let n: DocNode | undefined
// TODO: Re-write this to not be terrible.
outer: while ((n = docNodes[idx])) {
// TSDoc may include HTML markup, it processes this in a flat stream of DocNodes
// that may look like [HtmlStartTag, HtmlStartTag, PlainText, HtmlEndTag, HtmlEndTag]
// which is fine if you're rendering everything in series but we need to handle the
// nesting manually for React.createElement(). So we break out an inner loop here
// to process the children.
if (n.kind === DocNodeKind.HtmlStartTag) {
const html = n as DocHtmlStartTag
const attrs: { [attr: string]: string } = {}
for (const attr of html.htmlAttributes) {
attrs[attr.name] = attr.value
}
// Bail early if we find a self-closing tag.
if (html.selfClosingTag) {
rendered.push(createElement(html.name, attrs))
idx += 1
continue
}
// Collect the children.
let child: DocNode | undefined
let childIdx = idx + 1 // Move on to next node and process children until we find a closing tag.
let depth = 0 // Track the depth of nested HtmlStartTags
const children: DocNode[] = []
while (!html.selfClosingTag && (child = docNodes[childIdx])) {
if (child.kind === DocNodeKind.HtmlStartTag && !(child as DocHtmlStartTag).selfClosingTag) {
depth += 1
} else if (child.kind === DocNodeKind.HtmlEndTag) {
if (depth === 0) {
idx = childIdx + 1 // update parent index and continue the outer loop
rendered.push(createElement(html.name, attrs, renderNodes(children)))
continue outer
} else {
depth -= 1
}
}
children.push(child)
childIdx += 1
}
rendered.push(createElement(html.name, attrs))
} else {
rendered.push(renderNode(n))
}
idx += 1
}
// Need to provide a key for each element...
return rendered.map((child, idx) => createElement(Fragment, { key: idx }, child))
}
if (docNode === undefined) {
return null
}
// NOTE: Fragments are used to keep the HTML structure as flat as possible for styling.
// ideally we want just one level of block elements, e.g <p>, <pre> etc.
// We use the `key` attribute to tag the fragments to help identify the type of
// DocNodeKind that created it for debugging.
const kind = docNode.kind as DocNodeKind
switch (kind) {
// A semantic group of content denoted by a block tag
case DocNodeKind.Block:
const docBlock: DocBlock = docNode as DocBlock
return createElement(Fragment, { key: "DocBlock" }, renderNode(docBlock.content))
// A block tag, e.g. @remarks or @example
case DocNodeKind.BlockTag:
return null // We don't want to show @ tags
// Inline code
case DocNodeKind.CodeSpan:
const docCodeSpan: DocCodeSpan = docNode as DocCodeSpan
return createElement("code", { className: `DocCodeSpan` }, docCodeSpan.code)
// Full TSDoc Comment denoted by /** */
case DocNodeKind.Comment:
const docComment: DocComment = docNode as DocComment
const content = renderNodes([
...docComment.customBlocks,
docComment.summarySection,
docComment.remarksBlock,
docComment.privateRemarks,
docComment.deprecatedBlock,
docComment.params,
docComment.typeParams,
docComment.returnsBlock,
docComment.inheritDocTag,
])
if (docComment.modifierTagSet.nodes.length > 0) {
//this._ensureLineSkipped()
content.push(...renderNodes(docComment.modifierTagSet.nodes))
}
return createElement(Fragment, { key: "DocComment" }, content)
// Package part of a link reference e.g. "pkg" in {@link pkg#MyClass.Button}
// http://api-extractor.com/pages/tsdoc/syntax/#api-item-references
case DocNodeKind.DeclarationReference:
const docDeclarationReference: DocDeclarationReference = docNode as DocDeclarationReference
let reference = `${docDeclarationReference.packageName || ""}${docDeclarationReference.importPath || ""}`
if (docDeclarationReference.packageName !== undefined || docDeclarationReference.importPath !== undefined) {
reference += "#"
}
return createElement(
"code",
{ className: "DocDeclarationReference" },
createElement("a", { "data-link-ref": "" }, [
reference,
...renderNodes(docDeclarationReference.memberReferences),
])
)
// Error text...
case DocNodeKind.ErrorText:
const docErrorText: DocErrorText = docNode as DocErrorText
return createElement("span", { className: "DocErrorText" }, docErrorText.text)
// ???
case DocNodeKind.EscapedText:
const docEscapedText: DocEscapedText = docNode as DocEscapedText
return createElement(Fragment, { key: "DocEscapedText" }, docEscapedText.encodedText)
// Block level code denoted by three back-ticks ```tsx
case DocNodeKind.FencedCode:
const docFencedCode: DocFencedCode = docNode as DocFencedCode
return createElement(
"pre",
{ className: "DocFencedCode" },
createElement(
"code",
{
"data-lang": docFencedCode.language,
className: `language-${docFencedCode.language}`,
},
docFencedCode.code
)
)
case DocNodeKind.HtmlAttribute:
throw new Error("Encountered unexpected HtmlAttribute. Should be handled by renderNodes()")
case DocNodeKind.HtmlEndTag:
throw new Error("Encountered unexpected HtmlEndTag. Should be handled by renderNodes()")
case DocNodeKind.HtmlStartTag:
throw new Error("Encountered unexpected HtmlStartTag. Should be handled by renderNodes()")
// @inherit
case DocNodeKind.InheritDocTag:
//const docInheritDocTag: DocInheritDocTag = docNode as DocInheritDocTag
return null // We don't want to render this tag
// Inline custom tag e.g @foo can be wrapped in curly braces to include content e.g. {@foo bar}
// http://api-extractor.com/pages/tsdoc/syntax/#inline-tags
case DocNodeKind.InlineTag:
const docInlineTag: DocInlineTag = docNode as DocInlineTag
let docInlineContent = ""
if (docInlineTag.tagContent.length > 0) {
docInlineContent = " " + docInlineTag.tagContent
}
return createElement(Fragment, { key: "DocInlineTag" }, `{${docInlineTag.tagName}${docInlineContent}}`)
// Inline @link tag.
case DocNodeKind.LinkTag:
const docLinkTag: DocLinkTag = docNode as DocLinkTag
if (docLinkTag.codeDestination) {
const ref = declarationReference(docLinkTag.codeDestination)
const name = createElement("code", {}, declarationReferenceDisplayName(docLinkTag.codeDestination))
return createElement("a", { className: "DocRefTag", "data-link-ref": ref }, docLinkTag.linkText || name)
}
let href = docLinkTag.urlDestination || "#"
return createElement("a", { className: "DocLinkTag", href }, docLinkTag.linkText)
// The "MyClass" and "foo" part of a {@link MyClass.Foo} reference
case DocNodeKind.MemberIdentifier:
throw new Error("Should be handled by renderDeclarationReference")
// The "MyClass.Foo" part of a {@link MyClass.Foo} reference
case DocNodeKind.MemberReference:
throw new Error("Should be handled by renderDeclarationReference")
// Part of a selectior {@link MyClass.(foo:1)} reference
case DocNodeKind.MemberSelector:
throw new Error("Should be handled by renderDeclarationReference")
case DocNodeKind.MemberSymbol:
throw new Error("Should be handled by renderDeclarationReference")
// Every DocBlock has a corresponding section.
case DocNodeKind.Section:
const docSection: DocSection = docNode as DocSection
if (docSection.nodes.length === 0 || !PlainTextEmitter.hasAnyTextContent(docSection)) return null
return createElement(Fragment, { key: "DocSection" }, renderNodes(docSection.nodes))
// A chunk of text seperated by two newlines.
case DocNodeKind.Paragraph:
const listDelimiter = /[*-]\s*/
const trimmedParagraph: DocParagraph = DocNodeTransforms.trimSpacesInParagraph(docNode as DocParagraph)
if (trimmedParagraph.nodes.length === 0) return null
// TSDoc doesn't support lists but we'd like to render markdown style ones so if a paragraph
// begins with a dash we break it down into list items by splitting on hyphens or asterisks.
const firstChild = trimmedParagraph.nodes[0]
const firstChildText = firstChild.kind === DocNodeKind.PlainText ? (firstChild as DocPlainText).text : null
if (!firstChildText || firstChildText.search(listDelimiter) !== 0) {
return createElement("p", {}, renderNodes(trimmedParagraph.nodes))
}
const items: DocNode[][] = []
let fragments: DocNode[] = []
trimmedParagraph.nodes.forEach(child => {
if (child.kind === DocNodeKind.PlainText) {
const textNode = child as DocPlainText
if (listDelimiter.test(textNode.text)) {
const listText = textNode.text.split(listDelimiter)
// Create a list item with first part of the current
// text string plus previous fragments.
if (fragments.length || listText[0]) {
const textNode = new DocPlainText({
configuration: docNode.configuration,
text: listText.shift() || "",
})
items.push([...fragments, textNode])
fragments = []
}
// Save the last fragment for later
if (listText.length) {
const textNode = new DocPlainText({
configuration: docNode.configuration,
text: listText.pop() || "",
})
fragments.push(textNode)
}
// Create list items for each of the middle text strings.
const listItems = listText
.filter(text => text.trim())
.map(text => [new DocPlainText({ configuration: docNode.configuration, text })])
items.push(...listItems)
} else {
fragments.push(child)
}
} else {
fragments.push(child)
}
})
// Append remaining fragments
if (fragments.length) {
// If we have only fragments add a new line entry.
if (items.length === 0) {
items.push([])
}
// If the first fragment is an empty string it's a new line
// marker. Append a new list entry.
if (fragments[0].kind === DocNodeKind.PlainText) {
const textNode = fragments[0] as DocPlainText
if (textNode.text === "") {
items.push([])
}
}
items[items.length - 1].push(...fragments)
}
const children = items.map(items => createElement("li", { key: id() }, renderNodes(items)))
return createElement("ul", {}, children)
// Defined by a @param
// http://api-extractor.com/pages/tsdoc/syntax/#param
case DocNodeKind.ParamBlock:
const docParamBlock: DocParamBlock = docNode as DocParamBlock
return createElement(
"div",
{ className: "DocParamBlock" },
createElement(Fragment, {}, docParamBlock.parameterName),
" ",
renderNode(docParamBlock.content)
)
// A collection of @param tags
case DocNodeKind.ParamCollection:
const docParamCollection: DocParamCollection = docNode as DocParamCollection
return createElement(Fragment, { key: "DocParamCollection" }, renderNodes(docParamCollection.blocks))
// Just good old fashioned text.
case DocNodeKind.PlainText:
const docPlainText: DocPlainText = docNode as DocPlainText
return createElement(Fragment, { key: "DocPlainText" }, docPlainText.text)
case DocNodeKind.Excerpt:
return null
// A single newline
case DocNodeKind.SoftBreak:
return null
default:
return assertNever(kind)
}
}
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x)
}
let moduleId = 0
function id() {
return ++moduleId
}
function declarationReferenceDisplayName(node: DocDeclarationReference): string {
let name: string[] = []
for (const member of node.memberReferences) {
if (member.hasDot) {
name.push(".")
}
if (member.memberIdentifier) {
name.push(member.memberIdentifier.identifier)
}
}
return name.join("")
}
function declarationReference(node: DocDeclarationReference) {
const emitter = new TSDocEmitter()
const builder = new StringBuilder()
emitter.renderDeclarationReference(builder, node)
return builder.toString()
}