aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHenry Jameson <me@hjkos.com>2021-06-10 18:52:01 +0300
committerHenry Jameson <me@hjkos.com>2021-06-10 18:52:01 +0300
commitcc00af7a3102034b05ebcd4aa1fd01c6f467184a (patch)
treefc2d34177416a03359a85567aa6b8c29374c2f07 /src
parent0f73e96194fb13e70be0222a7ab718d7894b62c2 (diff)
Hellthread(tm) Certified
Diffstat (limited to 'src')
-rw-r--r--src/components/mention_link/mention_link.js3
-rw-r--r--src/components/mention_link/mention_link.vue1
-rw-r--r--src/components/rich_content/rich_content.jsx176
-rw-r--r--src/components/status/status.js3
-rw-r--r--src/components/status/status.vue1
-rw-r--r--src/components/status_body/status_body.js8
-rw-r--r--src/components/status_body/status_body.vue12
-rw-r--r--src/components/status_content/status_content.js1
-rw-r--r--src/components/status_content/status_content.vue3
-rw-r--r--src/services/html_converter/html_line_converter.service.js (renamed from src/services/tiny_post_html_processor/tiny_post_html_processor.service.js)30
-rw-r--r--src/services/html_converter/html_tree_converter.service.js (renamed from src/services/mini_html_converter/mini_html_converter.service.js)20
11 files changed, 196 insertions, 62 deletions
diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js
index 711c87d6..00b9e388 100644
--- a/src/components/mention_link/mention_link.js
+++ b/src/components/mention_link/mention_link.js
@@ -70,9 +70,6 @@ const MentionLink = {
highlightClass () {
if (this.highlight) return highlightClass(this.user)
},
- oldPlace () {
- return !this.mergedConfig.mentionsOwnLine
- },
oldStyle () {
return !this.mergedConfig.mentionsNewStyle
},
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 281fab25..a65dbad3 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -1,7 +1,6 @@
<template>
<span
class="MentionLink"
- :class="{ '-oldPlace': oldPlace }"
>
<!-- eslint-disable vue/no-v-html -->
<a
diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx
index db24ca0e..590fea0f 100644
--- a/src/components/rich_content/rich_content.jsx
+++ b/src/components/rich_content/rich_content.jsx
@@ -1,7 +1,7 @@
import Vue from 'vue'
import { unescape, flattenDeep } from 'lodash'
-import { convertHtml, getTagName, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { convertHtmlToTree, getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/html_tree_converter.service.js'
+import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
import StillImage from 'src/components/still-image/still-image.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
@@ -31,11 +31,24 @@ export default Vue.component('RichContent', {
required: false,
type: Boolean,
default: false
+ },
+ // Whether to hide last mentions (hellthreads)
+ hideLastMentions: {
+ required: false,
+ type: Boolean,
+ default: false
+ },
+ // Whether to hide first mentions
+ hideFirstMentions: {
+ required: false,
+ type: Boolean,
+ default: false
}
},
render (h) {
// Pre-process HTML
- const html = this.greentext ? addGreentext(this.html) : this.html
+ const html = preProcessPerLine(this.html, this.greentext, this.hideLastMentions)
+ console.log(this.hideFirstMentions, this.hideLastMentions)
const renderImage = (tag) => {
return <StillImage
@@ -45,18 +58,20 @@ export default Vue.component('RichContent', {
}
const renderMention = (attrs, children, encounteredText) => {
- return <MentionLink
- url={attrs.href}
- content={flattenDeep(children).join('')}
- firstMention={!encounteredText}
- />
+ return (this.hideFirstMentions && !encounteredText)
+ ? ''
+ : <MentionLink
+ url={attrs.href}
+ content={flattenDeep(children).join('')}
+ firstMention={!encounteredText}
+ />
}
// We stop treating mentions as "first" ones when we encounter
// non-whitespace text
let encounteredText = false
// Processor to use with mini_html_converter
- const processItem = (item) => {
+ const processItem = (item, index, array, what) => {
// Handle text nodes - just add emoji
if (typeof item === 'string') {
const emptyText = item.trim() === ''
@@ -69,7 +84,7 @@ export default Vue.component('RichContent', {
encounteredText = true
}
if (item.includes(':')) {
- return processTextForEmoji(
+ unescapedItem = processTextForEmoji(
unescapedItem,
this.emoji,
({ shortcode, url }) => {
@@ -81,9 +96,8 @@ export default Vue.component('RichContent', {
/>
}
)
- } else {
- return unescapedItem
}
+ return unescapedItem
}
// Handle tag nodes
@@ -98,6 +112,8 @@ export default Vue.component('RichContent', {
const attrs = getAttrs(opener)
if (attrs['class'] && attrs['class'].includes('mention')) {
return renderMention(attrs, children, encounteredText)
+ } else if (attrs['class'] && attrs['class'].includes('hashtag')) {
+ return item // We'll handle it later
} else {
attrs.target = '_blank'
return <a {...{ attrs }}>
@@ -116,43 +132,129 @@ export default Vue.component('RichContent', {
}
}
}
+ // Processor for back direction (for finding "last" stuff, just easier this way)
+ let encounteredTextReverse = false
+ const renderHashtag = (attrs, children, encounteredTextReverse) => {
+ attrs.target = '_blank'
+ if (!encounteredTextReverse) {
+ attrs['data-parser-last'] = true
+ }
+ return <a {...{ attrs }}>
+ { children.map(processItem) }
+ </a>
+ }
+ const processItemReverse = (item, index, array, what) => {
+ // Handle text nodes - just add emoji
+ if (typeof item === 'string') {
+ const emptyText = item.trim() === ''
+ if (emptyText) return encounteredTextReverse ? item : item.trim()
+ if (!encounteredTextReverse) encounteredTextReverse = true
+ return item
+ } else if (Array.isArray(item)) {
+ // Handle tag nodes
+ const [opener, children] = item
+ const Tag = getTagName(opener)
+ switch (Tag) {
+ case 'a': // replace mentions with MentionLink
+ if (!this.handleLinks) break
+ const attrs = getAttrs(opener)
+ // should only be this
+ if (attrs['class'] && attrs['class'].includes('hashtag')) {
+ return renderHashtag(attrs, children, encounteredTextReverse)
+ }
+ }
+ }
+ return item
+ }
return <span class="RichContent">
{ this.$slots.prefix }
- { convertHtml(html).map(processItem) }
+ { convertHtmlToTree(html).map(processItem).reverse().map(processItemReverse).reverse() }
{ this.$slots.suffix }
</span>
}
})
-export const addGreentext = (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>`
+/** Pre-processing HTML
+ *
+ * Currently this does two things:
+ * - add green/cyantexting
+ * - wrap and mark last line containing only mentions as ".lastMentionsLine" for
+ * more compact hellthreads.
+ *
+ * @param {String} html - raw HTML to process
+ * @param {Boolean} greentext - whether to enable greentexting or not
+ * @param {Boolean} removeLastMentions - whether to remove last mentions
+ */
+export const preProcessPerLine = (html, greentext, removeLastMentions) => {
+ // Only mark first (last) encounter
+ let lastMentionsMarked = false
+
+ return convertHtmlToLines(html).reverse().map((item, index, array) => {
+ if (!item.text) return item
+ const string = item.text
+
+ // Greentext stuff
+ if (greentext && (string.includes('&gt;') || string.includes('&lt;'))) {
+ const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ if (cleanedString.startsWith('&gt;')) {
+ return `<span class='greentext'>${string}</span>`
+ } else if (cleanedString.startsWith('&lt;')) {
+ return `<span class='cyantext'>${string}</span>`
+ }
+ }
+
+ const tree = convertHtmlToTree(string)
+
+ // If line has loose text, i.e. text outside a mention or a tag
+ // we won't touch mentions.
+ let hasLooseText = false
+ let hasMentions = false
+ const process = (item) => {
+ if (Array.isArray(item)) {
+ const [opener, children, closer] = item
+ const tag = getTagName(opener)
+ if (tag === 'a') {
+ const attrs = getAttrs(opener)
+ if (attrs['class'] && attrs['class'].includes('mention')) {
+ hasMentions = true
+ return [opener, children, closer]
+ } else {
+ hasLooseText = true
+ return [opener, children, closer]
+ }
+ } else if (tag === 'span' || tag === 'p') {
+ return [opener, [...children].reverse().map(process).reverse(), closer]
} else {
- return string
+ hasLooseText = true
+ return [opener, children, closer]
+ }
+ }
+
+ if (typeof item === 'string') {
+ if (item.trim() !== '') {
+ hasLooseText = true
}
- })
+ return item
+ }
+ }
+
+ const result = [...tree].reverse().map(process).reverse()
+
+ if (removeLastMentions && hasMentions && !hasLooseText && !lastMentionsMarked) {
+ lastMentionsMarked = true
+ return ''
} else {
- return html
+ return flattenDeep(result).join('')
}
- } catch (e) {
- console.error('Failed to process status html', e)
- return html
- }
+ }).reverse().join('')
}
export const getHeadTailLinks = (html) => {
// Exported object properties
const firstMentions = [] // Mentions that appear in the beginning of post body
+ const lastMentions = [] // Mentions that appear at the end of post body
const lastTags = [] // Tags that appear at the end of post body
const writtenMentions = [] // All mentions that appear in post body
const writtenTags = [] // All tags that appear in post body
@@ -170,7 +272,7 @@ export const getHeadTailLinks = (html) => {
}
}
- // Processor to use with mini_html_converter
+ // Processor to use with html_tree_converter
const processItem = (item) => {
// Handle text nodes - stop treating mentions as "first" when text encountered
if (typeof item === 'string') {
@@ -182,6 +284,7 @@ export const getHeadTailLinks = (html) => {
}
// Encountered text? That means tags we've been collectings aren't "last"!
lastTags.splice(0)
+ lastMentions.splice(0)
return
}
// Handle tag nodes
@@ -197,6 +300,7 @@ export const getHeadTailLinks = (html) => {
firstMentions.push(linkData)
}
writtenMentions.push(linkData)
+ lastMentions.push(linkData)
} else if (attrs['class'].includes('hashtag')) {
lastTags.push(linkData)
writtenTags.push(linkData)
@@ -206,6 +310,6 @@ export const getHeadTailLinks = (html) => {
children && children.forEach(processItem)
}
}
- convertHtml(html).forEach(processItem)
- return { firstMentions, writtenMentions, writtenTags, lastTags }
+ convertHtmlToTree(html).forEach(processItem)
+ return { firstMentions, writtenMentions, writtenTags, lastTags, lastMentions }
}
diff --git a/src/components/status/status.js b/src/components/status/status.js
index e9a5ec0d..bab818fc 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -196,6 +196,9 @@ const Status = {
hasMentionsLine () {
return this.mentionsLine.length > 0
},
+ hideLastMentions () {
+ return this.headTailLinks.firstMentions.length === 0
+ },
muted () {
if (this.statusoid.user.id === this.currentUser.id) return false
const { status } = this
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 7cc25be9..0190d864 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -306,6 +306,7 @@
:highlight="highlight"
:focused="isFocused"
:hide-first-mentions="mentionsOwnLine && isReply"
+ :hide-last-mentions="hideLastMentions"
:head-tail-links="headTailLinks"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index c2edb601..2fc9abbf 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -30,7 +30,8 @@ const StatusContent = {
// if this was computed at upper level it can be passed here, otherwise
// it will be in this component
'headTailLinks',
- 'hideFirstMentions'
+ 'hideFirstMentions',
+ 'hideLastMentions'
],
data () {
return {
@@ -80,9 +81,12 @@ const StatusContent = {
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
- mentions () {
+ mentionsFirst () {
return this.headTailLinksComputed.firstMentions
},
+ mentionsLast () {
+ return this.headTailLinksComputed.lastMentions
+ },
...mapGetters(['mergedConfig'])
},
components: {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index 4df29934..bd599a8c 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -49,11 +49,19 @@
:emoji="status.emojis"
:handle-links="true"
:greentext="mergedConfig.greentext"
+ :hide-first-mentions="hideFirstMentions"
+ :hide-last-mentions="hideLastMentions"
>
<template v-slot:prefix>
<MentionsLine
- v-if="!hideFirstMentions"
- :mentions="mentions"
+ v-if="!hideFirstMentions && mentionsFirst"
+ :mentions="mentionsFirst"
+ />
+ </template>
+ <template v-slot:suffix>
+ <MentionsLine
+ v-if="!hideFirstMentions && mentionsLast"
+ :mentions="mentionsLast"
/>
</template>
</RichContent>
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index 363a9cb0..64cc6d44 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -33,6 +33,7 @@ const StatusContent = {
'fullContent',
'singleLine',
'hideFirstMentions',
+ 'hideLastMentions',
'headTailLinks'
],
computed: {
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 18f6e7be..c32bbbfb 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -5,7 +5,8 @@
:status="status"
:single-line="singleLine"
:hide-first-mentions="hideFirstMentions"
- :headTailLinks="headTailLinks"
+ :hide-last-mentions="hideLastMentions"
+ :head-tail-links="headTailLinks"
>
<div v-if="status.poll && status.poll.options">
<poll :base-poll="status.poll" />
diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/html_converter/html_line_converter.service.js
index de6f20ef..80482c9a 100644
--- a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js
+++ b/src/services/html_converter/html_line_converter.service.js
@@ -1,18 +1,26 @@
/**
- * 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
+ * This is a tiny purpose-built HTML parser/processor. This basically detects
+ * any type of visual newline and converts entire HTML into a array structure.
+ *
+ * Text nodes are represented as object with single property - text - containing
+ * the visual line. Intended usage is to process the array with .map() in which
+ * map function returns a string and resulting array can be converted back to html
+ * with a .join('').
+ *
+ * Generally this isn't very useful except for when you really need to either
+ * modify visual lines (greentext i.e. simple quoting) or do something with
+ * first/last line.
*
* 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
+ * @return {(string|{ text: string })[]} processed html in form of a list.
*/
-export const processHtml = (html, processor) => {
+export const convertHtmlToLines = (html) => {
const handledTags = new Set(['p', 'br', 'div'])
const openCloseTags = new Set(['p', 'div'])
- let buffer = '' // Current output buffer
+ 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
@@ -25,27 +33,27 @@ export const processHtml = (html, processor) => {
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
if (textBuffer.trim().length > 0) {
- buffer += processor(textBuffer)
+ buffer.push({ text: textBuffer })
} else {
- buffer += textBuffer
+ buffer.push(textBuffer)
}
textBuffer = ''
}
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
flush()
- buffer += tag
+ buffer.push(tag)
}
const handleOpen = (tag) => { // handles opening tags
flush()
- buffer += tag
+ buffer.push(tag)
level.push(tag)
}
const handleClose = (tag) => { // handles closing tags
flush()
- buffer += tag
+ buffer.push(tag)
if (level[level.length - 1] === tag) {
level.pop()
}
diff --git a/src/services/mini_html_converter/mini_html_converter.service.js b/src/services/html_converter/html_tree_converter.service.js
index 900752cd..badd473a 100644
--- a/src/services/mini_html_converter/mini_html_converter.service.js
+++ b/src/services/html_converter/html_tree_converter.service.js
@@ -1,15 +1,23 @@
/**
- * This is a not-so-tiny purpose-built HTML parser/processor. It was made for use
- * with StatusBody component for purpose of replacing tags with vue components
+ * This is a not-so-tiny purpose-built HTML parser/processor. This parses html
+ * and converts it into a tree structure representing tag openers/closers and
+ * children.
*
- * known issue: doesn't handle CDATA so nested CDATA might not work well
+ * Structure follows this pattern: [opener, [...children], closer] except root
+ * node which is just [...children]. Text nodes can only be within children and
+ * are represented as strings.
+ *
+ * Intended use is to convert HTML structure and then recursively iterate over it
+ * most likely using a map. Very useful for dynamically rendering html replacing
+ * tags with JSX elements in a render function.
+ *
+ * known issue: doesn't handle CDATA so CDATA might not work well
+ * known issue: doesn't handle HTML comments
*
* @param {Object} input - input data
- * @param {(string) => string} lineProcessor - function that will be called on every line
- * @param {{ key[string]: (string) => string}} tagProcessor - map of processors for tags
* @return {string} processed html
*/
-export const convertHtml = (html) => {
+export const convertHtmlToTree = (html) => {
// Elements that are implicitly self-closing
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
const emptyElements = new Set([