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:
parent
cae1839154
commit
0bc943ad54
3 changed files with 89 additions and 5 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue