Răsfoiți Sursa

Styling: Use zero-width space and maintain position of newline char

Fixes #2879
JC Brand 2 ani în urmă
părinte
comite
97be0bd8ac
4 a modificat fișierele cu 76 adăugiri și 65 ștergeri
  1. 1 0
      CHANGES.md
  2. 30 27
      src/plugins/chatview/tests/styling.js
  3. 34 33
      src/shared/rich-text.js
  4. 11 5
      src/shared/styling.js

+ 1 - 0
CHANGES.md

@@ -12,6 +12,7 @@
 - Update `nick` attribute on ChatRoom when user nickname changes
 - Update `nick` attribute on ChatRoom when user nickname changes
 - Restrict editing of MUC messages to ones with the same XEP-0421 occupant ID
 - 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.
 - #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.
 - #2925: Fix missing disco-items in browser storage.
 - #2936: Fix documentation about enable_smacks option, which is true by default.
 - #2936: Fix documentation about enable_smacks option, which is true by default.
 - #3005: Fix MUC messages with a fallback body not rendering.
 - #3005: Fix MUC messages with a fallback body not rendering.

+ 30 - 27
src/plugins/chatview/tests/styling.js

@@ -202,7 +202,7 @@ describe("An incoming chat Message", function () {
         await _converse.handleMessageStanza(msg);
         await _converse.handleMessageStanza(msg);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         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'+
             'Here\'s a code block: \n'+
             '<div class="styling-directive">```</div><code class="block">Inside the code-block, &lt;code&gt;hello&lt;/code&gt; we don\'t enable *styling hints* like ~these~\n'+
             '<div class="styling-directive">```</div><code class="block">Inside the code-block, &lt;code&gt;hello&lt;/code&gt; we don\'t enable *styling hints* like ~these~\n'+
             '</code><div class="styling-directive">```</div>'
             '</code><div class="styling-directive">```</div>'
@@ -247,67 +247,70 @@ describe("An incoming chat Message", function () {
         await _converse.handleMessageStanza(msg);
         await _converse.handleMessageStanza(msg);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1);
         await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 1);
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
-            '<blockquote><a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a> \n <a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a></blockquote>');
+        expect(msg_el.innerHTML.replace(/<!-.*?->/g, '')).toBe(
+            '<blockquote>'+
+                '<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>\n\u200B\u200B'+
+                '<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>'+
+            '</blockquote>');
 
 
         msg_text = `> This is quoted text\n>This 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)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
-            '<blockquote>This is quoted text\nThis is also quoted</blockquote>\nThis is not quoted');
+        expect(msg_el.innerHTML.replace(/<!-.*?->/g, '')).toBe(
+            '<blockquote>This is quoted text\n\u200BThis is also quoted</blockquote>\nThis is not quoted');
 
 
         msg_text = `> This is *quoted* text\n>This 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)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
-            '<blockquote>This is <span class="styling-directive">*</span><b>quoted</b><span class="styling-directive">*</span> text\n'+
+        expect(msg_el.innerHTML.replace(/<!-.*?->/g, '')).toBe(
+            '<blockquote>This is <span class="styling-directive">*</span><b>quoted</b><span class="styling-directive">*</span> text\n\u200B'+
             'This is <span class="styling-directive">`</span><code>also _quoted_</code><span class="styling-directive">`</span></blockquote>\n'+
             'This is <span class="styling-directive">`</span><code>also _quoted_</code><span class="styling-directive">`</span></blockquote>\n'+
             'This is not quoted');
             'This is not quoted');
 
 
         msg_text = `> > This is doubly quoted text`;
         msg_text = `> > This is doubly quoted text`;
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
+        expect(msg_el.innerHTML.replace(/<!-.*?->/g, '')).toBe("<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
 
 
         msg_text = `>> This is doubly quoted text`;
         msg_text = `>> This is doubly quoted text`;
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === "<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
+        expect(msg_el.innerHTML.replace(/<!-.*?->/g, '')).toBe("<blockquote><blockquote>This is doubly quoted text</blockquote></blockquote>");
 
 
         msg_text = ">```\n>ignored\n> <span></span> (println \"Hello, world!\")\n>```\n> This should show up as monospace, preformatted text ^";
         msg_text = ">```\n>ignored\n> <span></span> (println \"Hello, world!\")\n>```\n> This should show up as monospace, preformatted text ^";
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         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(
             '<blockquote>'+
             '<blockquote>'+
                 '<div class="styling-directive">```</div>'+
                 '<div class="styling-directive">```</div>'+
-                '<code class="block">ignored\n &lt;span&gt;&lt;/span&gt; (println "Hello, world!")\n'+
-                '</code><div class="styling-directive">```</div>\n'+
-                ' This should show up as monospace, preformatted text ^'+
+                '<code class="block">\u200Bignored\n\u200B\u200B&lt;span&gt;&lt;/span&gt; (println "Hello, world!")\n\u200B'+
+                '</code><div class="styling-directive">```</div>\n\u200B\u200B'+
+                'This should show up as monospace, preformatted text ^'+
             '</blockquote>');
             '</blockquote>');
 
 
         msg_text = '> ```\n> (println "Hello, world!")\n\nThe entire blockquote is a preformatted text block, but this line is plaintext!';
         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)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
