Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/assets/typst/packs/preview/oxifmt/0.2.1.pack

21.3 KiB, 1 run

created by r2519314175:1069, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1DAIMOND TYPST PACK 1
2namespace preview
3name oxifmt
4version 0.2.1
5entrypoint oxifmt.typ
6file 906 LICENSE
7file 20504 oxifmt.typ
8file 255 typst.toml
9
10MIT No Attribution
11
12Copyright (c) 2023 Pg Biel
13
14Permission is hereby granted, free of charge, to any person obtaining a copy of this
15software and associated documentation files (the "Software"), to deal in the Software
16without restriction, including without limitation the rights to use, copy, modify,
17merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
18permit persons to whom the Software is furnished to do so.
19
20THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
21INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
22PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26// oxifmt v0.2.1
27
28// For compatibility with pre-0.8.0 Typst types, which were strings
29#let _int-type = type(0)
30#let _float-type = type(5.5)
31#let _str-type = type("")
32#let _label-type = type(<hello>)
33
34#let _strfmt_formatparser(s) = {
35 if type(s) != _str-type {
36 panic("String format parsing internal error: String format parser given non-string.")
37 }
38 let result = ()
39 let codepoints = s.codepoints()
40
41 // -- parsing state --
42 let current-fmt-span = none
43 let current-fmt-name = none
44 // if the last character was an unescaped {
45 let last-was-lbracket = false
46 // if the last character was an unescaped }
47 let last-was-rbracket = false
48
49 // -- procedures --
50 let write-format-span(last-i, result, current-fmt-span, current-fmt-name) = {
51 current-fmt-span.at(1) = last-i + 1 // end index
52 result.push((format: (name: current-fmt-name, span: current-fmt-span)))
53 current-fmt-span = none
54 current-fmt-name = none
55 (result, current-fmt-span, current-fmt-name)
56 }
57
58 // -- errors --
59 let excessive-lbracket() = {
60 panic("String format parsing error: Inserted a second, non-escaped { inside a {format specifier}. Did you forget to insert a } somewhere, or to escape the { with {{?")
61 }
62 let excessive-rbracket() = {
63 panic("String format parsing error: Inserted a stray } (doesn't match any { from before). Did you forget to insert a { somewhere, or to escape the } with }}?")
64 }
65 let missing-rbracket() = {
66 panic("String format parsing error: Reached end of string with an open format specifier {, but without a closing }. Did you forget to insert a right bracket, or to escape the { with {{?")
67 }
68
69 // -- parse loop --
70 let last-i = none
71 let i = 0
72 for character in codepoints {
73 if character == "{" {
74 // double l-bracket = escape
75 if last-was-lbracket {
76 last-was-lbracket = false // escape {{
77 last-was-rbracket = false
78 if current-fmt-span.at(0) == last-i {
79 current-fmt-span = none // cancel this span
80 current-fmt-name = none
81 }
82 if current-fmt-name != none {
83 // if in the middle of a larger span ({ ... {{ <-):
84 // add the escaped character to the format name
85 current-fmt-name += character
86 } else {
87 // outside a span ({...} {{ <-) => emit an 'escaped' token
88 result.push((escape: (escaped: "{", span: (last-i, i + 1))))
89 }
90
91 last-i = i
92 i += 1 // '{' is ASCII, so 1 byte
93 continue
94 }
95 if last-was-rbracket {
96 // { ... }{ <--- ok, close the previous span
97 (result, current-fmt-span, current-fmt-name) = write-format-span(last-i, result, current-fmt-span, current-fmt-name)
98 last-was-rbracket = false
99 }
100 if current-fmt-span == none {
101 // begin span
102 current-fmt-span = (i, none)
103 current-fmt-name = ""
104 }
105 last-was-lbracket = true
106 } else if character == "}" {
107 last-was-lbracket = false
108 if last-was-rbracket {
109 last-was-rbracket = false // escape }}
110 if current-fmt-name != none {
111 current-fmt-name += character
112 } else {
113 result.push((escape: (escaped: "}", span: (last-i, i + 1))))
114 }
115
116 last-i = i
117 i += 1 // '}' is ASCII, so 1 byte
118 continue
119 }
120 // delay closing the span to the next iteration
121 // in case this is an escaped }
122 last-was-rbracket = true
123 } else {
124 // { ... {A <--- non-escaped { inside larger {}
125 if last-was-lbracket and (current-fmt-span != none and current-fmt-span.at(0) != last-i) {
126 excessive-lbracket()
127 }
128 if last-was-rbracket {
129 if current-fmt-span == none {
130 // {...} }A <--- non-escaped } with no matching {
131 excessive-rbracket()
132 } else {
133 // { ... }A <--- ok, close the previous span
134 (result, current-fmt-span, current-fmt-name) = write-format-span(last-i, result, current-fmt-span, current-fmt-name)
135 }
136 }
137 // {abc <--- add character to the format name
138 if current-fmt-name != none {
139 current-fmt-name += character
140 }
141 last-was-lbracket = false
142 last-was-rbracket = false
143 }
144
145 last-i = i
146 i += character.len() // index must be in bytes, and a UTF-8 codepoint can have more than one byte
147 }
148 // { ...
149 if current-fmt-span != none {
150 if last-was-rbracket {
151 // ... } <--- ok, close span
152 (result, current-fmt-span, current-fmt-name) = write-format-span(last-i, result, current-fmt-span, current-fmt-name)
153 } else {
154 // {abcd| <--- string ended with unclosed span
155 missing-rbracket()
156 }
157 }
158
159 result
160}
161
162#let _strfmt_parse-fmt-name(name) = {
163 // {a:b} => separate 'a' from 'b'
164 // (also accepts {a}, {}, {0}, {:...})
165 let subparts = name.match(regex("^([^:]*)(?::(.*))?$")).captures
166 let name = subparts.at(0)
167 let extras = subparts.at(1)
168 let name = if type(name) != _str-type {
169 name
170 } else if name == "" {
171 none
172 } else if regex("^\\d+$") in name {
173 int(name)
174 } else {
175 name
176 }
177 (name, extras)
178}
179
180#let _strfmt_is-numeric-type(obj) = {
181 type(obj) in (_int-type, _float-type)
182}
183
184#let _strfmt_stringify(obj) = {
185 if type(obj) in (_int-type, _float-type) {
186 // Fix negative sign not being a hyphen
187 // for consistency with our rich formatting output
188 str(obj).replace("\u{2212}", "-")
189 } else if type(obj) in (_label-type, _str-type) {
190 str(obj)
191 } else {
192 repr(obj)
193 }
194}
195
196#let _strfmt_display-radix(num, radix, signed: true, lowercase: false) = {
197 let num = int(num)
198 if type(radix) != _int-type or num == 0 or radix <= 1 {
199 return "0"
200 }
201 let sign = if num < 0 and signed { "-" } else { "" }
202 let num = calc.abs(num)
203 let radix = calc.min(radix, 16)
204 let digits = if lowercase {
205 ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f")
206 } else {
207 ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F")
208 }
209 let result = ""
210
211 while (num > 0) {
212 let quot = calc.quo(num, radix)
213 let rem = calc.floor(calc.rem(num, radix))
214 let digit = digits.at(rem)
215 result = digit + result
216 num = quot
217 }
218
219 sign + result
220}
221
222#let _strfmt_with-precision(num, precision) = {
223 if precision == none {
224 return _strfmt_stringify(num)
225 }
226 let result = _strfmt_stringify(calc.round(float(num), digits: calc.min(50, precision)))
227 let digits-match = result.match(regex("^\\d+\\.(\\d+)$"))
228 let digits-len-diff = 0
229 if digits-match != none and digits-match.captures.len() > 0 {
230 // get the digits capture group; its length will be digit amount
231 let digits = digits-match.captures.first()
232 digits-len-diff = precision - digits.len()
233 } else if "." not in result { // 5.0 or something
234 // 0 digits! Difference will be exactly 'precision'
235 digits-len-diff = precision
236 }
237
238 // add missing zeroes for precision
239 if digits-len-diff > 0 {
240 if "." not in result {
241 result += "." // decimal separator missing
242 }
243 result += "0" * digits-len-diff
244 }
245
246 result
247}
248
249#let _strfmt_exp-format(num, exponent-sign: "e", base: 10, precision: none) = {
250 assert(_strfmt_is-numeric-type(num), message: "String formatter internal error: Cannot convert '" + repr(num) + "' to a number to obtain its scientific notation representation.")
251 let f = float(num)
252 let exponent = if f == 0 { 1 } else { calc.floor(calc.log(calc.abs(f), base: base)) }
253 let mantissa = f / calc.pow(10, exponent)
254 let mantissa = _strfmt_with-precision(mantissa, precision)
255
256 mantissa + exponent-sign + _strfmt_stringify(exponent)
257}
258
259// Parses {format:specslikethis}.
260// Rust's format spec grammar:
261/*
262format_spec := [[fill]align][sign]['#']['0'][width]['.' precision]type
263fill := character
264align := '<' | '^' | '>'
265sign := '+' | '-'
266width := count
267precision := count | '*'
268type := '' | '?' | 'x?' | 'X?' | identifier
269count := parameter | integer
270parameter := argument '$'
271*/
272#let _generate-replacement(fullname, extras, replacement, pos-replacements: (), named-replacements: (:), fmt-decimal-separator: auto) = {
273 if extras == none {
274 if not _strfmt_is-numeric-type(replacement) {
275 fmt-decimal-separator = auto
276 }
277 replacement = _strfmt_stringify(replacement)
278 if fmt-decimal-separator not in (auto, none) {
279 replacement = replacement.replace(".", _strfmt_stringify(fmt-decimal-separator))
280 }
281 return replacement
282 }
283 let extras = _strfmt_stringify(extras)
284 // note: usage of [\s\S] in regex to include all characters, incl. newline
285 // (dot format ignores newline)
286 let extra-parts = extras.match(
287 // fill align sign # 0 width(from param) width precision(from param) precision spectype
288 regex("^(?:([\\s\\S])?([<^>]))?([+-])?(#)?(0)?(?:(?:(\\d+)|([^.$]+))\$|(\\d+))?(?:\\.(?:(?:(\\d+)|([^$]+))\$|(\\d+|\*)))?([^\\s]*)\\s*$")
289 )
290 if extra-parts == none {
291 panic("String formatter error: Invalid format spec '" + extras + "', from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}'. Try escaping the braces { } with {{ }} if you wanted to insert literal braces.")
292 }
293
294 let (fill, align, sign, hashtag, zero, width-posarg, width-namedarg, width-lit, precision-posarg, precision-namedarg, precision-lit, spectype) = extra-parts.captures
295
296 // 'count' type parameters in the spec (width, precision) can be either a literal number (123),
297 // a number referring to a positional argument (123$), or some text referring to a named argument (abc$).
298 // The final $ is mandatory for the last two cases.
299 let parse-count(lit, pos, named, spec-part-name: "unknown") = {
300 if lit != none {
301 int(lit)
302 } else if pos != none {
303 let i = int(pos)
304 assert(
305 pos-replacements.len() > 0,
306 message: "String formatter error: Attempted to use positional argument " + str(i) + " for " + spec-part-name + ", but no positional arguments were given (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}')."
307 )
308 assert(
309 i >= 0 and i < pos-replacements.len(),
310 message: "String formatter error: Attempted to use positional argument " + str(i) + " for " + spec-part-name + ", but there is no argument at that position (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}'). Please note that positional arguments start at position 0, and are specified in order after the format string in the 'strfmt' call."
311 )
312 let arg = pos-replacements.at(i)
313 assert(
314 type(arg) == _int-type,
315 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("}", "}}") + "}')."
316 )
317
318 int(arg)
319 } else if named != none {
320 assert(
321 named-replacements.len() > 0,
322 message: "String formatter error: Attempted to use named argument '" + named + "' for " + spec-part-name + ", but no named arguments were given (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}')."
323 )
324 assert(
325 named in named-replacements,
326 message: "String formatter error: Attempted to use named argument '" + named + "' for " + spec-part-name + ", but there is no argument associated to that name (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}'). Ensure you pass that argument in the 'strfmt' call, e.g. strfmt(\"string {:.myarg$}\", 5.823, myarg: 10)."
327 )
328 let arg = named-replacements.at(named)
329 assert(
330 type(arg) == _int-type,
331 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("}", "}}") + "}')."
332 )
333
334 int(arg)
335 } else {
336 none
337 }
338 }
339
340 if precision-lit == "*" {
341 panic("String formater error: Precision specification of type `.*` is not supported yet (from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}'). Try specifying your desired precision directly on the format spec, e.g. `.5`, or through some argument, e.g. `.name$` to take it from the 'name' named argument.")
342 }
343
344 let align = if align == "" {
345 none
346 } else if align == "<" {
347 left
348 } else if align == ">" {
349 right
350 } else if align == "^" {
351 center
352 } else if align != none {
353 panic("String formatter error: Invalid alignment in the format spec: '" + align + "' (must be either '<', '^' or '>').")
354 }
355 let width = parse-count(width-lit, width-posarg, width-namedarg, spec-part-name: "width")
356 let width = if width == none { 0 } else { int(width) }
357 let precision = parse-count(precision-lit, precision-posarg, precision-namedarg, spec-part-name: "precision")
358 let hashtag = hashtag == "#"
359 let zero = zero == "0"
360 let hashtag-prefix = ""
361
362 let valid-specs = ("", "?", "b", "x", "X", "o", "x?", "X?", "e", "E")
363 let spec-error() = {
364 panic(
365 "String formatter error: Unknown spec type '" + spectype + "', from '{" + fullname.replace("{", "{{").replace("}", "}}") + "}'. Valid options include: '" + valid-specs.join("', '") + "'. Maybe you specified some invalid formatting spec syntax (after the ':'), which can also prompt this error. Check the oxifmt docs for more information.")
366 }
367 if spectype not in valid-specs {
368 spec-error()
369 }
370
371 let is-numeric = _strfmt_is-numeric-type(replacement)
372 if is-numeric {
373 if zero {
374 // disable fill, we will be prefixing with zeroes if necessary
375 fill = none
376 } else if fill == none {
377 fill = " "
378 zero = false
379 }
380 // default number alignment to right
381 if align == none {
382 align = right
383 }
384
385 // if + is specified, + will appear before all numbers >= 0.
386 if sign == "+" and replacement >= 0 {
387 sign = "+"
388 } else if replacement < 0 {
389 sign = "-"
390 } else {
391 sign = ""
392 }
393
394 // we'll add the sign back later!
395 replacement = calc.abs(replacement)
396
397 if spectype in ("e", "E") {
398 let exponent-sign = if spectype == "E" { "E" } else { "e" }
399 replacement = _strfmt_exp-format(calc.abs(replacement), exponent-sign: exponent-sign, precision: precision)
400 } else if type(replacement) != _int-type and precision != none {
401 replacement = _strfmt_with-precision(replacement, precision)
402 } else if type(replacement) == _int-type and spectype in ("x", "X", "b", "o", "x?", "X?") {
403 let radix-map = (x: 16, X: 16, "x?": 16, "X?": 16, b: 2, o: 8)
404 let radix = radix-map.at(spectype)
405 let lowercase = spectype.starts-with("x")
406 replacement = _strfmt_stringify(_strfmt_display-radix(replacement, radix, lowercase: lowercase, signed: false))
407 if hashtag {
408 let hashtag-prefix-map = ("16": "0x", "2": "0b", "8": "0o")
409 hashtag-prefix = hashtag-prefix-map.at(str(radix))
410 }
411 } else {
412 precision = none
413 replacement = if spectype.ends-with("?") {
414 repr(replacement)
415 } else {
416 _strfmt_stringify(replacement)
417 }
418 }
419 if fmt-decimal-separator not in (auto, none) {
420 replacement = replacement.replace(".", _strfmt_stringify(fmt-decimal-separator))
421 }
422 if zero {
423 let width-diff = width - (replacement.len() + sign.len() + hashtag-prefix.len())
424 if width-diff > 0 { // prefix with the appropriate amount of zeroes
425 replacement = ("0" * width-diff) + replacement
426 }
427 }
428 } else {
429 sign = ""
430 hashtag-prefix = ""
431 hashtag = false
432 zero = false
433 replacement = if spectype.ends-with("?") {
434 repr(replacement)
435 } else {
436 _strfmt_stringify(replacement)
437 }
438 if fill == none {
439 fill = " "
440 }
441 if align == none {
442 align = left
443 }
444 if precision != none and replacement.len() > precision {
445 replacement = replacement.slice(0, precision)
446 }
447 }
448
449 // use number prefixes parsed above
450 replacement = sign + hashtag-prefix + replacement
451
452 if fill != none {
453 // perform fill/width adjustments: "x" ---> " x" if width is 4
454 let width-diff = width - replacement.len() // number prefixes are also considered for width
455 if width-diff > 0 {
456 if align == left {
457 replacement = replacement + (fill * width-diff)
458 } else if align == right {
459 replacement = (fill * width-diff) + replacement
460 } else if align == center {
461 let width-fill = fill * (calc.ceil(float(width-diff) / 2))
462 replacement = width-fill + replacement + width-fill
463 }
464 }
465 }
466
467 replacement
468}
469
470#let strfmt(format, ..replacements) = {
471 if format == "" { return "" }
472 let formats = _strfmt_formatparser(format)
473 let num-replacements = replacements.pos()
474 let named-replacements = replacements.named()
475 let unnamed-format-index = 0
476 let fmt-decimal-separator = if "fmt-decimal-separator" in named-replacements { named-replacements.at("fmt-decimal-separator") } else { auto }
477
478 let parts = ()
479 let last-span-end = 0
480 for f in formats {
481 let replace-by = none
482 let replace-span = none
483 if "escape" in f {
484 replace-by = f.escape.escaped
485 replace-span = f.escape.span
486 } else if "format" in f {
487 let f = f.format
488 let (name, extras) = _strfmt_parse-fmt-name(f.name)
489 if name == none {
490 let fmt-index = unnamed-format-index
491 let amount-pos-replacements = num-replacements.len()
492 if amount-pos-replacements == 0 {
493 panic("String formatter error: Specified a {} (or similar) format to extract positional replacements, but none were given. Try specifying them sequentially after the format string, e.g. strfmt(\"{}, {}\", 5, 1+1) would become \"5, 2\".")
494 }
495 if amount-pos-replacements <= fmt-index {
496 let were-was = if amount-pos-replacements == 1 { "was" } else { "were" }
497 panic("String formatter error: Specified more {} (or similar) formats than positional replacements (only " + str(amount-pos-replacements) + " of them " + were-was + " given!). Please specify the missing positional arguments sequentially after the format string in the 'strfmt' call.")
498 }
499 replace-by = num-replacements.at(fmt-index)
500 unnamed-format-index += 1
501 } else if type(name) == _int-type {
502 let fmt-index = name
503 let amount-pos-replacements = num-replacements.len()
504 if amount-pos-replacements == 0 {
505 panic("String formatter error: format key '" + str(name) + "' would attempt to get a positional replacement, but none were given after the string. Try specifying positional replacements after the format string in the 'strfmt' call, e.g. strfmt(\"{1}, {0}\", 2, 3) would become \"3, 2\".")
506 }
507 if amount-pos-replacements <= fmt-index {
508 let were-was = if amount-pos-replacements == 1 { "was" } else { "were" }
509 panic("String formatter error: format key '" + str(name) + "', from '{" + f.name.replace("{", "{{").replace("}", "}}") + "}', is not a valid positional replacement position (only " + str(amount-pos-replacements) + " of them " + were-was + " given). Note that the first position is 0. For example, strfmt(\"{1}, {0}\", 2, 3) would become \"3, 2\".")
510 }
511 replace-by = num-replacements.at(fmt-index)
512 } else { // named replacement
513 if name not in named-replacements {
514 panic("String formatter error: format key '" + name + "', from '{" + f.name.replace("{", "{{").replace("}", "}}") + "}', does not match any given named replacement. Try specifying it after the format string, e.g. like so: strfmt(\"Test: {myarg}\", myarg: 1 + 1) would become \"Test: 2\".")
515 }
516 replace-by = named-replacements.at(name)
517 }
518 replace-by = _generate-replacement(f.name, extras, replace-by, pos-replacements: num-replacements, named-replacements: named-replacements, fmt-decimal-separator: fmt-decimal-separator)
519 replace-span = f.span
520 } else {
521 panic("String formatter error: Internal error (unexpected format received).")
522 }
523 // {...}ABCABCABC{...} <--- push ABCABCABC to parts
524 parts.push(format.slice(last-span-end, replace-span.at(0)))
525 // push the replacement string instead of the {...} at the end
526 parts.push(replace-by)
527 last-span-end = replace-span.at(1)
528 }
529 if last-span-end < format.len() {
530 parts.push(format.slice(last-span-end, format.len()))
531 }
532
533 // join all the string parts (constant parts + formatted parts + escaped parts)
534 parts.join()
535}
536[package]
537name = "oxifmt"
538version = "0.2.1"
539authors = ["PgBiel <https://github.com/PgBiel>"]
540license = "MIT-0"
541description = "Convenient Rust-like string formatting in Typst"
542entrypoint = "oxifmt.typ"
543repository = "https://github.com/PgBiel/typst-oxifmt"