Coverage Summary for Class: TableBlockParser (io.heapy.kotmark.ext.gfm.tables.internal)

Class Method, % Branch, % Line, % Instruction, %
TableBlockParser 100% (7/7) 96.9% (31/32) 100% (64/64) 99.5% (369/371)
TableBlockParser$Companion 100% (2/2) 86.3% (44/51) 96% (72/75) 97.4% (259/266)
TableBlockParser$Factory 100% (2/2) 90% (9/10) 100% (16/16) 100% (88/88)
TableBlockParser$TableCellInfo 100% (1/1) 100% (3/3) 100% (12/12)
Total 100% (12/12) 90.3% (84/93) 98.1% (155/158) 98.8% (728/737)


 package io.heapy.kotmark.ext.gfm.tables.internal
 
 import io.heapy.kotmark.ext.gfm.tables.TableBlock
 import io.heapy.kotmark.ext.gfm.tables.TableBody
 import io.heapy.kotmark.ext.gfm.tables.TableCell
 import io.heapy.kotmark.ext.gfm.tables.TableHead
 import io.heapy.kotmark.ext.gfm.tables.TableRow
 import io.heapy.kotmark.node.Block
 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 TableBlockParser private constructor(
     private val columns: List<TableCellInfo>,
     headerLine: SourceLine,
     private val maxCells: Int?,
 ) : AbstractBlockParser() {
     private val table = TableBlock()
     private val rowLines = mutableListOf(headerLine)
     private var cells = 0
     private var lazyContinuationLines = true
 
     override fun canHaveLazyContinuationLines(): Boolean = lazyContinuationLines
 
     override val block: Block
         get() = table
 
     override fun tryContinue(parserState: ParserState): BlockContinue? {
         val content = parserState.line.content
         val pipe = Characters.find('|', content, parserState.nextNonSpaceIndex)
         if (pipe == -1) {
             return null
         }
         if (
             pipe == parserState.nextNonSpaceIndex &&
             Characters.skipSpaceTab(content, pipe + 1, content.length) == content.length
         ) {
             lazyContinuationLines = false
             return null
         }
         return BlockContinue.AtIndex(parserState.index)
     }
 
     override fun addLine(line: SourceLine) {
         rowLines.add(line)
     }
 
     override fun parseInlines(inlineParser: InlineParser) {
         val sourceSpans = table.getSourceSpans()
         val headerSourceSpan = sourceSpans.firstOrNull()
         val head = TableHead()
         headerSourceSpan?.let(head::addSourceSpan)
         table.appendChild(head)
 
         val headerRow = TableRow()
         headerRow.setSourceSpans(head.getSourceSpans())
         head.appendChild(headerRow)
         val headerCells = split(rowLines[0])
         val headerColumnCount = headerCells.size
         for ([index, cell] in headerCells.withIndex()) {
             headerRow.appendChild(
                 parseCell(cell, index, inlineParser).apply { isHeader = true },
             )
         }
 
         var body: TableBody? = null
         for (rowIndex in 2 until rowLines.size) {
             val rowLine = rowLines[rowIndex]
             val sourceSpan = sourceSpans.getOrNull(rowIndex)
             val cells = split(rowLine)
             val row = TableRow()
             sourceSpan?.let(row::addSourceSpan)
             for (index in 0 until headerColumnCount) {
                 val cell = cells.getOrNull(index) ?: SourceLine.of("", null)
                 row.appendChild(parseCell(cell, index, inlineParser))
             }
             val tableBody =
                 body ?: TableBody().also {
                     body = it
                     table.appendChild(it)
                 }
             tableBody.appendChild(row)
             sourceSpan?.let(tableBody::addSourceSpan)
         }
     }
 
     private fun parseCell(
         cell: SourceLine,
         column: Int,
         inlineParser: InlineParser,
     ): TableCell {
         if (maxCells != null) {
             require(cells < maxCells) {
                 "Aborting parsing because maximum number of cells reached (maxCells = $maxCells)"
             }
             cells++
         }
         val tableCell = TableCell()
         cell.sourceSpan?.let(tableCell::addSourceSpan)
         columns.getOrNull(column)?.let { info ->
             tableCell.alignment = info.alignment
             tableCell.width = info.width
         }
         val content = cell.content
         val start = Characters.skipSpaceTab(content, 0, content.length)
         val end = Characters.skipSpaceTabBackwards(content, content.length - 1, start)
         inlineParser.parse(SourceLines.of(cell.substring(start, end + 1)), tableCell)
         return tableCell
     }
 
     class Factory(private val maxCells: Int?) : AbstractBlockParserFactory() {
         override fun tryStart(
             state: ParserState,
             matchedBlockParser: MatchedBlockParser,
         ): BlockStart? {
             val paragraphLines = matchedBlockParser.paragraphLines.lines
             if (
                 paragraphLines.isNotEmpty() &&
                 Characters.find('|', paragraphLines.last().content, 0) != -1
             ) {
                 val line = state.line
                 val separatorLine = line.substring(state.index, line.content.length)
                 val columns = parseSeparator(separatorLine.content)
                 if (!columns.isNullOrEmpty()) {
                     val paragraph = paragraphLines.last()
                     val headerCells = split(paragraph)
                     if (columns.size >= headerCells.size) {
                         return BlockStart.of(TableBlockParser(columns, paragraph, maxCells))
                             .atIndex(state.index)
                             .replaceParagraphLines(1)
                     }
                 }
             }
             return BlockStart.none()
         }
     }
 
     private data class TableCellInfo(
         val alignment: TableCell.Alignment?,
         val width: Int,
     )
 
     companion object {
         private fun split(line: SourceLine): List<SourceLine> {
             val row = line.content
             val nonSpace = Characters.skipSpaceTab(row, 0, row.length)
             var cellStart = nonSpace
             var cellEnd = row.length
             if (nonSpace < row.length && row[nonSpace] == '|') {
                 cellStart = nonSpace + 1
                 val nonSpaceEnd =
                     Characters.skipSpaceTabBackwards(row, row.length - 1, cellStart)
                 cellEnd = nonSpaceEnd + 1
             }
             val cells = mutableListOf<SourceLine>()
             val content = StringBuilder()
             var index = cellStart
             while (index < cellEnd) {
                 when (row[index]) {
                     '\\' ->
                         if (index + 1 < cellEnd && row[index + 1] == '|') {
                             content.append('|')
                             index++
                         } else {
                             content.append('\\')
                         }
                     '|' -> {
                         cells.add(
                             SourceLine.of(
                                 content.toString(),
                                 line.substring(cellStart, index).sourceSpan,
                             ),
                         )
                         content.setLength(0)
                         cellStart = index + 1
                     }
                     else -> content.append(row[index])
                 }
                 index++
             }
             if (content.isNotEmpty()) {
                 cells.add(
                     SourceLine.of(
                         content.toString(),
                         line.substring(cellStart, line.content.length).sourceSpan,
                     ),
                 )
             }
             return cells
         }
 
         private fun parseSeparator(separator: CharSequence): List<TableCellInfo>? {
             val columns = mutableListOf<TableCellInfo>()
             var pipes = 0
             var valid = false
             var index = 0
             var width = 0
             while (index < separator.length) {
                 when (val character = separator[index]) {
                     '|' -> {
                         index++
                         pipes++
                         if (pipes > 1) {
                             return null
                         }
                         valid = true
                     }
                     '-', ':' -> {
                         if (pipes == 0 && columns.isNotEmpty()) {
                             return null
                         }
                         var left = false
                         var right = false
                         if (character == ':') {
                             left = true
                             index++
                             width++
                         }
                         var haveDash = false
                         while (index < separator.length && separator[index] == '-') {
                             index++
                             width++
                             haveDash = true
                         }
                         if (!haveDash) {
                             return null
                         }
                         if (index < separator.length && separator[index] == ':') {
                             right = true
                             index++
                             width++
                         }
                         val alignment =
                             when {
                                 left && right -> TableCell.Alignment.CENTER
                                 left -> TableCell.Alignment.LEFT
                                 right -> TableCell.Alignment.RIGHT
                                 else -> null
                             }
                         columns.add(TableCellInfo(alignment, width))
                         width = 0
                         pipes = 0
                     }
                     ' ', '\t' -> index++
                     else -> return null
                 }
             }
             return if (valid) columns else null
         }
     }
 }