summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorshirleyquirk <31934565+shirleyquirk@users.noreply.github.com>2021-04-12 06:32:37 +0100
committerGitHub <noreply@github.com>2021-04-12 07:32:37 +0200
commit0bc943ad54d707f93d47a6d52bf7d445c45388d8 (patch)
treea120cdc72b96e554d78e08c07ea56b963915904c
parentcae183915490846a0ec7dbcd52b29a74c7c59f05 (diff)
downloadNim-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.md1
-rw-r--r--lib/pure/strformat.nim33
-rw-r--r--tests/stdlib/tstrformat.nim62
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()