diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js index 995d96cef96..5d8bd8ff49b 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js @@ -664,13 +664,13 @@ eviltrout

assert.cooked( "># #category-hashtag\n", - '
\n

#category-hashtag

\n
', + '
\n

#category-hashtag

\n
', "it handles category hashtags in simple quotes" ); assert.cooked( "# #category-hashtag", - '

#category-hashtag

', + '

#category-hashtag

', "it works within ATX-style headers" ); @@ -696,7 +696,7 @@ eviltrout

test("Heading", function (assert) { assert.cooked( "**Bold**\n----------", - "

Bold

", + '

Bold

', "It will bold the heading" ); }); @@ -939,7 +939,7 @@ eviltrout

assert.cooked( "## a\nb\n```\nc\n```", - '

a

\n

b

\n
c\n
', + '

a

\n

b

\n
c\n
', "it handles headings with code blocks after them." ); }); diff --git a/app/assets/javascripts/pretty-text/addon/allow-lister.js b/app/assets/javascripts/pretty-text/addon/allow-lister.js index ab4fee7e798..698f64c5d73 100644 --- a/app/assets/javascripts/pretty-text/addon/allow-lister.js +++ b/app/assets/javascripts/pretty-text/addon/allow-lister.js @@ -130,6 +130,7 @@ export default class AllowLister { // Only add to `default` when you always want your allowlist to occur. In other words, // don't change this for a plugin or a feature that can be disabled export const DEFAULT_LIST = [ + "a.anchor", "a.attachment", "a.hashtag", "a.mention", diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js b/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js new file mode 100644 index 00000000000..37924d263f8 --- /dev/null +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/anchor.js @@ -0,0 +1,29 @@ +export function setup(helper) { + helper.registerPlugin((md) => { + md.core.ruler.push("anchor", (state) => { + for (let idx = 0; idx < state.tokens.length; idx++) { + if (state.tokens[idx].type !== "heading_open") { + continue; + } + + const linkOpen = new state.Token("link_open", "a", 1); + const linkClose = new state.Token("link_close", "a", -1); + + const slug = state.tokens[idx + 1].content + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w\-]+/g, "") + .replace(/\-\-+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, ""); + + linkOpen.attrSet("name", slug); + linkOpen.attrSet("class", "anchor"); + linkOpen.attrSet("href", "#" + slug); + + state.tokens[idx + 1].children.unshift(linkClose); + state.tokens[idx + 1].children.unshift(linkOpen); + } + }); + }); +} diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index cbf07b9ee3d..0bd40e0f582 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -94,6 +94,20 @@ $quote-share-maxwidth: 150px; h6 { margin: 30px 0 10px; line-height: $line-height-medium; + + &:hover { + a.anchor { + &:before { + content: svg-uri( + '' + ); + float: left; + margin-left: -20px; + padding-right: 4px; + position: absolute; + } + } + } } h1 { diff --git a/plugins/poll/spec/lib/pretty_text_spec.rb b/plugins/poll/spec/lib/pretty_text_spec.rb index d887b10e753..c5b25e09a93 100644 --- a/plugins/poll/spec/lib/pretty_text_spec.rb +++ b/plugins/poll/spec/lib/pretty_text_spec.rb @@ -189,8 +189,8 @@ describe PrettyText do HTML - expect(cooked).to include("

Pre-heading

") - expect(cooked).to include("

Post-heading

") + expect(cooked).to include("

\nPre-heading

") + expect(cooked).to include("

\nPost-heading

") end it "does not break when there are headings before/after a poll without a title" do @@ -211,7 +211,7 @@ describe PrettyText do
HTML - expect(cooked).to include("

Pre-heading

") - expect(cooked).to include("

Post-heading

") + expect(cooked).to include("

\nPre-heading

") + expect(cooked).to include("

\nPost-heading

") end end diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index e154b54bb9f..b241870c2cc 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -1903,4 +1903,17 @@ HTML expect(cooked).to eq(html.strip) end end + + it "adds anchor links to headings" do + cooked = PrettyText.cook('# Hello world') + + html = <<~HTML +

+ + Hello world +

+ HTML + + expect(cooked).to match_html(html) + end end diff --git a/spec/integrity/common_mark_spec.rb b/spec/integrity/common_mark_spec.rb index 4b69d225160..852e3ea0505 100644 --- a/spec/integrity/common_mark_spec.rb +++ b/spec/integrity/common_mark_spec.rb @@ -33,6 +33,7 @@ describe "CommonMark" do cooked.strip! cooked.gsub!(" class=\"lang-auto\"", '') cooked.gsub!(/(.*)<\/span>/, "\\1") + cooked.gsub!(/<\/a>/, "") # we don't care about this cooked.gsub!("
\n
", "
") html.gsub!("
\n
", "
")