@ -4,8 +4,7 @@ import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_con
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
import { convertHtmlToLines } from 'src/services/html_converter/html_line_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 StillImage from 'src/components/still-image/still-image.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
import MentionsLine , { MENTIONS _LIMIT } from 'src/components/mentions_line/mentions_line.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import './rich_content.scss'
import './rich_content.scss'
@ -13,12 +12,11 @@ import './rich_content.scss'
* RichContent , The Über - powered component for rendering Post HTML .
* RichContent , The Über - powered component for rendering Post HTML .
*
*
* This takes post HTML and does multiple things to it :
* This takes post HTML and does multiple things to it :
* - Converts mention links to < MentionLink > - s
* - Groups all mentions into < MentionsLine > , this affects all mentions regardles
* - Removes mentions from beginning and end ( hellthread style only )
* of where they are ( beginning / middle / end ) , even single mentions are converted
* to a < MentionsLine > containing single < MentionLink > .
* - Replaces emoji shortcodes with < StillImage > ' d images .
* - Replaces emoji shortcodes with < StillImage > ' d images .
*
*
* Stuff like removing mentions from beginning and end is done so that they could
* be either replaced by collapsible < MentionsLine > or moved to separate place .
* There are two problems with this component ' s architecture :
* There are two problems with this component ' s architecture :
* 1. Parsing HTML and rendering are inseparable . Attempts to separate the two
* 1. Parsing HTML and rendering are inseparable . Attempts to separate the two
* proven to be a massive overcomplication due to amount of things done here .
* proven to be a massive overcomplication due to amount of things done here .
@ -56,25 +54,22 @@ export default Vue.component('RichContent', {
required : false ,
required : false ,
type : Boolean ,
type : Boolean ,
default : false
default : false
} ,
hideMentions : {
required : false ,
type : Boolean ,
default : false
}
}
} ,
} ,
/ / N E V E R E V E R T O U C H D A T A I N S I D E R E N D E R
/ / N E V E R E V E R T O U C H D A T A I N S I D E R E N D E R
render ( h ) {
render ( h ) {
/ / P r e - p r o c e s s H T M L
/ / P r e - p r o c e s s H T M L
const { newHtml : html , lastMentions } = preProcessPerLine ( this . html , this . greentext , this . handleLinks )
const { newHtml : html } = preProcessPerLine ( this . html , this . greentext )
const firstMentions = [ ] / / M e n t i o n s t h a t a p p e a r i n t h e b e g i n n i n g o f p o s t b o d y
let currentMentions = null / / C u r r e n t c h a i n o f m e n t i o n s , w e g r o u p a l l m e n t i o n s t o g e t h e r
const lastTags = [ ] / / T a g s t h a t a p p e a r a t t h e e n d o f p o s t b o d y
const lastTags = [ ] / / T a g s t h a t a p p e a r a t t h e e n d o f p o s t b o d y
const writtenMentions = [ ] / / A l l m e n t i o n s t h a t a p p e a r i n p o s t b o d y
const writtenMentions = [ ] / / A l l m e n t i o n s t h a t a p p e a r i n p o s t b o d y
const invisibleMentions = [ ] / / A l l m e n t i o n s t h a t g o b e y o n d t h e l i m i t e r ( s e e M e n t i o n s L i n e )
/ / t o c o l l a p s e t o o m a n y m e n t i o n s i n a r o w
const writtenTags = [ ] / / A l l t a g s t h a t a p p e a r i n p o s t b o d y
const writtenTags = [ ] / / A l l t a g s t h a t a p p e a r i n p o s t b o d y
/ / u n i q u e i n d e x f o r v u e " t a g " p r o p e r t y
/ / u n i q u e i n d e x f o r v u e " t a g " p r o p e r t y
let mentionIndex = 0
let mentionIndex = 0
let tagsIndex = 0
let tagsIndex = 0
let firstMentionReplaced = false
const renderImage = ( tag ) => {
const renderImage = ( tag ) => {
return < StillImage
return < StillImage
@ -98,41 +93,35 @@ export default Vue.component('RichContent', {
const renderMention = ( attrs , children ) => {
const renderMention = ( attrs , children ) => {
const linkData = getLinkData ( attrs , children , mentionIndex ++ )
const linkData = getLinkData ( attrs , children , mentionIndex ++ )
linkData . notifying = this . attentions . some ( a => a . statusnet _profile _url === linkData . url )
linkData . notifying = this . attentions . some ( a => a . statusnet _profile _url === linkData . url )
if ( ! linkData . notifying ) {
encounteredText = true
}
writtenMentions . push ( linkData )
writtenMentions . push ( linkData )
if ( ! encounteredText ) {
if ( currentMentions === null ) {
firstMentions . push ( linkData )
currentMentions = [ ]
if ( ! firstMentionReplaced && ! this . hideMentions ) {
}
firstMentionReplaced = true
currentMentions . push ( linkData )
return < MentionsLine mentions = { firstMentions } / >
if ( currentMentions . length > MENTIONS _LIMIT ) {
} else {
invisibleMentions . push ( linkData )
return ''
}
}
if ( currentMentions . length === 1 ) {
return < MentionsLine mentions = { currentMentions } / >
} else {
} else {
return < MentionLink
return ''
url = { attrs . href }
content = { flattenDeep ( children ) . join ( '' ) }
/ >
}
}
}
}
/ / W e s t o p t r e a t i n g m e n t i o n s a s " f i r s t " o n e s w h e n w e e n c o u n t e r
/ / n o n - w h i t e s p a c e t e x t
let encounteredText = false
/ / P r o c e s s o r t o u s e w i t h h t m l _ t r e e _ c o n v e r t e r
/ / P r o c e s s o r t o u s e w i t h h t m l _ t r e e _ c o n v e r t e r
const processItem = ( item , index , array , what ) => {
const processItem = ( item , index , array , what ) => {
/ / H a n d l e t e x t n o d e s - j u s t a d d e m o j i
/ / H a n d l e t e x t n o d e s - j u s t a d d e m o j i
if ( typeof item === 'string' ) {
if ( typeof item === 'string' ) {
const emptyText = item . trim ( ) === ''
const emptyText = item . trim ( ) === ''
if ( emptyText ) {
if ( item. includes ( '\n' ) ) {
return encounteredText ? item : item . trim ( )
currentMentions = null
}
}
if ( ! encounteredText ) {
if ( emptyText ) {
item = item . trimStart ( )
/ / d o n ' t i n c l u d e s p a c e s w h e n p r o c e s s i n g m e n t i o n s - w e ' l l i n c l u d e t h e m
encounteredText = true
/ / i n M e n t i o n s L i n e
return currentMentions !== null ? item . trim ( ) : item
}
}
currentMentions = null
if ( item . includes ( ':' ) ) {
if ( item . includes ( ':' ) ) {
item = [ '' , processTextForEmoji (
item = [ '' , processTextForEmoji (
item ,
item ,
@ -156,28 +145,25 @@ export default Vue.component('RichContent', {
const Tag = getTagName ( opener )
const Tag = getTagName ( opener )
const attrs = getAttrs ( opener )
const attrs = getAttrs ( opener )
switch ( Tag ) {
switch ( Tag ) {
case 'span' : / / R e p l a c e l a s t m e n t i o n s c l a s s w i t h m e n t i o n s l i n e
case 'br' :
if ( attrs [ 'class' ] && attrs [ 'class' ] . includes ( 'lastMentions' ) ) {
currentMentions = null
if ( firstMentions . length > 1 && lastMentions . length > 1 ) {
break
break
} else {
return ! this . hideMentions ? < MentionsLine mentions = { lastMentions } / > : ''
}
} else {
break
}
case 'img' : / / r e p l a c e i m a g e s w i t h S t i l l I m a g e
case 'img' : / / r e p l a c e i m a g e s w i t h S t i l l I m a g e
return renderImage ( opener )
return renderImage ( opener )
case 'a' : / / r e p l a c e m e n t i o n s w i t h M e n t i o n L i n k
case 'a' : / / r e p l a c e m e n t i o n s w i t h M e n t i o n L i n k
if ( ! this . handleLinks ) break
if ( ! this . handleLinks ) break
if ( attrs [ 'class' ] && attrs [ 'class' ] . includes ( 'mention' ) ) {
if ( attrs [ 'class' ] && attrs [ 'class' ] . includes ( 'mention' ) ) {
/ / H a n d l i n g m e n t i o n s h e r e
/ / H a n d l i n g m e n t i o n s h e r e
return renderMention ( attrs , children , encounteredText )
return renderMention ( attrs , children )
} else {
} else {
/ / E v e r y t h i n g e l s e w i l l b e h a n d l e d i n r e v e r s e p a s s
/ / E v e r y t h i n g e l s e w i l l b e h a n d l e d i n r e v e r s e p a s s
encounteredText = true
currentMentions = null
return item / / W e ' l l h a n d l e i t l a t e r
return item / / W e ' l l h a n d l e i t l a t e r
}
}
case 'span' :
if ( this . handleLinks && attrs [ 'class' ] && attrs [ 'class' ] . includes ( 'h-card' ) ) {
return [ '' , children . map ( processItem ) , '' ]
}
}
}
if ( children !== undefined ) {
if ( children !== undefined ) {
@ -246,11 +232,10 @@ export default Vue.component('RichContent', {
< / span >
< / span >
const event = {
const event = {
firstMentions ,
lastMentions ,
lastTags ,
lastTags ,
writtenMentions ,
writtenMentions ,
writtenTags
writtenTags ,
invisibleMentions
}
}
/ / D O N O T M O V E T O U P D A T E . B A D I D E A .
/ / D O N O T M O V E T O U P D A T E . B A D I D E A .
@ -261,44 +246,46 @@ export default Vue.component('RichContent', {
} )
} )
const getLinkData = ( attrs , children , index ) => {
const getLinkData = ( attrs , children , index ) => {
const stripTags = ( item ) => {
if ( typeof item === 'string' ) {
return item
} else {
return item [ 1 ] . map ( stripTags ) . join ( '' )
}
}
const textContent = children . map ( stripTags ) . join ( '' )
return {
return {
index ,
index ,
url : attrs . href ,
url : attrs . href ,
hashtag : attrs [ 'data-tag' ] ,
hashtag : attrs [ 'data-tag' ] ,
content : flattenDeep ( children ) . join ( '' )
content : flattenDeep ( children ) . join ( '' ) ,
textContent
}
}
}
}
/ * * P r e - p r o c e s s i n g H T M L
/ * * P r e - p r o c e s s i n g H T M L
*
*
* Currently this does tw o thing s :
* Currently this does one thing :
* - add green / cyantexting
* - 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 { String } html - raw HTML to process
* @ param { Boolean } greentext - whether to enable greentexting or not
* @ param { Boolean } greentext - whether to enable greentexting or not
* @ param { Boolean } handleLinks - whether to handle links or not
* /
* /
export const preProcessPerLine = ( html , greentext , handleLinks ) => {
export const preProcessPerLine = ( html , greentext ) => {
const lastMentions = [ ]
const greentextHandle = new Set ( [ 'p' , 'div' ] )
const greentextHandle = new Set ( [ 'p' , 'div' ] )
let nonEmptyIndex = - 1
const lines = convertHtmlToLines ( html )
const lines = convertHtmlToLines ( html )
const linesNum = lines . filter ( c => c . text ) . length
const newHtml = lines . reverse ( ) . map ( ( item , index , array ) => {
const newHtml = lines . reverse ( ) . map ( ( item , index , array ) => {
/ / G o i n g o v e r e a c h l i n e i n r e v e r s e t o d e t e c t l a s t m e n t i o n s ,
/ / G o i n g o v e r e a c h l i n e i n r e v e r s e t o d e t e c t l a s t m e n t i o n s ,
/ / k e e p i n g n o n - t e x t s t u f f a s - i s
/ / k e e p i n g n o n - t e x t s t u f f a s - i s
if ( ! item . text ) return item
if ( ! item . text ) return item
const string = item . text
const string = item . text
nonEmptyIndex += 1
/ / G r e e n t e x t s t u f f
/ / G r e e n t e x t s t u f f
if (
if (
/ / O n l y i f g r e e n t e x t i s e n g a g e d
/ / O n l y i f g r e e n t e x t i s e n g a g e d
greentext &&
greentext &&
/ / O n l y h a n d l e p ' s a n d d i v s . D o n ' t w a n t t o a f f e c t b l o c qu o t e s , c o d e e t c
/ / O n l y h a n d l e p ' s a n d d i v s . D o n ' t w a n t t o a f f e c t b l o c k qu o t e s , c o d e e t c
item . level . every ( l => greentextHandle . has ( l ) ) &&
item . level . every ( l => greentextHandle . has ( l ) ) &&
/ / O n l y i f l i n e b e g i n s w i t h ' > ' o r ' < '
/ / O n l y i f l i n e b e g i n s w i t h ' > ' o r ' < '
( string . includes ( '>' ) || string . includes ( '<' ) )
( string . includes ( '>' ) || string . includes ( '<' ) )
@ -313,80 +300,8 @@ export const preProcessPerLine = (html, greentext, handleLinks) => {
}
}
}
}
/ / C o n v e r t i n g t h a t l i n e p a r t i n t o t r e e
return string
const tree = convertHtmlToTree ( string )
/ / I f l i n e h a s l o o s e t e x t , i . e . t e x t o u t s i d e a m e n t i o n o r a t a g
/ / w e w o n ' t t o u c h m e n t i o n s .
let hasLooseText = false
let mentionsNum = 0
const process = ( item ) => {
if ( Array . isArray ( item ) ) {
const [ opener , children , closer ] = item
const tag = getTagName ( opener )
/ / I f w e h a v e a l i n k w e p r o b a b l y h a v e m e n t i o n s
if ( tag === 'a' ) {
if ( ! handleLinks ) return [ opener , children , closer ]
const attrs = getAttrs ( opener )
if ( attrs [ 'class' ] && attrs [ 'class' ] . includes ( 'mention' ) ) {
/ / G o t m e n t i o n s
mentionsNum ++
return [ opener , children , closer ]
} else {
/ / N o t a m e n t i o n ? M e a n s w e h a v e l o o s e t e x t o r w h a t e v e r
hasLooseText = true
return [ opener , children , closer ]
}
} else if ( tag === 'span' || tag === 'p' ) {
/ / F o r s p a n a n d p w e n e e d t o g o d e e p e r
return [ opener , [ ... children ] . map ( process ) , closer ]
} else {
/ / E v e r y t h i n g e l s e e q u a l s t o a l o o s e t e x t
hasLooseText = true
return [ opener , children , closer ]
}
}
if ( typeof item === 'string' ) {
if ( item . trim ( ) !== '' ) {
/ / o n l y m e a n i n g f u l s t r i n g s a r e l o o s e t e x t
hasLooseText = true
}
return item
}
}
/ / W e n o w p r o c e s s e d o u r t r e e , n o w w e n e e d t o m a r k l i n e a s l a s t M e n t i o n s
const result = [ ... tree ] . map ( process )
if (
handleLinks && / / D o w e h a n d l e l i n k s a t a l l ?
mentionsNum > 1 && / / D o e s i t h a v e m o r e t h a n o n e m e n t i o n ?
! hasLooseText && / / D o n ' t d o a n y t h i n g i f i t h a s s o m e t h i n g b e s i d e s m e n t i o n s
nonEmptyIndex === 0 && / / O n l y c h e c k l a s t ( f i r s t s i n c e l i s t i s r e v e r s e d ) l i n e
nonEmptyIndex !== linesNum - 1 / / D o n ' t d o a n y t h i n g i f t h e r e ' s o n l y o n e l i n e
) {
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 )
/ / w e D O n e e d m e n t i o n s h e r e s o t h a t w e c o n d i t i o n a l l y r e m o v e t h e m i f d o n ' t
/ / h a v e f i r s t m e n t i o n s
return [ '<span class="lastMentions">' , flattenDeep ( result ) . join ( '' ) , '</span>' ] . join ( '' )
} else {
return flattenDeep ( result ) . join ( '' )
}
} ) . reverse ( ) . join ( '' )
} ) . reverse ( ) . join ( '' )
return { newHtml , lastMentions }
return { newHtml }
}
}