-        await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
-            '<blockquote>```\n (println "Hello, world!")</blockquote>\n\n'+
+        expect(msg_el.innerHTML.replace(/<!-.*?->/g, '')).toBe(
+            '<blockquote>```\n\u200B\u200B(println "Hello, world!")</blockquote>\n\n'+
             'The entire blockquote is a preformatted text block, but this line is plaintext!');
             '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_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)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
             '<blockquote>Also, icons.js is loaded from /dist, instead of dist.</blockquote>\n'+
             '<blockquote>Also, icons.js is loaded from /dist, instead of dist.</blockquote>\n'+
@@ -316,7 +319,7 @@ describe("An incoming chat Message", function () {
         msg_text = '> Where is it located?\ngeo:37.786971,-122.399677';
         msg_text = '> Where is it located?\ngeo:37.786971,-122.399677';
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
             '<blockquote>Where is it located?</blockquote>\n'+
             '<blockquote>Where is it located?</blockquote>\n'+
@@ -326,7 +329,7 @@ describe("An incoming chat Message", function () {
         msg_text = '> What do you think of it?\n :poop:';
         msg_text = '> What do you think of it?\n :poop:';
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
             '<blockquote>What do you think of it?</blockquote>\n <span title=":poop:">💩</span>');
             '<blockquote>What do you think of it?</blockquote>\n <span title=":poop:">💩</span>');
@@ -334,7 +337,7 @@ describe("An incoming chat Message", function () {
         msg_text = '> What do you think of it?\n~hello~';
         msg_text = '> What do you think of it?\n~hello~';
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
             '<blockquote>What do you think of it?</blockquote>\n<span class="styling-directive">~</span><del>hello</del><span class="styling-directive">~</span>');
             '<blockquote>What do you think of it?</blockquote>\n<span class="styling-directive">~</span><del>hello</del><span class="styling-directive">~</span>');
@@ -342,7 +345,7 @@ describe("An incoming chat Message", function () {
         msg_text = 'hello world > this is not a quote';
         msg_text = 'hello world > this is not a quote';
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         msg = mock.createChatMessage(_converse, contact_jid, msg_text)
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === 'hello world &gt; this is not a quote');
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') === 'hello world &gt; this is not a quote');
 
 
@@ -369,7 +372,7 @@ describe("An incoming chat Message", function () {
                     }).nodeTree;
                     }).nodeTree;
         await _converse.handleMessageStanza(msg);
         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();
         msg_el = Array.from(view.querySelectorAll('converse-chat-message-body')).pop();
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
         await u.waitUntil(() => msg_el.innerHTML.replace(/<!-.*?->/g, '') ===
             `<blockquote>What do you think of it <span class="mention" data-uri="romeo@montague.lit">romeo</span>?</blockquote>\n `+
             `<blockquote>What do you think of it <span class="mention" data-uri="romeo@montague.lit">romeo</span>?</blockquote>\n `+

+ 34 - 33
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 () {
     addStyling () {
+        if (!containsDirectives(this, this.mentions)) {
+            return;
+        }
+
         const references = [];
         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++;
                 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));
         references.forEach(ref => this.addTemplateResult(ref.begin, ref.end, ref.template));
     }
     }

+ 11 - 5
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 { String } text - The text in which  the directive appears
  * @param { Number } i - The directive index
  * @param { Number } i - The directive index
  * @param { Boolean } opening - Whether we're looking for an opening or closing directive
  * @param { Boolean } opening - Whether we're looking for an opening or closing directive
  */
  */
 function getDirective (text, i, opening=true) {
 function getDirective (text, i, opening=true) {
     let d;
     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);
         d = text.slice(i, i+3);
     } else if (styling_directives.includes(text.slice(i, i+1))) {
     } else if (styling_directives.includes(text.slice(i, i+1))) {
         d = 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
  * @param { String } text -The text in which the directive appears
  */
  */
 function getDirectiveLength (d, text, i) {
 function getDirectiveLength (d, text, i) {
-    if (!d) { return 0; }
+    if (!d) return 0;
+
     const begin = i;
     const begin = i;
     i += d.length;
     i += d.length;
     if (isQuoteDirective(d)) {
     if (isQuoteDirective(d)) {
@@ -145,7 +149,9 @@ export function getDirectiveTemplate (d, text, offset, options) {
     const template = styling_templates[styling_map[d].name];
     const template = styling_templates[styling_map[d].name];
     if (isQuoteDirective(d)) {
     if (isQuoteDirective(d)) {
         const newtext = text
         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
             .replace(/\n$/, ''); // Trim line-break at the end
         return template(newtext, offset, options);
         return template(newtext, offset, options);
     } else {
     } else {