Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add language dropdown to code_block nodes #104

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
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>
b-kelly marked this conversation as resolved.
Show resolved Hide resolved
`;

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