From 50dc9df8a44d408dd83ae4b17c407fa36c85cf8e Mon Sep 17 00:00:00 2001
From: Henry Jameson <me@hjkos.com>
Date: Thu, 14 Nov 2019 00:18:14 +0200
Subject: [PATCH] adds greentext, also small fixes

---
 src/components/status/status.js               | 49 +++++++++--
 .../tiny_post_html_processor.service.js       | 84 +++++++++++++++++++
 2 files changed, 124 insertions(+), 9 deletions(-)
 create mode 100644 src/services/tiny_post_html_processor/tiny_post_html_processor.service.js

diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4fbd5ac3..6dbb2199 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -13,10 +13,11 @@ import Timeago from '../timeago/timeago.vue'
 import StatusPopover from '../status_popover/status_popover.vue'
 import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
 import fileType from 'src/services/file_type/file_type.service'
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
 import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
 import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
 import { filter, unescape, uniqBy } from 'lodash'
-import { mapGetters } from 'vuex'
+import { mapGetters, mapState } from 'vuex'
 
 const Status = {
   name: 'Status',
@@ -42,8 +43,8 @@ const Status = {
       showingTall: this.inConversation && this.focused,
       showingLongSubject: false,
       error: null,
+      // Initial state
       expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
-      betterShadow: this.$store.state.interface.browserSupport.cssFilter
     }
   },
   computed: {
@@ -103,7 +104,7 @@ const Status = {
       return this.$store.state.statuses.allStatusesObject[this.status.id]
     },
     loggedIn () {
-      return !!this.$store.state.users.currentUser
+      return !!this.currentUser
     },
     muteWordHits () {
       const statusText = this.status.text.toLowerCase()
@@ -163,7 +164,7 @@ const Status = {
       if (this.inConversation || !this.isReply) {
         return false
       }
-      if (this.status.user.id === this.$store.state.users.currentUser.id) {
+      if (this.status.user.id === this.currentUser.id) {
         return false
       }
       if (this.status.type === 'retweet') {
@@ -178,7 +179,7 @@ const Status = {
         if (checkFollowing && taggedUser && taggedUser.following) {
           return false
         }
-        if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
+        if (this.status.attentions[i].id === this.currentUser.id) {
           return false
         }
       }
@@ -255,11 +256,37 @@ const Status = {
     maxThumbnails () {
       return this.mergedConfig.maxThumbnails
     },
+    postBodyHtml () {
+      const html = this.status.statusnet_html
+
+      try {
+        if (html.includes('&gt;')) {
+          // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+          return processHtml(html, (string) => {
+            if (string.includes('&gt;') &&
+                string
+                .replace(/<[^>]+?>/gi, '') // remove all tags
+                .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+                .trim()
+                .startsWith('&gt;')) {
+              return `<span class='greentext'>${string}</span>`
+            } else {
+              return string
+            }
+          })
+        } else {
+          return html
+        }
+      } catch (e) {
+        console.err('Failed to process status html', e)
+        return html
+      }
+    },
     contentHtml () {
       if (!this.status.summary_html) {
-        return this.status.statusnet_html
+        return this.postBodyHtml
       }
-      return this.status.summary_html + '<br />' + this.status.statusnet_html
+      return this.status.summary_html + '<br />' + this.postBodyHtml
     },
     combinedFavsAndRepeatsUsers () {
       // Use the status from the global status repository since favs and repeats are saved in it
@@ -270,7 +297,7 @@ const Status = {
       return uniqBy(combinedUsers, 'id')
     },
     ownStatus () {
-      return this.status.user.id === this.$store.state.users.currentUser.id
+      return this.status.user.id === this.currentUser.id
     },
     tags () {
       return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
@@ -278,7 +305,11 @@ const Status = {
     hidePostStats () {
       return this.mergedConfig.hidePostStats
     },
-    ...mapGetters(['mergedConfig'])
+    ...mapGetters(['mergedConfig']),
+    ...mapState({
+      betterShadow: state => state.interface.browserSupport.cssFilter,
+      currentUser: state => state.users.currentUser
+    })
   },
   components: {
     Attachment,
diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
new file mode 100644
index 00000000..c9ff81e1
--- /dev/null
+++ b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
@@ -0,0 +1,84 @@
+/**
+ * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
+ * allows it to be processed, useful for greentexting, mostly
+ *
+ * @param {Object} input - input data
+ * @param {(string) => string} processor - function that will be called on every line
+ * @return {string} processed html
+ */
+export const processHtml = (html, processor) => {
+  const handledTags = new Set(['p', 'br', 'div'])
+  const openCloseTags = new Set(['p', 'div'])
+  const tagRegex = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi
+
+  let buffer = '' // Current output buffer
+  const level = [] // How deep we are in tags and which tags were there
+  let textBuffer = '' // Current line content
+  let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
+
+  // Extracts tagname from tag, i.e. <span a="b"> => span
+  const getTagName = (tag) => {
+    // eslint-disable-next-line no-unused-vars
+    const result = tagRegex.exec(tag)
+    return result && (result[1] || result[2])
+  }
+
+  const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
+    buffer += processor(textBuffer)
+    textBuffer = ''
+  }
+
+  const handleBr = (tag) => { // handles single newlines/linebreaks
+    flush()
+    buffer += tag
+  }
+
+  const handleOpen = (tag) => { // handles opening tags
+    flush()
+    buffer += tag
+    level.push(tag)
+  }
+
+  const handleClose = (tag) => { // handles closing tags
+    flush()
+    buffer += tag
+    if (level[level.length - 1] === tag) {
+      level.pop()
+    }
+  }
+
+  for (let i = 0; i < html.length; i++) {
+    const char = html[i]
+    if (char === '<' && tagBuffer !== null) {
+      tagBuffer = char
+    } else if (char !== '>' && tagBuffer !== null) {
+      tagBuffer += char
+    } else if (char === '>' && tagBuffer !== null) {
+      tagBuffer += char
+      const tagName = getTagName(tagBuffer)
+      if (handledTags.has(tagName)) {
+        if (tagName === 'br') {
+          handleBr(tagBuffer)
+        }
+        if (openCloseTags.has(tagBuffer)) {
+          if (tagBuffer[1] === '/') {
+            handleClose(tagBuffer)
+          } else {
+            handleOpen(tagBuffer)
+          }
+        }
+      } else {
+        textBuffer += tagBuffer
+      }
+      tagBuffer = null
+    } else if (char === '\n') {
+      handleBr(char)
+    } else {
+      textBuffer += char
+    }
+  }
+
+  flush()
+
+  return buffer
+}