diff --git a/app/assets/javascripts/discourse/tests/helpers/rich-editor-helper.gjs b/app/assets/javascripts/discourse/tests/helpers/rich-editor-helper.gjs
index ce4b011f898..6495ee1e8dd 100644
--- a/app/assets/javascripts/discourse/tests/helpers/rich-editor-helper.gjs
+++ b/app/assets/javascripts/discourse/tests/helpers/rich-editor-helper.gjs
@@ -2,12 +2,7 @@ import { tracked } from "@glimmer/tracking";
import { click, render, settled, waitFor } from "@ember/test-helpers";
import DEditor from "discourse/components/d-editor";
-export async function testMarkdown(
- assert,
- markdown,
- expectedHtml,
- expectedMarkdown
-) {
+export async function setupRichEditor(assert, markdown, multiToggle = false) {
const self = new (class {
@tracked value = markdown;
@tracked view;
@@ -26,10 +21,14 @@ export async function testMarkdown(
);
- // ensure toggling to rich editor and back works
- await click(".composer-toggle-switch");
- await click(".composer-toggle-switch");
- await click(".composer-toggle-switch");
+ if (multiToggle) {
+ // ensure toggling to rich editor and back works
+ await click(".composer-toggle-switch");
+ await click(".composer-toggle-switch");
+ await click(".composer-toggle-switch");
+ } else {
+ await click(".composer-toggle-switch");
+ }
await waitFor(".ProseMirror");
await settled();
@@ -64,9 +63,25 @@ export async function testMarkdown(
// or a trailing-paragraph with an optional
inside
.replace(/
( This text will be hidden This text will be hidden This text will be hidden This text will be hidden
)?<\/p>$/, "");
+ return [self, html];
+}
+
+export async function testMarkdown(
+ assert,
+ markdown,
+ expectedHtml,
+ expectedMarkdown,
+ multiToggle = false
+) {
+ const [editorClass, html] = await setupRichEditor(
+ assert,
+ markdown,
+ multiToggle
+ );
+
assert.strictEqual(html, expectedHtml, `HTML should match for "${markdown}"`);
assert.strictEqual(
- self.value,
+ editorClass.value,
expectedMarkdown,
`Markdown should match for "${markdown}"`
);
diff --git a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/code-block-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/code-block-test.js
similarity index 99%
rename from app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/code-block-test.gjs
rename to app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/code-block-test.js
index 11743fab9b7..866953eac48 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/code-block-test.gjs
+++ b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/code-block-test.js
@@ -51,7 +51,7 @@ module(
],
}).forEach(([name, [markdown, html, expectedMarkdown]]) => {
test(name, async function (assert) {
- await testMarkdown(assert, markdown, html, expectedMarkdown);
+ await testMarkdown(assert, markdown, html, expectedMarkdown, true);
});
});
}
diff --git a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js
index bd7a2d505d4..fd2ef6da623 100644
--- a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js
+++ b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js
@@ -1,6 +1,7 @@
import $ from "jquery";
import { withPluginApi } from "discourse/lib/plugin-api";
import { i18n } from "discourse-i18n";
+import richEditorExtension from "../lib/rich-editor-extension";
function initializeDetails(api) {
api.decorateCooked(($elem) => $("details", $elem), {
@@ -19,6 +20,8 @@ function initializeDetails(api) {
icon: "caret-right",
label: "details.title",
});
+
+ api.registerRichEditorExtension(richEditorExtension);
}
export default {
diff --git a/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js b/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js
new file mode 100644
index 00000000000..e65688a2a08
--- /dev/null
+++ b/plugins/discourse-details/assets/javascripts/lib/rich-editor-extension.js
@@ -0,0 +1,86 @@
+/** @type {RichEditorExtension} */
+const extension = {
+ nodeSpec: {
+ details: {
+ allowGapCursor: true,
+ attrs: { open: { default: true } },
+ content: "summary block+",
+ group: "block",
+ draggable: true,
+ selectable: true,
+ defining: true,
+ isolating: true,
+ parseDOM: [{ tag: "details" }],
+ toDOM: (node) => ["details", { open: node.attrs.open || undefined }, 0],
+ },
+ summary: {
+ content: "inline*",
+ parseDOM: [{ tag: "summary" }],
+ toDOM: () => ["summary", 0],
+ },
+ },
+ parse: {
+ bbcode_open(state, token) {
+ if (token.tag === "details") {
+ state.openNode(state.schema.nodes.details, {
+ open: token.attrGet("open") !== null,
+ });
+ return true;
+ }
+
+ if (token.tag === "summary") {
+ state.openNode(state.schema.nodes.summary);
+ return true;
+ }
+ },
+ bbcode_close(state, token) {
+ if (token.tag === "details" || token.tag === "summary") {
+ state.closeNode();
+ return true;
+ }
+ },
+ },
+ serializeNode: {
+ details(state, node) {
+ state.renderContent(node);
+ state.write("[/details]\n\n");
+ },
+ summary(state, node, parent) {
+ let hasSummary = false;
+ // If the [details] tag has no summary.
+ if (node.content.childCount === 0) {
+ state.write("[details");
+ } else {
+ hasSummary = true;
+ state.write('[details="');
+ node.content.forEach(
+ (child) =>
+ child.text &&
+ state.text(child.text.replace(/"/g, "“"), state.inAutolink)
+ );
+ }
+ let finalState = `${parent.attrs.open ? " open" : ""}]\n`;
+ if (hasSummary) {
+ finalState = `"${finalState}`;
+ }
+ state.write(finalState);
+ },
+ },
+ plugins: {
+ props: {
+ handleClickOn(view, pos, node, nodePos) {
+ if (node.type.name === "summary") {
+ const details = view.state.doc.nodeAt(nodePos - 1);
+ view.dispatch(
+ view.state.tr.setNodeMarkup(nodePos - 1, null, {
+ open: !details.attrs.open,
+ })
+ );
+ return true;
+ }
+ },
+ },
+ },
+};
+
+export default extension;
diff --git a/plugins/discourse-details/assets/stylesheets/details.scss b/plugins/discourse-details/assets/stylesheets/details.scss
index a83b8bdcfed..422a9985ccb 100644
--- a/plugins/discourse-details/assets/stylesheets/details.scss
+++ b/plugins/discourse-details/assets/stylesheets/details.scss
@@ -3,7 +3,8 @@ details {
.topic-body .cooked &,
.d-editor-preview,
- &.details__boxed {
+ &.details__boxed,
+ .ProseMirror & {
background-color: var(--primary-very-low);
padding: 0.25rem 0.75rem;
margin-bottom: 0.5rem;
diff --git a/plugins/discourse-details/test/javascripts/integration/prosemirror-editor/details-test.js b/plugins/discourse-details/test/javascripts/integration/prosemirror-editor/details-test.js
new file mode 100644
index 00000000000..599f3f0fd07
--- /dev/null
+++ b/plugins/discourse-details/test/javascripts/integration/prosemirror-editor/details-test.js
@@ -0,0 +1,76 @@
+import { click } from "@ember/test-helpers";
+import { module, test } from "qunit";
+import { setupRenderingTest } from "discourse/tests/helpers/component-test";
+import {
+ setupRichEditor,
+ testMarkdown,
+} from "discourse/tests/helpers/rich-editor-helper";
+
+module(
+ "Integration | Component | prosemirror-editor - details extension",
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ const testCases = {
+ details: [
+ [
+ `[details="Summary"]This text will be hidden[/details]`,
+ `Summary
Summary