diff --git a/CHANGELOG.md b/CHANGELOG.md index a1b8ad952..aa5050ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,18 @@ - 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. This means that any projects setting `markedOptions` needs to be updated to use `markdownItOptions`. 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. +- Updated Shiki from 0.14 to 1.3. This should mostly be a transparent update which adds another 23 supported languages and 13 supported themes. - Removed deprecated `navigation.fullTree` option. - API: `MapOptionDeclaration.mapError` has been removed. - API: Deprecated `BindOption` decorator has been removed. - API: `DeclarationReflection.indexSignature` has been renamed to `DeclarationReflection.indexSignatures`. Note: This also affects JSON serialization. TypeDoc will support JSON output from 0.25 until 0.28. +### Features + +- 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. + ### Bug Fixes - TypeDoc now supports objects with multiple index signatures, #2470. diff --git a/package-lock.json b/package-lock.json index 2969a58e8..287f695e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.4", - "shiki": "^0.14.7" + "shiki": "^1.3.0" }, "bin": { "typedoc": "bin/typedoc" @@ -629,6 +629,11 @@ "node": ">= 8" } }, + "node_modules/@shikijs/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.3.0.tgz", + "integrity": "sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1003,11 +1008,6 @@ "node": ">=8" } }, - "node_modules/ansi-sequence-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==" - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2226,11 +2226,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3061,14 +3056,11 @@ } }, "node_modules/shiki": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.3.0.tgz", + "integrity": "sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==", "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" + "@shikijs/core": "1.3.0" } }, "node_modules/signal-exit": { @@ -3426,16 +3418,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/vscode-oniguruma": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" - }, - "node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==" - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 796fd7e5d..7e05930bd 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.4", - "shiki": "^0.14.7" + "shiki": "^1.3.0" }, "peerDependencies": { "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x" diff --git a/src/index.ts b/src/index.ts index ed1969820..1cf34e7cc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export { Renderer, DefaultTheme, DefaultThemeRenderContext, + Slugger, UrlMapping, Theme, PageEvent, @@ -105,5 +106,12 @@ export { SerializeEvent, } from "./lib/serialization"; +export { + type TranslationProxy, + type TranslatedString, + type TranslatableStrings, + Internationalization, +} from "./lib/internationalization/internationalization"; + import TypeScript from "typescript"; export { TypeScript }; diff --git a/src/lib/application.ts b/src/lib/application.ts index 27a276337..501b6c3b7 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -46,6 +46,7 @@ import { Internationalization, type TranslatedString, } from "./internationalization/internationalization"; +import { loadShikiMetadata } from "./utils/highlighter"; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageInfo = require("../../package.json") as { @@ -188,6 +189,7 @@ export class Application extends ChildableComponent< options: Partial = {}, readers: readonly OptionsReader[] = DEFAULT_READERS, ): Promise { + await loadShikiMetadata(); const app = new Application(DETECTOR); readers.forEach((r) => app.options.addReader(r)); app.options.reset(); @@ -217,6 +219,7 @@ export class Application extends ChildableComponent< options: Partial = {}, readers: readonly OptionsReader[] = DEFAULT_READERS, ): Promise { + await loadShikiMetadata(); const app = new Application(DETECTOR); readers.forEach((r) => app.options.addReader(r)); await app._bootstrap(options); diff --git a/src/lib/output/index.ts b/src/lib/output/index.ts index 9e63faa68..49c59d1ed 100644 --- a/src/lib/output/index.ts +++ b/src/lib/output/index.ts @@ -6,6 +6,7 @@ export type { RendererHooks } from "./renderer"; export { Theme } from "./theme"; export { DefaultTheme, + Slugger, type NavigationElement, } from "./themes/default/DefaultTheme"; export { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext"; diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index 3f8b65d6d..215401cb2 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -20,7 +20,7 @@ import { RendererComponent } from "./components"; import { Component, ChildableComponent } from "../utils/component"; import { Option, EventHooks } from "../utils"; import { loadHighlighter } from "../utils/highlighter"; -import type { Theme as ShikiTheme } from "shiki"; +import type { BundledTheme as ShikiTheme } from "shiki" with { "resolution-mode": "import" }; import { Reflection } from "../models"; import type { JsxElement } from "../utils/jsx.elements"; import type { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext"; diff --git a/src/lib/output/themes/MarkedPlugin.tsx b/src/lib/output/themes/MarkedPlugin.tsx index e144a343d..d15c00c79 100644 --- a/src/lib/output/themes/MarkedPlugin.tsx +++ b/src/lib/output/themes/MarkedPlugin.tsx @@ -6,7 +6,7 @@ import { Component, ContextAwareRendererComponent } from "../components"; import { type RendererEvent, MarkdownEvent, type PageEvent } from "../events"; import { Option, readFile, copySync, isFile, type Logger } from "../../utils"; import { highlight, isSupportedLanguage } from "../../utils/highlighter"; -import type { Theme } from "shiki"; +import type { BundledTheme } from "shiki" with { "resolution-mode": "import" }; import { escapeHtml, getTextContent } from "../../utils/html"; import type { DefaultTheme } from ".."; import { Slugger } from "./default/DefaultTheme"; @@ -33,10 +33,10 @@ export class MarkedPlugin extends ContextAwareRendererComponent { accessor mediaSource!: string; @Option("lightHighlightTheme") - accessor lightTheme!: Theme; + accessor lightTheme!: BundledTheme; @Option("darkHighlightTheme") - accessor darkTheme!: Theme; + accessor darkTheme!: BundledTheme; private parser?: MarkdownIt; diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 882c8de58..015a5468c 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -49,6 +49,9 @@ export interface NavigationElement { children?: NavigationElement[]; } +/** + * Responsible for getting a unique anchor for elements within a page. + */ export class Slugger { private seen = new Map(); diff --git a/src/lib/utils/highlighter.tsx b/src/lib/utils/highlighter.tsx index c41e6f1a9..69604c1bb 100644 --- a/src/lib/utils/highlighter.tsx +++ b/src/lib/utils/highlighter.tsx @@ -1,74 +1,61 @@ -import { ok as assert } from "assert"; -import { BUNDLED_LANGUAGES, getHighlighter, type Highlighter, type Theme } from "shiki"; -import { unique, zip } from "./array"; +import { ok as assert, ok } from "assert"; +import type { BundledLanguage, BundledTheme, Highlighter, TokenStyles } from "shiki" with { "resolution-mode": "import" }; import * as JSX from "./jsx"; +import { unique } from "./array"; const aliases = new Map(); -for (const lang of BUNDLED_LANGUAGES) { - for (const alias of lang.aliases || []) { - aliases.set(alias, lang.id); +let supportedLanguages: string[] = []; +let supportedThemes: string[] = []; + +export async function loadShikiMetadata() { + if (aliases.size) return; + + const shiki = await import("shiki"); + for (const lang of shiki.bundledLanguagesInfo) { + for (const alias of lang.aliases || []) { + aliases.set(alias, lang.id); + } } -} -const supportedLanguages = unique(["text", ...aliases.keys(), ...BUNDLED_LANGUAGES.map((lang) => lang.id)]).sort(); + supportedLanguages = unique([ + "text", + ...aliases.keys(), + ...shiki.bundledLanguagesInfo.map((lang) => lang.id), + ]).sort(); + + supportedThemes = Object.keys(shiki.bundledThemes); +} class DoubleHighlighter { private schemes = new Map(); constructor( private highlighter: Highlighter, - private light: Theme, - private dark: Theme, + private light: BundledTheme, + private dark: BundledTheme, ) {} highlight(code: string, lang: string) { - const lightTokens = this.highlighter.codeToThemedTokens(code, lang, this.light, { includeExplanation: false }); - const darkTokens = this.highlighter.codeToThemedTokens(code, lang, this.dark, { includeExplanation: false }); - - // If this fails... something went *very* wrong. - assert(lightTokens.length === darkTokens.length); + const tokens = this.highlighter.codeToTokensWithThemes(code, { + themes: { light: this.light, dark: this.dark }, + lang: lang as BundledLanguage, + }); const docEls: JSX.Element[] = []; - for (const [lightLine, darkLine] of zip(lightTokens, darkTokens)) { - // Different themes can have different rules for when colors change... so unfortunately we have to deal with different - // sets of tokens.Example: light_plus and dark_plus tokenize " = " differently in the `schemes` - // declaration for this file. - - while (lightLine.length && darkLine.length) { - // Simple case, same token. - if (lightLine[0].content === darkLine[0].content) { - docEls.push( - - {lightLine[0].content} - , - ); - lightLine.shift(); - darkLine.shift(); - continue; - } - - if (lightLine[0].content.length < darkLine[0].content.length) { - docEls.push( - - {lightLine[0].content} - , - ); - darkLine[0].content = darkLine[0].content.substring(lightLine[0].content.length); - lightLine.shift(); - continue; - } - + for (const line of tokens) { + for (const token of line) { docEls.push( - {darkLine[0].content}, + + {token.content} + , ); - lightLine[0].content = lightLine[0].content.substring(darkLine[0].content.length); - darkLine.shift(); } docEls.push(
); } + docEls.pop(); // Remove last
docEls.pop(); // Remove last
return JSX.renderElement(<>{docEls}); @@ -121,8 +108,8 @@ class DoubleHighlighter { return style.join("\n"); } - private getClass(lightColor?: string, darkColor?: string): string { - const key = `${lightColor} | ${darkColor}`; + private getClass(variants: Record): string { + const key = `${variants["light"].color} | ${variants["dark"].color}`; let scheme = this.schemes.get(key); if (scheme == null) { scheme = `hl-${this.schemes.size}`; @@ -134,9 +121,11 @@ class DoubleHighlighter { let highlighter: DoubleHighlighter | undefined; -export async function loadHighlighter(lightTheme: Theme, darkTheme: Theme) { +export async function loadHighlighter(lightTheme: BundledTheme, darkTheme: BundledTheme) { if (highlighter) return; - const hl = await getHighlighter({ themes: [lightTheme, darkTheme] }); + + const shiki = await import("shiki"); + const hl = await shiki.getHighlighter({ themes: [lightTheme, darkTheme], langs: getSupportedLanguages() }); highlighter = new DoubleHighlighter(hl, lightTheme, darkTheme); } @@ -145,9 +134,15 @@ export function isSupportedLanguage(lang: string) { } export function getSupportedLanguages(): string[] { + ok(supportedLanguages.length > 0, "loadShikiMetadata has not been called"); return supportedLanguages; } +export function getSupportedThemes(): string[] { + ok(supportedThemes.length > 0, "loadShikiMetadata has not been called"); + return supportedThemes; +} + export function highlight(code: string, lang: string): string { assert(highlighter, "Tried to highlight with an uninitialized highlighter"); if (!isSupportedLanguage(lang)) { diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index 8bf9053bf..3645a9791 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -1,4 +1,4 @@ -import type { Theme as ShikiTheme } from "shiki"; +import type { BundledTheme as ShikiTheme } from "shiki" with { "resolution-mode": "import"}; import type { LogLevel } from "../loggers"; import type { SortStrategy } from "../sort"; import { isAbsolute, join, resolve } from "path"; diff --git a/src/lib/utils/options/help.ts b/src/lib/utils/options/help.ts index 5b7c318c3..e5f2a1a96 100644 --- a/src/lib/utils/options/help.ts +++ b/src/lib/utils/options/help.ts @@ -5,8 +5,7 @@ import { ParameterType, type DeclarationOption, } from "./declaration"; -import { getSupportedLanguages } from "../highlighter"; -import { BUNDLED_THEMES } from "shiki"; +import { getSupportedLanguages, getSupportedThemes } from "../highlighter"; import type { TranslationProxy } from "../../internationalization/internationalization"; export interface ParameterHelp { @@ -69,7 +68,7 @@ function toEvenColumns(values: string[], maxLineWidth: number) { const columnWidth = values.reduce((acc, val) => Math.max(acc, val.length), 0) + 2; - const numColumns = Math.max(1, Math.min(maxLineWidth / columnWidth)); + const numColumns = Math.max(1, Math.floor(maxLineWidth / columnWidth)); let line = ""; const out: string[] = []; @@ -109,7 +108,7 @@ export function getOptionsHelp( output.push( "", "Supported highlighting themes:", - ...toEvenColumns(BUNDLED_THEMES, 80), + ...toEvenColumns(getSupportedThemes(), 80), ); return output.join("\n"); diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 0b7235277..65293d224 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -6,13 +6,14 @@ import { EmitStrategy, CommentStyle, } from "../declaration"; -import { BUNDLED_THEMES, type Theme } from "shiki"; import { SORT_STRATEGIES } from "../../sort"; import { EntryPointStrategy } from "../../entry-point"; import { ReflectionKind } from "../../../models/reflections/kind"; import * as Validation from "../../validation"; import { blockTags, inlineTags, modifierTags } from "../tsdoc-defaults"; import { getEnumKeys } from "../../enum"; +import type { BundledTheme } from "shiki" with { "resolution-mode": "import"}; +import { getSupportedThemes } from "../../highlighter"; // For convenience, added in the same order as they are documented on the website. export function addTypeDocOptions(options: Pick) { @@ -271,8 +272,8 @@ export function addTypeDocOptions(options: Pick) { defaultValue: "default", }); - const defaultLightTheme: Theme = "light-plus"; - const defaultDarkTheme: Theme = "dark-plus"; + const defaultLightTheme: BundledTheme = "light-plus"; + const defaultDarkTheme: BundledTheme = "dark-plus"; options.addDeclaration({ name: "lightHighlightTheme", @@ -280,11 +281,11 @@ export function addTypeDocOptions(options: Pick) { type: ParameterType.String, defaultValue: defaultLightTheme, validate(value, i18n) { - if (!(BUNDLED_THEMES as readonly string[]).includes(value)) { + if (!getSupportedThemes().includes(value)) { throw new Error( i18n.highlight_theme_0_must_be_one_of_1( "lightHighlightTheme", - BUNDLED_THEMES.join(", "), + getSupportedThemes().join(", "), ), ); } @@ -296,11 +297,11 @@ export function addTypeDocOptions(options: Pick) { type: ParameterType.String, defaultValue: defaultDarkTheme, validate(value, i18n) { - if (!(BUNDLED_THEMES as readonly string[]).includes(value)) { + if (!getSupportedThemes().includes(value)) { throw new Error( i18n.highlight_theme_0_must_be_one_of_1( "darkHighlightTheme", - BUNDLED_THEMES.join(", "), + getSupportedThemes().join(", "), ), ); } diff --git a/src/test/utils/options/default-options.test.ts b/src/test/utils/options/default-options.test.ts index 3cdf09577..f7ae99ddd 100644 --- a/src/test/utils/options/default-options.test.ts +++ b/src/test/utils/options/default-options.test.ts @@ -1,5 +1,4 @@ import { ok, throws, strictEqual, doesNotThrow } from "assert"; -import { BUNDLED_THEMES } from "shiki"; import { Options } from "../../../lib/utils"; import { Internationalization } from "../../../lib/internationalization/internationalization"; @@ -11,17 +10,14 @@ describe("Default Options", () => { throws(() => opts.setValue("lightHighlightTheme", "randomTheme" as never), ); - opts.setValue("lightHighlightTheme", BUNDLED_THEMES[0]); - strictEqual( - opts.getValue("lightHighlightTheme"), - BUNDLED_THEMES[0], - ); + opts.setValue("lightHighlightTheme", "github-light"); + strictEqual(opts.getValue("lightHighlightTheme"), "github-light"); throws(() => opts.setValue("darkHighlightTheme", "randomTheme" as never), ); - opts.setValue("darkHighlightTheme", BUNDLED_THEMES[0]); - strictEqual(opts.getValue("darkHighlightTheme"), BUNDLED_THEMES[0]); + opts.setValue("darkHighlightTheme", "github-light"); + strictEqual(opts.getValue("darkHighlightTheme"), "github-light"); }); }); diff --git a/tsconfig.json b/tsconfig.json index 7730e63ff..271954e70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ // Speed up dev compilation time "incremental": true, "tsBuildInfoFile": "dist/.tsbuildinfo", - // "skipLibCheck": true, + // Shiki's types are broken again, https://github.com/shikijs/shiki/issues/665 + "skipLibCheck": true, "strict": true, "alwaysStrict": true,