@ -1,6 +1,274 @@
let LabelAccumulator = require ( './LabelAccumulator' ) . default ;
let ComponentUtil = require ( './ComponentUtil' ) . default ;
// Hash of prepared regexp's for tags
var tagPattern = {
// HTML
'<b>' : /<b>/ ,
'<i>' : /<i>/ ,
'<code>' : /<code>/ ,
'</b>' : /<\/b>/ ,
'</i>' : /<\/i>/ ,
'</code>' : /<\/code>/ ,
// Markdown
'*' : /\*/ , // bold
'_' : /\_/ , // ital
'`' : /`/ , // mono
'afterBold' : /[^\*]/ ,
'afterItal' : /[^_]/ ,
'afterMono' : /[^`]/ ,
} ;
/ * *
* Internal helper class for parsing the markup tags for HTML and Markdown .
*
* NOTE : Sequences of tabs and spaces are reduced to single space .
* Scan usage of ` this.spacing ` within method
* /
class MarkupAccumulator {
/ * *
* Create an instance
*
* @ param { string } text text to parse for markup
* /
constructor ( text ) {
this . text = text ;
this . bold = false ;
this . ital = false ;
this . mono = false ;
this . spacing = false ;
this . position = 0 ;
this . buffer = "" ;
this . modStack = [ ] ;
this . blocks = [ ] ;
}
/ * *
* Return the mod label currently on the top of the stack
*
* @ returns { string } label of topmost mod
* @ private
* /
mod ( ) {
return ( this . modStack . length === 0 ) ? 'normal' : this . modStack [ 0 ] ;
}
/ * *
* Return the mod label currently active
*
* @ returns { string } label of active mod
* @ private
* /
modName ( ) {
if ( this . modStack . length === 0 )
return 'normal' ;
else if ( this . modStack [ 0 ] === 'mono' )
return 'mono' ;
else {
if ( this . bold && this . ital ) {
return 'boldital' ;
} else if ( this . bold ) {
return 'bold' ;
} else if ( this . ital ) {
return 'ital' ;
}
}
}
/ * *
* @ private
* /
emitBlock ( ) {
if ( this . spacing ) {
this . add ( " " ) ;
this . spacing = false ;
}
if ( this . buffer . length > 0 ) {
this . blocks . push ( { text : this . buffer , mod : this . modName ( ) } ) ;
this . buffer = "" ;
}
}
/ * *
* Output text to buffer
*
* @ param { string } text text to add
* @ private
* /
add ( text ) {
if ( text === " " ) {
this . spacing = true ;
}
if ( this . spacing ) {
this . buffer += " " ;
this . spacing = false ;
}
if ( text != " " ) {
this . buffer += text ;
}
}
/ * *
* Handle parsing of whitespace
*
* @ param { string } ch the character to check
* @ returns { boolean } true if the character was processed as whitespace , false otherwise
* /
parseWS ( ch ) {
if ( /[ \t]/ . test ( ch ) ) {
if ( ! this . mono ) {
this . spacing = true ;
} else {
this . add ( ch ) ;
}
return true ;
}
return false ;
}
/ * *
* @ param { string } tagName label for block type to set
* @ private
* /
setTag ( tagName ) {
this . emitBlock ( ) ;
this [ tagName ] = true ;
this . modStack . unshift ( tagName ) ;
}
/ * *
* @ param { string } tagName label for block type to unset
* @ private
* /
unsetTag ( tagName ) {
this . emitBlock ( ) ;
this [ tagName ] = false ;
this . modStack . shift ( ) ;
}
/ * *
* @ param { string } tagName label for block type we are currently processing
* @ param { string | RegExp } tag string to match in text
* @ returns { boolean } true if the tag was processed , false otherwise
* /
parseStartTag ( tagName , tag ) {
// Note: if 'mono' passed as tagName, there is a double check here. This is OK
if ( ! this . mono && ! this [ tagName ] && this . match ( tag ) ) {
this . setTag ( tagName ) ;
return true ;
}
return false ;
}
/ * *
* @ param { string | RegExp } tag
* @ param { number } [ advance = true ] if set , advance current position in text
* @ returns { boolean } true if match at given position , false otherwise
* @ private
* /
match ( tag , advance = true ) {
let [ regExp , length ] = this . prepareRegExp ( tag ) ;
let matched = regExp . test ( this . text . substr ( this . position , length ) ) ;
if ( matched && advance ) {
this . position += length - 1 ;
}
return matched ;
}
/ * *
* @ param { string } tagName label for block type we are currently processing
* @ param { string | RegExp } tag string to match in text
* @ param { RegExp } [ nextTag ] regular expression to match for characters * following * the current tag
* @ returns { boolean } true if the tag was processed , false otherwise
* /
parseEndTag ( tagName , tag , nextTag ) {
let checkTag = ( this . mod ( ) === tagName ) ;
if ( tagName === 'mono' ) { // special handling for 'mono'
checkTag = checkTag && this . mono ;
} else {
checkTag = checkTag && ! this . mono ;
}
if ( checkTag && this . match ( tag ) ) {
if ( nextTag !== undefined ) {
// Purpose of the following match is to prevent a direct unset/set of a given tag
// E.g. '*bold **still bold*' => '*bold still bold*'
if ( ( this . position === this . text . length - 1 ) || this . match ( nextTag , false ) ) {
this . unsetTag ( tagName ) ;
}
} else {
this . unsetTag ( tagName ) ;
}
return true ;
}
return false ;
}
/ * *
* @ param { string | RegExp } tag string to match in text
* @ param { value } value string to replace tag with , if found at current position
* @ returns { boolean } true if the tag was processed , false otherwise
* /
replace ( tag , value ) {
if ( this . match ( tag ) ) {
this . add ( value ) ;
this . position += length - 1 ;
return true ;
}
return false ;
}
/ * *
* Create a regular expression for the tag if it isn ' t already one .
*
* @ param { string | RegExp } tag string to match in text
* @ returns { [ RegExp , number ] } regular expression to use and length of input string to match
* @ private
* /
prepareRegExp ( tag ) {
let length ;
let regExp ;
if ( tag instanceof RegExp ) {
regExp = tag ;
length = 1 ; // ASSUMPTION: regexp only tests one character
} else {
// use prepared regexp if present
var prepared = tagPattern [ tag ] ;
if ( prepared !== undefined ) {
regExp = prepared ;
} else {
regExp = new RegExp ( tag ) ;
}
length = tag . length ;
}
return [ regExp , length ] ;
}
}
/ * *
* Helper class for Label which explodes the label text into lines and blocks within lines
@ -159,123 +427,43 @@ class LabelSplitter {
* @ returns { Array }
* /
splitHtmlBlocks ( text ) {
let blocks = [ ] ;
// TODO: consolidate following + methods/closures with splitMarkdownBlocks()
// NOTE: sequences of tabs and spaces are reduced to single space; scan usage of `this.spacing` within method
let s = {
bold : false ,
ital : false ,
mono : false ,
spacing : false ,
position : 0 ,
buffer : "" ,
modStack : [ ]
} ;
let s = new MarkupAccumulator ( text ) ;
s . mod = function ( ) {
return ( this . modStack . length === 0 ) ? 'normal' : this . modStack [ 0 ] ;
} ;
let parseEntities = ( ch ) => {
if ( /&/ . test ( ch ) ) {
let parsed = s . replace ( s . text , '<' , '<' )
|| s . replace ( s . text , '&' , '&' ) ;
s . modName = function ( ) {
if ( this . modStack . length === 0 )
return 'normal' ;
else if ( this . modStack [ 0 ] === 'mono' )
return 'mono' ;
else {
if ( s . bold && s . ital ) {
return 'boldital' ;
} else if ( s . bold ) {
return 'bold' ;
} else if ( s . ital ) {
return 'ital' ;
if ( ! parsed ) {
s . add ( "&" ) ;
}
}
} ;
s . emitBlock = function ( override = false ) { // eslint-disable-line no-unused-vars
if ( this . spacing ) {
this . add ( " " ) ;
this . spacing = false ;
}
if ( this . buffer . length > 0 ) {
blocks . push ( { text : this . buffer , mod : this . modName ( ) } ) ;
this . buffer = "" ;
return true ;
}
} ;
s . add = function ( text ) {
if ( text === " " ) {
s . spacing = true ;
}
if ( s . spacing ) {
this . buffer += " " ;
this . spacing = false ;
}
if ( text != " " ) {
this . buffer += text ;
}
return false ;
} ;
while ( s . position < text . length ) {
let ch = text . charAt ( s . position ) ;
if ( /[ \t]/ . test ( ch ) ) {
if ( ! s . mono ) {
s . spacing = true ;
} else {
s . add ( ch ) ;
}
} else if ( /</ . test ( ch ) ) {
if ( ! s . mono && ! s . bold && /<b>/ . test ( text . substr ( s . position , 3 ) ) ) {
s . emitBlock ( ) ;
s . bold = true ;
s . modStack . unshift ( "bold" ) ;
s . position += 2 ;
} else if ( ! s . mono && ! s . ital && /<i>/ . test ( text . substr ( s . position , 3 ) ) ) {
s . emitBlock ( ) ;
s . ital = true ;
s . modStack . unshift ( "ital" ) ;
s . position += 2 ;
} else if ( ! s . mono && /<code>/ . test ( text . substr ( s . position , 6 ) ) ) {
s . emitBlock ( ) ;
s . mono = true ;
s . modStack . unshift ( "mono" ) ;
s . position += 5 ;
} else if ( ! s . mono && ( s . mod ( ) === 'bold' ) && /<\/b>/ . test ( text . substr ( s . position , 4 ) ) ) {
s . emitBlock ( ) ;
s . bold = false ;
s . modStack . shift ( ) ;
s . position += 3 ;
} else if ( ! s . mono && ( s . mod ( ) === 'ital' ) && /<\/i>/ . test ( text . substr ( s . position , 4 ) ) ) {
s . emitBlock ( ) ;
s . ital = false ;
s . modStack . shift ( ) ;
s . position += 3 ;
} else if ( ( s . mod ( ) === 'mono' ) && /<\/code>/ . test ( text . substr ( s . position , 7 ) ) ) {
s . emitBlock ( ) ;
s . mono = false ;
s . modStack . shift ( ) ;
s . position += 6 ;
} else {
s . add ( ch ) ;
}
} else if ( /&/ . test ( ch ) ) {
if ( /</ . test ( text . substr ( s . position , 4 ) ) ) {
s . add ( "<" ) ;
s . position += 3 ;
} else if ( /&/ . test ( text . substr ( s . position , 5 ) ) ) {
s . add ( "&" ) ;
s . position += 4 ;
} else {
s . add ( "&" ) ;
}
} else {
while ( s . position < s . text . length ) {
let ch = s . text . charAt ( s . position ) ;
let parsed = s . parseWS ( ch )
|| ( /</ . test ( ch ) && (
s . parseStartTag ( 'bold' , '<b>' )
|| s . parseStartTag ( 'ital' , '<i>' )
|| s . parseStartTag ( 'mono' , '<code>' )
|| s . parseEndTag ( 'bold' , '</b>' )
|| s . parseEndTag ( 'ital' , '</i>' )
|| s . parseEndTag ( 'mono' , '</code>' ) ) )
|| parseEntities ( ch ) ;
if ( ! parsed ) {
s . add ( ch ) ;
}
s . position ++
}
s . emitBlock ( ) ;
return blocks ;
return s . blocks ;
}
@ -285,129 +473,49 @@ class LabelSplitter {
* @ returns { Array }
* /
splitMarkdownBlocks ( text ) {
let blocks = [ ] ;
// TODO: consolidate following + methods/closures with splitHtmlBlocks()
// NOTE: sequences of tabs and spaces are reduced to single space; scan usage of `this.spacing` within method
let s = {
bold : false ,
ital : false ,
mono : false ,
beginable : true ,
spacing : false ,
position : 0 ,
buffer : "" ,
modStack : [ ]
} ;
s . mod = function ( ) {
return ( this . modStack . length === 0 ) ? 'normal' : this . modStack [ 0 ] ;
} ;
s . modName = function ( ) {
if ( this . modStack . length === 0 )
return 'normal' ;
else if ( this . modStack [ 0 ] === 'mono' )
return 'mono' ;
else {
if ( s . bold && s . ital ) {
return 'boldital' ;
} else if ( s . bold ) {
return 'bold' ;
} else if ( s . ital ) {
return 'ital' ;
}
}
} ;
s . emitBlock = function ( override = false ) { // eslint-disable-line no-unused-vars
if ( this . spacing ) {
this . add ( " " ) ;
this . spacing = false ;
}
if ( this . buffer . length > 0 ) {
blocks . push ( { text : this . buffer , mod : this . modName ( ) } ) ;
this . buffer = "" ;
}
} ;
let s = new MarkupAccumulator ( text ) ;
let beginable = true ;
s . add = function ( text ) {
if ( text === " " ) {
s . spacing = true ;
}
if ( s . spacing ) {
this . buffer += " " ;
this . spacing = false ;
}
if ( text != " " ) {
this . buffer += text ;
}
} ;
while ( s . position < text . length ) {
let ch = text . charAt ( s . position ) ;
if ( /[ \t]/ . test ( ch ) ) {
if ( ! s . mono ) {
s . spacing = true ;
} else {
s . add ( ch ) ;
}
s . beginable = true
} else if ( /\\/ . test ( ch ) ) {
if ( s . position < text . length + 1 ) {
let parseOverride = ( ch ) => {
if ( /\\/ . test ( ch ) ) {
if ( s . position < this . text . length + 1 ) {
s . position ++ ;
ch = text . charAt ( s . position ) ;
ch = this . text . charAt ( s . position ) ;
if ( / \t/ . test ( ch ) ) {
s . spacing = true ;
} else {
s . add ( ch ) ;
s . beginable = false ;
beginable = false ;
}
}
} else if ( ! s . mono && ! s . bold && ( s . beginable || s . spacing ) && /\*/ . test ( ch ) ) {
s . emitBlock ( ) ;
s . bold = true ;
s . modStack . unshift ( "bold" ) ;
} else if ( ! s . mono && ! s . ital && ( s . beginable || s . spacing ) && /\_/ . test ( ch ) ) {
s . emitBlock ( ) ;
s . ital = true ;
s . modStack . unshift ( "ital" ) ;
} else if ( ! s . mono && ( s . beginable || s . spacing ) && /`/ . test ( ch ) ) {
s . emitBlock ( ) ;
s . mono = true ;
s . modStack . unshift ( "mono" ) ;
} else if ( ! s . mono && ( s . mod ( ) === "bold" ) && /\*/ . test ( ch ) ) {
if ( ( s . position === text . length - 1 ) || /[.,_` \t\n]/ . test ( text . charAt ( s . position + 1 ) ) ) {
s . emitBlock ( ) ;
s . bold = false ;
s . modStack . shift ( ) ;
} else {
s . add ( ch ) ;
}
} else if ( ! s . mono && ( s . mod ( ) === "ital" ) && /\_/ . test ( ch ) ) {
if ( ( s . position === text . length - 1 ) || /[.,*` \t\n]/ . test ( text . charAt ( s . position + 1 ) ) ) {
s . emitBlock ( ) ;
s . ital = false ;
s . modStack . shift ( ) ;
} else {
s . add ( ch ) ;
}
} else if ( s . mono && ( s . mod ( ) === "mono" ) && /`/ . test ( ch ) ) {
if ( ( s . position === text . length - 1 ) || ( /[.,*_ \t\n]/ . test ( text . charAt ( s . position + 1 ) ) ) ) {
s . emitBlock ( ) ;
s . mono = false ;
s . modStack . shift ( ) ;
} else {
s . add ( ch ) ;
}
} else {
return true
}
return false ;
}
while ( s . position < s . text . length ) {
let ch = s . text . charAt ( s . position ) ;
let parsed = s . parseWS ( ch )
|| parseOverride ( ch )
|| ( ( beginable || s . spacing ) && (
s . parseStartTag ( 'bold' , '*' )
|| s . parseStartTag ( 'ital' , '_' )
|| s . parseStartTag ( 'mono' , '`' ) ) )
|| s . parseEndTag ( 'bold' , '*' , 'afterBold' )
|| s . parseEndTag ( 'ital' , '_' , 'afterItal' )
|| s . parseEndTag ( 'mono' , '`' , 'afterMono' ) ;
if ( ! parsed ) {
s . add ( ch ) ;
s . beginable = false ;
beginable = false ;
}
s . position ++
}
s . emitBlock ( ) ;
return blocks ;
return s . blocks ;
}