From 2f27f4b88fc5be58eb29f0e1a221fd7ae5375847 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:02:21 -0300 Subject: [PATCH 1/9] create FUNDING.yml add sponsorship info --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5a130ed --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# You may support oxifmt by sponsoring the account(s) below + +github: PgBiel From 5340e49346af1e94d62c642062b41cec68efcbbc Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:39:29 -0300 Subject: [PATCH 2/9] support typst 0.8.0 types --- oxifmt.typ | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index aba04be..6e78cf0 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -1,6 +1,13 @@ -// oxifmt v0.2.0 +// oxifmt v0.2.1 + +// For compatibility with pre-0.8.0 Typst types, which were strings +#let _int-type = type(0) +#let _float-type = type(5.5) +#let _str-type = type("") +#let _label-type = type() + #let _strfmt_formatparser(s) = { - if type(s) != "string" { + if type(s) != _str-type { panic("String format parsing internal error: String format parser given non-string.") } let result = () @@ -124,7 +131,7 @@ let subparts = name.match(regex("^([^:]*)(?::(.*))?$")).captures let name = subparts.at(0) let extras = subparts.at(1) - let name = if type(name) != "string" { + let name = if type(name) != _str-type { name } else if name == "" { none @@ -137,11 +144,11 @@ } #let _strfmt_is-numeric-type(obj) = { - type(obj) in ("integer", "float") + type(obj) in (_int-type, _float-type) } #let _strfmt_stringify(obj) = { - if type(obj) in ("integer", "float", "label", "string") { + if type(obj) in (_int-type, _float-type, _label-type, _str-type) { str(obj) } else { repr(obj) @@ -150,7 +157,7 @@ #let _strfmt_display-radix(num, radix, signed: true, lowercase: false) = { let num = int(num) - if type(radix) != "integer" or num == 0 or radix <= 1 { + if type(radix) != _int-type or num == 0 or radix <= 1 { return "0" } let sign = if num < 0 and signed { "-" } else { "" } @@ -266,7 +273,7 @@ parameter := argument '$' ) let arg = pos-replacements.at(i) assert( - type(arg) == "integer", + type(arg) == _int-type, message: "String formatter error: Attempted to use positional argument " + str(i) + " for " + spec-part-name + ", but it was a(n) '" + type(arg) + "', not an integer (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}')." ) @@ -282,7 +289,7 @@ parameter := argument '$' ) let arg = named-replacements.at(named) assert( - type(arg) == "integer", + type(arg) == _int-type, message: "String formatter error: Attempted to use named argument '" + named + "' for " + spec-part-name + ", but it was a(n) '" + type(arg) + "', not an integer (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}')." ) @@ -352,9 +359,9 @@ parameter := argument '$' if spectype in ("e", "E") { let exponent-sign = if spectype == "E" { "E" } else { "e" } replacement = _strfmt_exp-format(calc.abs(replacement), exponent-sign: exponent-sign, precision: precision) - } else if type(replacement) != "integer" and precision != none { + } else if type(replacement) != _int-type and precision != none { replacement = _strfmt_with-precision(replacement, precision) - } else if type(replacement) == "integer" and spectype in ("x", "X", "b", "o", "x?", "X?") { + } else if type(replacement) == _int-type and spectype in ("x", "X", "b", "o", "x?", "X?") { let radix-map = (x: 16, X: 16, "x?": 16, "X?": 16, b: 2, o: 8) let radix = radix-map.at(spectype) let lowercase = spectype.starts-with("x") @@ -453,7 +460,7 @@ parameter := argument '$' } replace-by = num-replacements.at(fmt-index) unnamed-format-index += 1 - } else if type(name) == "integer" { + } else if type(name) == _int-type { let fmt-index = name let amount-pos-replacements = num-replacements.len() if amount-pos-replacements == 0 { From be4818529aa0628a52bb9c723a076e8cf10bed6c Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 27 Apr 2024 12:44:55 -0300 Subject: [PATCH 3/9] fix minus sign inconsistency here we diverge from str(...) as we always use hyphens, so we can consider reverting this in the future, but it was inconsistent otherwise, as we would use hyphen on rich formatting --- oxifmt.typ | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index 6e78cf0..d190905 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -148,7 +148,11 @@ } #let _strfmt_stringify(obj) = { - if type(obj) in (_int-type, _float-type, _label-type, _str-type) { + if type(obj) in (_int-type, _float-type) { + // Fix negative sign not being a hyphen + // for consistency with our rich formatting output + str(obj).replace("\u{2212}", "-") + } else if type(obj) in (_label-type, _str-type) { str(obj) } else { repr(obj) @@ -215,7 +219,7 @@ let mantissa = f / calc.pow(10, exponent) let mantissa = _strfmt_with-precision(mantissa, precision) - mantissa + exponent-sign + str(exponent) + mantissa + exponent-sign + _strfmt_stringify(exponent) } // Parses {format:specslikethis}. From 13f9bedf5573f41c3f9508766044326d27eeb7ac Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sat, 27 Apr 2024 14:58:56 -0300 Subject: [PATCH 4/9] fix utf-8 parsing - Spans were using codepoint indices, not byte indices - Fix by adding "character.len()" to the index on each iteration --- oxifmt.typ | 33 +++++++++++++++++++++------------ tests/strfmt-tests.typ | 11 +++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/oxifmt.typ b/oxifmt.typ index d190905..1283f24 100644 --- a/oxifmt.typ +++ b/oxifmt.typ @@ -20,11 +20,10 @@ let last-was-lbracket = false // if the last character was an unescaped } let last-was-rbracket = false - let last-i = 0 // -- procedures -- - let write-format-span(i, result, current-fmt-span, current-fmt-name) = { - current-fmt-span.at(1) = i // end index + let write-format-span(last-i, result, current-fmt-span, current-fmt-name) = { + current-fmt-span.at(1) = last-i + 1 // end index result.push((format: (name: current-fmt-name, span: current-fmt-span))) current-fmt-span = none current-fmt-name = none @@ -43,14 +42,15 @@ } // -- parse loop -- - for (i, character) in codepoints.enumerate() { - last-i = i + let last-i = none + let i = 0 + for character in codepoints { if character == "{" { // double l-bracket = escape if last-was-lbracket { last-was-lbracket = false // escape {{ last-was-rbracket = false - if current-fmt-span.at(0) == i - 1 { + if current-fmt-span.at(0) == last-i { current-fmt-span = none // cancel this span current-fmt-name = none } @@ -60,13 +60,16 @@ current-fmt-name += character } else { // outside a span ({...} {{ <-) => emit an 'escaped' token - result.push((escape: (escaped: "{", span: (i - 1, i + 1)))) + result.push((escape: (escaped: "{", span: (last-i, i + 1)))) } + + last-i = i + i += 1 // '{' is ASCII, so 1 byte continue } if last-was-rbracket { // { ... }{ <--- ok, close the previous span - (result, current-fmt-span, current-fmt-name) = write-format-span(i, result, current-fmt-span, current-fmt-name) + (result, current-fmt-span, current-fmt-name) = write-format-span(last-i, result, current-fmt-span, current-fmt-name) last-was-rbracket = false } if current-fmt-span == none { @@ -82,8 +85,11 @@ if current-fmt-name != none { current-fmt-name += character } else { - result.push((escape: (escaped: "}", span: (i - 1, i + 1)))) + result.push((escape: (escaped: "}", span: (last-i, i + 1)))) } + + last-i = i + i += 1 // '}' is ASCII, so 1 byte continue } // delay closing the span to the next iteration @@ -91,7 +97,7 @@ last-was-rbracket = true } else { // { ... {A <--- non-escaped { inside larger {} - if last-was-lbracket and (current-fmt-span != none and current-fmt-span.at(0) != i - 1) { + if last-was-lbracket and (current-fmt-span != none and current-fmt-span.at(0) != last-i) { excessive-lbracket() } if last-was-rbracket { @@ -100,7 +106,7 @@ excessive-rbracket() } else { // { ... }A <--- ok, close the previous span - (result, current-fmt-span, current-fmt-name) = write-format-span(i, result, current-fmt-span, current-fmt-name) + (result, current-fmt-span, current-fmt-name) = write-format-span(last-i, result, current-fmt-span, current-fmt-name) } } // {abc <--- add character to the format name @@ -110,12 +116,15 @@ last-was-lbracket = false last-was-rbracket = false } + + last-i = i + i += character.len() // index must be in bytes, and a UTF-8 codepoint can have more than one byte } // { ... if current-fmt-span != none { if last-was-rbracket { // ... } <--- ok, close span - (result, current-fmt-span, current-fmt-name) = write-format-span(last-i + 1, result, current-fmt-span, current-fmt-name) + (result, current-fmt-span, current-fmt-name) = write-format-span(last-i, result, current-fmt-span, current-fmt-name) } else { // {abcd| <--- string ended with unclosed span missing-rbracket() diff --git a/tests/strfmt-tests.typ b/tests/strfmt-tests.typ index 3a91788..27862c1 100644 --- a/tests/strfmt-tests.typ +++ b/tests/strfmt-tests.typ @@ -46,6 +46,17 @@ // test custom decimal separators (III) - ensure we can fetch it from inside assert.eq(strfmt("5{fmt-decimal-separator}6", fmt-decimal-separator: "|"), "5|6") } +// Issue #6: UTF-8 +#{ + // shouldn't crash + assert.eq(strfmt("Hello € {}", "man"), "Hello € man") + + // should replace at the appropriate location + assert.eq(strfmt("Bank: {company-bank-name} € IBAN: {company-bank-iban}", company-bank-name: "FAKE", company-bank-iban: "Broken stuff"), "Bank: FAKE € IBAN: Broken stuff") + + // test grapheme clusters + assert.eq(strfmt("Ĺo͂řȩ{}m̅", 5.5), "Ĺo͂řȩ5.5m̅") +} // DOC TESTS #{ // --- Usage --- From ef9d37ccdef52e13ed3a54211c220c0123966d24 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 5 May 2024 21:03:13 -0300 Subject: [PATCH 5/9] create tests-ci.yml Closes https://github.com/PgBiel/typst-oxifmt/issues/8 --- .github/workflows/tests-ci.yml | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/tests-ci.yml diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml new file mode 100644 index 0000000..c4f3159 --- /dev/null +++ b/.github/workflows/tests-ci.yml @@ -0,0 +1,39 @@ +name: Tests CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the branches below + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + # Allows one to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + strategy: + matrix: + # Test for the following Typst versions + # 0.4.0 (earliest supported), 0.6.0 (first version with package management), + # 0.11.0 (latest supported) + typst-version: [v0.4.0, v0.6.0, v0.11.0] + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: 📥 Setup Typst + uses: typst-community/setup-typst@v3 + id: setup-typst + with: + version: ${{ matrix.typst-version }} + + - name: 🛠️ Compile test document + run: "typst compile tests/strfmt-tests.typ --root ." From a03bbed15e192f2679f74880205751e9e0e8b3f2 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 5 May 2024 21:06:35 -0300 Subject: [PATCH 6/9] ci: fix typst-version parameter --- .github/workflows/tests-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index c4f3159..8c9c5b9 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -33,7 +33,7 @@ jobs: uses: typst-community/setup-typst@v3 id: setup-typst with: - version: ${{ matrix.typst-version }} + typst-version: ${{ matrix.typst-version }} - name: 🛠️ Compile test document run: "typst compile tests/strfmt-tests.typ --root ." From edf009e2b187a816a1674d493af9e88b7f77411e Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 5 May 2024 21:10:34 -0300 Subject: [PATCH 7/9] ci: fix --root parameter order --- .github/workflows/tests-ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index 8c9c5b9..d2ca340 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -35,5 +35,10 @@ jobs: with: typst-version: ${{ matrix.typst-version }} - - name: 🛠️ Compile test document + - name: 🛠️ Compile test document ( Date: Sun, 5 May 2024 21:12:06 -0300 Subject: [PATCH 8/9] ci: another attempt at fixing --root --- .github/workflows/tests-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-ci.yml b/.github/workflows/tests-ci.yml index d2ca340..3504f46 100644 --- a/.github/workflows/tests-ci.yml +++ b/.github/workflows/tests-ci.yml @@ -37,7 +37,7 @@ jobs: - name: 🛠️ Compile test document ( Date: Mon, 6 May 2024 11:24:50 -0300 Subject: [PATCH 9/9] Release v0.2.1 (#7) --- README.md | 72 +++++++++++++++++++++++++++++------------------------- typst.toml | 2 +- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7f04eae..4782894 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# typst-oxifmt (v0.2.0) +# typst-oxifmt (v0.2.1) A Typst library that brings convenient string formatting and interpolation through the `strfmt` function. Its syntax is taken directly from Rust's `format!` syntax, so feel free to read its page for more information (https://doc.rust-lang.org/std/fmt/); however, this README should have enough information and examples for all expected uses of the library. Only a few things aren't supported from the Rust syntax, such as the `p` (pointer) format type, or the `.*` precision specifier. A few extras (beyond the Rust-like syntax) will be added over time, though (feel free to drop suggestions at the repository: https://github.com/PgBiel/typst-oxifmt). The first "extra" so far is the `fmt-decimal-separator: "string"` parameter, which lets you customize the decimal separator for decimal numbers (floats) inserted into strings. E.g. `strfmt("Result: {}", 5.8, fmt-decimal-separator: ",")` will return the string `"Result: 5,8"` (comma instead of dot). See more below. -**Compatible with:** [Typst](https://github.com/typst/typst) v0.4.0, v0.5.0, v0.6.0 +**Compatible with:** [Typst](https://github.com/typst/typst) v0.4.0+ ## Table of Contents @@ -21,13 +21,13 @@ A few extras (beyond the Rust-like syntax) will be added over time, though (feel You can use this library through Typst's package manager (for Typst v0.6.0+): -```js -#import "@preview/oxifmt:0.2.0": strfmt +```typ +#import "@preview/oxifmt:0.2.1": strfmt ``` For older Typst versions, download the `oxifmt.typ` file either from Releases or directly from the repository. Then, move it to your project's folder, and write at the top of your Typst file(s): -```js +```typ #import "oxifmt.typ": strfmt ``` @@ -35,8 +35,8 @@ Doing the above will give you access to the main function provided by this libra Its syntax is almost identical to Rust's `format!` (as specified here: https://doc.rust-lang.org/std/fmt/). You can escape formats by duplicating braces (`{{` and `}}` become `{` and `}`). Here's an example (see more examples in the file `tests/strfmt-tests.typ`): -```js -#import "@preview/oxifmt:0.2.0": strfmt +```typ +#import "@preview/oxifmt:0.2.1": strfmt #let s = strfmt("I'm {}. I have {num} cars. I'm {0}. {} is {{cool}}.", "John", "Carl", num: 10) #assert.eq(s, "I'm John. I have 10 cars. I'm John. Carl is {cool}.") @@ -73,8 +73,8 @@ You can use `{:spec}` to customize your output. See the Rust docs linked above f Some examples: -```js -#import "@preview/oxifmt:0.2.0": strfmt +```typ +#import "@preview/oxifmt:0.2.1": strfmt #let s1 = strfmt("{0:?}, {test:+012e}, {1:-<#8x}", "hi", -74, test: 569.4) #assert.eq(s1, "\"hi\", +00005.694e2, -0x4a---") @@ -89,57 +89,57 @@ Some examples: ### Examples - **Inserting labels, text and numbers into strings:** -```js -#import "@preview/oxifmt:0.2.0": strfmt +```typ +#import "@preview/oxifmt:0.2.1": strfmt #let s = strfmt("First: {}, Second: {}, Fourth: {3}, Banana: {banana} (brackets: {{escaped}})", 1, 2.1, 3, label("four"), banana: "Banana!!") #assert.eq(s, "First: 1, Second: 2.1, Fourth: four, Banana: Banana!! (brackets: {escaped})") ``` - **Forcing `repr()` with `{:?}`** (which adds quotes around strings, and other things - basically represents a Typst value): -```js -#import "@preview/oxifmt:0.2.0": strfmt +```typ +#import "@preview/oxifmt:0.2.1": strfmt #let s = strfmt("The value is: {:?} | Also the label is {:?}", "something", label("label")) #assert.eq(s, "The value is: \"something\" | Also the label is