commonMain.org.intellij.markdown.parser.constraints.CommonMarkdownConstraints.kt Maven / Gradle / Ivy
package org.intellij.markdown.parser.constraints
import org.intellij.markdown.lexer.Compat.assert
import org.intellij.markdown.parser.LookaheadText
import org.intellij.markdown.parser.markerblocks.providers.HorizontalRuleProvider
import kotlin.math.min
open class CommonMarkdownConstraints protected constructor(private val indents: IntArray,
override val types: CharArray,
override val isExplicit: BooleanArray,
override val charsEaten: Int) : MarkdownConstraints {
protected open val base: CommonMarkdownConstraints
get() = BASE
protected open fun createNewConstraints(indents: IntArray,
types: CharArray,
isExplicit: BooleanArray,
charsEaten: Int): CommonMarkdownConstraints {
return CommonMarkdownConstraints(indents, types, isExplicit, charsEaten)
}
override val indent: Int get() = indents.lastOrNull() ?: 0
override fun startsWith(other: MarkdownConstraints): Boolean {
if (other !is CommonMarkdownConstraints) return false
val n = indents.size
val m = other.indents.size
if (n < m) {
return false
}
return (0 until m).none { types[it] != other.types[it] }
}
override fun containsListMarkers(upToIndex: Int): Boolean {
return (0 until upToIndex).any { types[it] != BQ_CHAR && isExplicit[it] }
}
override fun addModifierIfNeeded(pos: LookaheadText.Position?): CommonMarkdownConstraints? {
if (pos == null || pos.offsetInCurrentLine == -1)
return null
if (HorizontalRuleProvider.isHorizontalRule(pos.currentLine, pos.offsetInCurrentLine)) {
return null
}
return tryAddListItem(pos) ?: tryAddBlockQuote(pos)
}
override fun applyToNextLine(pos: LookaheadText.Position?): CommonMarkdownConstraints {
if (pos == null) {
return base
}
assert(pos.offsetInCurrentLine == -1) { "given $pos" }
val line = pos.currentLine
val prevN = indents.size
var indexPrev = 0
val getBlockQuoteIndent = { startOffset: Int ->
var offset = startOffset
var blockQuoteIndent = 0
// '\t' can be omitted here since it'll add at least 4 indent
while (blockQuoteIndent < 3 && offset < line.length && line[offset] == ' ') {
blockQuoteIndent++
offset++
}
if (offset < line.length && line[offset] == BQ_CHAR) {
blockQuoteIndent + 1
} else {
null
}
}
val fillMaybeBlockquoteAndListIndents = fun(constraints: CommonMarkdownConstraints): CommonMarkdownConstraints {
if (indexPrev >= prevN) {
return constraints
}
var offset = constraints.getCharsEaten(line)
var totalSpaces = 0
var spacesSeen = 0
val hasKMoreSpaces = { k: Int ->
val oldSpacesSeen = spacesSeen
val oldOffset = offset
afterSpaces@
while (spacesSeen < k && offset < line.length) {
val deltaSpaces = when (line[offset]) {
' ' -> 1
'\t' -> 4 - totalSpaces % 4
else -> break@afterSpaces
}
spacesSeen += deltaSpaces
totalSpaces += deltaSpaces
offset++
}
if (offset == line.length) {
spacesSeen = Int.MAX_VALUE
}
if (k <= spacesSeen) {
spacesSeen -= k
true
} else {
offset = oldOffset
spacesSeen = oldSpacesSeen
false
}
}
val bqIndent: Int?
if (types[indexPrev] == BQ_CHAR) {
bqIndent = getBlockQuoteIndent(offset)
?: return constraints
offset += bqIndent
indexPrev++
} else {
bqIndent = null
}
val oldIndexPrev = indexPrev
while (indexPrev < prevN && types[indexPrev] != BQ_CHAR) {
val deltaIndent = indents[indexPrev] -
if (indexPrev == 0)
0
else
indents[indexPrev - 1]
if (!hasKMoreSpaces(deltaIndent)) {
break
}
indexPrev++
}
var result = constraints
if (bqIndent != null) {
val bonusForTheBlockquote = if (hasKMoreSpaces(1)) 1 else 0
result = create(result, bqIndent + bonusForTheBlockquote, BQ_CHAR, true, offset)
}
for (index in oldIndexPrev until indexPrev) {
val deltaIndent = indents[index] -
if (index == 0)
0
else
indents[index - 1]
result = create(result, deltaIndent, types[index], false, offset)
}
return result
}
var result = base
while (true) {
val nextConstraints = fillMaybeBlockquoteAndListIndents(result)
if (nextConstraints == result) {
return result
}
result = nextConstraints
}
}
protected open fun fetchListMarker(pos: LookaheadText.Position): ListMarkerInfo? {
val c = pos.char
if (c == '*' || c == '-' || c == '+') {
return ListMarkerInfo(1, c, 1)
}
val line = pos.currentLine
var offset = pos.offsetInCurrentLine
while (offset < line.length && line[offset] in '0'..'9') {
offset++
}
return if (offset > pos.offsetInCurrentLine
&& offset - pos.offsetInCurrentLine <= 9
&& offset < line.length
&& (line[offset] == '.' || line[offset] == ')')) {
ListMarkerInfo(offset + 1 - pos.offsetInCurrentLine,
line[offset],
offset + 1 - pos.offsetInCurrentLine)
} else {
null
}
}
protected data class ListMarkerInfo(val markerLength: Int, val markerType: Char, val markerIndent: Int)
private fun tryAddListItem(pos: LookaheadText.Position): CommonMarkdownConstraints? {
val line = pos.currentLine
var offset = pos.offsetInCurrentLine
var spacesBefore = if (offset > 0 && line[offset - 1] == '\t')
(4 - indent % 4) % 4
else
0
// '\t' can be omitted here since it'll add at least 4 indent
while (offset < line.length && line[offset] == ' ' && spacesBefore < 3) {
spacesBefore++
offset++
}
if (offset == line.length)
return null
val markerInfo = fetchListMarker(pos.nextPosition(offset - pos.offsetInCurrentLine)!!)
?: return null
offset += markerInfo.markerLength
var spacesAfter = 0
val markerEndOffset = offset
afterSpaces@
while (offset < line.length) {
when (line[offset]) {
' ' -> spacesAfter++
'\t' -> spacesAfter += 4 - spacesAfter % 4
else -> break@afterSpaces
}
offset++
}
// By the classification http://spec.commonmark.org/0.20/#list-items
// 1. Basic case
if (spacesAfter in 1..4 && offset < line.length) {
return create(this, spacesBefore + markerInfo.markerIndent + spacesAfter, markerInfo.markerType, true, offset)
}
if (spacesAfter >= 5 && offset < line.length // 2. Starts with an indented code
|| offset == line.length) {
// 3. Starts with an empty string
return create(this, spacesBefore + markerInfo.markerIndent + 1, markerInfo.markerType, true,
min(offset, markerEndOffset + 1))
}
return null
}
private fun tryAddBlockQuote(pos: LookaheadText.Position): CommonMarkdownConstraints? {
val line = pos.currentLine
var offset = pos.offsetInCurrentLine
var spacesBefore = 0
// '\t' can be omitted here since it'll add at least 4 indent
while (offset < line.length && line[offset] == ' ' && spacesBefore < 3) {
spacesBefore++
offset++
}
if (offset == line.length || line[offset] != BQ_CHAR) {
return null
}
offset++
var spacesAfter = 0
if (offset >= line.length || line[offset] == ' ' || line[offset] == '\t') {
spacesAfter = 1
if (offset < line.length) {
offset++
}
}
return create(this, spacesBefore + 1 + spacesAfter, BQ_CHAR, true, offset)
}
override fun toString(): String {
return "MdConstraints: " + types.concatToString() + "(" + indent + ")"
}
companion object {
val BASE: CommonMarkdownConstraints = CommonMarkdownConstraints(IntArray(0), CharArray(0), BooleanArray(0), 0)
const val BQ_CHAR: Char = '>'
private fun create(parent: CommonMarkdownConstraints,
newIndentDelta: Int,
newType: Char,
newExplicit: Boolean,
newOffset: Int): CommonMarkdownConstraints {
val n = parent.indents.size
val indents = parent.indents.copyOf(n + 1)
val types = parent.types.copyOf(n + 1)
val isExplicit = parent.isExplicit.copyOf(n + 1)
indents[n] = parent.indent + newIndentDelta
types[n] = newType
isExplicit[n] = newExplicit
return parent.createNewConstraints(indents, types, isExplicit, newOffset)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy