Skip to content

Commit 7f39d70

Browse files
committed
Add highlightLanguages option
1 parent 8bb3596 commit 7f39d70

File tree

9 files changed

+109
-26
lines changed

9 files changed

+109
-26
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
This means that any projects setting `markedOptions` needs to be updated to use `markdownItOptions`.
1717
Unlike `marked@4`, `markdown-it` pushes lots of functionality to plugins. To use plugins, a JavaScript config file must be used with the `markdownItLoader` option.
1818
- Updated Shiki from 0.14 to 1.x. This should mostly be a transparent update which adds another 23 supported languages and 13 supported themes.
19+
As Shiki adds additional languages, the time it takes to load the highlighter increases linearly. To avoid rendering taking longer than necessary,
20+
TypeDoc now only loads a few common languages. Additional languages can be loaded by setting the `--highlightLanguages` option.
1921
- Renamed `--sitemapBaseUrl` to `--hostedBaseUrl` to reflect that it can be used for more than just the sitemap.
2022
- Removed deprecated `navigation.fullTree` option.
2123
- (WIP) Removed `--media` option, TypeDoc will now detect image links within your comments and markdown documents and automatically copy them to the site.
@@ -47,6 +49,7 @@
4749
- Added three new sort strategies `documents-first`, `documents-last`, and `alphabetical-ignoring-documents` to order markdown documents.
4850
- Added new `--alwaysCreateEntryPointModule` option. When set, TypeDoc will always create a `Module` for entry points, even if only one is provided.
4951
If `--projectDocuments` is used to add documents, this option defaults to `true`, otherwise, defaults to `false`.
52+
- Added new `--highlightLanguages` option to control what Shiki language packages are loaded.
5053

5154
### Bug Fixes
5255

