Skip to content
93 changes: 76 additions & 17 deletions src/rich-text/node-views/code-block.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -14,7 +17,7 @@ export class CodeBlockView implements NodeView {
dom: HTMLElement | null;
contentDOM?: HTMLElement | null;

private language: string = null;
private language: ReturnType<CodeBlockView["getLanguageFromBlock"]> = null;

constructor(
node: ProsemirrorNode,
Expand All @@ -38,7 +41,7 @@ export class CodeBlockView implements NodeView {
const rawLanguage = this.getLanguageFromBlock(node);

const processorApplies = this.getValidProcessorResult(
rawLanguage,
rawLanguage.raw,
node
);

Expand All @@ -57,7 +60,6 @@ export class CodeBlockView implements NodeView {
const randomId = generateRandomId();

this.dom.innerHTML = escapeHTML`
<div class="ps-absolute t2 r4 fs-fine pe-none us-none fc-black-300 js-language-indicator" contenteditable=false></div>
<div class="d-flex ps-absolute t0 r0 js-processor-toggle">
<label class="flex--item mr4" for="js-editor-toggle-${randomId}">
Edit
Expand All @@ -68,7 +70,8 @@ export class CodeBlockView implements NodeView {
</div>
</div>
<div class="d-none js-processor-view"></div>
<pre class="s-code-block js-code-view js-code-mode"><code class="content-dom"></code></pre>`;
<pre class="s-code-block js-code-view js-code-mode"><code class="content-dom"></code></pre>
<div class="s-select s-select__sm ps-absolute t6 r6 js-language-indicator"><select class="js-lang-select"></select></div>`;

this.contentDOM = this.dom.querySelector(".content-dom");

Expand All @@ -91,6 +94,8 @@ export class CodeBlockView implements NodeView {
})
);
});

this.initializeLanguageSelect(view, getPos);
}

/** Switches the view between editor mode and processor mode */
Expand All @@ -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<HTMLSelectElement>(
".js-language-indicator"
).value = rawLanguage.raw;
this.language = rawLanguage;
this.updateDisplayedLanguage();
}
}

Expand Down Expand Up @@ -182,4 +186,59 @@ export class CodeBlockView implements NodeView {

return processors;
}

private initializeLanguageSelect(view: EditorView, getPos: getPosParam) {
const $sel =
this.dom.querySelector<HTMLSelectElement>(".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<HTMLSelectElement>(".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");
}
}
}
84 changes: 46 additions & 38 deletions src/shared/highlighting/highlight-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down
1 change: 1 addition & 0 deletions src/shared/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down