mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: add code-block rich editor extension
This commit is contained in:
@@ -58,7 +58,7 @@ export default async function highlightSyntax(elem, siteSettings, session) {
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureHighlightJs(langFile) {
|
||||
export async function ensureHighlightJs(langFile) {
|
||||
try {
|
||||
if (!hljsLoadPromise) {
|
||||
hljsLoadPromise = loadHighlightJs(langFile);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { highlightPlugin } from "prosemirror-highlightjs";
|
||||
import { ensureHighlightJs } from "discourse/lib/highlight-syntax";
|
||||
|
||||
// cached hljs instance with custom plugins/languages
|
||||
let hljs;
|
||||
|
||||
class CodeBlockWithLangSelectorNodeView {
|
||||
#selectAdded = false;
|
||||
|
||||
constructor(node, view, getPos) {
|
||||
this.node = node;
|
||||
this.view = view;
|
||||
this.getPos = getPos;
|
||||
|
||||
const code = document.createElement("code");
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(code);
|
||||
pre.classList.add("code-block");
|
||||
|
||||
this.dom = pre;
|
||||
this.contentDOM = code;
|
||||
|
||||
this.appendSelect();
|
||||
}
|
||||
|
||||
changeListener(e) {
|
||||
this.view.dispatch(
|
||||
this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||
params: e.target.value,
|
||||
})
|
||||
);
|
||||
|
||||
if (e.target.firstChild.textContent) {
|
||||
e.target.firstChild.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
appendSelect() {
|
||||
if (!hljs || this.#selectAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#selectAdded = true;
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.contentEditable = false;
|
||||
select.addEventListener("change", (e) => this.changeListener(e));
|
||||
select.classList.add("code-language-select");
|
||||
|
||||
const languages = hljs.listLanguages();
|
||||
|
||||
const empty = document.createElement("option");
|
||||
empty.textContent = languages.includes(this.node.attrs.params)
|
||||
? ""
|
||||
: this.node.attrs.params;
|
||||
select.appendChild(empty);
|
||||
|
||||
languages.forEach((lang) => {
|
||||
const option = document.createElement("option");
|
||||
option.textContent = lang;
|
||||
option.selected = lang === this.node.attrs.params;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
this.dom.appendChild(select);
|
||||
}
|
||||
|
||||
update(node) {
|
||||
this.appendSelect();
|
||||
|
||||
return node.type === this.node.type;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dom.removeEventListener("change", (e) => this.changeListener(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {RichEditorExtension} */
|
||||
const extension = {
|
||||
nodeViews: { code_block: CodeBlockWithLangSelectorNodeView },
|
||||
plugins({ pmState: { Plugin }, getContext }) {
|
||||
return [
|
||||
async () =>
|
||||
highlightPlugin(
|
||||
(hljs = await ensureHighlightJs(
|
||||
getContext().session.highlightJsPath
|
||||
)),
|
||||
["code_block", "html_block"]
|
||||
),
|
||||
new Plugin({
|
||||
props: {
|
||||
// Handles removal of the code_block when it's at the start of the document
|
||||
handleKeyDown(view, event) {
|
||||
if (
|
||||
event.key === "Backspace" &&
|
||||
view.state.selection.$from.parent.type ===
|
||||
view.state.schema.nodes.code_block &&
|
||||
view.state.selection.$from.start() === 1 &&
|
||||
view.state.selection.$from.parentOffset === 0
|
||||
) {
|
||||
const { tr } = view.state;
|
||||
|
||||
const codeBlock = view.state.selection.$from.parent;
|
||||
const paragraph = view.state.schema.nodes.paragraph.create(
|
||||
null,
|
||||
codeBlock.content
|
||||
);
|
||||
tr.replaceWith(
|
||||
view.state.selection.$from.before(),
|
||||
view.state.selection.$from.after(),
|
||||
paragraph
|
||||
);
|
||||
tr.setSelection(
|
||||
new view.state.selection.constructor(tr.doc.resolve(1))
|
||||
);
|
||||
|
||||
view.dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default extension;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { registerRichEditorExtension } from "discourse/lib/composer/rich-editor-extensions";
|
||||
import codeBlock from "./code-block";
|
||||
|
||||
/**
|
||||
* List of default extensions
|
||||
@@ -6,6 +7,6 @@ import { registerRichEditorExtension } from "discourse/lib/composer/rich-editor-
|
||||
*
|
||||
* @type {RichEditorExtension[]}
|
||||
*/
|
||||
const defaultExtensions = [];
|
||||
const defaultExtensions = [codeBlock];
|
||||
|
||||
defaultExtensions.forEach(registerRichEditorExtension);
|
||||
|
||||
@@ -23,10 +23,15 @@ export async function testMarkdown(
|
||||
@onSetup={{handleSetup}}
|
||||
/>
|
||||
</template>);
|
||||
|
||||
// ensure toggling to rich editor and back works
|
||||
await click(".composer-toggle-switch");
|
||||
await click(".composer-toggle-switch");
|
||||
await click(".composer-toggle-switch");
|
||||
|
||||
await waitFor(".ProseMirror");
|
||||
await settled();
|
||||
|
||||
const editor = document.querySelector(".ProseMirror");
|
||||
|
||||
// typeIn for contentEditable isn't reliable, and is slower
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
|
||||
|
||||
module(
|
||||
"Integration | Component | prosemirror-editor - code-block extension",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const select = (lang = "") =>
|
||||
`<select contenteditable="false" class="code-language-select"><option>${lang}</option><option>javascript</option><option>ruby</option><option>sql</option></select>`;
|
||||
|
||||
Object.entries({
|
||||
"basic code block": [
|
||||
"```plaintext\nconsole.log('Hello, world!');\n```",
|
||||
`<pre class="code-block"><code>console.log('Hello, world!');</code>${select(
|
||||
"plaintext"
|
||||
)}</pre>`,
|
||||
"```plaintext\nconsole.log('Hello, world!');\n```",
|
||||
],
|
||||
"code block within list item": [
|
||||
"- ```plaintext\n console.log('Hello, world!');\n ```",
|
||||
`<ul><li><pre class="code-block"><code>console.log('Hello, world!');</code>${select(
|
||||
"plaintext"
|
||||
)}</pre></li></ul>`,
|
||||
"* ```plaintext\n console.log('Hello, world!');\n ```",
|
||||
],
|
||||
"code block with language": [
|
||||
'```javascript\nconsole.log("Hello, world!");\n```',
|
||||
`<pre class="code-block"><code><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">"Hello, world!"</span>);</code>${select()}</pre>`,
|
||||
'```javascript\nconsole.log("Hello, world!");\n```',
|
||||
],
|
||||
"code block with 4 spaces": [
|
||||
" print('Hello, world!')",
|
||||
`<pre class="code-block"><code><span class="hljs-title function_">print</span>(<span class="hljs-string">'Hello, world!'</span>)</code>${select()}</pre>`,
|
||||
"```\nprint('Hello, world!')\n```",
|
||||
],
|
||||
"code block with 4 spaces within list item": [
|
||||
"- print('Hello, world!')",
|
||||
`<ul><li><pre class="code-block"><code><span class="hljs-title function_">print</span>(<span class="hljs-string">\'Hello, world!\'</span>)</code>${select()}</pre></li></ul>`,
|
||||
"* ```\n print('Hello, world!')\n ```",
|
||||
],
|
||||
}).forEach(([name, [markdown, html, expectedMarkdown]]) => {
|
||||
test(name, async function (assert) {
|
||||
this.siteSettings.rich_editor = true;
|
||||
await testMarkdown(assert, markdown, html, expectedMarkdown);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user