All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.exoquery.pprint.Renderer.kt Maven / Gradle / Ivy

package io.exoquery.pprint

import io.exoquery.fansi.Attrs
import io.exoquery.fansi.Str

//fun  Sequence.hasNext(): Boolean = this.iterator().hasNext()
//fun  Sequence.next(): T = this.iterator().next()

/**
 * Basically like mkString, but for nested iterators. Used whenever
 * you want to put stuff "in between" the elements of the larger
 * iterator
 *
 * Kotlin Note: In general I have chosen to represent things with Iterators but I need to use
 * .asSequence() when I want to perform map, flatMap, filter etc... operators on the interator
 * because Kotlin iterators don't support these operators. The other option would be to make everything
 * a Sequence instead of an iterator but the problem with that is that Sequence does not support a
 * the .next() and .hasNext() operators. Now we could go down to the Sequence.iterator() level and
 * call these operators but the problem with that is if you go back up to an iterator by doing
 * Sequence.iterator().{stuff}.asSequence() and then do .iterator() on the result of that again,
 * you can only call the .iterator() on it once. This is because Iterator.asSequence() calls a
 * `Sequence { this }.constrainOnce()` operation which creates a sequence whose .iterator() function
 * can only be call once (have a look at Sequences.kt for more info). This make sense because
 * if you mess around calling methods of an iterator of a once-only sequence unexpected things
 * will happen to that sequence. The only probelm is that the we are doing .asSequence() on that
 * ConcatIterator which means we can't do concatIterator.asSequence().{stuff}.hasNext() and then .next()
 * on it. The way to work around that would be to write a custom .asSequenceMulti operator on ConcatIterator
 * which returns a sequence whose iterator can be returned many times. This might be fine with how
 * ConcatIterator works. If there are problems with the current approach of converting to a sequence
 * whenver a .map/filter/flatMap call is needed we can take a look at that approach.
 */