src/lib/internationalization/translatable.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export const translatable = {
113113
// output plugins
114114
custom_css_file_0_does_not_exist: `Custom CSS file at {0} does not exist.`,
115115
unsupported_highlight_language_0_not_highlighted_in_comment_for_1: `Unsupported highlight language {0} will not be highlighted in comment for {1}.`,
116+
unloaded_language_0_not_highlighted_in_comment_for_1: `Code block with language {0} will not be highlighted in comment for {1} as it was not included in the highlightLanguages option.`,
116117
yaml_frontmatter_not_an_object: `Expected YAML frontmatter to be an object.`,
117118

118119
// renderer
@@ -218,6 +219,8 @@ export const translatable = {
218219
"Specify the code highlighting theme in light mode.",
219220
help_darkHighlightTheme:
220221
"Specify the code highlighting theme in dark mode.",
222+
help_highlightLanguages:
223+
"Specify the languages which will be loaded to highlight code when rendering.",
221224
help_customCss: "Path to a custom CSS file to for the theme to import.",
222225
help_markdownItOptions:
223226
"Specify the options passed to markdown-it, the Markdown parser used by TypeDoc.",
@@ -350,6 +353,8 @@ export const translatable = {
350353
external_symbol_link_mappings_must_be_object:
351354
"externalSymbolLinkMappings must be a Record<package name, Record<symbol name, link>>",
352355
highlight_theme_0_must_be_one_of_1: "{0} must be one of the following: {1}",
356+
highlightLanguages_contains_invalid_languages_0:
357+
"highlightLanguages contains invalid languages: {0}, run typedoc --help for a list of supported languages.",
353358
hostedBaseUrl_must_start_with_http:
354359
"hostedBaseUrl must start with http:// or https://",
355360
option_0_must_be_an_object: "The '{0}' option must be a non-array object.",

src/lib/output/renderer.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import { RendererComponent } from "./components";
2020
import { Component, ChildableComponent } from "../utils/component";
2121
import { Option, EventHooks } from "../utils";
2222
import { loadHighlighter } from "../utils/highlighter";
23-
import type { BundledTheme as ShikiTheme } from "shiki" with { "resolution-mode": "import" };
23+
import type {
24+
BundledLanguage,
25+
BundledTheme as ShikiTheme,
26+
} from "shiki" with { "resolution-mode": "import" };
2427
import { Reflection } from "../models";
2528
import type { JsxElement } from "../utils/jsx.elements";
2629
import type { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext";
@@ -187,35 +190,32 @@ export class Renderer extends ChildableComponent<
187190

188191
/** @internal */
189192
@Option("theme")
190-
accessor themeName!: string;
193+
private accessor themeName!: string;
191194

192-
/** @internal */
193195
@Option("cleanOutputDir")
194-
accessor cleanOutputDir!: boolean;
196+
private accessor cleanOutputDir!: boolean;
195197

196-
/** @internal */
197198
@Option("cname")
198-
accessor cname!: string;
199+
private accessor cname!: string;
199200

200-
/** @internal */
201201
@Option("githubPages")
202-
accessor githubPages!: boolean;
202+
private accessor githubPages!: boolean;
203203

204204
/** @internal */
205205
@Option("cacheBust")
206206
accessor cacheBust!: boolean;
207207

208-
/** @internal */
209208
@Option("lightHighlightTheme")
210-
accessor lightTheme!: ShikiTheme;
209+
private accessor lightTheme!: ShikiTheme;
211210

212-
/** @internal */
213211
@Option("darkHighlightTheme")
214-
accessor darkTheme!: ShikiTheme;
212+
private accessor darkTheme!: ShikiTheme;
213+
214+
@Option("highlightLanguages")
215+
private accessor highlightLanguages!: string[];
215216

216-
/** @internal */
217217
@Option("pretty")
218-
accessor pretty!: boolean;
218+
private accessor pretty!: boolean;
219219

220220
renderStartTime = -1;
221221

@@ -299,7 +299,12 @@ export class Renderer extends ChildableComponent<
299299
}
300300

301301
private async loadHighlighter() {
302-
await loadHighlighter(this.lightTheme, this.darkTheme);
302+
await loadHighlighter(
303+
this.lightTheme,
304+
this.darkTheme,
305+
// Checked in option validation
306+
this.highlightLanguages as BundledLanguage[],
307+
);
303308
}
304309

305310
/**

src/lib/output/themes/MarkedPlugin.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import markdown from "markdown-it";
33
import { Component, ContextAwareRendererComponent } from "../components";
44
import { type RendererEvent, MarkdownEvent, type PageEvent } from "../events";
55
import { Option, type Logger, renderElement } from "../../utils";
6-
import { highlight, isSupportedLanguage } from "../../utils/highlighter";
6+
import { highlight, isLoadedLanguage, isSupportedLanguage } from "../../utils/highlighter";
77
import type { BundledTheme } from "shiki" with { "resolution-mode": "import" };
88
import { escapeHtml, getTextContent } from "../../utils/html";
99
import type { DefaultTheme } from "..";
@@ -72,6 +72,15 @@ export class MarkedPlugin extends ContextAwareRendererComponent {
7272
);
7373
return text;
7474
}
75+
if (!isLoadedLanguage(lang)) {
76+
this.application.logger.warn(
77+
this.application.i18n.unloaded_language_0_not_highlighted_in_comment_for_1(
78+
lang,
79+
this.page?.model.getFriendlyFullName() ?? "(unknown)",
80+
),
81+
);
82+
return text;
83+
}
7584

7685
return highlight(text, lang);
7786
}

src/lib/utils/highlighter.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as JSX from "./jsx";
99
import { unique } from "./array";
1010

1111
const aliases = new Map<string, string>();
12+
let supportedLanguagesWithoutAliases: string[] = [];
1213
let supportedLanguages: string[] = [];
1314
let supportedThemes: string[] = [];
1415

@@ -28,6 +29,8 @@ export async function loadShikiMetadata() {
2829
...shiki.bundledLanguagesInfo.map((lang) => lang.id),
2930
]).sort();
3031

32+
supportedLanguagesWithoutAliases = unique(["text", ...shiki.bundledLanguagesInfo.map((lang) => lang.id)]);
33+
3134
supportedThemes = Object.keys(shiki.bundledThemes);
3235
}
3336

@@ -40,6 +43,10 @@ class DoubleHighlighter {
4043
private dark: BundledTheme,
4144
) {}
4245

46+
supports(lang: string) {
47+
return this.highlighter.getLoadedLanguages().includes(lang);
48+
}
49+
4350
highlight(code: string, lang: string) {
4451
const tokens = this.highlighter.codeToTokensWithThemes(code, {
4552
themes: { light: this.light, dark: this.dark },
@@ -122,11 +129,11 @@ class DoubleHighlighter {
122129

123130
let highlighter: DoubleHighlighter | undefined;
124131

125-
export async function loadHighlighter(lightTheme: BundledTheme, darkTheme: BundledTheme) {
132+
export async function loadHighlighter(lightTheme: BundledTheme, darkTheme: BundledTheme, langs: BundledLanguage[]) {
126133
if (highlighter) return;
127134

128135
const shiki = await import("shiki");
129-
const hl = await shiki.getHighlighter({ themes: [lightTheme, darkTheme], langs: getSupportedLanguages() });
136+
const hl = await shiki.getHighlighter({ themes: [lightTheme, darkTheme], langs });
130137
highlighter = new DoubleHighlighter(hl, lightTheme, darkTheme);
131138
}
132139

@@ -139,16 +146,22 @@ export function getSupportedLanguages(): string[] {
139146
return supportedLanguages;
140147
}
141148

149+
export function getSupportedLanguagesWithoutAliases(): string[] {
150+
ok(supportedLanguagesWithoutAliases.length > 0, "loadShikiMetadata has not been called");
151+
return supportedLanguages;
152+
}
153+
142154
export function getSupportedThemes(): string[] {
143155
ok(supportedThemes.length > 0, "loadShikiMetadata has not been called");
144156
return supportedThemes;
145157
}
146158

159+
export function isLoadedLanguage(lang: string): boolean {
160+
return highlighter?.supports(lang) ?? false;
161+
}
162+
147163
export function highlight(code: string, lang: string): string {
148164
assert(highlighter, "Tried to highlight with an uninitialized highlighter");
149-
if (!isSupportedLanguage(lang)) {
150-
return code;
151-
}
152165

153166
if (lang === "text") {
154167
return JSX.renderElement(<>{code}</>);

src/lib/utils/options/declaration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export interface TypeDocOptionMap {
135135
theme: string;
136136
lightHighlightTheme: ShikiTheme;
137137
darkHighlightTheme: ShikiTheme;
138+
highlightLanguages: string[];
138139
customCss: string;
139140
markdownItOptions: ManuallyValidatedOption<Record<string, unknown>>;
140141
/**

src/lib/utils/options/help.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
ParameterType,
66
type DeclarationOption,
77
} from "./declaration";
8-
import { getSupportedLanguages, getSupportedThemes } from "../highlighter";
8+
import {
9+
getSupportedLanguagesWithoutAliases,
10+
getSupportedThemes,
11+
} from "../highlighter";
912
import type { TranslationProxy } from "../../internationalization/internationalization";
1013

1114
export interface ParameterHelp {
@@ -102,7 +105,7 @@ export function getOptionsHelp(
102105
output.push(
103106
"",
104107
"Supported highlighting languages:",
105-
...toEvenColumns(getSupportedLanguages(), 80),
108+
...toEvenColumns(getSupportedLanguagesWithoutAliases(), 80),
106109
);
107110

108111
output.push(

src/lib/utils/options/sources/typedoc.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ import { ReflectionKind } from "../../../models/reflections/kind";
1212
import * as Validation from "../../validation";
1313
import { blockTags, inlineTags, modifierTags } from "../tsdoc-defaults";
1414
import { getEnumKeys } from "../../enum";
15-
import type { BundledTheme } from "shiki" with { "resolution-mode": "import" };
16-
import { getSupportedThemes } from "../../highlighter";
15+
import type {
16+
BundledLanguage,
17+
BundledTheme,
18+
} from "shiki" with { "resolution-mode": "import" };
19+
import {
20+
getSupportedLanguagesWithoutAliases,
21+
getSupportedThemes,
22+
} from "../../highlighter";
23+
import { setDifference } from "../../set";
1724

1825
// For convenience, added in the same order as they are documented on the website.
1926
export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
@@ -319,6 +326,35 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
319326
}
320327
},
321328
});
329+
options.addDeclaration({
330+
name: "highlightLanguages",
331+
help: (i18n) => i18n.help_highlightLanguages(),
332+
type: ParameterType.Array,
333+
defaultValue: [
334+
"bash",
335+
"console",
336+
"css",
337+
"html",
338+
"javascript",
339+
"json",
340+
"jsonc",
341+
"tsx",
342+
"typescript",
343+
] satisfies BundledLanguage[],
344+
validate(value, i18n) {
345+
const invalid = setDifference(
346+
value,
347+
getSupportedLanguagesWithoutAliases(),
348+
);
349+
if (invalid.size) {
350+
throw new Error(
351+
i18n.highlightLanguages_contains_invalid_languages_0(
352+
Array.from(invalid).join(", "),
353+
),
354+
);
355+
}
356+
},
357+
});
322358

323359
options.addDeclaration({
324360
name: "customCss",

src/lib/utils/set.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export function setIntersection<T>(a: Set<T>, b: Set<T>): Set<T> {
1+
export function setIntersection<T>(a: Iterable<T>, b: Set<T>): Set<T> {
22
const result = new Set<T>();
33
for (const elem of a) {
44
if (b.has(elem)) {
@@ -7,3 +7,11 @@ export function setIntersection<T>(a: Set<T>, b: Set<T>): Set<T> {
77
}
88
return result;
99
}
10+
11+
export function setDifference<T>(a: Iterable<T>, b: Iterable<T>): Set<T> {
12+
const result = new Set(a);
13+
for (const elem of b) {
14+
result.delete(elem);
15+
}
16+
return result;
17+
}

0 commit comments

Comments
 (0)