Notes/apps/client/src/services/syntax_highlight.ts

119 lines
4.0 KiB
TypeScript
Raw Normal View History

import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
import mime_types from "./mime_types.js";
import options from "./options.js";
2025-05-26 15:21:23 +03:00
import { t } from "./i18n.js";
import { copyText } from "./clipboard.js";
let highlightingLoaded = false;
/**
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
* Additionally, adds a "Copy to clipboard" button.
2025-01-09 18:07:02 +02:00
*
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
*/
export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
const syntaxHighlightingEnabled = isSyntaxHighlightEnabled();
if (syntaxHighlightingEnabled) {
await ensureMimeTypesForHighlighting();
2025-01-09 18:07:02 +02:00
}
const codeBlocks = $container.find("pre code");
for (const codeBlock of codeBlocks) {
const normalizedMimeType = extractLanguageFromClassList(codeBlock);
if (!normalizedMimeType) {
continue;
}
2025-01-09 18:07:02 +02:00
applyCopyToClipboardButton($(codeBlock));
if (syntaxHighlightingEnabled) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
}
}
}
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
const $copyButton = $("<button>")
.addClass("bx component icon-action tn-tool-button bx-copy copy-button")
2025-05-26 15:40:57 +03:00
.attr("title", t("code_block.copy_title"))
.on("click", () => copyText($codeBlock.text()));
$codeBlock.parent().append($copyButton);
}
/**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*/
export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLElement>, normalizedMimeType: string) {
$codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text();
let highlightedText: HighlightResult | AutoHighlightResult | null = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
await ensureMimeTypesForHighlighting();
highlightedText = highlightAuto(text);
} else if (normalizedMimeType) {
2025-05-18 15:16:53 +03:00
await ensureMimeTypesForHighlighting();
highlightedText = highlight(text, { language: normalizedMimeType });
}
2025-01-09 18:07:02 +02:00
if (highlightedText) {
$codeBlock.html(highlightedText.value);
}
}
2025-05-18 15:16:53 +03:00
export async function ensureMimeTypesForHighlighting() {
if (highlightingLoaded) {
return;
}
// Load theme.
2025-05-18 17:50:31 +03:00
const currentThemeName = String(options.get("codeBlockTheme"));
loadHighlightingTheme(currentThemeName);
// Load mime types.
const mimeTypes = mime_types.getMimeTypes();
await ensureMimeTypes(mimeTypes);
highlightingLoaded = true;
}
export function loadHighlightingTheme(themeName: string) {
2025-05-18 17:50:31 +03:00
const themePrefix = "default:";
let theme: Theme | null = null;
if (themeName.includes(themePrefix)) {
theme = Themes[themeName.substring(themePrefix.length)];
2025-05-18 17:50:31 +03:00
}
if (!theme) {
theme = Themes.default;
}
loadTheme(theme);
2025-05-18 15:16:53 +03:00
}
/**
* Indicates whether syntax highlighting should be enabled for code blocks, by querying the value of the `codeblockTheme` option.
* @returns whether syntax highlighting should be enabled for code blocks.
*/
export function isSyntaxHighlightEnabled() {
const theme = options.get("codeBlockTheme");
return !!theme && theme !== "none";
}
/**
* Given a HTML element, tries to extract the `language-` class name out of it.
2025-01-09 18:07:02 +02:00
*
* @param el the HTML element from which to extract the language tag.
* @returns the normalized MIME type (e.g. `text-css` instead of `language-text-css`).
*/
function extractLanguageFromClassList(el: HTMLElement) {
const prefix = "language-";
for (const className of el.classList) {
if (className.startsWith(prefix)) {
return className.substring(prefix.length);
}
}
return null;
2025-01-09 18:07:02 +02:00
}