class Renderer(
  val maxWidth: Int,
  val colorApplyPrefix: Attrs,
  val colorLiteral: Attrs,
  val indentStep: Int
){

  fun rec(x: Tree, leftOffset: Int, indentCount: Int): Result = when(x) {
    is Tree.Apply -> {
      val prefix = x.prefix
      val body = x.body
      val nonEmpty = body.hasNext()

      // Render children and buffer them until you fill up a single line,
      // or you run out of children.
      //
      // Even before rendering any children, the indentation, prefix
      // and the two open/close parens already take up a few characters
      var totalHorizontalWidth = leftOffset + prefix.length + 2
      val buffer = mutableListOf>()
      var lastChildIter = sequenceOf().iterator()
      var childCompletedLineCount = 0
      while (body.hasNext() && totalHorizontalWidth <= maxWidth && childCompletedLineCount == 0) {

        val child = body.next()
        val childRes = rec(child, (indentCount + 1) * indentStep, indentCount + 1)

//        run {
//          val res = rec(child, (indentCount + 1) * indentStep, indentCount + 1)
//          println(res.iter.toList())
//          println("here")
//        }

        val childBuffer = mutableListOf()
        while (childRes.iter.hasNext() && totalHorizontalWidth < maxWidth) {
          val next = childRes.iter.next()
          childBuffer += next
          totalHorizontalWidth += next.length
        }


        if (body.hasNext()) {
          totalHorizontalWidth += 2
        }

        if (!childRes.iter.hasNext()) {
          childCompletedLineCount = childCompletedLineCount + childRes.completedLineCount
        } else {
          lastChildIter = childRes.iter

        }

        buffer += childBuffer.toList()
      }

      fun applyHeader() = sequenceOf(colorApplyPrefix(prefix), Renderer.openParen)

      val indentPlusOne = Renderer.indent((indentCount + 1) * indentStep)

      fun separator() = sequenceOf(Renderer.commaNewLine, indentPlusOne).iterator()

      if (
        totalHorizontalWidth <= maxWidth &&
          childCompletedLineCount == 0 &&
          !lastChildIter.hasNext()
      ) {
        val iter = Util.concat(
          { applyHeader().iterator() },
          {
            Renderer.joinIter(
              buffer.asSequence().map { it.iterator() }.iterator(),
              { sequenceOf(Renderer.commaSpace).iterator() }
            )
          },
          { sequenceOf(Renderer.closeParen).iterator() }
        )

        val length: Int = buffer.asSequence().map { it.asSequence().map { it.length }.sum() }.sum()
        Result(iter, {0}, {length})
      } else if (!nonEmpty && totalHorizontalWidth > maxWidth) {
        val iter = Util.concat(
          { applyHeader().iterator() },
          {sequenceOf(
            Renderer.newLine,
            Renderer.indent(indentCount * indentStep),
            Renderer.closeParen
          ).iterator()}
        )

        val length: Int = buffer.asSequence().map{it.asSequence().map {it.length}.sum()}.sum()
        Result(iter, {0}, {length})
      } else {
        fun bufferedFragments() = Renderer.joinIter(
          buffer.asSequence().withIndex().map { (i, v) ->
            if (i < buffer.size - 1) v.iterator()
            else (v.asSequence() + lastChildIter.asSequence()).iterator()
          }.iterator(),
          { separator() }
        )

        fun nonBufferedFragments() = Renderer.joinIter(
          body.asSequence().map { c -> rec(c, (indentCount + 1) * indentStep, indentCount + 1).iter }.iterator(),
          { separator() }
        )

        fun allFragments() =
          if (buffer.isEmpty()) nonBufferedFragments()
          else if (!body.hasNext()) bufferedFragments()
          else Renderer.joinIter(sequenceOf(bufferedFragments(), nonBufferedFragments()).iterator(), { separator() })

        fun iter() = Util.concat(
          { applyHeader().iterator() },
          { sequenceOf(Renderer.newLine, indentPlusOne).iterator() },
          { allFragments() },
          { sequenceOf(
            Renderer.newLine,
            Renderer.indent(indentCount * indentStep),
            Renderer.closeParen
          ).iterator() }
        )


        Result(iter(), {childCompletedLineCount + 2}, {indentCount * indentStep + 1})
      }
    }

    is Tree.Infix -> {
      val lhs = x.lhs
      val op = x.op
      val rhs = x.rhs
      rec(lhs, leftOffset, indentCount).flatMap { lhsNewline, lhsLastLineLength ->
        Result.fromString { " " + op + " " }.flatMap { _, _ ->
          rec(rhs, lhsLastLineLength, indentCount)
        }
      }
    }

    is Tree.Lazy -> {
      val t: Tree.Lazy = x
      val str by lazy {
        t.body0(Tree.Ctx(
          maxWidth, leftOffset, indentCount,
          indentStep, colorLiteral, colorApplyPrefix
        ))
      }
      Truncated(str.asSequence().map { Str(it) }.iterator(), maxWidth, height = 99999999).toResult()
    }

    is Tree.Literal ->
      Result.fromStr { colorLiteral(x.body) }

    is Tree.KeyValue -> {
      val k = x.key
      val v = x.value
      val prefix = "$k = "
      Result.fromString({ prefix })
        .flatMap { _, _ -> rec(v, leftOffset + prefix.length, indentCount) }
    }
  }

  companion object {
    fun  joinIter(it0: Iterator>, joiner: () -> Iterator) =
      Util.ConcatIterator(it0, joiner)


    val openParen = Str("(")
    val closeParen = Str(")")
    val commaSpace = Str(", ")
    val newLine = Str("\n")
    val commaNewLine = Str(",\n")
    private val cachedIndents = (0 until 64).map { Str(" ".repeat(it)) }.toTypedArray()
    fun indent(n: Int) = if (n < 64) cachedIndents[n] else Str(" ".repeat(n))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy