Merge branch 'better-still-emoji' into shigusegubu
* better-still-emoji: lint & cleanup fix #935 fixed console errors, improved user-selecting, added cyantexting fix infinite loop better handling of hellthreads with mentions at bottom stylistic changes Hellthread(tm) Certified don't hide mentions for OPs mentions on same line as replies use icon instead of symbol for @ in mentions links stylistic improvements for single-line mentions proper cachin of headTailLinks, show mentions in notificaitons always moved mentions onto reply line, replies moved below post body Moved greentext to RichContent, improved how first mentions are restored, now shows mentions not uh, mention in post body
This commit is contained in:
commit
3fbf6fc5ac
21 changed files with 618 additions and 330 deletions
|
@ -1,11 +1,20 @@
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAt
|
||||
)
|
||||
|
||||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
|
@ -19,11 +28,6 @@ const MentionLink = {
|
|||
userScreenName: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
firstMention: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -61,9 +65,6 @@ const MentionLink = {
|
|||
highlightClass () {
|
||||
if (this.highlight) return highlightClass(this.user)
|
||||
},
|
||||
oldPlace () {
|
||||
return !this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
oldStyle () {
|
||||
return !this.mergedConfig.mentionsNewStyle
|
||||
},
|
||||
|
@ -83,7 +84,6 @@ const MentionLink = {
|
|||
{
|
||||
'-you': this.isYou,
|
||||
'-highlighted': this.highlight,
|
||||
'-firstMention': this.firstMention,
|
||||
'-oldStyle': this.oldStyle
|
||||
},
|
||||
this.highlightType
|
||||
|
|
|
@ -28,22 +28,21 @@
|
|||
z-index: 1;
|
||||
margin-top: 0.25em;
|
||||
padding: 0.5em;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.short {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& .short,
|
||||
& .full {
|
||||
&::before {
|
||||
content: '@';
|
||||
}
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.new {
|
||||
margin-right: 0.25em;
|
||||
|
||||
&.-firstMention {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.-you {
|
||||
& .shortName,
|
||||
& .full {
|
||||
|
@ -51,17 +50,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
.at {
|
||||
color: var(--link);
|
||||
opacity: 0.8;
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
line-height: 1;
|
||||
padding: 0 0.1em;
|
||||
vertical-align: -25%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:not(.-oldStyle) {
|
||||
.short {
|
||||
padding-left: 0.25em;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
line-height: 1.5;
|
||||
font-size: inherit;
|
||||
|
||||
&::before {
|
||||
.at {
|
||||
color: var(--faint);
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
line-height: 1;
|
||||
vertical-align: 6%;
|
||||
opacity: 0.8;
|
||||
padding-right: 0.25em;
|
||||
vertical-align: -20%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,18 +82,11 @@
|
|||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.short {
|
||||
padding-left: 0.25em;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.userName {
|
||||
display: inline-block;
|
||||
color: var(--link);
|
||||
line-height: inherit;
|
||||
margin-left: 0.125em;
|
||||
margin-left: 0;
|
||||
padding-left: 0.125em;
|
||||
padding-right: 0.25em;
|
||||
padding-top: 0;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<span
|
||||
class="MentionLink"
|
||||
:class="{ '-oldPlace': oldPlace }"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<a
|
||||
|
@ -23,7 +22,11 @@
|
|||
@click.prevent="onClick"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span class="shortName"><span
|
||||
<FAIcon
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/><span class="shortName"><span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/></span>
|
||||
|
@ -41,7 +44,7 @@
|
|||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="userNameFull"
|
||||
v-html="userNameFull"
|
||||
v-text="'@' + userNameFull"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { mapGetters } from 'vuex'
|
|||
const MentionsLine = {
|
||||
name: 'MentionsLine',
|
||||
props: {
|
||||
attentions: {
|
||||
mentions: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
|
@ -20,11 +20,11 @@ const MentionsLine = {
|
|||
limit () {
|
||||
return 6
|
||||
},
|
||||
mentions () {
|
||||
return this.attentions.slice(0, this.limit)
|
||||
mentionsComputed () {
|
||||
return this.mentions.slice(0, this.limit)
|
||||
},
|
||||
extraMentions () {
|
||||
return this.attentions.slice(this.limit)
|
||||
return this.mentions.slice(this.limit)
|
||||
},
|
||||
manyMentions () {
|
||||
return this.extraMentions.length > 0
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.MentionsLine {
|
||||
.showMoreLess {
|
||||
white-space: normal;
|
||||
&.-newStyle {
|
||||
line-height: 1.5;
|
||||
font-size: inherit;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<span class="MentionsLine">
|
||||
<MentionLink
|
||||
v-for="mention in mentions"
|
||||
:key="mention.statusnet_profile_url"
|
||||
v-for="mention in mentionsComputed"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.statusnet_profile_url"
|
||||
:url="mention.statusnet_profile_url"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
:first-mention="false"
|
||||
/><span
|
||||
v-if="manyMentions"
|
||||
|
@ -17,10 +17,10 @@
|
|||
>
|
||||
<MentionLink
|
||||
v-for="mention in extraMentions"
|
||||
:key="mention.statusnet_profile_url"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.statusnet_profile_url"
|
||||
:url="mention.statusnet_profile_url"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
:first-mention="false"
|
||||
/>
|
||||
</span><button
|
||||
|
|
|
@ -1,6 +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 { 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'
|
||||
|
||||
|
@ -24,27 +25,72 @@ export default Vue.component('RichContent', {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Meme arrows
|
||||
greentext: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Whether to hide last mentions (hellthreads)
|
||||
hideMentions: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
// NEVER EVER TOUCH DATA INSIDE RENDER
|
||||
render (h) {
|
||||
// Pre-process HTML
|
||||
const { newHtml: html, lastMentions } = preProcessPerLine(this.html, this.greentext, this.hideMentions)
|
||||
const firstMentions = [] // Mentions that appear in the beginning 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
|
||||
// unique index for vue "tag" property
|
||||
let mentionIndex = 0
|
||||
let tagsIndex = 0
|
||||
|
||||
const renderImage = (tag) => {
|
||||
return <StillImage
|
||||
{...{ attrs: getAttrs(tag) }}
|
||||
class="img"
|
||||
/>
|
||||
}
|
||||
const renderMention = (attrs, children, encounteredText) => {
|
||||
return <MentionLink
|
||||
url={attrs.href}
|
||||
content={flattenDeep(children).join('')}
|
||||
firstMention={!encounteredText}
|
||||
/>
|
||||
|
||||
const renderHashtag = (attrs, children, encounteredTextReverse) => {
|
||||
const linkData = getLinkData(attrs, children, tagsIndex++)
|
||||
writtenTags.push(linkData)
|
||||
attrs.target = '_blank'
|
||||
if (!encounteredTextReverse) {
|
||||
lastTags.push(linkData)
|
||||
attrs['data-parser-last'] = true
|
||||
}
|
||||
return <a {...{ attrs }}>
|
||||
{ children.map(processItem) }
|
||||
</a>
|
||||
}
|
||||
|
||||
const renderMention = (attrs, children, encounteredText) => {
|
||||
const linkData = getLinkData(attrs, children, mentionIndex++)
|
||||
writtenMentions.push(linkData)
|
||||
if (!encounteredText) {
|
||||
firstMentions.push(linkData)
|
||||
return ''
|
||||
} else {
|
||||
return <MentionLink
|
||||
url={attrs.href}
|
||||
content={flattenDeep(children).join('')}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
// Handle text noes - just add emoji
|
||||
// Processor to use with html_tree_converter
|
||||
const processItem = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
if (typeof item === 'string') {
|
||||
const emptyText = item.trim() === ''
|
||||
if (emptyText) {
|
||||
|
@ -56,7 +102,7 @@ export default Vue.component('RichContent', {
|
|||
encounteredText = true
|
||||
}
|
||||
if (item.includes(':')) {
|
||||
return processTextForEmoji(
|
||||
unescapedItem = processTextForEmoji(
|
||||
unescapedItem,
|
||||
this.emoji,
|
||||
({ shortcode, url }) => {
|
||||
|
@ -68,24 +114,42 @@ export default Vue.component('RichContent', {
|
|||
/>
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return unescapedItem
|
||||
}
|
||||
return unescapedItem
|
||||
}
|
||||
|
||||
// Handle tag nodes
|
||||
if (Array.isArray(item)) {
|
||||
const [opener, children] = item
|
||||
const Tag = getTagName(opener)
|
||||
const attrs = getAttrs(opener)
|
||||
switch (Tag) {
|
||||
case 'span': // replace images with StillImage
|
||||
if (attrs['class'] && attrs['class'].includes('lastMentions')) {
|
||||
if (firstMentions.length > 0) {
|
||||
break
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
case 'img': // replace images with StillImage
|
||||
return renderImage(opener)
|
||||
case 'a': // replace mentions with MentionLink
|
||||
if (!this.handleLinks) break
|
||||
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 }}>
|
||||
{ children.map(processItem) }
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
// Render tag as is
|
||||
if (children !== undefined) {
|
||||
return <Tag {...{ attrs: getAttrs(opener) }}>
|
||||
|
@ -96,8 +160,163 @@ export default Vue.component('RichContent', {
|
|||
}
|
||||
}
|
||||
}
|
||||
return <span class="RichContent">
|
||||
{ convertHtml(this.html).map(processItem) }
|
||||
|
||||
// Processor for back direction (for finding "last" stuff, just easier this way)
|
||||
let encounteredTextReverse = false
|
||||
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
|
||||
}
|
||||
|
||||
// DO NOT USE SLOTS they cause a re-render feedback loop here.
|
||||
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
|
||||
// at least until vue3?
|
||||
const result = <span class="RichContent">
|
||||
{ convertHtmlToTree(html).map(processItem).reverse().map(processItemReverse).reverse() }
|
||||
</span>
|
||||
|
||||
const event = {
|
||||
firstMentions,
|
||||
lastMentions,
|
||||
lastTags,
|
||||
writtenMentions,
|
||||
writtenTags
|
||||
}
|
||||
|
||||
// DO NOT MOVE TO UPDATE. BAD IDEA.
|
||||
this.$emit('parseReady', event)
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
const getLinkData = (attrs, children, index) => {
|
||||
return {
|
||||
index,
|
||||
url: attrs.href,
|
||||
hashtag: attrs['data-tag'],
|
||||
content: flattenDeep(children).join('')
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
*/
|
||||
export const preProcessPerLine = (html, greentext) => {
|
||||
const lastMentions = []
|
||||
|
||||
const newHtml = convertHtmlToLines(html).reverse().map((item, index, array) => {
|
||||
// Going over each line in reverse to detect last mentions,
|
||||
// keeping non-text stuff as-is
|
||||
if (!item.text) return item
|
||||
const string = item.text
|
||||
|
||||
// Greentext stuff
|
||||
if (greentext && (string.includes('>') || string.includes('<'))) {
|
||||
const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
if (cleanedString.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else if (cleanedString.startsWith('<')) {
|
||||
return `<span class='cyantext'>${string}</span>`
|
||||
}
|
||||
}
|
||||
|
||||
// Converting that line part into tree
|
||||
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 we have a link we probably have mentions
|
||||
if (tag === 'a') {
|
||||
const attrs = getAttrs(opener)
|
||||
if (attrs['class'] && attrs['class'].includes('mention')) {
|
||||
// Got mentions
|
||||
hasMentions = true
|
||||
return [opener, children, closer]
|
||||
} else {
|
||||
// Not a mention? Means we have loose text or whatever
|
||||
hasLooseText = true
|
||||
return [opener, children, closer]
|
||||
}
|
||||
} else if (tag === 'span' || tag === 'p') {
|
||||
// For span and p we need to go deeper
|
||||
return [opener, [...children].map(process), closer]
|
||||
} else {
|
||||
// Everything else equals to a loose text
|
||||
hasLooseText = true
|
||||
return [opener, children, closer]
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
if (item.trim() !== '') {
|
||||
// only meaningful strings are loose text
|
||||
hasLooseText = true
|
||||
}
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
// We now processed our tree, now we need to mark line as lastMentions
|
||||
const result = [...tree].map(process)
|
||||
|
||||
// Only check last (first since list is reversed) line
|
||||
if (hasMentions && !hasLooseText && index === 0) {
|
||||
let mentionIndex = 0
|
||||
const process = (item) => {
|
||||
if (Array.isArray(item)) {
|
||||
const [opener, children] = item
|
||||
const tag = getTagName(opener)
|
||||
if (tag === 'a') {
|
||||
const attrs = getAttrs(opener)
|
||||
lastMentions.push(getLinkData(attrs, children, mentionIndex++))
|
||||
} else if (children) {
|
||||
children.forEach(process)
|
||||
}
|
||||
}
|
||||
}
|
||||
result.forEach(process)
|
||||
// we DO need mentions here so that we conditionally remove them if don't
|
||||
// have first mentions
|
||||
return ['<span class="lastMentions">', flattenDeep(result).join(''), '</span>'].join('')
|
||||
} else {
|
||||
return flattenDeep(result).join('')
|
||||
}
|
||||
}).reverse().join('')
|
||||
|
||||
return { newHtml, lastMentions }
|
||||
}
|
||||
|
|
|
@ -100,7 +100,8 @@ const Status = {
|
|||
userExpanded: false,
|
||||
mediaPlaying: [],
|
||||
suspendable: true,
|
||||
error: null
|
||||
error: null,
|
||||
headTailLinks: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -141,7 +142,8 @@ const Status = {
|
|||
replyProfileLink () {
|
||||
if (this.isReply) {
|
||||
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
|
||||
return user && user.statusnet_profile_url
|
||||
// FIXME Why user not found sometimes???
|
||||
return user ? user.statusnet_profile_url : 'NOT_FOUND'
|
||||
}
|
||||
},
|
||||
retweet () { return !!this.statusoid.retweeted_status },
|
||||
|
@ -166,17 +168,31 @@ const Status = {
|
|||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
},
|
||||
mentionsOwnLine () {
|
||||
return this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
mentions () {
|
||||
return this.status.attentions.filter(attn => {
|
||||
return attn.screen_name !== this.replyToName &&
|
||||
attn.screen_name !== this.status.user.screen_name
|
||||
}).map(attn => ({
|
||||
url: attn.statusnet_profile_url,
|
||||
content: attn.screen_name,
|
||||
userId: attn.id
|
||||
}))
|
||||
},
|
||||
alsoMentions () {
|
||||
if (!this.headTailLinks) return []
|
||||
const set = new Set(this.headTailLinks.writtenMentions.map(m => m.url))
|
||||
return this.headTailLinks.writtenMentions.filter(mention => {
|
||||
return !set.has(mention.url)
|
||||
})
|
||||
},
|
||||
hasMentions () {
|
||||
return this.mentions.length > 0
|
||||
mentionsLine () {
|
||||
return this.mentionsOwnLine ? this.mentions : this.alsoMentions
|
||||
},
|
||||
mentionsOwnLine () {
|
||||
return this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
hasMentionsLine () {
|
||||
return this.mentionsLine.length > 0
|
||||
},
|
||||
muted () {
|
||||
if (this.statusoid.user.id === this.currentUser.id) return false
|
||||
|
@ -343,6 +359,9 @@ const Status = {
|
|||
},
|
||||
removeMediaPlaying (id) {
|
||||
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
|
||||
},
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
this.headTailLinks = headTailLinks
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -353,7 +372,7 @@ const Status = {
|
|||
// Post is above screen, match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.height >= (window.innerHeight - 50)) {
|
||||
// Post we want to see is taller than screen so match its top to screen top
|
||||
// Post we wahttp://localhost:8080/users/hj/dmsnt to see is taller than screen so match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.bottom > window.innerHeight - 50) {
|
||||
// Post is below screen, match its bottom to screen bottom
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
@ -151,36 +150,24 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.glued-label {
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeago {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
& .heading-mentions-row,
|
||||
& .heading-reply-row {
|
||||
position: relative;
|
||||
align-content: baseline;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
line-height: 160%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.reply-to-and-accountname {
|
||||
display: flex;
|
||||
height: 18px;
|
||||
margin-right: 0.5em;
|
||||
max-width: 100%;
|
||||
|
||||
.reply-to-link {
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
& .reply-to-popover,
|
||||
& .reply-to-no-popover {
|
||||
min-width: 0;
|
||||
|
@ -217,21 +204,25 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
& .mentions,
|
||||
& .reply-to {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.reply-to-text {
|
||||
& .reply-to-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.replies-separator {
|
||||
margin-left: 0.4em;
|
||||
.mentions-line {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.replies {
|
||||
margin-top: 0.25em;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
|
|
|
@ -221,10 +221,13 @@
|
|||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="heading-reply-row">
|
||||
<div
|
||||
<div
|
||||
v-if="isReply || hasMentionsLine"
|
||||
class="heading-reply-row"
|
||||
>
|
||||
<span
|
||||
v-if="isReply"
|
||||
class="reply-to-and-accountname"
|
||||
class="glued-label"
|
||||
>
|
||||
<StatusPopover
|
||||
v-if="!isPreview"
|
||||
|
@ -257,81 +260,77 @@
|
|||
>
|
||||
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
|
||||
</span>
|
||||
|
||||
<MentionLink
|
||||
class="mention-link"
|
||||
:content="replyToName"
|
||||
:url="replyProfileLink"
|
||||
:user-id="status.in_reply_to_user_id"
|
||||
:user-screen-name="status.in_reply_to_screen_name"
|
||||
:first-mention="false"
|
||||
/>
|
||||
<span
|
||||
v-if="replies && replies.length"
|
||||
class="faint replies-separator"
|
||||
>
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
class="replies"
|
||||
>
|
||||
<span class="faint">{{ $t('status.replies_list') }}</span>
|
||||
<StatusPopover
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:status-id="reply.id"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link reply-link"
|
||||
@click.prevent="gotoOriginal(reply.id)"
|
||||
>
|
||||
{{ reply.name }}
|
||||
</button>
|
||||
</StatusPopover>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="hasMentions && mentionsOwnLine"
|
||||
class="heading-mentions-row"
|
||||
>
|
||||
<div
|
||||
class="mentions"
|
||||
<!-- This little wrapper is made for sole purpose of "gluing" -->
|
||||
<!-- "Mentions" label to the first mention -->
|
||||
<span
|
||||
v-if="hasMentionsLine"
|
||||
class="glued-label"
|
||||
>
|
||||
<span
|
||||
class="button-unstyled reply-to"
|
||||
:aria-label="$t('tool_tip.reply')"
|
||||
class="mentions"
|
||||
:aria-label="$t('tool_tip.mentions')"
|
||||
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="at"
|
||||
/>
|
||||
<span
|
||||
class="faint-link reply-to-text"
|
||||
class="faint-link mentions-text"
|
||||
>
|
||||
{{ $t('status.mentions') }}
|
||||
</span>
|
||||
</span>
|
||||
<MentionsLine
|
||||
:attentions="mentions"
|
||||
class="mentions-line"
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(0, 1)"
|
||||
class="mentions-line-first"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<MentionsLine
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(1)"
|
||||
class="mentions-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
ref="content"
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
:hide-mentions="mentionsOwnLine && (isReply || true)"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
class="replies"
|
||||
>
|
||||
<span class="faint">{{ $t('status.replies_list') }}</span>
|
||||
<StatusPopover
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:status-id="reply.id"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link reply-link"
|
||||
@click.prevent="gotoOriginal(reply.id)"
|
||||
>
|
||||
{{ reply.name }}
|
||||
</button>
|
||||
</StatusPopover>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import RichContent, { getHeadTailLinks } from 'src/components/rich_content/rich_content.jsx'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { set } from 'vue'
|
||||
import {
|
||||
faFile,
|
||||
faMusic,
|
||||
|
@ -28,14 +27,18 @@ const StatusContent = {
|
|||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
'singleLine'
|
||||
'singleLine',
|
||||
'hideMentions'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||
showingLongSubject: false,
|
||||
// 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,
|
||||
headTailLinks: null,
|
||||
firstMentions: [],
|
||||
lastMentions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -72,45 +75,9 @@ const StatusContent = {
|
|||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
postBodyHtml () {
|
||||
const html = this.status.raw_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.error('Failed to process status html', e)
|
||||
return html
|
||||
}
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
},
|
||||
attachmentTypes () {
|
||||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
},
|
||||
mentionsOwnLine () {
|
||||
return this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
mentions () {
|
||||
return this.status.attentions
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
components: {
|
||||
|
@ -124,21 +91,6 @@ const StatusContent = {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
linkClicked (event) {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from dataset or link url
|
||||
const tag = target.dataset.tag || extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
|
@ -146,6 +98,11 @@ const StatusContent = {
|
|||
this.expandingSubject = !this.expandingSubject
|
||||
}
|
||||
},
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
set(this, 'headTailLinks', headTailLinks)
|
||||
set(this, 'firstMentions', headTailLinks.firstMentions)
|
||||
set(this, 'lastMentions', headTailLinks.lastMentions)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
}
|
||||
|
|
|
@ -112,6 +112,10 @@
|
|||
color: var(--postGreentext, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.cyantext {
|
||||
color: var(--postCyantext, $fallback--cBlue);
|
||||
}
|
||||
|
||||
/* Not sure if this is necessary */
|
||||
video {
|
||||
max-width: 100%;
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
class="media-body summary"
|
||||
:html="status.summary_raw_html"
|
||||
:emoji="status.emojis"
|
||||
@click.prevent="linkClicked"
|
||||
/>
|
||||
<button
|
||||
v-if="longSubject && showingLongSubject"
|
||||
|
@ -39,22 +38,24 @@
|
|||
>
|
||||
{{ $t("general.show_more") }}
|
||||
</button>
|
||||
<span
|
||||
class="text-wrapper"
|
||||
v-if="!hideSubjectStatus && !(singleLine && status.summary_html)"
|
||||
>
|
||||
<span v-if="!hideSubjectStatus && !(singleLine && status.summary_html)">
|
||||
<MentionsLine
|
||||
v-if="!mentionsOwnLine"
|
||||
:attentions="status.attentions"
|
||||
class="mentions-line"
|
||||
v-if="!hideMentions && firstMentions && firstMentions.length > 0"
|
||||
:mentions="firstMentions"
|
||||
/>
|
||||
<RichContent
|
||||
:class="{ '-single-line': singleLine }"
|
||||
class="text media-body"
|
||||
:html="postBodyHtml"
|
||||
:html="status.raw_html"
|
||||
:emoji="status.emojis"
|
||||
:handle-links="true"
|
||||
@click.prevent="linkClicked"
|
||||
:hide-mentions="hideMentions"
|
||||
:greentext="mergedConfig.greentext"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
<MentionsLine
|
||||
v-if="!hideMentions && lastMentions.length > 0 && firstMentions.length === 0"
|
||||
:mentions="lastMentions"
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ const StatusContent = {
|
|||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
'singleLine'
|
||||
'singleLine',
|
||||
'hideMentions'
|
||||
],
|
||||
computed: {
|
||||
hideAttachments () {
|
||||
|
@ -91,6 +92,9 @@ const StatusContent = {
|
|||
StatusBody
|
||||
},
|
||||
methods: {
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
this.$emit('parseReady', headTailLinks)
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
<StatusBody
|
||||
:status="status"
|
||||
:single-line="singleLine"
|
||||
:hide-mentions="hideMentions"
|
||||
@parseReady="setHeadTailLinks"
|
||||
>
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<poll :base-poll="status.poll" />
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
/**
|
||||
* 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) => {
|
||||
const handledTags = new Set(['p', 'br', 'div'])
|
||||
const openCloseTags = new Set(['p', 'div'])
|
||||
export const convertHtmlToLines = (html) => {
|
||||
const ignoredTags = new Set(['code', 'blockquote'])
|
||||
const handledTags = new Set(['p', 'br', 'div', 'pre', 'code', 'blockquote'])
|
||||
const openCloseTags = new Set(['p', 'div', 'pre', 'code', 'blockquote'])
|
||||
|
||||
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
|
||||
|
@ -24,30 +33,30 @@ 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)
|
||||
if (textBuffer.trim().length > 0 && !level.some(l => ignoredTags.has(l))) {
|
||||
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
|
||||
level.push(tag)
|
||||
buffer.push(tag)
|
||||
level.unshift(getTagName(tag))
|
||||
}
|
||||
|
||||
const handleClose = (tag) => { // handles closing tags
|
||||
flush()
|
||||
buffer += tag
|
||||
if (level[level.length - 1] === tag) {
|
||||
level.pop()
|
||||
buffer.push(tag)
|
||||
if (level[0] === getTagName(tag)) {
|
||||
level.shift()
|
||||
}
|
||||
}
|
||||
|
|
@ -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([
|
|
@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = {
|
|||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
postCyantext: {
|
||||
depends: ['cBlue'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
|
||||
const mapOnlyText = (processor) => (input) => input.text ? processor(input.text) : input
|
||||
|
||||
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/>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).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>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with very broken HTML with broken composition', () => {
|
||||
const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with sorta valid HTML but tags aren\'t closed', () => {
|
||||
const inputOutput = 'just leaving a <div> hanging'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).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='
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).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>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).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"/> ?'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with valid XHTML containing a CDATA', () => {
|
||||
const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).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/>'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).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 = '_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).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>'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with sorta valid HTML but tags aren\'t closed', () => {
|
||||
const input = 'just leaving a <div> hanging'
|
||||
const output = '_<div>_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).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 = '_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).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>'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).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"/>_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with valid XHTML containing a CDATA', () => {
|
||||
const input = 'Yes, it is me, <![CDATA[DIO]]>'
|
||||
const output = '_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('Testing handling ignored blocks', () => {
|
||||
const input = `
|
||||
<pre><code>> rei = "0"
|
||||
'0'
|
||||
> rei == 0
|
||||
true
|
||||
> rei == null
|
||||
false</code></pre><blockquote>That, christian-like JS diagram but it’s evangelion instead.</blockquote>
|
||||
`
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(input)
|
||||
})
|
||||
it('Testing handling ignored blocks 2', () => {
|
||||
const input = `
|
||||
<blockquote>An SSL error has happened.</blockquote><p>Shakespeare</p>
|
||||
`
|
||||
const output = `
|
||||
<blockquote>An SSL error has happened.</blockquote><p>_</p>
|
||||
`
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,10 +1,10 @@
|
|||
import { convertHtml, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
|
||||
import { convertHtmlToTree, processTextForEmoji, getAttrs } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
|
||||
describe('MiniHtmlConverter', () => {
|
||||
describe('convertHtml', () => {
|
||||
describe('convertHtmlToTree', () => {
|
||||
it('converts html into a tree structure', () => {
|
||||
const input = '1 <p>2</p> <b>3<img src="a">4</b>5'
|
||||
expect(convertHtml(input)).to.eql([
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
'1 ',
|
||||
[
|
||||
'<p>',
|
||||
|
@ -26,7 +26,7 @@ describe('MiniHtmlConverter', () => {
|
|||
})
|
||||
it('converts html to tree while preserving tag formatting', () => {
|
||||
const input = '1 <p >2</p><b >3<img src="a">4</b>5'
|
||||
expect(convertHtml(input)).to.eql([
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
'1 ',
|
||||
[
|
||||
'<p >',
|
||||
|
@ -47,7 +47,7 @@ describe('MiniHtmlConverter', () => {
|
|||
})
|
||||
it('converts semi-broken html', () => {
|
||||
const input = '1 <br> 2 <p> 42'
|
||||
expect(convertHtml(input)).to.eql([
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
'1 ',
|
||||
['<br>'],
|
||||
' 2 ',
|
||||
|
@ -59,7 +59,7 @@ describe('MiniHtmlConverter', () => {
|
|||
})
|
||||
it('realistic case 1', () => {
|
||||
const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>'
|
||||
expect(convertHtml(input)).to.eql([
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
[
|
||||
'<p>',
|
||||
[
|
||||
|
@ -112,7 +112,7 @@ describe('MiniHtmlConverter', () => {
|
|||
})
|
||||
it('realistic case 2', () => {
|
||||
const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso'
|
||||
expect(convertHtml(inputOutput)).to.eql([
|
||||
expect(convertHtmlToTree(inputOutput)).to.eql([
|
||||
'Country improv: give me a city',
|
||||
[
|
||||
'<br/>'
|
|
@ -1,96 +0,0 @@
|
|||
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)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Add table
Reference in a new issue