diff --git a/src/rich-text/node-views/code-block.ts b/src/rich-text/node-views/code-block.ts index ba9d5f7f..ee56a193 100644 --- a/src/rich-text/node-views/code-block.ts +++ b/src/rich-text/node-views/code-block.ts @@ -1,7 +1,10 @@ import { Node as ProsemirrorNode } from "prosemirror-model"; import { EditorView, NodeView } from "prosemirror-view"; import type { IExternalPluginProvider } from "../../shared/editor-plugin"; -import { getBlockLanguage } from "../../shared/highlighting/highlight-plugin"; +import { + getBlockLanguage, + getLoadedLanguages, +} from "../../shared/highlighting/highlight-plugin"; import { _t } from "../../shared/localization"; import { escapeHTML, generateRandomId } from "../../shared/utils"; @@ -14,7 +17,7 @@ export class CodeBlockView implements NodeView { dom: HTMLElement | null; contentDOM?: HTMLElement | null; - private language: string = null; + private language: ReturnType = null; constructor( node: ProsemirrorNode, @@ -38,7 +41,7 @@ export class CodeBlockView implements NodeView { const rawLanguage = this.getLanguageFromBlock(node); const processorApplies = this.getValidProcessorResult( - rawLanguage, + rawLanguage.raw, node ); @@ -57,7 +60,6 @@ export class CodeBlockView implements NodeView { const randomId = generateRandomId(); this.dom.innerHTML = escapeHTML` -
-
`; +
+
`; this.contentDOM = this.dom.querySelector(".content-dom"); @@ -91,6 +94,8 @@ export class CodeBlockView implements NodeView { }) ); }); + + this.initializeLanguageSelect(view, getPos); } /** Switches the view between editor mode and processor mode */ @@ -106,24 +111,23 @@ export class CodeBlockView implements NodeView { /** Gets the codeblock language from the node */ private getLanguageFromBlock(node: ProsemirrorNode) { - let autodetectedLanguage = node.attrs + const autodetectedLanguage = node.attrs .detectedHighlightLanguage as string; - if (autodetectedLanguage) { - autodetectedLanguage = _t("nodes.codeblock_lang_auto", { - lang: autodetectedLanguage, - }); - } - - return autodetectedLanguage || getBlockLanguage(node); + return { + raw: autodetectedLanguage || getBlockLanguage(node, "auto"), + autodetected: !!autodetectedLanguage, + }; } /** Updates the edit/code view */ - private updateCodeBlock(rawLanguage: string) { - if (this.language !== rawLanguage) { - this.dom.querySelector(".js-language-indicator").textContent = - rawLanguage; + private updateCodeBlock(rawLanguage: CodeBlockView["language"]) { + if (this.language?.raw !== rawLanguage.raw) { + this.dom.querySelector( + ".js-language-indicator" + ).value = rawLanguage.raw; this.language = rawLanguage; + this.updateDisplayedLanguage(); } } @@ -182,4 +186,59 @@ export class CodeBlockView implements NodeView { return processors; } + + private initializeLanguageSelect(view: EditorView, getPos: getPosParam) { + const $sel = + this.dom.querySelector(".js-lang-select"); + + // add an "auto" dropdown that we can target via JS + const autoOpt = document.createElement("option"); + autoOpt.textContent = "auto"; + autoOpt.value = "auto"; + autoOpt.className = "js-auto-option"; + $sel.appendChild(autoOpt); + + getLoadedLanguages().forEach((lang) => { + const opt = document.createElement("option"); + opt.value = lang; + opt.textContent = lang; + opt.defaultSelected = lang === this.language?.raw; + $sel.appendChild(opt); + }); + + if (typeof getPos !== "function") { + return; + } + + // when the dropdown is changed, update the language on the node + $sel.addEventListener("change", (e) => { + e.stopPropagation(); + + const newLang = $sel.value; + + view.dispatch( + view.state.tr.setNodeMarkup(getPos(), null, { + params: newLang === "auto" ? null : newLang, + detectedHighlightLanguage: null, + }) + ); + }); + } + + private updateDisplayedLanguage() { + const lang = this.language?.raw; + const $sel = + this.dom.querySelector(".js-lang-select"); + const $auto = $sel.querySelector(".js-auto-option"); + + if (this.language?.autodetected) { + $sel.value = "auto"; + $auto.textContent = _t("nodes.codeblock_lang_auto", { + lang, + }); + } else { + $sel.value = lang; + $auto.textContent = _t("nodes.codeblock_auto"); + } + } } diff --git a/src/shared/highlighting/highlight-plugin.ts b/src/shared/highlighting/highlight-plugin.ts index 22e56b81..057c2808 100644 --- a/src/shared/highlighting/highlight-plugin.ts +++ b/src/shared/highlighting/highlight-plugin.ts @@ -7,44 +7,47 @@ import { getHljsInstance } from "./hljs-instance"; * Register the languages we're going to use here so we can strongly type our inputs */ //TODO missing: regex -type Language = - | "markdown" - | "bash" - | "cpp" - | "csharp" - | "coffeescript" - | "xml" - | "java" - | "json" - | "perl" - | "python" - | "ruby" - | "clojure" - | "css" - | "dart" - | "erlang" - | "go" - | "haskell" - | "javascript" - | "kotlin" - | "tex" - | "lisp" - | "scheme" - | "lua" - | "matlab" - | "mathematica" - | "ocaml" - | "pascal" - | "protobuf" - | "r" - | "rust" - | "scala" - | "sql" - | "swift" - | "vhdl" - | "vbscript" - | "yml" - | "none"; +const SUPPORTED_LANGS = [ + "plaintext", + "markdown", + "bash", + "cpp", + "csharp", + "coffeescript", + "xml", + "java", + "json", + "perl", + "python", + "ruby", + "clojure", + "css", + "dart", + "erlang", + "go", + "haskell", + "javascript", + "kotlin", + "tex", + "lisp", + "scheme", + "lua", + "matlab", + "mathematica", + "ocaml", + "pascal", + "protobuf", + "r", + "rust", + "scala", + "sql", + "swift", + "vhdl", + "vbscript", + "yml", +] as const; + +type Language = typeof SUPPORTED_LANGS[number]; // Aliases are neatly grouped onto the same line, so tell prettier not to format // prettier-ignore @@ -103,6 +106,11 @@ export function getBlockLanguage( return dealiasLanguage(rawLanguage); } +/** Returns all supported language codes */ +export function getLoadedLanguages() { + return SUPPORTED_LANGS; +} + /** * Plugin that highlights all code within all code_blocks in the parent */ diff --git a/src/shared/localization.ts b/src/shared/localization.ts index adb61893..a5e392d8 100644 --- a/src/shared/localization.ts +++ b/src/shared/localization.ts @@ -70,6 +70,7 @@ export const defaultStrings = { mode_toggle_richtext_title: "Rich text mode" as string, }, nodes: { + codeblock_auto: "auto" as string, codeblock_lang_auto: ({ lang }: { lang: string }) => `${lang} (auto)`, spoiler_reveal_text: "Reveal spoiler" as string, },