Coverage Summary for Class: AlertBlockParser (io.heapy.kotmark.ext.gfm.alerts.internal)

Class Method, % Branch, % Line, % Instruction, %
AlertBlockParser 100% (7/7) 92.9% (13/14) 100% (25/25) 100% (107/107)
AlertBlockParser$Companion
AlertBlockParser$Factory 100% (4/4) 82% (41/50) 98.2% (54/55) 99.2% (235/237)
Total 100% (11/11) 84.4% (54/64) 98.8% (79/80) 99.4% (342/344)


 package io.heapy.kotmark.ext.gfm.alerts.internal
 
 import io.heapy.kotmark.ext.gfm.alerts.Alert
 import io.heapy.kotmark.ext.gfm.alerts.AlertTitle
 import io.heapy.kotmark.node.Block
 import io.heapy.kotmark.node.BlockQuote
 import io.heapy.kotmark.node.Document
 import io.heapy.kotmark.parser.InlineParser
 import io.heapy.kotmark.parser.SourceLine
 import io.heapy.kotmark.parser.SourceLines
 import io.heapy.kotmark.parser.block.AbstractBlockParser
 import io.heapy.kotmark.parser.block.AbstractBlockParserFactory
 import io.heapy.kotmark.parser.block.BlockContinue
 import io.heapy.kotmark.parser.block.BlockStart
 import io.heapy.kotmark.parser.block.MatchedBlockParser
 import io.heapy.kotmark.parser.block.ParserState
 import io.heapy.kotmark.text.Characters
 
 class AlertBlockParser private constructor(
     type: String,
     private val titleLine: SourceLine?,
 ) : AbstractBlockParser() {
     private val alert = Alert(type)
 
     override val block: Block
         get() = alert
 
     override fun isContainer(): Boolean = true
 
     override fun canContain(childBlock: Block): Boolean = true
 
     override fun tryContinue(parserState: ParserState): BlockContinue? {
         val line = parserState.line.content
         val nextNonSpace = parserState.nextNonSpaceIndex
         if (
             parserState.indent >= 4 ||
             nextNonSpace >= line.length ||
             line[nextNonSpace] != '>'
         ) {
             return null
         }
 
         var newColumn = parserState.column + parserState.indent + 1
         if (Characters.isSpaceOrTab(line, nextNonSpace + 1)) {
             newColumn++
         }
         return BlockContinue.AtColumn(newColumn)
     }
 
     override fun parseInlines(inlineParser: InlineParser) {
         val line = titleLine ?: return
         if (line.content.isEmpty()) {
             return
         }
         val title = AlertTitle()
         inlineParser.parse(SourceLines.of(line), title)
         line.sourceSpan?.let { title.setSourceSpans(listOf(it)) }
         alert.prependChild(title)
     }
 
     class Factory(
         private val allowedTypes: Set<String>,
         private val customTitlesAllowed: Boolean,
         private val nestedAlertsAllowed: Boolean,
     ) : AbstractBlockParserFactory() {
         override fun tryStart(
             state: ParserState,
             matchedBlockParser: MatchedBlockParser,
         ): BlockStart? {
             if (state.indent >= 4) {
                 return BlockStart.none()
             }
 
             val matched = matchedBlockParser.matchedBlockParser.block
             if (!nestedAlertsAllowed && !isAtRoot(matched)) {
                 return BlockStart.none()
             }
 
             val line = state.line.content
             val nextNonSpace = state.nextNonSpaceIndex
             if (nextNonSpace < line.length && line[nextNonSpace] == '>') {
                 return tryStartFresh(line, nextNonSpace, state)
             }
             if (matched is BlockQuote && matched.firstChild == null) {
                 return tryStartFresh(line, nextNonSpace, state)
             }
             return BlockStart.none()
         }
 
         private fun tryStartFresh(
             line: CharSequence,
             nextNonSpace: Int,
             state: ParserState,
         ): BlockStart? {
             val afterGt =
                 if (nextNonSpace < line.length && line[nextNonSpace] == '>') {
                     var index = nextNonSpace + 1
                     if (Characters.isSpaceOrTab(line, index)) {
                         index++
                     }
                     index
                 } else {
                     state.index
                 }
 
             val pattern =
                 if (customTitlesAllowed) ALERT_PATTERN_CUSTOM_TITLE else ALERT_PATTERN_NO_CUSTOM_TITLE
             val match = pattern.matchEntire(line.subSequence(afterGt, line.length))
                 ?: return BlockStart.none()
             val type = match.groupValues[1].uppercase()
             if (type !in allowedTypes) {
                 return BlockStart.none()
             }
 
             val titleLine =
                 if (customTitlesAllowed) {
                     var absoluteStart = afterGt + match.groupValues[1].length + 3
                     var absoluteEnd = afterGt + match.value.length
                     while (
                         absoluteStart < absoluteEnd &&
                         Characters.isSpaceOrTab(state.line.content, absoluteStart)
                     ) {
                         absoluteStart++
                     }
                     while (
                         absoluteEnd > absoluteStart &&
                         Characters.isSpaceOrTab(state.line.content, absoluteEnd - 1)
                     ) {
                         absoluteEnd--
                     }
                     state.line.substring(absoluteStart, absoluteEnd)
                 } else {
                     null
                 }
 
             var start: BlockStart =
                 BlockStart.of(AlertBlockParser(type, titleLine)).atIndex(line.length)
             val active = state.activeBlockParser.block
             if (
                 active is BlockQuote &&
                 active.firstChild == null &&
                 (nextNonSpace >= line.length || line[nextNonSpace] != '>')
             ) {
                 start = start.replaceActiveBlockParser()
             }
             return start
         }
 
         private fun isAtRoot(matched: Block): Boolean =
             matched is Document ||
                 (
                     matched is BlockQuote &&
                         matched.firstChild == null &&
                         matched.parent is Document
                 )
     }
 
     companion object {
         private val ALERT_PATTERN_NO_CUSTOM_TITLE = Regex("""^\[!([a-zA-Z]+)\]\s*$""")
         private val ALERT_PATTERN_CUSTOM_TITLE = Regex("""^\[!([a-zA-Z]+)\](.*)$""")
     }
 }