import MarkdownIt from "markdown-it" ; import Token from "markdown-it/lib/token" ; import StateCore from "markdown-it/lib/rules_core/state_core" ; const STRING_CHAR_CODE = 0x20 ; /**
 * Style attribute names
 */ export enum Attributes { backgroundColor = "backgroundColor" , color = "color" , textDecoration = "textDecoration" } /**
 * Name of inline tags.
 */ enum InlineTags { sub = "sub" , sup = "sup" } enum StartTags { cite = "cite" , h1 = "h1" , h2 = "h2" , h3 = "h3" , h4 = "h4" , h5 = "h5" , h6 = "h6" } type RuleNames = Attributes | InlineTags ; /**
 * Regex rule for detecting inline tags.
 */ const tagRule = /{![a-zA-Z][a-zA-Z0-9]*\} / ; const tagRules = { "{!cite} " : StartTags . cite , "{!h1} " : StartTags . h1 , "{!h2} " : StartTags . h2 , "{!h3} " : StartTags . h3 , "{!h4} " : StartTags . h4 , "{!h5} " : StartTags . h5 , "{!h6} " : StartTags . h6 , } ; /**
 * Array of tuples of rule names and regex.
 */ const rules = [ [ Attributes . color , /\{color:(([a-zA-Z]*)|#([0-9a-fA-F]*))\}/ ] , [ Attributes . textDecoration , /\{text-decoration:((underline( line-through){0,1})|(line-through( underline){0,1}))\}/ , ] , [ Attributes . backgroundColor , /\{background-color:(([a-zA-Z]*)|#([0-9a-fA-F]*))\}/ , ] , [ InlineTags . sup , /{!sup}/ ] , [ InlineTags . sub , /{!sub}/ ] , ] ; param md param src param start function scanDelims ( md , src , start ) { let pos = start ; const max = src . length ; let leftFlanking = true ; let rightFlanking = true ; const lastChar = start > 0 ? src . charCodeAt ( start - 1 ) : STRING_CHAR_CODE ; while ( pos < max && src [ pos ] !== "}" ) { pos ++ ; } pos ++ ; const count = pos - start ; const nextChar = pos < max ? src . charCodeAt ( pos ) : STRING_CHAR_CODE ; const isLastPunctChar = md . utils . isMdAsciiPunct ( lastChar ) || md . utils . isPunctChar ( String . fromCharCode ( lastChar ) ) ; const isNextPunctChar = md . utils . isMdAsciiPunct ( nextChar ) || md . utils . isPunctChar ( String . fromCharCode ( nextChar ) ) ; const isLastWhiteSpace = md . utils . isWhiteSpace ( lastChar ) ; const isNextWhiteSpace = md . utils . isWhiteSpace ( nextChar ) ; if ( isNextWhiteSpace ) { leftFlanking = false ; } else if ( isNextPunctChar ) { if ( ! ( isLastWhiteSpace || isLastPunctChar ) ) { leftFlanking = false ; } } if ( isLastWhiteSpace ) { rightFlanking = false ; } else if ( isLastPunctChar ) { if ( ! ( isNextWhiteSpace || isNextPunctChar ) ) { rightFlanking = false ; } } return { canClose : rightFlanking , canOpen : leftFlanking , length : count , } ; } /**
 * Contains a stack of the delimiting tokens.
 */ let delimStack = [ ] ; /**
 * An array of sets which contain equivalent strings inside the markdown.
 */ const equivalentValues = [ new Set ( [ "{text-decoration:underline line-through}" , "{text-decoration:line-through underline}" , ] ) , ] ; /**
 * Compares the two values to see if they're identical strings, or if they have
 * equivalent values, as specified in `equivalentValues`.
 */ function compareDelimStackItems ( val1 , val2 ) { if ( val1 . toLowerCase ( ) === val2 . toLowerCase ( ) ) { return true ; } else { for ( const valueSet of equivalentValues ) { if ( valueSet . has ( val1 ) && valueSet . has ( val2 ) ) { return true ; } } } return false ; } function handleMatch ( state , { ruleName , content , startIndex } , currentToken , tokens ) { const endIndex = startIndex + content . length ; const pos = state . src . length - currentToken . content . length + startIndex ; const tokenBefore = { ... new Token ( currentToken . type , currentToken . tag , currentToken . nesting ) , attrs : currentToken . attrs , block : currentToken . block , children : currentToken . children , content : currentToken . content . slice ( 0 , startIndex ) , hidden : currentToken . hidden , info : currentToken . info , level : currentToken . level , map : currentToken . map , markup : currentToken . markup , meta : currentToken . meta , nesting : currentToken . nesting , tag : currentToken . tag , type : currentToken . type , } ; const tokenAfter = { ... new Token ( currentToken . type , currentToken . tag , currentToken . nesting ) , attrs : currentToken . attrs , block : currentToken . block , children : currentToken . children , content : currentToken . content . slice ( endIndex ) , hidden : currentToken . hidden , info : currentToken . info , level : currentToken . level , map : currentToken . map , markup : currentToken . markup , meta : currentToken . meta , nesting : currentToken . nesting , tag : currentToken . tag , type : currentToken . type , } ; const result = scanDelims ( state . md , state . src , pos ) ; const matchedToken = new Token ( ruleName , ruleName in InlineTags ? ruleName : "span" , result . canClose ? - 1 : 1 ) ; matchedToken . markup = currentToken . content . slice ( startIndex , endIndex ) ; if ( result . canClose && delimStack [ delimStack . length - 1 ] && compareDelimStackItems ( delimStack [ delimStack . length - 1 ] . markup , content ) ) { delimStack . pop ( ) ; matchedToken . nesting = - 1 ; } else { delimStack . push ( matchedToken ) ; matchedToken . nesting = 1 ; } if ( ! ( ruleName in InlineTags ) ) { matchedToken . attrPush ( [ ruleName , content . split ( ":" ) [ 1 ] . slice ( 0 , - 1 ) ] ) ; } tokens . pop ( ) ; tokens = tokens . concat ( [ tokenBefore , matchedToken , tokenAfter ] . reduce ( ( acc , curr ) : Token [ ] => { if ( curr . content || ( curr . attrs && curr . attrGet ( ruleName ) ) || curr . type in InlineTags ) { acc . push ( curr ) ; } return acc ; } , [ ] as Token [ ] ) ) ; return { pos , tokens } ; } interface Match { content : string ; ruleName : RuleNames ; startIndex : number ; } function compare ( a , b ) { if ( a . startIndex < b . startIndex ) { return - 1 ; } if ( a . startIndex > b . startIndex ) { return 1 ; } return 0 ; } function getMatches ( content ) { const matches = [ ] ; for ( const [ ruleName , rule ] of rules ) { const match = rule . exec ( content ) ; if ( match ) { matches . push ( { content : match [ 0 ] , ruleName , startIndex : match . index , } ) ; } } matches . sort ( compare ) ; return matches ; } param content export function hasStartTag ( content ) { const match = tagRule . exec ( content ) ; if ( match && match . index === 0 && tagRules . hasOwnProperty ( match [ 0 ] ) ) { return { length : match [ 0 ] . length , tag : tagRules [ match [ 0 ] ] , } ; } return null ; } param state param token function customSyntax ( state , token ) { let tokens = [ token ] ; let currentToken = tokens [ tokens . length - 1 ] ; const hasStart = hasStartTag ( currentToken . content ) ; if ( hasStart ) { currentToken . content = currentToken . content . slice ( hasStart . length ) ; } while ( true ) { currentToken = tokens [ tokens . length - 1 ] ; const matches = getMatches ( currentToken . content ) ; if ( matches . length === 0 ) { return tokens ; } ( { tokens } = handleMatch ( state , matches [ 0 ] , currentToken , tokens ) ) ; } } param inlineTokens function closeTags ( inlineTokens ) { delimStack . forEach ( ( ) : void => { const newToken = new Token ( "unfinishedEnd" , "span" , - 1 ) ; newToken . children = [ ] ; inlineTokens . push ( newToken ) ; } ) ; } /**
 * Changes the open tags which have no corresponding closing tags to be normal
 * text tags.
 */ function revertUnclosedTags ( ) { delimStack . forEach ( ( token ) : void => { token . type = "text" ; token . content = token . markup ; token . markup = "" ; token . tag = "" ; token . attrs = [ ] ; token . nesting = 0 ; } ) ; } function rule ( state , closeOpenTags = false ) { state . tokens . forEach ( ( token ) : void => { if ( token . type === "inline" ) { let inlineTokens = [ ] ; token . children . forEach ( ( currentToken ) : void => { inlineTokens = inlineTokens . concat ( customSyntax ( state , currentToken ) ) ; } ) ; if ( closeOpenTags ) { closeTags ( inlineTokens ) ; } else { revertUnclosedTags ( ) ; } delimStack = [ ] ; token . children = inlineTokens ; } } ) ; } export function autoCloseRule ( state ) { rule ( state , true ) ; } export function nonAutoCloseRule ( state ) { rule ( state , false ) ; }