diff --git a/CHANGES.md b/CHANGES.md index 1a889aa53..70da9e232 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ - Update `nick` attribute on ChatRoom when user nickname changes - Restrict editing of MUC messages to ones with the same XEP-0421 occupant ID - #2870: Fix for multiple URLs to be linkified when sent together in chat and adds a test for this. +- #2879: Quotes, lines not aligned to the first line - #2925: Fix missing disco-items in browser storage. - #2936: Fix documentation about enable_smacks option, which is true by default. - #3005: Fix MUC messages with a fallback body not rendering. diff --git a/src/plugins/chatview/tests/styling.js b/src/plugins/chatview/tests/styling.js index a6f6ccb87..ef074efba 100644 --- a/src/plugins/chatview/tests/styling.js +++ b/src/plugins/chatview/tests/styling.js @@ -202,7 +202,7 @@ describe("An incoming chat Message", function () { await _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === + expect(msg_el.innerHTML.replace(//g, '')).toBe( 'Here\'s a code block: \n'+ '
```
Inside the code-block, <code>hello</code> we don\'t enable *styling hints* like ~these~\n'+ '
```
' @@ -247,67 +247,70 @@ describe("An incoming chat Message", function () { await _converse.handleMessageStanza(msg); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === - '
https://conversejs.org \n https://conversejs.org
'); + expect(msg_el.innerHTML.replace(//g, '')).toBe( + '
'+ + 'https://conversejs.org\n\u200B\u200B'+ + 'https://conversejs.org'+ + '
'); msg_text = `> This is quoted text\n>This is also quoted\nThis is not quoted`; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === - '
This is quoted text\nThis is also quoted
\nThis is not quoted'); + expect(msg_el.innerHTML.replace(//g, '')).toBe( + '
This is quoted text\n\u200BThis is also quoted
\nThis is not quoted'); msg_text = `> This is *quoted* text\n>This is \`also _quoted_\`\nThis is not quoted`; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === - '
This is *quoted* text\n'+ + expect(msg_el.innerHTML.replace(//g, '')).toBe( + '
This is *quoted* text\n\u200B'+ 'This is `also _quoted_`
\n'+ 'This is not quoted'); msg_text = `> > This is doubly quoted text`; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === "
This is doubly quoted text
"); + expect(msg_el.innerHTML.replace(//g, '')).toBe("
This is doubly quoted text
"); msg_text = `>> This is doubly quoted text`; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === "
This is doubly quoted text
"); + expect(msg_el.innerHTML.replace(//g, '')).toBe("
This is doubly quoted text
"); msg_text = ">```\n>ignored\n> (println \"Hello, world!\")\n>```\n> This should show up as monospace, preformatted text ^"; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 6); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === + expect(msg_el.innerHTML.replace(//g, '')).toBe( '
'+ '
```
'+ - 'ignored\n <span></span> (println "Hello, world!")\n'+ - '
```
\n'+ - ' This should show up as monospace, preformatted text ^'+ + '\u200Bignored\n\u200B\u200B<span></span> (println "Hello, world!")\n\u200B'+ + '
```
\n\u200B\u200B'+ + 'This should show up as monospace, preformatted text ^'+ '
'); msg_text = '> ```\n> (println "Hello, world!")\n\nThe entire blockquote is a preformatted text block, but this line is plaintext!'; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 6); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 7); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); - await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === - '
```\n (println "Hello, world!")
\n\n'+ + expect(msg_el.innerHTML.replace(//g, '')).toBe( + '
```\n\u200B\u200B(println "Hello, world!")
\n\n'+ 'The entire blockquote is a preformatted text block, but this line is plaintext!'); msg_text = '> Also, icons.js is loaded from /dist, instead of dist.\nhttps://conversejs.org/docs/html/configuration.html#assets-path' msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 7); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 8); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === '
Also, icons.js is loaded from /dist, instead of dist.
\n'+ @@ -316,7 +319,7 @@ describe("An incoming chat Message", function () { msg_text = '> Where is it located?\ngeo:37.786971,-122.399677'; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 8); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 9); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === '
Where is it located?
\n'+ @@ -326,7 +329,7 @@ describe("An incoming chat Message", function () { msg_text = '> What do you think of it?\n :poop:'; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 9); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 10); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === '
What do you think of it?
\n 💩'); @@ -334,7 +337,7 @@ describe("An incoming chat Message", function () { msg_text = '> What do you think of it?\n~hello~'; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 10); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 11); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === '
What do you think of it?
\n~hello~'); @@ -342,7 +345,7 @@ describe("An incoming chat Message", function () { msg_text = 'hello world > this is not a quote'; msg = mock.createChatMessage(_converse, contact_jid, msg_text) await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 11); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 12); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === 'hello world > this is not a quote'); @@ -369,7 +372,7 @@ describe("An incoming chat Message", function () { }).nodeTree; await _converse.handleMessageStanza(msg); - await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 12); + await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 13); msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop(); await u.waitUntil(() => msg_el.innerHTML.replace(//g, '') === `
What do you think of it romeo?
\n `+ diff --git a/src/shared/rich-text.js b/src/shared/rich-text.js index ad7a1a60f..c9c77a998 100644 --- a/src/shared/rich-text.js +++ b/src/shared/rich-text.js @@ -209,44 +209,45 @@ export class RichText extends String { } /** - * Look for XEP-0393 styling directives and add templates for rendering - * them. + * Look for XEP-0393 styling directives and add templates for rendering them. */ addStyling () { + if (!containsDirectives(this, this.mentions)) { + return; + } + const references = []; - if (containsDirectives(this, this.mentions)) { - const mention_ranges = this.mentions.map(m => - Array.from({ 'length': Number(m.end) }, (v, i) => Number(m.begin) + i) - ); - let i = 0; - while (i < this.length) { - if (mention_ranges.filter(r => r.includes(i)).length) { // eslint-disable-line no-loop-func - // Don't treat potential directives if they fall within a - // declared XEP-0372 reference - i++; - continue; - } - const { d, length } = getDirectiveAndLength(this, i); - if (d && length) { - const is_quote = isQuoteDirective(d); - const end = i + length; - const slice_end = is_quote ? end : end - d.length; - let slice_begin = d === '```' ? i + d.length + 1 : i + d.length; - if (is_quote && this[slice_begin] === ' ') { - // Trim leading space inside codeblock - slice_begin += 1; - } - const offset = slice_begin; - const text = this.slice(slice_begin, slice_end); - references.push({ - 'begin': i, - 'template': getDirectiveTemplate(d, text, offset, this.options), - end - }); - i = end; - } + const mention_ranges = this.mentions.map(m => + Array.from({ 'length': Number(m.end) }, (_, i) => Number(m.begin) + i) + ); + let i = 0; + while (i < this.length) { + if (mention_ranges.filter(r => r.includes(i)).length) { // eslint-disable-line no-loop-func + // Don't treat potential directives if they fall within a + // declared XEP-0372 reference i++; + continue; } + const { d, length } = getDirectiveAndLength(this, i); + if (d && length) { + const is_quote = isQuoteDirective(d); + const end = i + length; + const slice_end = is_quote ? end : end - d.length; + let slice_begin = d === '```' ? i + d.length + 1 : i + d.length; + if (is_quote && this[slice_begin] === ' ') { + // Trim leading space inside codeblock + slice_begin += 1; + } + const offset = slice_begin; + const text = this.slice(slice_begin, slice_end); + references.push({ + 'begin': i, + 'template': getDirectiveTemplate(d, text, offset, this.options), + end + }); + i = end; + } + i++; } references.forEach(ref => this.addTemplateResult(ref.begin, ref.end, ref.template)); } diff --git a/src/shared/styling.js b/src/shared/styling.js index 9c89ca9d8..a4c7a61ad 100644 --- a/src/shared/styling.js +++ b/src/shared/styling.js @@ -70,15 +70,18 @@ function isValidDirective (d, text, i, opening) { } /** - * Given a specific index "i" of "text", return the directive it matches or - * null otherwise. + * Given a specific index "i" of "text", return the directive it matches or null otherwise. * @param { String } text - The text in which the directive appears * @param { Number } i - The directive index * @param { Boolean } opening - Whether we're looking for an opening or closing directive */ function getDirective (text, i, opening=true) { let d; - if ((/(^```\s*\n|^```\s*$)/).test(text.slice(i)) && (i === 0 || text[i-1] === '\n' || text[i-1] === '>')) { + + if ( + (/(^```[\s,\u200B]*\n)|(^```[\s,\u200B]*$)/).test(text.slice(i)) && + (i === 0 || text[i-1] === '>' || (/\n\u200B{0,2}$/).test(text.slice(0, i))) + ) { d = text.slice(i, i+3); } else if (styling_directives.includes(text.slice(i, i+1))) { d = text.slice(i, i+1); @@ -98,7 +101,8 @@ function getDirective (text, i, opening=true) { * @param { String } text -The text in which the directive appears */ function getDirectiveLength (d, text, i) { - if (!d) { return 0; } + if (!d) return 0; + const begin = i; i += d.length; if (isQuoteDirective(d)) { @@ -145,7 +149,9 @@ export function getDirectiveTemplate (d, text, offset, options) { const template = styling_templates[styling_map[d].name]; if (isQuoteDirective(d)) { const newtext = text - .replace(/\n>/g, ' \n') // Don't show the directive itself + // Don't show the directive itself + .replace(/\n>\s/g, '\n\u200B\u200B') + .replace(/\n>/g, '\n\u200B') .replace(/\n$/, ''); // Trim line-break at the end return template(newtext, offset, options); } else {