From 50dc9df8a44d408dd83ae4b17c407fa36c85cf8e Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 00:18:14 +0200 Subject: [PATCH 1/7] 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 4fbd5ac3e0..6dbb2199c8 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('>')) { + // 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 `${string}` + } 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 + '
' + this.status.statusnet_html + return this.status.summary_html + '
' + 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 0000000000..c9ff81e1af --- /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 + 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 +} From 692ee0e95a852b1f803b7ae92d65cbf4f3ce3445 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 00:41:14 +0200 Subject: [PATCH 2/7] Fix regex, tag detector condition --- src/components/status/status.js | 2 +- .../tiny_post_html_processor.service.js | 25 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 6dbb2199c8..416aa36ada 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -43,7 +43,7 @@ const Status = { showingTall: this.inConversation && this.focused, showingLongSubject: false, error: null, - // Initial state + // not as computed because it sets the initial state which will be changed later expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, } }, 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 index c9ff81e1af..b96c1ccfff 100644 --- 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 @@ -9,17 +9,15 @@ 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 + // Extracts tag name from tag, i.e. => span const getTagName = (tag) => { - // eslint-disable-next-line no-unused-vars - const result = tagRegex.exec(tag) + const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag) return result && (result[1] || result[2]) } @@ -49,28 +47,29 @@ export const processHtml = (html, processor) => { for (let i = 0; i < html.length; i++) { const char = html[i] - if (char === '<' && tagBuffer !== null) { + if (char === '<' && tagBuffer === null) { tagBuffer = char } else if (char !== '>' && tagBuffer !== null) { tagBuffer += char } else if (char === '>' && tagBuffer !== null) { tagBuffer += char - const tagName = getTagName(tagBuffer) + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) if (handledTags.has(tagName)) { if (tagName === 'br') { - handleBr(tagBuffer) + handleBr(tagFull) } - if (openCloseTags.has(tagBuffer)) { - if (tagBuffer[1] === '/') { - handleClose(tagBuffer) + if (openCloseTags.has(tagFull)) { + if (tagFull[1] === '/') { + handleClose(tagFull) } else { - handleOpen(tagBuffer) + handleOpen(tagFull) } } } else { - textBuffer += tagBuffer + textBuffer += tagFull } - tagBuffer = null } else if (char === '\n') { handleBr(char) } else { From 897131572f62c36277e06e464f328f37cf6acad2 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 00:47:20 +0200 Subject: [PATCH 3/7] Made it optional --- src/components/settings/settings.vue | 11 ++++++++ src/components/status/status.js | 40 +++++++++++++++------------- src/components/status/status.vue | 5 ++-- src/i18n/en.json | 2 ++ src/modules/config.js | 1 + src/modules/instance.js | 1 + 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index a83489d2cc..c4021137e7 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -270,6 +270,17 @@ + +
+

{{ $t('settings.fun') }}

+
    +
  • + + {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} + +
  • +
+
diff --git a/src/components/status/status.js b/src/components/status/status.js index 416aa36ada..dd0ce3abd2 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -259,26 +259,30 @@ const Status = { postBodyHtml () { const html = this.status.statusnet_html - 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 `${string}` - } else { - return string - } - }) - } else { + 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 `${string}` + } else { + return string + } + }) + } else { + return html + } + } catch (e) { + console.err('Failed to process status html', e) return html } - } catch (e) { - console.err('Failed to process status html', e) + } else { return html } }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 65778b2e5d..d291e762b2 100644 --- 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 index ead333c136..7b0b3b947b 100644 --- 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/modules/config.js b/src/modules/config.js index d4819ee8ba..329b4091cb 100644 --- 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 index 7b0e0da4da..96f14ed5da 100644 --- 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, From bce750c5717419b8f2f2de6d8a4380a4e664f986 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 00:49:31 +0200 Subject: [PATCH 4/7] making dtluna proud or disgusted --- src/i18n/ru.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/ru.json b/src/i18n/ru.json index f8bcd9969f..19e10f1e96 100644 --- 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": "Лайки", From 51ea295704c52b1f9a922868aedf264e53a5ec92 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 00:52:38 +0200 Subject: [PATCH 5/7] eslint --- src/components/status/status.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index dd0ce3abd2..714ea6d222 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -44,7 +44,7 @@ const Status = { showingLongSubject: false, error: null, // not as computed because it sets the initial state which will be changed later - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, + expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject } }, computed: { @@ -266,10 +266,10 @@ const Status = { return processHtml(html, (string) => { if (string.includes('>') && string - .replace(/<[^>]+?>/gi, '') // remove all tags - .replace(/@\w+/gi, '') // remove mentions (even failed ones) - .trim() - .startsWith('>')) { + .replace(/<[^>]+?>/gi, '') // remove all tags + .replace(/@\w+/gi, '') // remove mentions (even failed ones) + .trim() + .startsWith('>')) { return `${string}` } else { return string From bd2a682b83743311645241fe644e853e1a359b67 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 22:40:20 +0200 Subject: [PATCH 6/7] tests + updates --- .../tiny_post_html_processor.service.js | 19 +++- .../tiny_post_html_processor.spec.js | 96 +++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js 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 index b96c1ccfff..de6f20ef0b 100644 --- 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 @@ -2,6 +2,8 @@ * 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 @@ -22,11 +24,15 @@ export const processHtml = (html, processor) => { } const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer - buffer += processor(textBuffer) + if (textBuffer.trim().length > 0) { + buffer += processor(textBuffer) + } else { + buffer += textBuffer + } textBuffer = '' } - const handleBr = (tag) => { // handles single newlines/linebreaks + const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing flush() buffer += tag } @@ -59,10 +65,12 @@ export const processHtml = (html, processor) => { if (handledTags.has(tagName)) { if (tagName === 'br') { handleBr(tagFull) - } - if (openCloseTags.has(tagFull)) { + } else if (openCloseTags.has(tagName)) { if (tagFull[1] === '/') { handleClose(tagFull) + } else if (tagFull[tagFull.length - 2] === '/') { + // self-closing + handleBr(tagFull) } else { handleOpen(tagFull) } @@ -76,6 +84,9 @@ export const processHtml = (html, processor) => { textBuffer += char } } + if (tagBuffer) { + textBuffer += tagBuffer + } flush() 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 new file mode 100644 index 0000000000..f301429d06 --- /dev/null +++ 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
2

