followup strformat PR. backslash escapes, tests, docs (#17700)

* Allow use of colons inside fmt
allowing colons inside fmt by replacing the format specifier delimiter lets arbitrary nim code be run within fmt expressions.

Co-authored-by: flywind <xzsflywind@gmail.com>

* formatting,documentation,backslash escapes

Adding support for evaluating expressions by special-casing parentheses causes this regression: `&"""{ "(hello)" }"""` no longer parses.
In addition, code such as &"""{(if open: '(' else: ')')}""" wouldn't work.
To enable that, as well as the use of, e.g. Table constructors inside curlies, I've added backslash escapes.
This also means that if/for/etc statements, unparenthesized, will work, if the colons are escaped, but i've left that under-documented.

It's not exactly elegant having two types of escape, but I believe it's the least bad option.

* changelog
* added json strformat test
* pulled my thumb out and wrote a parser

Co-authored-by: Andreas Rumpf <rumpf_a@web.de>
Co-authored-by: flywind <xzsflywind@gmail.com>
This commit is contained in:
shirleyquirk 2021-04-12 06:32:37 +01:00 • committed by GitHub
commit 0bc943ad54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 89 additions and 5 deletions

View file

@ -13,6 +13,7 @@ The changes should go to changelog.md!
- Changed `example.foo` to take additional `bar` parameter. - Changed `example.foo` to take additional `bar` parameter.
- Added support for evaluating parenthesised expressions in strformat
## Language changes ## Language changes

View file

@ -144,6 +144,19 @@ An expression like `&"{key} is {value:arg} {{z}}"` is transformed into:
Parts of the string that are enclosed in the curly braces are interpreted Parts of the string that are enclosed in the curly braces are interpreted
as Nim code, to escape a `{` or `}`, double it. as Nim code, to escape a `{` or `}`, double it.
Within a curly expression,however, '{','}', must be escaped with a backslash.
To enable evaluating Nim expressions within curlies, inside parentheses
colons do not need to be escaped.
]##
runnableExamples:
let x = "hello"
assert fmt"""{ "\{(" & x & ")\}" }""" == "{(hello)}"
assert fmt"""{{({ x })}}""" == "{(hello)}"
assert fmt"""{ $(\{x:1,"world":2\}) }""" == """[("hello", 1), ("world", 2)]"""
##[
`&` delegates most of the work to an open overloaded set `&` delegates most of the work to an open overloaded set
of `formatValue` procs. The required signature for a type `T` that supports of `formatValue` procs. The required signature for a type `T` that supports
formatting is usually `proc formatValue(result: var string; x: T; specifier: string)`. formatting is usually `proc formatValue(result: var string; x: T; specifier: string)`.
@ -289,6 +302,7 @@ keep the hygiene of `myTemplate`, and we do not want `arg1`
to be injected into the context where `myTemplate` is expanded, to be injected into the context where `myTemplate` is expanded,
everything is wrapped in a `block`. everything is wrapped in a `block`.
# Future directions # Future directions
A curly expression with commas in it like `{x, argA, argB}` could be A curly expression with commas in it like `{x, argA, argB}` could be
@ -588,10 +602,21 @@ proc strformatImpl(pattern: NimNode; openChar, closeChar: char): NimNode =
var subexpr = "" var subexpr = ""
var inParens = 0 var inParens = 0
while i < f.len and f[i] != closeChar and (f[i] != ':' or inParens!=0): var inSingleQuotes = false
var inDoubleQuotes = false
template notEscaped:bool = f[i-1]!='\\'
while i < f.len and f[i] != closeChar and (f[i] != ':' or inParens != 0):
case f[i] case f[i]
of '(': inc inParens of '\\':
of ')': dec inParens if i < f.len-1 and f[i+1] in {openChar,closeChar,':'}: inc i
of '\'':
if not inDoubleQuotes and notEscaped: inSingleQuotes = not inSingleQuotes
of '\"':
if notEscaped: inDoubleQuotes = not inDoubleQuotes
of '(':
if not (inSingleQuotes or inDoubleQuotes): inc inParens
of ')':
if not (inSingleQuotes or inDoubleQuotes): dec inParens
of '=': of '=':
let start = i let start = i
inc i inc i
@ -604,7 +629,7 @@ proc strformatImpl(pattern: NimNode; openChar, closeChar: char): NimNode =
else: discard else: discard
subexpr.add f[i] subexpr.add f[i]
inc i inc i
var x: NimNode var x: NimNode
try: try:
x = parseExpr(subexpr) x = parseExpr(subexpr)

View file

