From bc6ae819ecc1c9664766707fe7ff5f4433a8359c Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Fri, 3 May 2024 15:39:54 -0600 Subject: [PATCH] Add `DocumentReflection` to represent markdown documents in generated site This is a very large change and needs a significant amount of testing still. It seems to work correctly for TypeDoc's changelog, but there's still several TODO items remaining before this goes in a full release. Closes #247 Closes #1870 Closes #2288 Closes #2565 --- .config/typedoc.json | 1 + CHANGELOG.md | 20 +- src/lib/application.ts | 9 +- src/lib/converter/comments/discovery.ts | 2 + src/lib/converter/comments/parser.ts | 75 ++++++- src/lib/converter/comments/rawLexer.ts | 90 ++------ src/lib/converter/context.ts | 6 +- src/lib/converter/converter.ts | 49 +++-- src/lib/converter/plugins/CategoryPlugin.ts | 47 ++-- src/lib/converter/plugins/GroupPlugin.ts | 58 ++--- src/lib/converter/plugins/PackagePlugin.ts | 16 +- .../internationalization.ts | 4 + src/lib/internationalization/translatable.ts | 12 +- src/lib/models/ReflectionCategory.ts | 8 +- src/lib/models/ReflectionGroup.ts | 9 +- src/lib/models/reflections/abstract.ts | 5 + src/lib/models/reflections/container.ts | 103 ++++++++- src/lib/models/reflections/document.ts | 54 +++++ src/lib/models/reflections/index.ts | 11 +- src/lib/models/reflections/kind.ts | 4 + src/lib/models/reflections/project.ts | 14 +- src/lib/models/reflections/variant.ts | 2 + src/lib/output/events.ts | 13 +- .../output/plugins/JavascriptIndexPlugin.ts | 39 +++- src/lib/output/themes/MarkedPlugin.tsx | 3 +- .../output/themes/default/DefaultTheme.tsx | 53 +++-- .../default/DefaultThemeRenderContext.ts | 7 +- .../default/assets/typedoc/Application.ts | 1 - .../assets/typedoc/components/Search.ts | 5 +- .../output/themes/default/partials/icon.tsx | 10 + .../output/themes/default/partials/member.tsx | 26 ++- .../themes/default/templates/document.tsx | 10 + src/lib/serialization/deserializer.ts | 7 +- src/lib/serialization/schema.ts | 64 +++--- src/lib/utils/entry-point.ts | 61 +++++- src/lib/utils/jsx.elements.ts | 17 ++ src/lib/utils/options/declaration.ts | 2 + src/lib/utils/options/sources/typedoc.ts | 10 + src/lib/utils/sort.ts | 56 ++++- src/test/comments.test.ts | 201 ++---------------- src/test/utils/sort.test.ts | 45 +++- static/style.css | 6 + 42 files changed, 786 insertions(+), 449 deletions(-) create mode 100644 src/lib/models/reflections/document.ts create mode 100644 src/lib/output/themes/default/templates/document.tsx diff --git a/.config/typedoc.json b/.config/typedoc.json index e02a71553..a1578fd53 100644 --- a/.config/typedoc.json +++ b/.config/typedoc.json @@ -13,6 +13,7 @@ ], "sort": ["kind", "instance-first", "required-first", "alphabetical"], "entryPoints": ["../src/index.ts"], + "projectDocuments": ["../CHANGELOG.md"], "excludeExternals": true, "excludeInternal": false, "excludePrivate": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 1280082df..deb31b344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ -# Beta +# Beta (full release: 2024-06-18) -Docs needing updating: -@license, @import -sitemapBaseUrl -markedOptions -> markdownItOptions, markdownItLoader, navigation +### To Do -## Breaking Changes +- Handle YAML (ick, don't want to add a dependency for that, YAML-like?) frontmatter at the top of documents to set the category/group/title +- Handle `@document` tag to add documents to the tree anywhere +- Handle image and relative markdown links within documents +- Update website docs - consider if reworking website to just be a TypeDoc generated site is a good idea + `@license`, `@import`, sitemapBaseUrl, markedOptions -> markdownItOptions, markdownItLoader, navigation + sort - documents-first, documents-last, alphabetical-ignoring-documents + searchInDocuments +- Correctly handle the `html` being set/not set in markdown-it (currently hardcoded to `true`) + +### Breaking Changes - Drop support for Node 16. - Moved from `marked` to `markdown-it` for parsing as marked has moved to an async model which supporting would significantly complicate TypeDoc's rendering code. @@ -28,9 +34,11 @@ markedOptions -> markdownItOptions, markdownItLoader, navigation ### Features - Added support for TypeScript 5.5. +- Added new `--projectDocuments` option to specify additional Markdown documents to be included in the generated site #247, #1870, #2288, #2565. - TypeDoc now has the architecture in place to support localization. No languages besides English are currently shipped in the package, but it is now possible to add support for additional languages, #2475. - `--hostedBaseUrl` will now be used to generate a `` element in the project root page, #2550. +- Added three new sort strategies `documents-first`, `documents-last`, and `alphabetical-ignoring-documents` to order markdown documents. ### Bug Fixes diff --git a/src/lib/application.ts b/src/lib/application.ts index 501b6c3b7..4ded52a36 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -271,7 +271,7 @@ export class Application extends ChildableComponent< } catch (error) { ok(error instanceof Error); if (reportErrors) { - this.logger.error(error.message as TranslatedString); // GERRIT review + this.logger.error(error.message as TranslatedString); } } } @@ -724,6 +724,13 @@ export class Application extends ChildableComponent< ); this.logger.verbose(`Reviving projects took ${Date.now() - start}ms`); + // If we only revived one project, the project documents were set for + // it when it was created. If we revived more than one project then + // it's convenient to be able to add more documents now. + if (jsonProjects.length > 1) { + this.converter.addProjectDocuments(result); + } + this.trigger(ApplicationEvents.REVIVE, result); return result; } diff --git a/src/lib/converter/comments/discovery.ts b/src/lib/converter/comments/discovery.ts index 2b1c06c83..db8cb720c 100644 --- a/src/lib/converter/comments/discovery.ts +++ b/src/lib/converter/comments/discovery.ts @@ -102,6 +102,8 @@ const wantedKinds: Record = { ts.SyntaxKind.NamespaceExport, ts.SyntaxKind.ExportSpecifier, ], + // Non-TS kind, will never have comments. + [ReflectionKind.Document]: [], }; export interface DiscoveredComment { diff --git a/src/lib/converter/comments/parser.ts b/src/lib/converter/comments/parser.ts index 55dec2dd5..621747e04 100644 --- a/src/lib/converter/comments/parser.ts +++ b/src/lib/converter/comments/parser.ts @@ -1,4 +1,4 @@ -import { ok } from "assert"; +import assert, { ok } from "assert"; import type { CommentParserConfig } from "."; import { Comment, @@ -107,6 +107,75 @@ export function parseComment( } } +/** + * Intended for parsing markdown documents. This only parses code blocks and + * inline tags outside of code blocks, everything else is text. + * + * If you change this, also look at blockContent, as it likely needs similar + * modifications to ensure parsing is consistent. + */ +export function parseCommentString( + tokens: Generator, + config: CommentParserConfig, + file: MinimalSourceFile, + logger: Logger, +) { + const suppressWarningsConfig: CommentParserConfig = { + ...config, + jsDocCompatibility: { + defaultTag: true, + exampleTag: true, + ignoreUnescapedBraces: true, + inheritDocTag: true, + }, + }; + + const content: CommentDisplayPart[] = []; + const lexer = makeLookaheadGenerator(tokens); + + while (!lexer.done()) { + let consume = true; + const next = lexer.peek(); + + switch (next.kind) { + case TokenSyntaxKind.TypeAnnotation: + // Shouldn't have been produced by our lexer + assert(false, "Should be unreachable"); + break; + case TokenSyntaxKind.NewLine: + case TokenSyntaxKind.Text: + case TokenSyntaxKind.Tag: + case TokenSyntaxKind.CloseBrace: + content.push({ kind: "text", text: next.text }); + break; + + case TokenSyntaxKind.Code: + content.push({ kind: "code", text: next.text }); + break; + + case TokenSyntaxKind.OpenBrace: + inlineTag( + lexer, + content, + suppressWarningsConfig, + logger.i18n, + (message, token) => logger.warn(message, token.pos, file), + ); + consume = false; + break; + + default: + assertNever(next.kind); + } + + if (consume) { + lexer.take(); + } + } + + return content; +} + const HAS_USER_IDENTIFIER: `@${string}`[] = [ "@callback", "@param", @@ -391,6 +460,10 @@ function exampleBlock( } } +/** + * If you change this, also look at parseCommentString as it + * likely needs similar modifications to ensure parsing is consistent. + */ function blockContent( comment: Comment, lexer: LookaheadGenerator, diff --git a/src/lib/converter/comments/rawLexer.ts b/src/lib/converter/comments/rawLexer.ts index 63c323bd0..44d6189da 100644 --- a/src/lib/converter/comments/rawLexer.ts +++ b/src/lib/converter/comments/rawLexer.ts @@ -1,5 +1,11 @@ import { type Token, TokenSyntaxKind } from "./lexer"; +/** + * Note: This lexer intentionally *only* recognizes inline tags and code blocks. + * This is because it is intended for use on markdown documents, and we shouldn't + * take some stray `@user` mention within a "Thanks" section of someone's changelog + * as starting a block! + */ export function* lexCommentString( file: string, ): Generator { @@ -44,7 +50,7 @@ function* lexCommentString2( } let lineStart = true; - let braceStartsType = false; + let expectingTag = false; for (;;) { if (pos >= end) { @@ -59,23 +65,17 @@ function* lexCommentString2( case "\n": yield makeToken(TokenSyntaxKind.NewLine, 1); lineStart = true; + expectingTag = false; break; case "{": - if (braceStartsType && nextNonWs(pos + 1) !== "@") { - yield makeToken( - TokenSyntaxKind.TypeAnnotation, - findEndOfType(pos) - pos, - ); - braceStartsType = false; - } else { - yield makeToken(TokenSyntaxKind.OpenBrace, 1); - } + yield makeToken(TokenSyntaxKind.OpenBrace, 1); + expectingTag = true; break; case "}": yield makeToken(TokenSyntaxKind.CloseBrace, 1); - braceStartsType = false; + expectingTag = false; break; case "`": { @@ -84,7 +84,6 @@ function* lexCommentString2( // 2. Code block: <3 ticks>\n\n<3 ticks>\n // 3. Unmatched tick(s), not code, but part of some text. // We don't quite handle #2 correctly yet. PR welcome! - braceStartsType = false; let tickCount = 1; let lookahead = pos; @@ -107,6 +106,7 @@ function* lexCommentString2( text: codeText.join(""), pos, }; + expectingTag = false; pos = lookahead; break; } else if (file[lookahead] === "`") { @@ -141,9 +141,11 @@ function* lexCommentString2( text: codeText.join(""), pos, }; + expectingTag = false; pos = lookahead; } else { yield makeToken(TokenSyntaxKind.Text, tickCount); + expectingTag = false; } } @@ -166,10 +168,10 @@ function* lexCommentString2( } if ( + expectingTag && lookahead !== pos + 1 && (lookahead === end || /[\s}]/.test(file[lookahead])) ) { - braceStartsType = true; yield makeToken(TokenSyntaxKind.Tag, lookahead - pos); break; } @@ -212,7 +214,7 @@ function* lexCommentString2( textParts.push(file.substring(lookaheadStart, lookahead)); if (textParts.some((part) => /\S/.test(part))) { - braceStartsType = false; + expectingTag = false; } // This piece of text had line continuations or escaped text @@ -245,64 +247,4 @@ function* lexCommentString2( return file.startsWith("`".repeat(n), pos) && file[pos + n] !== "`"; } - - function findEndOfType(pos: number): number { - let openBraces = 0; - - while (pos < end) { - if (file[pos] === "{") { - openBraces++; - } else if (file[pos] === "}") { - if (--openBraces === 0) { - break; - } - } else if ("`'\"".includes(file[pos])) { - pos = findEndOfString(pos); - } - - pos++; - } - - if (pos < end && file[pos] === "}") { - pos++; - } - - return pos; - } - - function findEndOfString(pos: number): number { - const endOfString = file[pos]; - pos++; - while (pos < end) { - if (file[pos] === endOfString) { - break; - } else if (file[pos] === "\\") { - pos++; // Skip escaped character - } else if ( - endOfString === "`" && - file[pos] === "$" && - file[pos + 1] === "{" - ) { - // Template literal with data inside a ${} - while (pos < end && file[pos] !== "}") { - if ("`'\"".includes(file[pos])) { - pos = findEndOfString(pos) + 1; - } else { - pos++; - } - } - } - - pos++; - } - - return pos; - } - - function nextNonWs(pos: number): string | undefined { - while (pos < end && /\s/.test(file[pos])) { - pos++; - } - return file[pos]; - } } diff --git a/src/lib/converter/context.ts b/src/lib/converter/context.ts index 57c41b52d..9ab84949c 100644 --- a/src/lib/converter/context.ts +++ b/src/lib/converter/context.ts @@ -8,6 +8,7 @@ import { DeclarationReflection, ReflectionKind, ReflectionFlag, + type DocumentReflection, } from "../models/index"; import type { Converter } from "./converter"; @@ -228,10 +229,9 @@ export class Context { ); } - addChild(reflection: DeclarationReflection) { + addChild(reflection: DeclarationReflection | DocumentReflection) { if (this.scope instanceof ContainerReflection) { - this.scope.children ??= []; - this.scope.children.push(reflection); + this.scope.addChild(reflection); } } diff --git a/src/lib/converter/converter.ts b/src/lib/converter/converter.ts index f4de9c0a2..48fd20950 100644 --- a/src/lib/converter/converter.ts +++ b/src/lib/converter/converter.ts @@ -4,6 +4,7 @@ import type { Application } from "../application"; import { Comment, type CommentDisplayPart, + DocumentReflection, ProjectReflection, type Reflection, ReflectionKind, @@ -13,7 +14,13 @@ import { import { Context } from "./context"; import { ConverterComponent } from "./components"; import { Component, ChildableComponent } from "../utils/component"; -import { Option, MinimalSourceFile, readFile, unique } from "../utils"; +import { + Option, + MinimalSourceFile, + readFile, + unique, + getDocumentEntryPoints, +} from "../utils"; import { convertType } from "./types"; import { ConverterEvents } from "./converter-events"; import { convertSymbol } from "./symbols"; @@ -26,7 +33,7 @@ import type { CommentStyle, ValidationOptions, } from "../utils/options/declaration"; -import { parseComment } from "./comments/parser"; +import { parseCommentString } from "./comments/parser"; import { lexCommentString } from "./comments/rawLexer"; import { resolvePartLinks, @@ -236,6 +243,7 @@ export class Converter extends ChildableComponent< this.trigger(Converter.EVENT_BEGIN, context); + this.addProjectDocuments(project); this.compile(entryPoints, context); this.resolve(context); @@ -245,6 +253,24 @@ export class Converter extends ChildableComponent< return project; } + addProjectDocuments(project: ProjectReflection) { + const projectDocuments = getDocumentEntryPoints( + this.application.logger, + this.application.options, + ); + for (const { displayName, path } of projectDocuments) { + const file = new MinimalSourceFile(readFile(path), path); + const content = this.parseRawComment(file); + const docRefl = new DocumentReflection( + displayName, + project, + content, + ); + project.addChild(docRefl); + project.registerReflection(docRefl); + } + } + /** @internal */ convertSymbol( context: Context, @@ -272,7 +298,7 @@ export class Converter extends ChildableComponent< * Parse the given file into a comment. Intended to be used with markdown files. */ parseRawComment(file: MinimalSourceFile) { - return parseComment( + return parseCommentString( lexCommentString(file.text), this.config, file, @@ -408,23 +434,10 @@ export class Converter extends ChildableComponent< if (entryPoint.readmeFile) { const readme = readFile(entryPoint.readmeFile); - const comment = this.parseRawComment( + const content = this.parseRawComment( new MinimalSourceFile(readme, entryPoint.readmeFile), ); - - if (comment.blockTags.length || comment.modifierTags.size) { - const ignored = [ - ...comment.blockTags.map((tag) => tag.tag), - ...comment.modifierTags, - ]; - context.logger.warn( - this.application.i18n.block_and_modifier_tags_ignored_within_readme_0( - ignored.join("\n\t"), - ), - ); - } - - reflection.readme = comment.summary; + reflection.readme = content; } reflection.packageVersion = entryPoint.version; diff --git a/src/lib/converter/plugins/CategoryPlugin.ts b/src/lib/converter/plugins/CategoryPlugin.ts index 14bc76fa0..9c5b3cc48 100644 --- a/src/lib/converter/plugins/CategoryPlugin.ts +++ b/src/lib/converter/plugins/CategoryPlugin.ts @@ -2,6 +2,7 @@ import { ContainerReflection, type DeclarationReflection, Comment, + type DocumentReflection, } from "../../models"; import { ReflectionCategory } from "../../models"; import { Component, ConverterComponent } from "../components"; @@ -16,7 +17,9 @@ import { Option, getSortFunction, removeIf } from "../../utils"; */ @Component({ name: "category" }) export class CategoryPlugin extends ConverterComponent { - sortFunction!: (reflections: DeclarationReflection[]) => void; + sortFunction!: ( + reflections: Array, + ) => void; @Option("defaultCategory") accessor defaultCategory!: string; @@ -154,7 +157,7 @@ export class CategoryPlugin extends ConverterComponent { */ private getReflectionCategories( parent: ContainerReflection, - reflections: DeclarationReflection[], + reflections: Array, ): ReflectionCategory[] { const categories = new Map(); @@ -217,19 +220,23 @@ export class CategoryPlugin extends ConverterComponent { * @privateRemarks * If you change this, also update getGroups in GroupPlugin accordingly. */ - private extractCategories(reflection: DeclarationReflection) { + private extractCategories( + reflection: DeclarationReflection | DocumentReflection, + ) { const categories = CategoryPlugin.getCategories(reflection); reflection.comment?.removeTags("@category"); - for (const sig of reflection.getNonIndexSignatures()) { - sig.comment?.removeTags("@category"); - } - - if (reflection.type?.type === "reflection") { - reflection.type.declaration.comment?.removeTags("@category"); - for (const sig of reflection.type.declaration.getNonIndexSignatures()) { + if (reflection.isDeclaration()) { + for (const sig of reflection.getNonIndexSignatures()) { sig.comment?.removeTags("@category"); } + + if (reflection.type?.type === "reflection") { + reflection.type.declaration.comment?.removeTags("@category"); + for (const sig of reflection.type.declaration.getNonIndexSignatures()) { + sig.comment?.removeTags("@category"); + } + } } categories.delete(""); @@ -276,7 +283,9 @@ export class CategoryPlugin extends ConverterComponent { return aWeight - bWeight; } - static getCategories(reflection: DeclarationReflection) { + static getCategories( + reflection: DeclarationReflection | DocumentReflection, + ) { const categories = new Set(); function discoverCategories(comment: Comment | undefined) { if (!comment) return; @@ -290,15 +299,17 @@ export class CategoryPlugin extends ConverterComponent { } discoverCategories(reflection.comment); - for (const sig of reflection.getNonIndexSignatures()) { - discoverCategories(sig.comment); - } - - if (reflection.type?.type === "reflection") { - discoverCategories(reflection.type.declaration.comment); - for (const sig of reflection.type.declaration.getNonIndexSignatures()) { + if (reflection.isDeclaration()) { + for (const sig of reflection.getNonIndexSignatures()) { discoverCategories(sig.comment); } + + if (reflection.type?.type === "reflection") { + discoverCategories(reflection.type.declaration.comment); + for (const sig of reflection.type.declaration.getNonIndexSignatures()) { + discoverCategories(sig.comment); + } + } } categories.delete(""); diff --git a/src/lib/converter/plugins/GroupPlugin.ts b/src/lib/converter/plugins/GroupPlugin.ts index 540b92d5d..0b0c25359 100644 --- a/src/lib/converter/plugins/GroupPlugin.ts +++ b/src/lib/converter/plugins/GroupPlugin.ts @@ -2,6 +2,7 @@ import { ReflectionKind, ContainerReflection, type DeclarationReflection, + type DocumentReflection, } from "../../models/reflections/index"; import { ReflectionGroup } from "../../models/ReflectionGroup"; import { Component, ConverterComponent } from "../components"; @@ -40,7 +41,9 @@ const defaultGroupOrder = [ */ @Component({ name: "group" }) export class GroupPlugin extends ConverterComponent { - sortFunction!: (reflections: DeclarationReflection[]) => void; + sortFunction!: ( + reflections: Array, + ) => void; @Option("searchGroupBoosts") accessor boosts!: Record; @@ -116,22 +119,26 @@ export class GroupPlugin extends ConverterComponent { } private group(reflection: ContainerReflection) { - if ( - reflection.children && - reflection.children.length > 0 && - !reflection.groups - ) { - if ( - this.sortEntryPoints || - !reflection.children.some((c) => - c.kindOf(ReflectionKind.Module), - ) - ) { - this.sortFunction(reflection.children); + if (reflection.childrenIncludingDocuments && !reflection.groups) { + if (reflection.children) { + if ( + this.sortEntryPoints || + !reflection.children.some((c) => + c.kindOf(ReflectionKind.Module), + ) + ) { + this.sortFunction(reflection.children); + this.sortFunction(reflection.documents || []); + this.sortFunction(reflection.childrenIncludingDocuments!); + } + } else if (reflection.documents) { + this.sortFunction(reflection.documents); + this.sortFunction(reflection.childrenIncludingDocuments!); } + reflection.groups = this.getReflectionGroups( reflection, - reflection.children, + reflection.childrenIncludingDocuments, ); } } @@ -142,7 +149,7 @@ export class GroupPlugin extends ConverterComponent { * @privateRemarks * If you change this, also update extractCategories in CategoryPlugin accordingly. */ - getGroups(reflection: DeclarationReflection) { + getGroups(reflection: DeclarationReflection | DocumentReflection) { const groups = new Set(); function extractGroupTags(comment: Comment | undefined) { if (!comment) return; @@ -156,17 +163,20 @@ export class GroupPlugin extends ConverterComponent { }); } - extractGroupTags(reflection.comment); - for (const sig of reflection.getNonIndexSignatures()) { - extractGroupTags(sig.comment); - } - - if (reflection.type?.type === "reflection") { - extractGroupTags(reflection.type.declaration.comment); - for (const sig of reflection.type.declaration.getNonIndexSignatures()) { + if (reflection.isDeclaration()) { + extractGroupTags(reflection.comment); + for (const sig of reflection.getNonIndexSignatures()) { extractGroupTags(sig.comment); } + + if (reflection.type?.type === "reflection") { + extractGroupTags(reflection.type.declaration.comment); + for (const sig of reflection.type.declaration.getNonIndexSignatures()) { + extractGroupTags(sig.comment); + } + } } + // GERRIT: YAML metadata to add group groups.delete(""); if (groups.size === 0) { @@ -198,7 +208,7 @@ export class GroupPlugin extends ConverterComponent { */ getReflectionGroups( parent: ContainerReflection, - reflections: DeclarationReflection[], + reflections: Array, ): ReflectionGroup[] { const groups = new Map(); diff --git a/src/lib/converter/plugins/PackagePlugin.ts b/src/lib/converter/plugins/PackagePlugin.ts index f3255ca17..3cbd22bcb 100644 --- a/src/lib/converter/plugins/PackagePlugin.ts +++ b/src/lib/converter/plugins/PackagePlugin.ts @@ -146,23 +146,11 @@ export class PackagePlugin extends ConverterComponent { private addEntries(project: ProjectReflection) { if (this.readmeFile && this.readmeContents) { - const comment = this.application.converter.parseRawComment( + const content = this.application.converter.parseRawComment( new MinimalSourceFile(this.readmeContents, this.readmeFile), ); - if (comment.blockTags.length || comment.modifierTags.size) { - const ignored = [ - ...comment.blockTags.map((tag) => tag.tag), - ...comment.modifierTags, - ]; - this.application.logger.warn( - this.application.i18n.block_and_modifier_tags_ignored_within_readme_0( - ignored.join("\n\t"), - ), - ); - } - - project.readme = comment.summary; + project.readme = content; } if (this.packageJson) { diff --git a/src/lib/internationalization/internationalization.ts b/src/lib/internationalization/internationalization.ts index 6641edeff..8d2646a7c 100644 --- a/src/lib/internationalization/internationalization.ts +++ b/src/lib/internationalization/internationalization.ts @@ -176,6 +176,8 @@ export class Internationalization { return this.proxy.kind_type_alias(); case ReflectionKind.Reference: return this.proxy.kind_reference(); + case ReflectionKind.Document: + return this.proxy.kind_document(); } } @@ -227,6 +229,8 @@ export class Internationalization { return this.proxy.kind_plural_type_alias(); case ReflectionKind.Reference: return this.proxy.kind_plural_reference(); + case ReflectionKind.Document: + return this.proxy.kind_plural_document(); } } diff --git a/src/lib/internationalization/translatable.ts b/src/lib/internationalization/translatable.ts index de6e145b9..1a213784e 100644 --- a/src/lib/internationalization/translatable.ts +++ b/src/lib/internationalization/translatable.ts @@ -46,8 +46,6 @@ export const translatable = { "The entrypoint glob {0} did not match any files.", failed_to_parse_json_0: `Failed to parse file at {0} as json.`, - block_and_modifier_tags_ignored_within_readme_0: `Block and modifier tags will be ignored within the readme:\n\t{0}`, - converting_union_as_interface: `Using @interface on a union type will discard properties not present on all branches of the union. TypeDoc's output may not accurately describe your source code.`, converting_0_as_class_requires_value_declaration: `Converting {0} as a class requires a declaration which represents a non-type value.`, converting_0_as_class_without_construct_signatures: `{0} is being converted as a class, but does not have any construct signatures`, @@ -135,8 +133,8 @@ export const translatable = { "Watch mode does not support 'merge' style entry points.", entry_point_0_not_in_program: `The entry point {0} is not referenced by the 'files' or 'include' option in your tsconfig.`, use_expand_or_glob_for_files_in_dir: `If you wanted to include files inside this directory, set --entryPointStrategy to expand or specify a glob.`, - entry_point_0_did_not_match_any_files: `The entry point glob {0} did not match any files.`, - entry_point_0_did_not_match_any_files_after_exclude: `The entry point glob {0} did not match any files after applying exclude patterns.`, + glob_0_did_not_match_any_files: `The glob {0} did not match any files.`, + entry_point_0_did_not_match_any_files_after_exclude: `The glob {0} did not match any files after applying exclude patterns.`, entry_point_0_did_not_exist: `Provided entry point {0} does not exist.`, entry_point_0_did_not_match_any_packages: `The entry point glob {0} did not match any directories containing package.json.`, file_0_not_an_object: `The file {0} is not an object.`, @@ -183,6 +181,8 @@ export const translatable = { help_entryPoints: "The entry points of your documentation.", help_entryPointStrategy: "The strategy to be used to convert entry points into documentation modules.", + help_projectDocuments: + "Documents which should be added as children to the root of the generated documentation. Supports globs to match multiple files.", help_exclude: "Define patterns to be excluded when expanding a directory that was specified as an entry point.", help_externalPattern: @@ -261,6 +261,8 @@ export const translatable = { help_cacheBust: "Include the generation time in links to static assets.", help_searchInComments: "If set, the search index will also include comments. This will greatly increase the size of the search index.", + help_searchInDocuments: + "If set, the search index will also include documents. This will greatly increase the size of the search index.", help_cleanOutputDir: "If set, TypeDoc will remove the output directory before writing output.", help_titleLink: @@ -382,6 +384,7 @@ export const translatable = { kind_set_signature: "Set Signature", kind_type_alias: "Type Alias", kind_reference: "Reference", + kind_document: "Document", // ReflectionKind plural translations kind_plural_project: "Projects", @@ -407,6 +410,7 @@ export const translatable = { kind_plural_set_signature: "Set Signatures", kind_plural_type_alias: "Type Aliases", kind_plural_reference: "References", + kind_plural_document: "Documents", // ================================================================== // Strings that show up in the default theme diff --git a/src/lib/models/ReflectionCategory.ts b/src/lib/models/ReflectionCategory.ts index ae07a5fe5..2d2887008 100644 --- a/src/lib/models/ReflectionCategory.ts +++ b/src/lib/models/ReflectionCategory.ts @@ -1,5 +1,9 @@ import { Comment } from "./comments"; -import type { CommentDisplayPart, DeclarationReflection } from "."; +import type { + CommentDisplayPart, + DeclarationReflection, + DocumentReflection, +} from "."; import type { Serializer, JSONOutput, Deserializer } from "../serialization"; /** @@ -23,7 +27,7 @@ export class ReflectionCategory { /** * All reflections of this category. */ - children: DeclarationReflection[] = []; + children: Array = []; /** * Create a new ReflectionCategory instance. diff --git a/src/lib/models/ReflectionGroup.ts b/src/lib/models/ReflectionGroup.ts index 2f56d164d..983ec3d45 100644 --- a/src/lib/models/ReflectionGroup.ts +++ b/src/lib/models/ReflectionGroup.ts @@ -1,6 +1,11 @@ import { ReflectionCategory } from "./ReflectionCategory"; import { Comment } from "./comments"; -import type { CommentDisplayPart, DeclarationReflection, Reflection } from "."; +import type { + CommentDisplayPart, + DeclarationReflection, + DocumentReflection, + Reflection, +} from "."; import type { Serializer, JSONOutput, Deserializer } from "../serialization"; /** @@ -24,7 +29,7 @@ export class ReflectionGroup { /** * All reflections of this group. */ - children: DeclarationReflection[] = []; + children: Array = []; /** * Categories contained within this group. diff --git a/src/lib/models/reflections/abstract.ts b/src/lib/models/reflections/abstract.ts index f406e7d43..4291a6506 100644 --- a/src/lib/models/reflections/abstract.ts +++ b/src/lib/models/reflections/abstract.ts @@ -6,6 +6,7 @@ import { ReflectionKind } from "./kind"; import type { Serializer, Deserializer, JSONOutput } from "../../serialization"; import type { ReflectionVariant } from "./variant"; import type { DeclarationReflection } from "./declaration"; +import type { DocumentReflection } from "./document"; import { NonEnumerable } from "../../utils/general"; /** @@ -212,6 +213,7 @@ export class ReflectionFlags extends Array { export enum TraverseProperty { Children, + Documents, Parameters, TypeLiteral, TypeParameter, @@ -468,6 +470,9 @@ export abstract class Reflection { isDeclaration(): this is DeclarationReflection { return false; } + isDocument(): this is DocumentReflection { + return false; + } /** * Check if this reflection or any of its parents have been marked with the `@deprecated` tag. diff --git a/src/lib/models/reflections/container.ts b/src/lib/models/reflections/container.ts index b4668ea65..72a8308c4 100644 --- a/src/lib/models/reflections/container.ts +++ b/src/lib/models/reflections/container.ts @@ -7,16 +7,41 @@ import { ReflectionCategory } from "../ReflectionCategory"; import { ReflectionGroup } from "../ReflectionGroup"; import type { ReflectionKind } from "./kind"; import type { Serializer, JSONOutput, Deserializer } from "../../serialization"; +import type { DocumentReflection } from "./document"; import type { DeclarationReflection } from "./declaration"; +import { removeIfPresent } from "../../utils"; /** * @category Reflections */ export abstract class ContainerReflection extends Reflection { /** - * The children of this reflection. + * The children of this reflection. Do not add reflections to this array + * manually. Instead call {@link addChild}. */ - children?: DeclarationReflection[]; + children?: Array; + + /** + * Documents associated with this reflection. + * + * These are not children as including them as children requires code handle both + * types, despite being mostly unrelated and handled separately. + * + * Including them here in a separate array neatly handles that problem, but also + * introduces another one for rendering. When rendering, documents should really + * actually be considered part of the "children" of a reflection. For this reason, + * we also maintain a list of child declarations with child documents which is used + * when rendering. + */ + documents?: Array; + + /** + * Union of the {@link children} and {@link documents} arrays which dictates the + * sort order for rendering. + */ + childrenIncludingDocuments?: Array< + DeclarationReflection | DocumentReflection + >; /** * All children grouped by their kind. @@ -38,18 +63,63 @@ export abstract class ContainerReflection extends Reflection { return (this.children || []).filter((child) => child.kindOf(kind)); } + addChild(child: DeclarationReflection | DocumentReflection) { + if (child.isDeclaration()) { + this.children ||= []; + this.children.push(child); + } else { + this.documents ||= []; + this.documents.push(child); + } + + this.childrenIncludingDocuments ||= []; + this.childrenIncludingDocuments.push(child); + } + + removeChild(child: DeclarationReflection | DocumentReflection) { + if (child.isDeclaration()) { + removeIfPresent(this.children, child); + if (this.children?.length === 0) { + delete this.children; + } + } else { + removeIfPresent(this.documents, child); + if (this.documents?.length === 0) { + delete this.documents; + } + } + + removeIfPresent(this.childrenIncludingDocuments, child); + if (this.childrenIncludingDocuments?.length === 0) { + delete this.childrenIncludingDocuments; + } + } + override traverse(callback: TraverseCallback) { for (const child of this.children?.slice() || []) { if (callback(child, TraverseProperty.Children) === false) { return; } } + + for (const child of this.documents?.slice() || []) { + if (callback(child, TraverseProperty.Documents) === false) { + return; + } + } } override toObject(serializer: Serializer): JSONOutput.ContainerReflection { return { ...super.toObject(serializer), children: serializer.toObjectsOptional(this.children), + documents: serializer.toObjectsOptional(this.documents), + // If we only have one type of child, don't bother writing the duplicate info about + // ordering with documents to the serialized file. + childrenIncludingDocuments: + this.children?.length && this.documents?.length + ? this.childrenIncludingDocuments?.map((refl) => refl.id) + : undefined, groups: serializer.toObjectsOptional(this.groups), categories: serializer.toObjectsOptional(this.categories), }; @@ -60,6 +130,35 @@ export abstract class ContainerReflection extends Reflection { this.children = de.reviveMany(obj.children, (child) => de.constructReflection(child), ); + this.documents = de.reviveMany(obj.documents, (child) => + de.constructReflection(child), + ); + + const byId = new Map< + number, + DeclarationReflection | DocumentReflection + >(); + for (const child of this.children || []) { + byId.set(child.id, child); + } + for (const child of this.documents || []) { + byId.set(child.id, child); + } + for (const id of obj.childrenIncludingDocuments || []) { + const child = byId.get(de.oldIdToNewId[id] ?? -1); + if (child) { + this.childrenIncludingDocuments ||= []; + this.childrenIncludingDocuments.push(child); + byId.delete(de.oldIdToNewId[id] ?? -1); + } + } + if (byId.size) { + // Anything left in byId wasn't included in the childrenIncludingDocuments array. + // This is expected if we're dealing with a JSON file produced by TypeDoc 0.25. + this.childrenIncludingDocuments ||= []; + this.childrenIncludingDocuments.push(...byId.values()); + } + this.groups = de.reviveMany( obj.groups, (group) => new ReflectionGroup(group.title, this), diff --git a/src/lib/models/reflections/document.ts b/src/lib/models/reflections/document.ts new file mode 100644 index 000000000..875d179e3 --- /dev/null +++ b/src/lib/models/reflections/document.ts @@ -0,0 +1,54 @@ +import type { Deserializer, JSONOutput, Serializer } from "../../serialization"; +import { Comment, type CommentDisplayPart } from "../comments"; +import { Reflection, type TraverseCallback } from "./abstract"; +import { ReflectionKind } from "./kind"; + +/** + * Non-TS reflection type which is used to represent markdown documents included in the docs. + */ +export class DocumentReflection extends Reflection { + override readonly variant = "document"; + + /** + * The content to be displayed on the page for this reflection. + */ + content: CommentDisplayPart[]; + + /** + * A precomputed boost derived from the searchCategoryBoosts and searchGroupBoosts options, used when + * boosting search relevance scores at runtime. May be modified by plugins. + */ + relevanceBoost?: number; + + constructor( + name: string, + parent: Reflection, + content: CommentDisplayPart[], + ) { + super(name, ReflectionKind.Document, parent); + this.content = content; + } + + override isDocument(): this is DocumentReflection { + return true; + } + + override traverse(_callback: TraverseCallback): void { + // Nothing to do here, we have no children. + } + + override toObject(serializer: Serializer): JSONOutput.DocumentReflection { + return { + ...super.toObject(serializer), + variant: this.variant, + content: Comment.serializeDisplayParts(serializer, this.content), + relevanceBoost: this.relevanceBoost, + }; + } + + override fromObject(de: Deserializer, obj: JSONOutput.DocumentReflection) { + super.fromObject(de, obj); + this.content = Comment.deserializeDisplayParts(de, obj.content); + this.relevanceBoost = obj.relevanceBoost; + } +} diff --git a/src/lib/models/reflections/index.ts b/src/lib/models/reflections/index.ts index c2c3fa32f..31d8ac8cc 100644 --- a/src/lib/models/reflections/index.ts +++ b/src/lib/models/reflections/index.ts @@ -4,19 +4,20 @@ export { ReflectionFlags, TraverseProperty, } from "./abstract"; -export type { TraverseCallback, ReflectionVisitor } from "./abstract"; +export type { ReflectionVisitor, TraverseCallback } from "./abstract"; export { ContainerReflection } from "./container"; export { DeclarationReflection } from "./declaration"; export type { DeclarationHierarchy } from "./declaration"; +export { DocumentReflection } from "./document"; export { ReflectionKind } from "./kind"; export { ParameterReflection } from "./parameter"; export { ProjectReflection } from "./project"; export { ReferenceReflection } from "./reference"; -export { SignatureReflection } from "./signature"; -export { TypeParameterReflection, VarianceModifier } from "./type-parameter"; -export { splitUnquotedString } from "./utils"; -export type { ReflectionVariant } from "./variant"; export { ReflectionSymbolId, type ReflectionSymbolIdString, } from "./ReflectionSymbolId"; +export { SignatureReflection } from "./signature"; +export { TypeParameterReflection, VarianceModifier } from "./type-parameter"; +export { splitUnquotedString } from "./utils"; +export type { ReflectionVariant } from "./variant"; diff --git a/src/lib/models/reflections/kind.ts b/src/lib/models/reflections/kind.ts index 161689961..d7439a07a 100644 --- a/src/lib/models/reflections/kind.ts +++ b/src/lib/models/reflections/kind.ts @@ -28,6 +28,10 @@ export enum ReflectionKind { SetSignature = 0x100000, TypeAlias = 0x200000, Reference = 0x400000, + /** + * Generic non-ts content to be included in the generated docs as its own page. + */ + Document = 0x800000, } /** @category Reflections */ diff --git a/src/lib/models/reflections/project.ts b/src/lib/models/reflections/project.ts index de08711e2..f84594d47 100644 --- a/src/lib/models/reflections/project.ts +++ b/src/lib/models/reflections/project.ts @@ -14,6 +14,7 @@ import { ReflectionSymbolId } from "./ReflectionSymbolId"; import type { Serializer } from "../../serialization/serializer"; import type { Deserializer, JSONOutput } from "../../serialization/index"; import { DefaultMap, StableKeyMap } from "../../utils/map"; +import type { DocumentReflection } from "./document"; /** * A reflection that represents the root of the project. @@ -157,14 +158,13 @@ export class ProjectReflection extends ContainerReflection { return true; // Continue iteration } - if (property === TraverseProperty.Children) { - removeIfPresent( - parent.children, - reflection as DeclarationReflection, + if ( + property === TraverseProperty.Children || + property == TraverseProperty.Documents + ) { + parent.removeChild( + reflection as DeclarationReflection | DocumentReflection, ); - if (!parent.children?.length) { - delete parent.children; - } } else if (property === TraverseProperty.GetSignature) { delete parent.getSignature; } else if (property === TraverseProperty.IndexSignature) { diff --git a/src/lib/models/reflections/variant.ts b/src/lib/models/reflections/variant.ts index 361a855db..0ffcec521 100644 --- a/src/lib/models/reflections/variant.ts +++ b/src/lib/models/reflections/variant.ts @@ -1,4 +1,5 @@ import type { DeclarationReflection } from "./declaration"; +import type { DocumentReflection } from "./document"; import type { ParameterReflection } from "./parameter"; import type { ProjectReflection } from "./project"; import type { ReferenceReflection } from "./reference"; @@ -16,4 +17,5 @@ export interface ReflectionVariant { reference: ReferenceReflection; signature: SignatureReflection; typeParam: TypeParameterReflection; + document: DocumentReflection; } diff --git a/src/lib/output/events.ts b/src/lib/output/events.ts index 8ab246b86..d65c08339 100644 --- a/src/lib/output/events.ts +++ b/src/lib/output/events.ts @@ -5,6 +5,7 @@ import type { ProjectReflection } from "../models/reflections/project"; import type { RenderTemplate, UrlMapping } from "./models/UrlMapping"; import type { DeclarationReflection, + DocumentReflection, Reflection, ReflectionKind, } from "../models"; @@ -204,11 +205,11 @@ export class IndexEvent extends Event { * same index from {@link searchFields}. The {@link removeResult} helper * will do this for you. */ - searchResults: DeclarationReflection[]; + searchResults: Array; /** * Additional search fields to be used when creating the search index. - * `name` and `comment` may be specified to overwrite TypeDoc's search fields. + * `name`, `comment` and `document` may be specified to overwrite TypeDoc's search fields. * * Do not use `id` as a custom search field. */ @@ -216,7 +217,7 @@ export class IndexEvent extends Event { /** * Weights for the fields defined in `searchFields`. The default will weight - * `name` as 10x more important than comment content. + * `name` as 10x more important than comment and document content. * * If a field added to {@link searchFields} is not added to this object, it * will **not** be searchable. @@ -227,6 +228,7 @@ export class IndexEvent extends Event { readonly searchFieldWeights: Record = { name: 10, comment: 1, + document: 1, }; /** @@ -237,7 +239,10 @@ export class IndexEvent extends Event { this.searchFields.splice(index, 1); } - constructor(name: string, searchResults: DeclarationReflection[]) { + constructor( + name: string, + searchResults: Array, + ) { super(name); this.searchResults = searchResults; this.searchFields = Array.from( diff --git a/src/lib/output/plugins/JavascriptIndexPlugin.ts b/src/lib/output/plugins/JavascriptIndexPlugin.ts index d32a34e23..d9bfffe36 100644 --- a/src/lib/output/plugins/JavascriptIndexPlugin.ts +++ b/src/lib/output/plugins/JavascriptIndexPlugin.ts @@ -4,7 +4,9 @@ import { Builder, trimmer } from "lunr"; import { type Comment, DeclarationReflection, + DocumentReflection, ProjectReflection, + type Reflection, } from "../../models"; import { Component, RendererComponent } from "../components"; import { IndexEvent, RendererEvent } from "../events"; @@ -34,7 +36,10 @@ interface SearchDocument { @Component({ name: "javascript-index" }) export class JavascriptIndexPlugin extends RendererComponent { @Option("searchInComments") - accessor searchComments!: boolean; + private accessor searchComments!: boolean; + + @Option("searchInDocuments") + private accessor searchDocuments!: boolean; /** * Create a new JavascriptIndexPlugin instance. @@ -70,12 +75,13 @@ export class JavascriptIndexPlugin extends RendererComponent { event.project.reflections, ).filter((refl) => { return ( - refl instanceof DeclarationReflection && + (refl instanceof DeclarationReflection || + refl instanceof DocumentReflection) && refl.url && refl.name && !refl.flags.isExternal ); - }) as DeclarationReflection[]; + }) as Array; const indexEvent = new IndexEvent( IndexEvent.PREPARE_INDEX, @@ -128,6 +134,7 @@ export class JavascriptIndexPlugin extends RendererComponent { { name: reflection.name, comment: this.getCommentSearchText(reflection), + document: this.getDocumentSearchText(reflection), ...indexEvent.searchFields[rows.length], id: rows.length, }, @@ -158,18 +165,20 @@ export class JavascriptIndexPlugin extends RendererComponent { ); } - private getCommentSearchText(reflection: DeclarationReflection) { + private getCommentSearchText(reflection: Reflection) { if (!this.searchComments) return; const comments: Comment[] = []; if (reflection.comment) comments.push(reflection.comment); - reflection.signatures?.forEach( - (s) => s.comment && comments.push(s.comment), - ); - reflection.getSignature?.comment && - comments.push(reflection.getSignature.comment); - reflection.setSignature?.comment && - comments.push(reflection.setSignature.comment); + if (reflection.isDeclaration()) { + reflection.signatures?.forEach( + (s) => s.comment && comments.push(s.comment), + ); + reflection.getSignature?.comment && + comments.push(reflection.getSignature.comment); + reflection.setSignature?.comment && + comments.push(reflection.setSignature.comment); + } if (!comments.length) { return; @@ -182,4 +191,12 @@ export class JavascriptIndexPlugin extends RendererComponent { .map((part) => part.text) .join("\n"); } + + private getDocumentSearchText(reflection: Reflection) { + if (!this.searchDocuments) return; + + if (reflection.isDocument()) { + return reflection.content.flatMap((c) => c.text).join("\n"); + } + } } diff --git a/src/lib/output/themes/MarkedPlugin.tsx b/src/lib/output/themes/MarkedPlugin.tsx index d5dcdee34..5d6e30d40 100644 --- a/src/lib/output/themes/MarkedPlugin.tsx +++ b/src/lib/output/themes/MarkedPlugin.tsx @@ -191,6 +191,7 @@ export class MarkedPlugin extends ContextAwareRendererComponent { private setupParser() { this.parser = markdown({ ...(this.application.options.getValue("markdownItOptions") as {}), + html: true, highlight: (code, lang) => { code = highlight(code, lang || "ts"); code = code.replace(/\n$/, "") + "\n"; @@ -222,7 +223,7 @@ export class MarkedPlugin extends ContextAwareRendererComponent { level, }); - return `<${token.tag}>`; + return `<${token.tag} class="tsd-anchor-link">`; }; this.parser.renderer.rules["heading_close"] = (tokens, idx) => { return `${renderElement(anchorIcon(this.renderContext, `md:${this.lastHeaderSlug}`))}`; diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 015a5468c..0377c84e2 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -10,6 +10,7 @@ import { ReflectionCategory, ReflectionGroup, TypeParameterReflection, + type DocumentReflection, } from "../../../models"; import { type RenderTemplate, UrlMapping } from "../../models/UrlMapping"; import type { PageEvent } from "../../events"; @@ -113,6 +114,9 @@ export class DefaultTheme extends Theme { return new DefaultThemeRenderContext(this, pageEvent, this.application.options); } + documentTemplate = (pageEvent: PageEvent) => { + return this.getRenderContext(pageEvent).documentTemplate(pageEvent); + }; reflectionTemplate = (pageEvent: PageEvent) => { return this.getRenderContext(pageEvent).reflectionTemplate(pageEvent); }; @@ -126,7 +130,7 @@ export class DefaultTheme extends Theme { return this.getRenderContext(pageEvent).defaultLayout(template, pageEvent); }; - getReflectionClasses(reflection: DeclarationReflection) { + getReflectionClasses(reflection: DeclarationReflection | DocumentReflection) { const filters = this.application.options.getValue("visibilityFilters") as Record; return getReflectionClasses(reflection, filters); } @@ -170,6 +174,11 @@ export class DefaultTheme extends Theme { directory: "variables", template: this.reflectionTemplate, }, + { + kind: [ReflectionKind.Document], + directory: "variables", + template: this.documentTemplate, + }, ]; static URL_PREFIX = /^(http|ftp)s?:\/\//; @@ -213,11 +222,7 @@ export class DefaultTheme extends Theme { urls.push(new UrlMapping("hierarchy.html", project, this.hierarchyTemplate)); } - project.children?.forEach((child: Reflection) => { - if (child instanceof DeclarationReflection) { - this.buildUrls(child, urls); - } - }); + project.childrenIncludingDocuments?.forEach((child) => this.buildUrls(child, urls)); return urls; } @@ -246,7 +251,7 @@ export class DefaultTheme extends Theme { * @param reflection The reflection whose mapping should be resolved. * @returns The found mapping or undefined if no mapping could be found. */ - private getMapping(reflection: DeclarationReflection): TemplateMapping | undefined { + private getMapping(reflection: DeclarationReflection | DocumentReflection): TemplateMapping | undefined { return this.mappings.find((mapping) => reflection.kindOf(mapping.kind)); } @@ -257,7 +262,7 @@ export class DefaultTheme extends Theme { * @param urls The array the url should be appended to. * @returns The altered urls array. */ - buildUrls(reflection: DeclarationReflection, urls: UrlMapping[]): UrlMapping[] { + buildUrls(reflection: DeclarationReflection | DocumentReflection, urls: UrlMapping[]): UrlMapping[] { const mapping = this.getMapping(reflection); if (mapping) { if (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) { @@ -270,7 +275,7 @@ export class DefaultTheme extends Theme { } reflection.traverse((child) => { - if (child instanceof DeclarationReflection) { + if (child.isDeclaration() || child.isDocument()) { this.buildUrls(child, urls); } else { DefaultTheme.applyAnchorUrl(child, reflection); @@ -313,7 +318,7 @@ export class DefaultTheme extends Theme { return getNavigationElements(project) || []; function toNavigation( - element: ReflectionCategory | ReflectionGroup | DeclarationReflection, + element: ReflectionCategory | ReflectionGroup | DeclarationReflection | DocumentReflection, ): NavigationElement { if (element instanceof ReflectionCategory || element instanceof ReflectionGroup) { return { @@ -332,7 +337,12 @@ export class DefaultTheme extends Theme { } function getNavigationElements( - parent: ReflectionCategory | ReflectionGroup | DeclarationReflection | ProjectReflection, + parent: + | ReflectionCategory + | ReflectionGroup + | DeclarationReflection + | ProjectReflection + | DocumentReflection, ): undefined | NavigationElement[] { if (parent instanceof ReflectionCategory) { return parent.children.map(toNavigation); @@ -349,7 +359,7 @@ export class DefaultTheme extends Theme { return; } - if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project)) { + if (!parent.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project) || parent.isDocument()) { return; } @@ -363,16 +373,18 @@ export class DefaultTheme extends Theme { if ( opts.includeFolders && - parent.children?.every((child) => child.kindOf(ReflectionKind.Module)) && - parent.children.some((child) => child.name.includes("/")) + parent.childrenIncludingDocuments?.every((child) => + child.kindOf(ReflectionKind.Module | ReflectionKind.Document), + ) && + parent.childrenIncludingDocuments.some((child) => child.name.includes("/")) ) { - return deriveModuleFolders(parent.children); + return deriveModuleFolders(parent.childrenIncludingDocuments); } - return parent.children?.map(toNavigation); + return parent.childrenIncludingDocuments?.map(toNavigation); } - function deriveModuleFolders(children: DeclarationReflection[]) { + function deriveModuleFolders(children: Array) { const result: NavigationElement[] = []; const resolveOrCreateParents = ( @@ -472,14 +484,17 @@ function hasReadme(readme: string) { return !readme.endsWith("none"); } -function getReflectionClasses(reflection: DeclarationReflection, filters: Record) { +function getReflectionClasses( + reflection: DeclarationReflection | DocumentReflection, + filters: Record, +) { const classes: string[] = []; // Filter classes should match up with the settings function in // partials/navigation.tsx. for (const key of Object.keys(filters)) { if (key === "inherited") { - if (reflection.inheritedFrom) { + if (reflection.isDeclaration() && reflection.inheritedFrom) { classes.push("tsd-is-inherited"); } } else if (key === "protected") { diff --git a/src/lib/output/themes/default/DefaultThemeRenderContext.ts b/src/lib/output/themes/default/DefaultThemeRenderContext.ts index da12a998a..a31e427ec 100644 --- a/src/lib/output/themes/default/DefaultThemeRenderContext.ts +++ b/src/lib/output/themes/default/DefaultThemeRenderContext.ts @@ -5,6 +5,7 @@ import type { } from "../../../internationalization/internationalization"; import { Comment, + type DocumentReflection, type CommentDisplayPart, type DeclarationReflection, type Reflection, @@ -49,6 +50,7 @@ import { type } from "./partials/type"; import { typeAndParent } from "./partials/typeAndParent"; import { typeParameters } from "./partials/typeParameters"; import { indexTemplate } from "./templates"; +import { documentTemplate } from "./templates/document"; import { hierarchyTemplate } from "./templates/hierarchy"; import { reflectionTemplate } from "./templates/reflection"; @@ -104,7 +106,7 @@ export class DefaultThemeRenderContext { ) => { if (md instanceof Array) { return this.theme.markedPlugin.parseMarkdown( - Comment.displayPartsToMarkdown(md, this.urlTo, false), // GERRIT come back here + Comment.displayPartsToMarkdown(md, this.urlTo, true), // GERRIT come back here this.page, this, ); @@ -116,9 +118,10 @@ export class DefaultThemeRenderContext { getNavigation = () => this.theme.getNavigation(this.page.project); - getReflectionClasses = (refl: DeclarationReflection) => + getReflectionClasses = (refl: DeclarationReflection | DocumentReflection) => this.theme.getReflectionClasses(refl); + documentTemplate = bind(documentTemplate, this); reflectionTemplate = bind(reflectionTemplate, this); indexTemplate = bind(indexTemplate, this); hierarchyTemplate = bind(hierarchyTemplate, this); diff --git a/src/lib/output/themes/default/assets/typedoc/Application.ts b/src/lib/output/themes/default/assets/typedoc/Application.ts index 0406e1d27..13ee383ba 100644 --- a/src/lib/output/themes/default/assets/typedoc/Application.ts +++ b/src/lib/output/themes/default/assets/typedoc/Application.ts @@ -68,7 +68,6 @@ export class Application { public showPage() { if (!document.body.style.display) return; - console.log("Show page"); document.body.style.removeProperty("display"); this.ensureFocusedElementVisible(); this.updateIndexVisibility(); diff --git a/src/lib/output/themes/default/assets/typedoc/components/Search.ts b/src/lib/output/themes/default/assets/typedoc/components/Search.ts index 877f827af..7e1d553ac 100644 --- a/src/lib/output/themes/default/assets/typedoc/components/Search.ts +++ b/src/lib/output/themes/default/assets/typedoc/components/Search.ts @@ -120,12 +120,15 @@ function bindEvents( /** * Start searching by pressing slash. */ - document.body.addEventListener("keyup", (e) => { + document.body.addEventListener("keypress", (e) => { if (e.altKey || e.ctrlKey || e.metaKey) return; if (!field.matches(":focus") && e.key === "/") { e.preventDefault(); field.focus(); } + }); + + document.body.addEventListener("keyup", (e) => { if ( searchEl.classList.contains("has-focus") && (e.key === "Escape" || diff --git a/src/lib/output/themes/default/partials/icon.tsx b/src/lib/output/themes/default/partials/icon.tsx index c899cf5cb..b50eddeeb 100644 --- a/src/lib/output/themes/default/partials/icon.tsx +++ b/src/lib/output/themes/default/partials/icon.tsx @@ -184,6 +184,16 @@ export const icons: Record< />, "var(--color-ts-variable)", ), + [ReflectionKind.Document]: () => + kindIcon( + + + + + + , + "var(--color-document)", + ), chevronDown: () => ( + + {!!props.name && ( + + )} +
+ +
+ + ); + } + return (
diff --git a/src/lib/output/themes/default/templates/document.tsx b/src/lib/output/themes/default/templates/document.tsx new file mode 100644 index 000000000..5ea74b5e9 --- /dev/null +++ b/src/lib/output/themes/default/templates/document.tsx @@ -0,0 +1,10 @@ +import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext"; +import type { DocumentReflection } from "../../../../models"; +import type { PageEvent } from "../../../events"; +import { JSX, Raw } from "../../../../utils"; + +export const documentTemplate = ({ markdown }: DefaultThemeRenderContext, props: PageEvent) => ( +
+ +
+); diff --git a/src/lib/serialization/deserializer.ts b/src/lib/serialization/deserializer.ts index 904ca39cc..d85f4f0b8 100644 --- a/src/lib/serialization/deserializer.ts +++ b/src/lib/serialization/deserializer.ts @@ -4,6 +4,7 @@ import { ArrayType, ConditionalType, DeclarationReflection, + DocumentReflection, IndexedAccessType, InferredType, IntersectionType, @@ -66,6 +67,9 @@ export class Deserializer { declaration(parent, obj) { return new DeclarationReflection(obj.name, obj.kind, parent); }, + document(parent, obj) { + return new DocumentReflection(obj.name, parent, []); + }, param(parent, obj) { return new ParameterReflection(obj.name, obj.kind, parent); }, @@ -259,7 +263,6 @@ export class Deserializer { } const project = new ProjectReflection(name); - project.children = []; this.project = project; for (const proj of projects) { @@ -274,7 +277,7 @@ export class Deserializer { project, ); project.registerReflection(projModule); - project.children.push(projModule); + project.addChild(projModule); this.oldIdToNewId = { [proj.id]: projModule.id }; this.fromObject(projModule, proj); diff --git a/src/lib/serialization/schema.ts b/src/lib/serialization/schema.ts index 5470176b5..2409d5460 100644 --- a/src/lib/serialization/schema.ts +++ b/src/lib/serialization/schema.ts @@ -51,31 +51,33 @@ type _ModelToObject = ? ParameterReflection : T extends M.DeclarationReflection ? DeclarationReflection - : T extends M.TypeParameterReflection - ? TypeParameterReflection - : T extends M.ProjectReflection - ? ProjectReflection - : T extends M.ContainerReflection - ? ContainerReflection - : T extends M.ReferenceReflection - ? ReferenceReflection - : T extends M.Reflection - ? Reflection - : // Types - T extends M.SomeType - ? TypeKindMap[T["type"]] - : T extends M.Type - ? SomeType - : // Miscellaneous - T extends M.Comment - ? Comment - : T extends M.CommentTag - ? CommentTag - : T extends M.CommentDisplayPart - ? CommentDisplayPart - : T extends M.SourceReference - ? SourceReference - : never; + : T extends M.DocumentReflection + ? DocumentReflection + : T extends M.TypeParameterReflection + ? TypeParameterReflection + : T extends M.ProjectReflection + ? ProjectReflection + : T extends M.ContainerReflection + ? ContainerReflection + : T extends M.ReferenceReflection + ? ReferenceReflection + : T extends M.Reflection + ? Reflection + : // Types + T extends M.SomeType + ? TypeKindMap[T["type"]] + : T extends M.Type + ? SomeType + : // Miscellaneous + T extends M.Comment + ? Comment + : T extends M.CommentTag + ? CommentTag + : T extends M.CommentDisplayPart + ? CommentDisplayPart + : T extends M.SourceReference + ? SourceReference + : never; type Primitive = string | number | undefined | null | boolean; @@ -118,6 +120,11 @@ export type SomeReflection = { [K in keyof M.ReflectionVariant]: ModelToObject; }[keyof M.ReflectionVariant]; +/** @category Reflections */ +export interface DocumentReflection + extends Omit, + S {} + /** @category Reflections */ export interface ReferenceReflection extends Omit, @@ -203,7 +210,12 @@ export interface ProjectReflection /** @category Reflections */ export interface ContainerReflection extends Reflection, - S {} + S< + M.ContainerReflection, + "children" | "documents" | "groups" | "categories" + > { + childrenIncludingDocuments?: number[]; +} /** @category Reflections */ export interface Reflection diff --git a/src/lib/utils/entry-point.ts b/src/lib/utils/entry-point.ts index 32104807a..8a030fd70 100644 --- a/src/lib/utils/entry-point.ts +++ b/src/lib/utils/entry-point.ts @@ -45,6 +45,11 @@ export interface DocumentationEntryPoint { version?: string; } +export interface DocumentEntryPoint { + displayName: string; + path: string; +} + export function getEntryPoints( logger: Logger, options: Options, @@ -99,6 +104,42 @@ export function getEntryPoints( return result; } +/** + * Document entry points are markdown documents that the user has requested we include in the project with + * an option rather than a `@document` tag. + * + * @returns A list of `.md` files to include in the documentation as documents. + */ +export function getDocumentEntryPoints( + logger: Logger, + options: Options, +): DocumentEntryPoint[] { + const docGlobs = options.getValue("projectDocuments"); + if (docGlobs.length === 0) { + return []; + } + + const docPaths = expandGlobs(docGlobs, [], logger); + + // We might want to expand this in the future, there are quite a lot of extensions + // that have at some point or another been used for markdown: https://superuser.com/a/285878 + const supportedFileRegex = /\.(md|markdown)$/; + + const expanded = expandInputFiles( + logger, + docPaths, + options, + supportedFileRegex, + ); + const baseDir = options.getValue("basePath") || deriveRootDir(expanded); + return expanded.map((path) => { + return { + displayName: relative(baseDir, path).replace(/\.[^.]+$/, ""), + path, + }; + }); +} + export function getWatchEntryPoints( logger: Logger, options: Options, @@ -230,9 +271,15 @@ export function getExpandedEntryPointsForPaths( options: Options, programs = getEntryPrograms(inputFiles, logger, options), ): DocumentationEntryPoint[] { + const compilerOptions = options.getCompilerOptions(); + const supportedFileRegex = + compilerOptions.allowJs || compilerOptions.checkJs + ? /\.([cm][tj]s|[tj]sx?)$/ + : /\.([cm]ts|tsx?)$/; + return getEntryPointsForPaths( logger, - expandInputFiles(logger, inputFiles, options), + expandInputFiles(logger, inputFiles, options, supportedFileRegex), options, programs, ); @@ -254,9 +301,7 @@ function expandGlobs(inputFiles: string[], exclude: string[], logger: Logger) { if (result.length === 0) { logger.warn( - logger.i18n.entry_point_0_did_not_match_any_files( - nicePath(entry), - ), + logger.i18n.glob_0_did_not_match_any_files(nicePath(entry)), ); } else if (filtered.length === 0) { logger.warn( @@ -338,16 +383,12 @@ function expandInputFiles( logger: Logger, entryPoints: string[], options: Options, + supportedFile: RegExp, ): string[] { const files: string[] = []; const exclude = createMinimatch(options.getValue("exclude")); - const compilerOptions = options.getCompilerOptions(); - const supportedFileRegex = - compilerOptions.allowJs || compilerOptions.checkJs - ? /\.([cm][tj]s|[tj]sx?)$/ - : /\.([cm]ts|tsx?)$/; function add(file: string, entryPoint: boolean) { let stats: FS.Stats; try { @@ -365,7 +406,7 @@ function expandInputFiles( FS.readdirSync(file).forEach((next) => { add(join(file, next), false); }); - } else if (supportedFileRegex.test(file)) { + } else if (supportedFile.test(file)) { if (!entryPoint && matchesAny(exclude, file)) { return; } diff --git a/src/lib/utils/jsx.elements.ts b/src/lib/utils/jsx.elements.ts index f858080e7..51e599f0b 100644 --- a/src/lib/utils/jsx.elements.ts +++ b/src/lib/utils/jsx.elements.ts @@ -122,6 +122,7 @@ export interface IntrinsicElements { ellipse: JsxEllipseElementProps; polygon: JsxPolygonElementProps; polyline: JsxPolylineElementProps; + line: JsxLineElementProps; use: JsxUseElementProps; } @@ -1136,6 +1137,22 @@ export interface JsxPolylineElementProps pathLength?: number; } +/** Properties permitted on the `` element. + * + * Reference: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line + */ +export interface JsxLineElementProps + extends JsxSvgCoreProps, + JsxSvgStyleProps, + JsxSvgConditionalProcessingProps, + JsxSvgPresentationProps { + x1?: string | number; + y1?: string | number; + x2?: string | number; + y2?: string | number; + pathLength?: number; +} + /** * Properties permitted on the `` element. * diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index daf47e66f..cb9017cf2 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -103,6 +103,7 @@ export interface TypeDocOptionMap { // Input entryPoints: string[]; entryPointStrategy: typeof EntryPointStrategy; + projectDocuments: string[]; exclude: string[]; externalPattern: string[]; excludeExternals: boolean; @@ -162,6 +163,7 @@ export interface TypeDocOptionMap { hideGenerator: boolean; hideParameterTypesInTitle: boolean; searchInComments: boolean; + searchInDocuments: boolean; cleanOutputDir: boolean; titleLink: string; navigationLinks: ManuallyValidatedOption>; diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index acdbd775b..64203193b 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -95,6 +95,11 @@ export function addTypeDocOptions(options: Pick) { map: EntryPointStrategy, defaultValue: EntryPointStrategy.Resolve, }); + options.addDeclaration({ + name: "projectDocuments", + help: (i18n) => i18n.help_projectDocuments(), + type: ParameterType.GlobArray, + }); options.addDeclaration({ name: "exclude", @@ -464,6 +469,11 @@ export function addTypeDocOptions(options: Pick) { help: (i18n) => i18n.help_searchInComments(), type: ParameterType.Boolean, }); + options.addDeclaration({ + name: "searchInDocuments", + help: (i18n) => i18n.help_searchInDocuments(), + type: ParameterType.Boolean, + }); options.addDeclaration({ name: "cleanOutputDir", help: (i18n) => i18n.help_cleanOutputDir(), diff --git a/src/lib/utils/sort.ts b/src/lib/utils/sort.ts index 476fb65fd..1bde9f739 100644 --- a/src/lib/utils/sort.ts +++ b/src/lib/utils/sort.ts @@ -7,10 +7,12 @@ import { ReflectionKind } from "../models/reflections/kind"; import type { DeclarationReflection } from "../models/reflections/declaration"; import { LiteralType } from "../models/types"; import type { Options } from "./options"; +import type { DocumentReflection } from "../models"; export const SORT_STRATEGIES = [ "source-order", "alphabetical", + "alphabetical-ignoring-documents", "enum-value-ascending", "enum-value-descending", "enum-member-source-order", @@ -20,11 +22,14 @@ export const SORT_STRATEGIES = [ "required-first", "kind", "external-last", + "documents-first", + "documents-last", ] as const; export type SortStrategy = (typeof SORT_STRATEGIES)[number]; const defaultKindSortOrder = [ + ReflectionKind.Document, ReflectionKind.Reference, ReflectionKind.Project, ReflectionKind.Module, @@ -56,8 +61,8 @@ const defaultKindSortOrder = [ const sorts: Record< SortStrategy, ( - a: DeclarationReflection, - b: DeclarationReflection, + a: DeclarationReflection | DocumentReflection, + b: DeclarationReflection | DocumentReflection, data: { kindSortOrder: ReflectionKind[] }, ) => boolean > = { @@ -88,15 +93,31 @@ const sorts: Record< alphabetical(a, b) { return a.name < b.name; }, + "alphabetical-ignoring-documents"(a, b) { + if ( + a.kindOf(ReflectionKind.Document) || + b.kindOf(ReflectionKind.Document) + ) { + return false; + } + return a.name < b.name; + }, "enum-value-ascending"(a, b) { if ( a.kind == ReflectionKind.EnumMember && b.kind == ReflectionKind.EnumMember ) { + const aRefl = a as DeclarationReflection; + const bRefl = b as DeclarationReflection; + const aValue = - a.type instanceof LiteralType ? a.type.value : -Infinity; + aRefl.type instanceof LiteralType + ? aRefl.type.value + : -Infinity; const bValue = - b.type instanceof LiteralType ? b.type.value : -Infinity; + bRefl.type instanceof LiteralType + ? bRefl.type.value + : -Infinity; return aValue! < bValue!; } @@ -107,10 +128,17 @@ const sorts: Record< a.kind == ReflectionKind.EnumMember && b.kind == ReflectionKind.EnumMember ) { + const aRefl = a as DeclarationReflection; + const bRefl = b as DeclarationReflection; + const aValue = - a.type instanceof LiteralType ? a.type.value : -Infinity; + aRefl.type instanceof LiteralType + ? aRefl.type.value + : -Infinity; const bValue = - b.type instanceof LiteralType ? b.type.value : -Infinity; + bRefl.type instanceof LiteralType + ? bRefl.type.value + : -Infinity; return bValue! < aValue!; } @@ -155,6 +183,18 @@ const sorts: Record< "external-last"(a, b) { return !a.flags.isExternal && b.flags.isExternal; }, + "documents-first"(a, b) { + return ( + a.kindOf(ReflectionKind.Document) && + !b.kindOf(ReflectionKind.Document) + ); + }, + "documents-last"(a, b) { + return ( + !a.kindOf(ReflectionKind.Document) && + b.kindOf(ReflectionKind.Document) + ); + }, }; export function getSortFunction(opts: Options) { @@ -171,7 +211,9 @@ export function getSortFunction(opts: Options) { const strategies = opts.getValue("sort"); const data = { kindSortOrder }; - return function sortReflections(reflections: DeclarationReflection[]) { + return function sortReflections( + reflections: (DeclarationReflection | DocumentReflection)[], + ) { reflections.sort((a, b) => { for (const s of strategies) { if (sorts[s](a, b, data)) { diff --git a/src/test/comments.test.ts b/src/test/comments.test.ts index 964c4aeaf..484678f20 100644 --- a/src/test/comments.test.ts +++ b/src/test/comments.test.ts @@ -959,54 +959,22 @@ describe("Raw Lexer", () => { ]); }); - it("Should recognize tags", () => { - const tokens = lex("@tag @a @abc234"); - - equal(tokens, [ - { kind: TokenSyntaxKind.Tag, text: "@tag", pos: 0 }, - { kind: TokenSyntaxKind.Text, text: " ", pos: 4 }, - { kind: TokenSyntaxKind.Tag, text: "@a", pos: 5 }, - { kind: TokenSyntaxKind.Text, text: " ", pos: 7 }, - { kind: TokenSyntaxKind.Tag, text: "@abc234", pos: 8 }, - ]); - }); - - it("Should not indiscriminately create tags", () => { - const tokens = lex("@123 @@ @"); - equal(tokens, [ - { kind: TokenSyntaxKind.Text, text: "@123 @@ @", pos: 0 }, - ]); - }); - - it("Should allow escaping @ to prevent a tag creation", () => { - const tokens = lex("not a \\@tag"); - equal(tokens, [ - { kind: TokenSyntaxKind.Text, text: "not a @tag", pos: 0 }, - ]); - }); + it("Should not recognize tags", () => { + const tokens = lex("@123 @@ @ @tag @a @abc234"); - it("Should not mistake an email for a modifier tag", () => { - const tokens = lex("test@example.com"); - equal(tokens, [ - { kind: TokenSyntaxKind.Text, text: "test@example.com", pos: 0 }, - ]); - }); - - it("Should not mistake a scoped package for a tag", () => { - const tokens = lex("@typescript-eslint/parser @jest/globals"); equal(tokens, [ { kind: TokenSyntaxKind.Text, - text: "@typescript-eslint/parser @jest/globals", + text: "@123 @@ @ @tag @a @abc234", pos: 0, }, ]); }); - it("Should allow escaping @ in an email", () => { - const tokens = lex("test\\@example.com"); + it("Should allow escaping @ to prevent a tag creation", () => { + const tokens = lex("not a \\@tag"); equal(tokens, [ - { kind: TokenSyntaxKind.Text, text: "test@example.com", pos: 0 }, + { kind: TokenSyntaxKind.Text, text: "not a @tag", pos: 0 }, ]); }); @@ -1061,55 +1029,6 @@ describe("Raw Lexer", () => { ]); }); - it("Should handle tags after unclosed code", () => { - const tokens = lex( - dedent(` - Text - code? \`\` fake - @blockTag text - `), - ); - equal(tokens, [ - { kind: TokenSyntaxKind.Text, text: "Text", pos: 0 }, - { kind: TokenSyntaxKind.NewLine, text: "\n", pos: 4 }, - { kind: TokenSyntaxKind.Text, text: "code? `` fake", pos: 5 }, - { kind: TokenSyntaxKind.NewLine, text: "\n", pos: 18 }, - { kind: TokenSyntaxKind.Tag, text: "@blockTag", pos: 19 }, - { kind: TokenSyntaxKind.Text, text: " text", pos: 28 }, - ]); - }); - - it("Should handle a full comment", () => { - const tokens = lex( - dedent(` - This is a summary. - - @remarks - Detailed text here with a {@link Inline | inline link} - - @alpha @beta - `), - ).map((t) => ({ kind: t.kind, text: t.text })); - - equal(tokens, [ - { kind: TokenSyntaxKind.Text, text: "This is a summary." }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - { kind: TokenSyntaxKind.Tag, text: "@remarks" }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - { kind: TokenSyntaxKind.Text, text: "Detailed text here with a " }, - { kind: TokenSyntaxKind.OpenBrace, text: "{" }, - { kind: TokenSyntaxKind.Tag, text: "@link" }, - { kind: TokenSyntaxKind.Text, text: " Inline | inline link" }, - { kind: TokenSyntaxKind.CloseBrace, text: "}" }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - { kind: TokenSyntaxKind.Tag, text: "@alpha" }, - { kind: TokenSyntaxKind.Text, text: " " }, - { kind: TokenSyntaxKind.Tag, text: "@beta" }, - ]); - }); - it("Should handle unclosed code blocks", () => { const tokens = lex( dedent(` @@ -1125,107 +1044,29 @@ describe("Raw Lexer", () => { ]); }); - it("Should handle type annotations after tags at the start of a line", () => { - const tokens = lex(`@param {string} foo`); - - equal(tokens, [ - { kind: TokenSyntaxKind.Tag, text: "@param", pos: 0 }, - { kind: TokenSyntaxKind.Text, text: " ", pos: 6 }, - { kind: TokenSyntaxKind.TypeAnnotation, text: "{string}", pos: 7 }, - { kind: TokenSyntaxKind.Text, text: " foo", pos: 15 }, - ]); - }); - - it("Should handle type annotations containing string literals", () => { - const tokens = lex( - dedent(` - @param {"{{}}"} - @param {\`\${"{}"}\`} - @param {"text\\"more {}"} - @param {'{'} - EOF - `), - ); - - const expectedAnnotations = [ - '{"{{}}"}', - '{`${"{}"}`}', - '{"text\\"more {}"}', - "{'{'}", - ]; - - const expectedTokens = expectedAnnotations.flatMap((text) => [ - { kind: TokenSyntaxKind.Tag, text: "@param" }, - { kind: TokenSyntaxKind.Text, text: " " }, - { kind: TokenSyntaxKind.TypeAnnotation, text }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - ]); - expectedTokens.push({ kind: TokenSyntaxKind.Text, text: "EOF" }); - - equal( - tokens.map((t) => ({ kind: t.kind, text: t.text })), - expectedTokens, - ); - }); - - it("Should handle type annotations with object literals", () => { - const tokens = lex( - dedent(` - @param {{ a: string }} - @param {{ a: string; b: { c: { d: string }} }} - EOF - `), - ); - - const expectedAnnotations = [ - "{{ a: string }}", - "{{ a: string; b: { c: { d: string }} }}", - ]; - - const expectedTokens = expectedAnnotations.flatMap((text) => [ - { kind: TokenSyntaxKind.Tag, text: "@param" }, - { kind: TokenSyntaxKind.Text, text: " " }, - { kind: TokenSyntaxKind.TypeAnnotation, text }, - { kind: TokenSyntaxKind.NewLine, text: "\n" }, - ]); - expectedTokens.push({ kind: TokenSyntaxKind.Text, text: "EOF" }); - - equal( - tokens.map((t) => ({ kind: t.kind, text: t.text })), - expectedTokens, - ); - }); - - it("Should handle unclosed type annotations", () => { - const tokens = lex("@type {oops"); - equal(tokens, [ - { kind: TokenSyntaxKind.Tag, text: "@type", pos: 0 }, - { kind: TokenSyntaxKind.Text, text: " ", pos: 5 }, - { kind: TokenSyntaxKind.TypeAnnotation, text: "{oops", pos: 6 }, - ]); - }); - - it("Should not parse inline tags as types", () => { - const tokens = lex("@param { @link foo}"); + it("Should allow inline tags directly next to braces", () => { + const tokens = lex("{@inline}"); equal(tokens, [ - { kind: TokenSyntaxKind.Tag, text: "@param", pos: 0 }, - { kind: TokenSyntaxKind.Text, text: " ", pos: 6 }, - { kind: TokenSyntaxKind.OpenBrace, text: "{", pos: 7 }, - { kind: TokenSyntaxKind.Text, text: " ", pos: 8 }, - { kind: TokenSyntaxKind.Tag, text: "@link", pos: 9 }, - { kind: TokenSyntaxKind.Text, text: " foo", pos: 14 }, - { kind: TokenSyntaxKind.CloseBrace, text: "}", pos: 18 }, + { kind: TokenSyntaxKind.OpenBrace, text: "{", pos: 0 }, + { kind: TokenSyntaxKind.Tag, text: "@inline", pos: 1 }, + { kind: TokenSyntaxKind.CloseBrace, text: "}", pos: 8 }, ]); }); - it("Should allow inline tags directly next to braces", () => { - const tokens = lex("{@inline}"); + it("Should allow inline tags with spaces surrounding the braces", () => { + const tokens = lex("{ @link https://example.com example }"); equal(tokens, [ { kind: TokenSyntaxKind.OpenBrace, text: "{", pos: 0 }, - { kind: TokenSyntaxKind.Tag, text: "@inline", pos: 1 }, - { kind: TokenSyntaxKind.CloseBrace, text: "}", pos: 8 }, + { kind: TokenSyntaxKind.Text, text: " ", pos: 1 }, + { kind: TokenSyntaxKind.Tag, text: "@link", pos: 2 }, + { + kind: TokenSyntaxKind.Text, + text: " https://example.com example ", + pos: 7, + }, + { kind: TokenSyntaxKind.CloseBrace, text: "}", pos: 36 }, ]); }); }); diff --git a/src/test/utils/sort.test.ts b/src/test/utils/sort.test.ts index 12c730cd2..d53ef3408 100644 --- a/src/test/utils/sort.test.ts +++ b/src/test/utils/sort.test.ts @@ -1,6 +1,7 @@ import { deepStrictEqual as equal } from "assert"; import { DeclarationReflection, + DocumentReflection, LiteralType, ProjectReflection, ReflectionFlag, @@ -14,7 +15,7 @@ import { Internationalization } from "../../lib/internationalization/internation describe("Sort", () => { function sortReflections( - arr: DeclarationReflection[], + arr: Array, strategies: SortStrategy[], ) { const opts = new Options(new Internationalization(null).proxy); @@ -333,4 +334,46 @@ describe("Sort", () => { ["a", "c", "b", "d"], ); }); + + it("Should handle documents-first ordering", () => { + const proj = new ProjectReflection(""); + const a = new DocumentReflection("a", proj, []); + const b = new DocumentReflection("b", proj, []); + const c = new DeclarationReflection("c", ReflectionKind.Class, proj); + + const arr = [a, b, c]; + sortReflections(arr, ["documents-first", "alphabetical"]); + equal( + arr.map((r) => r.name), + ["a", "b", "c"], + ); + + const arr2 = [c, b, a]; + sortReflections(arr2, ["documents-first", "alphabetical"]); + equal( + arr2.map((r) => r.name), + ["a", "b", "c"], + ); + }); + + it("Should handle documents-last ordering", () => { + const proj = new ProjectReflection(""); + const a = new DocumentReflection("a", proj, []); + const b = new DocumentReflection("b", proj, []); + const c = new DeclarationReflection("c", ReflectionKind.Class, proj); + + const arr = [a, b, c]; + sortReflections(arr, ["documents-last", "alphabetical"]); + equal( + arr.map((r) => r.name), + ["c", "a", "b"], + ); + + const arr2 = [a, c, b]; + sortReflections(arr2, ["documents-last", "alphabetical"]); + equal( + arr2.map((r) => r.name), + ["c", "a", "b"], + ); + }); }); diff --git a/static/style.css b/static/style.css index da5e73a66..aba0f7ce2 100644 --- a/static/style.css +++ b/static/style.css @@ -35,6 +35,7 @@ --light-color-ts-set-signature: var(--light-color-ts-accessor); --light-color-ts-type-alias: #d51270; /* reference not included as links will be colored with the kind that it points to */ + --light-color-document: #000000; --light-external-icon: url("data:image/svg+xml;utf8,"); --light-color-scheme: light; @@ -75,6 +76,7 @@ --dark-color-ts-set-signature: var(--dark-color-ts-accessor); --dark-color-ts-type-alias: #ff6492; /* reference not included as links will be colored with the kind that it points to */ + --dark-color-document: #ffffff; --dark-external-icon: url("data:image/svg+xml;utf8,"); --dark-color-scheme: dark; @@ -116,6 +118,7 @@ --color-ts-get-signature: var(--light-color-ts-get-signature); --color-ts-set-signature: var(--light-color-ts-set-signature); --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); --external-icon: var(--light-external-icon); --color-scheme: var(--light-color-scheme); @@ -158,6 +161,7 @@ --color-ts-get-signature: var(--dark-color-ts-get-signature); --color-ts-set-signature: var(--dark-color-ts-set-signature); --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); --external-icon: var(--dark-external-icon); --color-scheme: var(--dark-color-scheme); @@ -207,6 +211,7 @@ body { --color-ts-get-signature: var(--light-color-ts-get-signature); --color-ts-set-signature: var(--light-color-ts-set-signature); --color-ts-type-alias: var(--light-color-ts-type-alias); + --color-document: var(--light-color-document); --external-icon: var(--light-external-icon); --color-scheme: var(--light-color-scheme); @@ -247,6 +252,7 @@ body { --color-ts-get-signature: var(--dark-color-ts-get-signature); --color-ts-set-signature: var(--dark-color-ts-set-signature); --color-ts-type-alias: var(--dark-color-ts-type-alias); + --color-document: var(--dark-color-document); --external-icon: var(--dark-external-icon); --color-scheme: var(--dark-color-scheme);