Login
Artifact [5f12271244]
Login

Artifact 5f12271244b92bda4066f69ba299a61f085fdd176a4f6835915123dd1e4beb62:


#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
####
#### This is based on code by jkfurtney: https://github.com/jkfurtney/clformat
require "./extensions"

# The `RemiLib::Format` module contains alternate methods to format strings with
# various arguments.  Unlike `printf`/`sprintf`, the format strings for this
# module are based on those for Common Lisp's `FORMAT` function.
#
# Note that not all of the directives from Common Lisp are supported.  These are
# currently implemented:
#
# * `~a`: Same as `to_s`
# * `~s`: Same as `to_s`, except strings are automatically wrapped in double quotes.
# * `~d`: Formats an integer in decimal.  All modifiers supported.
# * `~x`: Formats an integer in hexadecimal.  All modifiers supported.
# * `~b`: Formats an integer in binary.  All modifiers supported.
# * `~o`: Formats an integer in octal.  All modifiers supported.
# * `~r`: Formats an integer in English words using `Int#toSpoken`, or
#         `Int#toRoman` if the @ modifier is used.  The : modifier is not yet
#         supported.
# * `~e`: Only partially supported, currently the same as `.to_f.to_s`.
# * `~f`: Only partially supported, currently the same as `.to_f.to_s`.
# * `~g`: Only partially supported, currently the same as `.to_f.to_s`.
# * `~$`: Only partially supported, currently the same as `.to_f.to_s`.
# * `~p`: Plural-izes a word.  All modifiers supported.
# * `~%`: Prints a newline.  All modifiers supported.
# * `~{`: Iteration.  Currently broken.
# * `~^`: Used with iteration.
# * `~~`: Prints a literal `~`.  All modifiers supported.
# * `~*`: Skips around within the argument list.  All modifiers supported.
# * `~[`: Conditional formatting.  All modifiers except # supported.
#
# Documentation for the original Common Lisp function can be found here:
# [http://www.lispworks.com/documentation/HyperSpec/Body/22_c.htm](http://www.lispworks.com/documentation/HyperSpec/Body/22_c.htm)
module RemiLib::Format
  # :nodoc:
  DIRECTIVE_LIST = "%&|tcdboxrpfeg$as~<>{}[]();^*"

  # :nodoc:
  PAIR_DIRECTIVES = "[<({"

  # :nodoc:
  PAIR_COMPLEMENTS = { "[" => "]",
                       "{" => "}",
                       "(" => ")",
                       "<" => ">" }.to_h

  # :nodoc:
  alias DirectiveTuple = Tuple(String, StaticArray(String|Nil, 7), Bool, Bool)

  # :nodoc:
  private class Node
    property parent : Node? = nil
    getter children : Array(Node) = [] of Node
    property value : String|DirectiveTuple|Nil = nil

    def initialize(@parent : Node? = nil, @value : String|DirectiveTuple|Nil = nil)
    end

    def <<(val : String|DirectiveTuple)
      @children << Node.new(self, val)
      self
    end
  end

  # :nodoc:
  private class ArgumentList(*T)
    getter data : Tuple(*T)
    @dataIdx = 0

    def initialize(@data : Tuple(*T))
    end

    protected def initialize(@data, @dataIdx)
    end

    def dup
      ArgumentList.new(@data, @dataIdx)
    end

    def size
      @data.size - @dataIdx
    end

    def backupSize
      @dataIdx
    end

    def origSize
      @data.size
    end

    def shift
      ret = @data[@dataIdx]
      @dataIdx += 1
      ret
    end

    def backup
      @dataIdx -= 1
    end

    def empty?
      @dataIdx == @data.size
    end

    def rewind(count : UInt32) : Nil
      if @dataIdx - count < 0
        raise ArgumentError.new("Can't rewind argument list, not enough arguments")
      else
        @dataIdx -= count
      end
    end

    def goto(pos : UInt32) : Nil
      @dataIdx = pos
    end

    def lastUsedData
      @data[@dataIdx - 1]
    end
  end

  # The `Formatter` class is used to format string data.
  #
  # See the documentation on the `RemiLib::Format` module for the supported
  # syntax.
  class Formatter
    # The control string associated with this `Formatter` instance.
    getter controlString : String

    # Creates a new `Formatter` for the given control string.
    def initialize(@controlString : String)
      @tree = Formatter.buildTree(Formatter.tokenize(@controlString))
    end

    protected def self.parseSinglePrefixArg(queue : Deque(String)) : String?
      return nil if queue.empty?
      str = queue.join("")
      str.match(/^([+-]?[0-9]+|V|v|#|'.)/).try do |match|
        arg = match[1]
        arg.size.times do |_|
          queue.shift
        end
        return arg
      end

      nil
    end

    protected def self.parsePrefixArgList(queue : Deque(String)) : StaticArray(String|Nil, 7)
      ret = StaticArray(String|Nil, 7).new(nil)

      if arg = parseSinglePrefixArg(queue)
        ret[0] = arg
      end

      (1...7).each do |i|
        if queue[0] == ","
          queue.shift
          if arg = parseSinglePrefixArg(queue)
            ret[i] = arg
          end
        end
      end

      ret
    end

    protected def self.parseDirectiveModifiers(queue : Deque(String)) : Tuple(Bool, Bool)
      colon = false
      at = false

      # TODO add support for # modifier

      if queue[0] == ":" || queue[0] == "@"
        if queue.shift == ":"
          colon = true
        else
          at = true
        end
      end

      if queue[0] == ":" || queue[0] == "@"
        if queue.shift == ":"
          if colon
            raise ArgumentError.new("More than one : in format directive")
          end
          colon = true
        else
          if at
            raise ArgumentError.new("More than one @ in format directive")
          end
          at = true
        end
      end

      {colon, at}
    end

    protected def self.parseDirectiveType(queue : Deque(String))
      if queue[0].chars.all? &.alphanumeric?
        queue[0] = queue[0].downcase
      end

      if DIRECTIVE_LIST.includes?(queue[0])
        return queue.shift
      end

      raise ArgumentError.new("Unknown format directive: '#{queue[0]}'")
    end

    protected def self.parseDirective(queue : Deque(String)) : DirectiveTuple
      prefixes = parsePrefixArgList(queue)
      colon, at = parseDirectiveModifiers(queue)
      dtype = parseDirectiveType(queue)
      {dtype, prefixes, colon, at}
    end

    protected def self.tokenize(str : String) : Array(String|DirectiveTuple)
      queue = Deque(String).new(str.chars.map &.to_s)
      ret : Array(IO::Memory|DirectiveTuple) = [] of IO::Memory|DirectiveTuple
      ret << IO::Memory.new

      while !queue.empty?
        ch = queue.shift
        if ch == "~"
          if ret[-1].is_a?(IO::Memory) && ret[-1].empty?
            ret[-1] = parseDirective(queue)
          else
            ret << parseDirective(queue)
          end
          ret << IO::Memory.new
        else
          ret[-1].as(IO::Memory) << ch
        end
      end

      ret.map { |x| x.is_a?(DirectiveTuple) ? x : x.to_s }
    end

    protected def self.buildTree(parent : Node, tokenList : Deque(String|DirectiveTuple)) : Nil
      while !tokenList.empty?
        if tokenList[0].is_a?(String)
          parent << tokenList.shift
        else
          chr = tokenList[0][0]
          if PAIR_DIRECTIVES.includes?(chr)
            parent << tokenList.shift
            current = parent.children[-1]

            nestedCount =0
            closingChar = PAIR_COMPLEMENTS[chr]
            childTokens = [] of String|DirectiveTuple
            looking = true

            while looking
              if tokenList.empty?
                raise ArgumentError.new("End of format string before finding closing #{closingChar}")
              end

              if tokenList[0].is_a?(String)
                childTokens << tokenList.shift
              else
                nestedChar = tokenList[0][0]
                if nestedChar == chr
                  nestedCount += 1
                  childTokens << tokenList.shift
                elsif nestedChar == closingChar
                  if nestedCount == 0
                    tokenList.shift # Drop the closing directive
                    looking = false
                  else
                    childTokens << tokenList.shift
                    nestedCount -= 1
                  end
                else
                  childTokens << tokenList.shift
                end
              end
            end

            buildTree(current, Deque(String|DirectiveTuple).new(childTokens))
          else
            parent << tokenList.shift
          end
        end
      end
    end

    protected def self.buildTree(tokenList : Array(String|DirectiveTuple)) : Node
      ret = Node.new
      tokenListDeque = Deque(String|DirectiveTuple).new(tokenList)
      buildTree(ret, tokenListDeque)
      ret
    end

    # Formats this `Formatter`'s control string with the given arguments, then
    # returns a new string.
    def format(*args) : String
      executor = FormatExecutor.new(@tree, ArgumentList.new(args))
      executor.output
    end
  end

  private class FormatExecutor(*T)
    @output : IO::Memory = IO::Memory.new
    property args : ArgumentList(*T)

    def initialize(@tree : Node, @args : ArgumentList(*T))
      @tree.children.each do |child|
        break unless processNode(child, @output)
      end
    end

    protected def initialize(@tree : Node, @output : IO::Memory, @args : ArgumentList(*T))
      @tree.children.each do |child|
        break unless processNode(child, @output)
      end
    end

    def self.dupWithArgs(old : FormatExecutor(*T), newArgs)
      FormatExecutor.new(old.tree, old.outputIO, newArgs)
    end

    def output : String
      @output.to_s
    end

    def outputIO : IO::Memory
      @output
    end

    def processNode(node : Node, dest : IO::Memory) : String?
      if node.value.is_a?(String)
        dest << node.value.to_s
      else
        if val = node.value.as?(DirectiveTuple)
          directive = val[0]
          case directive
          when "a"
            if ret = RemiLib::Format::FormatMethods.a(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "s"
            if ret = RemiLib::Format::FormatMethods.s(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "d"
            if ret = RemiLib::Format::FormatMethods.d(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "x"
            if ret = RemiLib::Format::FormatMethods.x(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "o"
            if ret = RemiLib::Format::FormatMethods.o(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "b"
            if ret = RemiLib::Format::FormatMethods.b(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "r"
            if ret = RemiLib::Format::FormatMethods.r(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "e"
            if ret = RemiLib::Format::FormatMethods.e(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "f"
            if ret = RemiLib::Format::FormatMethods.f(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "g"
            if ret = RemiLib::Format::FormatMethods.g(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "$"
            if ret = RemiLib::Format::FormatMethods.dollar(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "p"
            if ret = RemiLib::Format::FormatMethods.p(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "%"
            if ret = RemiLib::Format::FormatMethods.percent(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "{"
            if ret = RemiLib::Format::FormatMethods.list(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "^"
            if ret = RemiLib::Format::FormatMethods.circumflex(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "~"
            if ret = RemiLib::Format::FormatMethods.tilde(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "*"
            if ret = RemiLib::Format::FormatMethods.asterisk(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          when "["
            if ret = RemiLib::Format::FormatMethods.conditional(node, @args, self)
              dest << ret.to_s
            else
              return nil
            end

          # when "("
          #   if ret = RemiLib::Format::FormatMethods.capitalization(node, @args, self)
          #     dest << ret.to_s
          #   else
          #     return nil
          #   end

          else
            # Unimplemented directive
            dest << directive
          end
        end
      end

      ""
    end
  end

  # :nodoc:
  module FormatMethods
    # :nodoc:
    private def self.preProcessPrefixArgs(prefixes, args : ArgumentList(*T)) forall T
      {% begin %}
        {% actualTypes = [] of TypeNode %}
        {% needInt32 = true %}
        {% needString = true %}
        {% needNil = true %}
        {% for typ in T %}
          {% if typ == Int32 %}
            {% needInt32 = false %}
          {% elsif typ == String %}
            {% needString = false %}
          {% elsif typ == Nil %}
            {% needNil = false %}
          {% end %}
          {% actualTypes << typ %}
        {% end %}

        {% if needInt32 %}
          {% actualTypes << Int32 %}
        {% end %}

        {% if needString %}
          {% actualTypes << String %}
        {% end %}

        {% if needNil %}
          {% actualTypes << Nil %}
        {% end %}

        localPrefixes = Array({{actualTypes.join("|").id}}).new

        (0...prefixes.size).each do |i|
          case
          when prefixes[i] == "#"
            localPrefixes << args.size
          when prefixes[i] == "v"
            localPrefixes << args.shift
          else
            localPrefixes << prefixes[i]
          end
        end

        localPrefixes
      {% end %}
    end

    # :nodoc:
    private def self.getPrefixInt(arg, default : Int = 0)
      if arg.nil?
        default
      elsif arg.is_a?(Int) || arg.is_a?(String)
        arg.to_i
      else
        raise "Cannot get an integer from #{arg}"
      end
    end

    # :nodoc:
    private def self.getPrefixChar(arg, default : String = " ")
      arg.nil? ? default : arg.as(String).[1].to_s
    end

    # :nodoc:
    private def self.padGeneral(string : String, atModifier : Bool, prefixes) : String
      minCol = getPrefixInt(prefixes[0])
      colInc = getPrefixInt(prefixes[1], 1)
      minPad = getPrefixInt(prefixes[2])
      padChar = getPrefixChar(prefixes[3])

      if string.size < minCol || minPad > 0
        numPad = ((minCol - string.size) / colInc).ceil.to_i32! * colInc
        numPad = 0 if numPad < 0
        numPad += minPad
        if atModifier
          "#{padChar * numPad}#{string}"
        else
          "#{string}#{padChar * numPad}"
        end
      else
        string
      end
    end

    # :nodoc:
    def self.a(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      arg = args.shift
      padGeneral(arg.to_s, at, prefixes)
    end

    # :nodoc:
    private def self.splitLen(seq : T, length : Int) forall T
      ret = [] of T
      0.step(to: (seq.size - 1), by: length) do |i|
        ret << seq[i...(i + length)]
      end
      ret
    end

    # :nodoc:
    def self.s(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      arg = args.shift
      str = if arg.is_a?(String)
              "\"#{arg}\""
            else
              arg.to_s
            end
      padGeneral(str, at, prefixes)
    end

    # :nodoc:
    def self.formatInteger(number, string, at, colon, prefixes)
      str = IO::Memory.new
      if at && number > 0
        str << '+'
      end
      str << string

      if colon
        commaChar = getPrefixChar(prefixes[2], ",")
        commaInterval = getPrefixInt(prefixes[3], 3).to_u32!
        groups = splitLen(string.reverse, commaInterval).reverse.map &.reverse
        str.clear
        str << '+' if at && number > 0
        str << groups.join(commaChar)
      end

      # Padding
      minWidth = getPrefixInt(prefixes[0])
      padChar = getPrefixChar(prefixes[1])
      if str.size < minWidth
        "#{padChar * (minWidth - str.size)}#{str}"
      elsif str.size > 0
        str.to_s
      else
        string
      end
    end

    # :nodoc:
    def self.d(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = args.shift.as?(Int) || raise "Expected an integer"
      formatInteger(num, num.to_s, at, colon, prefixes)
    end

    # :nodoc:
    def self.x(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = args.shift.as?(Int) || raise "Expected an integer"
      formatInteger(num, num.to_s(16), at, colon, prefixes)
    end

    # :nodoc:
    def self.b(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = args.shift.as?(Int) || raise "Expected an integer"
      formatInteger(num, num.to_s(2), at, colon, prefixes)
    end

    # :nodoc:
    def self.o(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = args.shift.as?(Int) || raise "Expected an integer"
      formatInteger(num, num.to_s(8), at, colon, prefixes)
    end

    # :nodoc:
    def self.r(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = args.shift.as?(Int) || raise "Expected an integer"

      unless prefixes[0]
        if at
          num.toRoman
        else
          num.toSpoken
        end
      else
        radix = prefixes[0].as?(Int) || raise "Expected integer radix"
        num.to_s(radix)
      end
    end

    # :nodoc:
    def self.e(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)

      # width = getPrefixInt(prefixes[0])
      # digitsAfter = getPrefixInt(prefixes[1], 1)
      # expDigits = getPrefixInt(prefixes[2], 1)
      # scaleFactor = getPrefixInt(prefixes[3], 1)
      # overflowChar = getPrefixChar(prefixes[4], "")
      # padChar = getPrefixChar(prefixes[5])
      # expChar = getPrefixChar(prefixes[6], "e")

      # TODO
      arg = args.shift
      num = (arg.as?(Float) || arg.as?(Int) || raise "Expected a float or integer")
      num.to_f.to_s
    end

    # :nodoc:
    def self.f(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)

      # width = getPrefixInt(prefixes[0])
      # digitsAfter = getPrefixInt(prefixes[1], 1)
      # scaleFactor = getPrefixInt(prefixes[3], 0)
      # overflowChar = getPrefixChar(prefixes[4], "")
      # padChar = getPrefixChar(prefixes[5])

      # TODO
      arg = args.shift
      num = (arg.as?(Float) || arg.as?(Int) || raise "Expected a float or integer")
      num.to_f.to_s
    end

    # :nodoc:
    def self.g(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)

      #width = getPrefixInt(prefixes[0])
      #digitsAfter = getPrefixInt(prefixes[1], 1)
      #expDigits = getPrefixInt(prefixes[2], 1)
      #scaleFactor = getPrefixInt(prefixes[3], 1)
      #overflowChar = getPrefixChar(prefixes[4], "")
      #padChar = getPrefixChar(prefixes[5])
      #expChar = getPrefixChar(prefixes[6], "e")

      # TODO
      arg = args.shift
      num = (arg.as?(Float) || arg.as?(Int) || raise "Expected a float or integer")
      num.to_f.to_s
    end

    # :nodoc:
    def self.dollar(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)

      # digitsAfter = getPrefixInt(prefixes[0], 2)
      # digitsBefore = getPrefixInt(prefixes[1], 1)
      # width = getPrefixInt(prefixes[3], 0)
      # padChar = getPrefixChar(prefixes[5])

      # TODO
      arg = args.shift
      num = (arg.as?(Float) || arg.as?(Int) || raise "Expected a float or integer")
      num.to_f.to_s
    end

    # :nodoc:
    def self.tilde(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = getPrefixInt(prefixes[0], 1)
      "~" * num
    end

    # :nodoc:
    def self.p(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)

      args.shift unless colon

      lastUsed = args.lastUsedData
      if lastUsed.is_a?(Number) && lastUsed == 1
        at ? "y" : ""
      else
        at ? "ies" : "s"
      end
    end

    # :nodoc:
    def self.circumflex(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)

      if args.size == 0
        nil
      else
        ""
      end
    end

    # :nodoc:
    def self.list(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      directive, prefixes, _, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      raise "Unexpected directive" unless directive == "{"

      maxIterations = if prefixes[0].nil?
                        100_000_000
                      else
                        prefixes[0].as?(Int64) || raise "Expected a number"
                      end

      ex = if at
             executor
           else
             FormatExecutor.new(node, ArgumentList.new({executor.args.shift}))
           end

      iteration = 0
      while !ex.args.empty?
        break if iteration >= maxIterations

        node.children.each do |child|
          unless ex.processNode(child, ex.outputIO)
            iteration = maxIterations
            break
          end
        end

        iteration += 1
      end
    end

    # :nodoc:
    def self.percent(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, _, _ = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = getPrefixInt(prefixes[0], 1)
      "\n" * num
    end

    # :nodoc:
    def self.asterisk(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      _, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      num = getPrefixInt(prefixes[0], 1)

      if colon
        if num > args.backupSize
          raise ArgumentError.new("Cannot jump forward by #{num}, there are only (#{args.backupSize} in the argument list)")
        end
        args.rewind(num.to_u32)
      elsif at
        if num < 0 || num > args.size
          raise ArgumentError.new("Cannot jump to argument #{num}, there are only #{args.origSize} in the argument list")
        end
        args.goto(num.to_u32)
      else
        if args.size < num
          raise ArgumentError.new("Cannot jump forward by #{num}, there are only (#{args.size} in the argument list)")
        end
        num.times { |_| args.shift }
      end
      ""
    end

    # :nodoc:
    def self.conditional(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
      directive, prefixes, colon, at = node.value.as(DirectiveTuple)
      prefixes = preProcessPrefixArgs(prefixes, args)
      raise "Unexpected directive" unless directive == "["

      maybeIndex = prefixes[0].nil? ? args.shift : prefixes[0]

      return "" if at && !maybeIndex
      index = if colon
                if maybeIndex
                  1
                else
                  0
                end
              else
                maybeIndex
              end

      current = 0
      childDeque = Deque.new(node.children.dup)
      if at
        child = childDeque.shift
        executor.processNode(child, executor.outputIO)
        child = childDeque.shift
        args.backup
        executor.processNode(child, executor.outputIO)
      else
        while !childDeque.empty?
          if current == index
            # We found what we are looking for.  Process nodes until the next ~;
            # or empty.
            loop do
              return "" if childDeque.empty?
              child = childDeque.shift

              if child.value.is_a?(String)
                executor.processNode(child, executor.outputIO)
              else
                val = child.value
                case val
                when Tuple, Indexable
                  break if val[0] == ";"
                when String
                  break if val[0] == ';'
                else
                  return nil unless executor.processNode(child, executor.outputIO)
                end
              end
            end

            return ""
          end

          # Looking for the correct clause
          child = childDeque.shift
          val = child.value
          case val
          when String then nil
          when Tuple, Indexable
            if val[0] == ";"
              if val[2]
                current = index.as(Int)
              else
                current += 1
              end
            end
          end
        end
      end

      ""
    end

    # def self.capitalization(node : Node, args : ArgumentList(*T), executor : FormatExecutor) forall T
    #   directive, prefixes, colon, at = node.value.as(DirectiveTuple)
    #   prefixes = preProcessPrefixArgs(prefixes, args)
    #   raise "Unexpected directive" unless directive == "("

    #   output = executor.outputIO
    #   startSize = output.size
    #   ret = ""

    #   node.children.each do |child|
    #     ret = executor.processNode(child, output)
    #     break unless ret
    #   end

    #   # TODO
    # end
  end

  # Formats `fmtString` with the given arguments using a
  # `RemiLib::Format::Formatter`, then returns the resulting string.
  def self.format(fmtString : String, *args) : String
    RemiLib::Format::Formatter.new(fmtString).format(*args)
  end

  # Formats `fmtString` with the given arguments using a
  # `RemiLib::Format::Formatter`, then writes the results to `io`.  This returns
  # the `io`.
  def self.format(io : IO, fmtString : String, *args) : IO
    io << RemiLib::Format::Formatter.new(fmtString).format(*args)
    io
  end
end


# Formats `fmtString` with the given arguments using a
# `RemiLib::Format::Formatter`, then prints the string to standard output.  A
# newline is not automatically appended to the end.  This returns `STDOUT`.
def format(fmtString : String, *args) : IO
  STDOUT << RemiLib::Format::Formatter.new(fmtString).format(*args)
  STDOUT
end

# Formats `fmtString` with the given arguments using a
# `RemiLib::Format::Formatter`, then returns a new string with the formatted
# output.  A newline is not automatically appended to the end.
def formatStr(fmtString : String, *args) : String
  RemiLib::Format::Formatter.new(fmtString).format(*args)
end

# Formats `fmtString` with the given arguments using a
# `RemiLib::Format::Formatter`, then writes the results to `io`.
def format(io : IO, fmtString : String, *args) : Nil
  io << RemiLib::Format::Formatter.new(fmtString).format(*args)
end