@ -1,7 +1,7 @@
# xxx: test js target # xxx: test js target
import genericstrformat import genericstrformat
import std/[strformat, strutils, times] import std/[strformat, strutils, times, tables, json]
proc main() = proc main() =
block: # issue #7632 block: # issue #7632
@ -284,6 +284,20 @@ proc main() =
doAssert fmt"{123.456=:>13e}" == "123.456= 1.234560e+02" doAssert fmt"{123.456=:>13e}" == "123.456= 1.234560e+02"
doAssert fmt"{123.456=:13e}" == "123.456= 1.234560e+02" doAssert fmt"{123.456=:13e}" == "123.456= 1.234560e+02"
let x = 3.14
doAssert fmt"{(if x!=0: 1.0/x else: 0):.5}" == "0.31847"
doAssert fmt"""{(block:
var res: string
for i in 1..15:
res.add (if i mod 15 == 0: "FizzBuzz"
elif i mod 5 == 0: "Buzz"
elif i mod 3 == 0: "Fizz"
else: $i) & " "
res)}""" == "1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz "
doAssert fmt"""{ "\{(" & msg & ")\}" }""" == "{(hello)}"
doAssert fmt"""{{({ msg })}}""" == "{(hello)}"
doAssert fmt"""{ $(\{msg:1,"world":2\}) }""" == """[("hello", 1), ("world", 2)]"""
block: # tests for debug format string block: # tests for debug format string
var name = "hello" var name = "hello"
let age = 21 let age = 21
@ -496,6 +510,50 @@ proc main() =
block: # test low(int64) block: # test low(int64)
doAssert &"{low(int64):-}" == "-9223372036854775808" doAssert &"{low(int64):-}" == "-9223372036854775808"
block: #expressions plus formatting
doAssert fmt"{if true\: 123.456 else\: 0=:>9.3f}" == "if true: 123.456 else: 0= 123.456"
doAssert fmt"{(if true: 123.456 else: 0)=}" == "(if true: 123.456 else: 0)=123.456"
doAssert fmt"{if true\: 123.456 else\: 0=:9.3f}" == "if true: 123.456 else: 0= 123.456"
doAssert fmt"{(if true: 123.456 else: 0)=:9.4f}" == "(if true: 123.456 else: 0)= 123.4560"
doAssert fmt"{(if true: 123.456 else: 0)=:>9.0f}" == "(if true: 123.456 else: 0)= 123."
doAssert fmt"{if true\: 123.456 else\: 0=:<9.4f}" == "if true: 123.456 else: 0=123.4560 "
doAssert fmt"""{(case true
of false: 0.0
of true: 123.456)=:e}""" == """(case true
of false: 0.0
of true: 123.456)=1.234560e+02"""
doAssert fmt"""{block\:
var res = 0.000123456
for _ in 0..5\:
res *= 10
res=:>13e}""" == """block:
var res = 0.000123456
for _ in 0..5:
res *= 10
res= 1.234560e+02"""
#side effects
var x = 5
doAssert fmt"{(x=7;123.456)=:13e}" == "(x=7;123.456)= 1.234560e+02"
doAssert x==7
block: #curly bracket expressions and tuples
proc formatValue(result: var string; value:Table|bool|JsonNode; specifier:string) = result.add $value
doAssert fmt"""{\{"a"\:1,"b"\:2\}.toTable() = }""" == """{"a":1,"b":2}.toTable() = {"a": 1, "b": 2}"""
doAssert fmt"""{(\{3: (1,"hi",0.9),4: (4,"lo",1.1)\}).toTable()}""" == """{3: (1, "hi", 0.9), 4: (4, "lo", 1.1)}"""
doAssert fmt"""{ (%* \{"name": "Isaac", "books": ["Robot Dreams"]\}) }""" == """{"name":"Isaac","books":["Robot Dreams"]}"""
doAssert """%( \%\* {"name": "Isaac"})*""".fmt('%','*') == """{"name":"Isaac"}"""
block: #parens in quotes that fool my syntax highlighter
doAssert fmt"{(if true: ')' else: '(')}" == ")"
doAssert fmt"{(if true: ']' else: ')')}" == "]"
doAssert fmt"""{(if true: "\")\"" else: "\"(")}""" == """")""""
doAssert &"""{(if true: "\")" else: "")}""" == "\")"
doAssert &"{(if true: \"\\\")\" else: \"\")}" == "\")"
doAssert fmt"""{(if true: "')" else: "")}""" == "')"
doAssert fmt"""{(if true: "'" & "'" & ')' else: "")}""" == "'')"
doAssert &"""{(if true: "'" & "'" & ')' else: "")}""" == "'')"
doAssert &"{(if true: \"\'\" & \"'\" & ')' else: \"\")}" == "'')"
doAssert fmt"""{(if true: "'" & ')' else: "")}""" == "')"
# xxx static: main() # xxx static: main()
main() main()