mirror of
https://github.com/discourse/discourse.git
synced 2025-05-24 23:24:10 +08:00
UX: avoid leading space when serializing some nodes from rich editor (#32270)
On some cases during serialization (like with headings), the previous
node is still "open" when it's the "turn" of the node we're serializing.
In this case, checking the boundaries before writing was getting the
wrong state, because the `write` call uses `flushClose()`, which makes
sure we have the newlines from closing the previous node.
This would create scenarios like (notice the space before each node
below the heading)
```markdown
# Heading
🎉
# Heading
@mention
# Heading
#hashtag
```
This PR makes sure we call flushClose() before checking boundaries, and
adds tests for that.
This commit is contained in:
@ -170,6 +170,7 @@ const extension = {
|
|||||||
|
|
||||||
serializeNode: {
|
serializeNode: {
|
||||||
emoji(state, node) {
|
emoji(state, node) {
|
||||||
|
state.flushClose();
|
||||||
if (!isBoundary(state.out, state.out.length - 1)) {
|
if (!isBoundary(state.out, state.out.length - 1)) {
|
||||||
state.write(" ");
|
state.write(" ");
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ const extension = {
|
|||||||
|
|
||||||
serializeNode: {
|
serializeNode: {
|
||||||
hashtag(state, node, parent, index) {
|
hashtag(state, node, parent, index) {
|
||||||
|
state.flushClose();
|
||||||
if (!isBoundary(state.out, state.out.length - 1)) {
|
if (!isBoundary(state.out, state.out.length - 1)) {
|
||||||
state.write(" ");
|
state.write(" ");
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,8 @@ const extension = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
serializeNode: {
|
serializeNode: {
|
||||||
mention: (state, node, parent, index) => {
|
mention(state, node, parent, index) {
|
||||||
|
state.flushClose();
|
||||||
if (!isBoundary(state.out, state.out.length - 1)) {
|
if (!isBoundary(state.out, state.out.length - 1)) {
|
||||||
state.write(" ");
|
state.write(" ");
|
||||||
}
|
}
|
||||||
|
@ -10,57 +10,50 @@ module(
|
|||||||
|
|
||||||
const testCases = {
|
const testCases = {
|
||||||
emoji: [
|
emoji: [
|
||||||
[
|
|
||||||
"Hey :tada:!",
|
"Hey :tada:!",
|
||||||
`<p>Hey <img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true">!</p>`,
|
`<p>Hey <img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true">!</p>`,
|
||||||
"Hey :tada:!",
|
"Hey :tada:!",
|
||||||
],
|
],
|
||||||
],
|
|
||||||
"emoji in heading": [
|
"emoji in heading": [
|
||||||
[
|
|
||||||
"# Heading :information_source:",
|
"# Heading :information_source:",
|
||||||
`<h1>Heading <img class="emoji" alt=":information_source:" title=":information_source:" src="/images/emoji/twitter/information_source.png?v=${v}" contenteditable="false" draggable="true"></h1>`,
|
`<h1>Heading <img class="emoji" alt=":information_source:" title=":information_source:" src="/images/emoji/twitter/information_source.png?v=${v}" contenteditable="false" draggable="true"></h1>`,
|
||||||
"# Heading :information_source:",
|
"# Heading :information_source:",
|
||||||
],
|
],
|
||||||
|
"emoji after a heading": [
|
||||||
|
"# Heading\n\n:tada:",
|
||||||
|
`<h1>Heading</h1><p><img class="emoji only-emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
||||||
|
"# Heading\n\n:tada:",
|
||||||
],
|
],
|
||||||
"single emoji in paragraph gets only-emoji class": [
|
"single emoji in paragraph gets only-emoji class": [
|
||||||
[
|
|
||||||
":tada:",
|
":tada:",
|
||||||
`<p><img class="emoji only-emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
`<p><img class="emoji only-emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
||||||
":tada:",
|
":tada:",
|
||||||
],
|
],
|
||||||
],
|
|
||||||
"three emojis in paragraph get only-emoji class": [
|
"three emojis in paragraph get only-emoji class": [
|
||||||
[
|
|
||||||
":tada: :smile: :heart:",
|
":tada: :smile: :heart:",
|
||||||
`<p><img class="emoji only-emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji only-emoji" alt=":smile:" title=":smile:" src="/images/emoji/twitter/smile.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji only-emoji" alt=":heart:" title=":heart:" src="/images/emoji/twitter/heart.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
`<p><img class="emoji only-emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji only-emoji" alt=":smile:" title=":smile:" src="/images/emoji/twitter/smile.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji only-emoji" alt=":heart:" title=":heart:" src="/images/emoji/twitter/heart.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
||||||
":tada: :smile: :heart:",
|
":tada: :smile: :heart:",
|
||||||
],
|
],
|
||||||
],
|
|
||||||
"more than three emojis don't get only-emoji class": [
|
"more than three emojis don't get only-emoji class": [
|
||||||
[
|
|
||||||
":tada: :smile: :heart: :+1:",
|
":tada: :smile: :heart: :+1:",
|
||||||
`<p><img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji" alt=":smile:" title=":smile:" src="/images/emoji/twitter/smile.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji" alt=":heart:" title=":heart:" src="/images/emoji/twitter/heart.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji" alt=":+1:" title=":+1:" src="/images/emoji/twitter/+1.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
`<p><img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji" alt=":smile:" title=":smile:" src="/images/emoji/twitter/smile.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji" alt=":heart:" title=":heart:" src="/images/emoji/twitter/heart.png?v=${v}" contenteditable="false" draggable="true"> <img class="emoji" alt=":+1:" title=":+1:" src="/images/emoji/twitter/+1.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
||||||
":tada: :smile: :heart: :+1:",
|
":tada: :smile: :heart: :+1:",
|
||||||
],
|
],
|
||||||
],
|
|
||||||
"emoji with text doesn't get only-emoji class": [
|
"emoji with text doesn't get only-emoji class": [
|
||||||
[
|
|
||||||
"Hello :tada:",
|
"Hello :tada:",
|
||||||
`<p>Hello <img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
`<p>Hello <img class="emoji" alt=":tada:" title=":tada:" src="/images/emoji/twitter/tada.png?v=${v}" contenteditable="false" draggable="true"></p>`,
|
||||||
"Hello :tada:",
|
"Hello :tada:",
|
||||||
],
|
],
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(testCases).forEach(([name, tests]) => {
|
Object.entries(testCases).forEach(
|
||||||
tests.forEach(([markdown, expectedHtml, expectedMarkdown]) => {
|
([name, [markdown, expectedHtml, expectedMarkdown]]) => {
|
||||||
test(name, async function (assert) {
|
test(name, async function (assert) {
|
||||||
this.siteSettings.rich_editor = true;
|
this.siteSettings.rich_editor = true;
|
||||||
|
|
||||||
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -9,27 +9,30 @@ module(
|
|||||||
|
|
||||||
const testCases = {
|
const testCases = {
|
||||||
hashtag: [
|
hashtag: [
|
||||||
[
|
|
||||||
"#hello",
|
"#hello",
|
||||||
'<p><a class="hashtag-cooked" data-name="hello" contenteditable="false" draggable="true">#hello</a></p>',
|
'<p><a class="hashtag-cooked" data-name="hello" contenteditable="false" draggable="true">#hello</a></p>',
|
||||||
"#hello",
|
"#hello",
|
||||||
],
|
],
|
||||||
[
|
"text with hashtag": [
|
||||||
"Hello #category",
|
"Hello #category",
|
||||||
'<p>Hello <a class="hashtag-cooked" data-name="category" contenteditable="false" draggable="true">#category</a></p>',
|
'<p>Hello <a class="hashtag-cooked" data-name="category" contenteditable="false" draggable="true">#category</a></p>',
|
||||||
"Hello #category",
|
"Hello #category",
|
||||||
],
|
],
|
||||||
|
"hashtag after heading": [
|
||||||
|
"## Hello\n\n#category",
|
||||||
|
'<h2>Hello</h2><p><a class="hashtag-cooked" data-name="category" contenteditable="false" draggable="true">#category</a></p>',
|
||||||
|
"## Hello\n\n#category",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(testCases).forEach(([name, tests]) => {
|
Object.entries(testCases).forEach(
|
||||||
tests.forEach(([markdown, expectedHtml, expectedMarkdown]) => {
|
([name, [markdown, expectedHtml, expectedMarkdown]]) => {
|
||||||
test(name, async function (assert) {
|
test(name, async function (assert) {
|
||||||
this.siteSettings.rich_editor = true;
|
this.siteSettings.rich_editor = true;
|
||||||
|
|
||||||
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -8,28 +8,31 @@ module(
|
|||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
const testCases = {
|
const testCases = {
|
||||||
hashtag: [
|
mention: [
|
||||||
[
|
|
||||||
"@hello",
|
"@hello",
|
||||||
'<p><a class="mention" data-name="hello" contenteditable="false" draggable="true">@hello</a></p>',
|
'<p><a class="mention" data-name="hello" contenteditable="false" draggable="true">@hello</a></p>',
|
||||||
"@hello",
|
"@hello",
|
||||||
],
|
],
|
||||||
[
|
"text with mention": [
|
||||||
"Hello @dude",
|
"Hello @dude",
|
||||||
'<p>Hello <a class="mention" data-name="dude" contenteditable="false" draggable="true">@dude</a></p>',
|
'<p>Hello <a class="mention" data-name="dude" contenteditable="false" draggable="true">@dude</a></p>',
|
||||||
"Hello @dude",
|
"Hello @dude",
|
||||||
],
|
],
|
||||||
|
"mention after heading": [
|
||||||
|
"## Hello\n\n@dude",
|
||||||
|
'<h2>Hello</h2><p><a class="mention" data-name="dude" contenteditable="false" draggable="true">@dude</a></p>',
|
||||||
|
"## Hello\n\n@dude",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(testCases).forEach(([name, tests]) => {
|
Object.entries(testCases).forEach(
|
||||||
tests.forEach(([markdown, expectedHtml, expectedMarkdown]) => {
|
([name, [markdown, expectedHtml, expectedMarkdown]]) => {
|
||||||
test(name, async function (assert) {
|
test(name, async function (assert) {
|
||||||
this.siteSettings.rich_editor = true;
|
this.siteSettings.rich_editor = true;
|
||||||
|
|
||||||
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
await testMarkdown(assert, markdown, expectedHtml, expectedMarkdown);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -146,6 +146,7 @@ const extension = {
|
|||||||
serializeNode({ utils: { isBoundary } }) {
|
serializeNode({ utils: { isBoundary } }) {
|
||||||
return {
|
return {
|
||||||
local_date(state, node, parent, index) {
|
local_date(state, node, parent, index) {
|
||||||
|
state.flushClose();
|
||||||
if (!isBoundary(state.out, state.out.length - 1)) {
|
if (!isBoundary(state.out, state.out.length - 1)) {
|
||||||
state.write(" ");
|
state.write(" ");
|
||||||
}
|
}
|
||||||
@ -166,6 +167,7 @@ const extension = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
local_date_range(state, node, parent, index) {
|
local_date_range(state, node, parent, index) {
|
||||||
|
state.flushClose();
|
||||||
if (!isBoundary(state.out, state.out.length - 1)) {
|
if (!isBoundary(state.out, state.out.length - 1)) {
|
||||||
state.write(" ");
|
state.write(" ");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user