commit: 0eda60eeb49f4fa460fe6f9f6196ddbb014427c7
parent: aa1b812484917c542eca416120420a77e068e260
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Tue, 19 Nov 2019 14:22:17 +0000
Merge branch 'greentext-strikes-back' into 'develop'
⑨ Added greentext support ⑨
Closes #9
See merge request pleroma/pleroma-fe!994
Diffstat:
10 files changed, 256 insertions(+), 13 deletions(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
@@ -24,7 +24,7 @@ test:
- apt install firefox-esr -y --no-install-recommends
- firefox --version
- yarn
- - npm run unit
+ - yarn unit
build:
stage: build
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
@@ -270,6 +270,17 @@
</li>
</ul>
</div>
+
+ <div class="setting-item">
+ <h2>{{ $t('settings.fun') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <Checkbox v-model="greentext">
+ {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
+ </Checkbox>
+ </li>
+ </ul>
+ </div>
</div>
<div :label="$t('settings.theme')">
diff --git 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,
- expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
- betterShadow: this.$store.state.interface.browserSupport.cssFilter
+ // not as computed because it sets the initial state which will be changed later
+ expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
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,41 @@ const Status = {
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
+ postBodyHtml () {
+ const html = this.status.statusnet_html
+
+ if (this.mergedConfig.greentext) {
+ try {
+ if (html.includes('>')) {
+ // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+ return processHtml(html, (string) => {
+ if (string.includes('>') &&
+ string
+ .replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ .startsWith('>')) {
+ return `<span class='greentext'>${string}</span>`
+ } else {
+ return string
+ }
+ })
+ } else {
+ return html
+ }
+ } catch (e) {
+ console.err('Failed to process status html', e)
+ return html
+ }
+ } else {
+ 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 +301,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 +309,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/components/status/status.vue b/src/components/status/status.vue
@@ -606,7 +606,7 @@ $status-margin: 0.75em;
height: 100%;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
- // Autoprefixed seem to ignore this one, and also syntax is different
+ /* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
@@ -752,7 +752,8 @@ $status-margin: 0.75em;
}
.greentext {
- color: green;
+ color: $fallback--cGreen;
+ color: var(--cGreen, $fallback--cGreen);
}
.status-conversation {
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -370,6 +370,8 @@
"false": "no",
"true": "yes"
},
+ "fun": "Fun",
+ "greentext": "Meme arrows",
"notifications": "Notifications",
"notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
@@ -174,6 +174,8 @@
"name_bio": "Имя и описание",
"new_email": "Новый email",
"new_password": "Новый пароль",
+ "fun": "Потешное",
+ "greentext": "Мемные стрелочки",
"notification_visibility": "Показывать уведомления",
"notification_visibility_follows": "Подписки",
"notification_visibility_likes": "Лайки",
diff --git a/src/modules/config.js b/src/modules/config.js
@@ -45,6 +45,7 @@ export const defaultState = {
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: false,
+ greentext: undefined, // instance default
hidePostStats: undefined, // instance default
hideUserStats: undefined // instance default
}
diff --git a/src/modules/instance.js b/src/modules/instance.js
@@ -32,6 +32,7 @@ const defaultState = {
noAttachmentLinks: false,
showFeaturesPanel: true,
minimalScopesMode: false,
+ greentext: false,
// Nasty stuff
pleromaBackend: true,
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
@@ -0,0 +1,94 @@
+/**
+ * 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
+ *
+ * known issue: doesn't handle CDATA so nested CDATA might not work well
+ *
+ * @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'])
+
+ 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 tag name from tag, i.e. <span a="b"> => span
+ const getTagName = (tag) => {
+ const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
+ return result && (result[1] || result[2])
+ }
+
+ const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
+ if (textBuffer.trim().length > 0) {
+ buffer += processor(textBuffer)
+ } else {
+ buffer += textBuffer
+ }
+ textBuffer = ''
+ }
+
+ const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
+ 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 tagFull = tagBuffer
+ tagBuffer = null
+ const tagName = getTagName(tagFull)
+ if (handledTags.has(tagName)) {
+ if (tagName === 'br') {
+ handleBr(tagFull)
+ } else if (openCloseTags.has(tagName)) {
+ if (tagFull[1] === '/') {
+ handleClose(tagFull)
+ } else if (tagFull[tagFull.length - 2] === '/') {
+ // self-closing
+ handleBr(tagFull)
+ } else {
+ handleOpen(tagFull)
+ }
+ }
+ } else {
+ textBuffer += tagFull
+ }
+ } else if (char === '\n') {
+ handleBr(char)
+ } else {
+ textBuffer += char
+ }
+ }
+ if (tagBuffer) {
+ textBuffer += tagBuffer
+ }
+
+ flush()
+
+ return buffer
+}
diff --git a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js
@@ -0,0 +1,96 @@
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+
+describe('TinyPostHTMLProcessor', () => {
+ describe('with processor that keeps original line should not make any changes to HTML when', () => {
+ const processorKeep = (line) => line
+ it('fed with regular HTML with newlines', () => {
+ const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with possibly broken HTML with invalid tags/composition', () => {
+ const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with very broken HTML with broken composition', () => {
+ const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with sorta valid HTML but tags aren\'t closed', () => {
+ const inputOutput = 'just leaving a <div> hanging'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with not really HTML at this point... tags that aren\'t finished', () => {
+ const inputOutput = 'do you expect me to finish this <div class='
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
+ const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with maybe valid HTML? self-closing divs and ps', () => {
+ const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+
+ it('fed with valid XHTML containing a CDATA', () => {
+ const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
+ expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
+ })
+ })
+ describe('with processor that replaces lines with word "_" should match expected line when', () => {
+ const processorReplace = (line) => '_'
+ it('fed with regular HTML with newlines', () => {
+ const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
+ const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with possibly broken HTML with invalid tags/composition', () => {
+ const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
+ const output = '_'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with very broken HTML with broken composition', () => {
+ const input = '</p> lmao what </div> whats going on <div> wha <p>'
+ const output = '</p>_</div>_<div>_<p>'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with sorta valid HTML but tags aren\'t closed', () => {
+ const input = 'just leaving a <div> hanging'
+ const output = '_<div>_'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with not really HTML at this point... tags that aren\'t finished', () => {
+ const input = 'do you expect me to finish this <div class='
+ const output = '_'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
+ const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
+ const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with maybe valid HTML? self-closing divs and ps', () => {
+ const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
+ const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+
+ it('fed with valid XHTML containing a CDATA', () => {
+ const input = 'Yes, it is me, <![CDATA[DIO]]>'
+ const output = '_'
+ expect(processHtml(input, processorReplace)).to.eql(output)
+ })
+ })
+})