Skip to content
8 changes: 6 additions & 2 deletions src/rich-text/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,12 @@ export class RichTextEditor extends BaseView {
],
}),
nodeViews: {
code_block(node: ProseMirrorNode) {
return new CodeBlockView(node);
code_block(
node: ProseMirrorNode,
view: EditorView,
getPos: () => number
) {
return new CodeBlockView(node, view, getPos);
},
image(
node: ProseMirrorNode,
Expand Down
87 changes: 73 additions & 14 deletions src/rich-text/node-views/code-block.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
import { Node as ProsemirrorNode } from "prosemirror-model";
import { NodeView } from "prosemirror-view";
import { getBlockLanguage } from "../../shared/highlighting/highlight-plugin";
import { EditorView, NodeView } from "prosemirror-view";
import {
getBlockLanguage,
getLoadedLanguages,
} from "../../shared/highlighting/highlight-plugin";
import { escapeHTML } from "../../shared/utils";

type getPosParam = boolean | (() => number);

/**
* View with <code> wrapping/decorations for code_block nodes
*/
export class CodeBlockView implements NodeView {
dom?: HTMLElement | null;
contentDOM?: HTMLElement | null;

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

constructor(node: ProsemirrorNode) {
constructor(node: ProsemirrorNode, view: EditorView, getPos: getPosParam) {
this.dom = document.createElement("div");
this.dom.classList.add("ps-relative", "p0", "ws-normal", "ow-normal");

const rawLanguage = this.getLanguageFromBlock(node);
this.language = rawLanguage;

this.dom.innerHTML = escapeHTML`
<div class="ps-absolute t2 r4 fs-fine pe-none us-none fc-black-300 js-language-indicator" contenteditable=false>${rawLanguage}</div>
<pre class="s-code-block"><code class="content-dom"></code></pre>
<div class="s-select s-select__sm ps-absolute t6 r6"><select class="js-lang-select"></select></div>
`;

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

this.initializeLanguageSelect(view, getPos);
this.updateDisplayedLanguage();
}

update(node: ProsemirrorNode): boolean {
Expand All @@ -35,24 +43,75 @@ export class CodeBlockView implements NodeView {

const rawLanguage = this.getLanguageFromBlock(node);

if (this.language !== rawLanguage) {
this.dom.querySelector(".js-language-indicator").textContent =
rawLanguage;
if (this.language.raw !== rawLanguage.raw) {
this.language = rawLanguage;
this.updateDisplayedLanguage();
}

return true;
}

private getLanguageFromBlock(node: ProsemirrorNode) {
let autodetectedLanguage = node.attrs
.detectedHighlightLanguage as string;
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();

if (autodetectedLanguage) {
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";
// TODO localization
autodetectedLanguage += " (auto)";
$auto.textContent = lang + " (auto)";
} else {
$sel.value = lang;
$auto.textContent = "auto";
}
}

private getLanguageFromBlock(node: ProsemirrorNode) {
const autodetectedLanguage = node.attrs
.detectedHighlightLanguage as string;

return autodetectedLanguage || getBlockLanguage(node);
return {
raw: autodetectedLanguage || getBlockLanguage(node, "auto"),
autodetected: !!autodetectedLanguage,
};
}
}
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 dealiasLangauge(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