3 4

5 \n 6

7
8


\n
' + expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) + }) + + it('fed with possibly broken HTML with invalid tags/composition', () => { + const inputOutput = ' ayylmao ' + expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) + }) + + it('fed with very broken HTML with broken composition', () => { + const inputOutput = '

lmao what
whats going on
wha

' + expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) + }) + + it('fed with sorta valid HTML but tags aren\'t closed', () => { + const inputOutput = 'just leaving a

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
{ + const inputOutput = 'look ma

p \nwithin

p!

and a
div!

' + expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) + }) + + it('fed with maybe valid HTML? self-closing divs and ps', () => { + const inputOutput = 'a
what now

?' + expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) + }) + + it('fed with valid XHTML containing a CDATA', () => { + const inputOutput = 'Yes, it is me, ' + 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
2

3 4

5 \n 6

7
8


\n
' + const output = '_
_

_

_\n_

_
_


\n
' + expect(processHtml(input, processorReplace)).to.eql(output) + }) + + it('fed with possibly broken HTML with invalid tags/composition', () => { + const input = ' ayylmao ' + const output = '_' + expect(processHtml(input, processorReplace)).to.eql(output) + }) + + it('fed with very broken HTML with broken composition', () => { + const input = '

lmao what
whats going on
wha

' + const output = '

_
_
_

' + expect(processHtml(input, processorReplace)).to.eql(output) + }) + + it('fed with sorta valid HTML but tags aren\'t closed', () => { + const input = 'just leaving a

hanging' + const output = '_
_' + 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
{ + const input = 'look ma

p \nwithin

p!

and a
div!

' + const output = '_

_\n_

_

_
_

' + expect(processHtml(input, processorReplace)).to.eql(output) + }) + + it('fed with maybe valid HTML? self-closing divs and ps', () => { + const input = 'a
what now

?' + const output = '_

_

_' + expect(processHtml(input, processorReplace)).to.eql(output) + }) + + it('fed with valid XHTML containing a CDATA', () => { + const input = 'Yes, it is me, ' + const output = '_' + expect(processHtml(input, processorReplace)).to.eql(output) + }) + }) +}) From 28efd7d862d3fb890969448479d3034af7201170 Mon Sep 17 00:00:00 2001 From: Henry Jameson Date: Thu, 14 Nov 2019 23:46:19 +0200 Subject: [PATCH 7/7] use yarn, try to restart pipeline --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9d42288e13..7f6d3c924b 100644 --- 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