From a5cacde681c5df374eead298c74378250f0300d4 Mon Sep 17 00:00:00 2001 From: Renato Atilio Date: Thu, 6 Mar 2025 20:43:33 -0300 Subject: [PATCH] FEATURE: add code-block rich editor extension (#31179) Continues the work done on https://github.com/discourse/discourse/pull/30815. Extends the ProseMirror-markdown `code-block` node by integrating our existing HighlightJS pipeline for code highlighting and adding a node view with a ``; + + Object.entries({ + "basic code block": [ + "```plaintext\nconsole.log('Hello, world!');\n```", + `
console.log('Hello, world!');${select(
+          "plaintext"
+        )}
`, + "```plaintext\nconsole.log('Hello, world!');\n```", + ], + "basic code block without a lanuage": [ + "```\nconsole.log('Hello, world!');\n```", + `
console.log('Hello, world!');${select()}
`, + "```\nconsole.log('Hello, world!');\n```", + ], + "code block within list item": [ + "- ```plaintext\n console.log('Hello, world!');\n ```", + ``, + "* ```plaintext\n console.log('Hello, world!');\n ```", + ], + "code block with language": [ + '```javascript\nconsole.log("Hello, world!");\n```', + `
console.log("Hello, world!");${select()}
`, + '```javascript\nconsole.log("Hello, world!");\n```', + ], + "code block with 4 spaces": [ + " print('Hello, world!')", + `
print('Hello, world!')${select()}
`, + "```\nprint('Hello, world!')\n```", + ], + "code block with 4 spaces within list item": [ + "- print('Hello, world!')", + ``, + "* ```\n print('Hello, world!')\n ```", + ], + }).forEach(([name, [markdown, html, expectedMarkdown]]) => { + test(name, async function (assert) { + await testMarkdown(assert, markdown, html, expectedMarkdown); + }); + }); + } +); diff --git a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-editor-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-editor-test.gjs index 10f32e83f44..131aa850447 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-editor-test.gjs +++ b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-editor-test.gjs @@ -12,13 +12,8 @@ import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper"; module("Integration | Component | prosemirror-editor", function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - clearRichEditorExtensions(); - }); - - hooks.afterEach(function () { - resetRichEditorExtensions(); - }); + hooks.beforeEach(() => clearRichEditorExtensions()); + hooks.afterEach(() => resetRichEditorExtensions()); test("renders the editor", async function (assert) { await render(); diff --git a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-markdown-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-markdown-test.gjs index eeb54849e21..d828493e176 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-markdown-test.gjs +++ b/app/assets/javascripts/discourse/tests/integration/components/prosemirror-editor/prosemirror-markdown-test.gjs @@ -1,4 +1,8 @@ import { module, test } from "qunit"; +import { + clearRichEditorExtensions, + resetRichEditorExtensions, +} from "discourse/lib/composer/rich-editor-extensions"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper"; @@ -7,6 +11,9 @@ module( function (hooks) { setupRenderingTest(hooks); + hooks.beforeEach(() => clearRichEditorExtensions()); + hooks.afterEach(() => resetRichEditorExtensions()); + const testCases = { "paragraphs and hard breaks": [ ["Hello", "

Hello

", "Hello"], diff --git a/app/assets/stylesheets/common/base/code_highlighting.scss b/app/assets/stylesheets/common/base/code_highlighting.scss index 0b8e75f8ab5..bf1a6cbe8fe 100644 --- a/app/assets/stylesheets/common/base/code_highlighting.scss +++ b/app/assets/stylesheets/common/base/code_highlighting.scss @@ -61,6 +61,12 @@ h6 code { color: var(--hljs-number); } +.hljs-tag, +.hljs-tag .hljs-title { + color: var(--hljs-tag); + font-weight: normal; +} + .hljs-string, .hljs-tag .hljs-string, .hljs-template-tag, @@ -73,10 +79,6 @@ h6 code { color: var(--hljs-title); } -.hljs-name { - color: var(--hljs-name); -} - .hljs-quote, .hljs-operator, .hljs-selector-pseudo, @@ -94,10 +96,8 @@ h6 code { color: var(--hljs-title); } -.hljs-tag, -.hljs-tag .hljs-title { - color: var(--hljs-tag); - font-weight: normal; +.hljs-name { + color: var(--hljs-name); } .hljs-punctuation { diff --git a/app/assets/stylesheets/common/rich-editor/rich-editor.scss b/app/assets/stylesheets/common/rich-editor/rich-editor.scss index 37caeae953a..06d18aa9257 100644 --- a/app/assets/stylesheets/common/rich-editor/rich-editor.scss +++ b/app/assets/stylesheets/common/rich-editor/rich-editor.scss @@ -134,10 +134,6 @@ white-space: normal; } - .code-block { - position: relative; - } - .code-language-select { position: absolute; right: 0.25rem; @@ -149,8 +145,11 @@ font-size: var(--font-down-1-rem); } - .html-block { + pre { position: relative; + } + + .html-block { border: 1px dashed var(--primary-low-mid); &::after { @@ -169,13 +168,14 @@ Section below from prosemirror-view/style/prosemirror.css ********************************************************/ -/* stylelint-disable-next-line no-duplicate-selectors */ +// stylelint-disable-next-line no-duplicate-selectors .ProseMirror { position: relative; word-wrap: break-word; white-space: break-spaces; } +// stylelint-disable-next-line no-duplicate-selectors .ProseMirror pre { white-space: pre-wrap; } diff --git a/spec/system/composer/prosemirror_editor_spec.rb b/spec/system/composer/prosemirror_editor_spec.rb index 64012db13c3..10af16c91bd 100644 --- a/spec/system/composer/prosemirror_editor_spec.rb +++ b/spec/system/composer/prosemirror_editor_spec.rb @@ -3,6 +3,7 @@ describe "Composer - ProseMirror editor", type: :system do fab!(:user) { Fabricate(:user, refresh_auto_groups: true) } fab!(:tag) + let(:cdp) { PageObjects::CDP.new } let(:composer) { PageObjects::Components::Composer.new } let(:rich) { composer.rich_editor } @@ -11,6 +12,12 @@ describe "Composer - ProseMirror editor", type: :system do SiteSetting.rich_editor = true end + def open_composer_and_toggle_rich_editor + page.visit "/new-topic" + expect(composer).to be_opened + composer.toggle_rich_editor + end + it "hides the Composer container's preview button" do page.visit "/new-topic" @@ -24,34 +31,21 @@ describe "Composer - ProseMirror editor", type: :system do context "with autocomplete" do it "triggers an autocomplete on mention" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor - + open_composer_and_toggle_rich_editor composer.type_content("@#{user.username}") expect(composer).to have_mention_autocomplete end it "triggers an autocomplete on hashtag" do - page.visit "/new-topic" - - expect(composer).to be_opened - - find(".composer-toggle-switch").click + open_composer_and_toggle_rich_editor composer.type_content("##{tag.name}") expect(composer).to have_hashtag_autocomplete end it "triggers an autocomplete on emoji" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content(":smile") expect(composer).to have_emoji_autocomplete @@ -60,22 +54,14 @@ describe "Composer - ProseMirror editor", type: :system do context "with inputRules" do it "supports > to create a blockquote" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("> This is a blockquote") expect(rich).to have_css("blockquote", text: "This is a blockquote") end it "supports n. to create an ordered list" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("1. Item 1\n5. Item 2") expect(rich).to have_css("ol li", text: "Item 1") @@ -83,11 +69,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports *, - or + to create an unordered list" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("* Item 1\n") composer.type_content("- Item 2\n") composer.type_content("+ Item 3") @@ -96,11 +78,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports ``` or 4 spaces to create a code block" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("```\nThis is a code block") composer.send_keys(%i[shift enter]) composer.type_content(" This is a code block") @@ -109,11 +87,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports 1-6 #s to create a heading" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("# Heading 1\n") composer.type_content("## Heading 2\n") composer.type_content("### Heading 3\n") @@ -130,11 +104,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports _ or * to create an italic text" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("_This is italic_\n") composer.type_content("*This is italic*") @@ -142,11 +112,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports __ or ** to create a bold text" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("__This is bold__\n") composer.type_content("**This is bold**") @@ -154,11 +120,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports ` to create a code text" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("`This is code`") expect(rich).to have_css("code", text: "This is code") @@ -168,11 +130,7 @@ describe "Composer - ProseMirror editor", type: :system do context "with keymap" do PLATFORM_KEY_MODIFIER = SystemHelpers::PLATFORM_KEY_MODIFIER it "supports Ctrl + B to create a bold text" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content([PLATFORM_KEY_MODIFIER, "b"]) composer.type_content("This is bold") @@ -180,11 +138,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + I to create an italic text" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content([PLATFORM_KEY_MODIFIER, "i"]) composer.type_content("This is italic") @@ -192,12 +146,7 @@ describe "Composer - ProseMirror editor", type: :system do end xit "supports Ctrl + K to create a link" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor - page.send_keys([PLATFORM_KEY_MODIFIER, "k"]) + open_composer_and_toggle_rich_editor page.send_keys([PLATFORM_KEY_MODIFIER, "k"]) page.send_keys("https://www.example.com\t") page.send_keys("This is a link") page.send_keys(:enter) @@ -206,11 +155,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + Shift + 7 to create an ordered list" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("Item 1") composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "7"]) @@ -218,11 +163,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + Shift + 8 to create a bullet list" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("Item 1") composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "8"]) @@ -230,11 +171,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + Shift + 9 to create a blockquote" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("This is a blockquote") composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "9"]) @@ -242,12 +179,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + Shift + 1-6 for headings, 0 for reset" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor - + open_composer_and_toggle_rich_editor (1..6).each do |i| composer.type_content("\nHeading #{i}") composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, i.to_s]) @@ -260,11 +192,7 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + Z and Ctrl + Shift + Z to undo and redo" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("This is a test") composer.send_keys([PLATFORM_KEY_MODIFIER, "z"]) @@ -276,15 +204,32 @@ describe "Composer - ProseMirror editor", type: :system do end it "supports Ctrl + Shift + _ to create a horizontal rule" do - page.visit "/new-topic" - - expect(composer).to be_opened - - composer.toggle_rich_editor + open_composer_and_toggle_rich_editor composer.type_content("This is a test") composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "_"]) expect(rich).to have_css("hr") end end + + describe "pasting content" do + it "does not freeze the editor when pasting markdown code blocks without a language" do + cdp.allow_clipboard + open_composer_and_toggle_rich_editor + + # The example is a bit convoluted, but it's the simplest way to reproduce the issue. + cdp.write_clipboard <<~MARKDOWN + ``` + puts SiteSetting.all_settings(filter_categories: ["uncategorized"]).map { |setting| setting[:setting] }.join("\n") + ``` + MARKDOWN + composer.type_content("This is a test\n\n") + page.send_keys([PLATFORM_KEY_MODIFIER, "v"]) + expect(page.driver.browser.logs.get(:browser)).not_to include( + "Maximum call stack size exceeded", + ) + expect(rich).to have_css("pre code", wait: 1) + expect(rich).to have_css("select.code-language-select", wait: 1) + end + end end diff --git a/spec/system/page_objects/cdp.rb b/spec/system/page_objects/cdp.rb index a21af946073..512817369d4 100644 --- a/spec/system/page_objects/cdp.rb +++ b/spec/system/page_objects/cdp.rb @@ -30,6 +30,13 @@ module PageObjects page.evaluate_async_script("navigator.clipboard.readText().then(arguments[0])") end + def write_clipboard(text) + page.evaluate_async_script( + "navigator.clipboard.writeText(arguments[0]).then(arguments[1])", + text, + ) + end + def clipboard_has_text?(text, chomp: false, strict: true) try_until_success do clipboard_text = chomp ? read_clipboard.chomp : read_clipboard