From 71303a509ff6fbb26049fcc70eec04bb67e65443 Mon Sep 17 00:00:00 2001 From: Renato Atilio Date: Thu, 6 Mar 2025 20:52:18 -0300 Subject: [PATCH] FEATURE: add html-block rich editor extension (#31181) Continues the work done on https://github.com/discourse/discourse/pull/30815. Adds an `html_block` node and its parsing/serialization logic. It's rendered as a code block with HTML syntax highlighting, and serialized as-is to the Markdown output. --- .../prosemirror/extensions/html-block.js | 35 +++++++++++++++++ .../extensions/register-default.js | 2 + .../prosemirror-editor/html-block-test.js | 38 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js create mode 100644 app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/html-block-test.js diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js new file mode 100644 index 00000000000..dcc9550b107 --- /dev/null +++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/html-block.js @@ -0,0 +1,35 @@ +/** @type {RichEditorExtension} */ +const extension = { + nodeSpec: { + html_block: { + attrs: { params: { default: "html" } }, + group: "block", + content: "text*", + code: true, + defining: true, + marks: "", + isolating: true, + selectable: true, + draggable: true, + parseDOM: [{ tag: "pre.html-block", preserveWhitespace: "full" }], + toDOM() { + return ["pre", { class: "html-block" }, ["code", 0]]; + }, + }, + }, + parse: { + html_block: (state, token) => { + state.openNode(state.schema.nodes.html_block); + state.addText(token.content.trim()); + state.closeNode(); + }, + }, + serializeNode: { + html_block: (state, node) => { + state.renderContent(node); + state.write("\n\n"); + }, + }, +}; + +export default extension; diff --git a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/register-default.js b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/register-default.js index cf66d64a13f..4e0c8590c2d 100644 --- a/app/assets/javascripts/discourse/app/static/prosemirror/extensions/register-default.js +++ b/app/assets/javascripts/discourse/app/static/prosemirror/extensions/register-default.js @@ -3,6 +3,7 @@ import codeBlock from "./code-block"; import emoji from "./emoji"; import hashtag from "./hashtag"; import heading from "./heading"; +import htmlBlock from "./html-block"; import htmlInline from "./html-inline"; import image from "./image"; import link from "./link"; @@ -31,6 +32,7 @@ const defaultExtensions = [ strikethrough, underline, htmlInline, + htmlBlock, table, markdownPaste, ]; diff --git a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/html-block-test.js b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/html-block-test.js new file mode 100644 index 00000000000..5ddc091646f --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/html-block-test.js @@ -0,0 +1,38 @@ +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 - html block extension", + function (hooks) { + setupRenderingTest(hooks); + + Object.entries({ + "unclosed html block": [ + "
Hello World\n\n", + '
<div>Hello World
', + "
Hello World\n\n", + ], + "html block with attributes": [ + 'Hey\n\n
Hello World
\nYou', + '

Hey

<div class="test">Hello World</div>\nYou
', + 'Hey\n\n
Hello World
\nYou\n\n', + ], + "html block with multiple lines": [ + "
\n

Hello

\n

World

\n
", + '
<div>\n  <p>Hello</p>\n  <p>World</p>\n</div>
', + "
\n

Hello

\n

World

\n
\n\n", + ], + "html block multiple times": [ + "
1
\n\nA\n\n
2
", + '
<div>1</div>

A

<div>2</div>
', + "
1
\n\nA\n\n
2
\n\n", + ], + }).forEach(([name, [markdown, html, expectedMarkdown]]) => { + test(name, async function (assert) { + this.siteSettings.rich_editor = true; + await testMarkdown(assert, markdown, html, expectedMarkdown); + }); + }); + } +);