diff options
author | shirleyquirk <31934565+shirleyquirk@users.noreply.github.com> | 2021-04-12 06:32:37 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-12 07:32:37 +0200 |
commit | 0bc943ad54d707f93d47a6d52bf7d445c45388d8 (patch) | |
tree | a120cdc72b96e554d78e08c07ea56b963915904c | |
parent | cae183915490846a0ec7dbcd52b29a74c7c59f05 (diff) | |
download | Nim-0bc943ad54d707f93d47a6d52bf7d445c45388d8.tar.gz |
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>
-rw-r--r-- | changelogs/changelog_X_XX_X.md | 1 | ||||
-rw-r--r-- | lib/pure/strformat.nim | 33 | ||||
-rw-r--r-- | tests/stdlib/tstrformat.nim | 62 |
3 files changed, 90 insertions, 6 deletions
diff --git a/changelogs/changelog_X_XX_X.md b/changelogs/changelog_X_XX_X.md index 77b421f33..d68ef4c20 100644 --- a/changelogs/changelog_X_XX_X.md +++ b/changelogs/changelog_X_XX_X.md @@ -13,6 +13,7 @@ The changes should go to changelog.md! - Changed `example.foo` to take additional `bar` parameter. +- Added support for evaluating parenthesised expressions in strformat ## Language changes diff --git a/lib/pure/strformat.nim b/lib/pure/strformat.nim index c232c8a46..736b4d501 100644 --- a/lib/pure/strformat.nim +++ b/lib/pure/strformat.nim @@ -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 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 of `formatValue` procs. The required signature for a type `T` that supports 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, everything is wrapped in a `block`. + # Future directions 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 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] - of '(': inc inParens - of ')': dec inParens + of '\\': + 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 '=': let start = i inc i @@ -604,7 +629,7 @@ proc strformatImpl(pattern: NimNode; openChar, closeChar: char): NimNode = else: discard subexpr.add f[i] inc i - + var x: NimNode try: x = parseExpr(subexpr) diff --git a/tests/stdlib/tstrformat.nim b/tests/stdlib/tstrformat.nim index 86100e421..1863d3138 100644 --- a/tests/stdlib/tstrformat.nim +++ b/tests/stdlib/tstrformat.nim @@ -1,7 +1,7 @@ # xxx: test js target import genericstrformat -import std/[strformat, strutils, times] +import std/[strformat, strutils, times, tables, json] proc main() = 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" + 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 var name = "hello" let age = 21 @@ -496,6 +510,50 @@ proc main() = block: # test low(int64) 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() main() |