#
#
# Nim's Runtime Library
# (c) Copyright 2012 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Nim support for `substitution expressions`:idx: (`subex`:idx:).
##
## .. include:: ../doc/subexes.txt
##
{.push debugger:off .} # the user does not want to trace a part
# of the standard library!
from strutils import parseInt, cmpIgnoreStyle, Digits
include "system/inclrtl"
proc findNormalized(x: string, inArray: openarray[string]): int =
var i = 0
while i < high(inArray):
if cmpIgnoreStyle(x, inArray[i]) == 0: return i
inc(i, 2) # incrementing by 1 would probably lead to a
# security hole...
return -1
type
SubexError* = object of ValueError ## exception that is raised for
## an invalid subex
{.deprecated: [EInvalidSubex: SubexError].}
proc raiseInvalidFormat(msg: string) {.noinline.} =
raise newException(SubexError, "invalid format string: " & msg)
type
TFormatParser = object {.pure, final.}
when defined(js):
f: string # we rely on the '\0' terminator
# which JS's native string doesn't have
else:
f: cstring
num, i, lineLen: int
template call(x: stmt) {.immediate.} =
p.i = i
x
i = p.i
template callNoLineLenTracking(x: stmt) {.immediate.} =
let oldLineLen = p.lineLen
p.i = i
x
i = p.i
p.lineLen = oldLineLen
proc getFormatArg(p: var TFormatParser, a: openArray[string]): int =
const PatternChars = {'a'..'z', 'A'..'Z', '0'..'9', '\128'..'\255', '_'}
var i = p.i
var f = p.f
case f[i]
of '#':
result = p.num
inc i
inc p.num
of '1'..'9', '-':
var j = 0
var negative = f[i] == '-'
if negative: inc i
while f[i] in Digits:
j = j * 10 + ord(f[i]) - ord('0')
inc i
result = if not negative: j-1 else: a.len-j
of 'a'..'z', 'A'..'Z', '\128'..'\255', '_':
var name = ""
while f[i] in PatternChars:
name.add(f[i])
inc(i)
result = findNormalized(name, a)+1
of '$':
inc(i)
call:
result = getFormatArg(p, a)
result = parseInt(a[result])-1
else:
raiseInvalidFormat("'#', '$', number or identifier expected")
if result >=% a.len: raiseInvalidFormat("index out of bounds: " & $result)
p.i = i
proc scanDollar(p: var TFormatParser, a: openarray[string], s: var string) {.
noSideEffect.}
proc emitChar(p: var TFormatParser, x: var string, ch: char) {.inline.} =
x.add(ch)
if ch == '\L': p.lineLen = 0
else: inc p.lineLen
proc emitStrLinear(p: var TFormatParser, x: var string, y: string) {.inline.} =
for ch in items(y): emitChar(p, x, ch)
proc emitStr(p: var TFormatParser, x: var string, y: string) {.inline.} =
x.add(y)
inc p.lineLen, y.len
proc scanQuote(p: var TFormatParser, x: var string, toAdd: bool) =
var i = p.i+1
var f = p.f
while true:
if f[i] == '\'':
inc i
if f[i] != '\'': break
inc i
if toAdd: emitChar(p, x, '\'')
elif f[i] == '\0': raiseInvalidFormat("closing \"'\" expected")
else:
if toAdd: emitChar(p, x, f[i])
inc i
p.i = i
proc scanBranch(p: var TFormatParser, a: openArray[string],
x: var string, choice: int) =
var i = p.i
var f = p.f
var c = 0
var elsePart = i
var toAdd = choice == 0
while true:
case f[i]
of ']': break
of '|':
inc i
elsePart = i
inc c
if toAdd: break
toAdd = choice == c
of '\'':
call: scanQuote(p, x, toAdd)
of '\0': raiseInvalidFormat("closing ']' expected")
else:
if toAdd:
if f[i] == '$':
inc i
call: scanDollar(p, a, x)
else:
emitChar(p, x, f[i])
inc i
else:
inc i
if not toAdd and choice >= 0:
# evaluate 'else' part:
var last = i
i = elsePart
while true:
case f[i]
of '|', ']': break
of '\'':
call: scanQuote(p, x, true)
of '$':
inc i
call: scanDollar(p, a, x)
else:
emitChar(p, x, f[i])
inc i
i = last
p.i = i+1
proc scanSlice(p: var TFormatParser, a: openarray[string]): tuple[x, y: int] =
var slice = false
var i = p.i
var f = p.f
if f[i] == '{': inc i
else: raiseInvalidFormat("'{' expected")
if f[i] == '.' and f[i+1] == '.':
inc i, 2
slice = true
else:
call: result.x = getFormatArg(p, a)
if f[i] == '.' and f[i+1] == '.':
inc i, 2
slice = true
if slice:
if f[i] != '}':
call: result.y = getFormatArg(p, a)
else:
result.y = high(a)
else:
result.y = result.x
if f[i] != '}': raiseInvalidFormat("'}' expected")
inc i
p.i = i
proc scanDollar(p: var TFormatParser, a: openarray[string], s: var string) =
var i = p.i
var f = p.f
case f[i]
of '$':
emitChar p, s, '$'
inc i
of '*':
for j in 0..a.high: emitStr p, s, a[j]
inc i
of '{':
call:
let (x, y) = scanSlice(p, a)
for j in x..y: emitStr p, s, a[j]
of '[':
inc i
var start = i
call: scanBranch(p, a, s, -1)
var x: int
if f[i] == '{':
inc i
call: x = getFormatArg(p, a)
if f[i] != '}': raiseInvalidFormat("'}' expected")
inc i
else:
call: x = getFormatArg(p, a)
var last = i
let choice = parseInt(a[x])
i = start
call: scanBranch(p, a, s, choice)
i = last
of '\'':
var sep = ""
callNoLineLenTracking: scanQuote(p, sep, true)
if f[i] == '~':
# $' '~{1..3}
# insert space followed by 1..3 if not empty
inc i
call:
let (x, y) = scanSlice(p, a)
var L = 0
for j in x..y: inc L, a[j].len
if L > 0:
emitStrLinear p, s, sep
for j in x..y: emitStr p, s, a[j]
else:
block StringJoin:
block OptionalLineLengthSpecifier:
var maxLen = 0
case f[i]
of '0'..'9':
while f[i] in Digits:
maxLen = maxLen * 10 + ord(f[i]) - ord('0')
inc i
of '$':
# do not skip the '$' here for `getFormatArg`!
call:
maxLen = getFormatArg(p, a)
else: break OptionalLineLengthSpecifier
var indent = ""
case f[i]
of 'i':
inc i
callNoLineLenTracking: scanQuote(p, indent, true)
call:
let (x, y) = scanSlice(p, a)
if maxLen < 1: emitStrLinear(p, s, indent)
var items = 1
emitStr p, s, a[x]
for j in x+1..y:
emitStr p, s, sep
if items >= maxLen:
emitStrLinear p, s, indent
items = 0
emitStr p, s, a[j]
inc items
of 'c':
inc i
callNoLineLenTracking: scanQuote(p, indent, true)
call:
let (x, y) = scanSlice(p, a)
if p.lineLen + a[x].len > maxLen: emitStrLinear(p, s, indent)
emitStr p, s, a[x]
for j in x+1..y:
emitStr p, s, sep
if p.lineLen + a[j].len > maxLen: emitStrLinear(p, s, indent)
emitStr p, s, a[j]
else: raiseInvalidFormat("unit 'c' (chars) or 'i' (items) expected")
break StringJoin
call:
let (x, y) = scanSlice(p, a)
emitStr p, s, a[x]
for j in x+1..y:
emitStr p, s, sep
emitStr p, s, a[j]
else:
call:
var x = getFormatArg(p, a)
emitStr p, s, a[x]
p.i = i
type
Subex* = distinct string ## string that contains a substitution expression
{.deprecated: [TSubex: Subex].}
proc subex*(s: string): Subex =
## constructs a *substitution expression* from `s`. Currently this performs
## no syntax checking but this may change in later versions.
result = Subex(s)
proc addf*(s: var string, formatstr: Subex, a: varargs[string, `$`]) {.
noSideEffect, rtl, extern: "nfrmtAddf".} =
## The same as ``add(s, formatstr % a)``, but more efficient.
var p: TFormatParser
p.f = formatstr.string
var i = 0
while i < len(formatstr.string):
if p.f[i] == '$':
inc i
call: scanDollar(p, a, s)
else:
emitChar(p, s, p.f[i])
inc(i)
proc `%` *(formatstr: Subex, a: openarray[string]): string {.noSideEffect,
rtl, extern: "nfrmtFormatOpenArray".} =
## The `substitution`:idx: operator performs string substitutions in
## `formatstr` and returns a modified `formatstr`. This is often called
## `string interpolation`:idx:.
##
result = newStringOfCap(formatstr.string.len + a.len shl 4)
addf(result, formatstr, a)
proc `%` *(formatstr: Subex, a: string): string {.noSideEffect,
rtl, extern: "nfrmtFormatSingleElem".} =
## This is the same as ``formatstr % [a]``.
result = newStringOfCap(formatstr.string.len + a.len)
addf(result, formatstr, [a])
proc format*(formatstr: Subex, a: varargs[string, `$`]): string {.noSideEffect,
rtl, extern: "nfrmtFormatVarargs".} =
## The `substitution`:idx: operator performs string substitutions in
## `formatstr` and returns a modified `formatstr`. This is often called
## `string interpolation`:idx:.
##
result = newStringOfCap(formatstr.string.len + a.len shl 4)
addf(result, formatstr, a)
{.pop.}
when isMainModule:
proc `%`(formatstr: string, a: openarray[string]): string =
result = newStringOfCap(formatstr.len + a.len shl 4)
addf(result, formatstr.Subex, a)
proc `%`(formatstr: string, a: string): string =
result = newStringOfCap(formatstr.len + a.len)
addf(result, formatstr.Subex, [a])
doAssert "$# $3 $# $#" % ["a", "b", "c"] == "a c b c"
doAssert "$animal eats $food." % ["animal", "The cat", "food", "fish"] ==
"The cat eats fish."
doAssert "$[abc|def]# $3 $# $#" % ["17", "b", "c"] == "def c b c"
doAssert "$[abc|def]# $3 $# $#" % ["1", "b", "c"] == "def c b c"
doAssert "$[abc|def]# $3 $# $#" % ["0", "b", "c"] == "abc c b c"
doAssert "$[abc|def|]# $3 $# $#" % ["17", "b", "c"] == " c b c"
doAssert "$[abc|def|]# $3 $# $#" % ["-9", "b", "c"] == " c b c"
doAssert "$1($', '{2..})" % ["f", "a", "b"] == "f(a, b)"
doAssert "$[$1($', '{2..})|''''|fg'$3']1" % ["7", "a", "b"] == "fg$3"
doAssert "$[$#($', '{#..})|''''|$3]1" % ["0", "a", "b"] == "0(a, b)"
doAssert "$' '~{..}" % "" == ""
doAssert "$' '~{..}" % "P0" == " P0"
doAssert "${$1}" % "1" == "1"
doAssert "${$$-1} $$1" % "1" == "1 $1"
doAssert "$#($', '10c'\n '{#..})" % ["doAssert", "longishA", "longish"] ==
"""doAssert(
longishA,
longish)"""
assert "type TMyEnum* = enum\n $', '2i'\n '{..}" % ["fieldA",
"fieldB", "FiledClkad", "fieldD", "fieldE", "longishFieldName"] ==
strutils.unindent """
type TMyEnum* = enum
fieldA, fieldB,
FiledClkad, fieldD,
fieldE, longishFieldName"""
doAssert subex"$1($', '{2..})" % ["f", "a", "b", "c"] == "f(a, b, c)"
doAssert subex"$1 $[files|file|files]{1} copied" % ["1"] == "1 file copied"
doAssert subex"$['''|'|''''|']']#" % "0" == "'|"
assert subex("type\n TEnum = enum\n $', '40c'\n '{..}") % [
"fieldNameA", "fieldNameB", "fieldNameC", "fieldNameD"] ==
strutils.unindent """
type
TEnum = enum
fieldNameA, fieldNameB, fieldNameC,
fieldNameD"""