123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- #
- #
- # 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
- proc raiseInvalidFormat(msg: string) {.noinline.} =
- raise newException(SubexError, "invalid format string: " & msg)
- type
- FormatParser = 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: untyped): untyped =
- p.i = i
- x
- i = p.i
- template callNoLineLenTracking(x: untyped): untyped =
- let oldLineLen = p.lineLen
- p.i = i
- x
- i = p.i
- p.lineLen = oldLineLen
- proc getFormatArg(p: var FormatParser, 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 FormatParser, a: openarray[string], s: var string) {.
- noSideEffect.}
- proc emitChar(p: var FormatParser, x: var string, ch: char) {.inline.} =
- x.add(ch)
- if ch == '\L': p.lineLen = 0
- else: inc p.lineLen
- proc emitStrLinear(p: var FormatParser, x: var string, y: string) {.inline.} =
- for ch in items(y): emitChar(p, x, ch)
- proc emitStr(p: var FormatParser, x: var string, y: string) {.inline.} =
- x.add(y)
- inc p.lineLen, y.len
- proc scanQuote(p: var FormatParser, 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 FormatParser, 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 FormatParser, 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 FormatParser, 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: FormatParser
- 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:
- from strutils import replace
- 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"]).replace(" \n", "\n") ==
- """doAssert(
- longishA,
- longish)""")
- doAssert(("type MyEnum* = enum\n $', '2i'\n '{..}" % ["fieldA",
- "fieldB", "FiledClkad", "fieldD", "fieldE", "longishFieldName"]).replace(" \n", "\n") ==
- strutils.unindent("""
- type MyEnum* = enum
- fieldA, fieldB,
- FiledClkad, fieldD,
- fieldE, longishFieldName""", 6))
- 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" == "'|"
- doAssert((subex("type\n Enum = enum\n $', '40c'\n '{..}") % [
- "fieldNameA", "fieldNameB", "fieldNameC", "fieldNameD"]).replace(" \n", "\n") ==
- strutils.unindent("""
- type
- Enum = enum
- fieldNameA, fieldNameB, fieldNameC,
- fieldNameD""", 6))
|