#### 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