From e696a3a09d4866080093d2e583fa8ce9aab52256 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Sun, 25 May 2025 10:34:41 +0200 Subject: [PATCH 01/20] Content To IR --- src/data-model.typ | 23 +-- src/display-intermediate-representation.typ | 61 ++++-- src/lib.typ | 2 +- ...se-content-intermediate-representation.typ | 99 +++++++++ ...se-formula-intermediate-representation.typ | 2 +- src/utils.typ | 100 ++++++++- tests/README-graphic1/test.typ | 6 +- tests/arrow-align/test.typ | 9 +- tests/brackets/test.typ | 4 +- tests/charges/test.typ | 4 +- tests/content-to-ir/.gitignore | 4 + tests/content-to-ir/ref/1.png | Bin 0 -> 1083 bytes tests/content-to-ir/test.typ | 14 ++ tests/get-element/test.typ | 2 +- .../test.typ | 136 ++++++------- .../test.typ | 192 +++++++++--------- tests/parse-ir-elements/test.typ | 90 ++++---- tests/parse-ir-groups/test.typ | 70 +++---- tests/shell-configuration/test.typ | 10 +- tests/simple-formulas/test.typ | 3 +- 20 files changed, 529 insertions(+), 302 deletions(-) create mode 100644 src/parse-content-intermediate-representation.typ create mode 100644 tests/content-to-ir/.gitignore create mode 100644 tests/content-to-ir/ref/1.png create mode 100644 tests/content-to-ir/test.typ diff --git a/src/data-model.typ b/src/data-model.typ index 42547be..68c9557 100644 --- a/src/data-model.typ +++ b/src/data-model.typ @@ -229,25 +229,4 @@ h-p: if override-h-p != none { override-h-p } else { molecule.h-p }, ghs: if override-ghs != none { override-ghs } else { molecule.ghs }, ) -} - -#let reaction(body) = { - let children = get-all-children(body) - - // repr(body) - - // linebreak() - let result = "" - for child in children { - if is-metadata(child) { - if is-kind(child, "molecule") { - result += child.value.formula - } else if is-kind(child, "element") { - result += child.value.symbol - } - } else { - result += child - } - } - ce(to-string(result)) -} +} \ No newline at end of file diff --git a/src/display-intermediate-representation.typ b/src/display-intermediate-representation.typ index c55c1a4..b066ac9 100644 --- a/src/display-intermediate-representation.typ +++ b/src/display-intermediate-representation.typ @@ -1,21 +1,47 @@ -#import "utils.typ": try-at, count-to-content, charge-to-content, get-bracket, get-arrow, phase-to-content - +#import "utils.typ": ( + try-at, + count-to-content, + charge-to-content, + get-bracket, + get-arrow, + phase-to-content, + typst-builtin-styled, + none-coalesce, + reconstruct-content, +) #let display-element(data) = { let isotope = data.at("isotope", default: none) + let symbol = data.symbol + let t = data.at("oxidation-number", default: none) + let tr = charge-to-content(data.at("charge", default: none), radical: data.at("radical", default: false)) + let br = count-to-content(data.at("count", default: none)) + let tl = try-at(isotope, "mass-number") + let bl = try-at(isotope, "atomic-number") + + symbol = reconstruct-content(data.at("symbol-body", default: none), symbol) + tr = reconstruct-content(data.at("charge-body", default: none), tr) + br = reconstruct-content(data.at("count-body", default: none), br) + math.attach( - data.symbol, - t: data.at("oxidation-number", default: none), - tr: charge-to-content(data.at("charge", default: none), radical: data.at("radical", default: false)), - br: count-to-content(data.at("count", default: none)), - tl: try-at(isotope, "mass-number"), - bl: try-at(isotope, "atomic-number"), + symbol, + t: t, + tr: tr, + br: br, + tl: tl, + bl: bl, ) } #let display-group(data) = { let children = data.at("children", default: ()) let kind = data.at("kind", default: 1) - math.attach( + let tr = charge-to-content(data.at("charge", default: none)) + let br = count-to-content(data.at("count", default: none)) + + tr = reconstruct-content(data.at("charge-body", default: none), tr) + br = reconstruct-content(data.at("count-body", default: none), br) + + let result = math.attach( math.lr({ get-bracket(kind, open: true) for child in children { @@ -31,14 +57,17 @@ } get-bracket(kind, open: false) }), - tr: charge-to-content(data.at("charge", default: none)), - br: count-to-content(data.at("count", default: none)), + tr: tr, + br: br, ) + + return reconstruct-content(data.at("body", default: none), result) } #let display-molecule(data) = { count-to-content(data.at("count", default: none)) - math.attach( + + let result = math.attach( [ #let children = data.at("children", default: ()) #for child in children { @@ -56,9 +85,13 @@ tr: charge-to-content(data.at("charge", default: none)), // br: phase-to-content(data.at("phase", default:none)), ) - context { - text(phase-to-content(data.at("phase", default: none)), size: text.size * 0.75) + if data.at("phase", default: none) != none { + result += context { + text(phase-to-content(data.at("phase", default: none)), size: text.size * 0.75) + } } + + return reconstruct-content(data.at("body", default: none), result) } #let display-ir(data) = { diff --git a/src/lib.typ b/src/lib.typ index 5fae9b0..bf76471 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,4 +1,4 @@ -#import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate, reaction +#import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate #import "display-shell-configuration.typ": ( get-electron-configuration, get-shell-configuration, diff --git a/src/parse-content-intermediate-representation.typ b/src/parse-content-intermediate-representation.typ new file mode 100644 index 0000000..c447e97 --- /dev/null +++ b/src/parse-content-intermediate-representation.typ @@ -0,0 +1,99 @@ +#import "utils.typ": get-all-children, is-metadata, typst-builtin-styled, typst-builtin-context +#import "parse-formula-intermediate-representation.typ": string-to-ir +#let content-to-ir(body) = { + if type(body) == str { + return string-to-ir(body) + } else if type(body) != content { + return none + } + let children = get-all-children(body) + + // body + // linebreak() + // repr(body) + // linebreak() + // linebreak() + + let result = () + + let string = "" + for child in children { + if is-metadata(child) { + if is-kind(child, "molecule") { + result += child.value.formula + } else if is-kind(child, "element") { + result += child.value.symbol + } + } else if type(child) == content{ + let func-type = child.func() + if func-type == text { + result += string-to-ir(child.at("text")) + string += child.at("text") + } else if func-type == typst-builtin-styled{ + let styles = child.at("styles") + let ir = content-to-ir(child.at("child")) + if type(ir) == array{ + if ir.len() == 1{ + ir = ir.at(0) + } else{ + for value in ir { + value.styles = styles + } + result += ir + } + } + if type(ir) == dictionary{ + ir.styles = styles + result.push(ir) + } + // result.push((type:"content", body:child)) + } else if func-type == typst-builtin-context { + result.push((type:"content", body:child)) + } + else if func-type in ( + pad, + figure, + quote, + strong, + emph, + highlight, + overline, + underline, + strike, + smallcaps, + sub, + super, + box, + block, + hide, + move, + scale, + circle, + ellipse, + rect, + square, + typst-builtin-styled + ) { + result.push((type:"content", body:child)) + } + else if child == [ ] { + } + else { + result.push((type:"content", body:child)) + } + + repr(type(child)) + h(1em) + repr(child.func()) + h(1em) + repr(child.fields()) + h(4em) + linebreak() + // result += child + } + } + // return result + + + // return string-to-ir(string) +} diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index 8a71f49..539f78f 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -5,7 +5,7 @@ // group: regex("^(\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])"), group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?"), reaction-plus: regex("^(\s?\+\s?)"), - reaction-arrow: regex("^\s?(<->|<=>|->|<-|=>|<=|-\/>|<\/-)(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?\s?"), + reaction-arrow: regex("^\s?(<->|↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-\/>|<\/-)(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?\s?"), math: regex("^(\$[^$]*\$)"), // Match physical states (s/l/g/aq) state: regex("^\((s|l|g|aq|solid|liquid|gas|aqueous)\)"), diff --git a/src/utils.typ b/src/utils.typ index 4f695da..b1e5131 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -91,13 +91,19 @@ ) #let arrow-kinds = ( "<->": 0, + "↔": 0, "->": 1, + "→": 1, "<-": 2, + "←": 2, "=>": 3, - "<+": 4, + "⇒": 3, + "<=": 4, + "⇐": 4, "-/>": 5, "": 7, + "⇔": 7, ) #let get-bracket(kind, open: true) = { @@ -176,10 +182,22 @@ // https://github.com/touying-typ/touying/blob/6316aa90553f5d5d719150709aec1396e750da63/src/utils.typ#L157C1-L166C2 #let typst-builtin-sequence = ([A] + [ ] + [B]).func() +#let typst-builtin-styled = [#set text(fill: red)].func() +#let typst-builtin-context = [#context { }].func() +#let typst-builtin-space = [ ].func() + #let is-sequence(it) = { type(it) == content and it.func() == typst-builtin-sequence } +#let is-empty-content(it) = { + it in ([ ], parbreak(), linebreak()) +} + +#let is-styled(it) = { + type(it) == content and it.func() == typst-builtin-styled +} + #let is-metadata(it) = { type(it) == content and it.func() == metadata } @@ -196,6 +214,8 @@ type(it) == content and it.func() == heading and it.depth <= depth } + + // Following utility method is from: // https://github.com/typst-community/linguify/blob/b220a5993c7926b1d2edcc155cda00d2050da9ba/lib/utils.typ#L3 #let if-auto-then(val, ret) = { @@ -214,6 +234,14 @@ } } +#let none-coalesce(value, default) = { + if value == none { + return default + } else { + return value + } +} + // own utils #let padright(array, targetLength) = { @@ -265,3 +293,73 @@ return element.value } } + +#let reconstruct-content(template, body) = { + if template == none or template == auto { + return body + } + + let func = template.func() + + if func == typst-builtin-styled { + return template.func()(body, template.styles) + } // else if func in (emph, smallcaps, sub, super, box, block, hide, heading) { + // return template.func()(body) + // } + else if ( + func + in ( + math.overbrace, + math.underbrace, + math.underbracket, + math.overbracket, + math.underparen, + math.overparen, + math.undershell, + math.overshell, + ) + ) { + return template.func()(body, template.at("annotation", default: none)) + } else if func == pad { + return template.func()( + body, + bottom: template.at("bottom", default: 0%), + top: template.at("top", default: 0%), + left: template.at("left", default: 0%), + right: template.at("right", default: 0%), + rest: template.at("rest", default: 0%), + ) + } else if func == strong { + return template.func()( + body, + delta: template.at("delta", default: 300), + ) + } else if func == highlight { + return template.func()( + body, + extent: template.at("extent", default: 0pt), + fill: template.at("fill", default: rgb("#fffd11a1")), + radius: template.at("radius", default: (:)), + stroke: template.at("stroke", default: (:)), + ) + } else if func in (overline, underline, strike) { + return template.func()( + body, + background: template.at("background", default: false), + extent: template.at("extent", default: 0pt), + offset: template.at("offset", default: auto), + stroke: template.at("stroke", default: auto), + ) + } else if func == math.cancel { + return template.func()( + body, + angle: template.at("angle", default: auto), + cross: template.at("cross", default: false), + inverted: template.at("inverted", default: false), + length: template.at("length", default: 100% + 3pt), + stroke: template.at("stroke", default: 0.5pt), + ) + } else { + return template.func()(body) + } +} diff --git a/tests/README-graphic1/test.typ b/tests/README-graphic1/test.typ index 2c7e2a8..a4995e3 100644 --- a/tests/README-graphic1/test.typ +++ b/tests/README-graphic1/test.typ @@ -1,4 +1,6 @@ -#import "../../src/lib.typ" : ce +#import "../../src/lib.typ": ce #set page(width: auto, height: auto, margin: 0.5em) -$#ce("[Cu(H2O)4]^2+ + 4NH3 -> [Cu(NH3)4]^2+ + 4H2O")$ +$ + #ce("[Cu(H2O)4]^2+ + 4NH3 -> [Cu(NH3)4]^2+ + 4H2O") +$ diff --git a/tests/arrow-align/test.typ b/tests/arrow-align/test.typ index 7c7bf5e..11d8127 100644 --- a/tests/arrow-align/test.typ +++ b/tests/arrow-align/test.typ @@ -1,9 +1,8 @@ -#import "../../src/lib.typ" : ce -//#import "@preview/whalogen:0.3.0": ce +#import "../../src/lib.typ": ce #set page(width: auto, height: auto, margin: 0.5em) $ -#ce("A &-> B")\ -#ce("AAAAAAAAAA &-> BBBB") -$ \ No newline at end of file + #ce("A &-> B")\ + #ce("AAAAAAAAAA &-> BBBB") +$ diff --git a/tests/brackets/test.typ b/tests/brackets/test.typ index 63825a2..b972907 100644 --- a/tests/brackets/test.typ +++ b/tests/brackets/test.typ @@ -1,6 +1,4 @@ -//#import "../../src/lib.typ" : ce -#import "@preview/whalogen:0.3.0": ce -//#import "@preview/typsium:0.2.0": ce +#import "../../src/lib.typ" : ce #set page(width: auto, height: auto, margin: 0.5em) diff --git a/tests/charges/test.typ b/tests/charges/test.typ index 931f744..1b64d1b 100644 --- a/tests/charges/test.typ +++ b/tests/charges/test.typ @@ -1,6 +1,4 @@ -#import "../../src/lib.typ" : ce -//#import "@preview/whalogen:0.3.0": ce - +#import "../../src/lib.typ": ce #set page(width: auto, height: auto, margin: 0.5em) #ce("Fe 3+") diff --git a/tests/content-to-ir/.gitignore b/tests/content-to-ir/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/content-to-ir/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/content-to-ir/ref/1.png b/tests/content-to-ir/ref/1.png new file mode 100644 index 0000000000000000000000000000000000000000..80870671f420cdd2ca724e2037d78df70fa723d8 GIT binary patch literal 1083 zcmV-B1jPG^P)R))<~M!+1b?8)Q9cK=kx9D?V0-G;vzgTuq2I0BuYp~c-WqMdwa{v%S;`!7oPL; z^LO^#-{0@*>MAcUuc)Z#>FGH+IZ=(LXo3VhdhJX8F9?O>LuihDBk(qf_#ArWNdYej zABA)2!+Vm(-`}6hBnrm(P3W#hTNO3$AX z%FlU{L6|^zUYN_YdV6~_SONk9Y;A3YLLt-S@pyhL9v&Wcc6NT(z;fN(+$5HRg9C4G zZ&;0vj?y(Sw#LTBAt50MV=C|zi?2L??~)9k@(9nXKL1^ieV3G#m3ex4c6WF8v6PmU zX0yb`#=5z=WnTk5;YKV1fxy7PU~q7d~J3juFFxBn9N> z=c7Y#aIiW(;pTh|7Z;bKqoZ3q<-?Dmp&>LzMn*^hO-)VcU~X=%GS9DHXi6ki-X*i1 zO0cl7ke;4SH!(Rm2?uaX&NwtQ#PmVvQjKSp5Sxco;d5Be`=PzPopIRO+9Cx^O-(U< z5UA8JZ|?;xX2t;lj1(|1Fu?R#Sy_3gp2x?>`uh5C=@$R`~$r#8;gU5hUNeO002ovPDHLkV1lr` B5HA1# literal 0 HcmV?d00001 diff --git a/tests/content-to-ir/test.typ b/tests/content-to-ir/test.typ new file mode 100644 index 0000000..ead8458 --- /dev/null +++ b/tests/content-to-ir/test.typ @@ -0,0 +1,14 @@ +#import "../../src/parse-content-intermediate-representation.typ": content-to-ir +#import "../../src/display-intermediate-representation.typ": display-ir +#set page(width: auto, height: auto, margin: 0.5em) + +#let x = ( + ( + type: "molecule", + children: ( + (type: "element", symbol: "H", count: 2, charge:2, symbol-body:text(red)[H], count-body:strike("gugugaga"), charge-body:strike("gugugaga")), + (type: "element", symbol: "O"), + ), + ), +) +#display-ir(x) \ No newline at end of file diff --git a/tests/get-element/test.typ b/tests/get-element/test.typ index baff382..36e76ee 100644 --- a/tests/get-element/test.typ +++ b/tests/get-element/test.typ @@ -1,4 +1,4 @@ -#import "../../src/lib.typ" : get-element +#import "../../src/lib.typ": get-element #let iron = get-element(symbol: "Fe") #let hydrogen = get-element(common-name: "Hydrogen") diff --git a/tests/intermediate-representation-molecules/test.typ b/tests/intermediate-representation-molecules/test.typ index c8fb9d2..62d4be3 100644 --- a/tests/intermediate-representation-molecules/test.typ +++ b/tests/intermediate-representation-molecules/test.typ @@ -1,90 +1,90 @@ -#import "../../src/display-intermediate-representation.typ" : display-ir +#import "../../src/display-intermediate-representation.typ": display-ir #set page(width: auto, height: auto, margin: 0.5em) #let co2 = ( - type:"molecule", - count:1, - phase:"g", - charge:0, - align:none, - arrow:none, - children:( + type: "molecule", + count: 1, + phase: "g", + charge: 0, + align: none, + arrow: none, + children: ( ( - type:"element", - count:1, - symbol:"C", - charge:0, - oxidation-number:none, - isotope:none, - align:none, + type: "element", + count: 1, + symbol: "C", + charge: 0, + oxidation-number: none, + isotope: none, + align: none, ), ( - type:"element", - count:2, - symbol:"O", - charge:0, - oxidation-number:none, - isotope:none, - align:none, - ) - ) + type: "element", + count: 2, + symbol: "O", + charge: 0, + oxidation-number: none, + isotope: none, + align: none, + ), + ), ) #let hexacyanidoferrat = ( - type:"molecule", - count:3, - phase:"s", - charge:0, - align:none, - arrow:none, - children:( + type: "molecule", + count: 3, + phase: "s", + charge: 0, + align: none, + arrow: none, + children: ( ( - type:"group", - count:2, - kind:1, - charge:4, - align:none, - children:( + type: "group", + count: 2, + kind: 1, + charge: 4, + align: none, + children: ( ( - type:"element", - count:1, - symbol:"Fe", - charge:0, - oxidation-number:none, - isotope:none, - align:none, + type: "element", + count: 1, + symbol: "Fe", + charge: 0, + oxidation-number: none, + isotope: none, + align: none, ), ( - type:"group", - count:6, - kind:0, - charge:0, - align:none, - children:( + type: "group", + count: 6, + kind: 0, + charge: 0, + align: none, + children: ( ( - type:"element", - count:1, - symbol:"C", - charge:0, - oxidation-number:none, - isotope:none, - align:none, + type: "element", + count: 1, + symbol: "C", + charge: 0, + oxidation-number: none, + isotope: none, + align: none, ), ( - type:"element", - count:1, - symbol:"N", - charge:0, - oxidation-number:none, - isotope:none, - align:none, + type: "element", + count: 1, + symbol: "N", + charge: 0, + oxidation-number: none, + isotope: none, + align: none, ), - ) + ), ), - ) + ), ), - ) + ), ) #display-ir(co2)\ -#display-ir(hexacyanidoferrat) \ No newline at end of file +#display-ir(hexacyanidoferrat) diff --git a/tests/intermediate-representation-reactions/test.typ b/tests/intermediate-representation-reactions/test.typ index c7e294d..9e0de1e 100644 --- a/tests/intermediate-representation-reactions/test.typ +++ b/tests/intermediate-representation-reactions/test.typ @@ -1,38 +1,38 @@ -#import "../../src/display-intermediate-representation.typ" : display-ir +#import "../../src/display-intermediate-representation.typ": display-ir #set page(width: auto, height: auto, margin: 0.5em) #let reaction1 = ( ( type: "molecule", - charge:2, - children:( + charge: 2, + children: ( ( - type:"group", - kind:1, - children:( + type: "group", + kind: 1, + children: ( ( - type:"element", - symbol:"Cu", + type: "element", + symbol: "Cu", ), ( - type:"group", - kind:0, - count:4, - children:( + type: "group", + kind: 0, + count: 4, + children: ( ( - type:"element", - count:2, - symbol:"H", + type: "element", + count: 2, + symbol: "H", ), ( - type:"element", - symbol:"O", + type: "element", + symbol: "O", ), - ) + ), ), - ) + ), ), - ) + ), ), (type: "align"), ( @@ -43,104 +43,104 @@ ), ( type: "molecule", - charge:2, - children:( + charge: 2, + children: ( ( - type:"group", - kind:1, - children:( + type: "group", + kind: 1, + children: ( ( - type:"element", - symbol:"Cu", + type: "element", + symbol: "Cu", ), ( - type:"group", - kind:0, - count:4, - children:( + type: "group", + kind: 0, + count: 4, + children: ( ( - type:"element", - symbol:"N", + type: "element", + symbol: "N", ), ( - type:"element", - count:3, - symbol:"H", + type: "element", + count: 3, + symbol: "H", ), - ) + ), ), - ) + ), ), - ) + ), ), (type: "+"), ( type: "molecule", - count:4, - children:( + count: 4, + children: ( ( - type:"element", - count:2, - symbol:"H", + type: "element", + count: 2, + symbol: "H", ), ( - type:"element", - symbol:"O", + type: "element", + symbol: "O", ), - ) - ) + ), + ), ) #let reaction2 = ( ( type: "molecule", - charge:2, - children:( + charge: 2, + children: ( ( - type:"group", - kind:1, - children:( + type: "group", + kind: 1, + children: ( ( - type:"element", - symbol:"Cu", + type: "element", + symbol: "Cu", ), ( - type:"group", - kind:0, - count:4, - children:( + type: "group", + kind: 0, + count: 4, + children: ( ( - type:"element", - count:2, - symbol:"H", + type: "element", + count: 2, + symbol: "H", ), ( - type:"element", - symbol:"O", + type: "element", + symbol: "O", ), - ) + ), ), - ) + ), ), - ) + ), ), ( - type:"+" + type: "+", ), ( type: "molecule", - count:4, - children:( + count: 4, + children: ( ( - type:"element", - symbol:"N", + type: "element", + symbol: "N", ), ( - type:"element", - count:3, - symbol:"H", + type: "element", + count: 3, + symbol: "H", ), - ) + ), ), (type: "align"), ( @@ -148,49 +148,49 @@ kind: 1, top: ( ( - type:"content", - body:[dissolve in ] + type: "content", + body: [dissolve in ], ), ( type: "molecule", - children:( + children: ( ( - type:"element", - count:2, - symbol:"H", + type: "element", + count: 2, + symbol: "H", ), ( - type:"element", - symbol:"O", + type: "element", + symbol: "O", ), - ) + ), ), ), bottom: ( ( - type:"content", - body:$Delta H^0$ + type: "content", + body: $Delta H^0$, ), ), ), ( type: "molecule", - count:4, - children:( + count: 4, + children: ( ( - type:"element", - count:2, - symbol:"H", + type: "element", + count: 2, + symbol: "H", ), ( - type:"element", - symbol:"O", + type: "element", + symbol: "O", ), - ) + ), ), ) $ #display-ir(reaction1)\ #display-ir(reaction2)\ -$ \ No newline at end of file +$ diff --git a/tests/parse-ir-elements/test.typ b/tests/parse-ir-elements/test.typ index 0b48ce8..15bebff 100644 --- a/tests/parse-ir-elements/test.typ +++ b/tests/parse-ir-elements/test.typ @@ -1,63 +1,63 @@ -#import "../../src/parse-formula-intermediate-representation.typ" : molecule-string-to-ir -#import "../../src/lib.typ" : display-ir +#import "../../src/parse-formula-intermediate-representation.typ": molecule-string-to-ir +#import "../../src/lib.typ": display-ir #set page(width: auto, height: auto, margin: 0.5em) #let co2 = ( - type: "molecule", - children: ( - (type: "element", symbol: "C"), - (type: "element", symbol: "O", count: 2), - ), - ) + type: "molecule", + children: ( + (type: "element", symbol: "C"), + (type: "element", symbol: "O", count: 2), + ), +) #let ir-co2 = molecule-string-to-ir("CO2") #let no = ( - type: "molecule", - children: ( - (type: "element", symbol: "N"), - ( - type: "element", - symbol: "O", - charge: -2, - radical: true, - ), - ), - ) + type: "molecule", + children: ( + (type: "element", symbol: "N"), + ( + type: "element", + symbol: "O", + charge: -2, + radical: true, + ), + ), +) #let ir-no = molecule-string-to-ir("NO^2.-") #let na = ( - type: "molecule", - children: ( - (type: "element", symbol: "Na", count: 3, charge: 1), - ), - ) + type: "molecule", + children: ( + (type: "element", symbol: "Na", count: 3, charge: 1), + ), +) #let ir-na1 = molecule-string-to-ir("Na_3^+") #let ir-na2 = molecule-string-to-ir("Na_3^+") #let cl = ( - type: "molecule", - children: ( - ( - type: "element", - symbol: "Cl", - count: 2, - charge: -1, - ), - ), - ) + type: "molecule", + children: ( + ( + type: "element", + symbol: "Cl", + count: 2, + charge: -1, + ), + ), +) #let ir-cl = molecule-string-to-ir("Cl2-1") #let fe = ( - type: "molecule", - children: ( - ( - type: "element", - symbol: "Fe", - count: 2, - charge: "III", - ), - ), - ) + type: "molecule", + children: ( + ( + type: "element", + symbol: "Fe", + count: 2, + charge: "III", + ), + ), +) #let ir-fe = molecule-string-to-ir("Fe2^III") #display-ir(ir-co2) @@ -76,4 +76,4 @@ #assert(na == ir-na1) #assert(na == ir-na2) #assert(cl == ir-cl) -#assert(fe == ir-fe) \ No newline at end of file +#assert(fe == ir-fe) diff --git a/tests/parse-ir-groups/test.typ b/tests/parse-ir-groups/test.typ index b171a3d..5b4c163 100644 --- a/tests/parse-ir-groups/test.typ +++ b/tests/parse-ir-groups/test.typ @@ -1,43 +1,43 @@ -#import "../../src/parse-formula-intermediate-representation.typ" : molecule-string-to-ir +#import "../../src/parse-formula-intermediate-representation.typ": molecule-string-to-ir #let trisethylendiamin = ( - type: "molecule", - children: ( - ( - type: "group", - kind: 1, - children: ( - (type: "element", symbol: "Co"), - ( - type: "group", - kind: 0, - children: ((type: "content", body: [en]),), - count: 3, - ), - ), - ), - (type: "element", symbol: "Cl", count: 3), - ), - ) + type: "molecule", + children: ( + ( + type: "group", + kind: 1, + children: ( + (type: "element", symbol: "Co"), + ( + type: "group", + kind: 0, + children: ((type: "content", body: [en]),), + count: 3, + ), + ), + ), + (type: "element", symbol: "Cl", count: 3), + ), +) #let ir-trisethylendiamin = molecule-string-to-ir("[Co(en)3]Cl3") #let fenh3 = ( - type: "molecule", - children: ( - (type: "element", symbol: "Fe"), - ( - type: "group", - kind: 1, - children: ( - (type: "element", symbol: "N"), - (type: "element", symbol: "H", count: 3), - ), - count: 2, - charge: 1, - ), - ), - ) + type: "molecule", + children: ( + (type: "element", symbol: "Fe"), + ( + type: "group", + kind: 1, + children: ( + (type: "element", symbol: "N"), + (type: "element", symbol: "H", count: 3), + ), + count: 2, + charge: 1, + ), + ), +) #let ir-fenh3 = molecule-string-to-ir("Fe[NH3]2+") #assert(trisethylendiamin == ir-trisethylendiamin) -#assert(fenh3 == ir-fenh3) \ No newline at end of file +#assert(fenh3 == ir-fenh3) diff --git a/tests/shell-configuration/test.typ b/tests/shell-configuration/test.typ index f0d7e80..7ccdbb0 100644 --- a/tests/shell-configuration/test.typ +++ b/tests/shell-configuration/test.typ @@ -1,8 +1,12 @@ -#import "../../src/display-shell-configuration.typ" : get-shell-configuration, display-electron-configuration, get-electron-configuration -#import "../../src/lib.typ" : get-element +#import "../../src/display-shell-configuration.typ": ( + get-shell-configuration, + display-electron-configuration, + get-electron-configuration, +) +#import "../../src/lib.typ": get-element #set page(width: auto, height: auto, margin: 0.5em) -#let carbon = get-element(symbol:"Y") +#let carbon = get-element(symbol: "Y") #let shells = get-shell-configuration(carbon) #let orbitals = get-electron-configuration(carbon) #display-electron-configuration(carbon) diff --git a/tests/simple-formulas/test.typ b/tests/simple-formulas/test.typ index 10ef719..bd6fc65 100644 --- a/tests/simple-formulas/test.typ +++ b/tests/simple-formulas/test.typ @@ -1,5 +1,4 @@ -#import "../../src/lib.typ" : ce -//#import "@preview/whalogen:0.3.0": ce +#import "../../src/lib.typ": ce #set page(width: auto, height: auto, margin: 0.5em) From 61dc6de057a46e761ab50109e6859800231a6083 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Tue, 27 May 2025 02:41:21 +0200 Subject: [PATCH 02/20] add elembic v0.0.1-alpha3 --- src/libs/elembic/.DS_Store | Bin 0 -> 6148 bytes src/libs/elembic/data.typ | 545 ++++ src/libs/elembic/element.typ | 3875 ++++++++++++++++++++++++++++ src/libs/elembic/fields.typ | 364 +++ src/libs/elembic/lib.typ | 10 + src/libs/elembic/pub/constants.typ | 1 + src/libs/elembic/pub/data.typ | 1 + src/libs/elembic/pub/element.typ | 2 + src/libs/elembic/pub/filters.typ | 1 + src/libs/elembic/pub/leaky.typ | 8 + src/libs/elembic/pub/native.typ | 2 + src/libs/elembic/pub/parsing.typ | 1 + src/libs/elembic/pub/stateful.typ | 8 + src/libs/elembic/pub/types.typ | 5 + src/libs/elembic/types/base.typ | 582 +++++ src/libs/elembic/types/custom.typ | 409 +++ src/libs/elembic/types/native.typ | 341 +++ src/libs/elembic/types/types.typ | 405 +++ 18 files changed, 6560 insertions(+) create mode 100644 src/libs/elembic/.DS_Store create mode 100644 src/libs/elembic/data.typ create mode 100644 src/libs/elembic/element.typ create mode 100644 src/libs/elembic/fields.typ create mode 100644 src/libs/elembic/lib.typ create mode 100644 src/libs/elembic/pub/constants.typ create mode 100644 src/libs/elembic/pub/data.typ create mode 100644 src/libs/elembic/pub/element.typ create mode 100644 src/libs/elembic/pub/filters.typ create mode 100644 src/libs/elembic/pub/leaky.typ create mode 100644 src/libs/elembic/pub/native.typ create mode 100644 src/libs/elembic/pub/parsing.typ create mode 100644 src/libs/elembic/pub/stateful.typ create mode 100644 src/libs/elembic/pub/types.typ create mode 100644 src/libs/elembic/types/base.typ create mode 100644 src/libs/elembic/types/custom.typ create mode 100644 src/libs/elembic/types/native.typ create mode 100644 src/libs/elembic/types/types.typ diff --git a/src/libs/elembic/.DS_Store b/src/libs/elembic/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..4462432e9cf5017b0e924c09346d8594e47b64b5 GIT binary patch literal 6148 zcmeHK!A`?447J&iDskD7V~$++2Vp8-updBW2o9@SY1@HYK7p^|a}W|gz;kRw0)-<& z%$Abp#CF`is!2>lygF=_L<=Hn&;(hO36beZ(}_8Yk!6kL`ex|Ii6#E-lsx;4MmkWp zy(^!;nSWUNp>4bE&|wDt>Gk5}>9VwLez1=B<*JVeqf^?`19INduFmf8{atQ#G%W@Xz zQcFloFpP#t5f%t*C{RP$N(|O;%m?#}hDlMwiLLlx%luxvaL$hPLv|;QirzZ|&Opt; zOot0O|F7`NOcwcdh>x5BXW*YP!1H$9uJBTJwtjg%Ico#jHJXU{B~c*It4ja|vX7i+ dquPV$@Qa2?QC5-p91ir0KqkaHXW$nYcmtAaLw5iG literal 0 HcmV?d00001 diff --git a/src/libs/elembic/data.typ b/src/libs/elembic/data.typ new file mode 100644 index 0000000..c48ae5c --- /dev/null +++ b/src/libs/elembic/data.typ @@ -0,0 +1,545 @@ +// Functions to extract data from custom elements and types, as well as associated constants. + +// Type constants: + +// Used by typeinfos +#let type-key = "__elembic_type" +// To be used by any custom type instances +#let custom-type-key = "__elembic_custom_type" +// Used by custom types themselves +#let custom-type-data-key = "__elembic_custom_type_data" + +// Versions: +#let element-version = 2 // v1 = alphas 1 and 2, v2 = alpha 3+ +#let type-version = 1 +#let custom-type-version = 2 // v1 = alphas 1 and 2, v2 = alpha 3+ +#let current-field-version = 2 // v1 = alphas 1 and 2, v2 = alpha 3+ + +// Potential modes for configuration of styles. +// This defines how we declare a set rule (or similar) +// within a certain scope. +#let style-modes = ( + // Normal mode: we store metadata in a bibliography.title set rule. + // + // Before doing so, we retrieve the original value for bibliography.title, + // allowing us to restore it later. The effect is that the library is + // fully hygienic, that is, the change to bibliography.title is not perceptible. + // + // The downside is that retrieving the original value for bibliography.title costs + // an additional nested context { } call, of which there is a limit of 64. This means + // that, in this mode, you can have up to 32 non-consecutive set rules. + normal: 0, + + // leaky mode: similar to normal mode, but we don't try to preserve the value of bibliography.title + // after applying our changes to the document. This doubles the limit to up to 64 non-consecutive + // set rules since we no longer have an extra step to retrieve the old value, but, as a downside, + // we lose the original value of bibliography.title. While, in a future change, we might be able to + // preserve the FIRST known value, we can't generally preserve its value at later points, so the + // value of bibliography.title is effectively frozen before the first custom set rule. + // + // This mode should be used by package authors which know there won't be a bibliography (or, really, + // any custom user input) at some point to avoid consuming the set rule cost. End users can also use + // this mode if they hit a "max show rule depth exceeded" error. + // + // Note that this mode can only be enabled on individual set rules. + leaky: 1, + + // Stateful mode: this is entirely different from the other modes and should only be set by the end + // user (not by packages). This stores the style chain - and, thus, set rules' updated fields - in + // a 'state()'. This is more likely to be slower and lead to trouble as it triggers at least one + // document relayout. However, **this mode does not have a set rule limit.** Therefore, it can be + // used as a last resort by the end user if they can't fix the "max show rule depth exceeded error". + // + // Enabling this mode is as simple as using `#show: e.stateful.toggle(true)` at the beginning of the + // document. This will trigger a compatibility behavior where existing set rules will push to the + // state, even if they're not in the stateful mode. It will also push existing set rule data into + // the style 'state()'. Therefore, existing set rules are compatible with stateful mode, but this + // only effectively fixes the error if the set rules are individually switched to stateful mode + // with `e.stateful.set_` instead of `e.set_`. + stateful: 2 +) + + +// When on stateful mode, this state holds the sequence of 'data' for each scope. +// The last element on the list is the "current" data. +#let style-state-key = "__elembic_element_state" +#let style-state = state(style-state-key, ()) + +// Element constants: + +// Prefix for the labels added to shown elements. +#let lbl-show-head = "__elembic_element_shown_" + +// Prefix for the labels added to the metadata of each element. +// Used for querying. +#let lbl-meta-head = "__elembic_element_meta_" + +// Prefix for the labels added outside shown elements. +// This is used to be able to effectively apply show-set rules to them. +#let lbl-outer-head = "__elembic_element_outer_" + +// Prefix for counters of elements. +// This is only used if the element isn't 'refable'. +#let lbl-counter-head = "__elembic_element_counter_" + +// Prefix for the figure kind used by 'refable' elements. +// This is not to be confused with figures containing the elements. +// This is the kind for a hidden figure used for ref purposes. +#let lbl-ref-figure-kind-head = "__elembic_element_refable_" + +// Custom label applied to the hidden reference figure when the user specifies their own label. +#let lbl-ref-figure-label-head = "__elembic_element_ref_figure_label_" + +// Label for the hidden figure used for references. +#let lbl-ref-figure = <__elembic_element_ref_figure> + +// Label for context blocks which have access to the virtual stylechain. +#let lbl-get = <__elembic_element_get> + +// Label for metadata indicating an element's initial properties post-construction. +#let lbl-tag = <__elembic_element_tag> + +// Label for metadata indicating a rule's parameters. +#let lbl-rule-tag = <__elembic_element_rule_v2> + +// 'lbl-rule-tag' from older Elembic versions. +#let lbl-old-rule-tag = <__elembic_element_rule> + +// Label for other functions which access or modify the style chain, namely +// 'get', 'select', 'debug-get', 'stateful.toggle'. +// +// This is attached to metadata and the 'special-rule-key' property +// indicates which kind of rule this is. +#let lbl-special-rule-tag = <__elembic_element_special_rule> + +// Label for metadata which stores the global data. +// In practice, this label is never present in the document +// unless one accidentally leaks the 'bibliography.title' +// override from our workaround. +#let lbl-data-metadata = <__elembic_element_global_data_metadata> + +#let lbl-stateful-mode = <__elembic_element_stateful_mode> +#let lbl-normal-mode = <__elembic_element_normal_mode> +#let lbl-leaky-mode = <__elembic_element_leaky_mode> +#let lbl-auto-mode = <__elembic_element_auto_mode> + +// Prefix for labels added by 'select' to matched elements. +// These labels are not specific to eids. +#let lbl-global-select-head = "__elembic_element_global_where_" + +// Special dictionary key to indicate this is a prepared rule. +#let prepared-rule-key = "__elembic-prepared-rule" + +// Special dictionary key to indicate this is a "special rule" +// ('get', 'select', 'debug-get', 'stateful.toggle'). +#let special-rule-key = "__elembic-special-rule" + +// Special dictionary key which stores element context and other data. +#let stored-data-key = "__elembic_stored_element_data" + +// Special dictionary key to indicate this is query metadata for an element. +#let element-meta-key = "__elembic_element_meta" + +#let element-key = "__elembic_element" +#let element-data-key = "__elembic_element_data" +#let global-data-key = "__elembic_element_global_data" +#let filter-key = "__elembic_element_filter" + +#let sequence = [].func() + +// Special values that can be passed to a type or element's constructor to retrieve some data or show +// some behavior. +#let special-data-values = ( + // Indicate that the constructor should return the type or element's data. + get-data: 0, + // Indicate that the constructor should return an element filter. + get-where: 1, +) + +// Extract data from a type's or element's constructor, as well as convert +// a custom type or element instance into a dictionary with keys such as body (for elements only), +// fields and func, allowing you to access its fields and information when given content (for elements) +// or the type instance (for types). +// +// When given content that is not a custom element, 'body' will be the given value, +// 'fields' will be 'body.fields()' and 'func' will be 'body.func()'. +// +// The returned 'data-kind' indicates which kind of data was retrieved. +#let data(it) = { + if type(it) == function { + it(__elembic_data: special-data-values.get-data) + } else if type(it) == dictionary and element-key in it { + (data-kind: "element", ..it) + } else if type(it) == dictionary and custom-type-data-key in it { + (data-kind: "custom-type-data", ..it) + } else if type(it) == dictionary and custom-type-key in it { + it.at(custom-type-key) + } else if type(it) == dictionary and stored-data-key in it { + it.at(stored-data-key) + } else if type(it) != content { + (data-kind: "unknown", body: it, fields: (:), func: none, eid: none, fields-known: false, valid: false) + } else if ( + it.has("label") + and str(it.label).starts-with(lbl-show-head) + ) { + // Decomposing an element inside a show rule + it.children.at(1).value + } else if it.func() == sequence and it.children.len() >= 2 { + let last = it.children.last() + if ( + last.at("label", default: none) == lbl-tag + // Workaround for 0.11.0 weirdly placing some 'meta' element sometimes + or sys.version < version(0, 12, 0) and { + last = it.children.at(it.children.len() - 2) + last.at("label", default: none) == lbl-tag + } + ) { + // Decomposing a recently-constructed (but not placed) element + last.value + } else { + (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) + } + } else if ( + it.has("label") + and str(it.label).starts-with(lbl-outer-head) + ) { + (data-kind: "incomplete-element-instance", body: it, fields: (:), func: (:), eid: str(it.label).slice(lbl-outer-head.len()), fields-known: false, valid: false) + } else { + (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) + } +} + +// Obtain the fields of a type instance or element instance (custom or not). +// +// SAMPLE USAGE: +// +// #show e.selector(elem): it => { +// let fields = e.fields(it) +// [Hello #fields.name!] +// } +#let fields(it) = { + let info = data(it) + + if type(info) == dictionary and "data-kind" in info { + if info.data-kind in ("content", "element-instance", "type-instance") { + return info.fields + } + } + + (:) +} + +// Obtain context at an element's site. +// +// SAMPLE USAGE: +// +// 1. In show rules: +// +// #show e.selector(elem): it => { +// let (get, ..) = e.ctx(it) +// let other-elem-ctx = get(other-elem) +// [The other element field was set to #other-elem-ctx.field at that point!] +// } +// +// 2. In element declarations: +// +// #e.element.declare( +// ... +// synthesize: it => { +// // Get context for other element +// it.some-field = (e.ctx(it).get)(other-elem).field +// }, +// ... +// ) +#let ctx(it) = { + let info = data(it) + if type(info) == dictionary and "ctx" in info { + info.ctx + } else { + none + } +} + +// Obtain an element's or type's scope (usually a module with important definitions). +// +// SAMPLE USAGE: +// +// #let subelem = e.scope(elem).subelem +#let scope(it) = { + let info = data(it) + if type(info) == dictionary and "scope" in info { + info.scope + } else { + none + } +} + +/// Obtain an element's or custom type's constructor function. +/// For native elements, this will be equivalent to `it.func()`. +/// +/// Useful in custom element show rules, for example. +/// +/// This is equivalent to `e.data(it).func`. +/// +/// SAMPLE USAGE: +/// +/// ```typ +/// #show selector.or(e.selector(elem1), e.selector(elem2)): it => { +/// // Will be either elem1 or elem2 +/// let elem = e.func(it) +/// // 'elem == elem1' works, but comparing 'eid's is recommended +/// if e.eid(elem) == e.eid(elem1) { +/// [This is elem1] +/// } else { +/// [This is elem2] +/// } +/// } +/// ``` +/// +/// - it (any): element/custom type instance (or element/custom type itself) to get the constructor from +/// -> function | none +#let func(it) = { + let info = data(it) + if type(info) == dictionary and "func" in info { + info.func + } else { + none + } +} + +/// Obtain an element's eid. It uniquely distinguishes this element from others, +/// even if they have the same name, by including both its prefix and name. +/// +/// This is equivalent to `e.data(elem).eid`. +/// +/// - elem (any): custom element (or an instance of it) to get 'eid' from +/// -> function | none +#let eid(it) = { + let info = data(it) + if type(info) == dictionary and "eid" in info { + info.eid + } else { + none + } +} + +/// Obtain a custom type's tid. It uniquely distinguishes a custom type from +/// others, even if they have the same name, by including both its prefix and +/// name. Returns `none` on invalid input. +/// +/// This is equivalent to `e.data(typ).tid`. +/// +/// - typ (any): custom type (or an instance of it) to get 'tid' from +/// -> function | none +#let tid(it) = { + let info = data(it) + if type(info) == dictionary and "tid" in info { + info.tid + } else { + none + } +} + +// Obtain an element's counter. +// +// USAGE: +// +// #context { +// [The element counter value is #e.counter(elem).get().first()] +// } +#let counter_(elem) = { + let info = data(elem) + + if type(info) == dictionary and "data-kind" in info and (info.data-kind == "element" or info.data-kind == "element-instance") { + info.counter + } else { + assert(false, message: "elembic: e.counter: this is not an element") + } +} + +/// Get the name of a content's constructor function as a string. +/// +/// Returns 'none' on invalid input. +/// +/// USAGE: +/// +/// ```typ +/// assert.eq(func-name(my-elem()), "my-elem") +/// assert.eq(func-name([= abc]), "heading") +/// ``` +/// +/// - c (content): content to get the constructor function of +/// -> function | none +#let func-name(c) = { + if type(c) == function { + let func-data = data(c) + return if "name" in func-data { + func-data.name + } else { + none + } + } else if type(c) != content { + return none + } + let name = repr(c.func()) + if c.func() == sequence { + let element-data = data(c) + if "eid" in element-data and element-data.eid != none { + name = if "name" in element-data and type(element-data.name) == str { element-data.name } else { "unknown custom element" } + } + } + name +} + +#let _letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" + +/// This is used to obtain a debug representation of custom elements and types. +/// +/// Also supports native types (just calls `repr()` for them). +/// +/// - value (any): value to represent +/// - depth (int): current depth (must start at 0, conservative limit of 10 for now) +/// -> str +#let repr_(value, depth: 0) = { + if depth >= 10 { + return repr(value) + } + let typename = "" + let value-type = type(value) + if value-type == content and value.func() == sequence { + let value-data = data(value) + if "eid" in value-data and value-data.eid != none { + value = value-data.fields + value-type = dictionary + typename = if "name" in value-data and type(value-data.name) == str { + value-data.name + } else { + "unknown-element" + } + } + } + + if value-type == dictionary { + let pairs = if typename != "" { + // Element fields => sort + value.pairs().sorted(key: ((k, _)) => k) + } else if custom-type-key in value { + let type-data = value.at(custom-type-key) + + let id = type-data.id + + typename = if "name" in id { + id.name + } else if id == "custom type" { + return if custom-type-data-key in value { + "custom-type(name: " + repr(value.name) + ", tid: " + repr(value.tid) + ")" + } else { + "custom-type()" + } + } else { + str(id) + } + + type-data.fields.pairs().sorted(key: ((k, _)) => k) + } else { + value.pairs() + } + + typename + "(" + pairs.map(((k, v)) => { + if k.codepoints().all(c => c in _letters) { + k + } else { + repr(k) + } + + ": " + + repr_(v, depth: depth + 1) + }).join(", ") + ")" + } else if value-type == array { + "(" + value.map(repr_.with(depth: depth + 1)).join(", ") + ")" + } else { + repr(value) + } +} + +/// Performs deep equality of values. +/// +/// This is necessary to reliably compare instances of the same element or +/// custom type, as well as data structures containing them such as arrays +/// or dictionary, between different versions of the same element or type, +/// by recursively comparing `eid(a) == eid(b) and fields(a) == fields(b)`. +/// However, this is notably slower than Typst's built-in equality check. +/// +/// - a (any): First value to compare. +/// - b (any): Second value to compare. +/// -> bool +#let eq(a, b) = { + if a == b { + return true + } + if type(a) != type(b) or type(a) not in (content, dictionary, array) { + return false + } + + // Recursively compare until we find a 'false' + let stack = ((a, b),) + let fuel = 3000 + while stack != () { + let (a, b) = stack.pop() + if a == b { + // Good! + continue + } + fuel -= 1 + if fuel == 0 { + return false + } + // Of course, the types must match + let a-type = type(a) + let b-type = type(b) + if a-type != b-type { + return false + } + + if a-type == array { + if a.len() != b.len() { + return false + } + stack += array.zip(a, b) + + // Only have special checks for composed types and custom types and elements + // of same type + } else if (a-type == content or a-type == dictionary) and eid(a) == eid(b) and tid(a) == tid(b) { + if eid(a) != none or tid(a) != none or a-type == content and a.func() == b.func() { + // Same element id, compare their fields + a = fields(a) + b = fields(b) + a-type = type(a) + if a-type != type(b) { + return false + } + } + if a-type != dictionary or a.len() != b.len() { + // Fields were invalid, or content didn't have the same func + return false + } + for (key, a-val) in a { + if key not in b { + return false + } + stack.push((a-val, b.at(key),)) + } + } else { + return false + } + } + + // No checks failed + true +} diff --git a/src/libs/elembic/element.typ b/src/libs/elembic/element.typ new file mode 100644 index 0000000..d95b8fc --- /dev/null +++ b/src/libs/elembic/element.typ @@ -0,0 +1,3875 @@ +#import "data.typ": data, lbl-show-head, lbl-meta-head, lbl-outer-head, lbl-counter-head, lbl-ref-figure-kind-head, lbl-ref-figure-label-head, lbl-ref-figure, lbl-get, lbl-tag, lbl-rule-tag, lbl-old-rule-tag, lbl-special-rule-tag, lbl-data-metadata, lbl-stateful-mode, lbl-leaky-mode, lbl-normal-mode, lbl-auto-mode, lbl-global-select-head, prepared-rule-key, stored-data-key, element-key, element-data-key, element-meta-key, global-data-key, filter-key, special-rule-key, special-data-values, custom-type-key, custom-type-data-key, type-key, element-version, style-modes, style-state +#import "fields.typ" as field-internals +#import "types/base.typ" +#import "types/types.typ" + +// Basic elements for our document tree analysis +#let sequence = [].func() +#let space = [ ].func() +#let styled = { set text(red); [a] }.func() +#let state-update-func = state(".").update(1).func() +#let counter-update-func = counter(".").update(1).func() + +// Default library-wide data. +#let default-global-data = ( + (global-data-key): true, + + // Keep track of versions in case we need some backwards-compatibility behavior + // in the future. + version: element-version, + + // If the style state should be read by set rules as the user has + // enabled stateful mode with `#shoW: e.stateful.toggle(true)`. + stateful: false, + + // First known bib title. + // This is used by leaky mode to attempt to preserve the correct bibliography.title + // property. Evidently, it's not perfect, and leaky mode should be avoided. + first-bib-title: (), + + // Identical to 'global.select-count', this is only here for compatibility + // with older elements. + where-rule-count: 0, + + // Some global settings changeable through set rules. + settings: ( + // Whether non-stateful rules should default to leaky mode. + prefer-leaky: false, + + // Additional elements for which ancestry should be tracked. + // Setting this to 'any' will enable ancestry tracking for all elements + // (POTENTIALLY SLOW!). + track-ancestry: (:), + + // Additional elements which should support ancestry-related filters when + // queried. + store-ancestry: (:), + ), + + // Shared state between elements. + // Differently from settings, this is not meant to be configurable by users. + global: ( + // Version that created the default global data. + version: element-version, + + // Amount of select rules in the style chain so far. + // Used to apply a unique label. + select-count: 0, + + // Current element ancestors, from outermost to innermost. + ancestry-chain: (), + ), + + // Per-element data (set rules and other style chain info). + elements: (:), +) + +// Default per-element data. +#let default-data = ( + (element-data-key): true, + + version: element-version, + + // Chain for foldable fields, that is, fields which have special behavior + // when changed through more than one set rule. By default, specifying the + // same field in two subsequent set rules will have the innermost set rule + // override the value from the previous one, but this can be overridden + // for certain types where it makes sense to combine the two values in + // some way instead. For example, stroke fields have custom folding: if + // you specify 4pt for a stroke field in one set rule and orange in another, + // the final stroke will be 4pt + orange, not orange. + // + // This data structure has an entry for each changed foldable field, laid out as follows: + // ( + // foldable-field-name: ( + // folder: auto or (outer, inner) => combined value // how to combine two values, auto = simple sum, equivalent to (a, b) => a + b + // default: stroke(), // default value for this field to begin folding. This is 'field.default' unless 'required = true'. + // // Then, it is the type's default. + // values: (4pt, orange, ...) // list of all set values for this field (length = amount of times this field was changed) + // // only 'values' is used if possible, for efficiency. E.g.: values.sum(default: stroke()) + // data: ( // list to associate each value with the real style chain index and name. + // (index: 3, name: none, names: (), value: 4pt), // If 'revoke' or 'reset' are used, this list is used instead + // (index: 5, name: none, names: (), value: orange), // so we can know which values were revoked. + // ... + // ) + // ), + // ... + // ) + // + // The final argument passed to the constructor, if any, also has to be folded with the latest folded value, + // or with the field's default value if nothing was changed. However, that step is done separately. So, if + // no set rules change a particular foldable field, it is not present in this dictionary at all. + fold-chain: (:), + + // The current accumulated styles (overridden values for arguments) for the element. + chain: (), + + // Maps each style in the chain to some data. + // This is used to assign names to styles, so they can be revoked later. + data-chain: (), + + // All known names, so we can be aware of invalid revoke rules. + names: (:), + + // List of active revokes, of the form: + // (index: last-chain-index, revoking: name-revoked, name: none / name of the revoke itself) + revoke-chain: (), + + // This is set to true when a rule with '.within(eid)' is used. + track-ancestry: false, + + // Data for filtering. + filters: ( + // Filters applying to this element. Each filter is associated with a rule below. + // While some filters might trivially apply to all instances of an element, + // others might require specific field values to match, for example. + all: (), + + // This is an array of rules to apply for each filter in the same index. + // These rules are applied whenever an element matching the given filter + // is found. + rules: (), + + // Data associated with each filter, such as its name(s). + // Format: + // ((names: ("name1", "name2", ...)), ...) + data: (), + ), + + // Conditional set rules, which only apply to matching instances of an + // element. + cond-sets: ( + // Filters to apply to each set rule. + filters: (), + // For each filter above, the associated set rule args with the changed + // fields. + args: (), + // Data associated with each conditional set rule. + // Format: + // ((names: ("name1", "name2", ...), index: 0), ...) + data: (), + ), + + // Show rules that might apply to this element. + show-rules: ( + // Filters for each show rule. + filters: (), + // For each filter above, the associated show rule function. + callbacks: (), + // Data associated with each show rule. + // Format: + // ((names: ("name1", "name2", ...), index: 0), ...) + data: (), + ), + + // Applied selectors (filters for labels). + selects: ( + filters: (), + labels: (), + data: (), + ) +) + +/// This is meant to be used in a show rule of the form `#show ref: e.ref` to ensure +/// references to custom elements work properly. +/// +/// Please use [`e.prepare`](#eprepare) as it does that automatically, and more if +/// necessary. +/// +/// - args (arguments): ref and extra arguments +/// -> content +#let ref_(..args) = { + assert(args.pos().len() > 0, message: "elembic: element.ref: expected at least one positional argument (reference or label)") + let first-arg = args.pos().first() + + set ref(..args.named()) + show ref: it => { + if ( + it.element == none + or it.element.has("label") and str(it.element.label).starts-with(lbl-ref-figure-label-head) + or type(it.target) != label + ) { + // This is known to be a reference to a custom element + // (or the target is not something we can deal with, i.e. not a label) + return it + } + + let info = data(it.element) + if type(info) == dictionary and "data-kind" in info and info.data-kind == "element-instance" { + let supplement = if it.has("supplement") and it.supplement != none { + (supplement: it.supplement) + } else { + (:) + } + + // Convert into a reference towards the reference figure + let converted-label = label(lbl-ref-figure-label-head + str(it.target)) + let reference = ref(converted-label, ..supplement) + + if "custom-ref" in info and info.custom-ref != none { + show ref.where(target: converted-label): [#info.custom-ref] + + reference + } else { + reference + } + } else { + it + } + } + + if type(first-arg) == content and first-arg.func() == ref { + first-arg + } else { + ref(..args) + } +} + +// Changes stateful mode settings within a certain scope. +// This function will sync all data between all modes (data from normal mode +// goes to state and data from stateful mode goes to normal mode). +// +// Setting it to 'true' tells all set rules to update the state, and also ensures +// getters retrieve the value from the state, even if not explicitly aware of +// stateful mode. +// +// By default, this function will not trigger any changes if one attempts to +// change the stateful mode to its current value. This behavior can be disabled +// with 'force: true', though that is not expected to make a difference in any way. +#let toggle-stateful-mode(enable, force: false) = doc => { + context { + let previous-bib-title = bibliography.title + [#context { + let (global-data, was-first-bib-title) = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + (bibliography.title.value, false) + } else { + ((..default-global-data, first-bib-title: previous-bib-title), true) + } + + set bibliography(title: previous-bib-title) + + if global-data.stateful != enable or force { + if not enable { + // Enabling stateful mode => use data from the style chain + // + // Disabling stateful mode => need to sync stateful with non-stateful, + // so we use data from the state + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + // Store the first known bib title in the state as well + if global-data.first-bib-title == () and was-first-bib-title { + global-data.first-bib-title = previous-bib-title + } + } + + // Notify both modes about it (non-stateful and stateful) + global-data.stateful = enable + + let (show-normal, show-stateful) = if enable { + // TODO: Have a way to keep track of previous toggles and undo them + (none, it => it.value.body) + } else { + (it => it.value.body, none) + } + + show lbl-auto-mode: none + show lbl-normal-mode: show-normal + show lbl-stateful-mode: show-stateful + + // Sync data with style chain for non-stateful modes + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + + // Sync data with state for stateful mode + // Push at the start of the scope, pop at the end + [#style-state.update(chain => { + chain.push(global-data) + chain + })#doc#style-state.update(chain => { + _ = chain.pop() + chain + })] + } else { + // Nothing to do: it is already toggled to this value + doc + } + }#lbl-get] + } + + [#metadata(((special-rule-key): "toggle-stateful-mode", version: element-version, enable: enable, force: force))#lbl-special-rule-tag] +} + +// Check if an element instance satisfies a filter. +// +// Assumes this filter already accepts this element, so eid is not checked. +#let verify-filter(fields, eid: none, filter: none, ancestry: ()) = { + if filter == none { + return false + } + if eid == none { + assert(false, message: "elembic: element.verify-filter: eid must not be none") + } + + if "__future" in filter and element-version <= filter.__future.max-version { + return (filter.__future.call)(fields, eid: eid, filter: filter, ancestry: ancestry, __future-version: element-version) + } else if filter.kind == "where" { + return eid == filter.eid and filter.fields.pairs().all(((k, v)) => k in fields and fields.at(k) == v) + } else if filter.kind == "where-any" { + return eid in filter.fields-any and filter.fields-any.at(eid).any(f => f.pairs().all(((k, v)) => k in fields and fields.at(k) == v)) + } else if filter.kind == "custom" { + return (filter.elements == none or eid in filter.elements) and (filter.call)( + fields, eid: eid, ancestry: if filter.may-need-ancestry { ancestry } else { () }, __please-use-var-args: true + ) + } + + // Manually simulate a recursive algorithm. + // Normally, for OR(A, B), we could just call (verify(A), verify(B)), but + // recursive calls are limited and expensive. + // We instead do the following: + // - Have a stack of filters pending evaluation. + // - Have a stack of evaluation results (operands). This is only used for + // non-short circuiting operations (see below). + // - Each time a filter is pushed to the pending stack, we push its operands + // to the pending stack too, until the top of the stack has no further + // operands, and mark the filter as "visited" so we don't add its operands + // again. Note that operands are always pushed in reverse for short circuit + // to work, since we have to evaluate - thus pop from the end - each operand + // in its original order. + // - We evaluate each leaf filter (where or custom) and push their results to + // 'operands' (in reverse order, from last to first). + // - We then reach the filter that will use the latest N results from + // 'operands' and push that filter's evaluated result (e.g. AND of the latest + // two results) into 'operands'. + // - Repeat the process until the filter stack is empty (all evaluated) and + // operands has only a single element (for the root filter). + // - If operands is empty or has more than one element, something bad + // happened. Otherwise, its only remaining element is the evaluated result of + // the root filter. + // + // We also have an "op-stack" to indicate the latest operation whose operands + // were expanded into the filter stack. + // + // The idea is to allow short circuiting when the latest operation is an AND + // or OR. Otherwise, the operation in op-stack is only used to indicate the + // latest operation doesn't short-circuit. + // + // It works as follows: we store the filter stack state in "op-stack" + // whenever we push an operation, such as and, or, xor etc. When the latest + // pushed operation is an "and" and the current filter returned false, we + // immediately restore the filter state at the "and" (ignore its other + // operands) and assign its value to "false". If it was an "or", we do the + // same if the current filter returned true, assigning its value to true. + let filter-stack = (filter,) + let op-stack = () + let operands = () + while filter-stack != () { + let last = filter-stack.last() + + // Expand the latest filter's children into the evaluation stack. + while ( + last.at(filter-key) != "visited" + and ("__future" not in last or element-version > last.__future.max-version) + and "operands" in last + and last.operands != () + ) { + // Ensure we don't reach the parent operation until we have evaluated + // each child operation. + op-stack.push((last.kind, filter-stack.len() - 1)) + filter-stack.last().at(filter-key) = "visited" + if "__subject" in filter { + // Ensure children filters apply to the same subject. + filter-stack += last.operands.map(op => if "__subject" in op { op } else { (..op, __subject: filter.__subject) }).rev() + } else { + // In reverse order to pop the first operand first. + filter-stack += last.operands.rev() + } + last = filter-stack.last() + } + + let filter = filter-stack.pop() + let (kind,) = filter + let fields = fields + let eid = eid + let ancestry = ancestry + if "__subject" in filter { + (fields, eid, ancestry) = filter.__subject + } + + let value = if "__future" in filter and element-version <= filter.__future.max-version { + (filter.__future.call)(fields, eid: eid, filter: filter, ancestry: ancestry, __future-version: element-version) + } else if kind == "where" { + eid == filter.eid and filter.fields.pairs().all(((k, v)) => k in fields and fields.at(k) == v) + } else if kind == "where-any" { + eid in filter.fields-any and filter.fields-any.at(eid).any(f => f.pairs().all(((k, v)) => k in fields and fields.at(k) == v)) + } else if kind == "custom" { + (filter.elements == none or eid in filter.elements) and (filter.call)( + fields, eid: eid, ancestry: if filter.may-need-ancestry { ancestry } else { () }, __please-use-var-args: true + ) + } else if kind == "within" { + // Expand 'within' filter into + // (ancestor 1 matches OR ancestor 2 matches OR ...) + if filter.elements == none or eid in filter.elements { + let (ancestor-filter,) = filter + let matching-ancestors = if "depth" in filter and filter.depth != none and filter.depth > 0 { + let total-depth = ancestry.len() + if total-depth >= filter.depth { + ((total-depth - filter.depth, ancestry.at(total-depth - filter.depth)),) + } else { + () + } + } else if "max-depth" in filter and filter.max-depth != none and filter.max-depth > 0 { + let total-depth = ancestry.len() + if total-depth <= filter.max-depth { + ancestry.enumerate() + } else { + ancestry.enumerate().slice(total-depth - filter.max-depth) + } + } else { + ancestry.enumerate() + } + + filter-stack.push( + ( + (filter-key): true, + element-version: element-version, + kind: "or", + operands: matching-ancestors.map(((i, ancestor)) => ( + ..ancestor-filter, + + // TODO: maybe don't clone the ancestry for each ancestor... + __subject: (eid: ancestor.eid, fields: ancestor.fields, ancestry: ancestry.slice(0, i)) + )), + elements: ancestor-filter.elements, + + // Since this is an internal filter, doesn't matter + ancestry-elements: (:), + may-need-ancestry: true, + ) + ) + + // This filter won't be evaluated, but rather the pushed OR. + continue + } + + // Invalid + false + } else if kind == "and" { + // Due to short-circuiting, a false would have failed earlier. + true + } else if kind == "or" { + // Due to short-circuiting, a true would have succeeded earlier. + false + } else if "operands" in filter { + let first-applied-operand = operands.len() - filter.operands.len() + // Operation requires N operands => take N operands from the top of the + // stack. + let applied-operands = operands.slice(first-applied-operand) + operands = operands.slice(0, first-applied-operand) + + if kind == "not" { + assert(applied-operands.len() == 1, message: "elembic: element.verify-filter: expected one child filter for 'not'") + (filter.elements == none or eid in filter.elements) and not applied-operands.first() + } else if kind == "xor" { + assert(applied-operands.len() == 2, message: "elembic: element.verify-filter: expected two children filters for 'xor'") + // Here the order doesn't matter, since we always need to evaluate both + // XOR operands (no short-circuit). + applied-operands.first() != applied-operands.at(1) + } else { + assert(false, message: "elembic: element.verify-filter: unsupported filter kind '" + kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") + } + } else { + assert(false, message: "elembic: element.verify-filter: unsupported or invalid filter kind '" + kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") + } + + if op-stack != () and op-stack.last().at(1) == filter-stack.len() { + // We have just evaluated this operation. + _ = op-stack.pop() + } + + // Short-circuit: for certain operations, a specific value must stop all + // other operand filters from running. + let (current-op, op-pos) = if op-stack == () { (none, none) } else { op-stack.last() } + while current-op == "and" and not value or current-op == "or" and value { + filter-stack = filter-stack.slice(0, op-pos) + _ = op-stack.pop() + if op-stack == () { + current-op = none + op-pos = none + break + } else { + (current-op, op-pos) = op-stack.last() + } + } + + if current-op not in ("and", "or") { + operands.push(value) + } + } + + if operands.len() != 1 or op-stack != () { + assert(false, message: "elembic: element.verify-filter: filter didn't receive enough operands.") + } + + operands.first() +} + +#let multi-operand-filter(kind: "", arg-count: none) = (..args) => { + assert(args.named() == (:), message: "elembic: filters: invalid named arguments given to '" + kind + "' filter constructor.") + let filters = args.pos() + if arg-count != none and filters.len() != arg-count { + assert(false, message: "elembic: filters: must give exactly " + str(arg-count) + " arguments to a '" + kind + "' filter constructor.") + } + + filters = filters.map(filter => { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: filters: invalid filter passed to '" + kind + "' constructor, please use 'custom-element.with(...)' to generate a filter.") + + // Flatten "and", "or" + if filter.kind == kind and kind in ("and", "or") { + filter.operands + } else { + (filter,) + } + }).flatten() + + let elements = none + if kind == "and" { + // Merge where filters as an optimization + let where-fields = (:) + let where-eid = none + let may-merge-filters = filters != () + + // Intersect elements. + // Start accepting all elements and narrow it down from there. + for filter in filters { + assert("elements" in filter, message: "elembic: filters.and: this filter operand is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + if elements == none { + elements = filter.elements + } else if filter.elements != none { + // Cannot add new elements, only remove non-shared elements. + for (eid, elem-data) in elements { + if eid not in filter.elements { + _ = elements.remove(eid) + } + } + } + + if filter.kind == "where" and may-merge-filters { + if where-eid == none { + where-eid = filter.eid + } else if where-eid != filter.eid { + // More than one element to check will never match. + may-merge-filters = false + continue + } + let (eid, fields) = filter + if where-fields == (:) { + where-fields = fields + } else { + for (field, value) in fields { + if field in where-fields and value != where-fields.at(field) { + // and(elem.with(a: 1), elem.with(a: 2)) + // impossible to match + may-merge-filters = false + break + } + + where-fields.insert(field, value) + } + } + } else if may-merge-filters { + // Has a custom filter, don't merge + may-merge-filters = false + } + } + + if may-merge-filters and where-eid != none { + // and(elem, elem.with(a: 0), elem.with(b: 1)) + return ( + (filter-key): true, + element-version: element-version, + kind: "where", + eid: where-eid, + fields: where-fields, + elements: ((where-eid): elements.at(where-eid)), + ancestry-elements: (:), + + // For optimizations + may-need-ancestry: false, + ) + } + + // Ensure the filters won't match on the wrong elements. + // Workaround for e.filters.and_(custom, element) + filters = filters.map(f => f + (elements: elements,)) + + elements + } else if kind == "or" or kind == "xor" { + // Join together. + elements = (:) + let may-merge-filters = kind == "or" and filters != () + let wheres = (:) + + for filter in filters { + if "elements" in filter and filter.elements == none { + // OR(Any, ...) is always Any. + // For XOR, we still have to check all operands so this would also be + // Any. + elements = none + } else if "elements" not in filter or type(filter.elements) != dictionary { + assert(false, message: "elembic: filters: invalid operand filter received by '" + kind + "' filter constructor\n\nhint: this filter was likely constructed with an old elembic version. Please update your packages.") + } else if elements != none { + elements += filter.elements + } + + if may-merge-filters and filter.kind in ("where", "where-any") { + for (eid, fields) in if filter.kind == "where-any" { filter.fields-any } else { ((filter.eid): (filter.fields,)) } { + if eid in wheres { + wheres.at(eid) += fields + } else { + wheres.insert(eid, fields) + } + } + } else if may-merge-filters { + // Not all "where" + may-merge-filters = false + } + } + + if may-merge-filters { + return ( + (filter-key): true, + element-version: element-version, + kind: "where-any", + fields-any: wheres, + elements: elements, + ancestry-elements: (:), + may-need-ancestry: false, + ) + } + + elements + } else if kind == "not" { + // No elements for NOT since it is unrestricted. + // User will have to restrict it manually. + none + } else { + assert(false, message: "elembic: filters: internal error: invalid kind '" + kind + "'") + } + + ( + (filter-key): true, + element-version: element-version, + kind: kind, + operands: filters, + elements: elements, + ancestry-elements: (:) + filters.map(f => f.at("ancestry-elements", default: none)).join(), + may-need-ancestry: filters.any(f => "may-need-ancestry" in f and f.may-need-ancestry), + ) +} + +#let or-filter = multi-operand-filter(kind: "or") +#let and-filter = multi-operand-filter(kind: "and") +#let not-filter = multi-operand-filter(kind: "not", arg-count: 1) +#let xor-filter = multi-operand-filter(kind: "xor", arg-count: 2) +#let custom-filter(callback) = { + assert(type(callback) == function, message: "elembic: filters.custom: 'callback' for custom filter must be a function (fields, eid: eid, ..) => bool.") + + ( + (filter-key): true, + element-version: element-version, + kind: "custom", + call: callback, + elements: none, + ancestry-elements: (:), + may-need-ancestry: true, + ) +} + +/// Filter that only matches when this element is inside another elembic element. +/// +/// For example: +/// +/// ```typ +/// #show: e.show_(e.filters.and(elem, e.filters.within(other-elem)), none) +/// +/// #other-elem(elem[This element will be matched and removed]) +/// #elem[This element stays, as it is not inside `other-elem`] +/// ``` +/// +/// - ancestor-filter (filter): a filter to match potential ancestors. +/// - depth (int): only match at this exact KNOWN depth. +/// - max-depth (int): only match up to this exact KNOWN depth. +/// -> filter +#let within-filter(ancestor-filter, depth: none, max-depth: none) = { + if type(ancestor-filter) == function { + ancestor-filter = ancestor-filter(__elembic_data: special-data-values.get-where) + } + assert(type(ancestor-filter) == dictionary and filter-key in ancestor-filter, message: "elembic: filters.within: invalid filter, please use 'custom-element.with(...)' to generate a filter.") + assert("elements" in ancestor-filter, message: "elembic: filters.within: the ancestor filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(ancestor-filter.elements != (:), message: "elembic: filters.within: the ancestor filter appears to not be restricted to any elements and is thus impossible to match. It must apply to at least one element (potential ancestor). Consider using a different filter.") + assert(ancestor-filter.elements != none, message: "elembic: filters.within: the ancestor filter appears to apply to any element. It must apply to exactly one element (the one receiving the set rule). Consider using an 'and' filter, e.g. 'e.filters.within(e.filters.and(wibble, e.not(wibble.with(a: 10))))' instead of just 'e.filters.within(e.not(wibble.with(a: 10)))', to restrict it.") + assert(depth == none or max-depth == none, message: "elembic: filters.within: cannot specify both depth and max-depth (please pick one).") + assert(depth == none or type(depth) == int and depth > 0, message: "elembic: filters.within: 'depth' parameter must be a positive integer or 'none'.") + assert(max-depth == none or type(max-depth) == int and max-depth > 0, message: "elembic: filters.within: 'max-depth' parameter must be a positive integer or 'none'.") + + ( + (filter-key): true, + element-version: element-version, + kind: "within", + ancestor-filter: ancestor-filter, + depth: depth, + max-depth: max-depth, + elements: none, + ancestry-elements: (:) + ancestor-filter.elements + ancestor-filter.at("ancestry-elements", default: (:)), + may-need-ancestry: true, + ) +} + +#let request-ancestry-tracking(elements, requests) = { + for (eid, elem-data) in requests { + if eid not in elements { + elements.insert(eid, elem-data.default-data) + } + elements.at(eid).track-ancestry = true + } + + elements +} + +// Apply set and revoke rules to the current per-element data. +#let apply-rules(rules, elements: none, settings: (:), global: (:), extra-output: (:)) = { + for rule in rules { + if "__future" in rule and element-version <= rule.__future.max-version { + let output = (rule.__future.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + let kind = rule.kind + if kind == "settings" { + let (write, transform) = rule + if write != none { + settings += write + } + if transform != none { + settings = transform(settings) + } + } else if kind == "set" { + let (element, args) = rule + let (eid, default-data, fields) = element + + // Forward-compatibility with newer elements + if ( + "__future-rules" in default-data + and "set" in default-data.__future-rules + and element-version <= default-data.__future-rules.set.max-version + ) { + let output = (default-data.__future-rules.set.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid in elements { + elements.at(eid).chain.push(args) + } else { + elements.insert(eid, (..default-data, chain: (args,))) + } + + let names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } + let compat-name = none + if names != () { + let element-data = elements.at(eid) + let index = element-data.chain.len() - 1 + compat-name = names.last() + + // Lazily fill the data chain with 'none' + // Add 'name' for compatibility with older elembic versions + elements.at(eid).data-chain += (none,) * (index - element-data.data-chain.len()) + elements.at(eid).data-chain.push((kind: "set", name: compat-name, names: names)) + + for rule-name in names { + elements.at(eid).names.insert(rule-name, true) + } + } + + if fields.foldable-fields != (:) and args.keys().any(n => n in fields.foldable-fields) { + // A foldable field was specified in this set rule, so we need to record the fold + // data in the corresponding data structures separately for later. + let element-data = elements.at(eid) + let index = element-data.chain.len() - 1 + for (field-name, fold-data) in fields.foldable-fields { + if field-name in args { + let value = args.at(field-name) + let value-data = (index: index, name: compat-name, names: names, value: value) + if field-name in element-data.fold-chain { + elements.at(eid).fold-chain.at(field-name).values.push(value) + elements.at(eid).fold-chain.at(field-name).data.push(value-data) + } else { + elements.at(eid).fold-chain.insert( + field-name, + ( + folder: fold-data.folder, + default: fold-data.default, + values: (value,), + data: (value-data,) + ) + ) + } + } + } + } + } else if kind == "revoke" { + let rule-names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } + let compat-name = if rule-names == () { + none + } else { + rule-names.last() + } + + for (name, element-data) in elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in element-data + and "revoke" in element-data.__future-rules + and element-version <= element-data.__future-rules.revoke.max-version + ) { + let output = (element-data.__future-rules.revoke.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + // Can only revoke what's before us. + // If this element has no rules with this name, there is nothing to revoke; + // we shouldn't revoke names that come after us (inner rules). + // Note that this potentially includes named revokes as well. + if rule.revoking in element-data.names { + elements.at(name).revoke-chain.push((kind: "revoke", name: compat-name, names: rule-names, index: element-data.chain.len(), revoking: rule.revoking)) + + if rule-names != () { + for rule-name in rule-names { + elements.at(name).names.insert(rule-name, true) + } + } + } + } + } else if kind == "reset" { + // Whether the list of elements that this reset applies to is restricted. + let filtering = rule.eids != () + let rule-names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } + let compat-name = if rule-names == () { + none + } else { + rule-names.last() + } + + for (name, element-data) in elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in element-data + and "reset" in element-data.__future-rules + and element-version <= element-data.__future-rules.reset.max-version + ) { + let output = (element-data.__future-rules.reset.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + // Can only revoke what's before us. + // If this element has no rules, no need to add a reset. + if (not filtering or name in rule.eids) and element-data.chain != () { + elements.at(name).revoke-chain.push((kind: "reset", name: compat-name, names: rule-names, index: element-data.chain.len())) + + if rule-names != () { + for rule-name in rule-names { + elements.at(name).names.insert(rule-name, true) + } + } + } + } + } else if kind == "filtered" { + let (filter, rule: inner-rule, names) = rule + if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { + assert(false, message: "elembic: element.filtered: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + let target-elements = filter.elements + if target-elements == none { + assert(false, message: "elembic: element.filtered: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + } + let base-data = (names: names) + + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + + for (eid, all-elem-data) in target-elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in all-elem-data.default-data + and "filtered" in all-elem-data.default-data.__future-rules + and element-version <= all-elem-data.default-data.__future-rules.filtered.max-version + ) { + let output = (all-elem-data.default-data.__future-rules.filtered.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid not in elements { + elements.insert(eid, all-elem-data.default-data) + } + if "filters" not in elements.at(eid) { + // Old version + elements.at(eid).filters = default-data.filters + } + + let index = elements.at(eid).chain.len() + let data = (..base-data, index: index) + + elements.at(eid).filters.all.push(filter) + elements.at(eid).filters.rules.push(inner-rule) + elements.at(eid).filters.data.push(data) + + // Push an entry to the data chain so we have an index to assign to + // this filter rule. This allows us to reset() it later. + elements.at(eid).chain.push((:)) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "filtered", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } + } else if kind == "show" { + let (filter, callback, names) = rule + if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { + assert(false, message: "elembic: element.show_: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + let target-elements = filter.elements + if target-elements == none { + assert(false, message: "elembic: element.show_: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + } + let base-data = (names: names) + + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + + for (eid, all-elem-data) in target-elements { + // Forward-compatibility with newer elements + if ( + "__future-rules" in all-elem-data.default-data + and "show" in all-elem-data.default-data.__future-rules + and element-version <= all-elem-data.default-data.__future-rules.show.max-version + ) { + let output = (all-elem-data.default-data.__future-rules.show.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid not in elements { + elements.insert(eid, all-elem-data.default-data) + } + if "show-rules" not in elements.at(eid) { + // Old version + elements.at(eid).show-rules = default-data.show-rules + } + + let index = elements.at(eid).chain.len() + let data = (..base-data, index: index) + + elements.at(eid).show-rules.filters.push(filter) + elements.at(eid).show-rules.callbacks.push(callback) + elements.at(eid).show-rules.data.push(data) + + // Push an entry to the data chain so we have an index to assign to + // this show rule. This allows us to reset() it later. + elements.at(eid).chain.push((:)) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "show", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } + } else if kind == "cond-set" { + let (filter, args, names, element) = rule + if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { + assert(false, message: "elembic: element.cond-set: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + + let (eid,) = element + + // Forward-compatibility with newer elements + if ( + "__future-rules" in element.default-data + and "cond-set" in element.default-data.__future-rules + and element-version <= element.default-data.__future-rules.cond-set.max-version + ) { + let output = (element.default-data.__future-rules.cond-set.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + if eid not in elements { + elements.insert(eid, element.default-data) + } + if "cond-sets" not in elements.at(eid) { + // Old version + elements.at(eid).cond-sets = default-data.cond-sets + } + + let index = elements.at(eid).chain.len() + let data = (names: names, index: index) + elements.at(eid).cond-sets.filters.push(filter) + elements.at(eid).cond-sets.args.push(args) + elements.at(eid).cond-sets.data.push(data) + + // Push an entry to the data chain so we have an index to assign to + // this filter rule. This allows us to reset() it later. + elements.at(eid).chain.push((:)) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "cond-set", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } else if kind == "select" { + let (element-data: target-elements, names) = rule + let base-data = (names: names) + + for (eid, elem-data) in target-elements { + if "filters" not in elem-data or "labels" not in elem-data { + assert(false, message: "elembic: element.select: missing filters or labels for element " + repr(eid)) + } + let (filters, labels) = elem-data + assert(filters.len() == labels.len(), message: "elembic: element.select: differing lengths for filters and labels found (this is an internal error)") + if filters == () { + continue + } + + let sample-filter = filters.first() + assert( + type(sample-filter) == dictionary + and "kind" in sample-filter + and "elements" in sample-filter + and type(sample-filter.elements) == dictionary + and eid in sample-filter.elements, + message: "elembic: element.select: invalid filter found for element " + repr(eid) + ", it must contain the element's data.\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages." + ) + + let all-elem-data = sample-filter.elements.at(eid) + + // Forward-compatibility with newer elements + if ( + "__future-rules" in all-elem-data.default-data + and "select" in all-elem-data.default-data.__future-rules + and element-version <= all-elem-data.default-data.__future-rules.select.max-version + ) { + let output = (all-elem-data.default-data.__future-rules.select.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + continue + } + + for filter in filters { + assert( + type(filter) == dictionary + and "kind" in filter + and "elements" in filter, + message: "elembic: element.select: invalid filter found for element " + repr(eid) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages." + ) + if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { + elements = request-ancestry-tracking(elements, filter.ancestry-elements) + } + } + + if eid not in elements { + elements.insert(eid, all-elem-data.default-data) + } + if "selects" not in elements.at(eid) { + // Old version + elements.at(eid).selects = default-data.selects + } + + let index = elements.at(eid).chain.len() + let data = (..base-data, index: index) + + elements.at(eid).selects.filters += filters + elements.at(eid).selects.labels += labels + elements.at(eid).selects.data += (data,) * filters.len() + + // Push an entry to the data chain so we have an index to assign to + // this filter rule. This allows us to reset() it later. + elements.at(eid).chain += (((:),) * filters.len()) + + // Lazily fill the data chain with 'none' + elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) + + // Keep "name" for some compatibility with older versions... + elements.at(eid).data-chain.push( + (kind: "select", name: if data.names == () { none } else { data.names.last() }, names: data.names) + ) + + for name in data.names { + // Ensure the name is registered so revoke rules on this name are + // treated as valid. + elements.at(eid).names.insert(name, true) + } + } + } else if kind == "apply" { + // Mostly a fallback in case the rule is accidentally passed here... + let output = apply-rules(rule.rules, elements: elements, settings: settings, global: global, extra-output: extra-output) + extra-output += output + if "elements" in output { + elements = output.elements + } + if "settings" in output { + settings = output.settings + } + if "global" in output { + global = output.global + } + } else { + assert(false, message: "elembic: element: invalid rule kind '" + rule.kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") + } + } + + (..extra-output, elements: elements, settings: settings) +} + +// Prepare rule(s), returning a function `doc => ...` to be used in +// `#show: rule`. The rule is attached as metadata to the returned +// content so it can still be accessed outside of a show rule. +// +// This is where we execute our main machinery to apply rules to the +// document, that is, modifications to the global data of custom +// elements. This is done in different ways depending on the mode: +// +// - In normal mode, we create 'get rule' points by annotating +// context blocks with `#lbl-get`. Any modifications to the global +// data are stored as 'set bibliography(title: metadata with data)' +// scoped to context blocks with that label. Therefore, we can access +// the data by retrieving bibliography.title inside those blocks. +// +// The downside is that the entire document is wrapped in context, +// so 'max show rule depth exceeded' errors can occur. +// +// - In leaky mode, it is similar, but we reset bibliography.title +// to an arbitrary value instead of having two context blocks to +// ensure it remains unchanged. +// +// - In stateful mode, we don't wrap anything around the document, +// removing the 'max show rule depth exceeded' problem. Rather, we +// place a state update at the start and another at the end of the +// scope, respectively updating the global data and then undoing +// the update, ensuring it only applies to that scope. +// +// The downside is that this uses 'state()', which can lead to +// relayouts (slower) and even diverging layout. +#let prepare-rule(rule) = { + let rules = if rule.kind == "apply" { rule.rules } else { (rule,) } + + doc => { + let rule = rule + let rules = rules + let mode = rule.mode + + // If there are two 'show:' in a row, flatten into a single set of rules + // instead of running this function multiple times, reducing the + // probability of accidental nested function limit errors. + // + // Note that all rules replace the document with + // [#context { ... doc .. }[#metadata(doc: doc, rule: rule)#lbl-rule-tag]] + // We get the second child to extract the original rule information. + // If 'doc' has the form above, this means the user wrote + // #show: rule1 + // #show: rule2 + // which we want to unify. So we check children len == 2 and unify if the tag is there. + // + // But we also want to accept some parbreaks before, i.e. + // + // #show: rule1 + // + // #show: rule2 + // + // This generates a doc of the form + // [#parbreak()[#context { ... doc .. }[#metadata(doc: doc, rule: rule)#lbl-rule-tag]]] + // So we also check for children len >= 2 (although == 2 is enough in that case) and + // strip any leading parbreaks / spaces / linebreaks, moving them to the new 'doc' (they + // now receive the rules, which is technically incorrect, but in practice is only a problem + // if you have a show rule on parbreak or space or something, which is odd). + // + // Note also that + // #show: rule1 + // + // // hello world! + // // hello world! + // // hello world! + // + // #show: rule2 + // + // produces + // [#parbreak()#space()#space()#parbreak()[... rule substructure with metadata... ]] + // which makes the need for stripping multiple kinds of whitespace explicit. + // We limit at 100 to prevent unbounded search. + // + // We also need to consider the case with + // #show: rule1 + // #set native(field: value) + // #show: rule2 + // + // in which case the document structure (from rule1's view) is + // + // styled(child: [... rule2 ...], styles: ..) + // + // Worse, there could be parbreaks around the set rule: + // + // #show: rule1 + // + // #set native(field: value) + // + // #show: rule2 + // + // leading to + // + // sequence(parbreak(), styled(child: sequence(parbreak(), [ ... rule2 ... ]), styles: ..)) + // + // so we need to perform a document tree walk to lift rule2 and transform this into + // + // #show: apply( + // rule1 + // rule2 + // ) + // + // #set native(field: value) + // ... + // + // Tree walk is performed as follows: + // + // this rule + // \ + // sequence + // \ space parbreak ... sequence + // \ space parbreak ... styled (styles = S) + // \ sequence + // \ space parbreak ... inner rule! + // \ (rule.doc, rule.rule) + // We store each tree level in 'wrappers' so we can reconstruct this document structure without 'rule!'. + // In the case above, that would correspond to + // wrappers = ((sequence, (space, parbreak, ...)), (sequence, space, parbreak, ...), (styled, S), (sequence, space, parbreak, ...)) + // and 'rule' would become 'potential-doc'. + // + // We would then wrap 'rule.doc' in reverse order, adding after the sequence prefix or + // making it the styled child, producing + // + // this rule + inner rule + // \ + // (sequence, apply(this rule, inner rule)) + // \ space parbreak ... sequence + // \ space parbreak ... styled (styles = S) + // \ sequence + // \ space parbreak ... rule.doc + // + // as desired. That is, we move the inner rule up into this rule in order to only consume 1 from + // the rule limit, which is valid since the rule won't apply to spaces, parbreaks, and styled. + // Of course, there could be show rules towards a different structure, but we assume that the user + // understands that show rules on spacing may cause unexpected behavior. + let potential-doc = [#doc] + let wrappers = () + let max-depth = 150 + // Acceptable content types for set rule lifting. + // These are content types that are leaves and we usually don't expect them to + // be replaced in a show rule by an actual custom element. + // If we find something that isn't here, e.g. a block, we stop searching as we can't lift any further rules. + // We also exclude anything with a label since that indicates there might be a show rule application incoming. + let whitespace-funcs = (parbreak, space, linebreak, h, v, state-update-func, counter-update-func) + // Content types we can peek at. + let recursing-funcs = (styled, sequence) + let loop-prefix = none + let loop-children = () + let loop-last = none + + while max-depth > 0 { + // Child is #{ + // set something(abc: def) + // show something: else + // [some stuff] + // } + if potential-doc.func() == styled { + max-depth -= 1 + wrappers.push((styled, potential-doc.styles)) + + // 'Recursively' check the child + potential-doc = [#potential-doc.child] + } else if ( + // Child is #[ + // (parbreak) + // (space) + // #[ sequence, rule or more styles ] + // ] + potential-doc.func() == sequence + and { loop-children = potential-doc.children; loop-children.len() >= 2 } // something like 'if let Sequence(children) = potential-doc { ... }' + and { loop-last = loop-children.last(); loop-last.func() in recursing-funcs } + and max-depth - loop-children.len() > 0 + and { + loop-prefix = loop-children.slice(0, -1); + loop-prefix.all(t => (t.func() in whitespace-funcs or t == []) and t.at("label", default: none) == none) + } + ) { + max-depth -= loop-children.len() + wrappers.push((sequence, loop-prefix)) + + // 'Recursively' check the last child + potential-doc = loop-last + } else { + break + } + } + + // Merge with the closest rule application below us, "moving" it upwards + // and reducing the rule count by 1 + let last-label = none + while ( + potential-doc.func() == sequence + and potential-doc.children.len() == 2 + and { + last-label = potential-doc.children.last().at("label", default: none) + last-label == lbl-rule-tag or last-label == lbl-old-rule-tag + } + ) { + let last = potential-doc.children.last() + let inner-rule = last.value.rule + + // Process all rules below us together with this one + if inner-rule.kind == "apply" { + // Note: apply should automatically distribute modes across its children, + // so it's okay if we don't inherit its own mode here. + rules += inner-rule.rules + } else { + rules.push(inner-rule) + } + + // We assume 'apply' already checked its own rules. + // Therefore, we only need to fold a single time. + // Don't check all rules every time again. + if ( + inner-rule.mode == style-modes.stateful + or mode != style-modes.stateful and inner-rule.mode == style-modes.leaky + or mode == auto + ) { + // Prioritize more explicit modes: + // stateful > leaky > normal + mode = inner-rule.mode + } + + // Convert this into an 'apply' rule + rule = ((prepared-rule-key): true, version: element-version, kind: "apply", rules: rules, mode: mode, name: none, names: ()) + + // Place what's inside, don't place the context block that would run our code again + doc = last.value.doc + + // Reconstruct the document structure. + // Must be in reverse (innermost wrapper to outermost). + for (func, data) in wrappers.rev() { + if func == styled { + doc = styled(doc, data) + } else { + // (sequence, prefix) + // Re-add stripped whitespace and stuff + doc = data.join() + doc + } + } + + if "__future" in last.value and element-version <= last.value.__future.max-version { + let res = (last.value.__future.call)(rule, doc, __future-version: element-version) + + if "doc" in res { + return res.doc + } + } + + if last-label == lbl-old-rule-tag { + // If we're merging with an older rule version, we may have to merge a + // newer version again + potential-doc = last.value.doc + } else { + break + } + } + + // Stateful mode: no context, just push in a state at the start of the scope + // and pop to previous data at the end. + let stateful = { + style-state.update(chain => { + let global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + assert( + global-data.stateful, + message: "elembic: element rule: cannot use a stateful rule without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.toggle(true)' somewhere above this rule, or at the top of the document to apply to all" + ) + + if "settings" not in global-data { + global-data.settings = default-global-data.settings + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) + + chain.push(global-data) + chain + }) + doc + style-state.update(chain => { + _ = chain.pop() + chain + }) + } + + // Leaky mode: one context resetting bibliography.title. + let leaky = [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + // Bibliography title wasn't overridden, so we can use it + (..default-global-data, first-bib-title: bibliography.title) + } + + if mode == auto and ("settings" not in global-data or "prefer-leaky" not in global-data.settings or not global-data.settings.prefer-leaky) { + // User didn't want leaky. + return none + } + + let first-bib-title = global-data.first-bib-title + if first-bib-title == () { + // Nobody has seen the bibliography title (bug?) + first-bib-title = auto + } + + if global-data.stateful { + if mode == auto { + // User chose something else. + // Don't even place anything. + return none + } else { + // Use state instead! + return { + set bibliography(title: first-bib-title) + stateful + } + } + } + + if "settings" not in global-data { + global-data.settings = default-global-data.settings + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) + + set bibliography(title: first-bib-title) + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + doc + }#lbl-get] + + // Normal mode: two nested contexts: one retrieves the current bibliography title, + // and the other retrieves the title with metadata and restores the current title. + let normal = context { + let previous-bib-title = bibliography.title + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + if mode == auto and "settings" in global-data and "prefer-leaky" in global-data.settings and global-data.settings.prefer-leaky { + // User wants leaky. + return none + } + + if global-data.stateful { + if mode == auto { + // User chose something else. + // Don't even place anything. + return none + } else { + // Use state instead! + return { + set bibliography(title: previous-bib-title) + stateful + } + } + } + + if "settings" not in global-data { + global-data.settings = default-global-data.settings + } + + if "global" not in global-data { + global-data.global = default-global-data.global + } + + global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) + + set bibliography(title: previous-bib-title) + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + doc + }#lbl-get] + } + + let body = if mode == auto { + // Allow user to pick the mode through show rules. + [#metadata((body: stateful))#lbl-stateful-mode] + [#metadata((body: normal))#lbl-normal-mode] + [#leaky] + [#normal#lbl-auto-mode] + } else if mode == style-modes.normal { + normal + } else if mode == style-modes.leaky { + leaky + } else if mode == style-modes.stateful { + stateful + } else { + panic("element rule: unknown mode: " + repr(mode)) + } + + // Add the rule tag after each rule application. + // This allows extracting information about the rule before it is applied. + // It also allows combining the rule with an outer rule before application, + // as we do earlier. + [#body#metadata((version: element-version, routines: (prepare-rule: prepare-rule, apply-rules: apply-rules), doc: doc, rule: rule))#lbl-rule-tag] + } +} + +/// Apply a set rule to a custom element. Check out the Styling guide for more information. +/// +/// Note that this function only accepts non-required fields (that have a `default`). +/// Any required fields must always be specified at call site and, as such, are always +/// be prioritized, so it is pointless to have set rules for those. +/// +/// Keep in mind the limitations when using set rules, as well as revoke, reset and +/// apply rules. +/// +/// As such, when applying many set rules at once, please use `e.apply` instead +/// (or specify them consecutively so `elembic` does that automatically). +/// +/// USAGE: +/// +/// ```typ +/// #show: e.set_(superbox, fill: red) +/// #show: e.set_(superbox, optional-pos-arg1, optional-pos-arg2) +/// +/// // This call will be equivalent to: +/// // #superbox(required-arg, optional-pos-arg1, optional-pos-arg2, fill: red) +/// #superbox(required-arg) +/// ``` +/// +/// - elem (function): element to apply the set rule on +/// - fields (arguments): optional fields to set (positionally or named, depending on the field) +/// -> function +#let set_(elem, ..fields) = { + if type(elem) == function { + elem = data(elem) + } + assert(type(elem) == dictionary, message: "elembic: element.set_: please specify the element's constructor or data in the first parameter") + let (res, args) = (elem.parse-args)(fields, include-required: false) + if not res { + assert(false, message: args) + } + + prepare-rule( + ((prepared-rule-key): true, version: element-version, kind: "set", name: none, names: (), mode: auto, element: (eid: elem.eid, default-data: elem.default-data, fields: elem.fields), args: args) + ) +} + +/// Prepare a selector similar to 'element.where(..args)' +/// which can be used in "show sel: set". Receives a filter +/// generated by 'element.with(fields)' or '(element-data.where)(fields)'. +/// +/// This works by checking the filter within all element instances and, +/// if they match, they receive a unique label to be matched +/// by that selector. The label is then provided to the callback function +/// as the selector. +/// +/// Each requested selector is passed as a separate parameter to the callback. +/// You must wrap the remainder of the document that depends on those selectors +/// in this callback. +/// +/// USAGE: +/// +/// ```typ +/// #e.select(superbox.with(fill: red), prefix: "my first select", superbox.with(width: auto), (red-superbox, auto-superbox) => { +/// // Hide superboxes with red fill or auto width +/// show red-superbox: none +/// show auto-superbox: none +/// +/// // This one is hidden +/// #superbox(fill: red) +/// +/// // This one is hidden +/// #superbox(width: auto) +/// +/// // This one is kept +/// #superbox(fill: green, width: 5pt) +/// }) +/// ``` +/// +/// - args (function): filters in the format 'element.with(field-a: a, field-b: b)'. Note that you must write fields' names even if they are positional. +/// - receiver (function): receives one requested selector per filter as separate arguments, must return content. +/// - prefix (str): a unique prefix for selectors generated by this 'selector' to disambiguate from other calls to this function. +/// -> content +#let select(..args, receiver, prefix: 0) = { + assert(type(prefix) == str, message: "elembic: element.select: please pick a unique string 'prefix:' argument for the selectors generated by this call to 'select' to ensure they don't clash with other calls to 'select'.") + assert(args.named() == (:), message: "elembic: element.select: unexpected named arguments") + assert(type(receiver) == function, message: "elembic: element.select: last argument must be a function receiving each prepared selector as a separate argument") + + let filters = args.pos() + + // (eid: ((index, filter), ...)) + // The idea is to apply all filters for a given eid at once + let filters-by-eid = (:) + // (eid: sel) + let labels-by-eid = (:) + // Elements which still require explicit show rules. + let old-elements = (:) + let ordered-eids = () + + let i = 0 + for filter in filters { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + + if type(filter) != dictionary or filter-key not in filter { + if type(filter) == selector { + assert(false, message: "elembic: element.select: Typst-native selectors cannot be specified here, only those of custom elements") + } + assert(false, message: "elembic: element.select: expected a valid filter, such as 'custom-element' or 'custom-element.with(field-name: value, ...)', got " + base.typename(filter)) + } + + if "elements" not in filter { + assert(false, message: "elembic: element.select: invalid filter found while applying rule, as it did not have an 'elements' field: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") + } + + for (eid, elem-data) in filter.elements { + if "sel" not in elem-data { + assert(false, message: "elembic: element.select: filter did not have the element's selector") + } + if elem-data.eid in labels-by-eid and labels-by-eid.at(elem-data.eid) != elem-data.sel { + assert(false, message: "elembic: element.select: filter had a different selector from the others for the same element ID, check if you're not using conflicting library versions (could also be a bug)") + } + + if elem-data.eid not in labels-by-eid { + labels-by-eid.insert(elem-data.eid, elem-data.sel) + } + + if elem-data.eid in filters-by-eid { + filters-by-eid.at(elem-data.eid).push((i, filter)) + } else { + filters-by-eid.insert(elem-data.eid, ((i, filter),)) + ordered-eids.push(elem-data.eid) + } + + if ("version" not in elem-data or elem-data.version <= 1) and ("default-data" not in elem-data or "selects" not in elem-data.default-data) { + old-elements.insert(elem-data.eid, true) + } + } + i += 1 + } + + context { + let previous-bib-title = bibliography.title + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + if global-data.stateful { + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + } + + // Amount of 'select rules' so far, so we can + // assign a unique number to each query + let rule-counter = global-data.global.select-count + + // Generate labels by counting up, and update counter + let matching-labels = range(0, filters.len()).map(i => label(lbl-global-select-head + prefix + str(rule-counter + i))) + rule-counter += matching-labels.len() + global-data.select-count = rule-counter + + // Provide labels to the body, one per filter + // These labels only match the shown bodies of + // elements with matching field values + let body = receiver(..matching-labels) + + // Apply show rules to the body to add labels to matching elements + let styled-body = ordered-eids.filter(e => e in old-elements).fold(body, (acc, eid) => { + let filters = filters-by-eid.at(eid) + show labels-by-eid.at(eid): it => { + let data = data(it) + let tag = [#metadata(data)#lbl-tag] + let fields = data.fields + + let labeled-it = it + for (i, filter) in filters { + // Check if all positional and named arguments match + // Note: no ancestry support since newer elements don't run this + // code, they use 'select' rules instead + if verify-filter(fields, eid: eid, filter: filter, ancestry: ()) { + // Add corresponding label and preserve tag so 'data(it)' still works + labeled-it = [#[#labeled-it#tag]#matching-labels.at(i)] + } + } + + labeled-it + } + + acc + }) + + set bibliography(title: previous-bib-title) + + let pairs-by-eid = (:) + for eid in ordered-eids { + if eid in old-elements or filters-by-eid.at(eid, default: ()) == () { + continue + } + let pairs = filters-by-eid.at(eid).map(((i, f)) => (f, matching-labels.at(i))) + let (filters, labels) = array.zip(..pairs) + pairs-by-eid.insert(eid, (filters: filters, labels: labels)) + } + + if pairs-by-eid != (:) { + let select-rule = ( + ((prepared-rule-key): true, + version: element-version, + kind: "select", + name: none, + names: (), + mode: auto, + element-data: pairs-by-eid, + ) + ) + global-data += apply-rules( + (select-rule,), + elements: global-data.elements, + settings: global-data.at("settings", default: default-global-data.settings), + global: global-data.at("global", default: default-global-data.global) + ) + } + + // Increase select rule counter for further select rules + if global-data.stateful { + style-state.update(chain => { + chain.push(global-data) + chain + }) + + styled-body + + style-state.update(chain => { + _ = chain.pop() + chain + }) + } else { + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + styled-body + } + }#lbl-get] + } + + [#metadata( + ( + (special-rule-key): "select", + version: element-version, + filters: filters, + computed: (filters-by-eid: filters-by-eid, labels-by-eid: labels-by-eid, ordered-eids: ordered-eids), + receiver: receiver, + prefix: prefix + ) + )#lbl-special-rule-tag] +} + +/// Apply filtered rules to a custom element's descendants +/// (but not to itself; for that use `cond-set`). +/// +/// USAGE: +/// +/// ```typ +/// #show: e.filtered( +/// elem, +/// e.set_(elem3, fields: ...) +/// ) +/// ``` +/// +/// When applying many set rules at once, use 'apply' instead of 'set' on the last parameter. +/// +/// - filter (filter): filter specifying which element instances should create this set rule +/// for their children. +/// - rule (rule): which rule to create under matched elements. +/// -> function +#let filtered(filter, rule) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.filtered: invalid filter, please use 'custom-element.with(...)' to generate a filter.") + assert(type(rule) == function, message: "elembic: element.filtered: this is not a valid rule (not a function), please use functions such as 'set_' to create one.") + assert("elements" in filter, message: "elembic: element.filtered: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(filter.elements != (:), message: "elembic: element.filtered: this filter appears to not be restricted to any elements and is thus impossible to match. It must apply to exactly one element (the one receiving the set rule). Consider using a different filter.") + assert(filter.elements != none, message: "elembic: element.filtered: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + + let rule = rule([]).children.last().value.rule + let filtered-rule = ((prepared-rule-key): true, version: element-version, kind: "filtered", filter: filter, rule: rule, name: none, names: (), mode: rule.at("mode", default: auto)) + if rule.kind == "apply" { + // Transpose filtered(filter, apply(a, b, c)) into apply(filtered(filter, a), filtered(filter, b), filtered(filter, c)) + let i = 0 + for inner-rule in rule.rules { + assert(inner-rule.kind in ("show", "set", "revoke", "reset", "cond-set", "filtered"), message: "elembic: element.filtered: can only filter apply, show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + inner-rule.kind + "'") + + rule.rules.at(i) = (..filtered-rule, rule: inner-rule, mode: inner-rule.at("mode", default: auto)) + + i += 1 + } + + // Keep the apply but with everything filtered. + prepare-rule(rule) + } else { + assert(rule.kind in ("show", "set", "revoke", "reset", "cond-set", "filtered"), message: "elembic: element.filtered: can only filter apply, show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + rule.kind + "'") + + prepare-rule(filtered-rule) + } +} + +/// Apply a conditional set rule to a custom element. The set rule is only applied if +/// the given filter matches for that element. +/// +/// Check out the Styling guide for more information. +/// +/// Note that this function only accepts non-required fields (that have a `default`). +/// Any required fields must always be specified at call site and, as such, are always +/// going to be prioritized, so it is pointless to have set rules for those. +/// +/// Keep in mind the limitations when using set rules, as well as revoke, reset and +/// apply rules. +/// +/// As such, when applying many set rules at once, please use `e.apply` instead +/// (or specify them consecutively so `elembic` does that automatically). +/// +/// USAGE: +/// +/// ```typ +/// #show: e.set_(superbox, fill: red) +/// #show: e.cond-set(superbox.with(data: 10), fill: blue) +/// +/// #superbox(data: 5) // this will have red fill +/// #superbox(data: 10) // this will have blue fill +/// ``` +/// +/// - filter (filter): filter specifying which element instances should receive this set rule. +/// - fields (arguments): optional fields to set (positionally or named, depending on the field) +/// -> function +#let cond-set(filter, ..fields) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.cond-set: invalid filter, please pass just 'custom-element' or use 'custom-element.with(...)' to generate a filter.") + assert("elements" in filter, message: "elembic: element.cond-set: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(filter.elements != (:), message: "elembic: element.cond-set: this filter appears to not be restricted to any elements and is thus impossible to match. It must apply to exactly one element (the one receiving the set rule). Consider using a different filter.") + assert(filter.elements != none, message: "elembic: element.cond-set: this filter appears to apply to any element. It must apply to exactly one element (the one receiving the set rule). Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + assert(filter.elements.len() == 1, message: "elembic: element.cond-set: this filter appears to apply to more than one element. It must apply to exactly one element (the one receiving the set rule).") + let (eid, elem) = filter.elements.pairs().first() + + let (res, args) = (elem.parse-args)(fields, include-required: false) + if not res { + assert(false, message: args) + } + + prepare-rule( + ((prepared-rule-key): true, version: element-version, kind: "cond-set", name: none, names: (), mode: auto, filter: filter, element: (eid: elem.eid, default-data: elem.default-data, fields: elem.fields), args: args) + ) +} + +/// Applies a show rule through the elembic stylechain, thus making it +/// revokable and also allowing easy usage of filters. +/// +/// Show rules allow you to transform all occurrences of one or more elements, +/// replacing them with arbitrary document content. +/// +/// For example: +/// +/// ```typ +/// #show: e.show_(elem.with(fill: blue), it => [Hello *#it*!]) +/// +/// #elem(fill: red)[First] +/// #elem(fill: blue)[Second] // displays as "Hello *Second*!" +/// ``` +/// +/// - filter (filter): which element(s) to apply the rule to, with which fields etc. +/// - callback (function | content | str | none): replacement content or transformation function (content -> content) +/// receiving any matched elements and returning what to replace it with. +/// -> function +#let show_(filter, replacement, mode: auto) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.show_: invalid filter, please use 'custom-element.with(...)' to generate a filter.") + assert(replacement == none or type(replacement) in (function, str, content), message: "elembic: element.show_: second parameter is not a valid show rule replacement or callback. Must be either a function 'it => content', or the content to unconditionally replace by (if it does not depend on the matched element). For example, you can write 'show: e.show_(elem, it => [*#it*])' to make an element bold, or 'show: e.show_(elem, [Hi])' to always replace it with the word 'Hi'.") + + let callback = replacement + if type(replacement) != function { + replacement = [#replacement] + callback = _ => replacement + } + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "show", filter: filter, callback: callback, name: none, names: (), mode: mode)) +} + +/// Apply multiple rules (set rules, etc.) at once. +/// +/// These rules do not count towards the "set rule limit" observed in 'Limitations'; +/// `apply` itself will always count as a single rule regardless of the amount of rules +/// inside it (be it 5, 50, or 500). Therefore, +/// **it is recommended to group rules together under `apply` whenever possible.** +/// +/// Note that Elembic will automatically wrap consecutive rules (only whitespace +/// or native set/show rules inbetween) into a single `apply`, bringing the same benefit. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.apply( +/// set_(elem, fields), +/// set_(elem, fields) +/// ) +/// ``` +/// +/// - mode (int): style mode given by the `style-modes` dictionary +/// - args (arguments): rules to apply +/// -> function +#let apply(mode: auto, ..args) = { + assert(args.named() == (:), message: "elembic: element.apply: unexpected named arguments") + assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.apply: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") + + let rules = args.pos().map( + rule => { + assert(type(rule) == function, message: "elembic: element.apply: invalid rule of type " + str(type(rule)) + ", please use 'set_' or some other function from this library to generate it") + + // Call it as if it we were in a show rule. + // It will have some trailing metadata indicating its arguments. + let inner = rule([]) + let rule-data = inner.children.last().value.rule + + if rule-data.kind == "apply" { + // Flatten 'apply' + rule-data.rules + } else { + (rule-data,) + } + } + ).sum(default: ()) + + if mode == auto { + mode = rules.fold(auto, (mode, rule) => { + if ( + rule.mode == style-modes.stateful + or mode != style-modes.stateful and rule.mode == style-modes.leaky + or mode == auto + ) { + // Prioritize more explicit modes: + // stateful > leaky > normal + rule.mode + } else { + mode + } + }) + } + + if mode != auto { + rules = rules.map(r => r + (mode: mode)) + } + + // Set this apply rule's mode as an optimization, but note that we have forcefully altered + // its children's modes above. + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "apply", rules: rules, mode: mode)) +} + +#let settings(..args, mode: auto) = { + assert(args.pos() == (), message: "elembic: element.settings: unexpected positional args") + let args = args.named() + assert(args != (:), message: "elembic: element.settings: please specify some setting, e.g. e.settings(prefer-leaky: true)") + + let write = (:) + let transform = () + for (key, val) in args { + if key not in default-global-data.settings { + assert(false, message: "elembic: element.settings: invalid setting '" + key + "', valid keys are " + default-global-data.settings.keys().map(repr).join(", ")) + } + + let default-setting = default-global-data.settings.at(key) + if key in ("track-ancestry", "store-ancestry") and val != "any" { + if type(val) == array { + let new-elements = (:) + for elem in val { + if type(elem) == function { + elem = data(elem) + } + if type(elem) != dictionary or "eid" not in elem { + assert(false, message: "elembic: element.settings: expected array of elements or literal \"any\" (apply to any element) for setting '" + key + "', got array of '" + str(type(elem)) + "'") + } + + new-elements.insert(elem.eid, elem) + } + + if new-elements != (:) { + transform.push(s => { + let existing = if s != none and key in s { s.at(key) } else { (:) } + if existing == "any" { + // Nothing to change, already applies to all elements + s + } else { + (:..s, (key): (:) + existing + new-elements) + } + }) + } + } else { + assert(false, message: "elembic: element.settings: expected array of elements or literal \"any\" (apply to any element) for setting '" + key + "', got '" + str(type(val)) + "'") + } + } else if key == "prefer-leaky" and type(val) != type(default-setting) { + assert(false, message: "elembic: element.settings: expected type of '" + str(type(default-setting)) + "' for setting '" + key + "', got '" + str(type(val)) + "'") + } else { + write.insert(key, val) + } + } + + let transform = if transform == () { + none + } else if transform.len() == 1 { + transform.first() + } else { + s => transform.fold(s, (acc, fun) => fun(acc)) + } + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "settings", write: write, transform: transform, mode: mode)) +} + +/// Name a certain rule. Use `e.apply` to name a group of rules. +/// This is used to be able to revoke the rule later with `e.revoke`. +/// +/// Please note that, at the moment, each rule can only have +/// one name. This means that applying multiple `named` on +/// the same set of rules will simply replace the previous +/// names. +/// +/// However, more than one rule can have the same name, allowing both to be +/// revoked at once if needed. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.named( +/// "cool set", +/// e.set_(elem, fields) +/// ) +/// ``` +/// +/// - name (str): The name to give to the rule. +/// - rule (function): The rule to apply this name to. +/// -> function +#let named(..names, rule) = { + assert(names.named() == (:), message: "elembic: element.named: unexpected named arguments") + let names = names.pos() + assert(names != (), message: "elembic: element.named: expected at least two arguments (one or more names and a rule)") + assert(type(rule) == function, message: "elembic: element.named: last parameter is not a valid rule (not a function), please use functions such as 'set_' to create one.") + for name in names { + assert(type(name) == str, message: "elembic: element.named: rule name must be a string, not " + str(type(name))) + assert(name != "", message: "elembic: element.named: name must not be empty") + } + + // For backwards compatibility when only one name was possible + let compat-name = names.last() + let rule = rule([]).children.last().value.rule + if rule.kind == "apply" { + let i = 0 + for inner-rule in rule.rules { + assert(inner-rule.kind in ("show", "set", "revoke", "reset", "filtered", "cond-set"), message: "elembic: element.named: can only name show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + inner-rule.kind + "'") + + rule.rules.at(i).name = compat-name + + if "names" in inner-rule { + rule.rules.at(i).names += names + } else { + rule.rules.at(i).names = names + } + + i += 1 + } + } else { + assert(rule.kind in ("show", "set", "revoke", "reset", "filtered", "cond-set"), message: "elembic: element.named: can only name show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + rule.kind + "'") + rule.name = compat-name + + if "names" in rule { + rule.names += names + } else { + rule.names = names + } + } + + // Re-prepare the rule + prepare-rule(rule) +} + +/// Revoke all rules with a certain name. +/// +/// This is intended to be used in a specific scope, +/// and temporary. This means you are supposed to only revoke the rule +/// for a short portion of the document. If you wish to do the opposite, +/// that is, only apply the rule for a short portion for the document +/// (and have it never apply again afterwards), then please just scope +/// the set rule itself instead. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.named("name", set_(element, fields)) +/// ... +/// #[ +/// #show: e.revoke("name") +/// // rule 'name' doesn't apply here +/// ... +/// ] +/// +/// // Applies here again +/// ... +/// ``` +/// +/// - name (str): name of rules to be revoked +/// - mode (int): style mode given by the `style-modes` dictionary +/// -> function +#let revoke(name, mode: auto) = { + assert(type(name) == str, message: "elembic: element.revoke: rule name must be a string, not " + str(type(name))) + assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.revoke: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "revoke", revoking: name, name: none, names: (), mode: mode)) +} + +/// Temporarily revoke all active set rules for certain elements (or even all elements if none are specified). +/// Applies only to the current scope, like other rules. +/// +/// USAGE: +/// +/// ```typ +/// #show: e.set_(element, fill: red) +/// #[ +/// // Revoke all previous set rules on 'element' for this scope +/// #show: e.reset(element) +/// #element[This is using the default fill (not red)] +/// ] +/// +/// // Rules not revoked outside the scope +/// #element[This is using red fill] +/// ``` +/// +/// - args (arguments): elements whose rules should be reset, or none to reset all rules +/// - mode (int): style mode given by the `style-modes` dictionary +/// -> function +#let reset(..args, mode: auto) = { + assert(args.named() == (:), message: "elembic: element.reset: unexpected named arguments") + assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.reset: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") + + let filters = args.pos().map(it => if type(it) == function { data(it) } else { it }) + assert(filters.all(x => type(x) == dictionary and "eid" in x), message: "elembic: element.reset: invalid arguments, please provide a function or element data with at least an 'eid'") + + prepare-rule(((prepared-rule-key): true, version: element-version, kind: "reset", eids: filters.map(x => x.eid), name: none, names: (), mode: mode)) +} + +// Stateful variants +#let stateful-set(..args) = { + apply(set_(..args), mode: style-modes.stateful) +} +#let stateful-cond-set(..args) = { + apply(cond-set(..args), mode: style-modes.stateful) +} +#let stateful-settings = settings.with(mode: style-modes.stateful) +#let stateful-apply = apply.with(mode: style-modes.stateful) +#let stateful-show = show_.with(mode: style-modes.stateful) +#let stateful-revoke = revoke.with(mode: style-modes.stateful) +#let stateful-reset = reset.with(mode: style-modes.stateful) + +// Leaky variants +#let leaky-set(..args) = { + apply(set_(..args), mode: style-modes.leaky) +} +#let leaky-cond-set(..args) = { + apply(cond-set(..args), mode: style-modes.leaky) +} +#let leaky-settings = settings.with(mode: style-modes.leaky) +#let leaky-apply = apply.with(mode: style-modes.leaky) +#let leaky-show = show_.with(mode: style-modes.leaky) +#let leaky-revoke = revoke.with(mode: style-modes.leaky) +#let leaky-reset = reset.with(mode: style-modes.leaky) + +#let leaky-toggle(enable) = leaky-settings(prefer-leaky: enable) + +// Apply revokes and other modifications to the chain and generate a final set +// of fields. +#let fold-styles(chain, data-chain, revoke-chain, fold-chain) = { + // Map name -> up to which index (exclusive) it is revoked. + // + // Importantly, a revoke at index B will apply to + // all rules with the revoked name before that index. + // If that revoke rule is, itself, revoked, that either + // completely eliminates the name from being revoked, + // or it simply leads the name to be revoked up to + // an index A < B. That, or it was also being revoked + // by another unrevoked revoke rule at index C > B, + // in which case the name is still revoked up to C. + // In all cases, the name is always revoked from the + // start until some end index. Otherwise, it isn't + // revoked at all (end index 0). + let active-revokes = (:) + + let first-active-index = 0 + + // Revoke revoked revokes by analyzing revokes in reverse + // order: a revoke that came later always takes priority. + for revoke in revoke-chain.rev() { + // This revoke will revoke rules named 'revoking' up to 'index' in the chain, which + // automatically revokes revoke rules before it as well, since they were added when + // the chain length was smaller (or the same), and 'index' is always the chain length + // at the moment the revoke rule was added. + // + // We don't explicitly add revoke rules to the chain as their order in the revoke-chain + // list is enough to know which revoke rules can revoke others, and the index indicates + // which set rules are revoked. + // + // Regarding the first part of the AND, note that, if a name is already revoked up to + // index C from a later revoke (since we're going in reverse, so this one appears earlier + // than the previous ones), then revoking it up to index B <= C for this revoke is + // unnecessary since the index interval [0, B) is already contained in [0, C). + // + // In other words, only the last revoke for a particular name matters, which is the + // first one we find in this loop. + // + // (As you can see, we assume above that, if revoke 1 comes before revoke 2 in the revoke-chain + // (before reversing), with revoke 1 applying up to chain index B and revoke 2 up to index C, + // then B <= C. This is enforced in 'prepare-rules' as we analyze revokes and push their + // information to the chain in order (outer to inner / earlier to later).) + let was-not-revoked = ( + ( + "names" not in revoke or revoke.names.all(n => n not in active-revokes) + ) + and ( + "names" in revoke or "name" not in revoke or revoke.name == none or revoke.name not in active-revokes + ) + ) + + if revoke.kind == "revoke" and revoke.revoking not in active-revokes and was-not-revoked { + active-revokes.insert(revoke.revoking, revoke.index) + } else if revoke.kind == "reset" and was-not-revoked { + // Applying a reset, so we delete everything before this index and stop revoking since + // any revokes before this reset won't count anymore. + first-active-index = revoke.index + + chain = if chain.len() <= first-active-index { + () + } else { + chain.slice(first-active-index) + } + + data-chain = if data-chain.len() <= first-active-index { + () + } else { + data-chain.slice(first-active-index) + } + + for (field-name, fold-data) in fold-chain { + let first-fold-index = fold-data.data.position(d => d.index >= first-active-index) + if first-fold-index == none { + // All folded values removed. + // The caller will be responsible for joining the default value with the + // final arguments (without any chain values inbetween) if that's necessary. + _ = fold-chain.remove(field-name) + } else { + fold-chain.at(field-name).values = fold-data.values.slice(first-fold-index) + fold-chain.at(field-name).data = fold-data.data.slice(first-fold-index) + } + } + + // No need to analyze any further revoke rules since everything was reset. + break + } + } + + if active-revokes != (:) { + let i = first-active-index + for data in data-chain { + if data != none and ( + "names" in data and data.names.any(n => n in active-revokes and i < active-revokes.at(n)) + or "names" not in data and "name" in data and data.name in active-revokes and i < active-revokes.at(data.name) + ) { + // Nullify changes at this stage + chain.at(i) = (:) + } + + i += 1 + } + + for (field-name, fold-data) in fold-chain { + let filtered-data = fold-data.data.filter(d => ( + // Only keep data without a name in the revoked name map, or, if the + // name is there, then data that came after the name was revoked. + ("names" not in d or d.names.all(n => n not in active-revokes or d.index >= active-revokes.at(n))) + and ("names" in d or "name" not in d or (d.name == none or d.name not in active-revokes or d.index >= active-revokes.at(d.name))) + )) + if filtered-data == () { + _ = fold-chain.remove(field-name) + } else { + fold-chain.at(field-name).data = filtered-data + fold-chain.at(field-name).values = filtered-data.map(d => d.value) + } + } + } + + let final-values = chain.sum(default: (:)) + + // Apply folds separately (their fields' values are meaningless in the above dict) + for (field-name, fold-data) in fold-chain { + final-values.at(field-name) = if fold-data.values == () { + fold-data.default + } else if fold-data.folder == auto { + fold-data.default + fold-data.values.sum() + } else { + fold-data.values.fold(fold-data.default, fold-data.folder) + } + } + + (folded: final-values, active-revokes: active-revokes, first-active-index: first-active-index) +} + +// Retrieves the final chain data for an element, after applying all set rules so far. +#let get-styles(element, elements: (:), use-routine: false) = { + if type(element) == function { + element = data(element) + } + let (eid, default-fields) = if type(element) == dictionary and "eid" in element and "default-fields" in element { + (element.eid, element.default-fields) + } else { + assert(false, message: "elembic: element.get: expected element (function / data dictionary), received " + str(type(element))) + } + + if ( + use-routine + and ("version" not in element or element.version != element-version) + and "routines" in element + and "get-styles" in element.routines + and type(element.routines.get-styles) == function + ) { + // Use the element's own "get styles". + return (element.routines.get-styles)(element, elements: elements) + } + + let element-data = elements.at(eid, default: default-data) + let folded-chain = if element-data.revoke-chain == default-data.revoke-chain and element-data.fold-chain == default-data.fold-chain { + element-data.chain.sum(default: (:)) + } else { + fold-styles(element-data.chain, element-data.data-chain, element-data.revoke-chain, element-data.fold-chain).folded + } + + // No need to do extra folding like in constructor: + // if a foldable field hasn't been specified, it is either equal to + // its default, or it is a required field which has no default and + // thus it is not returned here since it can't be set. + default-fields + folded-chain +} + +/// Reads the current values of element fields after applying set rules. +/// Must be in a context block. +/// +/// This is a stateful version, which doesn't require a callback, but only +/// works on stateful mode (less performant). +/// +/// USAGE: +/// ```typ +/// #show: e.set_(elem, fill: green) +/// // ... +/// #context { +/// // OK +/// assert(e.stateful.get(elem).fill == green) +/// } +/// ``` +/// +/// - receiver (function): function ('get' function) -> content +/// -> content +#let stateful-get(element) = { + let chain = style-state.get() + let global-data = if chain == () { + default-global-data + } else { + chain.last() + } + + assert( + global-data.stateful, + message: "elembic: stateful.get: cannot use this function without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.toggle(true)' somewhere above the 'context {}' in which this call happens, or at the top of the document to apply to all rules as well" + ) + + get-styles(element, elements: global-data.elements, use-routine: true) +} + +#let prepare-ctx(receiver, include-global: false) = context { + let previous-bib-title = bibliography.title + [#context { + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + if global-data.stateful { + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + } + + set bibliography(title: previous-bib-title) + + let getter = get-styles.with(elements: global-data.elements, use-routine: true) + if include-global { + receiver((:..global-data, ctx: (get: getter))) + } else { + receiver(getter) + } + }#lbl-get] +} + +/// Reads the current values of element fields after applying set rules. +/// +/// The callback receives a 'get' function which can be used to read the +/// values for a given element. The content returned by the function, which +/// depends on those values, is then placed into the document. +/// +/// USAGE: +/// ```typ +/// #show: e.set_(elem, fill: green) +/// // ... +/// #e.get(get => { +/// // OK +/// assert(get(elem).fill == green) +/// }) +/// ``` +/// +/// - receiver (function): function ('get' function) -> content +/// -> content +#let prepare-get(receiver) = { + let output = prepare-ctx(include-global: false, receiver) + [#output#metadata(((special-rule-key): "get", version: element-version, receiver: receiver))#lbl-special-rule-tag] +} + +#let prepare-debug(receiver) = { + let output = prepare-ctx(include-global: true, receiver) + [#output#metadata(((special-rule-key): "debug-get", version: element-version, receiver: receiver))#lbl-special-rule-tag] +} + +// Obtain a Typst selector to use to match this element in show rules or in the outline. +// Specify 'meta: true' to match this element in a query, as that selector is +// generated once regardless of show rules. +#let elem-selector(elem, outline: false, outer: false, meta: false) = { + if outline { + assert(not outer, message: "elembic: element.selector: cannot have 'outline: true' and 'outer: true' at the same time, please pick one selector") + assert(not meta, message: "elembic: element.selector: cannot have 'outline: true' and 'meta: true' at the same time, please pick one selector") + let elem-data = data(elem) + assert("outline-sel" in elem-data, message: "elembic: element.selector: this isn't a valid element") + assert(elem-data.outline-sel != none, message: "elembic: element.selector: this element isn't outlinable\n hint: try asking its author to define it as such with 'outline: auto', 'outline: (caption: [...])' or 'outline: (caption: it => ...)'") + elem-data.outline-sel + } else if outer { + assert(not meta, message: "elembic: element.selector: cannot have 'outer: true' and 'meta: true' at the same time, please pick one selector") + data(elem).outer-sel + } else if meta { + let elem-data = data(elem) + elem-data.at("meta-sel", default: elem-data.sel) + } else { + data(elem).sel + } +} + +#let elem-query(filter, before: none, after: none) = { + if type(filter) == function { + filter = filter(__elembic_data: special-data-values.get-where) + } + + if type(filter) != dictionary or filter-key not in filter { + if type(filter) == selector { + assert(false, message: "elembic: element.query: Typst-native selectors cannot be specified here, only those of custom elements") + } + assert(false, message: "elembic: element.query: expected a valid filter, such as 'custom-element' or 'custom-element.with(field-name: value, ...)', got " + base.typename(filter)) + } + + assert("elements" in filter, message: "elembic: element.query: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") + assert(filter.elements != none, message: "elembic: element.query: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") + + let results = () + for (eid, elem-data) in filter.elements { + if "meta-sel" in elem-data { + let sel = elem-data.meta-sel + if before != none { + sel = selector(sel).before(before) + } + if after != none { + sel = selector(sel).after(after) + } + + results += query(sel).filter( + instance => ( + instance.func() == metadata + and { + let meta = data(instance.value) + + verify-filter( + meta.at("fields", default: (:)), + eid: eid, + filter: filter, + ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry and meta.at("ctx", default: none) != none and "ancestry" in meta.ctx { + meta.ctx.ancestry + } else { + () + } + ) and "rendered" in instance.value + } + ) + ).map( + instance => instance.value.rendered + ) + } else if "sel" in elem-data { + let sel = elem-data.sel + if before != none { + sel = selector(sel).before(before) + } + if after != none { + sel = selector(sel).after(after) + } + // This element is probably too outdated to have ancestry checks anyway, so we don't bother + results += query(sel).filter(instance => verify-filter(data(instance).at("fields", default: (:)), eid: eid, filter: filter, ancestry: ())) + } else { + assert(false, message: "elembic: element.query: filter did not have the element's meta selector") + } + } + + results +} + +/// Applies necessary show rules to the entire document so that custom elements behave +/// properly. This is usually only needed for elements which have custom references, +/// since, in that case, the document-wide rule `#show ref: e.ref` is required. +/// **It is recommended to always use `e.prepare` when using Elembic.** +/// +/// However, **some custom elements also have their own `prepare` functions.** (Read +/// their documentation to know if that's the case.) Then, you may specify their functions +/// as parameters to this function, and this function will run the `prepare` function of +/// each element. Not specifying any elements will just run the default rules, which may +/// still be important. +/// +/// As an example, an element may use its own `prepare` function to apply some special +/// behavior to its `outline`. +/// +/// USAGE: +/// ```rs +/// // Apply default rules + special rules for these elements (if they need it) +/// #show: e.prepare(elemA, elemB) +/// +/// // Apply default rules only +/// #show: e.prepare() +/// ``` +/// - args (arguments): element functions which need special preparation, or none to just apply default rules +/// -> function +#let prepare( + ..args +) = { + assert(args.named() == (:), message: "elembic: element.prepare: unexpected named arguments") + let default-rules = doc => { + show ref: ref_ + + doc + } + + if args.pos() == () { + return default-rules + } + + let elems = args.pos().map(data) + + if elems.len() == 1 and type(args.pos().first()) == content { + assert(false, message: "elembic: element.prepare: expected (optional) element functions as arguments, not the document\n hint: write '#show: e.prepare()', not '#show: e.prepare' - note the parentheses") + } + + assert(elems.all(it => it.data-kind == "element"), message: "elembic: element.prepare: positional arguments must be elements") + let prepares = elems.filter(elem => "prepare" in elem and elem.prepare != none).map(elem => elem.prepare.with(elem.func)) + + doc => { + show: default-rules + prepares.fold(doc, (acc, prepare) => prepare(acc)) + } +} + +/// Creates a new element, returning its constructor. Read the "Creating custom elements" +/// chapter for more information. +/// +/// USAGE: +/// +/// ```typ +/// #import "@preview/elembic:X.X.X" as e: field +/// +/// // For references to apply +/// #show: e.prepare() +/// +/// #let elem = e.element.declare( +/// "elem", +/// prefix: "@preview/my-package,v1", +/// display: it => { +/// [== #it.title] +/// block(fill: it.fill)[#it.inner] +/// }, +/// fields: ( +/// field("fill", e.types.option(e.types.paint)), +/// field("inner", content, default: [Hello!]), +/// field("title", content, default: [Hello!]), +/// ), +/// reference: ( +/// supplement: [Elem], +/// numbering: "1" +/// ), +/// outline: (caption: it => it.title), +/// ) +/// +/// #outline(target: e.selector(elem, outline: true)) +/// +/// #elem() +/// #elem(title: [abc], label: ) +/// @abc +/// ``` +/// +/// - name (str): The element's name. +/// - prefix (str): The element's prefix, used to distinguish it from elements with the same name. This is usually your package's name alongside a (major) version. +/// - display (function): Function `fields => content` to display the element. +/// - fields (array): Array with this element's fields. +/// - parse-args (auto | function): Optional override for the built-in argument parser +/// (or `auto` to keep as is). Must be in the form +/// `function(args, include-required: bool) => dictionary`, where `include-required: true` +/// means required fields are enforced (constructor), while `include-required: false` means +/// they are forbidden (set rules). +/// - typecheck (bool): Set to `false` to disable field typechecking. +/// - allow-unknown-fields (bool): Set to `true` to allow users to specify unknown +/// fields to your element. They are not typechecked and are simply forwarded to +/// the element's fields by the argument parser. +/// - template (none | function): Optional function displayed element => content to define overridable default set rules for your elements, such as paragraph settings. Users can override these settings with show-set rules on elements. +/// - prepare (none | function): Optional function (element, document) => content +/// to define show and set rules that should be applied to the whole document for your +/// element to properly function. +/// - construct (none | function): Optional function that overrides the default +/// element constructor, returning arbitrary content. This should be used over +/// manually wrapping the returned constructor as it ensures set rules and data +/// extraction from the constructor still work. +/// - scope (none | dictionary | module): Optional scope with associated data for your +/// element. This could be a module with constructors for associated elements, for +/// instance. This value can be accessed with `e.scope(elem)`, e.g. +/// `#import e.scope(elem): sub-elem`. +/// - count (none | function): Optional function `counter => (content | function fields => content)` +/// which inserts a counter step before the element. Ensures the element's display function has +/// updated context to get the latest counter value (after the step / update) with +/// `e.counter(it).get()`. Defaults to `counter.step` to step the counter once before +/// each element placed. +/// - labelable (bool): Defaults to `true`, allows specifying `#element(label: )`, which +/// not only ensures show rules on that label work and have access to the element's final fields, +/// but also allows referring to that element. When `false`, the element may have a field +/// named `label` instead, but it won't have these effects. +/// - reference (none | (supplement: none | str | content | function fields => str | content, numbering: none | str | function fields => str | function, custom: none | function fields => content)): +/// When not `none`, allows referring to the new element with Typst's built-in +/// `@ref` syntax. Requires the user to execute `#show: e.prepare()` at the top +/// of their document (it is part of the default rules, so `prepare` needs no +/// arguments there). Specify either a `supplement` and `numbering` for references +/// looking like "Name 2", and/or `custom` to show some fully customized content +/// for the reference instead. +/// - outline (none | auto | dictionary): +/// Accepts either `auto` or a dictionary of the form +/// `(caption: str | content | function fields => content)`. +/// When not `none`, allows creating an outline for the element's appearances +/// with `#outline(target: e.selector(elem, outline: true))`. When set to `auto`, +/// the entries will display "Name 2" based on reference information. When a caption +/// is specified, it will display as "Name 2: caption", unless supplement and numbering +/// for reference are both none. +/// - synthesize (none | function): Can be set to a function `fields => fields` to +/// override final values of fields, or create new fields based on final values of +/// fields, before the first show rule. When computing new fields based on other +/// fields, please specify those new fields in the fields array with +/// `synthesized: true`. This forbids the user from specifying them manually, +/// but allows them to filter based on that field. +/// - contextual (bool): When set to `true`, functions `fields => something` for +/// other options, including `display`, will be able to access the current +/// values of set rules with `(e.ctx(fields).get)(other-elem)`. In addition, +/// an additional context block is created, so that you may access the correct +/// values for `native-elem.field` in the context. In practice, this is a bit +/// expensive, and so this option shouldn't be enabled unless you need precisely +/// `bibliography.title`, or you really need to get set rule information from +/// other elements within functions such as `synthesize` or `display`. +/// -> function +#let declare( + name, + display: none, + fields: none, + prefix: none, + parse-args: auto, + typecheck: true, + allow-unknown-fields: false, + template: none, + prepare: none, + construct: none, + scope: none, + count: counter.step, + labelable: true, + reference: none, + outline: none, + synthesize: none, + contextual: false, +) = { + assert(type(display) == function, message: "elembic: element.declare: please specify a show rule in 'display:' to determine how your element is displayed.") + + let fields-hint = if type(fields) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single field: write 'fields: (field,)', not 'fields: (field)'" } else { "" } + assert(type(fields) == array, message: "elembic: element.declare: please specify an array of fields, creating each field with the 'field' function. It can be empty with '()'." + fields-hint) + assert(prefix != none, message: "elembic: element.declare: please specify a 'prefix: ...' for your type, to distinguish it from types with the same name. If you are writing a package or template to be used by others, please do not use an empty prefix.") + assert(type(prefix) == str, message: "elembic: element.declare: the prefix must be a string, not '" + str(type(prefix)) + "'") + assert(parse-args == auto or type(parse-args) == function, message: "elembic: element.declare: 'parse-args' must be either 'auto' (use built-in parser) or a function (default arg parser, fields: dictionary, typecheck: bool) => (user arguments, include-required: true (required fields must be specified - in constructor) / false (required fields must be omitted - in set rules)) => (bool (true on success, false on error), dictionary with parsed fields (or error message string if the bool is false)).") + assert(type(typecheck) == bool, message: "elembic: element.declare: the 'typecheck' argument must be a boolean (true to enable typechecking, false to disable).") + assert(type(allow-unknown-fields) == bool, message: "elembic: element.declare: the 'allow-unknown-fields' argument must be a boolean.") + assert(template == none or type(template) == function, message: "elembic: element.declare: 'template' must be 'none' or a function displayed element => content (usually set rules applied on the displayed element). This is used to add a set of overridable set rules to the element, such as paragraph settings.") + assert(prepare == none or type(prepare) == function, message: "elembic: element.declare: 'prepare' must be 'none' or a function (element, document) => styled document (used to apply show and set rules to the document).") + assert(count == none or type(count) == function, message: "elembic: element.declare: 'count' must be 'none', a function counter => counter step/update element, or a function counter => final fields => counter step/update element.") + assert(synthesize == none or type(synthesize) == function, message: "elembic: element.declare: 'synthesize' must be 'none' or a function element fields => element fields.") + assert(contextual == auto or type(contextual) == bool, message: "elembic: element.declare: 'contextual' must be 'auto' (true if using a contextual feature) or a boolean (true to wrap the output in a 'context { ... }', false to not).") + assert(construct == none or type(construct) == function, message: "elembic: element.declare: 'construct' must be 'none' (use default constructor) or a function receiving the original constructor and returning the new constructor.") + assert(scope == none or type(scope) in (dictionary, module), message: "elembic: element.declare: 'scope' must be either 'none', a dictionary or a module") + assert(type(labelable) == bool, message: "elembic: element.declare: 'labelable' must be a boolean (true to enable the special 'label' constructor argument, false to disable it)") + assert( + reference == none + or type(reference) == dictionary + and reference.keys().all(x => x in ("supplement", "numbering", "custom")) + and ("supplement" not in reference or reference.supplement == none or type(reference.supplement) in (str, content, function)) + and ("numbering" not in reference or reference.numbering == none or type(reference.numbering) in (str, function)) + and ("custom" not in reference or reference.custom == none or type(reference.custom) == function), + message: "elembic: element.declare: 'reference' must be 'none' or a dictionary (supplement: \"Name\" or [Name] or function fields => supplement, numbering: \"1.\" or function fields => (str / function numbers => content), custom (optional): none (default) or function fields => content)." + ) + assert( + reference == none or "supplement" in reference and "numbering" in reference or "custom" in reference, + message: "elembic: element.declare: reference must either have 'custom', or have both 'supplement' and 'numbering' (or all three, though 'custom' has priority when displaying references)." + ) + assert( + outline == none + or outline == auto + or type(outline) == dictionary + and "caption" in outline, + message: "elembic: element.declare: 'outline' must be 'none', 'auto' (to use data from 'reference') or a dictionary with 'caption'." + ) + assert(outline != auto or reference != none, message: "elembic: element.declare: if 'outline' is set to 'auto', 'reference' must be specified and not be 'none'.") + assert(labelable or reference == none, message: "elembic: element.declare: 'labelable' must be true for 'reference' to not be 'none'") + + // All element args as originally provided. + let elem-args = arguments( + name, + display: display, + fields: fields, + prefix: prefix, + parse-args: parse-args, + typecheck: typecheck, + allow-unknown-fields: allow-unknown-fields, + template: template, + prepare: prepare, + construct: construct, + scope: scope, + count: count, + labelable: labelable, + reference: reference, + outline: outline, + synthesize: synthesize, + contextual: contextual, + ) + + if contextual == auto { + // Provide separate context for synthesize. + // By default, assume it isn't needed. + contextual = synthesize != none + } + + let eid = base.unique-id("e", prefix, name) + let lbl-show = label(lbl-show-head + eid) + let lbl-meta = label(lbl-meta-head + eid) + let lbl-outer = label(lbl-outer-head + eid) + let ref-figure-kind = if reference == none and outline == none { none } else { lbl-ref-figure-kind-head + eid } + // Use same counter as hidden figure for ease of use + let counter-key = lbl-counter-head + eid + let element-counter = counter(counter-key) + let count = if count == none { none } else { count(element-counter) } + let count-needs-fields = type(count) == function + let custom-ref = if reference != none and "custom" in reference and type(reference.custom) == function { reference.custom } else { none } + + let supplement-type = if reference == none or "supplement" not in reference { + none + } else { + type(reference.supplement) + } + let numbering-type = if reference == none or "numbering" not in reference { + none + } else { + type(reference.numbering) + } + let caption-type = if outline == none or outline == auto { + none + } else { + type(outline.caption) + } + + let fields = field-internals.parse-fields(fields, allow-unknown-fields: allow-unknown-fields) + let (all-fields, user-fields, foldable-fields) = fields + + if labelable and "label" in all-fields { + assert(false, message: "elembic: element.declare: labelable element cannot have a conflicting 'label' field\n hint: you can set 'labelable: false' to disable the special label parameter, but note that it will then be impossible to refer to your element") + } + + let default-arg-parser = field-internals.generate-arg-parser( + fields: fields, + general-error-prefix: "elembic: element '" + name + "': ", + field-error-prefix: field-name => "field '" + field-name + "' of element '" + name + "': ", + typecheck: typecheck + ) + + let parse-args = if parse-args == auto { + default-arg-parser + } else { + let parse-args = parse-args(default-arg-parser, fields: fields, typecheck: typecheck) + if type(parse-args) != function { + assert(false, message: "elembic: element.declare: 'parse-args', when specified as a function, receives the default arg parser alongside `fields: fields dictionary` and `typecheck: bool`, and must return a function (the new arg parser), and not " + base.typename(parse-args)) + } + + parse-args + } + + let default-fields = fields.user-fields.values().map(f => if f.required { (:) } else { ((f.name): f.default) }).sum(default: (:)) + + let set-rule = set_.with((parse-args: parse-args, eid: eid, default-data: default-data, fields: fields)) + + let get-rule(receiver) = prepare-get(g => receiver(g((eid: eid, default-fields: default-fields)))) + + // Partial version of element data to store in filters. + let partial-element-data = ( + version: element-version, + name: name, + eid: eid, + parse-args: parse-args, + default-data: default-data, + default-global-data: default-global-data, + fields: fields, + sel: lbl-show, + meta-sel: lbl-meta, + routines: ( + prepare-rule: prepare-rule, + apply-rules: apply-rules, + get-styles: get-styles, + fold-styles: fold-styles, + verify-filter: verify-filter, + select: select, + toggle-stateful: toggle-stateful-mode, + settings: settings + ) + ) + + // Prepare a filter which should be passed to 'select()'. + // This function will specify which field values for this + // element should be matched. + let where(func) = (..args) => { + assert(args.pos().len() == 0, message: "elembic: unexpected positional arguments\nhint: here, specify positional fields as named arguments, using their names") + let args = args.named() + + if not allow-unknown-fields { + // Note: 'where' on synthesized fields is legal, + // so we check 'all-fields' rather than 'user-fields'. + let unknown-fields = args.keys().filter(k => k not in all-fields and (not labelable or k != "label")) + if unknown-fields != () { + let s = if unknown-fields.len() == 1 { "" } else { "s" } + assert(false, message: "elembic: element.where: element '" + name + "': unknown field" + s + " " + unknown-fields.map(f => "'" + f + "'").join(", ")) + } + } + + ( + (filter-key): true, + element-version: element-version, + kind: "where", + eid: eid, + fields: args, + sel: lbl-show, + elements: ((eid): (:..partial-element-data, func: func)), + ancestry-elements: (:), + may-need-ancestry: false, + ) + } + + let elem-data = ( + (element-key): true, + version: element-version, + name: name, + eid: eid, + scope: scope, + set_: set-rule, + get: get-rule, + where: none, // Filled later when func is known + sel: lbl-show, + meta-sel: lbl-meta, + outer-sel: lbl-outer, + outline-sel: if outline == none { none } else { figure.where(kind: ref-figure-kind) }, + counter: element-counter, + parse-args: parse-args, + default-data: default-data, + default-global-data: default-global-data, + default-fields: default-fields, + routines: partial-element-data.routines, + user-fields: user-fields, + all-fields: all-fields, + fields: fields, + typecheck: typecheck, + allow-unknown-fields: allow-unknown-fields, + template: template, + prepare: prepare, + default-constructor: none, + func: none, + elem-args: elem-args, + ) + + // Figure placed for referencing to work. + let ref-figure(tag, synthesized-fields, ref-label) = { + let numbering = if numbering-type == str { + reference.numbering + } else if numbering-type == function { + let numbering = (reference.numbering)(synthesized-fields) + assert(type(numbering) in (str, function), message: "elembic: element: 'reference.numbering' must be a function fields => numbering (a string or a function), but returned " + str(type(numbering))) + numbering + } else { + none + } + + let number = if numbering == none { none } else { element-counter.display(numbering) } + + let caption = if caption-type == function { + (caption: (outline.caption)(synthesized-fields)) + } else if caption-type in (str, content) { + (caption: [#outline.caption]) + } else if outline == auto { + if ( + "supplement" in reference and "numbering" in reference + or "custom-ref" not in tag + or tag.custom-ref == none + ) { + // Add some caption so it is displayed with the supplement and + // number, but remove useless separator + (caption: figure.caption(separator: "")[]) + } else { + // No supplement or number, but there are custom reference + // contents, so we display that + (caption: tag.custom-ref) + } + } else { + (:) + } + + let ref-figure = [#figure( + supplement: if supplement-type in (str, content) { + [#reference.supplement] + } else if supplement-type == function { + (reference.supplement)(synthesized-fields) + } else { + [] + }, + + numbering: if number == none { none } else { _ => number }, + + kind: ref-figure-kind, + + ..if sys.version >= version(0, 12, 0) { + (placement: none, scope: "column") + }, + + ..caption + )[#[]#metadata(tag)#lbl-tag]#ref-label] + + let tagged-figure = [#[#ref-figure#metadata(tag)#lbl-tag]#lbl-ref-figure] + + show figure: none + + tagged-figure + } + + let apply-show-rules(body, rule, show-rules) = { + if rule >= show-rules.len() { + rule = show-rules.len() - 1 + } else if rule < 0 { + assert(false, message: "elembic: internal error: show rule index cannot be negative") + } + + // Show rules are applied from last to first. + // The first is the base case. + if rule == 0 { + show: show-rules.at(rule) + body + } else { + // Don't recursively apply show rules immediately. + // Do it lazily through a matching show rule. + // This is so that a show rule that doesn't place down 'it' + // stops further show rules from executing. + // + // We could use 'context' for this, but then the show rule + // limit is lower even for 'it => it' (60 vs 30). It is always + // lower when the show rule is of the form 'it => element(it)', + // however, but it still feels like a waste to force it to be + // lower in all cases. + let lbl-tmp-show = label(str(lbl-show) + "-rule" + str(rule)) + (show-rules.at(rule))({ + // Take just the first child to remove the label. + // Add tag AFTER the show rule so data() can still pick it up. + show lbl-tmp-show: it => apply-show-rules(it.children.first(), rule - 1, show-rules) + [#[#body#[]]#lbl-tmp-show] + } + [#metadata(data(body))#lbl-tag]) + } + } + + // Sentinel for 'unspecified value' + let _missing() = {} + let std-label = label + + let default-constructor(..args, __elembic_data: none, __elembic_func: auto, __elembic_mode: auto, __elembic_settings: (:), label: _missing) = { + if __elembic_func == auto { + __elembic_func = default-constructor + } + + let default-constructor = default-constructor.with(__elembic_func: __elembic_func) + if __elembic_data != none { + return if __elembic_data == special-data-values.get-data { + (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)) + } else if __elembic_data == special-data-values.get-where { + if label == _missing { + where(__elembic_func)(..args) + } else { + where(__elembic_func)(..args, label: label) + } + } else { + assert(false, message: "elembic: element: invalid data key to constructor: " + repr(__elembic_data)) + } + } + + let labeling = false + let ref-label = none + if labelable { + if label == _missing { + label = none + } else if type(label) == std-label { + ref-label = std-label(lbl-ref-figure-label-head + str(label)) + labeling = true + } else if label != none { + assert(false, message: "elembic: element '" + name + "': expected label or 'none' for 'label', found " + base.typename(label)) + } + } else if label == _missing { + label = none + } else { + // Also parse label as a field if we don't want element to be labelable + args = arguments(..args, label: label) + } + + let (res, args) = parse-args(args, include-required: true) + if not res { + assert(false, message: args) + } + + // Step the counter early if we don't need additional context + let early-step = if not count-needs-fields { count } + + let inner = early-step + [#context { + let previous-bib-title = bibliography.title + [#context { + set bibliography(title: previous-bib-title) + + // Only update style chain if needed, e.g. filtered rules + let data-changed = false + let global-data = if ( + type(bibliography.title) == content + and bibliography.title.func() == metadata + and bibliography.title.at("label", default: none) == lbl-data-metadata + ) { + bibliography.title.value + } else { + (..default-global-data, first-bib-title: previous-bib-title) + } + + let is-stateful = global-data.stateful + if is-stateful { + let chain = style-state.get() + global-data = if chain == () { + default-global-data + } else { + chain.last() + } + } + + let ancestry = () + let synthesized-futures = () // forward-compat callbacks which need synthesized fields + if "global" in global-data { + if "ancestry-chain" in global-data.global { + ancestry = global-data.global.ancestry-chain + } + + // For set rules from the future... + if "__futures" in global-data.global and "global-data" in global-data.global.__futures { + for future in global-data.global.__futures.global-data { + if element-version <= future.max-version { + let res = (future.call)( + global-data: global-data, + element-data: global-data.elements.at(eid, default: default-data), + args: args, + all-element-data: (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)), + __future-version: element-version + ) + + if "global-data" in res { + global-data = res.global-data + if "data-changed" in res { + // Maybe we don't want to forward changes to children + // More efficient etc. + data-changed = data-changed or res.data-changed + } else { + // Assume we want to forward these changes to children + data-changed = true + } + } + + continue + } + } + } + } + + let element-data = global-data.elements.at(eid, default: default-data) + + if "__futures" in element-data { + if "construct" in element-data.__futures { + for future in element-data.__futures.construct { + if element-version <= future.max-version { + let res = (future.call)( + global-data: global-data, + element-data: element-data, + args: args, + all-element-data: (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)), + __future-version: element-version + ) + + if "construct" in res { + return res.construct + } + } + } + } + + if "element-data" in element-data.__futures { + for future in element-data.__futures.element-data { + if element-version <= future.max-version { + let res = (future.call)( + global-data: global-data, + element-data: element-data, + args: args, + all-element-data: (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)), + __future-version: element-version + ) + + if "construct" in res { + return res.construct + } + + if "element-data" in res { + element-data = res.element-data + } + } + } + } + + if "synthesized-fields" in element-data.__futures { + synthesized-futures += element-data.__futures.synthesized-fields.filter(f => element-version <= f.max-version) + } + } + + let has-synthesized-futures = synthesized-futures != () + let settings = if "settings" in global-data { global-data.settings } else { default-global-data.settings } + let filters = element-data.at("filters", default: default-data.filters) + let has-filters = filters.all != () + let cond-sets = element-data.at("cond-sets", default: default-data.cond-sets) + let has-cond-sets = cond-sets.args != () + let show-rules = element-data.at("show-rules", default: default-data.show-rules) + let has-show-rules = show-rules.callbacks != () + let selects = element-data.at("selects", default: default-data.selects) + let has-selects = selects.filters != () + let has-ancestry-tracking = ( + // Either a rule with a 'within(this element)' filter was used, or + // the user specifically requested ancestry tracking. + element-data.at("track-ancestry", default: default-data.track-ancestry) + or "track-ancestry" in settings and ( + settings.track-ancestry == "any" + or eid in settings.track-ancestry + ) + ) + + // Whether ancestry should be made available in a query() for this + // element, allowing usage of 'within()' rules for that element in a + // query. + let store-ancestry = has-ancestry-tracking or "store-ancestry" in settings and ( + settings.store-ancestry == "any" + or eid in settings.store-ancestry + ) + + let updates-stylechain-inside = has-filters or has-ancestry-tracking + + let (folded-fields, constructed-fields, active-revokes, first-active-index) = if ( + element-data.revoke-chain == default-data.revoke-chain + and ( + foldable-fields == (:) + or element-data.fold-chain == default-data.fold-chain + and args.keys().all(f => f not in foldable-fields) + ) + ) { + let folded-fields = default-fields + element-data.chain.sum(default: (:)) + // Sum the chain of dictionaries so that the latest value specified for + // each property wins. + (folded-fields, folded-fields + args, (:), 0) + } else { + // We can't just sum, we need to filter and fold first. + // Memoize this operation through a function. + let (folded, active-revokes, first-active-index) = fold-styles(element-data.chain, element-data.data-chain, element-data.revoke-chain, element-data.fold-chain) + let outer-chain = default-fields + folded + let finalized-chain = outer-chain + args + + // Fold received arguments with outer chain or defaults + for (field-name, fold-data) in foldable-fields { + if field-name in args { + let outer = outer-chain.at(field-name, default: fold-data.default) + if fold-data.folder == auto { + finalized-chain.insert(field-name, outer + args.at(field-name)) + } else { + finalized-chain.insert(field-name, (fold-data.folder)(outer, args.at(field-name))) + } + } + } + + (outer-chain, finalized-chain, active-revokes, first-active-index) + } + + let filter-revokes + let filter-first-active-index + let editable-global-data + if has-filters or has-synthesized-futures { + // The closures inside context {} below will capture global-data, + // reducing potential for memoization of their output, so, for + // performance reasons, we only pass the real global data if + // necessary due to filtering (which will update the data on a + // match). + editable-global-data = global-data + filter-revokes = active-revokes + filter-first-active-index = first-active-index + } else if has-cond-sets or has-show-rules or has-selects { + // No need for global data, but still need revokes to see which + // conditional sets were revoked + filter-revokes = active-revokes + filter-first-active-index = first-active-index + if updates-stylechain-inside { + editable-global-data = global-data + } + } else if updates-stylechain-inside { + editable-global-data = global-data + } + + let cond-set-foldable-fields + if has-cond-sets { + cond-set-foldable-fields = foldable-fields + } + + let all-elem-data-for-futures + let element-data-for-futures + if has-synthesized-futures { + all-elem-data-for-futures = (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)) + element-data-for-futures = element-data + } + + let shown = { + let tag = ( + data-kind: "element-instance", + body: none, + fields: constructed-fields, + func: __elembic_func, + scope: scope, + default-constructor: default-constructor, + name: name, + eid: eid, + ctx: if contextual { + // Note: we add ancestry later if there is ancestry tracking + // to avoid interfering with memoization of other things + (get: get-styles.with(elements: global-data.elements), ancestry: ancestry) + } else { + (:) + }, + counter: element-counter, + reference: reference, + custom-ref: none, + fields-known: true, + valid: true + ) + + { + // Use context for synthesize as well + let synthesized-fields = if synthesize == none { + constructed-fields + } else { + // Pass contextual information to synthesize + // Remove it afterwards to ensure the final tag's 'fields' won't + // have its own copy of the tag + let new-fields = synthesize(constructed-fields + ((stored-data-key): tag)) + if type(new-fields) != dictionary { + assert(false, message: "elembic: element '" + name + "': 'synthesize' didn't return a dictionary, but rather " + repr(new-fields) + " (a(n) '" + str(type(new-fields)) + "') instead). Please contact the element author.") + } + if stored-data-key in new-fields { + _ = new-fields.remove(stored-data-key) + } + new-fields + } + + if labelable and label != none and label != _missing { + synthesized-fields.label = label + } + + // Update synthesized fields BEFORE applying filters! + if has-cond-sets { + let i = 0 + let new-synthesized-fields = folded-fields // only add args later (args must win) + let affected-fields = (:) + for filter in cond-sets.filters { + let data = cond-sets.data.at(i) + if ( + filter != none + and (data.index == none or data.index >= filter-first-active-index) + and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) + and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) + ) { + let cond-args = cond-sets.args.at(i) + + affected-fields += cond-args + + // Fold received arguments with existing fields or defaults + for (field-name, value) in cond-args { + if field-name in cond-set-foldable-fields { + let fold-data = cond-set-foldable-fields.at(field-name) + let outer = new-synthesized-fields.at(field-name, default: fold-data.default) + if fold-data.folder == auto { + new-synthesized-fields.insert(field-name, outer + value) + } else { + new-synthesized-fields.insert(field-name, (fold-data.folder)(outer, value)) + } + } else { + new-synthesized-fields.insert(field-name, value) + } + } + } + i += 1 + } + + // Fold args again (they must win). + for (field-name, value) in synthesized-fields { + if field-name not in args { + // Not an argument, and we already folded it with cond-sets + // before (not a synthesized field), so stop. + continue + } + + if ( + field-name in affected-fields + and field-name in cond-set-foldable-fields + // If field was changed due to synthetization, don't allow + // folding it further + and constructed-fields.at(field-name) == value + ) { + let fold-data = cond-set-foldable-fields.at(field-name) + if fold-data.folder == auto { + new-synthesized-fields.at(field-name) += args.at(field-name) + } else { + new-synthesized-fields.at(field-name) = (fold-data.folder)(new-synthesized-fields.at(field-name), args.at(field-name)) + } + } else { + // Undo (give precedence to already folded and synthesized argument) + new-synthesized-fields.insert(field-name, value) + } + } + + synthesized-fields = new-synthesized-fields + } + + let new-global-data = if data-changed { editable-global-data } else { none } + if has-synthesized-futures { + if new-global-data == none { + new-global-data = editable-global-data + } + let element-data-for-futures = element-data-for-futures + for future in synthesized-futures { + let res = (future.call)( + synthesized-fields: synthesized-fields, + global-data: new-global-data, + element-data: element-data-for-futures, + args: args, + all-element-data: all-elem-data-for-futures, + __future-version: element-version + ) + + if "construct" in res { + return res.construct + } + + if "global-data" in res { + new-global-data = res.global-data + } + + if "element-data" in res { + element-data-for-futures = res.element-data + } + + if "synthesized-fields" in res { + synthesized-fields = res.synthesized-fields + } + } + } + + let select-labels = () + if has-selects { + let i = 0 + for filter in selects.filters { + let data = selects.data.at(i) + if ( + filter != none + and (data.index == none or data.index >= filter-first-active-index) + and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) + and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) + ) { + select-labels.push(selects.labels.at(i)) + } + i += 1 + } + } + + let tag = tag + tag.fields = synthesized-fields + + // Store contextual information in synthesize + synthesized-fields.insert(stored-data-key, tag) + + if has-filters { + let i = 0 + let rules = () + for filter in filters.all { + let data = filters.data.at(i) + if ( + filter != none + and (data.index == none or data.index >= filter-first-active-index) + and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) + and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) + ) { + let rule = filters.rules.at(i) + if rule.kind == apply { + rules += rule.rules + } else { + rules.push(rule) + } + } + i += 1 + } + + if rules != () { + // Only update style chain if at least one filter matches + new-global-data = editable-global-data + + new-global-data += apply-rules( + rules, + elements: new-global-data.elements, + settings: new-global-data.at("settings", default: default-global-data.settings), + global: new-global-data.at("global", default: default-global-data.global) + ) + } + } + + if has-ancestry-tracking { + if new-global-data == none { + new-global-data = editable-global-data + } + + if "global" not in new-global-data { + new-global-data.global = default-global-data + } + if "ancestry-chain" in new-global-data.global { + new-global-data.global.ancestry-chain.push((eid: eid, fields: synthesized-fields)) + } else { + new-global-data.global.ancestry-chain = ((eid: eid, fields: synthesized-fields),) + } + } + + // Save updated styles from applied rules + show lbl-get: set bibliography(title: [#metadata(new-global-data)#lbl-data-metadata]) if new-global-data != none and not is-stateful + + if new-global-data != none and is-stateful { + // Popping after the if below + style-state.update(chain => { + chain.push(new-global-data) + chain + }) + } + + // Filter show rules + let show-rules = if has-show-rules { + let i = 0 + let final-rules = () + for filter in show-rules.filters { + let data = show-rules.data.at(i) + if ( + filter != none + and (data.index == none or data.index >= filter-first-active-index) + and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) + and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) + ) { + final-rules.push(show-rules.callbacks.at(i)) + } + i += 1 + } + final-rules + } else { + () + } + + if count-needs-fields or contextual { + if count-needs-fields { + count(synthesized-fields) + } + + // Wrap in additional context so the counter step is detected + context { + let body = display(synthesized-fields) + let tag = tag + tag.body = body + + if custom-ref != none { + // Update with body + let synthesized-fields = synthesized-fields + synthesized-fields.at(stored-data-key) = tag + + tag.custom-ref = custom-ref(synthesized-fields) + } + + let tag-metadata = metadata(tag) + + if reference != none and ref-label != none or outline != none { + // Update with custom-ref + let synthesized-fields = synthesized-fields + synthesized-fields.at(stored-data-key) = tag + + ref-figure(tag, synthesized-fields, ref-label) + } + + if not contextual and store-ancestry { + tag.ctx = (ancestry: ancestry) + } + + let body = [#[#body#metadata(tag)#lbl-tag]#lbl-show] + + if select-labels != () { + body = select-labels.fold(body, (acc, lbl) => [#[#acc#metadata(tag)#lbl-tag]#lbl]) + } + + let shown-body = if show-rules == () { + body + } else { + apply-show-rules(body, show-rules.len() - 1, show-rules) + } + + // Include metadata for querying + let meta-body = [#shown-body#metadata(((element-meta-key): true, kind: "element-meta", eid: eid, rendered: body, (stored-data-key): tag))#lbl-meta#metadata(tag)#lbl-tag] + + if labeling { + [#[#meta-body#metadata(tag)#lbl-tag]#label] + } else { + meta-body + } + } + } else { + let body = display(synthesized-fields) + let tag = tag + tag.body = body + + if custom-ref != none { + // Update with body + synthesized-fields.at(stored-data-key) = tag + + tag.custom-ref = custom-ref(synthesized-fields) + } + + let tag-metadata = metadata(tag) + + if reference != none and ref-label != none or outline != none { + // Update with custom-ref + synthesized-fields.at(stored-data-key) = tag + + ref-figure(tag, synthesized-fields, ref-label) + } + + if not contextual and store-ancestry { + tag.ctx = (ancestry: ancestry) + } + + let body = [#[#body#metadata(tag)#lbl-tag]#lbl-show] + + if select-labels != () { + body = select-labels.fold(body, (acc, lbl) => [#[#acc#metadata(tag)#lbl-tag]#lbl]) + } + + let shown-body = if show-rules == () { + body + } else { + apply-show-rules(body, show-rules.len() - 1, show-rules) + } + + // Include metadata for querying + let meta-body = [#shown-body#metadata(((element-meta-key): true, kind: "element-meta", eid: eid, rendered: body, (stored-data-key): tag))#lbl-meta#metadata(tag)#lbl-tag] + + if labeling { + [#[#meta-body#metadata(tag)#lbl-tag]#label] + } else { + meta-body + } + } + + if new-global-data != none and is-stateful { + // Pushed before the if above + style-state.update(chain => { + _ = chain.pop() + chain + }) + } + } + } + + if data-changed and not updates-stylechain-inside { + if is-stateful { + [#style-state.update(chain => { + chain.push(global-data) + chain + })#shown#style-state.update(chain => { + _ = chain.pop() + chain + })] + } else { + show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) + shown + } + } else { + shown + } + }#lbl-get] + }#lbl-outer] + + let tag = [#metadata(( + data-kind: "element-instance", + body: inner, + scope: scope, + fields: args, + func: __elembic_func, + default-constructor: default-constructor, + name: name, + eid: eid, + ctx: none, + counter: element-counter, + reference: reference, + custom-ref: none, + fields-known: false, + valid: true + ))#lbl-tag] + + if template != none { + inner = template[#inner#tag] + } + + [#inner#tag] + } + + let final-constructor = if construct != none { + { + let test-construct = construct(default-constructor) + assert(type(test-construct) == function, message: "elembic: element.declare: the 'construct' function must receive original constructor and return the new constructor, a new function, not '" + str(type(test-construct)) + "'.") + } + + let final-constructor(..args, __elembic_data: none, __elembic_mode: auto, __elembic_settings: (:)) = { + if __elembic_data != none { + return if __elembic_data == special-data-values.get-data { + (data-kind: "element", ..elem-data, func: final-constructor, default-constructor: default-constructor.with(__elembic_func: final-constructor), where: where(final-constructor)) + } else if __elembic_data == special-data-values.get-where { + where(final-constructor)(..args) + } else { + assert(false, message: "elembic: element: invalid data key to constructor: " + repr(__elembic_data)) + } + } + + construct(default-constructor.with(__elembic_func: final-constructor, __elembic_mode: __elembic_mode, __elembic_settings: __elembic_settings))(..args) + } + + final-constructor + } else { + default-constructor + } + + final-constructor +} diff --git a/src/libs/elembic/fields.typ b/src/libs/elembic/fields.typ new file mode 100644 index 0000000..e9fe51b --- /dev/null +++ b/src/libs/elembic/fields.typ @@ -0,0 +1,364 @@ +#import "data.typ": type-key, custom-type-key, current-field-version, eq +#import "types/types.typ" + +#let field-key = "__elembic_field" +#let fields-key = "__elembic_fields" + +#let _missing() = {} + +// Specifies an element field's properties. +#let field( + name, + type_, + doc: none, + required: false, + named: auto, + synthesized: false, + default: _missing, + folds: true, + internal: false, + meta: (:), +) = { + assert(type(name) == str, message: "elembic: field: Field name must be a string, not " + str(type(name))) + + let error-prefix = "elembic: field '" + name + "': " + assert(doc == none or type(doc) == str, message: error-prefix + "'doc' must be none or a string (add documentation)") + assert(type(synthesized) == bool, message: error-prefix + "'synthesized' must be a boolean (true: field is automatically synthesized and cannot be specified or overridden by the user; false: field can be manually specified and overridden by the user)") + assert(type(required) == bool, message: error-prefix + "'required' must be a boolean") + assert(type(folds) == bool, message: error-prefix + "'folds' must be a boolean") + assert(type(internal) == bool, message: error-prefix + "'internal' must be a boolean") + assert(type(meta) == dictionary, message: error-prefix + "'meta' must be a dictionary") + assert(named == auto or type(named) == bool, message: error-prefix + "'named' must be a boolean or auto") + let typeinfo = { + let (res, value) = types.validate(type_) + assert(res, message: if not res { error-prefix + value } else { "" }) + value + } + + if not required and default == _missing { + let (res, value) = types.default(typeinfo) + assert(res, message: if not res { error-prefix + value } else { "" }) + + default = value + } + + default = if required or synthesized { + // This value should be ignored in that case + auto + } else { + let (success, value) = types.cast(default, typeinfo) + if not success { + assert(false, message: error-prefix + value + "\n hint: given default for field had an incompatible type") + } + + value + } + + let fold = if folds and not synthesized and "fold" in typeinfo and typeinfo.fold != none { + assert(typeinfo.fold == auto or type(typeinfo.fold) == function, message: error-prefix + "type '" + typeinfo.name + "' doesn't appear to have a valid fold field (must be auto or function)") + let fold-default = if required { + // No field default, use the type's own default to begin folding + let (res, value) = types.default(typeinfo) + assert(res, message: if not res { error-prefix + value } else { "" }) + + value + } else { + // Use the field default as starting point for folding + default + } + + ( + folder: typeinfo.fold, + default: fold-default, + ) + } else { + none + } + + if named == auto { + // Pos arg is generally required + named = not required + } + + if synthesized and (required or not named) { + assert(false, message: error-prefix + "synthesized field cannot be required or positional, since it cannot be specified by the user") + } + + ( + (field-key): true, + version: current-field-version, + name: name, + doc: doc, + typeinfo: typeinfo, + default: default, + required: required, + synthesized: synthesized, + named: named, + fold: fold, + folds: folds, + internal: internal, + meta: meta, + ) +} + +#let parse-fields(fields, allow-unknown-fields: false) = { + assert(type(allow-unknown-fields) == bool, message: "elembic: element.fields: 'allow-unknown-fields' must be a boolean, not " + str(type(allow-unknown-fields))) + + let required-pos-fields = () + let optional-pos-fields = () + let required-named-fields = () + let optional-named-fields = () + let all-fields = (:) + let user-named-fields = (:) + let foldable-fields = (:) + let user-fields = (:) + let synthesized-fields = (:) + + for field in fields { + assert(type(field) == dictionary and field.at(field-key, default: none) == true, message: "elembic: element.fields: Invalid field received, please use the 'e.fields.field' constructor.") + assert(field.named or not field.required or optional-pos-fields == (), message: "elembic: element.fields: field '" + field.name + "' cannot be positional and required and appear after other positional but optional fields. Ensure there are only optional fields after the first positional optional field.") + assert(field.name not in all-fields, message: "elembic: element.fields: duplicate field name '" + field.name + "'") + + if field.required { + if field.named { + required-named-fields.push(field) + } else { + required-pos-fields.push(field) + } + } else if field.named { + optional-named-fields.push(field) + } else { + optional-pos-fields.push(field) + } + + if field.fold != none { + foldable-fields.insert(field.name, field.fold) + } + + if field.synthesized { + synthesized-fields.insert(field.name, field) + } else { + user-fields.insert(field.name, field) + + if field.named { + user-named-fields.insert(field.name, field) + } + } + + all-fields.insert(field.name, field) + } + + ( + (fields-key): true, + version: current-field-version, + required-pos-fields: required-pos-fields, + optional-pos-fields: optional-pos-fields, + required-named-fields: required-named-fields, + optional-named-fields: optional-named-fields, + foldable-fields: foldable-fields, + user-named-fields: user-named-fields, + user-fields: user-fields, + all-fields: all-fields, + allow-unknown-fields: allow-unknown-fields, + ) +} + +// Generates an argument parser function with the given general error +// prefix (for listing missing fields) and per-field error prefix function +// (for an invalid field; receives the field name). +// +// You can customize 'field-term' to customize what the word "field" is +// in error messages. It should be either a string or a two-element +// array with (singular, plural). Setting 'typecheck: false' also fully +// disables typechecking. +// +// Parse arguments into a dictionary of fields and their casted values. +// By default, include required arguments and error if they are missing. +// Setting 'include-required' to false will error if they are present +// instead. +#let generate-arg-parser( + fields: none, + general-error-prefix: "", + field-error-prefix: _ => "", + field-term: "field", + typecheck: true, +) = { + assert(type(fields) == dictionary and fields-key in fields, message: "elembic: generate-arg-parser: please use 'parse-fields' to generate the fields input.") + assert(type(general-error-prefix) == str, message: "elembic: generate-arg-parser: 'general-error-prefix' must be a string") + assert(type(field-error-prefix) == function, message: "elembic: generate-arg-parser: 'field-error-prefix' must be a function receiving field name and returning string") + assert(type(typecheck) == bool, message: "elembic: generate-arg-parser: 'typecheck' must be a boolean, not " + str(type(typecheck))) + + let (field-singular, field-plural) = if type(field-term) == str { + (field-term, field-term + "s") + } else if type(field-term) == array and field-term.len() == 2 and field-term.all(term => type(term) == str) { + field-term + } else { + assert(false, message: "elembic: generate-arg-parser: 'field-term' must either be a string (plural with 's') or a two-element array of strings (singular, plural).") + } + + let (required-pos-fields, optional-pos-fields, required-named-fields, optional-named-fields, all-fields, user-fields, user-named-fields, allow-unknown-fields) = fields + let required-pos-fields-amount = required-pos-fields.len() + let optional-pos-fields-amount = optional-pos-fields.len() + let total-pos-fields-amount = required-pos-fields-amount + optional-pos-fields-amount + let all-pos-fields = required-pos-fields + optional-pos-fields + + let has-required-fields = required-pos-fields-amount + required-named-fields.len() != 0 + + // If we allow unknown named fields, we still need to check whether a + // positional or synthesized field was accidentally specified as a named field. + let is-unknown-named-field = if allow-unknown-fields { + f => f in all-fields and f not in user-named-fields + } else { + f => f not in user-named-fields + } + + // Disable typechecking anyway if all fields are 'any' + // + // Have a separate typecheck option so type information can be kept in fields + // even if typechecking is undesirable + // Note: we don't parse args for synthesized fields, so we can exclude them when + // checking whether we will typecheck when parsing args + let typecheck = typecheck and user-fields.values().any(f => f.typeinfo.type-kind != "any") + + // Parse args (no typechecking) + let parse-args-no-typechecking(args, include-required: true) = { + let pos = args.pos() + + if include-required and pos.len() < required-pos-fields-amount { + // Plural + let term = if required-pos-fields-amount - pos.len() == 1 { field-singular } else { field-plural } + + return (false, general-error-prefix + "missing positional " + term + " " + fields.required-pos-fields.slice(pos.len()).map(f => "'" + f.name + "'").join(", ")) + } + + if pos.len() > if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } { + let expected-arg-amount = if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } + let excluding-required-hint = if include-required { "" } else { "\n hint: only optional fields are accepted here" } + return (false, general-error-prefix + "too many positional arguments, expected " + str(expected-arg-amount) + excluding-required-hint) + } + + let named-args = args.named() + if include-required { + if required-named-fields.any(f => f.name not in named-args) { + let missing-fields = required-named-fields.filter(f => f.name not in named-args) + let term = if missing-fields.len() == 1 { field-singular } else { field-plural } + + return (false, general-error-prefix + "missing required named " + term + " " + missing-fields.map(f => "'" + f.name + "'").join(", ")) + } + } else if required-named-fields.any(f => f.name in named-args) { + let field = required-named-fields.find(f => f.name in named-args) + return (false, field-error-prefix(field.name) + "this " + field-singular + " cannot be specified here\n hint: only optional " + field-plural + " are accepted here") + } + + // Here we simultaneously check for unknown fields and for positional fields + // being wrongly specified as named. If there are no positional fields and + // unknown fields are allowed, there is no point in doing this check. + if (not allow-unknown-fields or total-pos-fields-amount > 0) and named-args.keys().any(is-unknown-named-field) { + let field-name = named-args.keys().find(is-unknown-named-field) + let field = all-fields.at(field-name, default: none) + let expected-pos-hint = if field == none or field.named { "" } else { "\n hint: this " + field-singular + " must be specified positionally" } + let is-synthesized-hint = if field != none and field.synthesized { "\n hint: this " + field-singular + " is synthesized and cannot be specified manually" } else { "" } + + return (false, general-error-prefix + "unknown named " + field-singular + " '" + field-name + "'" + expected-pos-hint + is-synthesized-hint) + } + + let pos-fields = if include-required { all-pos-fields } else { optional-pos-fields } + let i = 0 + for value in pos { + let pos-field = pos-fields.at(i) + named-args.insert(pos-field.name, value) + + i += 1 + } + + (true, named-args) + } + + // Parse args (with typechecking) + let parse-args(args, include-required: true) = { + let result = (:) + + let pos = args.pos() + if include-required and pos.len() < required-pos-fields-amount { + // Plural + let term = if required-pos-fields-amount - pos.len() == 1 { field-singular } else { field-plural } + + return (false, general-error-prefix + "missing positional " + term + " " + fields.required-pos-fields.slice(pos.len()).map(f => "'" + f.name + "'").join(", ")) + } + + let expected-arg-amount = if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } + + if pos.len() > expected-arg-amount { + let excluding-required-hint = if include-required { "" } else { "\n hint: only optional fields are accepted here" } + return (false, general-error-prefix + "too many positional arguments, expected " + str(expected-arg-amount) + excluding-required-hint) + } + + let named-args = args.named() + + if include-required and required-named-fields.any(f => f.name not in named-args) { + let missing-fields = required-named-fields.filter(f => f.name not in named-args) + let term = if missing-fields.len() == 1 { field-singular } else { field-plural } + + return (false, general-error-prefix + "missing required named " + term + " " + missing-fields.map(f => "'" + f.name + "'").join(", ")) + } + + for (field-name, value) in named-args { + if allow-unknown-fields and field-name not in all-fields { + continue + } + + let field = all-fields.at(field-name, default: none) + + if field == none or field.synthesized or not field.named { + let expected-pos-hint = if field == none or field.named { "" } else { "\n hint: this " + field-singular + " must be specified positionally" } + let is-synthesized-hint = if field != none and field.synthesized { "\n hint: this " + field-singular + " is synthesized and cannot be specified manually" } else { "" } + + return (false, general-error-prefix + "unknown named " + field-singular + " '" + field-name + "'" + expected-pos-hint + is-synthesized-hint) + } + + if not include-required and field.required { + return (false, field-error-prefix(field-name) + "this " + field-singular + " cannot be specified here\n hint: only optional " + field-plural + " are accepted here") + } + + let typeinfo = field.typeinfo + let kind = typeinfo.type-kind + + if kind != "any" { + let (res, casted) = types.cast(value, typeinfo) + if not res { + return (false, field-error-prefix(field-name) + casted) + } + named-args.insert(field-name, casted) + } + } + + let pos-fields = if include-required { all-pos-fields } else { optional-pos-fields } + let i = 0 + for value in pos { + let pos-field = pos-fields.at(i) + let typeinfo = pos-field.typeinfo + let kind = typeinfo.type-kind + let casted = value + + if kind != "any" { + let res + (res, casted) = types.cast(value, typeinfo) + if not res { + return (false, field-error-prefix(pos-field.name) + casted) + } + } + + named-args.insert(pos-field.name, casted) + + i += 1 + } + + (true, named-args) + } + + if typecheck { + parse-args + } else { + parse-args-no-typechecking + } +} diff --git a/src/libs/elembic/lib.typ b/src/libs/elembic/lib.typ new file mode 100644 index 0000000..f902d66 --- /dev/null +++ b/src/libs/elembic/lib.typ @@ -0,0 +1,10 @@ +#import "element.typ": set_, data, apply, revoke, reset, named, filtered, cond-set, show_, style-modes, prepare-get as get, prepare-debug as debug-get, settings, elem-selector as selector, select, elem-query as query, ref_ as ref, prepare, within-filter as within +#import "fields.typ": field +#import "pub/data.typ": * +#import "pub/constants.typ" +#import "pub/element.typ" +#import "pub/filters.typ" +#import "pub/parsing.typ" +#import "pub/types.typ" +#import "pub/leaky.typ" +#import "pub/stateful.typ" diff --git a/src/libs/elembic/pub/constants.typ b/src/libs/elembic/pub/constants.typ new file mode 100644 index 0000000..27797dd --- /dev/null +++ b/src/libs/elembic/pub/constants.typ @@ -0,0 +1 @@ +#import "../data.typ": element-version, type-version, custom-type-version, current-field-version, style-modes diff --git a/src/libs/elembic/pub/data.typ b/src/libs/elembic/pub/data.typ new file mode 100644 index 0000000..f1b5c74 --- /dev/null +++ b/src/libs/elembic/pub/data.typ @@ -0,0 +1 @@ +#import "../data.typ": fields, counter_ as counter, ctx, scope, func, func-name, eid, tid, repr_ as repr, eq diff --git a/src/libs/elembic/pub/element.typ b/src/libs/elembic/pub/element.typ new file mode 100644 index 0000000..3b824dd --- /dev/null +++ b/src/libs/elembic/pub/element.typ @@ -0,0 +1,2 @@ +// Public re-exports for element-related functions. +#import "../element.typ": declare diff --git a/src/libs/elembic/pub/filters.typ b/src/libs/elembic/pub/filters.typ new file mode 100644 index 0000000..88e0611 --- /dev/null +++ b/src/libs/elembic/pub/filters.typ @@ -0,0 +1 @@ +#import "../element.typ": or-filter as or_, and-filter as and_, not-filter as not_, xor-filter as xor, custom-filter as custom diff --git a/src/libs/elembic/pub/leaky.typ b/src/libs/elembic/pub/leaky.typ new file mode 100644 index 0000000..40c1106 --- /dev/null +++ b/src/libs/elembic/pub/leaky.typ @@ -0,0 +1,8 @@ +// Exports rules defaulting to leaky mode. +#import "../element.typ": leaky-set as set_, leaky-apply as apply, leaky-show as show_, leaky-revoke as revoke, leaky-reset as reset, leaky-cond-set as cond-set, leaky-settings as settings, leaky-toggle as toggle + +/// Enable leaky mode by default. +#let enable = toggle.with(true) + +/// Disable leaky mode by default. +#let disable = toggle.with(false) diff --git a/src/libs/elembic/pub/native.typ b/src/libs/elembic/pub/native.typ new file mode 100644 index 0000000..d823aeb --- /dev/null +++ b/src/libs/elembic/pub/native.typ @@ -0,0 +1,2 @@ +// Public re-exports for native type-related functions and constants. +#import "../types/native.typ": content_, auto_, none_, float_, function_, int_, array_, dict_, datetime_, duration_, color_, gradient_, str_, type_, bool_, relative_, ratio_, typeinfo, angle_, arguments_, bytes_, tiling, tiling_, version_, fraction_, length_, stroke_ diff --git a/src/libs/elembic/pub/parsing.typ b/src/libs/elembic/pub/parsing.typ new file mode 100644 index 0000000..e4ff0b7 --- /dev/null +++ b/src/libs/elembic/pub/parsing.typ @@ -0,0 +1 @@ +#import "../fields.typ": parse-fields, generate-arg-parser diff --git a/src/libs/elembic/pub/stateful.typ b/src/libs/elembic/pub/stateful.typ new file mode 100644 index 0000000..9de9885 --- /dev/null +++ b/src/libs/elembic/pub/stateful.typ @@ -0,0 +1,8 @@ +// Exports rules defaulting to stateful mode. +#import "../element.typ": toggle-stateful-mode as toggle, stateful-set as set_, stateful-apply as apply, stateful-show as show_, stateful-revoke as revoke, stateful-reset as reset, stateful-cond-set as cond-set, stateful-get as get, stateful-settings as settings + +// Enable stateful mode. +#let enable = toggle.with(true) + +// Disable stateful mode. +#let disable = toggle.with(false) diff --git a/src/libs/elembic/pub/types.typ b/src/libs/elembic/pub/types.typ new file mode 100644 index 0000000..0c9d323 --- /dev/null +++ b/src/libs/elembic/pub/types.typ @@ -0,0 +1,5 @@ +// Public re-exports for type-related functions and constants. +#import "../types/base.typ": ok, err, is-ok, any, never, custom-type, typeid, typename, native-elem +#import "../types/types.typ": option, smart, union, paint, literal, exact, wrap, array_ as array, dict_ as dict, default, validate as typeinfo, typeof, cast, generate-cast-error +#import "../types/custom.typ": declare +#import "native.typ" diff --git a/src/libs/elembic/types/base.typ b/src/libs/elembic/types/base.typ new file mode 100644 index 0000000..768c9f8 --- /dev/null +++ b/src/libs/elembic/types/base.typ @@ -0,0 +1,582 @@ +// The shared fundamentals of the type system. +#import "../data.typ": data, type-key, custom-type-key, custom-type-data-key, repr_, func-name, type-version, eq + +// Typeinfo structure: +// - type-key: kind of type +// - version: 1 +// - name: type name +// - input: list of native types / custom types of input +// - output: list of native types / custom types of output +// - data: data specific for this type key +// - check: none (only check inputs) or function x => bool +// - cast: none (input is unchanged) or function to convert input to output +// - error: none or function x => string to customize check failure message +// - default: empty array (no default) or singleton array => default value for this type +// - fold: none, auto (equivalent to (a, b) => a + b but more efficient) or function (prev, next) => folded value: +// determines how to combine two consecutive values of this type in the stylechain +#let base-typeinfo = ( + (type-key): true, + type-kind: "base", + version: type-version, + name: "unknown", + input: (), + output: (), + data: none, + check: none, + cast: none, + error: none, + default: (), + fold: none, +) + +// Top type +// input and output have "any". +#let any = ( + ..base-typeinfo, + type-kind: "any", + name: "any", + input: ("any",), + output: ("any",), +) + +// Bottom type +// input and output are empty. +#let never = ( + ..base-typeinfo, + type-kind: "never", + name: "never", + input: (), + output: (), +) + +// Any custom type +#let custom-type = ( + ..base-typeinfo, + (type-key): "custom type", + name: "custom type", + input: ("custom type",), + output: ("custom type",), +) + +#let _sequence = [].func() + +#let element(name, eid) = ( + ..base-typeinfo, + type-kind: "element", + name: "element '" + name + "'", + input: (content,), + output: (content,), + check: c => c.func() == _sequence and data(c).eid == eid, + data: (name: name, eid: eid), + error: c => "expected element " + name + ", found " + func-name(c), +) + +#let native-elem(func) = { + assert(type(func) == function, message: "elembic: types.native-elem: expected native element constructor, got " + str(type(func))) + + ( + ..base-typeinfo, + type-kind: "native-element", + name: "native element '" + repr(func) + "'", + input: (content,), + output: (content,), + check: if func == _sequence { c => c.func() == _sequence and data(c).eid == none } else { c => c.func() == func }, + data: (func: func), + error: c => "expected native element " + repr(func) + ", found " + func-name(c), + ) +} + +// Get the type ID of a value. +// This is usually 'type(value)', unless value has a custom type. +// In that case, it has the format '(tid: ..., name: ...)'. +// This is the format expected by 'input' and 'output' arrays. +#let typeid(value) = { + let value-type = type(value) + if value-type == dictionary and custom-type-key in value { + value-type = value.at(custom-type-key).id + } + value-type +} + +// Returns the name of the value's type as a string. +#let typename(value) = { + let value-type = type(value) + if value-type == dictionary and custom-type-key in value { + let id = value.at(custom-type-key).id + if "name" in id { + id.name + } else { + str(id) + } + } else { + str(value-type) + } +} + +// Make a unique element or type ID based on prefix and name. +// +// Uses a separator and a "bit stuffing" technique to ensure +// the separator sequence doesn't appear in either of the +// prefix or the name in the final ID. +#let id-separator = "_---_" +#let trimmed-separator = id-separator.trim("_", at: end) +#let unique-id(kind, prefix, name) = { + ( + kind + "_" + ) + prefix.replace( + trimmed-separator, trimmed-separator + "-" + ) + id-separator + name.replace( + trimmed-separator, trimmed-separator + "-" + ) +} + +// Literal type +// Only accepted if value is equal to the literal. +// Input and output are equal to the value. +// +// Uses base typeinfo information for information such as casts and whatnot. +#let literal(value, typeinfo) = { + let represented = "'" + if type(value) == str { value } else { repr_(value) } + "'" + let value-type = typeid(value) + + let check = if typeinfo.check == none { x => eq(x, value) } else { x => eq(x, value) and (typeinfo.check)(x) } + + ( + ..typeinfo, + type-kind: "literal", + name: "literal " + represented, + data: (value: value, typeinfo: typeinfo, represented: represented), + check: check, + error: _ => "given value wasn't equal to literal " + represented, + default: (value,), + ) +} + +// Union type (one of many) +// Data is the list of typeinfos. +// Accepted if the value corresponds to one of the given types. +// Does not check the validity of typeinfos. +#let union(typeinfos) = { + // Flatten nested unions + let typeinfos = typeinfos.map(t => if t.type-kind == "union" { t.data } else { (t,) }).sum(default: ()).dedup() + if typeinfos == () { + // No inputs accepted... + return never + } + if typeinfos.len() == 1 { + // Simplify union if there's nothing else + return typeinfos.first() + } + if typeinfos.any(x => x.type-kind == "any") { + // Union with 'any' is just any + return any + } + + let name = typeinfos.map(t => t.name).join(", ", last: " or ") + let input = typeinfos.map(t => t.input).sum(default: ()).dedup() + let output = typeinfos.map(t => t.output).sum(default: ()).dedup() + + let has-any-input = "any" in input + let has-any-output = "any" in output + + if has-any-input { + input = ("any",) + } + + if has-any-output { + output = ("any",) + } + + // Try to optimize checks as much as possible + let check = if typeinfos.all(t => t.check == none) { + // If there are no checks, just checking inputs is enough + none + } else { + let checked-types = typeinfos.filter(t => t.check != none) + let unchecked-inputs = typeinfos.filter(t => t.check == none).map(t => t.input).sum(default: ()).dedup() + if input.all(t => t in unchecked-inputs) { + // Unchecked types include all possible input types, so some check will always succeed + // Note that this check also works for input reduced to just "any". If "any" is an + // unchecked input, then checks will never fail. + none + } else if checked-types.all(t => t.type-kind == "native-element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { + // From here onwards, we can assume unchecked-inputs doesn't contain "any", + // since it is a subset of input, therefore input would be just ("any",) and + // the check above would have had to pass in that case. + let all-funcs = checked-types.map(t => t.data.func) + let non-seq-funcs = all-funcs.filter(f => f != _sequence) + let has-seq = _sequence in all-funcs + + // Check sequence separately, as a sequence can also be a custom element, + // so we must tell them apart. + if has-seq { + if non-seq-funcs == () { + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + typ in unchecked-inputs or typ == content and x.func() == _sequence and data(x).eid == none + } + } else { + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + typ in unchecked-inputs or typ == content and (x.func() in non-seq-funcs or x.func() == _sequence and data(x).eid == none) + } + } + } else { + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + typ in unchecked-inputs or typ == content and x.func() in non-seq-funcs + } + } + } else if checked-types.all(t => t.type-kind == "element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { + let all-eids = checked-types.map(t => t.data.eid) + + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + typ in unchecked-inputs or typ == content and x.func() == _sequence and data(x).eid in all-eids + } + } else if checked-types.all(t => t.type-kind == "literal" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { + let values-inputs-and-checks = checked-types.map(t => (t.data.value, t.input, t.data.typeinfo.check)) + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + typ in unchecked-inputs or values-inputs-and-checks.any(((v, i, check)) => eq(x, v) and (typ in i or "any" in i) and (check == none or check(x))) + } + } else { + // If any check succeeds and the value has the correct input type, OK + let checks-and-inputs = checked-types.map(t => (t.input, t.check)) + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + // If one of the types without checks accepts this type as an input then we don't need + // to run any checks! + typ in unchecked-inputs or checks-and-inputs.any(((inp, check)) => (typ in inp or "any" in inp) and check(x)) + } + } + } + + // Try to optimize casts + let cast = if typeinfos.all(t => t.cast == none) { + none + } else { + let casting-types = typeinfos.filter(t => t.cast != none) + let first-casting-type = casting-types.first() + if ( + // If the casting types are all native, and none of the types before them + // accept their "cast-from" types, then we can fast track to a simple check: + // if within the 'cast-from' types, then cast, otherwise don't. + casting-types != () + and casting-types.all(t => t.type-kind == "native" and t.data in (float, content) and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) + and typeinfos.find(t => t.input.any(i => i == "any" or i in first-casting-type.input)) == first-casting-type + and (casting-types.len() == 1 or typeinfos.find(t => t.input.any(i => i == "any" or i in casting-types.at(1).input)) == casting-types.at(1)) + ) { + if casting-types.len() >= 2 { // just float and content + x => if type(x) == int { float(x) } else if x == none or type(x) in (str, symbol) [#x] else { x } + } else if first-casting-type.data == float { // just float + x => if type(x) == int { float(x) } else { x } + } else { // just content + x => if x == none or type(x) in (str, symbol) { [#x] } else { x } + } + } else { + // Generic case + x => { + let typ = type(x) + if typ == dictionary and custom-type-key in x { + // Custom type must be checked differently in inputs + typ = x.at(custom-type-key).id + } + let typeinfo = typeinfos.find(t => (typ in t.input or "any" in t.input) and (t.check == none or (t.check)(x))) + if typeinfo.cast == none { + x + } else { + (typeinfo.cast)(x) + } + } + } + } + + let error = if typeinfos.all(t => t.error == none) { + none + } else if typeinfos.all(t => t.type-kind == "literal" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { + let literals = typeinfos.map(t => str(t.data.represented)).join(", ", last: " or ") + let message = "given value wasn't equal to literals " + literals + x => message + } else if typeinfos.all(t => t.type-kind == "native-element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { + let funcs = typeinfos.map(t => repr(t.data.func)).join(", ", last: " or ") + let head = "expected native elements " + funcs + ", found " + x => head + { + if type(x) == content { func-name(x) } else { "a(n) " + typename(x) } + } + } else if typeinfos.all(t => (t.type-kind == "element" or t.type-kind == "native-element") and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { + let funcs = typeinfos.map(t => if t.type-kind == "element" { t.data.name } else { repr(t.data.func) + " (native)" }).join(", ", last: " or ") + let head = "expected elements " + funcs + ", found " + x => head + { + if type(x) == content { func-name(x) } else { "a(n) " + typename(x) } + } + } else { + let error-types = typeinfos.filter(t => t.error != none) + x => { + "all typechecks for union failed" + error-types.map(t => "\n hint (" + t.name + "): " + (t.error)(x)).sum(default: "") + } + } + + let is-option = typeinfos.first().type-kind == "native" and typeinfos.first().data == type(none) + let is-smart = typeinfos.first().type-kind == "native" and typeinfos.first().data == type(auto) + + let default = if is-option or is-smart { + // Default of 'none' for option(...) + // Default of 'auto' for smart(...) + typeinfos.first().default + } else { + () + } + + // Match built-in behavior by only folding option(T) or smart(T) if T can fold and the inner isn't explicitly none/auto + let fold = if typeinfos.len() == 2 and typeinfos.at(1).fold != none { + let other-typeinfo = typeinfos.at(1) + let other-fold = other-typeinfo.fold + if is-option { + if other-fold == auto { + (outer, inner) => if inner != none and outer != none { outer + inner } else { inner } + } else { + (outer, inner) => if inner != none and outer != none { other-fold(outer, inner) } else { inner } + } + } else if is-smart { + if other-fold == auto { + (outer, inner) => if inner != auto and outer != auto { outer + inner } else { inner } + } else { + (outer, inner) => if inner != auto and outer != auto { other-fold(outer, inner) } else { inner } + } + } else { + none + } + } else { + // TODO: We could consider folding an arbitrary union iff the outputs are all disjoint, + // so we can easily distinguish the typeinfo for an output based on the type. + // Otherwise, can't do much if e.g. an int could be typeinfo A (say, positive integer) + // or typeinfo B (say, negative integer) because checks apply to inputs and not outputs + // (unless, of course, there is no casting). + none + } + + ( + ..base-typeinfo, + type-kind: "union", + name: name, + data: typeinfos, + input: input, + output: output, + check: check, + cast: cast, + error: error, + default: default, + fold: fold, + ) +} + +// A result to indicate success and return a value. +#let ok(value) = { + (true, value) +} + +// A result to indicate failure, with an error value indicating what happened. +#let err(error) = { + (false, error) +} + +// Whether this result was successful. +#let is-ok(result) = { + type(result) == array and result.len() == 2 and result.first() == true +} + +// Wrap a typeinfo with some other data. +// Mostly unchecked variant of 'types.wrap'. +#let wrap(typeinfo, overrides) = { + ( + (..typeinfo, type-kind: "wrapped", data: (base: typeinfo, extra: none)) + + for (key, default) in base-typeinfo { + if key == type-key or key == "type-kind" { + continue + } + + if key in overrides { + let override = overrides.at(key) + if type(override) == function { + override = override(typeinfo.at(key, default: default)) + } + + if key == "data" { + (data: (base: typeinfo, extra: override)) + } else { + ((key): override) + } + } + } + ) +} + +// A particular collection of types. +#let collection(name, base, parameters, check: none, cast: none, error: none, ..args) = { + if check == none { + check = base.check + } + + if cast == none { + cast = base.cast + } + + if check == none and error == none { + error = base.error + } + + let other-args = args.named() + let default = if "default" in other-args { + other-args.default + } else { + base.default + } + + ( + ..base, + type-kind: "collection", + name: name + if parameters != () { " of " + parameters.map(t => t.name).join(", ", last: " and ") }, + data: (base: base, parameters: parameters), + check: check, + cast: cast, + error: error, + default: default, + ) +} + +// Create an array collection with a uniform parameter typeinfo for its elements. +#let array_(base-type, param, error: none) = { + assert(array in base-type.input or "any" in base-type.input) + let kind = param.type-kind + + collection( + "array", + base-type, + (param,), + check: if param.check == none and "any" in param.input { + none + } else if param.input == () { + // Propagate 'never' + _ => false + } else if "any" in param.input { + // Only need to run checks + a => a.all(param.check) + } else { + // Some optimizations ahead + // The proper code is at the bottom + let input = param.input + let check = param.check + if kind == "native" and param.data == dictionary and ("__future_cast" not in param or param.__future_cast.max-version < type-version) { + a => a.all(x => type(x) == dictionary and custom-type-key not in x) + } else if param.input.all(i => type(i) == type) and dictionary not in param.input { + // No custom types accepted (the check above excludes '(tid: ..., name: ...)' as well as "any") + // If this is a custom type, it will return type(x) = dictionary, so it will fail + // (Also excludes "custom type": the type of custom types) + // So that suffices + if input.len() == 1 { + let input = input.first() + if check == none { + a => a.all(x => type(x) == input) + } else { + a => a.all(x => type(x) == input and check(x)) + } + } else if input.len() == 2 { + let first = input.first() + let second = input.at(1) + if check == none { + a => a.all(x => type(x) == first or type(x) == second) + } else { + a => a.all(x => (type(x) == first or type(x) == second) and check(x)) + } + } else if check == none { + a => a.all(x => type(x) in input) + } else { + a => a.all(x => type(x) in input and check(x)) + } + } else if param.check == none { + a => a.all(x => typeid(x) in param.input) + } else { + a => a.all(x => typeid(x) in param.input and check(x)) + } + }, + + cast: if param.cast == none { + none + } else if kind == "native" and param.data == content and ("__future_cast" not in param or param.__future_cast.max-version < type-version) { + a => a.map(x => [#x]) + } else { + a => a.map(param.cast) + }, + + error: error + ) +} + +// Create a dict with a uniform parameter typeinfo for its values. +// (Keys are always strings.) +#let dict_(base-type, param, error: none) = { + assert(dictionary in base-type.input or "any" in base-type.input) + let kind = param.type-kind + + collection( + "dict", + base-type, + (param,), + check: { + // Simply check the array of values + // (We can pass 'any' as the base type since that doesn't affect the 'check') + let array-check = array_(any, param).check + if array-check == none { + none + } else { + d => array-check(d.values()) + } + }, + + cast: if param.cast == none { + none + } else if kind == "native" and param.data == content and ("__future_cast" not in param or param.__future_cast.max-version < type-version) { + d => { + for (k, v) in d { + d.at(k) = [#v] + } + d + } + } else { + let cast = param.cast + d => { + for (k, v) in d { + d.at(k) = cast(v) + } + d + } + }, + + error: error + ) +} diff --git a/src/libs/elembic/types/custom.typ b/src/libs/elembic/types/custom.typ new file mode 100644 index 0000000..96058ec --- /dev/null +++ b/src/libs/elembic/types/custom.typ @@ -0,0 +1,409 @@ +// Custom types! +#import "../data.typ": special-data-values, custom-type-key, custom-type-data-key, type-key, custom-type-version +#import "base.typ" +#import "types.typ" +#import "../fields.typ" as field-internals + +// Default folding procedure for custom types. +// Combines each inner type individually. +#let auto-fold(foldable-fields) = if foldable-fields == (:) { + // No fields to fold, so 'inner' always fully overwrites 'outer'. + // In that case, we can just sum inner with outer, adding its fields + // on top. + auto +} else { + (outer, inner) => { + let combined = outer + inner + + for (field-name, fold-data) in foldable-fields { + if field-name in inner { + let outer = outer.at(field-name, default: fold-data.default) + if fold-data.folder == auto { + combined.at(field-name) = outer + inner.at(field-name) + } else { + combined.at(field-name) = (fold-data.folder)(outer, inner.at(field-name)) + } + } + } + + combined + } +} + +#let auto-cast(from, fields: (:), constructor: none) = { + if from == dictionary { + value => constructor(..value) + } else { + assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.") + } +} + +#let auto-cast-check(from, fields: (:), parse-args: none) = { + if from == dictionary { + value => parse-args(arguments(..value)).first() + } else { + assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.") + } +} + +#let auto-cast-error(from, fields: (:), parse-args: none) = { + if from == dictionary { + value => parse-args(arguments(..value)).at(1) + } else { + assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.") + } +} + +#let declare( + name, + fields: none, + prefix: none, + default: none, + parse-args: auto, + typecheck: true, + allow-unknown-fields: false, + construct: none, + scope: none, + casts: none, + fold: none, +) = { + + let fields-hint = if type(fields) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single field: write 'fields: (field,)', not 'fields: (field)'" } else { "" } + let casts-hint = if type(casts) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single cast: write 'casts: ((from: ..., with: ...),)', not 'casts: ((from: ..., with: ...))'" } else { "" } + assert(type(fields) == array, message: "elembic: types.declare: please specify an array of fields, creating each field with the 'field' function." + fields-hint) + assert(prefix != none, message: "elembic: types.declare: please specify a 'prefix: ...' for your type, to distinguish it from types with the same name. If you are writing a package or template to be used by others, please do not use an empty prefix.") + assert(type(prefix) == str, message: "elembic: types.declare: the prefix must be a string, not '" + str(type(prefix)) + "'") + assert(parse-args == auto or type(parse-args) == function, message: "elembic: types.declare: 'parse-args' must be either 'auto' (use built-in parser) or a function (default arg parser, fields: dictionary, typecheck: bool) => (user arguments, include-required: true) => (bool (true on success, false on error), dictionary with parsed fields (or error message string if the bool is false)).") + assert(type(typecheck) == bool, message: "elembic: types.declare: the 'typecheck' argument must be a boolean (true to enable typechecking in the constructor, false to disable).") + assert(type(allow-unknown-fields) == bool, message: "elembic: types.declare: the 'allow-unknown-fields' argument must be a boolean.") + assert(construct == none or type(construct) == function, message: "elembic: types.declare: 'construct' must be 'none' (use default constructor) or a function receiving the original constructor and returning the new constructor.") + assert(default == none or type(default) == function, message: "elembic: types.declare: 'default' must be none or a function receiving the constructor and returning the default.") + assert(scope == none or type(scope) in (dictionary, module), message: "elembic: types.declare: 'scope' must be either 'none', a dictionary or a module") + assert( + casts == none + or type(casts) == array and casts.all( + d => ( + type(d) == dictionary + and "from" in d + and d.keys().all(k => k in ("from", "with", "check")) + and ("with" not in d or type(d.with) == function) + and ("check" not in d or d.check == none or type(d.check) == function) + ) + ), + message: "elembic: types.declare: 'casts' must be either 'none' or an array of dictionaries in the form (from: type, check (optional): none or casted value => bool, with (optional when 'from' is dictionary): constructor => casted value => your type)." + casts-hint + ) + assert(fold == none or fold == auto or type(fold) == function, message: "elembic: types.declare: 'fold' must be 'none' (no folding), 'auto' (fold each field individually) or a function 'default constructor => auto (same as (a, b) => a + b but more efficient) or function (outer, inner) => combined value'.") + + let tid = base.unique-id("t", prefix, name) + let fields = field-internals.parse-fields(fields, allow-unknown-fields: allow-unknown-fields) + let (all-fields, user-fields, foldable-fields) = fields + let auto-fold = if fold == auto { auto-fold(foldable-fields) } else { none } + + let default-arg-parser = field-internals.generate-arg-parser( + fields: fields, + general-error-prefix: "elembic: type '" + name + "': ", + field-error-prefix: field-name => "field '" + field-name + "' of type '" + name + "': ", + typecheck: typecheck + ) + + let parse-args = if parse-args == auto { + default-arg-parser + } else { + let parse-args = parse-args(default-arg-parser, fields: fields, typecheck: typecheck) + if type(parse-args) != function { + assert(false, message: "elembic: types.declare: 'parse-args', when specified as a function, receives the default arg parser alongside `fields: fields dictionary` and `typecheck: bool`, and must return a function (the new arg parser), and not " + base.typename(parse-args)) + } + + parse-args + } + + let default-fields = fields.user-fields.values().map(f => if f.required { (:) } else { ((f.name): f.default) }).sum(default: (:)) + + let typeid = (tid: tid, name: name) + + // We will specify default in a bit, once we declare the constructor + let typeinfo = ( + ..base.base-typeinfo, + type-kind: "custom", + name: name, + input: (typeid,), + output: (typeid,), + data: ( + id: typeid, + + // Original type before adding casts + // or none if this is already the type before casts + // (used for 'exact()') + pre-casts: none + ) + ) + + let type-data = ( + (custom-type-data-key): true, + (custom-type-key): ( + data-kind: "type-instance", + fields: ( + version: custom-type-version, + tid: tid, + id: typeid, + ), + func: declare, + default-constructor: declare, + tid: "b_custom type", + id: "custom type", + fields-known: true, + valid: true + ), + version: custom-type-version, + name: name, + tid: tid, + id: typeid, + // We will add this here once the constructor is declared + typeinfo: none, + scope: scope, + parse-args: parse-args, + default-fields: default-fields, + user-fields: user-fields, + all-fields: all-fields, + fields: fields, + typecheck: typecheck, + allow-unknown-fields: allow-unknown-fields, + default-constructor: none, + func: none, + ) + + let process-casts = if casts == none { + none + } else { + // Trick: We assign cast to each cast-from type and create a union, + // and use its generated check/cast functions as our own + default-constructor => { + let typeinfos = casts.map(cast => { + let (res, from) = types.validate(cast.from) + if not res { + assert(false, message: "elembic: types.declare: invalid cast-from type: " + from) + } + + let (cast-check, with, cast-error) = if "with" in cast { + (cast.at("check", default: none), (cast.with)(default-constructor), none) + } else if from.type-kind == "native" and from.data == dictionary { + assert(fields.required-pos-fields == (), message: "elembic: types.declare: cannot generate automatic cast from dict when there are required positional fields.") + + if "check" in cast { + ( + cast.check, + auto-cast(dictionary, fields: fields, constructor: default-constructor), + none, + ) + } else { + ( + auto-cast-check(dictionary, fields: fields, parse-args: parse-args), + auto-cast(dictionary, fields: fields, constructor: default-constructor), + auto-cast-error(dictionary, fields: fields, parse-args: parse-args), + ) + } + } else { + assert( + false, + message: "elembic: types.declare: cast 'with' can only be omitted for 'from: dictionary'. It must receive the default constructor and return a function 'casted value => your type'." + ) + } + + if type(with) != function { + assert( + false, + message: "elembic: types.declare: cast 'with' must receive the default constructor and return a function 'casted value => your type'. Received " + base.typename(with) + ) + } + + let from-cast = from.cast + + types.wrap( + from, + check: from-check => if from-check == none { + if cast-check == none { + none + } else if from.cast == none { + cast-check + } else { + value => cast-check(from-cast(value)) + } + } else if cast-check == none { + from-check + } else if from.cast == none { + value => from-check(value) and cast-check(value) + } else { + value => from-check(value) and cast-check(from-cast(value)) + }, + + output: (typeid,), + + cast: from-cast => if from-cast == none { + with + } else { + value => with(from-cast(value)) + }, + + default: (), + fold: none, + ..if cast-error == none { (:) } else { ( + error: if "check" not in from or from.check == none { + _ => cast-error + } else { + from-error => value => if from-error == none or from-check(value) { cast-error(value) } else { from-error(value) } + }, + ) } + ) + }) + + // Accept our own typeinfo first and foremost + let union = base.union((typeinfo,) + typeinfos) + + assert( + union.output == (typeid,) and union.default == () and union.fold == none, + message: "elembic: types.declare: internal error: cast generated invalid union: " + repr(union) + ) + + ( + input: union.input, + output: union.output, + check: union.check, + cast: union.cast, + error: if union.error == none { + _ => "failed to cast to custom type '" + name + "'" + } else { + x => (union.error)(x).replace("all typechecks for union failed", "all casts to custom type '" + name + "' failed") + }, + data: typeinfo.data + (pre-casts: typeinfo) + ) + } + } + + let default-constructor(..args, __elembic_data: none, __elembic_func: auto) = { + if __elembic_func == auto { + __elembic_func = default-constructor + } + + let default-constructor = default-constructor.with(__elembic_func: __elembic_func) + if __elembic_data != none { + return if __elembic_data == special-data-values.get-data { + let typeinfo = typeinfo + if process-casts != none { process-casts(default-constructor) } else { (:) } + if default != none { + typeinfo.default = (default(default-constructor),) + } + + if auto-fold != none { + typeinfo.fold = auto-fold + } else if type(fold) == function { + let fold = fold(default-constructor) + if fold != auto and type(fold) != function { + assert(false, message: "elembic: types: custom type did not specify a valid fold, must be a function default constructor => value, got " + base.typename(fold)) + } + typeinfo.fold = fold + } + + (data-kind: "custom-type-data", ..type-data, typeinfo: typeinfo, func: __elembic_func, default-constructor: default-constructor) + } else { + assert(false, message: "elembic: types: invalid data key to constructor: " + repr(__elembic_data)) + } + } + + let (res, args) = parse-args(args, include-required: true) + if not res { + assert(false, message: args) + } + + let final-fields = default-fields + args + + if foldable-fields != (:) { + // Fold received arguments with defaults + for (field-name, fold-data) in foldable-fields { + if field-name in args { + let outer = default-fields.at(field-name, default: fold-data.default) + if fold-data.folder == auto { + final-fields.at(field-name) = outer + args.at(field-name) + } else { + final-fields.at(field-name) = (fold-data.folder)(outer, args.at(field-name)) + } + } + } + } + + final-fields.insert( + custom-type-key, + ( + data-kind: "type-instance", + fields: final-fields, + func: __elembic_func, + default-constructor: default-constructor, + tid: tid, + id: (tid: tid, name: name), + scope: scope, + fields-known: true, + valid: true + ) + ) + + final-fields + } + + default = if default == none { + () + } else { + let default = default(default-constructor) + assert( + type(default) == dictionary and custom-type-key in default and default.at(custom-type-key).id == typeid, + message: "elembic: types.declare: the 'default' function must return an instance of the new type using the provided constructor, not " + repr(default) + ) + + + (default,) + } + + fold = if auto-fold != none { + auto-fold + } else if type(fold) == function { + let fold = fold(default-constructor) + if fold != auto and type(fold) != function { + assert(false, message: "elembic: types.declare: a valid fold was not specified, must be a function default constructor => value, got " + base.typename(fold)) + } + fold + } else { + none + } + + if process-casts != none { + typeinfo += process-casts(default-constructor) + } + typeinfo.default = default + typeinfo.fold = fold + type-data.typeinfo = typeinfo + + let final-constructor = if construct != none { + { + let test-construct = construct(default-constructor) + assert(type(test-construct) == function, message: "elembic: types.declare: the 'construct' function must receive the default constructor and return the new constructor, a new function, not '" + str(type(test-construct)) + "'.") + } + + let final-constructor(..args, __elembic_data: none) = { + if __elembic_data != none { + return if __elembic_data == special-data-values.get-data { + (data-kind: "custom-type-data", ..type-data, func: final-constructor, default-constructor: default-constructor.with(__elembic_func: final-constructor)) + } else { + assert(false, message: "elembic: types: invalid data key to constructor: " + repr(__elembic_data)) + } + } + + construct(default-constructor.with(__elembic_func: final-constructor))(..args) + } + + final-constructor + } else { + default-constructor + } + + type-data.default-constructor = default-constructor.with(__elembic_func: final-constructor) + type-data.func = final-constructor + + final-constructor +} diff --git a/src/libs/elembic/types/native.typ b/src/libs/elembic/types/native.typ new file mode 100644 index 0000000..aead5c0 --- /dev/null +++ b/src/libs/elembic/types/native.typ @@ -0,0 +1,341 @@ +// Typst-native types. +#import "../data.typ": type-key +#import "base.typ": base-typeinfo, ok, err + +// Tiling type (renamed in Typst 0.13.0) +#let tiling = if sys.version < version(0, 13, 0) { pattern } else { tiling } + +#let native-base = ( + ..base-typeinfo, + type-kind: "native", +) + +// Generic typeinfo for a native type. +// PROPERTY: if type key is native, then output has the native type, +// and input has a list of native types that can be cast to it. +#let generic-typeinfo(native-type) = { + assert(type(native-type) == type(str), message: "elembic: internal error: not a type") + + ( + ..native-base, + name: str(native-type), + input: (native-type,), + output: (native-type,), + data: native-type, + ) +} + +// Castable types + +#let content_ = ( + ..native-base, + name: str(content), + input: (type(none), content, str, symbol), + output: (content,), + data: content, + cast: x => [#x], + default: ([],), +) +#let float_ = ( + ..native-base, + name: str(float), + input: (float, int), + output: (float,), + data: float, + cast: float, + default: (0.0,), +) +#let stroke-keys = ("paint", "thickness", "cap", "join", "dash", "miter-limit") +#let stroke_ = ( + ..native-base, + name: str(stroke), + input: (stroke, length, color, gradient, tiling, dictionary), + output: (stroke,), + data: stroke, + cast: stroke, + check: v => type(v) != dictionary or v.keys().all(k => k in stroke-keys), + default: (stroke(),), + // Allow specifying e.g. 4pt in one set rule, red in the other => 4pt + red in the end + fold: (outer, inner) => { + // Can't sum stroke with stroke, so can't optimize with 'fold: auto' :( + stroke( + paint: if inner.paint == auto { outer.paint } else { inner.paint }, + thickness: if inner.thickness == auto { outer.thickness } else { inner.thickness }, + cap: if inner.cap == auto { outer.cap } else { inner.cap }, + join: if inner.join == auto { outer.join } else { inner.join }, + dash: if inner.dash == auto { outer.dash } else { inner.dash }, + miter-limit: if inner.miter-limit == auto { outer.miter-limit } else { inner.miter-limit }, + ) + }, +) +#let relative_ = ( + ..native-base, + name: str(relative), + input: (relative, length, ratio), + output: (relative,), + data: relative, + cast: x => x + 0% + 0pt, + default: (0% + 0pt,), +) +#let function_ = ( + ..native-base, + name: str(function), + // Would add symbol as well, but missing a reliable way to check for callable symbols + input: (type, function), + output: (type, function,), + data: function, +) + +// Folding types (also includes stroke above) +#let array_ = ( + ..native-base, + name: str(array), + input: (array,), + output: (array,), + data: array, + default: ((),), + + // Array fields are added together by default. + fold: auto, +) + +#let alignment_ = ( + ..native-base, + name: str(alignment), + input: (alignment,), + output: (alignment,), + data: alignment, + fold: (outer, inner) => if inner.axis() == none or outer.axis() == inner.axis() { + // If axis A == axis B, we override. For example, left -> right. (No sum) + // Same if both are none (2D alignments), in which case inner fully overrides as well (left + top -> center + bottom). + // In addition, if inner axis is none (it is a 2D alignment), it overrides in both ways (left -> right + top). + inner + } else if outer.axis() == none { + // Here, we know that inner isn't 2D, so either outer is 2D or both have different axes. + // If outer is 2D and inner is 1D, inner replaces its axis in outer, but the other axis is kept. + if inner.axis() == "horizontal" { + inner + outer.y + } else { + outer.x + inner + } + } else { + // Both are 1D and have distinct axes, so we just sum. + // left and top => left + top + // bottom and right => right + bottom + inner + outer + } +) + +// Simple types (no casting) + +#let str_ = ( + ..native-base, + name: str(str), + input: (str,), + output: (str,), + data: str, + default: ("",) +) +#let bool_ = ( + ..native-base, + name: str(bool), + input: (bool,), + output: (bool,), + data: bool, + default: (false,) +) +#let dict_ = ( + ..native-base, + name: str(dictionary), + input: (dictionary,), + output: (dictionary,), + data: dictionary, + default: ((:),), +) +#let int_ = ( + ..native-base, + name: str(int), + input: (int,), + output: (int,), + data: int, + default: (0,), +) +#let color_ = ( + ..native-base, + name: str(color), + input: (color,), + output: (color,), + data: color, +) +#let gradient_ = ( + ..native-base, + name: str(gradient), + input: (gradient,), + output: (gradient,), + data: gradient, +) +#let tiling_ = ( + ..native-base, + name: str(tiling), + input: (tiling,), + output: (tiling,), + data: tiling, +) +#let datetime_ = ( + ..native-base, + name: str(datetime), + input: (datetime,), + output: (datetime,), + data: datetime, +) +#let angle_ = ( + ..native-base, + name: str(angle), + input: (angle,), + output: (angle,), + data: angle, + default: (0deg,), +) +#let ratio_ = ( + ..native-base, + name: str(ratio), + input: (ratio,), + output: (ratio,), + data: ratio, + default: (0%,), +) +#let length_ = ( + ..native-base, + name: str(length), + input: (length,), + output: (length,), + data: length, + default: (0pt,), +) +#let fraction_ = ( + ..native-base, + name: str(fraction), + input: (fraction,), + output: (fraction,), + data: fraction, + default: (0fr,), +) +#let duration_ = ( + ..native-base, + name: str(duration), + input: (duration,), + output: (duration,), + data: duration, + default: (duration(seconds: 0),), +) +#let type_ = ( + ..native-base, + name: str(type), + input: (type,), + output: (type,), + data: type, +) +#let arguments_ = ( + ..native-base, + name: str(arguments), + input: (arguments,), + output: (arguments,), + data: arguments, + default: (arguments(),), +) +#let bytes_ = ( + ..native-base, + name: str(bytes), + input: (bytes,), + output: (bytes,), + data: bytes, + default: (bytes(()),), +) +#let version_ = ( + ..native-base, + name: str(version), + input: (version,), + output: (version,), + data: version, + default: (version(0, 0, 0),), +) + +// None / auto + +#let none_ = ( + ..native-base, + name: "none", + input: (type(none),), + output: (type(none),), + data: type(none), + default: (none,) +) +#let auto_ = ( + ..native-base, + name: "auto", + input: (type(auto),), + output: (type(auto),), + data: type(auto), + default: (auto,) +) + +// Return the typeinfo for a native type. +#let typeinfo(t) = { + let out = if t == content { + content_ + } else if t == int { + int_ + } else if t == bool { + bool_ + } else if t == float { + float_ + } else if t == type(none) { + none_ + } else if t == type(auto) { + auto_ + } else if t == dictionary { + dict_ + } else if t == array { + array_ + } else if t == str { + str_ + } else if t == color { + color_ + } else if t == gradient { + gradient_ + } else if t == datetime { + datetime_ + } else if t == duration { + duration_ + } else if t == function { + function_ + } else if t == relative { + relative_ + } else if t == stroke { + stroke_ + } else if t == tiling { + tiling_ + } else if t == type { + type_ + } else if t == angle { + angle_ + } else if t == alignment { + alignment_ + } else if t == ratio { + ratio_ + } else if t == length { + length_ + } else if t == fraction { + fraction_ + } else if t == arguments { + arguments_ + } else if t == bytes { + bytes_ + } else if t == version { + version_ + } else { + generic-typeinfo(t) + } + + (true, out) +} diff --git a/src/libs/elembic/types/types.typ b/src/libs/elembic/types/types.typ new file mode 100644 index 0000000..51fd1fb --- /dev/null +++ b/src/libs/elembic/types/types.typ @@ -0,0 +1,405 @@ +// The type system used by fields. +#import "../data.typ": data, special-data-values, type-key, custom-type-key, custom-type-data-key, eq, type-version +#import "base.typ" as base: ok, err +#import "native.typ" + +// The default value for a type. +#let default(type_) = { + if type_.default == () { + let prefix = if type_.type-kind in ("native", "union") { type_.type-kind + " " } else { "" } + err(prefix + "type '" + type_.name + "' has no known default, please specify an explicit 'default: value' or set 'required: true' for the field") + } else { + ok(type_.default.first()) + } +} + +#let sequence_ = [].func() +#let typeof(value) = { + let element-data + if type(value) == dictionary and custom-type-key in value { + if custom-type-data-key in value { + base.custom-type + } else { + (value.at(custom-type-key).func)(__elembic_data: special-data-values.get-data).typeinfo + } + } else if type(value) == content and value.func() == sequence_ and { + element-data = data(value) + element-data.eid != none + } { + if "name" in element-data and type(element-data.name) == str { + base.element(element-data.name, element-data.eid) + } else { + base.element("unknown-element", element-data.eid) + } + } else { + let (res, typeinfo) = native.typeinfo(type(value)) + if not res { + assert(false, message: "elembic: types.typeof: " + typeinfo) + } + + typeinfo + } +} + +// Literal type +// Only accepted if value is equal to the literal. +// Input and output are equal to the value. +// +// Uses base typeinfo information for information such as casts and whatnot. +#let literal(value) = { + if value == none { + native.none_ + } else if value == auto { + native.auto_ + } else { + base.literal(value, typeof(value)) + } +} + +// Obtain the typeinfo for a type. +// +// Returns ok(typeinfo), or err(error) if there is no corresponding typeinfo. +#let validate(type_) = { + if type(type_) == function { + let data = type_(__elembic_data: special-data-values.get-data) + let data-kind = data.at("data-kind", default: "unknown") + if data-kind == "custom-type-data" { + type_ = data.typeinfo + } else if data-kind == "element" { + type_ = base.element(data.name, data.eid) + } else { + return (false, "Received invalid type: " + repr(type_) + "\n hint: use 'types.literal(value)' to indicate only that particular value is valid") + } + } + + if type(type_) == type { + native.typeinfo(type_) + } else if type(type_) == dictionary and type-key in type_ { + (true, type_) + } else if type(type_) == dictionary and custom-type-data-key in type_ { + (true, type_.typeinfo) + } else if type(type_) == function { + (false, "A function is not a valid type. (You can use 'types.literal(func)' to only accept a particular function.)") + } else if type_ == none or type_ == auto { + // Accept none or auto to mean their types + native.typeinfo(type(type_)) + } else if type(type_) not in (dictionary, array, content) { + // Automatically accept literals + (true, literal(type_)) + } else { + (false, "Received invalid type: " + repr(type_) + "\n hint: use 'types.literal(value)' to indicate only that particular value is valid") + } +} + +// Error when a value doesn't conform to a certain cast +#let generate-cast-error(value, typeinfo, hint: none) = { + let message = if "any" not in typeinfo.input and base.typeid(value) not in typeinfo.input { + if typeinfo.input == () { + "type '" + typeinfo.name + "' does not accept any values" + } else { + ( + "expected " + + typeinfo.input.map(t => if type(t) == dictionary and "name" in t { t.name } else { str(t) }).join(", ", last: " or ") + + ", found " + + base.typename(value) + ) + } + } else if typeinfo.at("error", default: none) != none { + (typeinfo.error)(value) + } else { + "typecheck for " + typeinfo.name + " failed" + } + let given-hint = if hint == none { "" } else { "\n hint: " + hint } + + message + given-hint +} + +// Try to accept value via given typeinfo or return error +// Returns ok(value) a.k.a. (true, value) on success +// Returns err(value) a.k.a. (false, value) on error +#let cast(value, typeinfo) = { + if type(typeinfo) != dictionary or type-key not in typeinfo { + let (res, typeinfo-or-err) = validate(typeinfo) + if not res { + assert(false, message: "elembic: types.cast: " + typeinfo-or-err) + } + typeinfo = typeinfo-or-err + } + + let kind = typeinfo.type-kind + if kind == "any" { + (true, value) + } else { + let value-type = type(value) + if value-type == dictionary and custom-type-key in value { + value-type = value.at(custom-type-key).id + } + + if kind == "literal" and typeinfo.cast == none and ("__future_cast" not in typeinfo or typeinfo.__future_cast.max-version < type-version) { + if eq(value, typeinfo.data.value) and (value-type in typeinfo.input or "any" in typeinfo.input) and (typeinfo.data.typeinfo.check == none or (typeinfo.data.typeinfo.check)(value)) { + (true, value) + } else { + (false, generate-cast-error(value, typeinfo)) + } + } else if ( + value-type not in typeinfo.input and "any" not in typeinfo.input + or typeinfo.check != none and not (typeinfo.check)(value) + ) { + (false, generate-cast-error(value, typeinfo)) + } else if typeinfo.cast == none { + (true, value) + } else if kind == "native" and typeinfo.data == content and ("__future_cast" not in typeinfo or typeinfo.__future_cast.max-version < type-version) { + (true, [#value]) + } else { + (true, (typeinfo.cast)(value)) + } + } +} + +// Expected types for each typeinfo key. +#let overridable-typeinfo-types = ( + name: (check: a => type(a) == str, error: "string or function old name => new name"), + input: (check: a => type(a) == array and a.all(x => x == "any" or x == "custom type" or type(x) == type or (type(x) == dictionary and "tid" in x)), error: "array of \"any\", \"custom type\", type, or custom type id (tid: ...), or function old input => new input"), + output: (check: a => type(a) == array and a.all(x => x == "any" or x == "custom type" or type(x) == type or (type(x) == dictionary and "tid" in x)), error: "array of \"any\", \"custom type\", type, or custom type id (tid: ...), or function old output => new output"), + check: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function value => bool"), + cast: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function checked input => output"), + error: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function checked input => error string"), + default: (check: d => d == () or type(d) == array and d.len() == 1, error: "empty array for no default, singleton array for one default, or function old default => new default"), + fold: (check: f => f == none or f == auto or type(f) == function, error: "none for no folding, auto to fold with sum (same as (a, b) => a + b), or function receiving old fold and returning either none or auto, or a new function (outer, inner) => combined value"), +) + +// Wrap a type, altering its properties while keeping (or replacing) its input types and checks. +#let wrap(type_, ..data) = { + assert(data.pos() == (), message: "elembic: types.wrap: unexpected positional arguments") + let (res, typeinfo) = validate(type_) + if not res { + assert(false, message: "elembic: types.wrap: " + typeinfo) + } + + let overrides = data.named() + for (key, value) in overrides { + let (check: validate-value, error: key-error) = overridable-typeinfo-types.at(key, default: (check: none, error: none)) + if validate-value == none or key-error == none { + assert(false, message: "elembic: types.wrap: invalid key '" + key + "', must be one of " + overridable-typeinfo-types.keys().join(", ", last: " or ")) + } + + if type(value) == function { + value = value(typeinfo.at(key, default: base.base-typeinfo.at(key))) + } + + if type(value) != function and not validate-value(value) { + assert(false, message: "elembic: types.wrap: invalid value for key '" + key + "', expected " + key-error) + } + } + + if "any" not in typeinfo.output and "cast" in overrides and "output" not in overrides or "output" in overrides and "any" in overrides.output { + // - Collapse "any" + other types into just "any"; + // - If there is a cast and output is unknown, then set it to any for safety (should we error?) + overrides.output = ("any",) + } + + if typeinfo.cast != none and "output" in overrides and "cast" not in overrides and "any" not in overrides.output and typeinfo.output.any(o => o not in overrides.output) { + // If output was changed to a list which isn't 'any' and isn't a superset of the previous output, + // then remove casting as it is no longer safe (might produce something that is invalid) + // (TODO: Should we error?) + overrides.cast = none + } + + if "input" in overrides and "any" in overrides.input { + // - Collapse "any" + other types into just "any" + overrides.input = ("any",) + } + + if "default" not in overrides and typeinfo.default != () and ("check" in overrides or "output" in overrides and "any" not in overrides.output and typeinfo.output.any(o => o not in overrides.output)) { + // Not sure if default would fit those criteria anymore: + // 1. By overriding the check, it's possible that a type such as positive int (check: int > 0) would no longer + // have an acceptable default when changing its check to, say, negative int (check: int < 0). + // 2. By overriding the output and removing previous output types, it's possible the default no longer has a valid type (it must be a valid output). + overrides.default = () + } + + if ("check" in overrides or "output" in overrides) and "fold" not in overrides { + // Folding might not be valid anymore: + // 1. By overriding the check, it's possible a fold that, say, adds two numbers, would no longer be valid + // if, for example, the new check ensures each number is smaller than 59 (you might add up to that). + // In addition, the fold might now receive parameters that would fail the new check while being cast. + // 2. By overriding the output: + // a. and removing old output, it's possible the fold produces invalid output. + // b. and adding new output, it's possible the fold receives parameters of an unexpected type. + overrides.fold = none + } + + let new-default = overrides.at("default", default: typeinfo.default) + let new-output = overrides.at("output", default: typeinfo.output) + assert( + new-default == () + or "any" in new-output + or base.typeid(new-default.first()) in new-output, + + message: "elembic: types.wrap: new default (currently " + repr(if new-default == () { none } else { new-default.first() }) + ") must have a type within possible 'output' types of the new type (currently " + if new-output == () { "empty" } else { new-output.map(t => if type(t) == dictionary { t.name } else { str(t) }).join(", ", last: " or ") } + "), since it is itself an output\n hint: you can either change the default, or update possible output types with 'output: (new, list)' to indicate which native or custom types your wrapped type might end up as after casts (if there are casts)." + ) + + base.wrap(typeinfo, overrides) +} + +// Specifies that any from a given selection of types is accepted. +#let union(..args) = { + let types = args.pos() + assert(types != (), message: "elembic: types.union: please specify at least one type") + + let typeinfos = types.map(type_ => { + let (res, typeinfo-or-err) = validate(type_) + assert(res, message: if not res { "elembic: types.union: " + typeinfo-or-err } else { "" }) + + typeinfo-or-err + }) + + base.union(typeinfos) +} + +// An optional type (can be 'none'). +#let option(type_) = union(type(none), type_) + +// A type which can be 'auto'. +#let smart(type_) = union(type(auto), type_) + +#let array_(type_) = { + let (res, param) = validate(type_) + if not res { + assert(false, message: "elembic: types.array: " + param) + } + + base.array_( + native.array_, + param, + + error: if param.check == none { + a => { + let (count, message) = a.enumerate().fold((0, ""), ((count, message), (i, element)) => { + if "any" not in param.input and base.typeid(element) not in param.input { + (count + 1, message + "\n hint: at position " + str(i) + ": " + generate-cast-error(element, param)) + } else { + (count, message) + } + }) + + let n-elements = if count == 1 { "an element" } else { str(count) + " elements" } + n-elements + " in an array of " + param.name + " did not typecheck" + message + } + } else { + a => { + let (count, message) = a.enumerate().fold((0, ""), ((count, message), (i, element)) => { + if "any" not in param.input and base.typeid(element) not in param.input or not (param.check)(element) { + (count + 1, message + "\n hint: at position " + str(i) + ": " + generate-cast-error(element, param)) + } else { + (count, message) + } + }) + + let n-elements = if count == 1 { "an element" } else { str(count) + " elements" } + n-elements + " in an array of " + param.name + " did not typecheck" + message + } + } + ) +} + +#let dict_(type_) = { + let (res, param) = validate(type_) + if not res { + assert(false, message: "elembic: types.array: " + param) + } + + base.dict_( + native.dict_, + param, + + error: if param.check == none { + d => { + let (count, message) = d.pairs().fold((0, ""), ((count, message), (key, value)) => { + if "any" not in param.input and base.typeid(value) not in param.input { + (count + 1, message + "\n hint: at key " + repr(key) + ": " + generate-cast-error(value, param)) + } else { + (count, message) + } + }) + + let n-elements = if count == 1 { "a value" } else { str(count) + " values" } + n-elements + " in a dictionary of " + param.name + " did not typecheck" + message + } + } else { + d => { + let (count, message) = d.pairs().fold((0, ""), ((count, message), (key, value)) => { + if "any" not in param.input and base.typeid(value) not in param.input or not (param.check)(value) { + (count + 1, message + "\n hint: at key " + repr(key) + ": " + generate-cast-error(value, param)) + } else { + (count, message) + } + }) + + let n-elements = if count == 1 { "a value" } else { str(count) + " values" } + n-elements + " in a dictionary of " + param.name + " did not typecheck" + message + } + } + ) +} + +// Native paint type. Can be used for fills, strokes and so on. +#let paint = union(color, gradient, native.tiling_) + +// Force the type to only accept its outputs (disallow casting). +// Folding is kept if possible. +#let exact(type_) = { + let (res, type_) = validate(type_) + if not res { + assert(false, message: "elembic: types.exact: " + type_) + } + + let key = if type(type_) == dictionary and "type-kind" in type_ { type_.type-kind } else { none } + if key == "union" { + // exact(union(A, B)) === union(exact(A), exact(B)) + union(..type_.data.map(exact)) + } else if type(type_) == type or key == "native" { + // exact(float) => can only pass float, not int + // exact(stroke) => can only pass stroke, not length, gradient, dict, etc. + let native-type = type_.data + ( + ..native.generic-typeinfo(native-type), + default: if type_.default != () and type(type_.default.first()) == native-type { type_.default } else { () }, + + // Fold is an output => output function. The new output will be just (native-type,), + // so if fold previously accepted that native type, it will still accept it, so it + // can be kept. + fold: if native-type in type_.output { type_.fold } else { none }, + ) + } else if key == "literal" { + // exact(literal) => literal with base type modified to exact(base type) + assert(type(type_.data.value) not in (dictionary, array), message: "elembic: types.exact: exact literal types for custom types, dictionaries and arrays are not supported\n hint: consider customizing the check function to recursively check fields if the performance is acceptable") + + base.literal(type_.data.value, exact(type_.data.typeinfo)) + } else if key == "any" or key == "never" { + // exact(any) => any (same) + // exact(never) => never (same) + type_ + } else if key == "collection" { + if "base" in type_.data and "parameters" in type_.data { + let base-kind = type_.data.base.at("type-kind", default: none) + if base-kind == "native" and type_.data.base.data == array { + array_(..type_.data.parameters.map(exact)) + } else if base-kind == "native" and type_.data.base.data == dictionary { + dict_(..type_.data.parameters.map(exact)) + } else { + assert(false, message: "elembic: types.exact: unknown collection with type kind '" + base-kind + "'" + if base-kind == "native" { ", base native type '" + type_.data.base.name + "'" } else { "" }) + } + } else { + assert(false, message: "elembic: types.exact: invalid collection given") + } + } else if key == "custom" { + if type_.data.pre-casts == none { + type_ + } else { + type_.data.pre-casts + } + } else { + assert(false, message: "elembic: types.exact: unsupported type kind " + key + ", supported kinds include native types, literals, custom types, arrays, dicts, 'any' and 'never'") + } +} From c199b7674034061ca1393b2e25342ec974e5e390 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Tue, 27 May 2025 02:42:10 +0200 Subject: [PATCH 03/20] Elembic migration --- src/model/align.typ | 0 src/model/arrow.typ | 38 +++++++++++++++++ src/model/element.typ | 86 ++++++++++++++++++++++++++++++++++++++ src/model/group.typ | 76 +++++++++++++++++++++++++++++++++ src/model/molecule.typ | 93 +++++++++++++++++++++++++++++++++++++++++ src/model/reaction.typ | 43 +++++++++++++++++++ src/typing.typ | 22 ++++++++++ src/utils.typ | 95 +++++++++++++++++++++++++++++------------- 8 files changed, 423 insertions(+), 30 deletions(-) create mode 100644 src/model/align.typ create mode 100644 src/model/arrow.typ create mode 100644 src/model/element.typ create mode 100644 src/model/group.typ create mode 100644 src/model/molecule.typ create mode 100644 src/model/reaction.typ create mode 100644 src/typing.typ diff --git a/src/model/align.typ b/src/model/align.typ new file mode 100644 index 0000000..e69de29 diff --git a/src/model/arrow.typ b/src/model/arrow.typ new file mode 100644 index 0000000..3a92864 --- /dev/null +++ b/src/model/arrow.typ @@ -0,0 +1,38 @@ +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": ( + get-arrow, +) + +#let arrow( + kind: 0, + top: (), + bottom: (), +) = { } + +#let draw-arrow(it) = { + math.attach( + math.stretch( + get-arrow(it.kind), + size: 100% + 2em, + ), + t: for top-child in it.top { + top-child + }, + b: for bottom-child in it.bottom { + bottom-child + }, + ) +} + +#let arrow = e.element.declare( + "arrow", + prefix: "typsium", + + display: draw-arrow, + + fields: ( + e.field("kind", int, default: 0), + e.field("top", e.types.array(e.types.any), default: ()), + e.field("bottom", e.types.array(e.types.any), default: ()), + ), +) diff --git a/src/model/element.typ b/src/model/element.typ new file mode 100644 index 0000000..fabf9ee --- /dev/null +++ b/src/model/element.typ @@ -0,0 +1,86 @@ +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": ( + count-to-content, + charge-to-content, + none-coalesce, + customizable-attach, +) + +#let element( + symbol: "", + count: 1, + charge: 0, + oxidation: none, + a: none, + z: none, + rest: none, + radical: false, + affect-layout: true, +) = { } + +#let draw-element(it) = { + let base = it.symbol + if it.rest != none { + if type(it.rest) == content { + base += it.rest + } else if type(it.rest) == int { + base += ['] * it.rest + } + } + let mass-number = it.a + if type(it.a) == int { + mass-number = [#it.a] + } + let atomic-number = it.z + if type(it.z) == int { + atomic-number = [#it.z] + } + + customizable-attach( + base, + t: it.oxidation, + tr: charge-to-content(it.charge, radical: it.radical), + br: count-to-content(it.count), + tl: mass-number, + bl: atomic-number, + affect-layout: it.affect-layout, + ) + // } else { + // math.attach( + // base, + // t: it.oxidation, + // tr: box(place(bottom + left, charge-to-content(it.charge, radical: it.radical))), + // br: box(place(bottom + left, count-to-content(it.count))), + // tl: box(place(bottom+ right, mass-number)), + // bl: box(place(bottom+ right, atomic-number)), + // ) + + // box( + // place( + // bottom + left, + // // float: true, + // // box(fill:red, width:1em, height:1em), + // ), + // ) + // base +} +} + +#let element = e.element.declare( + "element", + prefix: "typsium", + + display: draw-element, + + fields: ( + e.field("symbol", e.types.union(str, content), default: none, required: true), + e.field("count", e.types.union(int, content), default: 1), + e.field("charge", e.types.union(int, content), default: 0), + e.field("oxidation", e.types.union(int, content), default: none), + e.field("a", e.types.union(int, content), default: none), + e.field("z", e.types.union(int, content), default: none), + e.field("rest", e.types.union(int, content), default: none), + e.field("radical", bool, default: false), + e.field("affect-layout", bool, default: true), + ), +) \ No newline at end of file diff --git a/src/model/group.typ b/src/model/group.typ new file mode 100644 index 0000000..a3b7d20 --- /dev/null +++ b/src/model/group.typ @@ -0,0 +1,76 @@ +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": ( + count-to-content, + charge-to-content, + get-bracket, + customizable-attach, +) + +#let group( + kind: 1, + count: 1, + charge: 0, + grow-brackets: true, + affect-layout: true, + ..children, +) = { } + +#let draw-group(it) = { + let result = customizable-attach( + if it.grow-brackets { + math.lr({ + get-bracket(it.kind, open: true) + for child in it.children { + child + } + get-bracket(it.kind, open: false) + }) + } else { + get-bracket(it.kind, open: true) + for child in it.children { + child + } + get-bracket(it.kind, open: false) + }, + tr: charge-to-content(it.charge), + br: count-to-content(it.count), + affect-layout: it.affect-layout + ) + + return result +} +} + + +#let group = e.element.declare( + "group", + prefix: "typsium", + + display: draw-group, + + fields: ( + // e.field("children", e.types.any, required: true), + e.field("children", e.types.array(content), required: true), + e.field("kind", int, default: 0), + e.field("count", e.types.union(int, content), default: 1), + e.field("charge", e.types.union(int, content), default: 0), + e.field("grow-brackets", bool, default: true), + e.field("affect-layout", bool, default: true), + ), + // parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => { + // let args = if include-required { + // let values = args.pos() + // arguments(values, ..args.named()) + // } else if args.pos() == () { + // args + // } else { + // assert( + // false, + // message: "element 'diagram': unexpected positional arguments\n hint: these can only be passed to the constructor", + // ) + // } + + // default-parser(args, include-required: include-required) + // }, +) + diff --git a/src/model/molecule.typ b/src/model/molecule.typ new file mode 100644 index 0000000..d5a324d --- /dev/null +++ b/src/model/molecule.typ @@ -0,0 +1,93 @@ +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": ( + count-to-content, + charge-to-content, + is-default, + customizable-attach, + phase-to-content, +) + +#let molecule( + count: 1, + charge: 0, + phase: none, + affect-layout: true, + ..children, +) = { } + +#let display-molecule(data) = { + count-to-content(data.at("count", default: none)) + + let result = math.attach( + [ + #let children = data.at("children", default: ()) + #for child in children { + if child.type == "content" { + child.body + } else if data.type == "align" { + $&$ + } else if child.type == "element" { + display-element(child) + } else if child.type == "group" { + display-group(child) + } + } + ], + tr: charge-to-content(data.at("charge", default: none)), + // br: phase-to-content(data.at("phase", default:none)), + ) + if data.at("phase", default: none) != none { + result += context { + text(phase-to-content(data.at("phase", default: none)), size: text.size * 0.75) + } + } + + return reconstruct-content(data.at("body", default: none), result) +} + +#let draw-molecule(it) = { + let result = count-to-content(it.count) + result += customizable-attach( + for child in it.children { + child + }, + tr: charge-to-content(it.charge), + affect-layout: it.affect-layout, + ) + if not is-default(it.phase) { + result += context { + text(phase-to-content(it.phase), size: text.size * 0.75) + } + } + return result +} + +#let molecule = e.element.declare( + "molecule", + prefix: "typsium", + + display: draw-molecule, + + fields: ( + // e.field("children", e.types.any, required: true), + e.field("children", e.types.array(content), required: true), + e.field("count", e.types.union(int, content), default: 1), + e.field("charge", e.types.union(int, content), default: 0), + e.field("phase", e.types.union(str, content), default: none), + e.field("affect-layout", bool, default: true), + ), + // parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => { + // let args = if include-required { + // let values = args.pos() + // arguments(values, ..args.named()) + // } else if args.pos() == () { + // args + // } else { + // assert( + // false, + // message: "element 'diagram': unexpected positional arguments\n hint: these can only be passed to the constructor", + // ) + // } + // default-parser(args, include-required: include-required) + // }, +) diff --git a/src/model/reaction.typ b/src/model/reaction.typ new file mode 100644 index 0000000..754a131 --- /dev/null +++ b/src/model/reaction.typ @@ -0,0 +1,43 @@ + +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": get-arrow, is-default + +#let reaction( + children: (), +) = { } + +#let draw-reaction(it) = { + for child in it.children { + let type-id = e.data(child).eid + if type-id == "e_typsium_---_arrow" { + h(0.4em, weak: true) + child + h(0.4em, weak: true) + } else if type-id == "e_typsium_---_molecule" { + child + let last = e.data(e.data(child).fields.children.last()) + let last-child-type-id = last.eid + let charge = last.fields.at("charge", default:none) + let count = last.fields.at("count", default:none) + if (last-child-type-id == "e_typsium_---_group" + and (not is-default(charge) or (not is-default(count) and count != 1)) + ) { + h(-0.3em) + } + } else { + child + } + } +} +} + +#let reaction = e.element.declare( + "reaction", + prefix: "typsium", + + display: draw-reaction, + + fields: ( + e.field("children", e.types.array(content), required: true), + ), +) diff --git a/src/typing.typ b/src/typing.typ new file mode 100644 index 0000000..5c0c6ff --- /dev/null +++ b/src/typing.typ @@ -0,0 +1,22 @@ +#import "libs/elembic/lib.typ" as e: selector +// #import "model/grid.typ": grid +// #import "model/label.typ": label +// #import "model/title.typ": title +// #import "model/legend.typ": legend +// #import "model/tick.typ": tick, tick-label +// #import "model/spine.typ": spine +// #import "model/diagram.typ": diagram +// #import "model/errorbar.typ": errorbar + +#let set_ = e.set_ +#let fields = e.fields +#let elembic = e +// #let set-grid = e.set_.with(grid) +// #let set-title = e.set_.with(title) +// #let set-label = e.set_.with(label) +// #let set-legend = e.set_.with(legend) +// #let set-tick = e.set_.with(tick) +// #let set-tick-label = e.set_.with(tick-label) +// #let set-spine = e.set_.with(spine) +// #let set-diagram = e.set_.with(diagram) +// #let set-errorbar = e.set_.with(errorbar) \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ index b1e5131..e871e4c 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -107,6 +107,7 @@ ) #let get-bracket(kind, open: true) = { + kind = calc.clamp(kind, 0, 3) if not open { kind += 4 } @@ -124,43 +125,22 @@ } } -#let count-to-content(factor) = { - if factor == none { - none - } else if type(factor) == int { - if factor > 1 { - str(factor) +#let count-to-content(count) = { + if count == none { + return none + } else if type(count) == int { + if count > 1 { + return [#count] } + } else if type(count) == content { + return count } + return none } #let arrow-string-to-kind(arrow) = { arrow = arrow.trim() arrow-kinds.at(arrow, default: 1) } -#let charge-to-content(charge, radical: false) = { - if charge == none { - none - } else if type(charge) == int { - if radical { - sym.bullet - } - if charge < 0 { - if calc.abs(charge) > 1 { - str(calc.abs(charge)) - } - math.minus - } else if charge > 0 { - if charge > 1 { - str(charge) - } - math.plus - } else { - none - } - } else if type(charge) == str { - charge.replace(".", sym.bullet).replace("-", math.minus).replace("+", math.plus) - } -} #let parser-config = ( arrow: (arrow_size: 120%, reversible_size: 120%), @@ -243,6 +223,36 @@ } // own utils +#let is-default(value) = { + if value == [] or value == none or value == auto or value == "" { + return true + } + return false +} + +#let customizable-attach( + base, + t: none, + tr: none, + br: none, + tl: none, + bl: none, + b: none, + affect-layout: true, +) = { + if affect-layout == false { + base = box(base) + } + math.attach( + base, + t: t, + tr: tr, + br: br, + tl: tl, + bl: bl, + b: b, + ) +} #let padright(array, targetLength) = { for value in range(array.len(), targetLength) { @@ -363,3 +373,28 @@ return template.func()(body) } } + +#let charge-to-content(charge, radical: false) = { + if is-default(charge){ + [] + } else if type(charge) == int { + if radical { + sym.bullet + } + if charge < 0 { + if calc.abs(charge) > 1 { + str(calc.abs(charge)) + } + math.minus + } else if charge > 0 { + if charge > 1 { + str(charge) + } + math.plus + } else { + [] + } + } else if type(charge) == str { + charge.replace(".", sym.bullet).replace("-", math.minus).replace("+", math.plus) + } +} From 54f90b45a9b8357d6a89beebf4d9fa9ace039966 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Tue, 27 May 2025 13:48:41 +0200 Subject: [PATCH 04/20] fixed parsing --- src/lib.typ | 6 +- src/model/element.typ | 21 +- src/model/reaction.typ | 45 ++- ...se-formula-intermediate-representation.typ | 350 ++++++------------ 4 files changed, 147 insertions(+), 275 deletions(-) diff --git a/src/lib.typ b/src/lib.typ index bf76471..08abde4 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -4,9 +4,9 @@ get-shell-configuration, display-electron-configuration, ) -#import "display-intermediate-representation.typ": display-ir -#import "parse-formula-intermediate-representation.typ": string-to-ir +#import "model/reaction.typ": reaction +#import "parse-formula-intermediate-representation.typ": string-to-reaction #let ce(formula) = { - display-ir(string-to-ir(formula)) + reaction(string-to-reaction(formula)) } diff --git a/src/model/element.typ b/src/model/element.typ index fabf9ee..5d35330 100644 --- a/src/model/element.typ +++ b/src/model/element.typ @@ -24,9 +24,10 @@ if type(it.rest) == content { base += it.rest } else if type(it.rest) == int { - base += ['] * it.rest + base += box['] * it.rest } } + let mass-number = it.a if type(it.a) == int { mass-number = [#it.a] @@ -45,24 +46,6 @@ bl: atomic-number, affect-layout: it.affect-layout, ) - // } else { - // math.attach( - // base, - // t: it.oxidation, - // tr: box(place(bottom + left, charge-to-content(it.charge, radical: it.radical))), - // br: box(place(bottom + left, count-to-content(it.count))), - // tl: box(place(bottom+ right, mass-number)), - // bl: box(place(bottom+ right, atomic-number)), - // ) - - // box( - // place( - // bottom + left, - // // float: true, - // // box(fill:red, width:1em, height:1em), - // ), - // ) - // base } } diff --git a/src/model/reaction.typ b/src/model/reaction.typ index 754a131..b0cfdbf 100644 --- a/src/model/reaction.typ +++ b/src/model/reaction.typ @@ -8,24 +8,39 @@ #let draw-reaction(it) = { for child in it.children { - let type-id = e.data(child).eid - if type-id == "e_typsium_---_arrow" { + if child == [+] { h(0.4em, weak: true) - child + math.plus h(0.4em, weak: true) - } else if type-id == "e_typsium_---_molecule" { - child - let last = e.data(e.data(child).fields.children.last()) - let last-child-type-id = last.eid - let charge = last.fields.at("charge", default:none) - let count = last.fields.at("count", default:none) - if (last-child-type-id == "e_typsium_---_group" - and (not is-default(charge) or (not is-default(count) and count != 1)) - ) { - h(-0.3em) - } } else { - child + let type-id = e.data(child).eid + if type-id == "e_typsium_---_arrow" { + h(0.4em, weak: true) + child + h(0.4em, weak: true) + } else if type-id == "e_typsium_---_molecule" { + child + let last = e.data(e.data(child).fields.children.last()) + let last-child-type-id = last.eid + let charge = last.fields.at("charge", default: none) + let count = last.fields.at("count", default: none) + if ( + last-child-type-id == "e_typsium_---_group" + and (not is-default(charge) or (not is-default(count) and count != 1)) + ) { + h(-0.4em) + } + } // else if type-id == "e_typsium_---_group"{ + // child + // let charge = last.fields.at("charge", default: none) + // let count = last.fields.at("count", default: none) + // if not is-default(charge) or (not is-default(count) and count != 1){ + // h(-0.3em) + // } + // } + else { + child + } } } } diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index 539f78f..f9c6959 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -1,4 +1,10 @@ -#import "utils.typ": arrow-string-to-kind +#import "utils.typ": arrow-string-to-kind, is-default +#import "model/molecule.typ": molecule +#import "model/reaction.typ": reaction +#import "model/element.typ": element +#import "model/group.typ": group +#import "model/arrow.typ": arrow + #let patterns = ( // element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^?[+-]?\d*\.?-?))?(?:(?P_?\d+)|(?P\^?[+-]?\d*\.?-?))?"), element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?"), @@ -51,227 +57,59 @@ #let string-to-element(formula) = { let element-match = formula.match(patterns.element) - if element-match != none { - let element = ( - type: "element", - symbol: element-match.captures.at(0), - ) - let x = get-count-and-charge( - element-match.captures.at(1), - element-match.captures.at(3), - element-match.captures.at(2), - element-match.captures.at(4), - ) - if x.at(0) != none { - element.count = x.at(0) - } - if x.at(1) != none { - element.charge = x.at(1) - } - if x.at(2) { - element.radical = x.at(2) - } - return (true, element, element-match.end) + if element-match == none { + return (false,) } - return (false,) + let x = get-count-and-charge( + element-match.captures.at(1), + element-match.captures.at(3), + element-match.captures.at(2), + element-match.captures.at(4), + ) + return ( + true, + element(element-match.captures.at(0), count: x.at(0), charge: x.at(1), radical: x.at(2)), + element-match.end, + ) } #let string-to-math(formula) = { let match = formula.match(patterns.math) - - if match != none { - let math-content = ( - type: "content", - body: eval(match.text), - ) - return (true, math-content, match.end) - } - return (false,) -} - -#let string-to-group(formula) = { - let group-match = formula.match(patterns.group) - if group-match != none { - let group-content = group-match.captures.at(0) - let kind = if group-content.at(0) == "(" { - group-content = group-content.trim(regex("[()]"), repeat: false) - 0 - } else if group-content.at(0) == "[" { - group-content = group-content.trim(regex("[\[\]]"), repeat: false) - 1 - } else if group-content.at(0) == "{" { - group-content = group-content.trim(regex("[{}]"), repeat: false) - 2 - } - let x = get-count-and-charge( - group-match.captures.at(1), - group-match.captures.at(3), - group-match.captures.at(2), - group-match.captures.at(4), - ) - - let group = ( - type: "group", - kind: kind, - children: (), - ) - if x.at(0) != none { - group.count = x.at(0) - } - if x.at(1) != none { - group.charge = x.at(1) - } - if x.at(2) { - group.radical = x.at(2) - } - - let random-content = "" - - let remaining = group-content - while remaining.len() > 0 { - if remaining.at(0) == "&" { - group.children.push((type: "align")) - remaining = remaining.slice(1) - continue - } - let math-result = string-to-math(remaining) - if math-result.at(0) { - if random-content != none and random-content != "" { - group.children.push((type: "content", body: [#random-content])) - } - random-content = "" - group.children.push(math-result.at(1)) - remaining = remaining.slice(math-result.at(2)) - continue - } - - let element = string-to-element(remaining) - if element.at(0) { - if random-content != none and random-content != "" { - group.children.push((type: "content", body: [#random-content])) - } - random-content = "" - group.children.push(element.at(1)) - remaining = remaining.slice(element.at(2)) - continue - } - - let result = string-to-group(remaining) - if result.at(0) { - if random-content != none and random-content != "" { - group.children.push((type: "content", body: [#random-content])) - } - random-content = "" - group.children.push(result.at(1)) - remaining = remaining.slice(result.at(2)) - continue - } - - random-content += remaining.codepoints().at(0) - remaining = remaining.slice(remaining.codepoints().at(0).len()) - } - - if random-content != none and random-content != "" { - group.children.push((type: "content", body: [#random-content])) - } - return (true, group, group-match.end) + if match == none { + return (false,) } - return (false,) + return (true, eval(match.text), match.end) } - -//this will assume that the string is a molecule for performance reasons -#let molecule-string-to-ir(formula) = { - let remaining = formula.trim() - if remaining.len() == 0 { - return none - } - - let molecule = ( - type: "molecule", - children: (), - ) - - let random-content = "" - - while remaining.len() > 0 { - if remaining.at(0) == "&" { - molecule.children.push((type: "align")) - remaining = remaining.slice(1) - continue - } - - let math-result = string-to-math(remaining) - if math-result.at(0) { - if random-content != none and random-content != "" { - molecule.children.push((type: "content", body: [#random-content])) - } - random-content = "" - molecule.children.push(math-result.at(1)) - remaining = remaining.slice(math-result.at(2)) - continue - } - - let element = string-to-element(remaining) - - if element.at(0) { - if random-content != none and random-content != "" { - molecule.children.push((type: "content", body: [#random-content])) - } - random-content = "" - molecule.children.push(element.at(1)) - remaining = remaining.slice(element.at(2)) - continue - } - - let group = string-to-group(remaining) - if group.at(0) { - if random-content != none and random-content != "" { - molecule.children.push((type: "content", body: [#random-content])) - } - random-content = "" - molecule.children.push(group.at(1)) - remaining = remaining.slice(group.at(2)) - continue - } - - random-content += remaining.codepoints().at(0) - remaining = remaining.slice(remaining.codepoints().at(0).len()) - } - - if random-content != none and random-content != "" { - molecule.children.push((type: "content", body: [#random-content])) - } - return molecule -} - -#let string-to-ir(reaction) = { - let remaining = reaction.trim() +#let string-to-reaction( + reaction-string, + create-molecules: true, +) = { + let remaining = reaction-string.trim() if remaining.len() == 0 { return none } let full-reaction = () - - let current-molecule = ( - type: "molecule", - children: (), - ) - + let current-molecule-children = () + let current-molecule-count = 1 + let current-molecule-phase = none + let current-molecule-charge = 0 let random-content = "" + while remaining.len() > 0 { if remaining.at(0) == "&" { - if current-molecule.children.len() > 0 { - full-reaction.push(current-molecule) - current-molecule = (type: "molecule", children: ()) + if current-molecule-children.len() > 0 { + full-reaction.push(molecule(current-molecule-children)) + current-molecule-children = () } - full-reaction.push((type: "align")) + full-reaction.push($&$) remaining = remaining.slice(1) continue } let math-result = string-to-math(remaining) if math-result.at(0) { - if random-content != none and random-content != "" { - full-reaction.push((type: "content", body: [#random-content])) + if not is-default(random-content) { + full-reaction.push([#random-content]) } random-content = "" full-reaction.push(math-result.at(1)) @@ -281,70 +119,104 @@ let element = string-to-element(remaining) if element.at(0) { - if random-content != none and random-content != "" { - if current-molecule.children.len() == 0 { - full-reaction.push((type: "content", body: [#random-content])) + if not is-default(random-content) { + if current-molecule-children.len() == 0 { + full-reaction.push([#random-content]) } else { - current-molecule.children.push((type: "content", body: [#random-content])) + current-molecule-children.push([#random-content]) } } random-content = "" - current-molecule.children.push(element.at(1)) + current-molecule-children.push(element.at(1)) remaining = remaining.slice(element.at(2)) continue } - let group = string-to-group(remaining) - if group.at(0) { - if random-content != none and random-content != "" { - if current-molecule.children.len() == 0 { - full-reaction.push((type: "content", body: [#random-content])) + + let group-match = remaining.match(patterns.group) + if group-match != none { + if not is-default(random-content) { + if current-molecule-children.len() == 0 { + full-reaction.push([#random-content]) } else { - current-molecule.children.push((type: "content", body: [#random-content])) + current-molecule-children.push([#random-content]) } } random-content = "" - current-molecule.children.push(group.at(1)) - remaining = remaining.slice(group.at(2)) + + let group-content = group-match.captures.at(0) + let kind = if group-content.at(0) == "(" { + group-content = group-content.trim(regex("[()]"), repeat: false) + 0 + } else if group-content.at(0) == "[" { + group-content = group-content.trim(regex("[\[\]]"), repeat: false) + 1 + } else if group-content.at(0) == "{" { + group-content = group-content.trim(regex("[{}]"), repeat: false) + 2 + } + let x = get-count-and-charge( + group-match.captures.at(1), + group-match.captures.at(3), + group-match.captures.at(2), + group-match.captures.at(4), + ) + let group-children = string-to-reaction(group-content, create-molecules: false) + + current-molecule-children.push(group(group-children, kind: kind, count: x.at(0), charge: x.at(1))) + remaining = remaining.slice(group-match.end) continue } let plus-match = remaining.match(patterns.reaction-plus) if plus-match != none { - if current-molecule.children.len() > 0 { - full-reaction.push(current-molecule) - current-molecule = (type: "molecule", children: ()) - } - if random-content != none and random-content != "" { - full-reaction.push((type: "content", body: [#random-content])) + if current-molecule-children.len() > 0 { + full-reaction.push( + molecule( + current-molecule-children, + count: current-molecule-count, + phase: current-molecule-phase, + charge: current-molecule-charge, + ), + ) + current-molecule-children = () + } + if not is-default(random-content) { + full-reaction.push([#random-content]) } random-content = "" - full-reaction.push((type: "+")) + full-reaction.push([+]) remaining = remaining.slice(plus-match.end) continue } let arrow-match = remaining.match(patterns.reaction-arrow) if arrow-match != none { - if current-molecule.children.len() > 0 { - full-reaction.push(current-molecule) - current-molecule = (type: "molecule", children: ()) - } - if random-content != none and random-content != "" { - full-reaction.push((type: "content", body: [#random-content])) + if current-molecule-children.len() > 0 { + full-reaction.push( + molecule( + current-molecule-children, + count: current-molecule-count, + phase: current-molecule-phase, + charge: current-molecule-charge, + ), + ) + current-molecule-children = () + } + if not is-default(random-content) { + full-reaction.push([#random-content]) } random-content = "" - let arrow = ( - type: "arrow", - kind: arrow-string-to-kind(arrow-match.captures.at(0)), - ) + let kind = arrow-string-to-kind(arrow-match.captures.at(0)) + let top = () + let bottom = () if arrow-match.captures.at(1) != none { - arrow.top = string-to-ir(arrow-match.captures.at(1)) + top = string-to-ir(arrow-match.captures.at(1)) } if arrow-match.captures.at(2) != none { - arrow.bottom = string-to-ir(arrow-match.captures.at(2)) + bottom = string-to-ir(arrow-match.captures.at(2)) } - full-reaction.push(arrow) + full-reaction.push(arrow(kind:kind, top:top, bottom:bottom)) remaining = remaining.slice(arrow-match.end) continue } @@ -352,11 +224,13 @@ random-content += remaining.codepoints().at(0) remaining = remaining.slice(remaining.codepoints().at(0).len()) } - if current-molecule.children.len() != 0 { - full-reaction.push(current-molecule) + if current-molecule-children.len() != 0 { + full-reaction.push( + molecule(current-molecule-children, count: current-molecule-count, phase: current-molecule-phase), + ) } - if random-content != none and random-content != "" { - full-reaction.push((type: "content", body: [#random-content])) + if not is-default(random-content) { + full-reaction.push([#random-content]) } return full-reaction From 5757eccd08e9bce2aebb18fdafdcfa93013e988a Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Tue, 27 May 2025 19:29:14 +0200 Subject: [PATCH 05/20] improved oxidation numbers --- src/model/element.typ | 9 ++- src/model/molecule.typ | 2 + ...se-formula-intermediate-representation.typ | 77 ++++++++++++++----- src/utils.typ | 74 ++++++++++++++---- 4 files changed, 128 insertions(+), 34 deletions(-) diff --git a/src/model/element.typ b/src/model/element.typ index 5d35330..44b8f19 100644 --- a/src/model/element.typ +++ b/src/model/element.typ @@ -2,6 +2,7 @@ #import "../utils.typ": ( count-to-content, charge-to-content, + oxidation-to-content, none-coalesce, customizable-attach, ) @@ -16,6 +17,8 @@ rest: none, radical: false, affect-layout: true, + roman-oxidation: true, + roman-charge: false, ) = { } #let draw-element(it) = { @@ -39,8 +42,8 @@ customizable-attach( base, - t: it.oxidation, - tr: charge-to-content(it.charge, radical: it.radical), + t: oxidation-to-content(it.oxidation, roman:it.roman-oxidation), + tr: charge-to-content(it.charge, radical: it.radical, roman:it.roman-charge), br: count-to-content(it.count), tl: mass-number, bl: atomic-number, @@ -65,5 +68,7 @@ e.field("rest", e.types.union(int, content), default: none), e.field("radical", bool, default: false), e.field("affect-layout", bool, default: true), + e.field("roman-oxidation", bool, default: true), + e.field("roman-charge", bool, default: false), ), ) \ No newline at end of file diff --git a/src/model/molecule.typ b/src/model/molecule.typ index d5a324d..f3c6cea 100644 --- a/src/model/molecule.typ +++ b/src/model/molecule.typ @@ -11,6 +11,8 @@ count: 1, charge: 0, phase: none, + //TODO: add up and down arrows + phase-transition:0, affect-layout: true, ..children, ) = { } diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index f9c6959..56f9e35 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -1,4 +1,4 @@ -#import "utils.typ": arrow-string-to-kind, is-default +#import "utils.typ": arrow-string-to-kind, is-default, roman-to-number #import "model/molecule.typ": molecule #import "model/reaction.typ": reaction #import "model/element.typ": element @@ -6,41 +6,45 @@ #import "model/arrow.typ": arrow #let patterns = ( - // element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^?[+-]?\d*\.?-?))?(?:(?P_?\d+)|(?P\^?[+-]?\d*\.?-?))?"), - element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?"), - // group: regex("^(\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])"), - group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^[+-]?[IV]+|\^?[+-]?\d?)\.?-?))?"), + element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?P\^\^[+-]?[IViv]{1,3}|\^\^[+-]?\d+)?"), + group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P(?:\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^?[+-]?\d?)\.?-?))?"), reaction-plus: regex("^(\s?\+\s?)"), reaction-arrow: regex("^\s?(<->|↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-\/>|<\/-)(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?\s?"), math: regex("^(\$[^$]*\$)"), // Match physical states (s/l/g/aq) - state: regex("^\((s|l|g|aq|solid|liquid|gas|aqueous)\)"), + state: regex("^\((s|l|g|aq|solid|liquid|gas|aqueous|aqua)\)"), ) #let get-count-and-charge(count1, count2, charge1, charge2) = { let radical = false - let count = if count1 != none and count1 != "" { + let roman-charge = false + let count = if not is-default(count1) { int(count1.replace("_", "")) - } else if count2 != none and count2 != "" { + } else if not is-default(count2) { int(count2.replace("_", "")) } else { none } - let charge = if charge1 != none and charge1 != "" { + let charge = if not is-default(charge1) { charge1.replace("^", "") - } else if charge2 != none and charge2 != "" { + } else if not is-default(charge2) { charge2.replace("^", "") } else { none } - if charge != none and charge != "" { + if not is-default(charge) { if charge.contains(".") { charge = charge.replace(".", "") radical = true } - if charge == "-" { + if charge.contains("I") or charge.contains("V"){ + let multiplier = if charge.contains("-") { -1 } else { 1 } + charge = charge.replace("-", "").replace("+", "") + charge = roman-to-number(charge) * multiplier + roman-charge = true + } else if charge == "-" { charge = -1 } else if charge.contains("-") { charge = -int(charge.replace("-", "")) @@ -48,15 +52,16 @@ charge = 1 } else if charge.replace("+", "").contains(regex("^[0-9]+$")) { charge = int(charge.replace("+", "")) + } else { + charge = 0 } } - return (count, charge, radical) + return (count, charge, radical, roman-charge) } #let string-to-element(formula) = { let element-match = formula.match(patterns.element) - if element-match == none { return (false,) } @@ -66,9 +71,43 @@ element-match.captures.at(2), element-match.captures.at(4), ) + let oxidation = element-match.captures.at(5) + let oxidation-number = none + let roman-oxidation = true + let roman-charge = false + if oxidation != none { + oxidation = upper(oxidation) + oxidation = oxidation.replace("^", "", count: 2) + let multiplier = if oxidation.contains("-") { -1 } else { 1 } + oxidation = oxidation.replace("-", "").replace("+", "") + if oxidation.contains("I") or oxidation.contains("V") { + oxidation-number = roman-to-number(oxidation) + } else { + roman-oxidation = false + oxidation-number = int(oxidation) + } + if oxidation-number != none { + oxidation-number *= multiplier + } + } + + if x.at(0) == none and x.at(1) == none and x.at(2) == false { + if formula.at(element-match.end + 1, default: "").match(regex("[a-z]")) != none { + return (false,) + } + } + return ( true, - element(element-match.captures.at(0), count: x.at(0), charge: x.at(1), radical: x.at(2)), + element( + element-match.captures.at(0), + count: x.at(0), + charge: x.at(1), + radical: x.at(2), + oxidation: oxidation-number, + roman-oxidation: roman-oxidation, + roman-charge: x.at(3), + ), element-match.end, ) } @@ -87,7 +126,7 @@ ) = { let remaining = reaction-string.trim() if remaining.len() == 0 { - return none + return () } let full-reaction = () let current-molecule-children = () @@ -211,12 +250,12 @@ let top = () let bottom = () if arrow-match.captures.at(1) != none { - top = string-to-ir(arrow-match.captures.at(1)) + top = string-to-reaction(arrow-match.captures.at(1)) } if arrow-match.captures.at(2) != none { - bottom = string-to-ir(arrow-match.captures.at(2)) + bottom = string-to-reaction(arrow-match.captures.at(2)) } - full-reaction.push(arrow(kind:kind, top:top, bottom:bottom)) + full-reaction.push(arrow(kind: kind, top: top, bottom: bottom)) remaining = remaining.slice(arrow-match.end) continue } diff --git a/src/utils.typ b/src/utils.typ index e871e4c..7614439 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -89,6 +89,22 @@ sym.arrow.l.not, sym.harpoons.rtlb, ) +#let roman-numerals = ( + "0", + "I", + "II", + "III", + "IV", + "V", + "VI", + "VII", + "VIII", + "IX", + "X", + "XI", + "XII", + "XIII", +) #let arrow-kinds = ( "<->": 0, "↔": 0, @@ -137,6 +153,34 @@ } return none } +#let roman-to-number(roman-number) = { + return roman-numerals.position(x => x == roman-number) + // return if oxidation == "I" { 1 } else if oxidation == "II" { 2 } else if oxidation == "III" { + // 3 + // } else if oxidation == "IV" { 4 } else if oxidation == "V" { 5 } else if oxidation == "VI" { 6 } else if ( + // oxidation == "VII" + // ) { 7 } else if oxidation == "VIII" { 8 } else { none } +} +#let oxidation-to-content(oxidation, roman: true) = { + if oxidation == none { + return none + } else if type(oxidation) == int { + let symbol = none + if oxidation < 0 { + symbol = math.minus + } else if oxidation > 0 { + symbol = math.plus + } + if roman { + return [#symbol#roman-numerals.at(calc.abs(oxidation))] + } else { + return [#symbol#calc.abs(oxidation)] + } + } else if type(oxidation) == content { + return oxidation + } + return none +} #let arrow-string-to-kind(arrow) = { arrow = arrow.trim() arrow-kinds.at(arrow, default: 1) @@ -374,25 +418,29 @@ } } -#let charge-to-content(charge, radical: false) = { - if is-default(charge){ +#let charge-to-content(charge, radical: false, roman: false) = { + if is-default(charge) { [] } else if type(charge) == int { if radical { sym.bullet } - if charge < 0 { - if calc.abs(charge) > 1 { - str(calc.abs(charge)) - } - math.minus - } else if charge > 0 { - if charge > 1 { - str(charge) - } - math.plus + if roman { + roman-numerals.at(calc.abs(charge)) } else { - [] + if charge < 0 { + if calc.abs(charge) > 1 { + str(calc.abs(charge)) + } + math.minus + } else if charge > 0 { + if charge > 1 { + str(charge) + } + math.plus + } else { + [] + } } } else if type(charge) == str { charge.replace(".", sym.bullet).replace("-", math.minus).replace("+", math.plus) From f76c7e5334a8a9a65f5f8960d951f1cb423c857b Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Tue, 27 May 2025 19:31:09 +0200 Subject: [PATCH 06/20] updated tests --- tests/README-graphic1/ref/1.png | Bin 3167 -> 3273 bytes tests/README-graphic1/test.typ | 2 +- tests/arrow-align/ref/1.png | Bin 1536 -> 2519 bytes tests/arrow-align/test.typ | 1 - tests/brackets/.DS_Store | Bin 0 -> 6148 bytes tests/brackets/ref/1.png | Bin 5229 -> 5612 bytes tests/brackets/test.typ | 8 +- tests/charges/ref/1.png | Bin 3254 -> 8760 bytes tests/charges/test.typ | 47 ++++- tests/content-to-ir/ref/1.png | Bin 1083 -> 0 bytes tests/content-to-ir/test.typ | 14 -- tests/{content-to-ir => elembic}/.gitignore | 0 tests/elembic/test.typ | 49 +++++ .../ref/1.png | Bin 2031 -> 2115 bytes .../test.typ | 107 ++++------ .../ref/1.png | Bin 6020 -> 0 bytes .../test.typ | 196 ------------------ .../.gitignore | 0 tests/oxidation-numbers/ref/1.png | Bin 0 -> 3918 bytes tests/oxidation-numbers/test.typ | 24 +++ tests/parse-ir-elements/test.typ | 79 ------- tests/parse-ir-groups/.gitignore | 5 - tests/parse-ir-groups/test.typ | 43 ---- .../.gitignore | 1 - tests/reactions/ref/1.png | Bin 0 -> 4396 bytes tests/reactions/test.typ | 9 + 26 files changed, 167 insertions(+), 418 deletions(-) create mode 100644 tests/brackets/.DS_Store delete mode 100644 tests/content-to-ir/ref/1.png delete mode 100644 tests/content-to-ir/test.typ rename tests/{content-to-ir => elembic}/.gitignore (100%) create mode 100644 tests/elembic/test.typ delete mode 100644 tests/intermediate-representation-reactions/ref/1.png delete mode 100644 tests/intermediate-representation-reactions/test.typ rename tests/{intermediate-representation-reactions => oxidation-numbers}/.gitignore (100%) create mode 100644 tests/oxidation-numbers/ref/1.png create mode 100644 tests/oxidation-numbers/test.typ delete mode 100644 tests/parse-ir-elements/test.typ delete mode 100644 tests/parse-ir-groups/.gitignore delete mode 100644 tests/parse-ir-groups/test.typ rename tests/{parse-ir-elements => reactions}/.gitignore (88%) create mode 100644 tests/reactions/ref/1.png create mode 100644 tests/reactions/test.typ diff --git a/tests/README-graphic1/ref/1.png b/tests/README-graphic1/ref/1.png index 88ec8da57cf1cc8b8114785f9c19482e1d3375e4..f10a2879257da1b7ba8cce458e899eeff7a07d95 100644 GIT binary patch literal 3273 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 Hu0mjf<)I!U literal 3167 zcmV-l450IgP)?+fJQ4_!Yi{9Mrd|~>NdyGkNHG^&5W6~3c|{Yf6fNWBR%)7SuI0^eUl0`+ z%yJjIiY96mPXGX%~Dlz%Q%%R+MUs-!SjESBk=l(}1rFScT!g|6IPuLK1b84y7e zpetf^H36ZC4nPXo4_(RQUkM7oYykF=xM~VU9iIink$o?Y!kIH?E};^-%Waenp$QJC z;uIcp^D1EeGP!-?Ahfn_&I_YZNGz@DD`Ea7SXtHh8Tz7{#8q_)=c$!d5179-R#q01 zq3>SP6L^6X6sWA61qvB%*8s>oJw3a$<1T}gRgr=%Dysz;y$s~o+uL{D1Fp!fE^bb) zSlqpbyPIoQ+;enwbMN7_#XJ#E-R8GUsB?p9qk9E_GS-T$%h#BN!WQdpcLBt?xRI+cu5S{6!W;)n>stV{X|t>% zGN8$uN9v{)00nn!$g;eWWBHwvKtXc3Edu})WjNB&IVx!K*HEjt0+cAa@vsa4Y3(~C zOQRsK)iMbz-_jIf9l#1CGuawLYNKSCjqG_FS?%l|L)Xr6&q86R9HM!U^=x&UPTjTO zO79I_`-n4GcH|OZ4hmt^Bac6+f0m}OcMMo=b4WKexU%_ijt~d1+K{s-*$t;>p>Xyn zL<=MVY?aX^Y`s96qhx7yy0gjfphX*{6bdSPD%_`Zn^F|U{9GP|0Qx)l{zHS1cMg;+m&3ls`E(xu?a8CW?K?$)DMz<0bfh4xqU6jWWwJa&$K%A)|U@Moa#%`|Z# z!^)*FhDL#JDg9Oog}t;%0rd4XrjTcQuN(?DEb6Ji_vCd;-oS)!O-3PQ_(6dJqAjiE zCa17-Mp+bsi6``F>l;(J)C_!XxKY#ciZlvqhntK-L4P?dhr)R(%!tg_YNbk`;7z?D zv~mlsY+Jb<_-os$ZS0^rpd1RzX>SO1ra!VZw7tnF_}n>EOaXK0dumftNN=VogMuBI z2O*gpCpIlV37p=v`~Usm932Ir0s83@NScW#Y+VHd3MxnXjR6IxTvpTPqJUwm z#3^JGvJFC7V1YB41uS;uePsJyZa zC^TdHwKt~FkxqxuU^+|!g*WLOBf5-2T}Gkp^70-KL+D6@4Sm6~-DMgIvRf!Y0ZVD8 z0#pCDG1_jo7=<)K4#6-5qY%dqB8NsAP{3OHPQHW(1YCnnLt*}Ypxg&UPdXk#9@Jj~ z1y?!_LK!IlY8y~!zP3CH!L&DoTu3R*Y%rBn)i zI|?T7auZd8Io*TSR3$CA^+Q_z~krF7}x1~3rZ^T_4 z0!E=0qu~6FIE5_}um9oVm8=y*jaZ6Fwe>ZTnjzHHUwF^y0VLo9u zB-2^}*4=tMg-xU*FR(5-2_X9fkVGR;(bO$c%c7thXt9ESp{EjD^#cKnBvaY6Gr-Dy z{VG`Z8}bx(lh$B0CWis!9s}-Ey@1Rb3e!-I~AuB|%jn@2P!)N2$ne0vho z*5@z)ZDRibH7eS{ekh9qG&61cZM(2_?1*&!eZH~z*U>vjU7rLX)QKJP2Tf`1_hX@J zPd@-VeOgia{;0yoqxul?wof>?k_L}20xP@=Ohw_m&)dY-dj$dpThj)^mf?ucJCyeF zeaP-i4Amd^ng+v;B<<_b;sB&lK;f>h$IcE{gD-RcrdaF@JNXbmx0B$CjZ3(b5Sxh1 znE1rRc)1d~WL=(sPr3C=*^C{95OO;LzCSJheohMMx4E4a4>Z?rC*HoH1S<=`J?{0y zxSJ@F>n2h>(&?Dkc^xWT@*Ob>LRssB{1%>uzU0uI=>@-pN9F@h>STiJx`G=kUl@xY zH!=RER(&%*Q8zb$9g{k*)PNQEJ^0k_|9wsh*)j3l9Z80Q-I*jjQp9r`%XxD{Tvg!9 z+c-@Y9HuhKx0rVMqKuY${0;GL*lYBIIFSs=6h1kC=k0#O;7a=z)3ha!TE?4G*qYnw z5%^}4Q<5qCFb@@Mme*sme4))fz6=Q;6+sTs{EmU|v&{w+%-p$K0KDrIRu+Y92T5C9 zxDMb$n|yFBTL!+!nFbV0-mCyM0c0Q$#+2ZM2Hpb3%;<;=PQpwi#`B8sHLhb z3ixFqB-G9YzzEAH0Mh#uft58j$BYzauLe@4n}Y)O1fz6|YCAi3fBOfBlsG&etdvht zi4^|53W%#yc@GF+Rw&AE@**I)!4v?wLsP&v@ot{^D12St+1aVpMzc`B-dq@`9`~`X zs}O15W3aS-*_BB_y+|hO6M{;lfQ=|W%~F}H_j)aW$OQ0h+6(hh$p4t7s5k{Pws2=k zN;vuJfdmS#)bByWO@$dL2vsQxD}#c929ZhYswtFg-XI^JfyBe_)uF%&B@a&EW z_o*;C@GA|>*ryJ(|7svCFtEu>MZq#Ka3##x>6s+p2yiYiuw&Jrpq9yG6I3u`N)1$n zz$R{^Q+Rob<>s9SGj=NC_lP6F32vV)FNQ+(UpJ|a?|&@kbu`|bUh)6{002ovPDHLk FV1mg})p!5^ diff --git a/tests/README-graphic1/test.typ b/tests/README-graphic1/test.typ index a4995e3..75536b7 100644 --- a/tests/README-graphic1/test.typ +++ b/tests/README-graphic1/test.typ @@ -1,5 +1,5 @@ #import "../../src/lib.typ": ce -#set page(width: auto, height: auto, margin: 0.5em) +#set page(width: auto, height: auto, margin: 0em) $ #ce("[Cu(H2O)4]^2+ + 4NH3 -> [Cu(NH3)4]^2+ + 4H2O") diff --git a/tests/arrow-align/ref/1.png b/tests/arrow-align/ref/1.png index b63e8a02602187a0a4bebf5aff4950e40433c189..69d1868afbc6516ca1dfbf1e354797c119f72c8e 100644 GIT binary patch literal 2519 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 HrxN%9!~hYY literal 0 HcmV?d00001 diff --git a/tests/brackets/ref/1.png b/tests/brackets/ref/1.png index b6da07b5b18b60dec87282e7fbdb51d7417082ff..e6cc486e3e8fff97373e0b06f7b8cdc29dce3329 100644 GIT binary patch literal 5612 zcmVT)M%0LiqQsztM659wqhO0zVu`UwCHAOjR1^&cWA8m8u{`$j*u@q* ziUI}!#exM3NuqKI7e@$=w*!7gpleIl)Qa} z@dsG%e4(c?#@)Obg;F58b@OI@c?|Cid5qz%uCAYd!d!ag=t+zvC_Q#uj2E58jT=se z?o?bE1{Nb%1}#bRiip8`Kw@CHG1APt1V>L8-tCJ-SH~4cBxa&Kg77Q zbN9}h1{-7i7U*F8WQ{RYMF$#OjDYqs=s?=$g0?XLgNxyG0XjMBY`ig!HH8A{%WF|m zK=uY2$r_z9;26ic6!3;Bm!-&;wb;7J@ILi_moA(NM%$JxTm1>f!!|D8eAPM|a=x7n zCWh##L?uS%%T>hUL2T@c8>=myp(5TS41$=?E%pP*_3Tt(3K*Fw?JHEc2SEC25#1XH zUa(n`VZ?Ygbnu9pzo>4La8eJPpjuF|rIrl5)JB!AgX2S|fYa57IOcJQTOgmb#&&~< z@wDaxU=v-VDF$59pv?6<2!5awASbdDVq|t8v%vZa1>AcWM2rPA3P_}_)W#4;15@uq znfYSyqD@Sq!J8(;$az}DG*C6h!YT$4R*xTD>n4xkTtNh26@4l{qY4MFJMF7B26hsgG^H3JGi~!?bQY6bL3q4IzfeX*w7!i->^% zCNW@~iv;5exeUQgvzB6T(%YtifHe6U46wEyGe{8#Ar`B{gq-|_@OzW?FOv|FWh19 zkw4u-!OErUP~f6nqvba*FKl-ToB_6^YWMqKooR~#D^10Q5F_OF9mcI^N-+{6q8>kf z6d4Uc-tBDxX^^v<0tK>@UPGqtNQKaLhed z>lWQkfp?PjyprhA+t^~@(o1M5`U(X5$T}d)Y?tB0C_y*LRRDFyl*eGaH9HBw($i34 zSUWp+m7t`Io(x2r1p&Cy+S%D$&tq(zKY#u@l$b=f$^LD3fbsHIW~N{$J3ee+&(Knj zQFb2QNQWk&zXE>MUxOvxZd5UJx1eDzvdy-DmD}roJjUIrK&t5`y@?TW<;s=tQcl23 z841A=ix@tJe!M$wDM2ZUuSf_P0A6~d1JIus3sO^aQL3_2Q-92lF=Quz)vm6tool-? zf{pe|muBOH81qg7m$X_L#vfzzk<5%cTgzjx&YM!kwelFO=hs;|1{fnX06Ggu)f_|B zbpwi#(c>X>_Mk_G)-hl>F~nW3;{#mpDmK&@F()=2j{}k)qL7W=_#oaJ4Kv2WK9vWr zn&B`px93u*O0!7>tEo*zi}_%L)Um8xqrL>lb!b%Es*b}-EOfB4s%_^8Wr;RvdLAR< z_>00ZPFHXG42NaNx3^Onp>i z{B7Pi5ggybF(g(Y;C<~+<$r8R8}QnY<6!M3T~J7H9jPA27+B{$(AVUdatui`vK4Y7 z>q0Ru)|BPPkoBRX!1_w77_yod^*Bbuc>w0pAmtb*=tsHW{a7f**nTL8kx-kS&^`wG zjn(5A$pA!-jkAlXh7#eky{rqj+upm zkxWl2#duer?o*1L9_=12|@6o`@pJWsWt{YXiwvbkyFK7q!i;QwUvRlZ7;^r zBVc_@v((1OXhgSZj1jVW>1~+-#mKJI7)mk3cJ#mC4SR^SbQ>5FQ*KxSJ*^T0S52yQ z&=6zxlGG^2-VmIo33?vm1evHD13?v>v%$IF9e8Rn7K|`D8!QWd2s((snMS|V5W{*T z@H>eE&))+28{_M75{SS2I7KA}4p(j+4@PMF*8o;l3k75yn&*IJpFb+L%29A~n&ro^ zl`kW#q>f>^Pqn&4`-p`>L10mnk7`J_8ANN?W4%m;I(|AN0IK#h( zT$74sCJlnYy3-4++|TVQRH*K_4vQVDR;bX_nG1Ei$u$VRAFjtSkP{J_t^PbCa`&R0 zVGukC$^!3{736XOh-6SC21^17SRw!kGrRVF2!YipBRlC5S%yO=z8}4W-2Iv&@X}+A zA_k_+gtjt|i-3p32XZri22a*yp>f1Wv`&SV_L2)gIPnH2)jA2hmDPQWBL% zPXcCq6b;VeCE$g6I~z-koQ`48(%)l8^cwOQoUo3$kjok(GMX4ja*ct`VqBBJoAw08 z69aLFp|ivB;2r%F1`(q)yczuV?OUgrZzbh1fZ)lD%55gUDUZRkT2Q`Cc*jQ2#uJju zV{mdZMLB6Q{f=Q~ZeCx4QuMZ#xw&a6#K^TDG;iQ~onl-#c<|sQl&aIrlz}L584c}Q z`$4N1#xKJQKzR%y#=8iyHZkrn;$eg&!OSOnE05u{_Qco0{~RNaFkE4Tju6Iruw)^? zQ9F?qG5#ALp9^D@5Fc+Zk0AjNLGHqP3~zk4h8Wjiyd2~)z8wlglB-Zqaxq>)TQOx4 zW8yX7WVO^%iE;3IXlv_FvNDJfHs@_zn_Vc47{_~yptaTG%OD1xEn9F8r4i%Jr|+SI zK1Wcrs3_>{SP?M} zV*!Y!enJYzSlkl%R+_q|0Z1M22?+!%p(<&d0Y>(Kw#g98`wAL$`VZu!RcReAbKY2o zIB=o}2?fiI{D?vvE*4)545Mqn@mK^U*GxZq56)OMF`PXi*g?9B!0E)tNa@pq`~=qF z-;wW?QTzsl zsO?fPI+O1U!khq*yK(fe-(Ep(CCvk`0r7@lZnzKw1IQ577_MEQUJ|*c5Ch{#ckN@q zZG17uNVI(dR+PP`D1AUjF#u~a8;YKNzAQ(2!ukw2(`ta1P3d+h5Mm_NrAJg^46%oL zq2!Tbj7&SSRXN732dn{VV+`+EoH6P;G_FXw7_wGvWI#OK2*8Z2hXQG9ZlSEN77Nw@ zd+?r98lWx)j#H}yvtuDaN<`Shi|o%xF=ZF;+=3uGivX9G9>7 z2E72(BR){Do9+c8mW~F?y*YRplezL z<6)QqsiGlgl zQd5j?|C@5wVgm%TKPj0Qu~pr<^fSRYO23tYb7K&2)5Hgii*z|ylRiSeWpoPUc6fo4 z=nhU+6QUSncq6D6M54iywg*Stl4^+YocaS@n?Nwit@vVer;CtpV}*+VR{a6i8q;2x zVBG#P1;FI`F@VUitpu#4CK3o_Khp^kFivz4f%Bhw@4&ffsu-hNN2nJ^0>L|5B_5mz zb0J17Ay2^SN4V-o(*P$A2zrhwt{8otOsQ$}_V>Zt_wo3(LzY4=z_I?E18e(z7YBxH zmHq5_jXW~J*+b${V2AA|zK52$jZX*bN>dY3wF5YF*P*}{L!m)~E#Un9i7Bn@CuKOckiVcndT#B!-H#wAO3M{zr9w`IeB!Fhp@HW>e)hB+akenO6fAM3NFotsijl!S zYHlXmXPm>FH1HmqC6@U~m;HR`>K1(p4er#2PNZErNFle`4RYDpWfo)3SJ2gBCur2- zEVLDW6TlXF6>=T+gY{^@h>p%z%P2;0#SG}E%R*>$rW3R^-ZTr?Oaj2U(F!byOETbH z`9aylKubU9CdMKL0?A&ea1DZ%_Vom$oxaEh$Nef;QFIp&OBR<=jAN~!8*dNfn{NXZ zkzZ#(OM8cn>^v!nFEbJx>C#x>6{~(Hy6AGm6W?TJhTi`J&A#61x`D-5Wo5Mp|Kw00H26gVRZXp|QouMcyZ0>vi{U%% zc@|iio4Pl1{5}AwL!s*A&(P%hC@ECLKetK(=Wx|0@Sh@hu1kyy+x&O@xqTawHgDgt z!++}oa74S^n>+N|4{<-pe)k+1ttQ6ge--05a4ywux<6BNsn^(Hji4f?s}vgfEk_~s zl7R3tz-!_INsa)z#Q1BiGpV}7AIY13CS=ftM_~PJTX*|AY2b+7GfW`c+<^qEiGgB> zalou~B3KJ(p%{ZNKr_i_32I~XtpUW4U!j#pD|&X{AXVNbTZkK2vWnypI9IDQc?Omr zH38sXQZb%Xt&{+ctgUK{+s-J;`f+MwY+eAYt@#{Ukp0r|iOi39DaY_5J;AXc(O@Oj zr{R$Mj*YRaq+;};+C`wATEOjxkebSggQP4&x*$tqs{XfK*BK!ilQIav@ z%xEBZZ{#sDg5tr+t8pK4lCWoB$x5T)@DTmz8QXV`{GtwO*!TN83C>{rbA;wsR^lx*KNaB}_W!lMk-F-FJ3C~Rt_ zz@lRW5poBDYb6+CC9MQSDGe3_xLD~bfVEEG%&?3F{3^u&mx%isUB^YBFtqw(_s^x*vxfyet))lM>vl#%gOlAVnqy>O*;tS6H z>i;^%(|+CSSoo{P_$rKJE|0OFkknF%v7gd36cANx1J(?34Hz~6tVP5>kI}dr8)Gbh zXJjroXNXAq7>9g)eQgIbmoO-cu{Mh;a+?Ga5>YBKo}17I5DZ2*>Ba}x2n-tt)^OrJ ziM5H1F_()m2b{C~)62Ap;p5@qQP0)GNxobx|*AF^ErR{uZ1$z0sG-|Y9m zl3(V{nI5}>y}kgDRXG`~<5R3dkRL-Qfnz!i{4EKJG4{-!H{aW9E?&=^>+L=FmmqKw z7dE$cYU!B{Kss%<_pC)F5JMo7K#kL`D9Yac;AB7Zv5ioTQ38iMVkju77@B<(iXyar z3_-^hr4R#U8zXEGG&}7H*2LKW#?ZG$9|LPvLz5?cGQny+8i0#63_OOJ`5OF_rGe03 zcp5lY`kW2(suRSeFsFtZBRe@+f?|-if7PKZuriX9VL&lTTzQQ07$p%SDD(ximmL~9 z)QDpc(iqy?N(eF97+{W&q&kk5pg{SXJLNITW0c1zIsXq#=ecLmO}}6O0000Npn@-f1H1w=k+?z=RQxydEHlX|BrXe|NYndU+;gtX~O5- z>RtQ!JJlOl-q<@P`d#RCo@nfC8~zUT{&sY_RrQ{jI`ssvrN2Om?mfrMA5*8E1VSca zUA;fn9=wOw8tjGY;cs&``a}3RF!!`wD2eSJT+1DBd8zb9`^5R|kGRX@4Cy^LdDzM) zYVRb_d{{Jp{(|M~>5#pHhii>ap*@DNs$4!{>kH67Pu@KFT;Y0a z9cJ9z!E<#`kV~T=&GnIS?1si&)@MYoOY@%>RLQ;4*bBPi(po@!=UZ`tMEo3394%;j z1z>drF%&A5eTu%d@jD1I${?J2eS=m zaR}M%JcS;hSSiW9=LXl3CLo)+5m{K-kR*C#-#3Mf?R98=ADANgC(*mp9aCrQQOBi8 z7*tbwzrU9Ky*~lwZbdQFJ;`4E;4=9KKFrwOYGRIDCaRkBN^LhK)dWO3E6H zfwS6+hg31jq<6o4lD+6LY8R0~yT#R0zxRgp@q8xXT%Qh60yi?N?-rAm!(pfh=krolo z67R3pJB!jgO{@W8-Wv>S@?qFt$0T}bDfLU}?a;%Tdk0p2j9D+#Q+m6Kwu9s3Y!536 zq8KVw!|3I~_}+m0Vt;&Zmo8S^JEK7;f9>6<2l$^j5-kH(=R$Ik!=||e7V*2ZfFq&v^DQljg_`xaL7i3;!|k>2E&ge^wM6+ZR~A47SMe=Am(MM zvsTvY)g~Ia-zn+diNZc0_bgj zBp9%+-l{^i$(*)qz@X6>+-j5as|B;#H1`Kq708k;$Ny0D z+l#`p0&q`^>*-u~qm}h)VpSM=(PKt`-B*k3kE+DG`|I@87r{7RMOJD@e3;kLo+L%Lo?zfN zpyz}z;E=PkH%asVCh&2SHYl{W(yP~c3>gc53Lj)Q2L1bcU8=zYvHl(DRc#gl8Pyy6 z{`>Ejj3T|wi@S{r2AWUY$f({JY0+*k(%s|TyFVL&!I;5-yOWcXWBIsCw}=01?{)qe zJ&nDq+|WHEJTNfuMDM^j{JVR9pA8p(H961!?%wmo92{Ddzii#TVZ-@5w(m{8cl}UV z%s<+zeX38Q*Skx_(w_`^$ehiXiTKi=Xg4=E_j>@h+-J_7HWkn59&={Ta_8lgS##!i zOh1WpXj~St$CNyJsz3$hJR@@`XM1}E6mHf95emBK(k@x2kLt$0ds4M zJ}bGfy1A5P{(2(UO%rdULgH@pH#gG4|09@B2jNd)hz3f1y+#EdoMLY zQLnN19@&j5$hJz=Q52C*lGuiUR@1v&I0E&>x?4wRZ=G-L&U=E9Ls6jR$^?8QO)#@m?X4{OV)X10u%gzCWi7sb z1@NgyLT^BRvCpF3NSg~*(;M(Z%e)6j)SFjw$JC@UL7;0_M+X$xlF-X~so0Bz-n=ao z+5Xx~!C$SW_m$7SPUZKQ^-jga)htT>Suqd7#Zi-9I!Iq7^j5vAD3V04{WNPj&)OQ< zP9;9igNUbRlGv#Xin?%L@-giV`ao>8;5<`zv^DiIK{zJpQ8}23U zWrrx}pGMiI!#3G5~+6BanY<;@;2_BFWwrg@SmNX(Xyb z8?B?4k6Q-r}Zy_30WC_rl>0cBMaYx`HBVUibDB z?<4aspa-H|-KS4+dx9)-AFzMQ^qDiJP9$v7v>7v|x&HzD+~R#dRL=2r)9$9wYjw~2 zjeo~dJ9F$srmx_O>Nj!nZRQl`1N{8_woB`7 zq)+bwc$41MUS~*0p3-*UxtCY-wC?RSY1{~{MZGbZGG)pWg)BB(wrmBWNH=48ebeiE zm>GAQ(WC+UxTX6d^F0ckCRo-htvh~!x6-xa$HZlfW*3auth)UA-s`-*)LwnF{jk_3 z7K49%Z`D)aQjvG1mx-4Nw%e74y;_nM)mw*=j%4%=BPl!A@plY;oP4j|<2%98zaZLjvWQJzKr0n ze(wjz;^t0^23RxNWy0vKxQusoa~ZdRgU&8)t`kOulOVA5v*db>EbqO%wyl(7#fA;5 ze{3NfNhGSP`2I0k@0u=o$7059J&N6VDjkLg&h@)wMYnHz38`cl5VZDlF?@@hHU!KC zH`KtrJ?=t`05~(daES#M@!VKQYV_MB{C9ShGOgMhi-}7e(ykW{ijPrDt&Ay6gG`In zi4Kx^@53rOWE(Qodx311$O=&3QS?T3zcPwCS1~tuEwYQPa%DP(%w?|ekG~A-DUX>i zz0RtX(TJ{#1F>Po8wfE2W7D0?%Kd2`4 z#nh65a;i&_7eI~a$kqrEP6D-;J)*#qgx(dUNn}?}5_)MTx|{Ttsx7OSMDPBrY1wPL ze5q?OwYN?VJuvjWC~73`eUf(oz>CcNf%n%STQ9`RzxL8sv`*;Vomq#ed1Xy|pA{C{ z6MAQa$aW;rdqH}f#^-sH(ihBX!9#LFrKuCdF`?W$*5*^7)>IUMA7puiYUvb{-pKO8*t@J8icMwI-aXk9_wu_C zN%q$32=k3E`QGfLS+A$CC*EH13qX@lpp%UuYs?CuU71K=R;H6cw}Ge*<*5D`*^MT# z07agmIQ^cncY*`5`vXQaHk`~L6ua7>8er)CpO}m6fG7vBW(zFJ7J_0)<+SQOzqpmi zK5pSr4Cdrr6MVh*4gmdVn=a?{uMV|#19jJ3QTA6k&m*6xvG52!aqF>}vMdT9#C|-g zuUyiq_te5=E0!-^jsPcnBAZ@J1JlHVjOf+%I!?-bI>lntwk`&zoOGnL`RB-M#YzRsrqqeZ&cnJNZGc|E+g$00gMSD-svoRNky+U)8F&g zV#dGxZqlsR`rWE?5&u{3Oz+J$PoU4sH}0Lc{Qzl9_tW^kt^7$BH;R?7TD4L+;KcTA zf4?`jNRcAtqDWf{I)LLRq>Ih{WwU^WaZA5yL2vq7T~kjVyLamUA?qqkvBKUvo}Ld$ zi8h>MgkUD-u;&S-cCo`m{=Tc8owi)KS>Q4IO+-a1Vz5-da&@+owCL-{o&qnT0Cdo|PM+hadIebCI|Rj)Tc`H^43Fx4)~wlQ4L32eanoi^ zo3uf8>zf>Pzph!?Q(McMUbfkO0%-E)y_$Z~z*oSAD&wWQ=DnJa z@9`SVk0sSx{5x18E|V1PPb9L}?ly#F&Ksik%50@SkxiAlBlF5sB?y_Oy6}R4)b?%= zrM0N~oA<8l!s{GpkW}x=$&atvIg_No>?ATubOp+beaZHQ6cYzfoiXdZ*(^@c25}cR zEbpx(=3;Qrytl~)UPqJrZW6utPWka9mi3-%wihTSZYSG2OH{?EWE$-6S&?w`B5MIW zT0Ugfd1Q|>Jw*|_dE_KH;rRM_7j>s*Z;C+G>Zc@n6_)j?Ziu=h+gn*0hS9gjQs2AU z^bId9u-R_Ui<31+Yk_qoQ9P{O=et&}Z)r{-OzRQ@EgxWYMR# z7b4s<&oJ#3hV!iSz7k^XYkN;={$B5HA)a7#nGn$cnX}jl-8v#0DSf*tPO%1Z?{GOw zj770e#9G?htA?zKO%26(Ox3p8ms~G}ggZtL!pwV(Ebo1lTO7wg*Nk(bg*qlYphtUT zPST8%GJm<(S?L{vVvo=OVY}S61f|q_)m!#kR&T`}RJv$@fl{Yrd;5ys7!C1FeJ_JV zcMN>`^Fq`{Hc;$>QBBl&($A>Od)F2M9yam`ossLV62S=hpzV$Q=ZWDxu!;TL9fW%zJ0l1b&uoq0QOnO?m@_I06h1t&;6! zq;N%bxEpEE%f+I$KFH<`0nTS$3>+3qfa5te$Oh)~2bRu*OJXQ+MT|nx|7##@H$1ml z|0ccIPXwYqwAmlutIsD6BO5QZMW$(MqZD$bK?rb5nvlv~9yQH1^w7CQpSvn`N6L=e zIlRPD0KM%W$TXu$&D}F{xSnyfM8WR&T|X5Cd)@}H`yIv84o=Z;)azf-dd&h3wkJtC!Elq^>u4{vT?2jd2q(2r4?lqTaBgBu(wLRC@I?@)wd9 zUO5uL=PJ-OdxRyuVBNi8jl)RR*k6%-R2%60Ubyx5cHa7w6!>0tyX) zsO<|<)jMeGODuC@tNk1FZfMzZEh)KhmSikNrJd5Pd&4[H2SO4][Hello World] C + D")\ +// #ce("2[Fe(CN)6]^4+") #linebreak() #ce("3[Co(NH3)4]^2+") #linebreak() #ce("[FeCo(CN)4 (NH3)2]^5-") #linebreak() -#ce("[Co(en)3]^3- + 3[HCl]^+") +#ce("[Co(en)3]^3- + 3[HCl]^+") \ No newline at end of file diff --git a/tests/charges/ref/1.png b/tests/charges/ref/1.png index 3d34e73312cc84d3c3da2381c0217db922d34609..298ea49090240977c50ca657aa5c13aea596a8d0 100644 GIT binary patch literal 8760 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 oR(_nXzP3UwRZG=Uwe*4f5A=@ H2O") -#linebreak() -#ce("OH-3") -#linebreak() -#ce("Fe(OH)2^0") -#linebreak() -#ce("PO4-3") + +#ce("H^-IV") +#ce("H^IV") +#ce("H^.2+") +#ce("H^+2") +#ce("H^3+") +#ce("H^.2") +#ce("H^+") +#ce("H+") +#ce("H+2") +#ce("H^.") +#ce("H+") + +#ce("H2^-IV") +#ce("H2^IV") +#ce("H2^.2+") +#ce("H2^+2") +#ce("H2^3+") +#ce("H2^.2") +#ce("H2^+") +#ce("H2+") +#ce("H2+2") +#ce("H2^.") +#ce("H2+") + +#ce("H2^-IV^^1") +#ce("H2^IV^^1") +#ce("H2^.2+^^1") +#ce("H2^+2^^1") +#ce("H2^3+^^1") +#ce("H2^.2^^1") +#ce("H2^+^^1") +#ce("H2+^^1") +#ce("H2+2^^1") +#ce("H2^.^^1") +#ce("H2+^^1") diff --git a/tests/content-to-ir/ref/1.png b/tests/content-to-ir/ref/1.png deleted file mode 100644 index 80870671f420cdd2ca724e2037d78df70fa723d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1083 zcmV-B1jPG^P)R))<~M!+1b?8)Q9cK=kx9D?V0-G;vzgTuq2I0BuYp~c-WqMdwa{v%S;`!7oPL; z^LO^#-{0@*>MAcUuc)Z#>FGH+IZ=(LXo3VhdhJX8F9?O>LuihDBk(qf_#ArWNdYej zABA)2!+Vm(-`}6hBnrm(P3W#hTNO3$AX z%FlU{L6|^zUYN_YdV6~_SONk9Y;A3YLLt-S@pyhL9v&Wcc6NT(z;fN(+$5HRg9C4G zZ&;0vj?y(Sw#LTBAt50MV=C|zi?2L??~)9k@(9nXKL1^ieV3G#m3ex4c6WF8v6PmU zX0yb`#=5z=WnTk5;YKV1fxy7PU~q7d~J3juFFxBn9N> z=c7Y#aIiW(;pTh|7Z;bKqoZ3q<-?Dmp&>LzMn*^hO-)VcU~X=%GS9DHXi6ki-X*i1 zO0cl7ke;4SH!(Rm2?uaX&NwtQ#PmVvQjKSp5Sxco;d5Be`=PzPopIRO+9Cx^O-(U< z5UA8JZ|?;xX2t;lj1(|1Fu?R#Sy_3gp2x?>`uh5C=@$R`~$r#8;gU5hUNeO002ovPDHLkV1lr` B5HA1# diff --git a/tests/content-to-ir/test.typ b/tests/content-to-ir/test.typ deleted file mode 100644 index ead8458..0000000 --- a/tests/content-to-ir/test.typ +++ /dev/null @@ -1,14 +0,0 @@ -#import "../../src/parse-content-intermediate-representation.typ": content-to-ir -#import "../../src/display-intermediate-representation.typ": display-ir -#set page(width: auto, height: auto, margin: 0.5em) - -#let x = ( - ( - type: "molecule", - children: ( - (type: "element", symbol: "H", count: 2, charge:2, symbol-body:text(red)[H], count-body:strike("gugugaga"), charge-body:strike("gugugaga")), - (type: "element", symbol: "O"), - ), - ), -) -#display-ir(x) \ No newline at end of file diff --git a/tests/content-to-ir/.gitignore b/tests/elembic/.gitignore similarity index 100% rename from tests/content-to-ir/.gitignore rename to tests/elembic/.gitignore diff --git a/tests/elembic/test.typ b/tests/elembic/test.typ new file mode 100644 index 0000000..4fc9a8f --- /dev/null +++ b/tests/elembic/test.typ @@ -0,0 +1,49 @@ + +#import "../../src/parse-formula-intermediate-representation.typ":* +#set page(width: auto, height: auto, margin: 0.5em) + +#[ + #lorem(5)\ + #lorem(5)\ + #lorem(10)\ + Hello World + #sym.bullet + #math.dot + $ + #string-to-element("H5+3").at(1)\ + #reaction(string-to-reaction("(H5+3)5+3"))\ + + #reaction(( + molecule((element("C"), + group((element("H", count:2), element("S"), element("O", count:4)), + charge:-2, + // count:5, + ),)), + + arrow(kind:1, top:(molecule((element("H", count:2), element("O")),count:2, ), ), bottom:(element("C", z:6, a:14),)), + + molecule((element("He", count: 2, charge: 1, a: 15, z: 11, oxidation: "+IV"),), + count:2, + phase:"aq", + ),) + ) + + #group(( + group(( + group(( + element("H", count: 2, oxidation: "+I"), + element("O", oxidation: "-II"), + ), + charge: -2, + count: 2, + kind: 1, + ), + element("R", ), + ),count: 2,), + )) + $ + + Hello World\ + #lorem(10)\ +R' +] diff --git a/tests/intermediate-representation-molecules/ref/1.png b/tests/intermediate-representation-molecules/ref/1.png index 59bf7b8cb6381d04889d94e6ab73c7626c282b07..83cc81783e69378f5660bc5e6e8a4810f7b0a2f1 100644 GIT binary patch literal 2115 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 literal 2031 zcmVJT{3yNhmjzPyHtOiWTjv8GxxYl5cB?edpqiaFjScolAP-3#`3MybD%DTFa zfCelpDiAfg7Qlifh*-diV1WpcVgYy=+zhVU8ia2JAin)2(YCyI9zu zq&f&5(r}b)rHz!9Zx#tp8(-p*SqiXDT&;C%vSg75aLIV72$Nys4JAV4su-Dw}QW>i`g8mS6kW z49hk@qbsX-A#mQzP6NObtL7QN??XY|WGbAJb^w2w@3kZgV7U#okLCQEZLa{N^uGYW z<^~g=1KA%p{|~^hb-=sBLHqW%kktj`#yQmmmiuFxj$Z$rOaEl#j&=WF5WXrXL8G_WA--Wrl;${44QM_rAm^JCr(@T?v7ei^ zTGKo!d^JV|z{Rc12?SVlh=oy@B=*Zia2SBdhqF4VA#n;p_V5~0 z$EIv}VjS!=d;5V&)Ib6D_Vy9zny)bu*3Q{))gx-46MlZy8vqEtf)2aE*VlLQCohJT zh5suyh#IS(4x2sV`|mf2)u)M%j*j+oe=(fEWU+;DadA7v?pu1>z*MnviQ+1?gN;2) zm4&8Ph7EIdnzX}M*xX~KorAKSan+*h?xIn>U4u*57EfGwc=&Aq4jedgC@O;-$IJ?` z`kf8>&DiVigM|2W#te6-_r%+j?a=@g0L3%S$3*{fVovv?O(O{P>LHq32UyrwYBMzn z(uPzE3kG6KPA>e+FJB)ExkBUuXjhsQQ6~4j0H_9-tp?!K>rw=;x|`Ia1b~il0L5F?`j7v0JEgI$OX#S@UD^%03)*jxNq~k2C&_F0kcJX zS*#E;7VjhIH**C5@sdS4a+;PpY~w>C{?yo4R+=J2|V4HVHph?EGP zgXC9BQpn3#0wvo*6fBJ*4Zv<`4QO{Q3Azn&7HJh@K{ZUQDQ6p8kg+|a$3VOYhKVld zY9%cz2g@L7Rw=g4zYM@m84S2D$Z__DXVUV@uq2Ak{uywlL6SCa{$}*+=~O+ji?lhWkYp2*mMBiGAjW&_fL_5qV#{v#8eb4 z!BV?{3bsX<0odjNz~38xkqKZwp;GL9sTVI{l}3nEfRb$RwL@3&hhi^s&;Lj%76(a^ zv8bd|24EY1tyV)+`xd7G>k7BQwI{%Ov9V(8Idgvi|8Ur4h1~5H@7_U|$1Z@sZADk6 zrGiZp=>}kXIH}c{M^S(L85EdBRfK)t>INw-N1=x;NZKaE=7bcGTlu>~aC{8#sNnbrI@-Ve^2r&ol%HMz6B%P#b>m&Urf zxw%f5gHp`s0WOP+0fuh{xySzPt%h{zaSXtBtn0)vA?WfeLf4e#0BHX0IsZ!4SpJZz zemZ*kR8KXma52)y_s&04TH&2yDoWip4IqDne_>)@=i_2z~57Y)imj+&2O?AS)Xq<~_6! z&;D8{&dTb!0hC6y*BSvUgowq61v|SgRRL~7h*E4!qiB9RHa#UJjhd(+B_-tv$R)iW z^U7TByV~a_Nl#x5>}8i6aLa!suvc1HSzBA#@Je8RSnz9`{x7ej{ujO23m+Ml({ca+ N002ovPDHLkV1f>N)I9(I diff --git a/tests/intermediate-representation-molecules/test.typ b/tests/intermediate-representation-molecules/test.typ index 62d4be3..2047a6a 100644 --- a/tests/intermediate-representation-molecules/test.typ +++ b/tests/intermediate-representation-molecules/test.typ @@ -1,90 +1,65 @@ -#import "../../src/display-intermediate-representation.typ": display-ir +#import "../../src/model/element.typ": element +#import "../../src/model/molecule.typ": molecule +#import "../../src/model/group.typ": group #set page(width: auto, height: auto, margin: 0.5em) -#let co2 = ( - type: "molecule", - count: 1, - phase: "g", - charge: 0, - align: none, - arrow: none, - children: ( - ( - type: "element", +#let co2 = molecule( + ( + element( + "C", count: 1, - symbol: "C", charge: 0, - oxidation-number: none, - isotope: none, - align: none, + oxidation: none, + a: none, + z: none, ), - ( - type: "element", + element( + "O", count: 2, - symbol: "O", charge: 0, - oxidation-number: none, - isotope: none, - align: none, + oxidation: none, + a: none, + z: none, ), ), + count: 1, + phase: "g", + charge: 0, ) -#let hexacyanidoferrat = ( - type: "molecule", - count: 3, - phase: "s", - charge: 0, - align: none, - arrow: none, - children: ( - ( - type: "group", - count: 2, - kind: 1, - charge: 4, - align: none, - children: ( - ( - type: "element", +#let hexacyanidoferrat = molecule( + ( + group( + ( + element( + "Fe", count: 1, - symbol: "Fe", - charge: 0, - oxidation-number: none, - isotope: none, - align: none, ), - ( - type: "group", - count: 6, - kind: 0, - charge: 0, - align: none, - children: ( - ( - type: "element", + group( + ( + element( + "C", count: 1, - symbol: "C", - charge: 0, - oxidation-number: none, - isotope: none, - align: none, ), - ( - type: "element", + element( + "N", count: 1, - symbol: "N", - charge: 0, - oxidation-number: none, - isotope: none, - align: none, ), ), + count: 6, + kind: 0, ), ), + count: 2, + kind: 1, + charge: 4, ), ), + count: 3, + phase: "s", + charge: 0, ) -#display-ir(co2)\ -#display-ir(hexacyanidoferrat) +#co2\ +#hexacyanidoferrat +// #display-ir(hexacyanidoferrat) diff --git a/tests/intermediate-representation-reactions/ref/1.png b/tests/intermediate-representation-reactions/ref/1.png deleted file mode 100644 index c3fbed2a0693d5f714dbeaae41a0bd3a6478c63e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6020 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 mLTu2WA;bm^AvS36=Kcrnn&Wo#@0ala0000[H2SO4][Hello World] C + D")\ +#ce("[Cu(H2O)4]^2 + 4NH3 ->[dissolve in H2O][$Delta H^0$] [Cu(NH3)4]^+2 4H2O")\ \ No newline at end of file From 67b30869ce77a84916d2fdb9b1846b4abfa5dcb4 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Fri, 30 May 2025 12:43:00 +0200 Subject: [PATCH 07/20] Updated Approach for content to formula --- src/lib.typ | 13 +- src/model/element.typ | 19 +- ...se-content-intermediate-representation.typ | 427 ++++++++++++++---- src/utils.typ | 69 ++- tests/content-to-reaction/.gitignore | 4 + tests/content-to-reaction/ref/1.png | Bin 0 -> 2935 bytes tests/content-to-reaction/test.typ | 32 ++ typst.toml | 2 +- 8 files changed, 460 insertions(+), 106 deletions(-) create mode 100644 tests/content-to-reaction/.gitignore create mode 100644 tests/content-to-reaction/ref/1.png create mode 100644 tests/content-to-reaction/test.typ diff --git a/src/lib.typ b/src/lib.typ index 08abde4..8ade537 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -6,7 +6,18 @@ ) #import "model/reaction.typ": reaction #import "parse-formula-intermediate-representation.typ": string-to-reaction +#import "parse-content-intermediate-representation.typ": content-to-reaction #let ce(formula) = { - reaction(string-to-reaction(formula)) + if type(formula) == str{ + reaction(string-to-reaction(formula)) + } else if type(formula) == content{ + // formula + let r = content-to-reaction(formula) + if type(r) == content{ + r + } else{ + reaction(content-to-reaction(formula)) + } + } } diff --git a/src/model/element.typ b/src/model/element.typ index 44b8f19..854b771 100644 --- a/src/model/element.typ +++ b/src/model/element.typ @@ -19,6 +19,9 @@ affect-layout: true, roman-oxidation: true, roman-charge: false, + radical-symbol: sym.dot, + negative-symbol: math.minus, + positive-symbol: math.plus, ) = { } #let draw-element(it) = { @@ -42,8 +45,15 @@ customizable-attach( base, - t: oxidation-to-content(it.oxidation, roman:it.roman-oxidation), - tr: charge-to-content(it.charge, radical: it.radical, roman:it.roman-charge), + t: oxidation-to-content(it.oxidation, roman: it.roman-oxidation), + tr: charge-to-content( + it.charge, + radical: it.radical, + roman: it.roman-charge, + radical-symbol: it.radical-symbol, + negative-symbol: it.negative-symbol, + positive-symbol: it.positive-symbol, + ), br: count-to-content(it.count), tl: mass-number, bl: atomic-number, @@ -70,5 +80,8 @@ e.field("affect-layout", bool, default: true), e.field("roman-oxidation", bool, default: true), e.field("roman-charge", bool, default: false), + e.field("radical-symbol", content, default: sym.dot), + e.field("negative-symbol", content, default: math.minus), + e.field("positive-symbol", content, default: math.plus), ), -) \ No newline at end of file +) diff --git a/src/parse-content-intermediate-representation.typ b/src/parse-content-intermediate-representation.typ index c447e97..5ceebc7 100644 --- a/src/parse-content-intermediate-representation.typ +++ b/src/parse-content-intermediate-representation.typ @@ -1,99 +1,356 @@ -#import "utils.typ": get-all-children, is-metadata, typst-builtin-styled, typst-builtin-context -#import "parse-formula-intermediate-representation.typ": string-to-ir -#let content-to-ir(body) = { - if type(body) == str { - return string-to-ir(body) - } else if type(body) != content { - return none +#import "utils.typ": get-all-children, is-metadata, typst-builtin-styled, typst-builtin-context, length +#import "parse-formula-intermediate-representation.typ": patterns + +#import "utils.typ": arrow-string-to-kind, is-default, roman-to-number +#import "model/molecule.typ": molecule +#import "model/reaction.typ": reaction +#import "model/element.typ": element +#import "model/group.typ": group +#import "model/arrow.typ": arrow + +#let get-count-and-charge(count1, count2, charge1, charge2, index, templates) = { + let radical = false + let roman-charge = false + let count = if not is-default(count1) { + templates.slice(index + if count1.contains("_"){1}, index + length(count1)).sum() + } else if not is-default(count2) { + templates.slice(index + length(charge1) + if count2.contains("_"){1}, index + length(charge1) + length(count2)).sum() + } else { + none + } + + let charge = if not is-default(charge1) { + templates.slice(index + if charge1.contains("^"){1}, index + length(charge1)).sum() + } else if not is-default(charge2) { + templates.slice(index + length(count1) + if charge2.contains("^"){1}, index + length(count1) + length(charge2)).sum() + } else { + none } - let children = get-all-children(body) - // body - // linebreak() - // repr(body) - // linebreak() - // linebreak() + // if not is-default(charge) { + // if charge.contains(".") { + // charge = charge.replace(".", "") + // radical = true + // } + // if charge.contains("I") or charge.contains("V") { + // let multiplier = if charge.contains("-") { -1 } else { 1 } + // charge = charge.replace("-", "").replace("+", "") + // charge = roman-to-number(charge) * multiplier + // roman-charge = true + // } else if charge == "-" { + // charge = -1 + // } else if charge.contains("-") { + // charge = -int(charge.replace("-", "")) + // } else if charge == "+" { + // charge = 1 + // } else if charge.replace("+", "").contains(regex("^[0-9]+$")) { + // charge = int(charge.replace("+", "")) + // } else { + // charge = 0 + // } + // } - let result = () + return (count, charge, radical, roman-charge) +} - let string = "" - for child in children { - if is-metadata(child) { - if is-kind(child, "molecule") { - result += child.value.formula - } else if is-kind(child, "element") { - result += child.value.symbol +#let string-to-element(formula, templates, index) = { + let element-match = formula.match(patterns.element) + if element-match == none { + return (false,) + } + let symbol = element-match.captures.at(0) + let oxidation = element-match.captures.at(5) + let x = get-count-and-charge( + element-match.captures.at(1), + element-match.captures.at(3), + element-match.captures.at(2), + element-match.captures.at(4), + index + symbol.len(), + templates + ) + let oxidation-number = none + let roman-oxidation = true + let roman-charge = false + if oxidation != none { + oxidation = upper(oxidation) + oxidation = oxidation.replace("^", "", count: 2) + let multiplier = if oxidation.contains("-") { -1 } else { 1 } + oxidation = oxidation.replace("-", "").replace("+", "") + if oxidation.contains("I") or oxidation.contains("V") { + oxidation-number = roman-to-number(oxidation) + } else { + roman-oxidation = false + oxidation-number = int(oxidation) + } + if oxidation-number != none { + oxidation-number *= multiplier + } + } + + if x.at(0) == none and x.at(1) == none and x.at(2) == false { + if formula.at(element-match.end + 1, default: "").match(regex("[a-z]")) != none { + return (false,) + } + } + + return ( + true, + element( + templates.slice(index, index + element-match.captures.at(0).len()).sum(), + count: x.at(0), + charge: x.at(1), + radical: x.at(2), + oxidation: oxidation-number, + roman-oxidation: roman-oxidation, + roman-charge: x.at(3), + ), + element-match.end, + ) +} + +#let string-to-math(formula) = { + let match = formula.match(patterns.math) + if match == none { + return (false,) + } + return (true, eval(match.text), match.end) +} + +#let string-to-reaction( + reaction-string, + templates, + create-molecules: true, +) = { + let remaining = reaction-string + if remaining.len() == 0 { + return () + } + let full-reaction = () + let current-molecule-children = () + let current-molecule-count = 1 + let current-molecule-phase = none + let current-molecule-charge = 0 + let random-content = "" + + let index = 0 + while remaining.len() > 0 { + if remaining.at(0) == "&" { + if current-molecule-children.len() > 0 { + full-reaction.push(molecule(current-molecule-children)) + current-molecule-children = () } - } else if type(child) == content{ - let func-type = child.func() - if func-type == text { - result += string-to-ir(child.at("text")) - string += child.at("text") - } else if func-type == typst-builtin-styled{ - let styles = child.at("styles") - let ir = content-to-ir(child.at("child")) - if type(ir) == array{ - if ir.len() == 1{ - ir = ir.at(0) - } else{ - for value in ir { - value.styles = styles - } - result += ir - } + full-reaction.push($&$) + remaining = remaining.slice(1) + index+=1 + continue + } + let math-result = string-to-math(remaining) + if math-result.at(0) { + if not is-default(random-content) { + full-reaction.push([#random-content]) + } + random-content = "" + full-reaction.push(math-result.at(1)) + remaining = remaining.slice(math-result.at(2)) + index+=math-result.at(2) + continue + } + + let element = string-to-element(remaining, templates, index) + if element.at(0) { + if not is-default(random-content) { + if current-molecule-children.len() == 0 { + full-reaction.push([#random-content]) + } else { + current-molecule-children.push([#random-content]) } - if type(ir) == dictionary{ - ir.styles = styles - result.push(ir) + } + random-content = "" + current-molecule-children.push(element.at(1)) + remaining = remaining.slice(element.at(2)) + index+= element.at(2) + continue + } + + + let group-match = remaining.match(patterns.group) + if group-match != none { + if not is-default(random-content) { + if current-molecule-children.len() == 0 { + full-reaction.push([#random-content]) + } else { + current-molecule-children.push([#random-content]) } - // result.push((type:"content", body:child)) - } else if func-type == typst-builtin-context { - result.push((type:"content", body:child)) } - else if func-type in ( - pad, - figure, - quote, - strong, - emph, - highlight, - overline, - underline, - strike, - smallcaps, - sub, - super, - box, - block, - hide, - move, - scale, - circle, - ellipse, - rect, - square, - typst-builtin-styled - ) { - result.push((type:"content", body:child)) - } - else if child == [ ] { + random-content = "" + + let group-content = group-match.captures.at(0) + let kind = if group-content.at(0) == "(" { + group-content = group-content.trim(regex("[()]"), repeat: false) + 0 + } else if group-content.at(0) == "[" { + group-content = group-content.trim(regex("[\[\]]"), repeat: false) + 1 + } else if group-content.at(0) == "{" { + group-content = group-content.trim(regex("[{}]"), repeat: false) + 2 + } + let x = get-count-and-charge( + group-match.captures.at(1), + group-match.captures.at(3), + group-match.captures.at(2), + group-match.captures.at(4), + ) + let group-children = string-to-reaction(group-content, create-molecules: false) + + current-molecule-children.push(group(group-children, kind: kind, count: x.at(0), charge: x.at(1))) + remaining = remaining.slice(group-match.end) + continue + } + + let plus-match = remaining.match(patterns.reaction-plus) + if plus-match != none { + if current-molecule-children.len() > 0 { + full-reaction.push( + molecule( + current-molecule-children, + count: current-molecule-count, + phase: current-molecule-phase, + charge: current-molecule-charge, + ), + ) + current-molecule-children = () + } + if not is-default(random-content) { + full-reaction.push([#random-content]) + } + random-content = "" + full-reaction.push([+]) + remaining = remaining.slice(plus-match.end) + continue + } + + let arrow-match = remaining.match(patterns.reaction-arrow) + if arrow-match != none { + if current-molecule-children.len() > 0 { + full-reaction.push( + molecule( + current-molecule-children, + count: current-molecule-count, + phase: current-molecule-phase, + charge: current-molecule-charge, + ), + ) + current-molecule-children = () + } + if not is-default(random-content) { + full-reaction.push([#random-content]) } - else { - result.push((type:"content", body:child)) + random-content = "" + let kind = arrow-string-to-kind(arrow-match.captures.at(0)) + let top = () + let bottom = () + if arrow-match.captures.at(1) != none { + top = string-to-reaction(arrow-match.captures.at(1)) } + if arrow-match.captures.at(2) != none { + bottom = string-to-reaction(arrow-match.captures.at(2)) + } + full-reaction.push(arrow(kind: kind, top: top, bottom: bottom)) + remaining = remaining.slice(arrow-match.end) + continue + } + + random-content += remaining.codepoints().at(0) + remaining = remaining.slice(remaining.codepoints().at(0).len()) + } + if current-molecule-children.len() != 0 { + full-reaction.push( + molecule(current-molecule-children, count: current-molecule-count, phase: current-molecule-phase), + ) + } + if not is-default(random-content) { + full-reaction.push([#random-content]) + } - repr(type(child)) - h(1em) - repr(child.func()) - h(1em) - repr(child.fields()) - h(4em) - linebreak() - // result += child + return full-reaction +} + + + + + +#let create-full-string(children) = { + let full-string = "" + let templates = () + for child in children { + if type(child) == content { + let func-type = child.func() + if child == [ ] { + full-string += " " + } else if func-type == text { + full-string += child.text + for value in child.text { + templates.push([#value]) + } + // templates += child.text.map(x=> [#x]) + } else if func-type == typst-builtin-styled { + let x = create-full-string(get-all-children(child.child)) + full-string += x.at(0) + templates.push(child) + } else if ( + func-type + in ( + math.overbrace, + math.underbrace, + math.underbracket, + math.overbracket, + math.underparen, + math.overparen, + math.undershell, + math.overshell, + pad, + strong, + highlight, + overline, + underline, + strike, + math.cancel, + //TODO: implement missing methods in utils: + figure, + quote, + emph, + smallcaps, + sub, + super, + box, + block, + hide, + move, + scale, + circle, + ellipse, + rect, + square, + ) + ) { + let x = create-full-string(get-all-children(child.body)) + full-string += x.at(0) + templates.push(child) + } else { + continue + } } } - // return result - - - // return string-to-ir(string) + return (full-string, templates) +} + +#let content-to-reaction(body) = { + if type(body) != content { + return () + } + let children = get-all-children(body) + let (full-string, templates) = create-full-string(children) + + return string-to-reaction(full-string, templates) } diff --git a/src/utils.typ b/src/utils.typ index 7614439..ddaf047 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -149,17 +149,37 @@ return [#count] } } else if type(count) == content { - return count + if count != [1] { + return count + } } return none } #let roman-to-number(roman-number) = { return roman-numerals.position(x => x == roman-number) - // return if oxidation == "I" { 1 } else if oxidation == "II" { 2 } else if oxidation == "III" { - // 3 - // } else if oxidation == "IV" { 4 } else if oxidation == "V" { 5 } else if oxidation == "VI" { 6 } else if ( - // oxidation == "VII" - // ) { 7 } else if oxidation == "VIII" { 8 } else { none } +} +#let show-roman(body, roman: true) = { + if roman { + show "1": "I" + show "2": "II" + show "3": "III" + show "4": "IV" + show "5": "V" + show "6": "VI" + show "7": "VII" + show "8": "VIII" + body + } else { + // show "V": "5" + // show "I": "1" + // show "II": "2" + // show "III": "3" + // show "IV": "4" + // show "VI": "6" + // show "VII": "7" + // show "VIII": "8" + body + } } #let oxidation-to-content(oxidation, roman: true) = { if oxidation == none { @@ -267,6 +287,12 @@ } // own utils +#let length(value) = { + if value == none { + return 0 + } + return value.len() +} #let is-default(value) = { if value == [] or value == none or value == auto or value == "" { return true @@ -384,17 +410,16 @@ rest: template.at("rest", default: 0%), ) } else if func == strong { - return template.func()( - body, - delta: template.at("delta", default: 300), - ) + return template.func()(body, delta: template.at("delta", default: 300)) } else if func == highlight { return template.func()( body, + bottom-edge: template.at("bottom-edge", default: "descender"), extent: template.at("extent", default: 0pt), fill: template.at("fill", default: rgb("#fffd11a1")), radius: template.at("radius", default: (:)), stroke: template.at("stroke", default: (:)), + top-edge: template.at("top-edge", default: "ascender"), ) } else if func in (overline, underline, strike) { return template.func()( @@ -418,12 +443,19 @@ } } -#let charge-to-content(charge, radical: false, roman: false) = { +#let charge-to-content( + charge, + radical: false, + roman: false, + radical-symbol: sym.dot, + negative-symbol: math.minus, + positive-symbol: math.plus, +) = { if is-default(charge) { - [] + none } else if type(charge) == int { if radical { - sym.bullet + radical-symbol } if roman { roman-numerals.at(calc.abs(charge)) @@ -432,17 +464,22 @@ if calc.abs(charge) > 1 { str(calc.abs(charge)) } - math.minus + negative-symbol } else if charge > 0 { if charge > 1 { str(charge) } - math.plus + positive-symbol } else { [] } } } else if type(charge) == str { - charge.replace(".", sym.bullet).replace("-", math.minus).replace("+", math.plus) + charge.replace(".", radical-symbol).replace("-", negative-symbol).replace("+", positive-symbol) + } else if type(charge) == content { + show ".": radical-symbol + show "-": negative-symbol + show "+": positive-symbol + show-roman(charge, roman: roman) } } diff --git a/tests/content-to-reaction/.gitignore b/tests/content-to-reaction/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/content-to-reaction/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/content-to-reaction/ref/1.png b/tests/content-to-reaction/ref/1.png new file mode 100644 index 0000000000000000000000000000000000000000..0fb8034cd1ed74f1df66a04513d8542366dff69e GIT binary patch literal 2935 zcmV--3yAcIP)(wC?(S6K?(X_<9+p6`BsdQ{+`W#6yL$)@!Gc4uhuh)q?oNXB{VH9|O?r0j za(8>X*}eHHm74u;@6PNu-CzH0eh?P^HvC{wV3BK)Yr!JdBG)3qTC@N7 zZQHg@n>KCc%$a=lAw27dS`|g@N}^6B!ONBY0xX;(IL62y z?>4C`d>RWoVb!WtnKNhZ-@m_~pI_g;eM^=sxqJ8SWXX~Z88XDCAVB)y!Gjslu3fto zD^@VCn>A~eK7IPpqep-H_RUD{)TvWDb?VfrRjcvi$1}j$Gq+JqQND;+zecq$T_mu> zvi-1Q$Bx8_6JNP<14Ij|L4ID8@Bw)EX$anAF<#_Sp#Xp+k`t|GXKyIQ$ ziCVU7sTj+ZE5|gvckiBJKYH{i?9`}HLzo~(jvUdVMZ0+MqI`Mg%$YWA+I;=`RlY=; zxo7<6`1$kaSImJ-y92oyGGs`eJo(e7Pc;jATDELiWc10CCq~kh8La@>Wk8)eb>vI- zx@_69vDaxcm#K~0;7YOp3;p`_W9w+#xbeh^6WMS?xZ&&T3mv(0=Vn`b^X5%ZP*D8% z@$taqbkl>`6eC6q1|L3rSeRh#+O-To#N|sA0G*ZO^1oE6Qk^?@ju@J|Y_T|hHq76G zS)xKwh1?S-PNYebX4tS{Y{bFA!SGwOXi;1%5k5S6^oV^QMnKf4QPET8tdvCsEOt4} zOc135C&#cWO3Uffr`hHB&U*FgF_F2*2tY0*{@6kMG$hPlr`8I&Oj2Zy)v8$@K77d1 zn>=~42sf@>yT(F>R!EVE6)P4#tTJrl#*Ok_P9aQi>Cz=RM%nQcIG((u4(xlQ4W6u#fkQ~4c&YwRoOmOhvK{@93?b|L- zZtvc``6ZPrSN^XKDO08_T)40ZALKd>2nZkwKx(BpR;f~juN^aHjL-oG7Tv&(E=!A< zH->5p;OgXd=+J>*i_aJw7Eq??21zMA$B`pPo;!EW7;Dw9U!O=JL4pJ%Xf!+Xj;}>1 zmGRs&xYHJj5j}eJU_hlxm4q$KpFiJ~$el4`1}sdNFhSS?ELX2yod^~BX3d&~k}6vK z;lqa{Ns^G7F@6KPT-&y76(cSx+!ZNOM3?}_3++~dH_a{MQeBB$GI*@Pnl)=GoFWb! zEL^w{#n2q`UcGwp%TZE2x%}V?6)K3ZU<|wK5QMmK<7xrIk|j%+ckDUZNa*7S zqtVBY9~V0G>C=bwg3J;adB1cgB=klO|1AKu#t1Q;6{P6kh)k z|1K@6l@VmWc)1Fi;AyV#_7F8I2<*J;RZq0?(r+UKS_BbzyEs))rdL(HAmRhOdi6@n zf|GxV88ap{F?sPrM~oOj3X$kGSFT(aE?f}6L~ZygWZK8ahr7r<=VJiU+ z-Y%Y;^T;*d&b9w~7%ew$+&Fpiq|NCDkli?O;vjrV7JS#PUDAS3vN7TzY~8vQhao(9 zbW-4m^`uFYSU041&9ZI;jT|_DT<74~vuFA9=hx3@nFV*idA+^81#nk#(OimG%pjSo zeevQ2j;hRp|K0HP^kkO{2nES_$VHk7k==w`ayqE6mII`?AjQ-%*sx)P|92qJkc&X? z+qcgh$bJ0yFjQFAwu1-2%iupnF(4hMTIxjJ&h1b8Gp zD1%z1b*tmXf<>-Hu0^h^0gr{!oEFqkK(ffS8>ripV9Oi|%JnooTIAXd<=md+B^r@q3u>a~<+gl@#c*(UFat53^N>(;G(eSOKT$jmH#7#w54&bwqo zxM*P~(51n7OOz-DMN?l;X)` zPMI==RvM>~D=(T3Lvn*eid-%^Z0gvu;y_A}zAo%kXZP;iOD$H|1MD!FGG)?Nx5xva zkDN$rXEIhC=nfe$V1U*g!QAZ9rAzea(fO=07(t+-eEITaE;wh-oV1=eSr!Nr`E8`Y z6+4v$>=i3k6!rsxNM%7Wjv6(}j9k_b%;i`0`*`nfMSv`LD#Nb8K9+ykXlNjaIIw^A=eIq=3sK^ z7()ut4cV?7eo#{6JL&w^Tz#$k)7Xp0#AGnfIQ&KjE(0=V%qU+TI&?_ipW3lwM>3y$ zt;>*0wUxb|S_AD_6t!5h)EPy%5fl`JQ1Z31c8l7|?hqrG$P5F&k#2`~2JnU2Y{k9B z&!NIZyA+K)Qfiz}ZmLwNvV=v76e*14(nvrj((Bi+~OHJ+-9iWI=YP}-W~ag%V~TN~Bs&x|mG#TY zzz(iny(&zA?^Np6r1%<)2n3jLL59$7S;X!-Kxic`h8l;&q+?Sla{ zso(;;Jh=pH{9t@l|FA&&lvT-sH8vh&A#f4~pei;SljyR>K4;DxVS=$^$1;GzomsWi zmB?lPfn59qVFG+cnicuYRjXFzH?vtOjd)CRn|TJb#mmb}vC}fb1<;!#OhD(38T&M> z(B0!o6MOgLwD^2zqk)xUmhNMNJT4^g5^#QxFJE4%x8u@5?yXz5Z1#>5ci^rf!NxK^ji+bNp0ycVFzcO3u6*p142X0*3l=O$ zmx}C^p&5&IHm)8!6Ngj?PbO-9F3Ay|j3A(-p`Im4zKTecx9GW}iHHNu1YU}C3f-1mL>y;U%c!C-!cibo)8ISkR72l+zJ*+{n~;l_hU@mfP!P_MU6k1` z#{*{^auEtQnYn&`e(}SCr)`A8ffvNnRmF=Jhg<}TtgcmBw[H2SO4][Hello World] C + D")\ +// +// #ce("2[Fe(CN)6]^4+") +// #linebreak() +// #ce("3[Co(NH3)4]^2+") +// #linebreak() +// #ce("[FeCo(CN)4 (NH3)2]^5-") +// #linebreak() +// #ce("[Co(en)3]^3- + 3[HCl]^+") \ No newline at end of file diff --git a/typst.toml b/typst.toml index 070b84c..b346531 100644 --- a/typst.toml +++ b/typst.toml @@ -1,6 +1,6 @@ [package] name = "typsium" -version = "0.2.0" +version = "0.3.0" repository = "https://github.com/Typsium/typsium" license = "MIT" entrypoint = "src/lib.typ" From 28f368bc0159b3d3599d25f1f34a4449c85ad8fb Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Fri, 30 May 2025 14:44:50 +0200 Subject: [PATCH 08/20] added nested content reconstruction --- ...se-content-intermediate-representation.typ | 112 ++++++------ src/utils.typ | 68 +++++++- tests/content-to-reaction/test.typ | 16 +- tests/main.typ | 161 ++++++++++++++++++ 4 files changed, 295 insertions(+), 62 deletions(-) create mode 100644 tests/main.typ diff --git a/src/parse-content-intermediate-representation.typ b/src/parse-content-intermediate-representation.typ index 5ceebc7..35665a1 100644 --- a/src/parse-content-intermediate-representation.typ +++ b/src/parse-content-intermediate-representation.typ @@ -1,4 +1,11 @@ -#import "utils.typ": get-all-children, is-metadata, typst-builtin-styled, typst-builtin-context, length +#import "utils.typ": ( + get-all-children, + is-metadata, + typst-builtin-styled, + typst-builtin-context, + length, + reconstruct-content-from-strings, +) #import "parse-formula-intermediate-representation.typ": patterns #import "utils.typ": arrow-string-to-kind, is-default, roman-to-number @@ -8,52 +15,49 @@ #import "model/group.typ": group #import "model/arrow.typ": arrow -#let get-count-and-charge(count1, count2, charge1, charge2, index, templates) = { +#let get-count-and-charge(count1, count2, charge1, charge2, full-string, templates, index) = { let radical = false let roman-charge = false let count = if not is-default(count1) { - templates.slice(index + if count1.contains("_"){1}, index + length(count1)).sum() + reconstruct-content-from-strings( + full-string, + templates, + start: index + if count1.contains("_") { 1 }, + end: index + length(count1), + ) + // templates.slice() } else if not is-default(count2) { - templates.slice(index + length(charge1) + if count2.contains("_"){1}, index + length(charge1) + length(count2)).sum() + reconstruct-content-from-strings( + full-string, + templates, + start: index + length(charge1) + if count2.contains("_") { 1 }, + end: index + length(charge1) + length(count2), + ) } else { none } let charge = if not is-default(charge1) { - templates.slice(index + if charge1.contains("^"){1}, index + length(charge1)).sum() + reconstruct-content-from-strings( + full-string, + templates, + start: index + if charge1.contains("^") { 1 }, + end: index + length(charge1), + ) } else if not is-default(charge2) { - templates.slice(index + length(count1) + if charge2.contains("^"){1}, index + length(count1) + length(charge2)).sum() + reconstruct-content-from-strings( + full-string, + templates, + start: index + length(count1) + if charge2.contains("^") { 1 }, + end: index + length(count1) + length(charge2), + ) } else { none } - - // if not is-default(charge) { - // if charge.contains(".") { - // charge = charge.replace(".", "") - // radical = true - // } - // if charge.contains("I") or charge.contains("V") { - // let multiplier = if charge.contains("-") { -1 } else { 1 } - // charge = charge.replace("-", "").replace("+", "") - // charge = roman-to-number(charge) * multiplier - // roman-charge = true - // } else if charge == "-" { - // charge = -1 - // } else if charge.contains("-") { - // charge = -int(charge.replace("-", "")) - // } else if charge == "+" { - // charge = 1 - // } else if charge.replace("+", "").contains(regex("^[0-9]+$")) { - // charge = int(charge.replace("+", "")) - // } else { - // charge = 0 - // } - // } - return (count, charge, radical, roman-charge) } -#let string-to-element(formula, templates, index) = { +#let string-to-element(formula, full-string, templates, index) = { let element-match = formula.match(patterns.element) if element-match == none { return (false,) @@ -65,8 +69,9 @@ element-match.captures.at(3), element-match.captures.at(2), element-match.captures.at(4), + full-string, + templates, index + symbol.len(), - templates ) let oxidation-number = none let roman-oxidation = true @@ -96,7 +101,12 @@ return ( true, element( - templates.slice(index, index + element-match.captures.at(0).len()).sum(), + reconstruct-content-from-strings( + full-string, + templates, + start: index, + end: index + element-match.captures.at(0).len(), + ), count: x.at(0), charge: x.at(1), radical: x.at(2), @@ -141,7 +151,7 @@ } full-reaction.push($&$) remaining = remaining.slice(1) - index+=1 + index += 1 continue } let math-result = string-to-math(remaining) @@ -152,11 +162,11 @@ random-content = "" full-reaction.push(math-result.at(1)) remaining = remaining.slice(math-result.at(2)) - index+=math-result.at(2) + index += math-result.at(2) continue } - let element = string-to-element(remaining, templates, index) + let element = string-to-element(remaining, reaction-string, templates, index) if element.at(0) { if not is-default(random-content) { if current-molecule-children.len() == 0 { @@ -168,7 +178,7 @@ random-content = "" current-molecule-children.push(element.at(1)) remaining = remaining.slice(element.at(2)) - index+= element.at(2) + index += element.at(2) continue } @@ -276,10 +286,6 @@ return full-reaction } - - - - #let create-full-string(children) = { let full-string = "" let templates = () @@ -288,16 +294,19 @@ let func-type = child.func() if child == [ ] { full-string += " " + templates.push(()) } else if func-type == text { full-string += child.text for value in child.text { - templates.push([#value]) + templates.push(()) } - // templates += child.text.map(x=> [#x]) } else if func-type == typst-builtin-styled { - let x = create-full-string(get-all-children(child.child)) - full-string += x.at(0) - templates.push(child) + let (inner-full-strings, inner-templates) = create-full-string(get-all-children(child.child)) + for value in range(inner-templates.len()) { + inner-templates.at(value).push(child) + } + full-string += inner-full-strings + templates += inner-templates } else if ( func-type in ( @@ -334,10 +343,15 @@ square, ) ) { - let x = create-full-string(get-all-children(child.body)) - full-string += x.at(0) - templates.push(child) + let (inner-full-strings, inner-templates) = create-full-string(get-all-children(child.body)) + for value in range(inner-templates.len()) { + inner-templates.at(value).push(child) + } + full-string += inner-full-strings + templates += inner-templates } else { + full-string += " " + templates.push((child,)) continue } } diff --git a/src/utils.typ b/src/utils.typ index ddaf047..1522ceb 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -170,14 +170,14 @@ show "8": "VIII" body } else { - // show "V": "5" - // show "I": "1" - // show "II": "2" - // show "III": "3" - // show "IV": "4" - // show "VI": "6" - // show "VII": "7" - // show "VIII": "8" + show "V": "5" + show "I": "1" + show "II": "2" + show "III": "3" + show "IV": "4" + show "VI": "6" + show "VII": "7" + show "VIII": "8" // highest priority is lowest, otherwise it will render VII as 511 body } } @@ -373,7 +373,6 @@ return element.value } } - #let reconstruct-content(template, body) = { if template == none or template == auto { return body @@ -442,6 +441,57 @@ return template.func()(body) } } +#let reconstruct-nested-content(templates, body) = { + let result = body + for template in templates { + result = reconstruct-content(template, result) + } + return result +} +#let templates-equal(a, b) = { + if a.func() != b.func(){ + return false + } + if a.func() == typst-builtin-styled{ + return true + } + for i in a.fields() { + if i.at(0) != "child" and i.at(0) != "text" and i.at(0) != "body"{ + if i.at(1) != b.at(i.at(0)){ + return false + } + } + } + return true +} +#let reconstruct-content-from-strings(strings, templates, start: 0, end: none) = { + if strings.len() == 1{ + return reconstruct-nested-content(templates.at(0), [#strings.at(0)]) + } + strings = strings.slice(start, end) + templates = templates.slice(start, end) + + let result = none + start = 0 + for i in range(1, templates.len()) { + let is-equal = templates.at(i).len() == templates.at(start).len() + if is-equal { + for j in range(0, templates.at(i).len()) { + if not templates-equal(templates.at(i).at(j), templates.at(start).at(j)){ + is-equal = false + } + } + } + if is-equal { + continue + } else { + result += reconstruct-nested-content(templates.at(start), [#strings.slice(start, i)]) + start = i + } + } + result += reconstruct-nested-content(templates.at(start), [#strings.slice(start, templates.len())]) + return result +} #let charge-to-content( charge, diff --git a/tests/content-to-reaction/test.typ b/tests/content-to-reaction/test.typ index 2574cd3..eadfc66 100644 --- a/tests/content-to-reaction/test.typ +++ b/tests/content-to-reaction/test.typ @@ -8,11 +8,19 @@ #set page(width: auto, height: auto, margin: 0.5em) -// #show: show-roman.with(roman: true) +// #show: show-roman.with(roman: false) + // #show: e.set_(element, affect-layout:true,roman-charge:false) -#ce[#text(red)[H]e_2#math.cancel[S]O4^5-]\ -// #ce[#text(red)[H]e_2#math.cancel[S]O4^IV]\ -#ce("He2SO4-5")\ + +#ce[#text(red)[He2]#math.cancel[S]O4^#math.cancel[5-]] + +// // #ce[#text(red)[H]e_2#math.cancel[S]O4^IV]\ +// #ce("He2SO4-5")\ +// +// #reconstruct-content-from-strings("Hello", ((text(red)[],), (text(red)[],), (text(red)[],), (text(blue)[],), (text(blue)[],))) +// #reconstruct-content-from-strings("Hello", ((math.cancel[],), (math.cancel[],), (math.cancel[],), (text(blue)[],), (text(blue)[],))) +// #reconstruct-nested-content(([Hello World], text(red)[h], math.cancel[h], underline[], math.overbrace[Hello][Hello])) + // #ce[12Fe2(SO4)3]\ // #ce("12Fe2(SO4)3")\ // #ce[514H2O]\ diff --git a/tests/main.typ b/tests/main.typ new file mode 100644 index 0000000..31611d6 --- /dev/null +++ b/tests/main.typ @@ -0,0 +1,161 @@ +// #import "/src/parse-content-intermediate-representation.typ": content-to-ir +// #import "/src/parse-formula-intermediate-representation.typ": string-to-ir +#import "/src/lib.typ": ce +#import "@preview/alchemist:0.1.4": * + +// #let ce(body) = display-ir(string-to-ir(body)) +// #let cem(body) = display-ir(content-to-ir(body)) + +#let alchemist-molecule = skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) +#set page(width: auto, height: auto, margin: 0.5em) + + + +// #cem[H2SO4 + H2O <=[H2O] OH- + #alchemist-molecule + #text("H2O",red)] +// $#cem[#text("H2O",red)]$ +// +// +// #let x = 3 +// #let x = x*5 +// #x + +// #ce[H2O] +// #linebreak() +#let x = ( + ( + type: "molecule", + children: ( + (type: "element", symbol: "H", count: 2, symbol-body:text(red)[H], count-body:math.cancel(angle:50deg)[2]), + (type: "element", symbol: "O"), + ), + ), +) +#let y = ( + ( + type: "molecule", + children: ( + (type: "element", symbol: "Na", symbol-body:strong[N]), + (type: "element", symbol: "H", count: 3, charge:2, count-body:text(green)[3], charge-body:text(red)[2+]), + ), + body:math.cancel(angle:90deg)[] + ), + (type: "+"), + ( + type: "molecule", + children: ( + (type: "element", symbol: "O",symbol-body:text("H",red)), + (type: "element", symbol: "H", charge:-1, symbol-body:text("H",blue)), + ), + body:math.overbrace[#text(red)[OH-]][Hydroxide-ion] + ), +) + +// #text(red)[Hello #text(blue)[World] ] +// #math.cancel(angle: 90deg)[Hello #math.attach("H", br:"ello", tr: "world") world] +// #display-ir(x)\ +// #display-ir(y) +// #let x = math.underbrace[2][Hello World] +// #math.attach("H", br:) +// #linebreak() +$ +#ce("AgCl + 2NH3 &<=> [Ag(NH3)2]+ + Cl-")\ +#ce("Co3^2- + H2O &<=> HCO3- + OH-")\ +#ce("Pb+2 + Co3-2 &-> PbCO3")\ +#ce("Pb+2 + 2OH- &-> Pb(OH)2")\ +#ce("2HClO &->[entwässern] H2O + Cl2O")\ +#ce("3ClO- &->[$Delta$][Disproportionierung] 2Cl- + ClO3- | ")\ +#ce("6KOH + 3Cl2 &->[][Disproportionierung] 5KCl- + KClO3 + H2O")\ +#ce("3HClO3 &->[][Disproportionierung] HClO4 + 2ClO2 + H2O")\ +#ce("6HCl^^+IVO2 + 3H2O &->[][Disproportionierung] 5HCl^^+VO3 + HCl^^-I")\ +#ce("ClO3- + H2SO4&-> HClO3 + HSO4-")\ +#ce("3HClO3 &->[H2SO4] HClO4 + H2O + ClO2 (gelb-grünes gas)")\ +#ce("2ClO2 &->[$Delta$][Explosionsartiger Zerfall] Cl2 + 2O2")\ +#ce("K+ + ClO4- &<=> KClO4")\ +#ce("8Fe(OH)2 + ClO4- + 4H2O &-> Fe(OH)3 + Cl-")\ +#ce("3Ti+3 + ClO4- + 12H2O &-> TiO+2 + Cl- + 8H3O+")\ +#ce("Br2 + 2OH- &-> BrO- + Br- + H2O")\ +#ce("3BrO- &->[$Delta$Raumtemperatur] 2Br- + BrO3-")\ +#ce("3IO- &->[$Delta$Tiefe Temp] 2I- + IO3-")\ +#ce("BrO3- + 5SO2 + 12H2O &-> Br2 + 5SO4-2 + 8H3O+")\ +#ce("Br2 + SO2 + 6H2O &-> 2Br- + 5SO4-3 + 4H3O+")\ +#ce("H2SO4 + 2BrO3- &-> 2HBrO3 + 2HSO4-")\ +#ce("2HBrO3 &->[H2SO4][-H2O] Br2O5")\ +#ce("Br2O5 &->2Br2 + 5O2")\ +#ce("S2- + 2HCl &->H2S + 2Cl")\ +#ce("H2S + Pb(OAc)2 &-> PBS + 2HOAc")\ +#ce("SO3-2 + 2H+ &-> H2O + SO2 (Schwefelpulvergeruch)")\ +#ce("2KHSO4 + SO3-2 &-> K2SO4 + So4-2 + H2O + SO2")\ +#ce("2KHSO4 + SO3-2 &-> K2SO4 + So4-2 + H2O + SO2")\ +#ce("4Zn + NO3- + 7OH- + 6H2O &-> NH3(g) + 4[Zn(OH4)]-2")\ +#ce("NH3 + H2O &<=> NH4+ + OH-")\ +#ce("NH3(g) + konz. HCl(g) &-> NH4Cl")\ +#ce("NO2- &-> HNO3 | 2HNO2 + O2 -> 2HNO3")\ +#ce("2HNO2 + CN2H4O(Harnstoff) &-> CO2 + 2N2")\ +#ce("HNO2 + Sulfonsäure &-> 2N2 + H2SO4")\ +#ce("[Fe(H2O)6]+2 + NO2- + 2H+&-> [Fe((H2O)6)]+3 + NO + H2O")\ +#ce("[Fe(H2O)6]+2 + NO &-> [Fe(H2O)5(NO)]+2 + H2O")\ +#ce("3[Fe(H2O)6]+2 + NO3- + 4H+ &-> [Fe(H2O)6]+3 + NO + 2H2O")\ +#ce("[Fe(H2O)6]+2 + NO &-> [Fe(H2O)5(NO)]+2 + H2O")\ +#ce("Sulfanilsäure + HNO2 + H+ &-> Diazoniumsalze (diazotierung)")\ +#ce("Zn+2 + 2H+ + NO3- &-> Zn+2 + H2O + NO2-")\ +#ce("HPO42- + 23H+ + 3NH4+ + 12MoO4 &-> (NH4)3[P(Mo3O10)4](aq) + 12H2O")\ +#ce("B(OH)3 + 3MeOH &->[konz. H2SO4] B(OMe)3 + 3H2O")\ +//HNO3 + (NH4)6Mo7O24*4H2O +$ + +$ + #ce("H2S &<=> H+ + HS- &&<=> 2H + S2-")\ + #ce("H2O + SO2 &<=> SO2(aq) &&<=> SO2*H2O &&<=> H2SO3")\ + #ce("H2O + SO3 &<=> H2SO4 &&<-> HSO4- &&<-> SO4-2")\ +$ +// #ce[H#text(red)[2]O] +// $#ce[#strike("H2SO4")]$ +// #linebreak() +// #ce[*Fe2* + #[H2O] ] +// #linebreak() +// #linebreak() +// $#ce[12Fe2(SO4)3]$ +// #linebreak() +// #linebreak() +// $#ce[514H2O]$ +// #linebreak() +// #linebreak() +// $#ce[9Fe(OH)3 + ]$ +// #linebreak() +// #linebreak() +// $cem("H2SO4" + "H2O" -/> "[H2O]" "OH-")$ \ No newline at end of file From b63c88d6340a55495b72906700a71762193cbcc8 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Fri, 30 May 2025 18:37:07 +0200 Subject: [PATCH 09/20] Fixed content parsing issues --- ...se-content-intermediate-representation.typ | 184 +++++++++++++++--- ...se-formula-intermediate-representation.typ | 4 +- src/utils.typ | 5 +- tests/content-to-reaction/ref/1.png | Bin 2935 -> 13175 bytes tests/content-to-reaction/test.typ | 69 ++++--- typst.toml | 3 +- 6 files changed, 210 insertions(+), 55 deletions(-) diff --git a/src/parse-content-intermediate-representation.typ b/src/parse-content-intermediate-representation.typ index 35665a1..2113d31 100644 --- a/src/parse-content-intermediate-representation.typ +++ b/src/parse-content-intermediate-representation.typ @@ -5,6 +5,7 @@ typst-builtin-context, length, reconstruct-content-from-strings, + reconstruct-nested-content ) #import "parse-formula-intermediate-representation.typ": patterns @@ -93,7 +94,7 @@ } if x.at(0) == none and x.at(1) == none and x.at(2) == false { - if formula.at(element-match.end + 1, default: "").match(regex("[a-z]")) != none { + if formula.at(element-match.end, default: "").match(regex("[a-z]")) != none { return (false,) } } @@ -140,26 +141,40 @@ let current-molecule-count = 1 let current-molecule-phase = none let current-molecule-charge = 0 - let random-content = "" + let random-content = 0 let index = 0 while remaining.len() > 0 { if remaining.at(0) == "&" { + //flush current molecule if current-molecule-children.len() > 0 { full-reaction.push(molecule(current-molecule-children)) current-molecule-children = () } + //end flush current molecule + full-reaction.push($&$) remaining = remaining.slice(1) index += 1 continue } + let math-result = string-to-math(remaining) if math-result.at(0) { - if not is-default(random-content) { - full-reaction.push([#random-content]) + //flush random content + if random-content != 0 { + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) + random-content = 0 } - random-content = "" + //end flush random content + full-reaction.push(math-result.at(1)) remaining = remaining.slice(math-result.at(2)) index += math-result.at(2) @@ -168,14 +183,30 @@ let element = string-to-element(remaining, reaction-string, templates, index) if element.at(0) { - if not is-default(random-content) { + //flush random content + if random-content != 0 { if current-molecule-children.len() == 0 { - full-reaction.push([#random-content]) + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) } else { - current-molecule-children.push([#random-content]) + current-molecule-children.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) } + random-content = 0 } - random-content = "" + //end flush random content current-molecule-children.push(element.at(1)) remaining = remaining.slice(element.at(2)) index += element.at(2) @@ -185,14 +216,30 @@ let group-match = remaining.match(patterns.group) if group-match != none { - if not is-default(random-content) { + //flush random content + if random-content != 0 { if current-molecule-children.len() == 0 { - full-reaction.push([#random-content]) + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) } else { - current-molecule-children.push([#random-content]) + current-molecule-children.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) } + random-content = 0 } - random-content = "" + //end flush random content let group-content = group-match.captures.at(0) let kind = if group-content.at(0) == "(" { @@ -215,11 +262,13 @@ current-molecule-children.push(group(group-children, kind: kind, count: x.at(0), charge: x.at(1))) remaining = remaining.slice(group-match.end) + index += group-match.end continue } let plus-match = remaining.match(patterns.reaction-plus) if plus-match != none { + //flush current molecule if current-molecule-children.len() > 0 { full-reaction.push( molecule( @@ -231,17 +280,31 @@ ) current-molecule-children = () } - if not is-default(random-content) { - full-reaction.push([#random-content]) + //end flush current molecule + + //flush random content + if random-content != 0 { + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) + random-content = 0 } - random-content = "" + //end flush random content + full-reaction.push([+]) remaining = remaining.slice(plus-match.end) + index += plus-match.end continue } let arrow-match = remaining.match(patterns.reaction-arrow) if arrow-match != none { + //flush current molecule if current-molecule-children.len() > 0 { full-reaction.push( molecule( @@ -253,34 +316,103 @@ ) current-molecule-children = () } - if not is-default(random-content) { - full-reaction.push([#random-content]) + //end flush current molecule + + //flush random content + if random-content != 0 { + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) + random-content = 0 } - random-content = "" + //end flush random content + let kind = arrow-string-to-kind(arrow-match.captures.at(0)) let top = () let bottom = () if arrow-match.captures.at(1) != none { - top = string-to-reaction(arrow-match.captures.at(1)) + top = string-to-reaction( + arrow-match.captures.at(1), + templates.slice( + index + arrow-match.captures.at(0).len() + 2, + count: arrow-match.captures.at(1).len() + 2, + ), + ) } if arrow-match.captures.at(2) != none { - bottom = string-to-reaction(arrow-match.captures.at(2)) + bottom = string-to-reaction( + arrow-match.captures.at(2), + templates.slice( + index + arrow-match.captures.at(0).len() + length(arrow-match.captures.at(1)) + 2 + 2, + count: arrow-match.captures.at(2).len() + 2, + ), + ) } full-reaction.push(arrow(kind: kind, top: top, bottom: bottom)) remaining = remaining.slice(arrow-match.end) + index += arrow-match.end continue } + let current-character = remaining.codepoints().at(0) + if (current-character == "#" and templates.at(index).len() != 0) { + //flush current molecule + if current-molecule-children.len() > 0 { + full-reaction.push( + molecule( + current-molecule-children, + count: current-molecule-count, + phase: current-molecule-phase, + charge: current-molecule-charge, + ), + ) + current-molecule-children = () + } + //end flush current molecule + + //flush random content + if random-content != 0 { + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: index - random-content, + end: index, + ), + ) + random-content = 0 + } + //end flush random content + + full-reaction.push(reconstruct-nested-content(templates.at(index).slice(1), templates.at(index).at(0))) + } else { + random-content += current-character.len() + } - random-content += remaining.codepoints().at(0) - remaining = remaining.slice(remaining.codepoints().at(0).len()) + remaining = remaining.slice(current-character.len()) + index += current-character.len() } + + //flush current molecule if current-molecule-children.len() != 0 { full-reaction.push( molecule(current-molecule-children, count: current-molecule-count, phase: current-molecule-phase), ) } - if not is-default(random-content) { - full-reaction.push([#random-content]) + //flush random content + if random-content != 0 { + full-reaction.push( + reconstruct-content-from-strings( + reaction-string, + templates, + start: reaction-string.len() - random-content, + end: reaction-string.len(), + ), + ) } return full-reaction @@ -350,7 +482,7 @@ full-string += inner-full-strings templates += inner-templates } else { - full-string += " " + full-string += "#" templates.push((child,)) continue } diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index 56f9e35..7f9d9a3 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -7,7 +7,7 @@ #let patterns = ( element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?P\^\^[+-]?[IViv]{1,3}|\^\^[+-]?\d+)?"), - group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P(?:\^?[+-]?\d?)\.?-?))?(?:(?P_?\d+)|(?P(?:\^?[+-]?\d?)\.?-?))?"), + group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P\^[+-]?\d+[+-]?|\^[+-]{1}|[+-]{1}\d?))?(?:(?P_?\d+)|(?P\^[+-]?\d+[+-]?|\^[+-]{1}|[+-]{1}\d?))?"), reaction-plus: regex("^(\s?\+\s?)"), reaction-arrow: regex("^\s?(<->|↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-\/>|<\/-)(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?\s?"), math: regex("^(\$[^$]*\$)"), @@ -92,7 +92,7 @@ } if x.at(0) == none and x.at(1) == none and x.at(2) == false { - if formula.at(element-match.end + 1, default: "").match(regex("[a-z]")) != none { + if formula.at(element-match.end, default: "").match(regex("[a-z]")) != none { return (false,) } } diff --git a/src/utils.typ b/src/utils.typ index 1522ceb..108fcec 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -382,7 +382,10 @@ if func == typst-builtin-styled { return template.func()(body, template.styles) - } // else if func in (emph, smallcaps, sub, super, box, block, hide, heading) { + } else if func == typst-builtin-context{ + template + } + // else if func in (emph, smallcaps, sub, super, box, block, hide, heading) { // return template.func()(body) // } else if ( diff --git a/tests/content-to-reaction/ref/1.png b/tests/content-to-reaction/ref/1.png index 0fb8034cd1ed74f1df66a04513d8542366dff69e..3eba1fdd9fb9422a525bda64455aa4fa57f810a2 100644 GIT binary patch literal 13175 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[H2SO4][Hello World] C + D")\ -// -// #ce("2[Fe(CN)6]^4+") -// #linebreak() -// #ce("3[Co(NH3)4]^2+") -// #linebreak() -// #ce("[FeCo(CN)4 (NH3)2]^5-") -// #linebreak() -// #ce("[Co(en)3]^3- + 3[HCl]^+") \ No newline at end of file +#let alchemist-molecule = skeletize({ + molecule(name: "A", "A") + single() + molecule("B") + branch({ + single(angle: 1) + molecule( + "W", + links: ( + "A": double(stroke: red), + ), + ) + single() + molecule(name: "X", "X") + }) + branch({ + single(angle: -1) + molecule("Y") + single() + molecule( + name: "Z", + "Z", + links: ( + "X": single(stroke: black + 3pt), + ), + ) + }) + single() + molecule( + "C", + links: ( + "X": cram-filled-left(fill: blue), + "Z": single(), + ), + ) +}) + +$ +#ce[H2SO4 ->H2O + #math.overbrace[#alchemist-molecule][Hello World]]\ +$ +#ce[#text(green)[He2]#math.cancel[S]O4^#text(blue)[#math.cancel[5]-]] + +#ce[A + B =>[PO4-3][Hello World] C + D]\ diff --git a/typst.toml b/typst.toml index b346531..f57525f 100644 --- a/typst.toml +++ b/typst.toml @@ -9,7 +9,8 @@ authors = [ "β-吲哚基丙氨酸 ", "Ants Aare Alamaa <@Ants-Aare>" ] +compiler = "0.13.1" description = "Typeset chemical formulas and reactions." categories = [ "text", "paper" ] disciplines = [ "education", "chemistry" ] -keywords = ["chemistry", "chemical", "reaction", "formula", "stochiometry", "oxidation", "equation", "electron", "isotope", "molecule","atom", "hazard", "precaution", "h and p", "mhchem"] \ No newline at end of file +keywords = ["chemistry", "chemical", "biotech","organic", "reaction", "formula", "stochiometry", "oxidation", "equation", "isotope", "molecule","atom", "mhchem"] \ No newline at end of file From 8d8e440c22cda92d3181661a9ab6337760890c1a Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Fri, 30 May 2025 19:35:44 +0200 Subject: [PATCH 10/20] Added molecule variable rendering --- src/display-intermediate-representation.typ | 145 ------------------ src/lib.typ | 13 +- src/model/align.typ | 0 ...se-content-intermediate-representation.typ | 21 ++- src/typing.typ | 28 ++-- tests/content-to-reaction/ref/1.png | Bin 13175 -> 15139 bytes tests/content-to-reaction/test.typ | 9 +- 7 files changed, 42 insertions(+), 174 deletions(-) delete mode 100644 src/display-intermediate-representation.typ delete mode 100644 src/model/align.typ diff --git a/src/display-intermediate-representation.typ b/src/display-intermediate-representation.typ deleted file mode 100644 index b066ac9..0000000 --- a/src/display-intermediate-representation.typ +++ /dev/null @@ -1,145 +0,0 @@ -#import "utils.typ": ( - try-at, - count-to-content, - charge-to-content, - get-bracket, - get-arrow, - phase-to-content, - typst-builtin-styled, - none-coalesce, - reconstruct-content, -) -#let display-element(data) = { - let isotope = data.at("isotope", default: none) - let symbol = data.symbol - let t = data.at("oxidation-number", default: none) - let tr = charge-to-content(data.at("charge", default: none), radical: data.at("radical", default: false)) - let br = count-to-content(data.at("count", default: none)) - let tl = try-at(isotope, "mass-number") - let bl = try-at(isotope, "atomic-number") - - symbol = reconstruct-content(data.at("symbol-body", default: none), symbol) - tr = reconstruct-content(data.at("charge-body", default: none), tr) - br = reconstruct-content(data.at("count-body", default: none), br) - - math.attach( - symbol, - t: t, - tr: tr, - br: br, - tl: tl, - bl: bl, - ) -} - -#let display-group(data) = { - let children = data.at("children", default: ()) - let kind = data.at("kind", default: 1) - let tr = charge-to-content(data.at("charge", default: none)) - let br = count-to-content(data.at("count", default: none)) - - tr = reconstruct-content(data.at("charge-body", default: none), tr) - br = reconstruct-content(data.at("count-body", default: none), br) - - let result = math.attach( - math.lr({ - get-bracket(kind, open: true) - for child in children { - if child.type == "content" { - child.body - } else if child.type == "element" { - display-element(child) - } else if data.type == "align" { - $&$ - } else if child.type == "group" { - display-group(child) - } - } - get-bracket(kind, open: false) - }), - tr: tr, - br: br, - ) - - return reconstruct-content(data.at("body", default: none), result) -} - -#let display-molecule(data) = { - count-to-content(data.at("count", default: none)) - - let result = math.attach( - [ - #let children = data.at("children", default: ()) - #for child in children { - if child.type == "content" { - child.body - } else if data.type == "align" { - $&$ - } else if child.type == "element" { - display-element(child) - } else if child.type == "group" { - display-group(child) - } - } - ], - tr: charge-to-content(data.at("charge", default: none)), - // br: phase-to-content(data.at("phase", default:none)), - ) - if data.at("phase", default: none) != none { - result += context { - text(phase-to-content(data.at("phase", default: none)), size: text.size * 0.75) - } - } - - return reconstruct-content(data.at("body", default: none), result) -} - -#let display-ir(data) = { - if data == none { - none - } else if type(data) == array { - for value in data { - display-ir(value) - //this removes spacing for groups that have long charges (looks better) - if value.type == "molecule" { - let last = value.children.last() - if ( - last.type == "group" and (last.at("charge", default: none) != none or last.at("count", default: none) != none) - ) { - h(-0.4em) - } - } - } - } else if type(data) == dictionary { - if data.type == "molecule" { - display-molecule(data) - } else if data.type == "+" { - h(0.4em, weak: true) - math.plus - h(0.4em, weak: true) - } else if data.type == "group" { - display-group(data) - } else if data.type == "element" { - display-element(data) - } else if data.type == "content" { - data.body - } else if data.type == "align" { - $&$ - } else if data.type == "arrow" { - h(0.4em, weak: true) - let top = display-ir(data.at("top", default: none)) - let bottom = display-ir(data.at("bottom", default: none)) - math.attach( - math.stretch( - get-arrow(data.at("kind", default: 0)), - size: 100% + 2em, - ), - t: top, - b: bottom, - ) - h(0.4em, weak: true) - } - } else if type(data) == content { - data - } -} diff --git a/src/lib.typ b/src/lib.typ index 8ade537..c40bdb0 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,12 +1,13 @@ #import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate -#import "display-shell-configuration.typ": ( - get-electron-configuration, - get-shell-configuration, - display-electron-configuration, -) -#import "model/reaction.typ": reaction +#import "display-shell-configuration.typ": get-electron-configuration,get-shell-configuration,display-electron-configuration, #import "parse-formula-intermediate-representation.typ": string-to-reaction #import "parse-content-intermediate-representation.typ": content-to-reaction +#import "typing.typ": set-arrow, set-element, set-group, set-molecule, set-reaction, elembic, fields, selector +#import "model/arrow.typ": arrow +#import "model/element.typ": element +#import "model/group.typ": group +#import "model/molecule.typ": molecule +#import "model/reaction.typ": reaction #let ce(formula) = { if type(formula) == str{ diff --git a/src/model/align.typ b/src/model/align.typ deleted file mode 100644 index e69de29..0000000 diff --git a/src/parse-content-intermediate-representation.typ b/src/parse-content-intermediate-representation.typ index 2113d31..8567409 100644 --- a/src/parse-content-intermediate-representation.typ +++ b/src/parse-content-intermediate-representation.typ @@ -5,11 +5,14 @@ typst-builtin-context, length, reconstruct-content-from-strings, - reconstruct-nested-content + reconstruct-nested-content, + is-kind, + arrow-string-to-kind, + is-default, + roman-to-number, ) #import "parse-formula-intermediate-representation.typ": patterns -#import "utils.typ": arrow-string-to-kind, is-default, roman-to-number #import "model/molecule.typ": molecule #import "model/reaction.typ": reaction #import "model/element.typ": element @@ -422,7 +425,19 @@ let full-string = "" let templates = () for child in children { - if type(child) == content { + if is-metadata(child) { + if is-kind(child, "molecule") { + full-string += child.value.formula + for value in child.value.formula { + templates.push(()) + } + } else if is-kind(child, "element") { + full-string += child.value.symbol + for value in child.value.symbol { + templates.push(()) + } + } + } else if type(child) == content { let func-type = child.func() if child == [ ] { full-string += " " diff --git a/src/typing.typ b/src/typing.typ index 5c0c6ff..bea7684 100644 --- a/src/typing.typ +++ b/src/typing.typ @@ -1,22 +1,14 @@ #import "libs/elembic/lib.typ" as e: selector -// #import "model/grid.typ": grid -// #import "model/label.typ": label -// #import "model/title.typ": title -// #import "model/legend.typ": legend -// #import "model/tick.typ": tick, tick-label -// #import "model/spine.typ": spine -// #import "model/diagram.typ": diagram -// #import "model/errorbar.typ": errorbar +#import "model/arrow.typ": arrow +#import "model/element.typ": element +#import "model/group.typ": group +#import "model/molecule.typ": molecule +#import "model/reaction.typ": reaction -#let set_ = e.set_ #let fields = e.fields #let elembic = e -// #let set-grid = e.set_.with(grid) -// #let set-title = e.set_.with(title) -// #let set-label = e.set_.with(label) -// #let set-legend = e.set_.with(legend) -// #let set-tick = e.set_.with(tick) -// #let set-tick-label = e.set_.with(tick-label) -// #let set-spine = e.set_.with(spine) -// #let set-diagram = e.set_.with(diagram) -// #let set-errorbar = e.set_.with(errorbar) \ No newline at end of file +#let set-arrow = e.set_.with(arrow) +#let set-element = e.set_.with(element) +#let set-group = e.set_.with(group) +#let set-molecule = e.set_.with(molecule) +#let set-reaction = e.set_.with(reaction) diff --git a/tests/content-to-reaction/ref/1.png b/tests/content-to-reaction/ref/1.png index 3eba1fdd9fb9422a525bda64455aa4fa57f810a2..9eee342eff5180961496fc72cb4ce8802e3d87b3 100644 GIT binary patch delta 13677 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 CbA}B7 diff --git a/tests/content-to-reaction/test.typ b/tests/content-to-reaction/test.typ index 179f18f..954e01a 100644 --- a/tests/content-to-reaction/test.typ +++ b/tests/content-to-reaction/test.typ @@ -1,10 +1,10 @@ -#import "../../src/lib.typ" : ce +#import "../../src/lib.typ" : ce, define-molecule, get-element #import "../../src/utils.typ" : * #import "../../src/libs/elembic/lib.typ" as e #import "../../src/model/group.typ":* #import "../../src/model/element.typ":* #import "../../src/parse-formula-intermediate-representation.typ": string-to-reaction, -#import "@preview/alchemist:0.1.5": * +#import "@preview/alchemist:0.1.4": * // #show: e.set_(group, grow-brackets:false, affect-layout:false) #set page(width: auto, height: auto, margin: 0.5em) @@ -57,3 +57,8 @@ $ #ce[#text(green)[He2]#math.cancel[S]O4^#text(blue)[#math.cancel[5]-]] #ce[A + B =>[PO4-3][Hello World] C + D]\ + +#let sulfuric-acid = define-molecule(formula: "H2SO4") +#let iron = get-element(symbol:"Fe") + +#ce[#sulfuric-acid + 2#iron] From 438c03cc4d51da35a11360e90784dcf4b8220e62 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Fri, 30 May 2025 20:19:43 +0200 Subject: [PATCH 11/20] fixed oxidation rendering --- src/data-model.typ | 2 -- src/regex.typ | 45 ------------------------------------- src/utils.typ | 30 ++++++++++++++----------- tests/show-rule/.gitignore | 4 ++++ tests/show-rule/ref/1.png | Bin 0 -> 51629 bytes tests/show-rule/test.typ | 9 ++++++++ 6 files changed, 30 insertions(+), 60 deletions(-) delete mode 100644 src/regex.typ create mode 100644 tests/show-rule/.gitignore create mode 100644 tests/show-rule/ref/1.png create mode 100644 tests/show-rule/test.typ diff --git a/src/data-model.typ b/src/data-model.typ index 68c9557..e1a2f10 100644 --- a/src/data-model.typ +++ b/src/data-model.typ @@ -11,8 +11,6 @@ get-molecule-dict, to-string, ) -#import "regex.typ": patterns - #let get-element( symbol: auto, atomic-number: auto, diff --git a/src/regex.typ b/src/regex.typ deleted file mode 100644 index 7758695..0000000 --- a/src/regex.typ +++ /dev/null @@ -1,45 +0,0 @@ -#let patterns = ( - // Match chemical elements with optional numbers (e.g., H2, Na, Fe3) - element: regex("^\s*?([A-Z][a-z]?)\s??(\d+(?:|[^\+|\-])*)?"), - - // Match brackets [] {} () with optional subscripts - bracket: regex("^\s*([\(\[\{\}\]\)])\s*?(\d+)?"), - - // Match ion charges (e.g., 2+, 3-, +) - charge: regex("^\s?\(?(\^?[0-9]?(\+|\-)|\^[0-9])\)?"), - - // Match physical states (s/l/g/aq) - state: regex("^\((s|l|g|aq|solid|liquid|gas|aqueous)\)"), - - // Match various types of reaction arrows with optional conditions in brackets - arrow: regex("^\s*(?:(<->|<==?>|-->|->|=|⇌|⇒|⇔)(?:\[([^\]]+)\])?|\[\])"), - - // Match plus signs between reactants/products - plus: regex("^\s\+\s?"), - - // Match heating conditions (Δ, heat, etc.) - heating: regex("^\s*(Δ|δ|Delta|delta|heat|fire|hot|heating)\s*"), - - // Match temperature specifications (e.g., T = 298K) - temperature: regex("^s*([Tt])\s*=\s*(\d+\.?\d*)\s*([K°C℃F])?"), - - // Match pressure specifications (e.g., P = 1atm) - pressure: regex("^\s*([Pp])\s*=\s*(\d+\.?\d*)\s*(atm|bar|Pa|kPa|mmHg)?"), - - // Match catalyst specifications - catalyst: regex("^\s*(cat|catalyst)\s*=?\s*([A-Za-z0-9\s]+)"), - - // Match general parameter assignments - parameter: regex("^\s*([A-Za-z0-9]+)\s*=?\s*([A-Za-z0-9\s]+)"), - - // Match commas separating conditions - comma: regex("^\s*,\s*"), - - // Match whitespace - whitespace: regex("^\s+"), - - // Match numerical values - number: regex("^\d+"), -) - -// === remove all non-regex related content === diff --git a/src/utils.typ b/src/utils.typ index 108fcec..9a6121b 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -181,15 +181,20 @@ body } } -#let oxidation-to-content(oxidation, roman: true) = { +#let oxidation-to-content( + oxidation, + roman: true, + negative-symbol: math.minus, + positive-symbol: math.plus, +) = { if oxidation == none { return none } else if type(oxidation) == int { let symbol = none if oxidation < 0 { - symbol = math.minus + symbol = negative-symbol } else if oxidation > 0 { - symbol = math.plus + symbol = positive-symbol } if roman { return [#symbol#roman-numerals.at(calc.abs(oxidation))] @@ -382,10 +387,9 @@ if func == typst-builtin-styled { return template.func()(body, template.styles) - } else if func == typst-builtin-context{ + } else if func == typst-builtin-context { template - } - // else if func in (emph, smallcaps, sub, super, box, block, hide, heading) { + } // else if func in (emph, smallcaps, sub, super, box, block, hide, heading) { // return template.func()(body) // } else if ( @@ -452,15 +456,15 @@ return result } #let templates-equal(a, b) = { - if a.func() != b.func(){ + if a.func() != b.func() { return false } - if a.func() == typst-builtin-styled{ + if a.func() == typst-builtin-styled { return true } for i in a.fields() { - if i.at(0) != "child" and i.at(0) != "text" and i.at(0) != "body"{ - if i.at(1) != b.at(i.at(0)){ + if i.at(0) != "child" and i.at(0) != "text" and i.at(0) != "body" { + if i.at(1) != b.at(i.at(0)) { return false } } @@ -468,7 +472,7 @@ return true } #let reconstruct-content-from-strings(strings, templates, start: 0, end: none) = { - if strings.len() == 1{ + if strings.len() == 1 { return reconstruct-nested-content(templates.at(0), [#strings.at(0)]) } strings = strings.slice(start, end) @@ -480,7 +484,7 @@ let is-equal = templates.at(i).len() == templates.at(start).len() if is-equal { for j in range(0, templates.at(i).len()) { - if not templates-equal(templates.at(i).at(j), templates.at(start).at(j)){ + if not templates-equal(templates.at(i).at(j), templates.at(start).at(j)) { is-equal = false } } @@ -505,7 +509,7 @@ positive-symbol: math.plus, ) = { if is-default(charge) { - none + [] } else if type(charge) == int { if radical { radical-symbol diff --git a/tests/show-rule/.gitignore b/tests/show-rule/.gitignore new file mode 100644 index 0000000..40223be --- /dev/null +++ b/tests/show-rule/.gitignore @@ -0,0 +1,4 @@ +# generated by tytanic, do not edit + +diff/** +out/** diff --git a/tests/show-rule/ref/1.png b/tests/show-rule/ref/1.png new file mode 100644 index 0000000000000000000000000000000000000000..3d033ac4ccfbcce1394df887984ca672cb59e78e GIT binary patch literal 51629 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 Cu^^0 + H2^^1O^^-2")\ +#show: set-element(roman-oxidation:true,)//affect-layout:false) +#ce("Cu-2^^2O^^-2 + H2^^0 &-> Cu^^0 + H2^^1O^^-2") \ No newline at end of file From a0eccfed1712150a3b2adb2a291311f3c6c2f941 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Fri, 30 May 2025 20:19:53 +0200 Subject: [PATCH 12/20] updated readme and makefile --- Makefile | 15 +++++++++++++++ README.md | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 9a8d15a..2cf8b4f 100644 --- a/Makefile +++ b/Makefile @@ -14,3 +14,18 @@ module: cp ./LICENSE $(TARGET_DIR)/ cp -r ./src/* $(TARGET_DIR)/src/ awk '{gsub("https://typst.app/universe/package/$(PACKAGE_NAME)", "https://github.com/Typsium/$(PACKAGE_NAME)");print}' ./README.md > $(TARGET_DIR)/README.md + + + +bump-minor: + @current_version=$$(grep '^version' typst.toml | awk -F ' = ' '{print $$2}' | tr -d '"'); \ + new_version=$$(echo $$current_version | awk -F. '{printf "%d.%d.%d", $$1, $$2+1, $$3}'); \ + sed -i '' "s|^version = .*|version = \"$$new_version\"|" typst.toml; \ + sed -i '' "s|@preview/typsium:$$current_version|@preview/typsium:$$new_version|" README.md; \ + echo "Version bumped to $$new_version" +bump-patch: + @current_version=$$(grep '^version' typst.toml | awk -F ' = ' '{print $$2}' | tr -d '"'); \ + new_version=$$(echo $$current_version | awk -F. '{printf "%d.%d.%d", $$1, $$2, $$3+1}'); \ + sed -i '' "s|^version = .*|version = \"$$new_version\"|" typst.toml; \ + sed -i '' "s|@preview/typsium:$$current_version|@preview/typsium:$$new_version|" README.md; \ + echo "Version bumped to $$new_version" \ No newline at end of file diff --git a/README.md b/README.md index 87a9881..81194e5 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,41 @@ [![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/Typsium/typsium/blob/main/LICENSE) ![User Manual](https://img.shields.io/badge/manual-.pdf-purple) -# Typst Chemical Formula Package +# Write beautiful chemical formulas and reactions with Typsium +## Usage +```typst +#import "@preview/typsium:0.3.0":* +``` +Enter your chemical formula or reaction into the `#ce"` method like this: +```typst +#ce("[Cu(H2O)4]^2+ + 4NH3 -> [Cu(NH3)4]^2+ + 4H2O") +``` +![result](https://raw.githubusercontent.com/Typsium/typsium/main/tests/README-graphic1/ref/1.png) -A Typst package for typesetting chemical formulas, currently working on inorganic. +You can also embed any kind of content into your chemical reactions like by using square brackets instead of a passing in a string. This will also apply any styling to the reaction. +```typst +#ce[...] +``` +![result2](https://raw.githubusercontent.com/Typsium/typsium/main/tests/README-graphic1/ref/1.png) -- Typeset chemical formulas with ease -- Reactions and equations, including reversible reactions -- Support for complex reaction conditions (e.g. temperature (T=), pressure (P=), etc.) +There are many different kinds of arrows to choose from. And you can add additional arguments to them (such as the top or bottom text) by adding square brackets. +```typst +//show arrows and how they look +``` -## Usage +The molecule parsing is flexible and allows many different ways of writing, so you can just copy paste in your formulas and they will probably work. Oxidation numbers can be added like this`^^`, radicals can be added like this`.` and hydration groups can be added like this`*`. -To use Typsium, you need to include the package in your document: -// update to newest typsium usage, add "#import "@preview/typsium:0.3.0": ce" when releasing 0.3.0 ```typst -#ce("[Cu(H2O)4] 2+ + 4NH3 -> [Cu(NH3)4] 2+ + 4H2O") +//examples +``` + +You can use many kinds of brackets. they will auto scale by default, but you can disable it with a show rule. +```typst +//brackets examples and grow-brackets show rule +``` +Inline formulas often need to be a bit more compact, for this purpose there is an `affect-layout` rule, which can be toggled on and off for each part of the reaction separately. +```typst +//brackets examples ``` -![result](https://raw.githubusercontent.com/Typsium/typsium/main/tests/README-graphic1/ref/formula-parser.svg) +You can use Typsium inside other packages and the styling will be consistent across the entire document. \ No newline at end of file From c1276fb82e1eebe3f665ed20936326b2efc3cd0e Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Sat, 31 May 2025 19:25:04 +0200 Subject: [PATCH 13/20] fixed oxidation number show rule --- src/lib.typ | 18 +++++--- src/model/element.typ | 7 ++- src/model/molecule.typ | 45 ++----------------- ...se-formula-intermediate-representation.typ | 38 +++++++++++----- src/utils.typ | 5 +++ tests/oxidation-numbers/test.typ | 36 +++++++-------- 6 files changed, 70 insertions(+), 79 deletions(-) diff --git a/src/lib.typ b/src/lib.typ index c40bdb0..98ec5f6 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -11,14 +11,18 @@ #let ce(formula) = { if type(formula) == str{ - reaction(string-to-reaction(formula)) + let result = string-to-reaction(formula) + if result.len() == 1{ + result.at(0) + } else { + reaction(result) + } } else if type(formula) == content{ - // formula - let r = content-to-reaction(formula) - if type(r) == content{ - r - } else{ - reaction(content-to-reaction(formula)) + let result = content-to-reaction(formula) + if result.len() == 1{ + result.at(0) + } else { + reaction(result) } } } diff --git a/src/model/element.typ b/src/model/element.typ index 854b771..b16a2a5 100644 --- a/src/model/element.typ +++ b/src/model/element.typ @@ -45,7 +45,12 @@ customizable-attach( base, - t: oxidation-to-content(it.oxidation, roman: it.roman-oxidation), + t: oxidation-to-content( + it.oxidation, + roman: it.roman-oxidation, + negative-symbol: it.negative-symbol, + positive-symbol: it.positive-symbol, + ), tr: charge-to-content( it.charge, radical: it.radical, diff --git a/src/model/molecule.typ b/src/model/molecule.typ index f3c6cea..da3304b 100644 --- a/src/model/molecule.typ +++ b/src/model/molecule.typ @@ -1,7 +1,6 @@ #import "../libs/elembic/lib.typ" as e #import "../utils.typ": ( count-to-content, - charge-to-content, is-default, customizable-attach, phase-to-content, @@ -9,53 +8,18 @@ #let molecule( count: 1, - charge: 0, phase: none, //TODO: add up and down arrows - phase-transition:0, + phase-transition: 0, affect-layout: true, ..children, ) = { } -#let display-molecule(data) = { - count-to-content(data.at("count", default: none)) - - let result = math.attach( - [ - #let children = data.at("children", default: ()) - #for child in children { - if child.type == "content" { - child.body - } else if data.type == "align" { - $&$ - } else if child.type == "element" { - display-element(child) - } else if child.type == "group" { - display-group(child) - } - } - ], - tr: charge-to-content(data.at("charge", default: none)), - // br: phase-to-content(data.at("phase", default:none)), - ) - if data.at("phase", default: none) != none { - result += context { - text(phase-to-content(data.at("phase", default: none)), size: text.size * 0.75) - } - } - - return reconstruct-content(data.at("body", default: none), result) -} - #let draw-molecule(it) = { let result = count-to-content(it.count) - result += customizable-attach( - for child in it.children { - child - }, - tr: charge-to-content(it.charge), - affect-layout: it.affect-layout, - ) + for child in it.children { + result += child + } if not is-default(it.phase) { result += context { text(phase-to-content(it.phase), size: text.size * 0.75) @@ -74,7 +38,6 @@ // e.field("children", e.types.any, required: true), e.field("children", e.types.array(content), required: true), e.field("count", e.types.union(int, content), default: 1), - e.field("charge", e.types.union(int, content), default: 0), e.field("phase", e.types.union(str, content), default: none), e.field("affect-layout", bool, default: true), ), diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index 7f9d9a3..acf6aa1 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -39,7 +39,7 @@ charge = charge.replace(".", "") radical = true } - if charge.contains("I") or charge.contains("V"){ + if charge.contains("I") or charge.contains("V") { let multiplier = if charge.contains("-") { -1 } else { 1 } charge = charge.replace("-", "").replace("+", "") charge = roman-to-number(charge) * multiplier @@ -99,15 +99,24 @@ return ( true, - element( - element-match.captures.at(0), - count: x.at(0), - charge: x.at(1), - radical: x.at(2), - oxidation: oxidation-number, - roman-oxidation: roman-oxidation, - roman-charge: x.at(3), - ), + if x.at(3) { + element( + element-match.captures.at(0), + count: x.at(0), + charge: x.at(1), + radical: x.at(2), + oxidation: oxidation-number, + roman-charge: true, + ) + } else { + element( + element-match.captures.at(0), + count: x.at(0), + charge: x.at(1), + radical: x.at(2), + oxidation: oxidation-number, + ) + }, element-match.end, ) } @@ -130,7 +139,7 @@ } let full-reaction = () let current-molecule-children = () - let current-molecule-count = 1 + let current-molecule-count = "" let current-molecule-phase = none let current-molecule-charge = 0 let random-content = "" @@ -265,12 +274,17 @@ } if current-molecule-children.len() != 0 { full-reaction.push( - molecule(current-molecule-children, count: current-molecule-count, phase: current-molecule-phase), + molecule( + current-molecule-children, + count: current-molecule-count, + phase: current-molecule-phase, + ), ) } if not is-default(random-content) { full-reaction.push([#random-content]) } + return full-reaction } diff --git a/src/utils.typ b/src/utils.typ index 9a6121b..d8751a6 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -516,6 +516,11 @@ } if roman { roman-numerals.at(calc.abs(charge)) + if charge < 0 { + negative-symbol + } else if charge > 0 { + positive-symbol + } } else { if charge < 0 { if calc.abs(charge) > 1 { diff --git a/tests/oxidation-numbers/test.typ b/tests/oxidation-numbers/test.typ index a92aa91..afdd013 100644 --- a/tests/oxidation-numbers/test.typ +++ b/tests/oxidation-numbers/test.typ @@ -1,24 +1,24 @@ -#import "../../src/lib.typ" : ce -#import "../../src/libs/elembic/lib.typ" as e -#import "../../src/model/element.typ":* +#import "../../src/lib.typ": * +// #import "../../src/libs/elembic/lib.typ" as e +#import "../../src/model/element.typ": * #set page(width: auto, height: auto, margin: 0.5em) -#ce("O^^-ii") -#ce("S^^+VI") -#ce("C^^+4") -#ce("H^^+1") +// #ce("O^^-ii") +// #ce("S^^+VI") +// #ce("C^^+4") +// #ce("H^^+1") -#show: e.set_(element, roman-oxidation:false) -#ce("O^^-2") -#ce("S^^+6") -#ce("C^^+IV") -#ce("H^^+I") +// #show: set-element(roman-oxidation: false) +// #ce("O^^-2") +// #ce("S^^+6") +// #ce("C^^+IV") +// #ce("H^^+I") -#ce("O^1^^-2") -#ce("S^2^^+6") -#show: e.set_(element, roman-oxidation:true) -#ce("C^-2^^+4") +// #show: set-element(roman-oxidation: true) +// #ce("O^1^^-2") +// #ce("S^2^^+6") +#ce("2H^.2-^^+1") -#ce("H^.-^^+1") -#ce("H_3^^+IO+^^-2") +// #ce("C^-2^^+4") +// #ce("H_3^^+IO+^^-2") From 33f24e7937f28389bc169dec4c570979f0854588 Mon Sep 17 00:00:00 2001 From: Ants-Aare Date: Sat, 31 May 2025 19:29:34 +0200 Subject: [PATCH 14/20] renamed model files --- src/data-model.typ | 230 ----------------- src/display-shell-configuration.typ | 76 ------ src/lib.typ | 14 +- src/model/{arrow.typ => arrow-element.typ} | 2 +- .../{element.typ => element-element.typ} | 2 +- src/model/element-variable.typ | 235 ++++++++++++++++++ src/model/{group.typ => group-element.typ} | 20 +- .../{molecule.typ => molecule-element.typ} | 17 +- src/model/molecule-variable.typ | 118 +++++++++ .../{reaction.typ => reaction-element.typ} | 5 +- ...se-content-intermediate-representation.typ | 10 +- ...se-formula-intermediate-representation.typ | 10 +- src/typing.typ | 10 +- tests/brackets/test.typ | 2 +- tests/content-to-reaction/test.typ | 4 +- .../test.typ | 6 +- tests/oxidation-numbers/test.typ | 2 +- 17 files changed, 390 insertions(+), 373 deletions(-) delete mode 100644 src/data-model.typ delete mode 100644 src/display-shell-configuration.typ rename src/model/{arrow.typ => arrow-element.typ} (94%) rename src/model/{element.typ => element-element.typ} (98%) create mode 100644 src/model/element-variable.typ rename src/model/{group.typ => group-element.typ} (64%) rename src/model/{molecule.typ => molecule-element.typ} (59%) create mode 100644 src/model/molecule-variable.typ rename src/model/{reaction.typ => reaction-element.typ} (93%) diff --git a/src/data-model.typ b/src/data-model.typ deleted file mode 100644 index e1a2f10..0000000 --- a/src/data-model.typ +++ /dev/null @@ -1,230 +0,0 @@ -#import "utils.typ": ( - is-sequence, - is-kind, - is-heading, - is-metadata, - padright, - get-all-children, - hydrates, - elements, - get-element-dict, - get-molecule-dict, - to-string, -) -#let get-element( - symbol: auto, - atomic-number: auto, - common-name: auto, - cas: auto, -) = { - let element = if symbol != auto { - elements.find(x => x.symbol == symbol) - } else if atomic-number != auto { - elements.find(x => x.atomic-number == atomic-number) - } else if common-name != auto { - elements.find(x => x.common-name == common-name) - } else if cas != auto { - elements.find(x => x.cas == cas) - } - return metadata(element) -} - -#let validate-element(element) = { - let type = type(element) - if type == str { - if element.len() > 2 { - return get-element(common-name: element) - } else { - return get-element(symbol: element) - } - } else if type == int { - return get-element(atomic-number: element) - } else if type == content { - return get-element-dict(element) - } else if type == dictionary { - return element - } -} - -//TODO: properly parse bracket contents -// maybe recursively with a bracket regex, passing in the bracket content and multiplier(?) -//TODO: Properly apply stochiometry -#let get-element-counts(molecule) = { - let found-elements = (:) - let remaining = molecule.trim() - while remaining.len() > 0 { - let match = remaining.match(patterns.element) - if match != none { - remaining = remaining.slice(match.end) - let element = match.captures.at(0) - let count = 1 //int(if match.captures.at(1, default: "") == "" {1} else{match.captures.at(1)}) - let current = found-elements.at(element, default: 0) - found-elements.insert(element, count) - } else { - let char-len = remaining.codepoints().at(0).len() - - remaining = remaining.slice(char-len) - } - } - return found-elements -} - -#let get-weight(molecule) = { - let element = get-element-dict(molecule) - molecule = get-molecule-dict(molecule) - if type(element) == dictionary and element.at("atomic-weight", default: none) != none { - return element.atomic-weight - } - let weight = 0 - for value in molecule.elements { - let element = elements.find(x => x.symbol == value.at(0)) - - weight += element.atomic-weight * value.at(1) - } - return weight -} - -#let define-ion( - element, - charge: 0, - delta: 0, - override-common-name: none, - override-iupac-name: none, - override-CAS: none, - override-h-p: none, - override-ghs: none, - validate: true, -) = { - if validate { - element = validate-element(element) - } - element = if charge != 0 { - element.charge = charge - } else { - element.charge = element.at("charge", default: 0) + delta - } - return element -} - -#let define-isotope( - element, - mass-number, - override-atomic-weight: none, - override-common-name: none, - override-iupac-name: none, - override-cas: none, - override-h-p: none, - override-ghs: none, - validate: true, -) = { - if validate { - element = validate-element(element) - } - - element.mass-number = mass-number - if override-atomic-weight != none { - element.atomic-weight = override-atomic-weight - } - if override-common-name != none { - element.common-name = override-common-name - } - if override-iupac-name != none { - element.iupac-name = override-iupac-name - } - if override-cas != none { - element.override-cas = override-cas - } - if override-common-name != none { - element.common-name = override-common-name - } - if override-common-name != none { - element.common-name = override-common-name - } - if override-common-name != none { - element.common-name = override-common-name - } - return element -} - -#let define-molecule( - common-name: none, - iupac-name: none, - formula: "", - smiles: "", - inchi: "", - cas: "", - h-p: (), - ghs: (), - validate: true, -) = { - let found-elements - if validate { - // TODO: continue to add more validation as we go - // things should fail here instead of causing errors down the line - if common-name == none { - common-name = formula - } - - if smiles == "" { - smiles == none - } else if formula == "" { - //TODO: actually calculate the formula based on the smiles code (don't forget to add H on Carbon atoms) - formula = smiles - } - - if cas == "" { - cas = none - } - - found-elements = get-element-counts(formula) - - if inchi != "" { - // TODO: create InChI keys from provided InChI: - // https://typst.app/universe/package/jumble - // https://www.inchi-trust.org/download/104/InChI_TechMan.pdf - } else { - inchi = none - } - } - - return metadata(( - kind: "molecule", - common-name: common-name, - iupac-name: iupac-name, - formula: formula, - smiles: smiles, - inchi: inchi, - cas: cas, - h-p: h-p, - ghs: ghs, - elements: found-elements, - )) -} - -#let define-hydrate( - molecule, - amount: 1, - override-common-name: none, - override-iupac-name: none, - override-smiles: none, - override-inchi: none, - override-cas: none, - override-h-p: none, - override-ghs: none, -) = { - molecule = get-molecule-dict(molecule) - define-molecule( - common-name: if override-common-name != none { override-common-name } else { - molecule.common-name + sym.space + hydrates.at(amount) - }, - iupac-name: if override-iupac-name != none { override-iupac-name } else { - molecule.iupac-name + sym.semi + hydrates.at(amount) - }, - formula: molecule.formula + sym.space.narrow + sym.dot + sym.space.narrow + str(amount) + "H2O", - smiles: if override-smiles != none { override-smiles } else { molecule.smiles }, - inchi: if override-inchi != none { override-inchi } else { molecule.inchi }, - cas: if override-cas != none { override-cas } else { molecule.cas }, - h-p: if override-h-p != none { override-h-p } else { molecule.h-p }, - ghs: if override-ghs != none { override-ghs } else { molecule.ghs }, - ) -} \ No newline at end of file diff --git a/src/display-shell-configuration.typ b/src/display-shell-configuration.typ deleted file mode 100644 index a78cdf3..0000000 --- a/src/display-shell-configuration.typ +++ /dev/null @@ -1,76 +0,0 @@ -#import "utils.typ": get-element-dict, shell-capacities, orbital-capacities - -#let get-shell-configuration(element) = { - element = get-element-dict(element) - let charge = element.at("charge", default: 0) - let electron-amount = element.atomic-number - charge - - let result = () - for value in shell-capacities { - if electron-amount <= 0 { - break - } - - if electron-amount >= value.at(1) { - result.push(value) - electron-amount -= value.at(1) - } else { - result.push((value.at(0), electron-amount)) - electron-amount = 0 - } - } - return result -} - -//TODO: fix Cr and Mo -#let get-electron-configuration(element) = { - element = get-element-dict(element) - let charge = element.at("charge", default: 0) - let electron-amount = element.atomic-number - charge - - let result = () - for value in orbital-capacities { - if electron-amount <= 0 { - break - } - if electron-amount >= value.at(1) { - result.push(value) - electron-amount -= value.at(1) - } else { - result.push((value.at(0), electron-amount)) - electron-amount = 0 - } - } - return result -} - -#let display-electron-configuration(element, short: false) = { - let configuration = get-electron-configuration(element) - - if short { - let prefix = "" - if configuration.at(14, default: (0, 0)).at(1) == 6 { - configuration = configuration.slice(15) - prefix = "[Rn]" - } else if configuration.at(10, default: (0, 0)).at(1) == 6 { - configuration = configuration.slice(11) - prefix = "[Xe]" - } else if configuration.at(7, default: (0, 0)).at(1) == 6 { - configuration = configuration.slice(8) - prefix = "[Kr]" - } else if configuration.at(4, default: (0, 0)).at(1) == 6 { - configuration = configuration.slice(5) - prefix = "[Ar]" - } else if configuration.at(2, default: (0, 0)).at(1) == 6 { - configuration = configuration.slice(3) - prefix = "[Ne]" - } else if configuration.at(0, default: (0, 0)).at(1) == 2 { - configuration = configuration.slice(1) - prefix = "[He]" - } - - return prefix + configuration.map(x => $#x.at(0)^#str(x.at(1))$).sum() - } else { - return configuration.map(x => $#x.at(0)^#str(x.at(1))$).sum() - } -} diff --git a/src/lib.typ b/src/lib.typ index 98ec5f6..ba07897 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,13 +1,13 @@ -#import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate -#import "display-shell-configuration.typ": get-electron-configuration,get-shell-configuration,display-electron-configuration, +// #import "data-model.typ": get-element-counts, get-element, get-weight, define-molecule, define-hydrate +// #import "display-shell-configuration.typ": get-electron-configuration,get-shell-configuration,display-electron-configuration, #import "parse-formula-intermediate-representation.typ": string-to-reaction #import "parse-content-intermediate-representation.typ": content-to-reaction #import "typing.typ": set-arrow, set-element, set-group, set-molecule, set-reaction, elembic, fields, selector -#import "model/arrow.typ": arrow -#import "model/element.typ": element -#import "model/group.typ": group -#import "model/molecule.typ": molecule -#import "model/reaction.typ": reaction +#import "model/arrow-element.typ": arrow +#import "model/element-element.typ": element +#import "model/group-element.typ": group +#import "model/molecule-element.typ": molecule +#import "model/reaction-element.typ": reaction #let ce(formula) = { if type(formula) == str{ diff --git a/src/model/arrow.typ b/src/model/arrow-element.typ similarity index 94% rename from src/model/arrow.typ rename to src/model/arrow-element.typ index 3a92864..61ba41c 100644 --- a/src/model/arrow.typ +++ b/src/model/arrow-element.typ @@ -26,7 +26,7 @@ #let arrow = e.element.declare( "arrow", - prefix: "typsium", + prefix: "@preview/typsium:0.3.0", display: draw-arrow, diff --git a/src/model/element.typ b/src/model/element-element.typ similarity index 98% rename from src/model/element.typ rename to src/model/element-element.typ index b16a2a5..b66ab19 100644 --- a/src/model/element.typ +++ b/src/model/element-element.typ @@ -69,7 +69,7 @@ #let element = e.element.declare( "element", - prefix: "typsium", + prefix: "@preview/typsium:0.3.0", display: draw-element, diff --git a/src/model/element-variable.typ b/src/model/element-variable.typ new file mode 100644 index 0000000..d6e321c --- /dev/null +++ b/src/model/element-variable.typ @@ -0,0 +1,235 @@ +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": ( + // is-sequence, + // is-kind, + // is-heading, + // is-metadata, + // padright, + // get-all-children, + // hydrates, + // elements, + // get-element-dict, + // get-molecule-dict, + // to-string, +) + + + +#let element-variable = e.types.declare( + "element-variable", + prefix: "@preview/typsium:0.3.0", + fields: ( + e.field("symbol", str, doc: "The symbol in the periodic table. For example: H, He, Li,..."), + e.field("common-name", str, doc: "The name of the element."), + e.field("atomic-number", int, doc: "Atomic number (Z) of the element / number of protons."), + e.field("most-common-isotope",int,doc: "Mass number (A) of the most common isotope / number of protons + neutrons."), + e.field("group", int, doc: "The column of the element inside the periodic table. ranges 1-18."), + e.field("period", int, doc: "The period of the element inside the periodic table."), + e.field("block", int, doc: "Is the element s-block, f-block, d-block or p-block?"), + e.field("atomic-weight", int, doc: "The average of the weights of all isotopes of the element."), + e.field("covalent-radius", int, doc: "Covalent radius of the element."), + e.field("van-der-waal-radius", int, doc: "Van der Waals radius of the element."), + e.field("outshell-electrons", int, doc: "The number of electrons in the outermost shell."), + e.field("density", int, doc: "The density of the pure element in kg/m^3"), + e.field("melting-point", int, doc: "The melting point of the element under standard conditions."), + e.field("boiling-point", int, doc: "The boiling point of the element under standard conditions."), + e.field("electronegativity", int, doc: "The Electronegativify of the element."), + e.field("phase", int, doc: "The boiling point of the element under standard conditions."), + e.field("cas", int, doc: "The CAS number of the pure element"), + ), +) + + +#let get-element( + symbol: auto, + atomic-number: auto, + common-name: auto, + cas: auto, +) = { + let element = if symbol != auto { + elements.find(x => x.symbol == symbol) + } else if atomic-number != auto { + elements.find(x => x.atomic-number == atomic-number) + } else if common-name != auto { + elements.find(x => x.common-name == common-name) + } else if cas != auto { + elements.find(x => x.cas == cas) + } + return metadata(element) +} + +#let validate-element(element) = { + let type = type(element) + if type == str { + if element.len() > 2 { + return get-element(common-name: element) + } else { + return get-element(symbol: element) + } + } else if type == int { + return get-element(atomic-number: element) + } else if type == content { + return get-element-dict(element) + } else if type == dictionary { + return element + } +} + +//TODO: properly parse bracket contents +// maybe recursively with a bracket regex, passing in the bracket content and multiplier(?) +//TODO: Properly apply stochiometry +#let get-element-counts(molecule) = { + let found-elements = (:) + let remaining = molecule.trim() + while remaining.len() > 0 { + let match = remaining.match(patterns.element) + if match != none { + remaining = remaining.slice(match.end) + let element = match.captures.at(0) + let count = 1 //int(if match.captures.at(1, default: "") == "" {1} else{match.captures.at(1)}) + let current = found-elements.at(element, default: 0) + found-elements.insert(element, count) + } else { + let char-len = remaining.codepoints().at(0).len() + + remaining = remaining.slice(char-len) + } + } + return found-elements +} + +#let define-ion( + element, + charge: 0, + delta: 0, + override-common-name: none, + override-iupac-name: none, + override-CAS: none, + override-h-p: none, + override-ghs: none, + validate: true, +) = { + if validate { + element = validate-element(element) + } + element = if charge != 0 { + element.charge = charge + } else { + element.charge = element.at("charge", default: 0) + delta + } + return element +} + +#let define-isotope( + element, + mass-number, + override-atomic-weight: none, + override-common-name: none, + override-iupac-name: none, + override-cas: none, + override-h-p: none, + override-ghs: none, + validate: true, +) = { + if validate { + element = validate-element(element) + } + + element.mass-number = mass-number + if override-atomic-weight != none { + element.atomic-weight = override-atomic-weight + } + if override-common-name != none { + element.common-name = override-common-name + } + if override-iupac-name != none { + element.iupac-name = override-iupac-name + } + if override-cas != none { + element.override-cas = override-cas + } + if override-common-name != none { + element.common-name = override-common-name + } + if override-common-name != none { + element.common-name = override-common-name + } + if override-common-name != none { + element.common-name = override-common-name + } + return element +} + +#let get-shell-configuration(element) = { + element = get-element-dict(element) + let charge = element.at("charge", default: 0) + let electron-amount = element.atomic-number - charge + + let result = () + for value in shell-capacities { + if electron-amount <= 0 { + break + } + + if electron-amount >= value.at(1) { + result.push(value) + electron-amount -= value.at(1) + } else { + result.push((value.at(0), electron-amount)) + electron-amount = 0 + } + } + return result +} + +//TODO: fix Cr and Mo +#let get-electron-configuration(element) = { + element = get-element-dict(element) + let charge = element.at("charge", default: 0) + let electron-amount = element.atomic-number - charge + + let result = () + for value in orbital-capacities { + if electron-amount <= 0 { + break + } + if electron-amount >= value.at(1) { + result.push(value) + electron-amount -= value.at(1) + } else { + result.push((value.at(0), electron-amount)) + electron-amount = 0 + } + } + return result +} + +#let display-electron-configuration(element, short: false) = { + let configuration = get-electron-configuration(element) + if short { + let prefix = "" + if configuration.at(14, default: (0, 0)).at(1) == 6 { + configuration = configuration.slice(15) + prefix = "[Rn]" + } else if configuration.at(10, default: (0, 0)).at(1) == 6 { + configuration = configuration.slice(11) + prefix = "[Xe]" + } else if configuration.at(7, default: (0, 0)).at(1) == 6 { + configuration = configuration.slice(8) + prefix = "[Kr]" + } else if configuration.at(4, default: (0, 0)).at(1) == 6 { + configuration = configuration.slice(5) + prefix = "[Ar]" + } else if configuration.at(2, default: (0, 0)).at(1) == 6 { + configuration = configuration.slice(3) + prefix = "[Ne]" + } else if configuration.at(0, default: (0, 0)).at(1) == 2 { + configuration = configuration.slice(1) + prefix = "[He]" + } + + return prefix + configuration.map(x => $#x.at(0)^#str(x.at(1))$).sum() + } else { + return configuration.map(x => $#x.at(0)^#str(x.at(1))$).sum() + } +} \ No newline at end of file diff --git a/src/model/group.typ b/src/model/group-element.typ similarity index 64% rename from src/model/group.typ rename to src/model/group-element.typ index a3b7d20..7916f96 100644 --- a/src/model/group.typ +++ b/src/model/group-element.typ @@ -34,7 +34,7 @@ }, tr: charge-to-content(it.charge), br: count-to-content(it.count), - affect-layout: it.affect-layout + affect-layout: it.affect-layout, ) return result @@ -44,12 +44,11 @@ #let group = e.element.declare( "group", - prefix: "typsium", + prefix: "@preview/typsium:0.3.0", display: draw-group, fields: ( - // e.field("children", e.types.any, required: true), e.field("children", e.types.array(content), required: true), e.field("kind", int, default: 0), e.field("count", e.types.union(int, content), default: 1), @@ -57,20 +56,5 @@ e.field("grow-brackets", bool, default: true), e.field("affect-layout", bool, default: true), ), - // parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => { - // let args = if include-required { - // let values = args.pos() - // arguments(values, ..args.named()) - // } else if args.pos() == () { - // args - // } else { - // assert( - // false, - // message: "element 'diagram': unexpected positional arguments\n hint: these can only be passed to the constructor", - // ) - // } - - // default-parser(args, include-required: include-required) - // }, ) diff --git a/src/model/molecule.typ b/src/model/molecule-element.typ similarity index 59% rename from src/model/molecule.typ rename to src/model/molecule-element.typ index da3304b..b850502 100644 --- a/src/model/molecule.typ +++ b/src/model/molecule-element.typ @@ -30,29 +30,14 @@ #let molecule = e.element.declare( "molecule", - prefix: "typsium", + prefix: "@preview/typsium:0.3.0", display: draw-molecule, fields: ( - // e.field("children", e.types.any, required: true), e.field("children", e.types.array(content), required: true), e.field("count", e.types.union(int, content), default: 1), e.field("phase", e.types.union(str, content), default: none), e.field("affect-layout", bool, default: true), ), - // parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => { - // let args = if include-required { - // let values = args.pos() - // arguments(values, ..args.named()) - // } else if args.pos() == () { - // args - // } else { - // assert( - // false, - // message: "element 'diagram': unexpected positional arguments\n hint: these can only be passed to the constructor", - // ) - // } - // default-parser(args, include-required: include-required) - // }, ) diff --git a/src/model/molecule-variable.typ b/src/model/molecule-variable.typ new file mode 100644 index 0000000..4d31591 --- /dev/null +++ b/src/model/molecule-variable.typ @@ -0,0 +1,118 @@ +#import "../libs/elembic/lib.typ" as e +#import "../utils.typ": ( + // is-sequence, + // is-kind, + // is-heading, + // is-metadata, + // padright, + // get-all-children, + // hydrates, + // elements, + // get-element-dict, + // get-molecule-dict, + // to-string, +) + +#let element-variable = e.types.declare( + "molecule-variable", + prefix: "@preview/typsium:0.3.0", + fields: () +) + +#let get-weight(molecule) = { + let element = get-element-dict(molecule) + molecule = get-molecule-dict(molecule) + if type(element) == dictionary and element.at("atomic-weight", default: none) != none { + return element.atomic-weight + } + let weight = 0 + for value in molecule.elements { + let element = elements.find(x => x.symbol == value.at(0)) + + weight += element.atomic-weight * value.at(1) + } + return weight +} + +#let define-molecule( + common-name: none, + iupac-name: none, + formula: "", + smiles: "", + inchi: "", + cas: "", + h-p: (), + ghs: (), + validate: true, +) = { + let found-elements + if validate { + // TODO: continue to add more validation as we go + // things should fail here instead of causing errors down the line + if common-name == none { + common-name = formula + } + + if smiles == "" { + smiles == none + } else if formula == "" { + //TODO: actually calculate the formula based on the smiles code (don't forget to add H on Carbon atoms) + formula = smiles + } + + if cas == "" { + cas = none + } + + found-elements = get-element-counts(formula) + + if inchi != "" { + // TODO: create InChI keys from provided InChI: + // https://typst.app/universe/package/jumble + // https://www.inchi-trust.org/download/104/InChI_TechMan.pdf + } else { + inchi = none + } + } + + return metadata(( + kind: "molecule", + common-name: common-name, + iupac-name: iupac-name, + formula: formula, + smiles: smiles, + inchi: inchi, + cas: cas, + h-p: h-p, + ghs: ghs, + elements: found-elements, + )) +} + +#let define-hydrate( + molecule, + amount: 1, + override-common-name: none, + override-iupac-name: none, + override-smiles: none, + override-inchi: none, + override-cas: none, + override-h-p: none, + override-ghs: none, +) = { + molecule = get-molecule-dict(molecule) + define-molecule( + common-name: if override-common-name != none { override-common-name } else { + molecule.common-name + sym.space + hydrates.at(amount) + }, + iupac-name: if override-iupac-name != none { override-iupac-name } else { + molecule.iupac-name + sym.semi + hydrates.at(amount) + }, + formula: molecule.formula + sym.space.narrow + sym.dot + sym.space.narrow + str(amount) + "H2O", + smiles: if override-smiles != none { override-smiles } else { molecule.smiles }, + inchi: if override-inchi != none { override-inchi } else { molecule.inchi }, + cas: if override-cas != none { override-cas } else { molecule.cas }, + h-p: if override-h-p != none { override-h-p } else { molecule.h-p }, + ghs: if override-ghs != none { override-ghs } else { molecule.ghs }, + ) +} \ No newline at end of file diff --git a/src/model/reaction.typ b/src/model/reaction-element.typ similarity index 93% rename from src/model/reaction.typ rename to src/model/reaction-element.typ index b0cfdbf..205c127 100644 --- a/src/model/reaction.typ +++ b/src/model/reaction-element.typ @@ -30,7 +30,8 @@ ) { h(-0.4em) } - } // else if type-id == "e_typsium_---_group"{ + } + // else if type-id == "e_typsium_---_group"{ // child // let charge = last.fields.at("charge", default: none) // let count = last.fields.at("count", default: none) @@ -48,7 +49,7 @@ #let reaction = e.element.declare( "reaction", - prefix: "typsium", + prefix: "@preview/typsium:0.3.0", display: draw-reaction, diff --git a/src/parse-content-intermediate-representation.typ b/src/parse-content-intermediate-representation.typ index 8567409..4594915 100644 --- a/src/parse-content-intermediate-representation.typ +++ b/src/parse-content-intermediate-representation.typ @@ -13,11 +13,11 @@ ) #import "parse-formula-intermediate-representation.typ": patterns -#import "model/molecule.typ": molecule -#import "model/reaction.typ": reaction -#import "model/element.typ": element -#import "model/group.typ": group -#import "model/arrow.typ": arrow +#import "model/molecule-element.typ": molecule +#import "model/reaction-element.typ": reaction +#import "model/element-element.typ": element +#import "model/group-element.typ": group +#import "model/arrow-element.typ": arrow #let get-count-and-charge(count1, count2, charge1, charge2, full-string, templates, index) = { let radical = false diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index acf6aa1..08a5f3d 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -1,9 +1,9 @@ #import "utils.typ": arrow-string-to-kind, is-default, roman-to-number -#import "model/molecule.typ": molecule -#import "model/reaction.typ": reaction -#import "model/element.typ": element -#import "model/group.typ": group -#import "model/arrow.typ": arrow +#import "model/molecule-element.typ": molecule +#import "model/reaction-element.typ": reaction +#import "model/element-element.typ": element +#import "model/group-element.typ": group +#import "model/arrow-element.typ": arrow #let patterns = ( element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?P\^\^[+-]?[IViv]{1,3}|\^\^[+-]?\d+)?"), diff --git a/src/typing.typ b/src/typing.typ index bea7684..4e4356c 100644 --- a/src/typing.typ +++ b/src/typing.typ @@ -1,9 +1,9 @@ #import "libs/elembic/lib.typ" as e: selector -#import "model/arrow.typ": arrow -#import "model/element.typ": element -#import "model/group.typ": group -#import "model/molecule.typ": molecule -#import "model/reaction.typ": reaction +#import "model/arrow-element.typ": arrow +#import "model/element-element.typ": element +#import "model/group-element.typ": group +#import "model/molecule-element.typ": molecule +#import "model/reaction-element.typ": reaction #let fields = e.fields #let elembic = e diff --git a/tests/brackets/test.typ b/tests/brackets/test.typ index bf00e82..8f4f7ac 100644 --- a/tests/brackets/test.typ +++ b/tests/brackets/test.typ @@ -1,6 +1,6 @@ #import "../../src/lib.typ" : ce #import "../../src/libs/elembic/lib.typ" as e -#import "../../src/model/group.typ":* +#import "../../src/model/group-element.typ":* // #show: e.set_(group, grow-brackets:false, affect-layout:false) #set page(width: auto, height: auto, margin: 0.5em) diff --git a/tests/content-to-reaction/test.typ b/tests/content-to-reaction/test.typ index 954e01a..7f307bf 100644 --- a/tests/content-to-reaction/test.typ +++ b/tests/content-to-reaction/test.typ @@ -1,8 +1,8 @@ #import "../../src/lib.typ" : ce, define-molecule, get-element #import "../../src/utils.typ" : * #import "../../src/libs/elembic/lib.typ" as e -#import "../../src/model/group.typ":* -#import "../../src/model/element.typ":* +#import "../../src/model/group-element.typ":* +#import "../../src/model/element-element.typ":* #import "../../src/parse-formula-intermediate-representation.typ": string-to-reaction, #import "@preview/alchemist:0.1.4": * // #show: e.set_(group, grow-brackets:false, affect-layout:false) diff --git a/tests/intermediate-representation-molecules/test.typ b/tests/intermediate-representation-molecules/test.typ index 2047a6a..33eafbb 100644 --- a/tests/intermediate-representation-molecules/test.typ +++ b/tests/intermediate-representation-molecules/test.typ @@ -1,6 +1,6 @@ -#import "../../src/model/element.typ": element -#import "../../src/model/molecule.typ": molecule -#import "../../src/model/group.typ": group +#import "../../src/model/element-element.typ": element +#import "../../src/model/molecule-element.typ": molecule +#import "../../src/model/group-element.typ": group #set page(width: auto, height: auto, margin: 0.5em) #let co2 = molecule( diff --git a/tests/oxidation-numbers/test.typ b/tests/oxidation-numbers/test.typ index afdd013..3943859 100644 --- a/tests/oxidation-numbers/test.typ +++ b/tests/oxidation-numbers/test.typ @@ -1,6 +1,6 @@ #import "../../src/lib.typ": * // #import "../../src/libs/elembic/lib.typ" as e -#import "../../src/model/element.typ": * +#import "../../src/model/element-element.typ": * #set page(width: auto, height: auto, margin: 0.5em) From 32a8b8e7cbc39f55083c519e7124d2b7f5b86c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B2-=E5=90=B2=E5=93=9A=E5=9F=BA=E4=B8=99=E6=B0=A8?= =?UTF-8?q?=E9=85=B8?= Date: Mon, 16 Jun 2025 20:08:26 +0800 Subject: [PATCH 15/20] fix a bug --- .gitignore | 3 ++- src/model/molecule-element.typ | 11 ++++++++++- src/parse-formula-intermediate-representation.typ | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a6698f5..ee97b28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Ignore PDF files -*.pdf \ No newline at end of file +*.pdf +/tests/temp \ No newline at end of file diff --git a/src/model/molecule-element.typ b/src/model/molecule-element.typ index b850502..96b4f19 100644 --- a/src/model/molecule-element.typ +++ b/src/model/molecule-element.typ @@ -1,6 +1,7 @@ #import "../libs/elembic/lib.typ" as e #import "../utils.typ": ( count-to-content, + charge-to-content, is-default, customizable-attach, phase-to-content, @@ -9,6 +10,7 @@ #let molecule( count: 1, phase: none, + charge: 0, //TODO: add up and down arrows phase-transition: 0, affect-layout: true, @@ -20,6 +22,13 @@ for child in it.children { result += child } + if not is-default(it.charge) { + result = customizable-attach( + result, + tr: charge-to-content(it.charge), + affect-layout: it.affect-layout, + ) + } if not is-default(it.phase) { result += context { text(phase-to-content(it.phase), size: text.size * 0.75) @@ -33,11 +42,11 @@ prefix: "@preview/typsium:0.3.0", display: draw-molecule, - fields: ( e.field("children", e.types.array(content), required: true), e.field("count", e.types.union(int, content), default: 1), e.field("phase", e.types.union(str, content), default: none), + e.field("charge", e.types.union(int, content), default: 0), e.field("affect-layout", bool, default: true), ), ) diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index 08a5f3d..b4aff58 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -270,14 +270,14 @@ } random-content += remaining.codepoints().at(0) - remaining = remaining.slice(remaining.codepoints().at(0).len()) - } + remaining = remaining.slice(remaining.codepoints().at(0).len()) } if current-molecule-children.len() != 0 { full-reaction.push( molecule( current-molecule-children, count: current-molecule-count, phase: current-molecule-phase, + charge: current-molecule-charge, ), ) } From 6573ba14d607410cc2fcbf35ad19463ee99d67bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B2-=E5=90=B2=E5=93=9A=E5=9F=BA=E4=B8=99=E6=B0=A8?= =?UTF-8?q?=E9=85=B8?= Date: Sun, 22 Jun 2025 21:12:00 +0800 Subject: [PATCH 16/20] add example sheet Only simple ones are here. --- tests/0. Example book/main.png | Bin 0 -> 223548 bytes tests/0. Example book/main.typ | 70 +++++++++++++++++++++++++++++++++ tests/elembic/test.typ | 11 +----- 3 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 tests/0. Example book/main.png create mode 100644 tests/0. Example book/main.typ diff --git a/tests/0. Example book/main.png b/tests/0. Example book/main.png new file mode 100644 index 0000000000000000000000000000000000000000..3626a5a4e3c3fcba3385e88084d69bb09a0b91be GIT binary patch literal 223548 zcmeFa3tZH7-ZngnN~Ok{6_pC5mbq3`rdV>YyIQx**y=8KH6hp1EmA}(W?+~B%~PhB zT6hF^TeI?3GS`3v29J0E$1+6&8P0<=$Y?0?f;Hz^c~{?GpY>6a9WUfUFkK5K92Bmd=Ii*|}4;H`jX zpMLV+l|8PM1Z)}pr%w-Ce90{qf;@16eHFP@5?6Mp@(U*Ajjjq&TA=>9gkPR+0H<$vRQ@x2`w80g*? z>h3XZ+O)!-win&ytC)Q1V2<}f&U{0f-Et+Icj> zYsoamcpGBIMMN}woT(ljquf6*|LXVLzOy5_d-QA@x_4e}&(ze^cc)LRcyH}XOK%vu zAo!Cx4^98>U_p{mc`hsMPs0;W``+1p`oqnehkX84T|t^TQ2VgKvBGe~Hzv$JKddy) zb2`pa72E!1+^5k#TePw32@E$X&W-I1;ma=<8C*rqYLh3{`G0H4|+BUl2qBirb4eQEhRhdpjdrmnw>79eUVX64gFXwdp zyP~>zMfK9Sj;(QZzU@UtksZaeqjHk2gs*QQI*S6lz zK0!vPBSY=V(~h^hY|*|XZr0#KgNkbhS+lgxtWD#SS8n&(|Cn3x!0mlo?CW!!>(%YE zwPSr*MXi%`k7?yA`<84hI=9i16m3s(YpvMHh4xfkxZQOq&izca9EWwC2iMTM`?lAe z3rss1XqjVZpHpSh``mgJ4pr=}a;|kp44(4_%Z)ka8=vkq#bf$9Hup~t++JnEK5$;v zd*6|TzmTPQ%iEsdyFEQmxje6Cwx(&e!`HAx)4W8Pmz)~jV!x#GUMg(4RQyoqKGgM9 zd^4vrifDHSb!-W$t1P}!nQy=$vV9Zg`sTGZZAD7atX+Sccin<-m7V3R)On^Us;v(i zRwGh1`PPK`ETP6B2Fs8F1EP-gqQsldxaE83x=mjm)x0ojX@cw9gnEtEYt)`NUVQoZ z{L{Yam)bTQ`tv?dlx=kM8t!)AH(8nsSxpJP_JkhF?RhDqG#9sNKJ>-#|MF7z=E*mZ zSW~jS@Ywcw|7?f5wgh$DEss{b!sT)1_#T%>3p?J|DvNBZN-Q0sMz9JyGBDfROH&)H z$tX5maAohjwC?j|iG%Mf{Wx*3N@q`vejz*JsHeDoeDOk$C#|D+n<;hpBa;UnZ(JRo zx;wmnK&G-+*b$Tagvq=_>saExV(>nQkb1}B45#|U-r`Gptvy1`JuY9qYz}+;i6`n} ze62Cg%?9TX&E-i zwm8deDhU^F#@5~zo3ZnfL;v+_4>=`)J?XWE`Buwc*9Fup9yGV+@Suc-89}Lc@O4ir z`f<{(KR%$;#7bD&pz~Dfde`hQbnI{*&2c{)(u`E(kW}|-OMd;$Cvu|olf%y+#MfA6 zgtpIkEvbBrJN_SM95_evFy6&FpG!A;=FEjtQnpU1`$Qt9>dUN@k=_R-$9X?A_&f%q z$>%jap4>8+>1UMAI?8q|*M01rciu5CaLea>G}(|Au5Fv5JyfCfR3zzQoNHrhYlF1o zg5GsI66I}HFYY|6&-6)uXJhBT87?oB?d$k9!8NwUZj|j?c=>qo17Sx}iz-qrh;eR- zn)iks$&-j+dor~3$-;-3u0|E^A2m<6aqf4^_L_<}8jX0ds!Y2wD7z9*Z&l^kRopjW z&edT@AIz)Tf9A~X%mBxA?nw$pK!fa=8L6rA)eOGHCRbFp<)hiF)>oNcj&6I|DPd2o z(8xmN#B}^UrY_UloT;33!dr4G##ie2{cz# zU))jsp+xoQ`s!5Ep|N(?mpL6H`5(`ppB~lra`Y!Sh_g4`mtDEB_`*i(X-Vr5?%E*Z zqCfpCDsm&#&y*bPC^xt#hjg4=Vm(_L_35OnhY~OC$eZuFl9yD!BHW6=rmhLp+--Ai z^4%ZpSQvd|gnIvodHP8Eyvt`FVIH`&bMHz#dhMBM35}Wr%k2sKkb%tZLPlOGcmhkN z>Ds2*kcJg*`FZ_AUAsb$t${y>*{WzXf7 z7Hh+Z==D-a*lr24+@k(zTiRXt2ARXX_v)30M?3x=(=nWXZ7?Oxwqy&X3hUPE5PCG&9tgk=_1}Z5H#A=uN?=cg-x#S6YU42H)lV{`=sc zowIOZ02duUu2i;A;oaXd>@t$!OdpmND%86oqFSGdUW*SfLBbLCy>X$u#~%5OpH6e^ z4|D%ptgkID2j8WtxGBDPai-^N=9^w#Vzn+6R?>%vg%u+(TKJmo`l!1=c2` z5igY|)GI+_)qw*~wb*6P6|3d13^GXY-;Y4EyP)7G5{zxtl86tO_p$YN^)ESmxKB*# zPp8)DjfL_>VbRVt(MNHvFBP|3n(t|=NRoYEmCb$pweQ|t=JNcM>$xk}^hcOTcXHnnK1Z($ANjp6y(Vy?p z7X-D+m(>+EOe!A!uWaAt#m&o=%}<3M^E|qyasJUE$yNKtc+AVr2bFrwr8%qHj0f~6E6IgY1e z+g^$LWO;Cy_3I^(A4%n`n;{75R_yYXE0%jgt@o&Efsg>BPxWAI2Pg4O$i z&GHJ{)lyUaHmvQXutS~`o5z=KFTS|likRk#M3;zN5OxGbsWb+*v2Wa$V7)J4KmWZd zyW_`g)^(*3i_hGpK#OoZFRe1q;zp@FMOuocw_;N7LNLTR&9@vLq?#)^IJXQXV^8yz zz{sU?_;n2jmko@_%W)Ot)bEgFlWz!>6*Qyp`4{TPxcWU(GC7fAV*eeT(@|y*9lM|Y z26KPa7XFM^;@VzOp+S;WH6==lk6o3lT#{TfLK*{wQqF5!v7DyOMq{J&V2X^BnWbHC zoLv;V`;QOg8+h;~yqzn&yW6`%)GVwWRD6$KKU==kjBin83YPj+iFVGrgGE3^%8rVj zHE+f?y~%1aJ155RK}-#bAt#bqv4pf$MkDKasYb>thAxm|m1hgxSVS~;^R%MV(-L)j z57_QX&CNI8jMS3*X}^(% z$#B>cC101dB%FKuFJds67y6 z2akWhU;1aC4P)z_+ETDY8tUjq*7Id(ORT656>4N8S+%%*Hr8~^fC{`6JvOs-KhqJG z`>01F2*o;XuHJ0pP_Xt%Uo#?x^#$_ufBTS|G~Z+7k4_x7HiH5=l0 z)><;P5hGsMRMY$MapO=#zoFHJDxP&qh!4orw0@?V`?D(D>6BhGGWOD**uLpm%4JzK zs1B4*YNW{j6nnKvV``skI&7D`WJzdyP%^i=d5PwJ#q^3qftYsf-hCix@0D*Wm8j~Y zj7d>Tf*k2VHO}t4-Yv!X?YWk_a`*K`>ax_AzO|!=S&H|g5=GQK^m;^hTVkj^QBhct zS`>`*a$oYZ{fC*J$8GC{Xl_MOcZ zB=NWK;H1J{_EdZl)*QxmMC-!S(~54hyQHiMkS!6rsH+(N7ux9ToXWX-S7fD~%W6b{ z&thD6czW{rLz9XdCe63hJ)@|$MH<$aI(C|R*08u=X>#1`M+{Q7=hekoQGT$7mZ~rJ zpnx5&-Fx;Nve^)O)U6Y>k13>jmA>$~k%f169fNt+hG$P?M;`BMn=9|1eU~D3bL?S6y@t*v%DfWanUI|W^l#XYTC9lgBh>~ae-g?rKq zIZ@?@mIwE62rTouz#zpL#xqmv(bC}w6#mY=ubf%9nE*bjS2LwTDbw(j281?Zof z7O9vYjH$(e=~70cH8JmJe-P(B6j$c?{H=exMXx_`;$}f|6+BsSQgQ2b4Z%`TsiZ1R zYO!~sQu;anIj~3M2}UQcA|VHf`<(i=t1B;;1BEZyR(&*GY3y>u`f<=EO!48 z=DEf9s`13;dK7O;iJxs>9`f#x!# zx#Lo#<^i;nuk(pZZ{Uc8(z2 zk^mKslR=(86wB_&)*gL1+j?t=^T@>cy1;11zKAPt#nczL| z#&#(xwih1VSa_piwNidw+M{dA4vt;_?5x%6-?)$eceW|5+L-3}I>_~~H%8>0iN!~LorCQ*gV~~$SZG2Et6TJVZ485 z?67Np_v)7%eZJb<->@pI<8NVg1#;3ogA>|bO*rK7N}NcaqWWaYKgU>shcI_`!&rR} z1V8jzjX*nx3It^@Fzm>yK2{!XDAOH!xF2^fs)vIpICsDH(7CFrhB2}AWAd9e#5+I} z{6Ds=&&IDPFQ2@4h60GUq`Y5PTJR zr5GBX@>zHtYPA9bTR}OGRzoeCE~iAmKeHkFl9SM)g!hJaG9jNJvm$^J>LZyYt-G7Z+Y%}%z?_osWdbXWd zh$hWyzFh?9CBe_kV#C2A;#<|}DQT~0q@!FlZEnG$z>fbn7oqUPrNWza!sqP!r329> zOYIa~_rVr>ytMD+9DJME(U7PEo}!vdB#2c@cLZ-x(f5N0O1US6w12$>C}B@>ZG33` z6=@Ttt4-AP4Sop7pm#@<^a$&O9u-WGU=%^j{}rcUjOI#(W(7$u^!7>#u8Fbh00tUa z9s)LCbs8=8Y)=B+sJO=^*7fqOCi05TL7|lIGKb8XFp6`DX5bkUbtJNgqd!^QI@z!m zeQS@Jd!^!V2#^*ZyAG9lV`{N6RWT0GO;u7*SlPC;pSD>LC94uP+&A3f7A|4lym^We z&^%VW_rm)_TDJry3a}br_DXfbV(^!aN^)97a^plz!^C_8KrXW!T#Gx#j9|kvDqWGb z)HAoB@pfRC0|rFo{p%+s^5Tn`b)cc`F8NIFar0`o>m48IYx)+~^vyQ}YH9+l(yo`g zQ~&2vhjs;nAsWeiPkUuS?czboCncJA7gQ)$RMfD~=5CbeMdAU6*!+Hi{e5?HruY7^ zG8AxT=}bO6+ouNI99y339I`MgB(4b`f+9hKqYob+-4q9F@#&gkf2n0tejNVm~3JlXM|&h3{17lV>$2#neZoy8rTamT>du*bC7~XBc9%3$qq8mjVpqx8HVZpreJy`p25awqL7ImZLTyAJ z62`GINr#HAp{V*&k@87DODJVQ;FdGT9`09o_dhvd!j-MVf(Ee0kQUuJ=(-aoxEa`e zIbqk($Et+E6NfJNg=7?G4Tv@e6qcqI1}cIy=7p-CPGujq?Vh^h?Afz5njFiWIs1(2 z6Gn3t-~SZlqA4{Qn)-0f!U~BfP0^L}3*Ly2U!K|VMP?m}Pt}v4yLf7Kj4r7ElyeEi zp+pZbca{9BmqfMxHKP7by`o0Q#VbxB8wO+x4lj6{q?&zI31c@Vii{z#qxCKCgAcuy zU-DkX^wP&BSD6^Wz+ieKyOrXM;Flzr|5Mg9wIVrf3b72AKxUp0KYMb~BOlBuyW<5y z=PWUuyUJNlqrfGBZn8z04g{nfS)#r{C_ckYCBkG8Ts{!$otK>xF0Z5sctH=L2Bcwl z`MFxB6L5Oy7_7qi{$||7LYf|LSyOJ6g&Skr$p-6v~DT0X1?=*U+>vp=sydmYy}hVVq#tvMV8@ z0cu`bdyhSp^}Jvv*-L8{srC=dE!%6MPYE&F$G>;+R;>UJ2k}N@*GX6d zjzN;Gd&5OQ4Vr|+f5wfcelkUMo6wKm4_sg^LJ1V<4vqy*aAJhkI%4KVEJcH8GCosj z19YhzVq-U7qQLe&gG3zg5o7X?-^5Ab=N1dD4UnUtD{e zaxOScrEoCuAS>qS0MpfwzkU);6p-iJfR@cS71?hYAW;`|A4_!!*GT)7_ke{63v$5YBZt{^|Md)k<|;q&(<0)ib4k@3t;bh@B;D;(1f9GSG+EV zQ8vo_x`Q`{e6i>7o{HGx08*4Bslk#5&$^=z;1yYLg$RgpOPFTdYJq+q=UWR%11Bo@ zwPC{3W;gj6!JAF{0BP75^-rShj1{(I_35_-O=k2VOp+Iij-&;!Pl~%W#`l;Ik=i5URu*2^SUl1$(O%jT z1pdGdsc!syzEL`kmRg`D0-(CmWP^lpx7&8_-FqB(1q5xLC<$JfF`ge|8lC6PovW>> zQ7E1zi6Xdl=>DWs97q6wHc+36!n4bZ?%>3j!me2FqjG8Mt==6>4ni`|vz3S25HmN8 zOI#56gdoE!gt+HXQ$)$xS|8?E5LQ#*krw=Je;YU40z8$BQ*Wnen|cuB4D6t`sp-ww z#Q^j8O+cOqDItebx;Mv7v2ygY&%JY4cB~VBnJ5RloZV5Ec1tDa=$D-%P*g(AuxRTr z)fSLj;1IZlw#DSk63iX+5%AQy@j{ELT0aY={O}ve^GSP!375asqyZF)&M@X_GsrWw z2-91}BUpGa4Xi5VzIW!lcA+C1^Esw70KZ5XU*4%hDm}V z1cm+=2>1x9OX}MWJZzqjX0Yv)*Hd^nzOWAnss1HsV=T~cFhW6`67-`zk}G*h${h>p zF@+SX?%wF24}`h*hs^_hUR5okw`|k~YT!IU|M26;L9HxV1+p9wq&o7HD3P*DD4D>n zeO`d9YD6-`HAc900KVj4W7Ks6SpsHPj;VO%Wz^8H7q5*%XK{=%k=t&`vfA6 zS|;mbMV0)IGs-Qew#C>U5)>}pYSnyg;^o&vUqoWpPI@e_iuqRbSEc4l08>0^Sx1{TeY_pDl8b7oYp<+MW~QSG=`BJ_OE;5uKWX5rMUm zQ~kooMK9g7`Y$`apE^@S$G~{hBFPx2V1t0-`O%H#U+j75xhHQ~FW}jXI}}POj~5Sb z$-ByJu&yP)fB*=RIBFTfSQkmI<{OHr9$2 z{dsKcqG>Zi#H89HtVD=yUFjdCjYm1$YBNT+f zd%kqj)>|K4BX5feLCEJsp`lr96s3ZQ_eAsiI}K_$Bw9@X0Gtv4{~hJ`fM>fH^50&k zu#Edo*jW?-MtzP*6_G&aU)fV(trB%MPJMyEr_=`-Q2AbmxRs8D+6nO7Uv?xO=pPQI z^ap#S@AN~lg-3wR$}%)Pk_j1_?|Bg%0WL-%^wx&_3!iVycQ$Rj@ZCz^m!M$*MT1L*u^k&HP!9-g(AF4x7yhkZ|<65AcCv1Sej7j@#P-`EN%ut0eA||^5 zhezT=_aAMzsk>j8o_Vg~ zy&tFUDks^*(@lM(?9ut%KD&05;*H8~AEI*(LY_xc5G6WP)B%Pl{C-f;0I6_t6I-etr`8a(aYNbq(S?GB zKSG_cZt)!A!mO5|<$zLVpOIv127vAmWhI>s!NmA zfxc7j3pw7AY==sK`4yi}(J^Ry7b#dsKts`@+BRLH;PzqKNu(N-sz#7$k zlyzA2>fqCBeDx#g$>*yPn0g5wsa6Vhc$L*blViK@^7 z{fA#_v!+V$C&~t8SjJQWs-(v}d^%`=lT}&CwUCTDvq--p;hX>sp_`-D#FOo9-DZOlILNswh|k?Q|MM?? z){|0ci;VmCbQH(>MQBewJUV+HI^vxLO?P1@4!7)Q3r~$7 zM^#XHMVtgOCGy*X`jz9yiAuIBtNF^WKZnr=$utPs^(CdNvH;Qh1B#$v!_>|R$lU`x znA)L=z^(j;$L}myj6o5>@C(!(+q6%Fm7(pn0wXJgM1ngvz=McG zC>F2}M&+!3!;7JO4|?2tmN8s$%;Q5!J~`PUU-lMbv^ z#H%Q2X?aNC0l&-wn{j7R`&0!BX269+OF-rXwE}cNxcqS{b@F~m29+m9LB8|79hX!; ztjRtoP^wHSisH;uEdXMUyn&p0wu&n%l>KAZVO*sidL;q!lzmy$2c{f*IeY)GQ0MPO zVIu;Q--{#+Iz}SXd-v|eTp}h*04Jy$CTfQJRttdnjlfF4$E^@WMx10lCyJZ%>dv{f zuB4zHeO)o*39>*1O<^vG0X^!pc{&+If|8k^X=6h1o&F#UUd@3vnoxzYotQcFj-j?k zLJt7zn5Xk=T?ILFp3U_9QOL>SkF>d&uKdh;Vo`#HC2+PWP_WtTgEK>HcXSjRYidLn z+&@@?l-{KS|$+ow@aWf%lcnnkNH0u5xC)~ z8Tj)q1-09LX+I^Ek_HJQ^^@LI9!1l5`+2om=^6bOMSoqPyFRNZDFEB>{e%p?iA zCUzZpR4gdbhiVB-n~m+j&eRb20aH7Iwmqradf}-kDbVK1d}3%#q#!p+X)2r>del1D<{K%v*QupQh41kWk5zV}&;kz&hW*Ng!x5V0@a z^k3~i@ZGkza@yZ2l+nNkIhoPdMn$W@Gb1|}!&c=Va}+kfh^k2?RBCdI9dbf9kVv9} zK#Xh)`W={HuovDRD>~tip#`o*QBW~Gd5B*JM!og;mi&d7XVNa$cl?LG4nv443~3e% zpqUligz&Ed0|b&Q6Wvv#C}Va>qUU4@_EPuKO!1#evnUyn%?1-&Hzv+DhDwasb->g> zMS)xeQ$g~?K3*{7CxcRc1ALGL^3Vk?CS+2BpiC^1bFw3%S-@di+GMY-q0rg48URhB z3@~mc7!f67525LTbs$D^2|%70HTC1HP^M!7i@Hd=3&034ZOg2L_F3RrL0lxt;ERp) z1l7MTX_KGiNkxu)Gc|QkdRYVN8D-tToFj-gOaa6KSyF)%Az0gO^9Cxq%@<3zxLZp9 zZ0p)>W{_ym5(-+(Xu+&?cGpbdk7WX&9IA`?QY$eiU@CA%Qqmeu$6{%?(J_^E6P^}f z$RGr$-bKgr_P78rr$PT{S86flnMCp(UC+QGV{t6iQjr&eLW_d|q zYnhH!D}q|(SUOSbi4Vpsao1qiC?`$k*@X1Jm-3S(X=j%}&T`%E2=BggFK5mO&pogPYQ{wI>GEsbj56>$=g)dp_?LYlxw+lfYM{VMo{D8_Tt~l;5vFL^f+b4Bta~y zKjK_{}1<(51*R-`Jre;c`oH3MS4C>wuwmOENj=0Oq}TOh2cq_Qe&iyf z5nwq2HH8?=9|*S`@$B4g+L7IBXrEt%fxuG(Uj-+4;OxrtVkm*~w!OXG!IlaNb=L!0nFb^d+QvCzuS;z`Ob%C_&p}L2b!T3Vodo6arxn4vD@JwqeE{ zRB;o;X`EiY=u)mO8>6lDn9y=?T}0Eo*4X}t)aZnDAW%Avq-vqwOWZ1kQU{Ra0>H~j z3UXSq(HaG}$llMw3g#nj2(Tf9$q4Z@Pqh7gMC3B#tMQHd7xjH-&KyCjL>EGeoq}Qn zO{au84O1VJG6-v3HBt5o{6{*0 z{J8iPoG(Lt%OVL1LUt8Y39(HImja zU=LnzRE)OcRDrM;1qQA#I5W3A8kG#aN;t)envCm=p_UUK?iGr>0Kh5zgB}u4cBusp z7(+rB_@rfmY-cy7e=}8OGslT1fwYj|51Q zpEG!(5U-UlgDrB(u~p_C?B)nW&+O!7fD5Hx;|` z4I+es41yL2Qpq33$?RfXvaX9(aZQ3K#pf1%D%0N@7VhY-3Oqvd4n^iX0WG)S4a;uG zuEdThmRPYGvb4tNV}wp1dH2gCq;M`xazRQ(^*G-gK6w7cxUu~L0La#l`z9}M;>bm= zBFH!pcJ}iP;-xCiHUVbXF$RtY^F-!#NQTf_yN~Ey?Yd|Ag|9pd6TOZ7v-Th?fbD@E1OS%L7OON~bbHqVnpBBTkB1hma6MFoG5N zQ{StyEllN55?R3f7M`yxl8PUQHCK%^E?|)!UzdwO;D`(u7l@ydkhfO{iXl#jlE!*7 z$EL2K*&Sim;jX$L*#LlzEWEV0c+}Mqo@?!2?{r2w2yT^IFnm{wR-r&TjppxG26mt< zlaQx`+Dk!`458AfvewD?A;NzW1SUr&M%|Jw3+8~xd=Ut@qHBXki)UX#$0c-{FhKF)lgOSI60N(Ci(LGoMnWv z+WVjsctm&}n@j^N0Q@M1-;~gb2&dQ-nffUNWc$WaRs>;!u7d497LZXZ!*3}Ai8jz( zr_8|r)-SB(`FKz$RK|*s=F_oPPBV>gd>jK;0h|JM;7f`nGOU>xW&OCyl$e=1m`H>= z$|%89YC#2+#n@7_3SJWtpy5(y2P#cN@e&4Tjj(XR;pg8Sh{NtLhno)wGUJfG<90T< zWcUsB`25n)J9@B>#tCzIq%*&dlj~57VM5VE(T=2b3Oy%4D|ZB0+^>NY7+^$K`n&PE z^tZKB?D=PC8(a2f7MoOC`~$#JLw6fs3hY4JAk#|BK^`KSjMzVHokS>{^4n>Jho+fP zv?v$>cZ}f+$6SW^vOw5d?J6=1O6NO3WtA_OaI-60{7!Df&>X*%8<)u9=H%+5@?K>^ zSW)xcrJeO);tLGW09VW2T@U34I#x^pD{9c%sKBr*C`YBhUP`D5w_I~h6IL~}OsMJS z;`=|#)PMu3U;efTkpy@7n0bXGO4lS!R0)t55&EXeglfu(OacWMTX}T4R8feAC+=`c z(dT;(%Y2`NN!<~S$4|g^Jj(tL1}5($B-j$#n2lKpB5;982rvWjR?9AbL`a#O{T#WU zz!^_EiQxwq3R*AYpm+l|h*hazqj1y1cuTnofCQQOJ1`c5X@-l)(V#En^Emp-cz5jL zzzyZIM6Vz`!SX{r(-9nu7*16YR*}T8-uHlC(ut{4AT<=~`c@-W8ZnhY{S#FYot(O^ zS9coQ_fq{hx(G=@K?ITog$5IdYFjxm6u}k@hoCza--nFh3i0Ve56rq4UKfcvsf%z7 zqAq?G_2I|;uNqpfTCO#`}Z`{1ni7Fh3#>D33#1b(< z@>2*>DwYbx9}@8g+Q)$}IH8bAsR@i;2L`f<=+eeK)gTsUt|3E)BwqY+q`XCT&k#J> z5)y?&n}7Vlym{j1WPz*3&PxjA-n`WDQh{hbJ9+;Oo;0{OOQ*aZR5yqUZ z_etIl4gSL$%Mq7XldmLNjlqzodxd?KI2Zzk4fnMa7(+k_g*0QdA8Ja(31!*LkO05` z4q6CeAQiEx57IE|vyis?X%vMR5ZZ{D3aQ8){rxi!Qc5>2DiEm(2t4t#6TxH*geCuM z3(f}wdL&tZU^7Ol`J?PE@GwZwncVggPG%-2XE6t&Fz+ET({UsDLK0&I=LP(Mkf>)3 z*BY-!@%dYSJ9ErK+b6=IjXvs1C!g+k>I(PL2084&yX^5oW?y)P&%NVp%=i*(`Jf4o^w|j2oi8{;|rfE z%E^e6oi-)J_+*UKu+Nqxvud{aqMYECAgQ>#4xTYo3d%D>>3IyhuZPT_nuFY^kMvjA z1EZ4FNJo)H-2cZfU&cPUI&uM*JHQTpyAP4O*)T`LVqG-w+G8H1lvvR_Y@RI>HI(mS z1nM7F!+D%NJXp(@ZlZMv=db0jN`n)z5HlU9$r0rJ#Wg}dYH*11;5)XU@YMLC zfkY@`+y`Sg;810Pn4G4m91ceCiO56YQp4#B@Q30G=Up1uf%!a+?lQ>%BjGH#45J_I zf@(};s8MFHYhYcU!E*qIwR(WKDp0(!>-@eB&lv0vWKGJ>xypYc&#;N0hH|IJ{R(R}Y`#IiU)#dFV1n@0yO^d?S@658L3%L?oOP+KgAc@y%h5r;uF z7x|pdVN{T!eg1vjGi{O)zzjJtn`8YIlz2|qqh+`LeT1cXV)O=3eqOgK zyAsj|G>iNPAO^Yu#APE}bFkYX&FCwnLsAAn&bg>>u)@z5!)C+S4y2Imt^Nw4D8wQ?9FpIQQvyG9IOA>s4T3=clE1m5^Pl3Xk=Ve?2l zkamJwhy`IEi|J6NYDK^8M0+hh-JC2aZeU_=os6 zrHOj3wx9^!VyqQd0t3GIuQ$su5oxFcs6xk-x&`xs;yxrwfc+@x#ni~$mp%(Yt*B3^ zw`BOImFtA$YSjoD8?d7RP3DeI1Kvh^A5`r&8be6cc5_gWYOciHUNvU(DoF@Z|4F$U zk%!|b_=5-&6l@7dh%=4Cog?U7(N!f%eRj{X#`({3gRfkdy5^o6;BiKCnAai zLM;Odg<%?|*I!Lcdf*UO@(d^XT3-+|4T4jhJZ4f?IRV1g-{n;KdtXd65-GuqS4cD7 zmh+3uj{BjVHe>yS{ltD@zyV4|z%Znl$q!h27lgFvajiE(+L<0T;Srj^y@GAeY8tOCS*bh{ zFXI?y_};QL3TlN2o=RsKINRTK3c*teJZc+L_=Vr&3rK|&oRUIYk_8@t$p$>QApZ_Y z$FO=+*_|BE$}f#v8Tg$DtMg?}`K%0?NQ3Q;N8Vxt#uUR4@h3nwcd({npv zCk}9k(Br#^lU Ez<72u9C^SW-4(Epd1C{SdOL)LOV3amYn&2}}`9;j>g80Ik?hb(mBRIz3M_$9uk@ zaSYo&ybq_$Q%`8Iljs(N;87MqE~$s+>G(Qv4rHE=4%=*Bh~{BWbJ2odnPnB~V1ooE zk!pEA3jC%V(6tOfEgr5QoQd~OH)9U52Cyq;oCL7gsR{R75AYhgrqzcoz=VYOs0Gy) zP}m6Afh`wJ;TAw~$5~GSw87UbVA;d|o(l!|qRIq0IoygNxGJ!n7~ew%N{UTA98y=M zvS;TcvvhhrUcaLcCbT*&_jpQBT0<~UWkMy$%>`Q&G1voOaN*gQI@0}6+=Vm~JAhr2 zI4l=0r!=qIj5Z=J_9#Dzp%cO<*M`pZDuhoF@wI)Q0Ssu_z!8dZPdxEwc%AcX;%_^^ z#87IZJ&KrX1};z~o=SrLBFBD1GT2GF5=u4l49=?o?-YXGUkdcXu@$1 z2Suod0k-5oTM^Ps=MrofxD9<>PG^{tOQO~yBtLnJf}I0Q!fB(0Q1#rofn|Ffod1#lw+4YX1?PHGcKrsz5jB9G>xAeB&T z(PNk(9Tybh0x>qWARp3TOL$}aReIxr*pPq6ZsB=JO--eV3a44&*d&cUhR)GiCrYj$ z6@Vbph>*t8k*Ns+mf=QO}$O>?WNp)^E36B>eVDkYrn{o^L zFPtq=p#N>{esM#qlh6y0nmbS-15!Lfz^43`cFoI+mr|_(2S*GLvx``trMgq-MybRW zFeTuq5wgdN%mfQ##Of+xWf1b0JNkg_JY|n0wJg21o<59FgYxM)TdX^bF(cIX;&J%Y z(-2TTE)ZWQ8K9s?GZt^@>w-E3PJJj#HUjY)$UD1Gg8!6TQ(){u5hXb+Cw&P3@g_vT3Vw4wG!9*Ox!^ryFwL4EQ0?SZ9!2VluIR|@K2(_2y*JE0pjmg zh_l9>0i?(b{#n!vBNuUy4Fo{_GZxe-)EZ)G#eA-wpbW|Zg7bkXz(NHEkmo3SWAH={ zR*+>O+z9z36cvLf3egg~0@{5=4cZZ9ReeY)tng)`FRY9e4CKf1I_Na1<>Nloz4t#1=7eA=7V5Ycd{PlQ@{hQ?&5@CwOM(exh4S zFV`;n`E^hHx+g@A`D>iGc74*d?ePD>_}PWH5F90Si;0a`;V?E|qNV-c=T4fS% zBN^XN_huJ_Ry9dVxmc;|HZk9#z=a{^HOWZ#IH9E+plvYkw-QwI?Hy}hx__%Ub3gO_ z)Oxqm##>7u8;i4YcnYSKY?T3i3SNZ)a0P5QAp3Gi4gMUVKGKSp&+S&ji4_#B6T?$2 zcC;q+?8XfH)z9PI0RWY~D)Kp$?=mEgdR^?pl_wX5h#$M<1`MTLL%T4?>hUYL^|fT> z(c96{dk3K&pt#0GPFT^3b~>tWB{2q|d(X$Ou&Z+HbbKr@0AVzvD^heZbVB|YhzI?j zcD!0;BBhJ_B(CkCI9M9sC(>9-s8+3_MD#`Yf`Ya)(@-{Mz_5pR($M-qba9V$BL4CK zZ3(>~#?Q`0kI{5q1{l6B55T-rh)h`Kl!-a0LPmw;x9%I|#~6;&@RL*~nC8N}iQ@yA zoiQlA)BRk*@=NM9jM#~x(lv)Mg_#I5CW+v{0S*BinQ!E)+izoQh+@V6Y#lil*vCjC z5eASqfC_k;lCmXDpF;qgn?UfH$Du38H$Y>9TIjl!8N`8*;ARjoPWUW`G;?+w4kL2` zGt<h zK-KFP8G$YF4Rnh{?k=&XGn5hIrEp8sH8uXN=2L-(`w(s0tPS@m=m$>|h~7t^r>{S- z_HBC>50}@o=hbU#&aX9aRgshU;;{3&UuCBjBh+a9 z6?rh=6>r>U7yi^Wp75COOTP;|_*M;t3D^Wzz1DmW5ETc|A!1`N|0tZ5FOtrNxPSQ^ zz&S~v`MV9nzvAmdW}vrtDa%DzaF+;tdxvGg)f*YuumkFih{nq;2kR3KxDr5E@#$n|wlefOZ~?N4yDZ!4%MQlLRU zPOW#|31CV!gSS|4#;XIgILCL4?U)!P>TBP=XFdjKbuvsPBA~D`$du&edRK~4j!H%Z zZtV&Y(y^z)UW4ruhrIyAIM0bl1nrzd_{&)>b~dT7q_b}n|2q+a`WIQ7N#|v8b37;) z`#{tnlTT3}Xm=pIahw>F5qBeq$bd`_D1kt*i>i?NJwZLNjc$?km$4gMWrQ9Z$4l%^ zm@{xy3d27-DHUdEASt|OnM=&7h0M|{lqcYo2RL2E*@9_9;3W1Tw9bUpZ15p~vP5M- z+wx+P3_7s@bz-lD+a|u)W376}222-%j@s`tHIIRhpCnc|hlJI|#b{&z1(1Q#8ya-F z6uk&85MFfhVrh)(lGNxKLc=%&Ytg86MOv9)Bkyw*kysQ!R+%fgqFK+to(i5r_<)v(ya1>7)LL0JnoGzVV8}rd{=C)Y72Du z%zuPNJ%p^Kk6Sfx#3qg!6FBk+pihf1G~1LQ zlELPDk=gv4bnH^jQ#k~ooF?>r@ls3#3CZ;v@j8R=T%6F20t%Ehz+9I1vXL{ETVA%_ z$_pZRU0Q)h0Ov;~is^7xqRqf)Fj;lB32DzE(fuJ|(43IF6P|^Zpvv!H0|`2SrCZV= zU7?Wy(KLYkhV4Z^ZMQ(hiF-u~?@q2t45aW8{NsF`|Ar^d=^_2;uK*B(;!6d5fdh_m zc0_`|>Y(heJ+Dgai^>q#Jhw*;LY@db`eRPN+eY%=$oZeb>IB^DB#$LoCD*aLs^yKyZ_E^lw11q?d~k#1$5oASkL^u2iq2SPLSm zHo?s?RjuGK2P&Sq-(8S9)@bktl>*e8;wgqyVFmieF(-cVsj|u1q^B+6@4XdE-94oO4H{-=%8MsZ+64^NB6AftRz{pWmE(Pv zAD-t z_Ezxi%B}=G$!Y$GRer-ORz`R{`duKF;es^V1bnp+O{PPDgT>t<;sfgb5Od^m)+!U> zU1&72D}@%ow>`mG#tbZ`Qps6jUpM~Rb;e3fQvTbq|tP#gMen@;lUTVryHF*6DK@faW3;sr;kiaQ$tu zgBcQ0bz%3M_VZAb0Njm8^c42)-*kY1M6k#!+>Eo3%G|WQ|E}))uzLgwO$5n z1>(+_8tfX)oZ*+B;R;m^ zV-B~d4v7E(+vSlxIFhzNNvXma(~32u8PaBGkCzSSOdok(+72KXc6BD+01{=v&A1ZH zf%XVSvH_dLG>_~E_*P!(MfG=Z12q(c&UqMk-Pv499w0lKubVnboC6ooBa*Mm!~1-A z#58hj=_=ZRmd-!iEO+;TTZXJjD8#`-hFB(ulbjKGo(9?+E9851$pn2k-se$U5`4tiXrNUUVo%*KsI`-73~EJbDvG7Touse^K#`l1|lO);=yRu*vUIh@r=~IxD$)w3f?36FJ_TdRPZ5{7Z@-HNr#{CmBZK^26%u@pgRaE zVFM1?WR(e!j?8;Yn*|<(=HD%La@dfM`3otJ9zKEo01aLW4zInPxUh?-1X9$-WijfZ z`YVhSQpLQDBB#a9M?n+}dKV#1_)odmL1qBfL)+D;!LQ}yx7?^uHp%{Xr2(d(xk-R{ z561BF<)Bt%q(Z#npjM3Kko9Jl$0=Jl6y&%faapbZV=$yZSJ+~gR<4C3@cKD2DkHr| ziyiAmC57*@uw~F%FUusR9qT1bgNjZa34YUb? z9W=GTwU5SjIKwcmNxKRCieBy+5jWhSqeVv`0$BLE$MU)l%<_pDsDdI$&cZOOGfCQLx?) zm@Z!YxV0`_p|@R z0n}@;W6ain;=0}Ff?7yyZ*7qgc_PD;rlP>W%LLJi7CtQGIw(8F70&geK@Vw$Q;I{@ zc5ZfMq`m3@ByRy8XnRPhcL7`4TQE4))>G!wif&_h8?fh$BQK@nRL%*|^C8WM9F*i| zSEBggJyBJ;xO5;Qng9rkhY3=<>GZgX-269ElpI=dwWaB$_4GG1I6F{CjEcNb5A`;mvnIRN#F*ipvnUv8`6w0$IAdkh9VE4 z1mBAEW3zR;i~<+<;o8#dN?t30RY*!5@=l1IN=vyrna13P)1%?XYUx}y%$rh>yL7^BQa0$ z-xbz!+s8GmPqhWJjsEK;UQ81V_+i@f22ixgD^rkaiW^gMqd`UeP5$zx45J5_k4yGd1brlrB;80dk zC&pNCCZZ>M!4<)$Pz`m}3k6b6d45CeLpC-7Jx1oJ0$0$shf*O8%P@{>S7B}lRdGl& zk~wE~yHz@CeyP&Y`3+w%=`IFysx6@E>7tv#n@y?CFG!Pw8xFwqxaaL1qTH}?_SA2a z&>1ocud481Oq9_D4?fcd^qHf9!uHD@edHIeL7)Chuwm%ydbW-NKzQ zyovWtPd`?VOLpxp0i0f9dUF*4_F~BR-xD_@PO_ngO8%qz8%ih`rzwvK+=T-sOf7Z< zSPH21SAbM>IrMXgwF_d6z7g7mlne?@*+t7flZ)>$ZNZ2Lt*TpMUsO=*2O}af{A62CoRC*Al-hm@XC*lvJV^}2s|3>GHMNZt%$4#wAkStAqU-k^R?)UOOxnW0}*|dNh}`e=z#r-{Ii7|5<`MjZIKn84_0m!JlPu%4+;zzv9igebuzn$FVibW8V8E4@mT$zT$_DpB3tW*HRz$~^=7dx&q?x)+NT>YjMwK*c zv$HGV&1c00#}Oy-6CvfY*>JrG9ixK&Aq6%^$FNUc(|Xzn3js-G!jtY2CJnF^s!Z%=U`R!Wav2( z!dM;e^om`Dz>9?kCxFXDcIvz-AOlFQmi+o>CyW(xg;)XaZsBw&jD~mHAzG-2`-9DjN`q>mEVn*N+o&J6Sypx?{2?64}LYxA;t#nF~MQoUt5UCxIeu8XaP=w}WF9h#{OT!S0dk8U#$-$ZjY@=KqUV zNY84o@7)bB-7NQeFp_{XMPhYgBvI-+nckGzbG8iY;UU?I_i%8v2#%(JAmPk^V21?D zcM0nUrgRolfhimXmszdlfgQ|3xaJXeC9pyOoGGD~lKQ~N<|qB?Ymg4owYa1RFoRJN zrMu`DF%E!%4&@eVjnVLn`1CrIMvNp@Bcpu2c0cV3fQzHCs*pP^kgow!*r5KSY+eCm zu#{IPOppsgLk~jT(B%VuZA&fTDp5y@wgz|A+jbWfO*YuY8$_^*h&<95fT?=0f2VR3 zgL)-n^;b}kaR7mEF&dBtIx&J%$(~m69&vJRCoHb53MDlGXOT|_Rtl+#YkKg(tfao8 z3E-j`v_dawG-7g+2b`sOm5gn(clDRZG!4K;p}79)R=N{E0vE)xYA2?R1dHh)+=S|s z5@tNK55ws}MOzURkLA1z3TKB2m8Eggvx8sk&>u*z#NDGd4_}7w*DzN8X*9Pr+pu)VKzr%9+NTLg5<__1y*dFqXs;e zJ#FM7DpQDP!B&El16T&Xn0hX0tW`nI-}rC%6w96(R*xw3l4Qy{^$}XyQT|LC4z>(f z=_E23R8wNF&JrOgrbFxSwm>F4A?Ci+jLXd0avRDGxkFVT@p_c$G{n?MF|q-1RW4J` zuH-~4J^P?i=+?XMWAC2i?;PcZsy{sUS6t$;biyrROXuJUjvZqJ1BNFr?x0mTq-mLo z0{6R}!-n3!Rk3RQKhB8cSvXfAe4~~@g~0neh7DbM!#ivIf5^c`URv@bOBXPe4L};< zeJyehj^Q=M7vCqh`SZD#6Lnf=Isx=R*9$#08v|$mjxsa|q{qUIl3P^<>-< zBDa}viU<>tDh;kVr)?5K^J{V&1a|>rt8f+lO4#}XHwPb0AXR9tPgnswHQ4dm`+Q%3=9Y4awQ-A%lt~b^7JKwMG zrK>ON_PhV~y*T3gxp zn|4;dH@!`FY7cbv5#4^LZ+|H$Wu!L?khkwqt?eUS=RM-xg^gd$PW>{l=vLoxzy{j& zI@MvrbtFpYCv^EWcmIaxVlO@!c8F$l&bX$8Y)g0-01q+K z)o!AMIV-?ag(4>+CCALt0;QhXshb@YmPZxgP)LygqY|u$%3n`So`u zpKKelTGIT?jfsQdxqar6Xtj0rjk?nL1%E&D+|ze2D?2#0{^9xc<>A)4Fi)L(;_>G- zZNZvP;=JeLn!cTuQ13oxd*_|G^G-b&ZuutH@lByEwP^H+7qYvl3$C5$4qxNX_JwwF zj`#Z<@sPeyQ82HiXN+UXh}Q?DUrH#<4vcJlGNNHdQ0>6zO9Oj2ta{h{?Dn|H`Y`Y_-S}%rKF1~SJnDf!Fqjq5?o zV?acBrMh~XIwZPnSoEPmMb1GMRh(1x?)1{Dr_2-cu6=W|&91cx`g2=s<89+Kb>n=4 zJQqT}BeUKQ>+!Yx!0m;Hx6eDR-LyRTA*)gAHb!(D3MnrS`fj~1Q*&vGc6t4Xm9|H7 z&5vG-2z{+Ti&4>)rRthe^LU+i{L{U(-nG%jMXIe6i*Av`|5a|=S87jkTBPrLZ?oOI zG)QX-+IDHYre?M#eMzKsM&aG-k1o;am)IuiToVhAPAj@G&T%^KNayEoT^{XOuKuZ1 zeV5&}GSqo^_}kYv}7M3D#B1 z6{)vrTSxYL+&nV3{WkaPKTe)ll{9Zesd2>PQPGE%zkUAIs$^yO%xOt~PTVlicB=4h zt;!y19-{T!rfwaT7Lqy6`Db_2j_A^u2=89U<%*8qop|T9QoXN1Z#$RMdQN?zLOU|o z^}F2TS067j`e*UfyF0BcP}5ImyQF*SX|plLnH0NjO3II;9$7gow_|Fq7&}%}*g`M7 z?n_Tr)+g6w?`^3)5am6f^aZ7Az4ykX9y_Hh4cAP3bWND+a9Dj?wWIB7#Q$yjc+<1R zR|3zdPo|dTdp9U*VtgqLS=!XPQ_=IE?B#n?W7($tIL5UurhbXGc1c0wzPyJw>X+W2 z(6pCoKMixYhKcaTGEABGf3f%G@lDnF{(s6=u|may6-yfxp(;WIgaU1oLDcG0%qXKB zSrV0z4i*}qLX&1WEJDFj!Xg$ZG&;z1u$F)bZE2d*(v7ZR570JAle7U!o2}VT&iTGS z2Z~;q@y_)wkKcX#?q56}Bu#S8`F!4=_xgN!q;b5R9?asgSSi2NcZw67aXRy~=xdI@ zf&-GyhDoB^dkVf-`RwZI`Ej<@3UaloHA+2_buNPJ94_g|lSO0xB&_k}w;693r?SW= z*oN`!^;(dJ?ncVrKTJ?~I~C`op7we>XT)&Sm)wMpT!xQNzPPwZs^ZWu8*l3oY-~N`CypfL1lT^bpgRE$CAA`NnTKJ?wW|qfQGtC&&1XsjhC)0n9cjlbFa46e?;{ z)!ka>zDulHR$^C@4}GB8pDWv!7AF`2IWJ?BS1q8}YN`-dWOHtJ4(zRr2hMeyqIW6V zw$ytF*6}kNIanir7*rPz^R|H-e?^=1o|-II7f^5TVbHzBBWdzHwM{{FC@fp@p+(u= zX$^J~RaPRTP&vDWMRl^EALpL&Bpud?vfFu~Y}3-m``=>_gfcv#_q2JuKC=}&!Y6L= z&6n8Sm%O+1-sLg<=R@tk;{*?LS}c-_7TZ*Xf2zt7ssi+txS~UBFTs2zL@S>f8DGDy zR4)kL7TE1fhyBHw5RSL0XUFLzz+B0S=K%J!2_oklwL3>>DiJO%XcLf)0!IVW*YK99 zW|FFLlFi6+8Ji{G`V`?rpl(@&O+9p+Uj*~VVo_m(64?+2~w>CF`cs%9{QWJc@3 zhsmOfz#=@}^pc|*w^hG^GS#q+O|8K(dg5=Ku*%e6YxcAsXYl*a=&OQcpPFL|!>FYq zQ+7q7)%7m3Hq5aW^Uo$aqw&_rioQ&T!6Y}xm{gjzRhpgY9IN!rNHCgnGVt>7bkiQp zBn|OyiSxaA+8^DQ`O+QxaGWd{HxfD*lCejWPD7k!LtnfqM;SP&>3lviraqv$TyNVF zE?(5<`4=pVS%!P3;PkiltjK;!#X)47!G zS}N3p3%PLoWR-i20?Hxq9m2`zbvt?)tF+OX_6dsM1l8rHiqY1<)7GX8v9?5*Lfev5 zSx;)IZ*E)$PJdD%)?etmtfBs>>+H~Nuvn1qD%6ySxH0Z?+4jSD|6%Bt_f^U}kHz;H zlVtWJuhxHxPs||A;pFEy)q|&|iE~oK#VMv!I>Ra9&1z;N>X$I-sig(P=dsR6%n=Fw zwBV-U+v5fJ9wu>&X;xB%Qe^RDrRrb_Zp3oitmzV`=a|pt*`E=d5mKojuAF0$bzrXU z3>&5TLm<1`P&tk>@$n4%**nquTsID~$G(xkr%(9uIAJ{7m%-kjnWf1zLPeL$$*DgS zn56d2#~xp=@gEBRLN+9-cc7W7VCPBu)l~j_mKS=-s*VSAy!t0 z)m&B245_R-UY|+2Qht~o-zvs0Cpp{I4moEp2k-Ite$;wD<=ts+T7xTuM5f&&r{31x z-eql#$GxF;;$ZxlJUMkLGxdFoq&kXv_S9z#;zBgupe1jm+V?Acu7vLXWP38#_m0{l zz;`HRhnd|sTB6GIFtkoiQn8ixGs-H9wAL~(Jrmyl+x*LSWL~jsAIo=#6=GPTw=D5} zt|5l-{>YVH1g)O9EHNX-X_UikTwmxt6bDtv@#kz?J`KW?!ttqZkBJE$XMFgntp|^D z?3Pb%Up1d$`+~Iu)@g7`?|y~9YeQj&?k^E}(fUyW@4{%7>slj-yXPy2BX?i;+|Yc> zP^|mAX>Q!(F*kp^HRO$E`ee)oD|OZCOyt-TRW(hjAq;5nIlyuqSYch^Y{I-vZy1OA zZiajRLfdyHxtZDhj$ArAyniawI~6&`1T3Y8Sl=D49?$md;pB0=hd7489LHc$i&i|p zz-=zHSLodp;2j5}t&o3h&6eKDzTk>xK%tLIUE^BhGLL*#!YuPw+3jab$jTB2+iGJU zj2G`%{Y07DpXTf05T8JwNk@Ky>*}z|4$lP4Sz6aqk(HEEc4r$mxi3*GY)QGpDD8gB z_V;YK9bD>DV;r>#+}5UN3@7A&S9L|LT`{Ew)YbTMqMNz>4s+FY$wXdvZJ5AuBk_o1 zoZ6YM-Xrij1cnsSn=(t<{c?E!Da`*tLSH-I-XidyWJs=BCC|-U*khXUn=OByIV@LC!OVp}u3d?**CbD5s$wZ;Z6B(c*I$*k|@AC zBhdWP%_4`jscxrLyuhcH@HN<%O5bi}XF2XG5AhzCBY}%*y(038)T679s&GVwta9|f z311}JbT?~4V*Wbok`oK_XIAwUT)+PKlKM}opLB<}9E}K>#;|&(Oy99`D8s^u1O-?z6Pn@-IKPOmJx4FZ7NcVAFa>Cyjt3J|7E_o(5OyrryOw#fr7vD&hQQX*d zp5}6O%l0JB_p9*V?jQ=^QFZmGCS)dfTHFIX1&^C|R94qOLU1c;6SDQ1`pT}WsahCb|+w*;S zgp2bQEXf^=apPW@dGh4=zLN4({|g}QaHlrr1L0xq?J_-!sHzM7V}AuRId*}BE; zSxJsOEN*r0So_n~*@cgX*mCgMP%fHjw`C7uIX~X}0jr?DzfM89YUalcVS3Xuo-F{P zhA()%{KtAZc~S0Y@4F_R6I#udvxB=C2~y9tA6Fz0=A~u-qFbtZPHph84`8zbd6Lc^ zX=pgx{SaeHG0X8Z4u0=(X&9dzrq_fy7nP6+dVe1O>>{?KLzou{*>!l=nt<+-@Z5={ zAFc~BWFlrJv%PgZS3OI*-~Kzi|0G^hD7#$fNaXkuRputueTmCoUDI68J)$EtlZ4Pd&8$*Ozj-HIPn6F)TQj&!*bH zt>rS+0$wcGG5cyv{OSSeqh#P3;S)_XmQ0R&b8O_Kl)4UR@s#i#PDR>Loh% zp+7*!By^I?u@BnnFL3`!+!)l|3JP6X(OR`TUJWc)G42ve%mLZ6yS-bi4F2Xo z&;`DT)#ln_i0^_Z7q@ui&Jia5xy-0xeOZF>g4|Gq_ZIm|yyXFMJBtX)yWUIBO4KW# z6F5?6Z@_)*#rWFkeD^SY_M;4LnQa!^RKnSsSJwBQuO=n<7;hiFZb_?jv~ta*9{Vn( z_d1algr4US*5Ep8OQf_n61r*Zbpv5?@Hd7#jhPZtrp?87yBI2yST$bZC{vuxxlOx& zo|3FgM!M+SWTII>K5{qQ2~zGtY=+$ld-((A$*(_Wc#ra*EVZm~z$UkpMNfub*0DP& z;YVA6fMY5fvFBD1qx8pA^_~1Y)t%%&>fH0QAjTy~C`Wces9X&xyKfn``w1;hFpAT% zDaQX)%uU)MJ;?QsE}% ztPX>dx<+wjy=Ul0eSkLzz=64?GIo7E+<>0c0N$R({*7A!7e<;vP8sesi zVEx!{W{)Xo8?cC>xMh33l9lj>+LC6m}LCwniJgO^)1-m`k@PrN^(IICfkoU<{q`@?Idld{0P(tu$d zyL&IbKBcsC1|6fJ5~QyuLEwlC|H!M=0(6+GyyUOd$BtFEQW9i^J71R;-eVUEFi-{5vp;Lt#F zMmX2)af_@an4p0Cve5aa!uh7^l2{cH-k;0~Buj25Nnfx6Y4U8W>}sszEMeqQ&nVne z6?NrmO}S&Z!Z$qOJj(OZq1JfZJ6`pi&NW%Lx1I``d|r)LsOy#Lb1jw%Jy`+W4&Er9 zZw7C_z^@Y+Wtb-g19)$*7?5%(89|6&Oz93&uWN{}_(YSOaAuIrTo|VgBxxl?C~Ma0 zw-QRMz7u1(SqmK>Fw4KT)`U5WIme9!f!*O*@}VCVw0X#D2V4vV`A!&PWcWytD^Ivq z=aTC7a=p!5gWh=G8>~MlTF$d?(Sp0&kxESC>ZQ;8w6gx(yo==r8#dfRZi&Op<+fui z^67;05gIC55sYUxHu0(3lJ+9WC|0vc6C9xklrvi=wc8Fz?o7f$sD&0yfyKqQyF~hM zDW7eOU~e2KUg;kwYf&qh&JE0JE8(|3I*RZuAP+`%?~QT&9wvW^)K`lfF1^d8ddbpS zuykhriHeggxVim$hRR-~p1Sf=Iu0(xOdzr*)y7F9`GF(+u*xKG2)bht}g#i7838uBAu4upa#N`Y^tnaD9DLSJ7`uPz4< zHsM{Fq(Af8?9v?Sye@bFoVxCTf|a>*iJ|;CP^N{{Z70w z2k*~uN;xhm1OVi1E5z_pE;Li1`->IlwTaNtF4LHz1~fRpV`Q7O;G~5=zS50+UJB+aWX`%`;^1dZNJb z??0yREF!&^L><|xp-kH}#X0;c%7I5!G)CD^DBLGh^#MF|poaRZj@Q%Ac%6mBY*sTe zt_VCREl%m`!Fzf{>H8Z{hS;{0V_Vw3>e7gWTXycpkjU^5>pkXZy(xOzL%f}+K4S@& zS;BYLXh^qa4JyO^RfuP1aYzT3+6|QU{%pKve_HDR$b-YN?MdsKozGaT`t!{0k7t^G zI9jDsU%&p?sWsQzaFJ$H`iqPN->qCLd0~b!@M*Nta~sfi`oxbQv9iwK;#HU?#4Hfb zeLUkt*`t7x{;dfh8}xdarm)S4oX=6lg??1Mx~^WKY#po5afanNL%T01nIrs&ZC${T zD~N9t<{ouV5BOU$;Hds$-3fwOf|lt8?niW zF}@>Pac%)Cp`b+CEOxr(B7OmLsm^NAeg4)T4q;~^YmW; zU<0yRmPB25qSvSleyqDWS-15+XU!5yl*C4*8I;qwL^JnWg$p#28d=`o%WjrqTQgvC>bl*8H@djOE#TwA+&sW#wU;JBLo9a@VZ9kvbKVa$FI(Pa0 zs^mM42@*{wQl%0wpc#^ecEgG z*OeXizj|vG+y^g*`Zlb-fGNNYqQrZ_dLcYPVt-5@TxSZ%G9^1LgiGT-Z_usnMbcS-|RUF z#$ukQ_)jsDp-|?V@px3&0}G8A3}LpQgTZ!@MO_3(Pa3Jw#8=#kr-Kc--z+y)aBf!o zgM}8up?U^$%P0HJc@AaZ0-t(JA$MDe_|jO7Moyire3Z z-7f#&e5Ua=J831zaWUf@^ElKzViEB#WeH&p?oS0>DD7QpF@V_B5%v}>c5{*ati+{| z48zt~1hmhk>OZAycJ6tb-4&q44}XW=)vm6Gh+?!jKt! zDt{3O5h{sU+KyFVhv>R>8}4pv7dmr^o2=k-QTb8#*V!dOvO1XkzN9&{dA4*K!&|`_ z>+9t_33Yg#!S`l$3KrP?(FkT>Lzut7Vw;2~uJL~>EX(`;trkg3iIU+x=gGhHp8a>2 zUpxL4lx^wr{HpKI?q0yzLL1|uB4vq~MY>tkR|CPOC(wrbA!wI!9$T*@-OBDG{JtZQ ztZ38(A65{%6wS#vk?hRX2Xdh?FXr4#zV{aYoSZVqoqbkMpQODI8~2NX2yZk_wc#3Y zNs|WI4sra4RQ*l(%g`Ht_BEYtuO5-ZWe%-u_nty72ROMQc?zCjGHMfs@&FumTUnNs4!9B}aCI&dHqWJEe~nUDDo~ zeR@1e2x6i3I85ir&EnlUGAaDW#Z?eM9EcO<7A%c7%Jk$2eQOEaLt7f=NJI5~uulV_ zfzJDpKqIaIYQ;uLhe9?I6V@wH4)9Qd*E_yc-w`OWzlPaXFJ8zuSMxhh3O7gbHen6m z(@jSHTh*yWarW6tSMCZ=y)d2Q`rO);#PqCGUCdRsB;gk=c7c7f!oHbsXvk@}FA{&V zc<-;8L6XgEWfE>CICa6ic94tB@AJ#cssR!8;MviReB#4X_ zOlrPysS3>9!H1>g%@{OSATaTTq zcShkvRJ&v6BJWnN_aVsU@eac4blZR>P8N%~ld!sZG329k??`PONLl|P9Y|-cBW1TE zQI`v!>fmc~D$Jndq@)~5OT4->-upev_oB`ptv?Xz-^TP0XHyG6vz@Lc?~;q2rQYXz zPbGt+I7#VhjAlfxYe|tb?e>TY$A)98I(;WPd(>&QwaE>OROI$Z?5#()F0%dw3g_~J z_&yZRmfrHe@}!!K&%4vNKTgz9ke9XMoA+1OcQn9$t-K`Q>U_mnJmZDEfkfAv+^%Sy zH`?3JB0kf(4*~A~UtHjfS>xQkYkYCa$SsLF@`SbiFt=i~hWkds?aRrI-{}J-{Cd=I zfVx%6Ro6o8;0aW%!?w3gN!-g9-Qe0#vqQb5(0f)JT=#Y}*QwmQU)4FO;%TM#Q{}lx zXv7*AIrv9am}>Ma5@3eC^J3owbyu2tbFe}R^NtYp0RedzTAC?XZ%U{E`H!ZOf7KXD5f3fnao>!0@+4 z>Bf6K4h%3d+IFeJciwX_16IMYF(vJf!Y+VF3zd9uclr3@xhWqVEWPtp)FzLxd~8(M zY8<*rw(-eB8&;h6eEh^Cm8Vi`%JJUqaE1ng_XFbpXjn8D&f;3WQ*E`W0T@V}4|VRr z$vp|@XYG(&x5&nFeDgSOBBsH2O5b-9U5YFwmV3zTqm9A+{0)}N^r@MhX}CG<1Lak^ zr$*^~U0EH$^t>u_WXpDkQ{HfxNpT)3)vqkkf&HzNI~Q;q3&!1HGFG8p9KXQfR|U%} z65}{Gmyt8vBb)Oi)w|JkgB=Jl*tzKvihImw4(A_-xM*isc{ef~hcaVFJ-YGe+@qka zbj|q#>zy)vZ8E&FtR)&=Ise|D!XFJ0cn?G??Z1!K^d8dn9|Bs2#LAVjsD)Uj9*ikY zolx1Ubp@WRqjHGD&v1g{NZSs+1+`s>Z1yC^D#$rA?a2ZS{w-^|I#4WwTY;*KGWRx4 z1X?9L-SoXQ;mn^p(lq4f}=>vgNDX)lblUHFRY-(v146 ziuu;!vhFi!-fjHgaKNFxX3xX?c@j;DEF7m6;hOzv8rx$m=VR?sOGiQ{v!HTBTt}r? zMTvnjNqq(LiUVd^2DzQXqwVJx!CG>eCLW8AUYt4K#+of=i*BsU757aKJ`y8Y+Ibdsk^U)tyI5u%nN+VVzr z|C@M-JOw=(m@gJvJ#dk686My;c-`SYh&TQP7E%62LO&Lf(e?ANBVs_`pOldg`c7+lzT(Ab7~+XgfnEDyG2W94-5Nsdu+-&aiU<2=VF zyxmwac%$gHqNRZRmi(B{A__GJy9b)CO8&tEq$0t;ls$s#9~?&x=j?mbR5nq;IatVX z-6Cu(;-$9uHUa}UhNDALogx{m51RBX^uczoFI)3qcQmz(@A-sZ&kdgA-ff8anWycd zF0jacmSZ{7xf~YfqmX{Yrn7=;SS{4+R;tyi(Nit@>A4Z%Vska!IfI%e@ezLI`-7Jy0O1z(k2ZpenIqW?ppsFx{;Vn=+SnpgU4d!8qxT^@S)ynF%r~`Q5 zu%Y<0kHjTy+){mys;6oh~t9eHwGyllG` zrUM5~j)5IZKR5mD2Re7DnSAF2el;|mLR+WeRZ#1ep3RmGqElVTs^;W>)Q#^FxE^S1 z!1gHpe^nX->fXS$_8Vr|X&@OcKj^K%gNZXsW)3XB0S^=*pZz5dnmD|6fujG9Z1*P4 zSCQtLh6ssAj>Qcmi|#k?BPMCg1I<7G=1T7^(_>lx*S~+i$^L)o_`|&pNS04{JfJ2^oX6NTCr$w|$%t^9mk@RRw zn4<##I>3=22GJ18fAdrC?MiAMJ@@xR^PekthzjUHcLAZ9hZ>Mx3)J5vufaU%srNzD z_+wB&NAuS-c9RhP-r^>noJ9tX@~5)d+ERf zqmqEmzyV*eUc3sYF?ch0rWsznaX=HeOphkh9e{VZu-GN=gd)CIU7Bi`Cx8G$0C&=D zqDJ0_T4&%0cugx@-#(!gVP zCo<`20t2X(dxUb20SzM~fgk$I5NnMA+&)=00_BKnc@8>iS?OMvdO`}MZ>D(9$|O^%x&8cgCNuon3uPaLXQH0U`+It z&!vTl=;?6~LO|$4%gp%^Ec;`uDg?`5(1YSMfsk_%;1RQdGsp)-R09ntz*{c$%`%V2 zZ315-H*0u|@9W77Syy*e{!EE{v)0k+w8xRy`KeG|0 zAwB{$HUJ(Gb%nMbtS{)@fNrxF$!-pKI(TC^bupSC1o~3bvQCIZHYfIWhN0+kwOuuA2op-?Op&wyn|ZgpBT8OEC71#R@z z(AuLNT4^-R%XYhGqRj*xY3exyoCm#0+9uKr18OGjDztdNvx=s1{Ckx(lE)d6Ho3l$ z&rZ)1_?z>(@r(4#IF0Io0qSWqi{u^<1$6%E5t-V7#;;XRp-XK=%rXb!QGoJ;>Jf^E zAj2XMn5aV33@2EOf&s(x9`y}9;_nn9;bIz@u@$LR?Spx!mmaw*X$T^+l{rQLFqG3a z4D7vZ`@QAuS2~X9?a!|BB<O`P&0h=#C$w^k0k>DN00-QhV>Bw$SDuVC>z)iqY^y z&=;KsyN|si(oE}xBOO&V#SYX_;n#@xO>^i$#*5a`1Ym@|9#LhS2y;L~bgA76K%I62 z%9=LntmX$+sM3YxAZVRfq&-YCP|!{SR7Q*QBus!It^_O$*#?@AfKH~_bLJS4eLZ^0 zfyn;QzD>?h2MPry59JuAC(Bqs2@a+)>#x4vwMS3wB2xT_>_8M#Gd}&gxkE=h&Zv9dcU%)l(>aS{YKrhX&ohqAo;`k}#nyylEX-HB zB)y_%Ywh>8YuPKE>jbOD7jpZ7EaKY~@aABp=cf0ujm_J}%B9yv3OvK&Uh3Ht>fgZh zhS29xYy#v60pM@m@RMXTBIg$c4Ft?0uYKwCOZYqq1|Yns7H z3w9%u+6A=0d$3Zprx&K(Meop?3jwEA!ACQY9*LYrZ1LB~GCw@{NTrrCELh$mxhzgN z7?2Wq6ePTUC=?cHD?-*Us287s4pUet30Qwzr0{lc_T^_{WK9#%-C5&Y8&=T9B(8`^ zo04U_+!j9j^cDNxvsc*NaAV^a?G63UEhNxdqrpV=z!UtLj>Es=3G4>+i1%UMKCbt3 z?$@X4&{5~4QJ&BJV?U32%jy`8)m2@n$+;cRv3@c;W6iM2thdzOwt{ntU~r1|;6eM& z{F&F{_X&-cJlC)Hd5v<>)bum!LeRU+ZHgKN+3{M;I_Jdj3eg^CWGLz1R^Bdb*{C6p z*RSe53L}2Mc^S#LnEw(DwxC@x#I}(@CDqT?OAP_5f8|3kdkfwSJWNCUzgg})}yo;Lwl9pwt2o9x88640%X+$p)tPswH)Fw?@2G4yp zZy!=Bpsx^JlSl*j%Yg&*JL(U#`z=zzR-Az}3qmW)LGsgfmX?*@-|#Oiq>_$`XD_v{ z^NixVmvFlybe>)9YdQw4*HcYS;+nF(Aw}v)e)KU$93MwrmlyczJ;>1!=EYTP%aoqX z-&J}?PIV9B+lsm7y?n>B3in+3AVW5GYowa8q(h;StWsU+;c0@T`%ST=TbsBePkLuH z+c__$CHR{Ur&2vF8jrZk38Y<4(~iRXrvQ+Q)=`y<+f6i;K5Px3mmSyY*CWIh& zvMEL4Ss?7umKS)WZJ`myA5z=b*2GG?^cmeX+m*q~d9N6MKrWks4p`tKKB0l~p>X{9 zCnF7HIZeh!XD+mfA(SR^Wz2|K2eli~Sl&*+0Mi;iAVvUPFaHI{ACe(X@HKF|(eXPF zno3^bB=DXZe~st@L)Xy`3bO>qPc! z>r0BEP4Af!IWCsayy>)eqdW_qQ5sGiC2lY~>a~)_WXJjZnIYa^HCg6{JwV^v9%=3? zw=O%ia5boqf!J3_C2|1mw}E#=E49+vw;AQVr`v?hOJk~Os_HyW|2)-G2)0S{qUig& zFDH_PK#M;GJ^YXJ?*UNlh~AFk7ywC%KEw+TAdOb94M@*rOCADHk^U^rtwYW# zAx3#?-?pM+nprL`jvSj~BsR$W5y+20%t3`DM+3m)>$O5&=bz*Nqr zJ}J|u$~sK@H5<9X$|QSx!D{U&COH(V&R2J@m7I%D{5-OIIG1BE;dfAP;hD{ulEYD= zblef@v9IOW{>b(x#S+pg=@TC(LFf<25U0nxHfM7oi1C>rj6a zE`5%HsJpNV6G~|98>BZM;zj541u!lz>xdfPm% z>CKWD+YE(ccS6aPIihTNFoH+c?r?s<^gqGdmm?l;^*xWlp#SX|lA7|w?H0F|TUwJU z=`aZ`ab?>w#m@W?Fc-h#Fqd|2i2_t&3?yU_DG$j9+&`NbH0GaekKpVI^+Q4+8sQz0 zaL8(0ZS^7Rb6158u@--`1AX6QJipHKdfoFOUOzMI$)dFmyEV9~&tvD49#v!VA#<Rr%$xrp)W=Hk~JWf`EPSEQ#qHa=4QyXQftfq^ukygPv-cb8U;gbLB)6@87u=s(ZQ zUyVO#E*x|$ek))7Jma1KT0%)!ksg+C76$zO1mZrn>UpR=f?P;5y-JCoMu~tPC*oGirHgsRtUaG#KO(JuN&Q+TR#M;8>*LZdVR5^JC- z{-E!}fWZ7-4o%aGsIExIhatSUr=SMOx%7FERy%B0Nf>NbYki5dt;DWeYxT_tO^Sdi ziBob&Q@BS9`fqCoty(QGpTs-ulJoKG&+FMMqSmLq-3n0z3wt*zNpqgLEZDKfUE0@taC&Uk$Q@5wU2uLajXB+7 z!OTlV>6%h;!}npw;uB4>`#E~Zs3vSkVmo#voJZO-4q$nccd@;m)y(=Z$EQm7r!>eB z>BQ7*iy)HD{5!!W^;jECYNX`=k&rs*{m7uP4HXGgm*OiTSniKmb;zbYvkkIrp%Mli zQ~q>TFNz*FU)2;#j8*i@ta@b8;3AMm`LYXyT*%F4Wh2!{)B%c>cf`t{MS?4@ddoN7C>#G8) zmQBJoExrNz?cDR&zDZO;lyZsfih|h9kZ3gRb@E{2Ar%Fsff89oPnjOBT^{%WpI2Fe z>6+e!;kExh6RmoVE`cCg=uiV9cGIPdn48EE^dZ(UI(6kVg?YnWX+@Opq=B+1-Axvg zWP@1Jm_iNK_wQ%5%-2qV$wYl=4wKj%5BEj#8gJ+Q!$;toqji!3>cdwSuXCS0fG)9m zi`7q{m4LE+v~M?=Z^_bGS2Eukk{4b4q@sT>rs+sck!UUWxVHQ;>;Fe1y))mRssl_A z=sv5eqEsPBi2fZ^u}DpL;|@Sm)MU@!k;#6XfC>>~Qt!O`-bZRnz+x zuIV|H-MnnGCTOd-?aNLde0=c?wk5xyQqK{|VcS$y3$B|BPEINi_JoWwM(8Y~z%x{U zXx>0E?>}xR{D|?pBRam0=C}WYtkic3gilB600@vvlU-vaXJc(hLG+-LJk%0fyQs&BQm0hzwOaH=@k!q zb9vt)`zK7wsCsI-*C-G_gL$=)rS==M!KUT}T9u-U#i4K5hFb#<@p?abJ7nh;R&YnG zJrrA8(DSxN)Iaa^wGO7JeSv#yp?R>b^J!IW8o&NOX-eqALzF-TlT24qx;E3&dPt*% z&QW?KAV>65HFhYf#!d-%<&;p3)R4D61<2uPdzyhw+mI)D6Y#?1!|PLon^O!d%E8+9 z&@&siJ~h4Jt!aB`7CpkN4-`(R;j){NR)%4Tt{)Vd#^{5i>2PPEa@B#zi4o8kZ#61o z@TTI-m>>2<#srQ>>+HGd=D+nkp74h!Wqx>-6;sj#F2tXg@`TOvGKFn%r5m!_OF>oW zAD9)Ir;aMu4iG9+t6JJrBUtC?$BRxJ6!a~F(2yGWV4PP6${;>Chh*@_vRu00fgs#S z)w-6>FG0n@m~fs}4?wQvXG@?Kb@j&Z!f0&$2U-xJXJg+oue1=LrxC^c=%!0G=GG$Y_iKtUL^+>lCYjQ@PL!%CNLe%0?=GIPiH zQFEW)Y)P%)P0w#(fb3zvRQ_ziqGcBs0&MzzuF_(Cup&woKj3N=k-h&Qc}m98S>0b< zZTG*4`uL~)kI>+tYHWi_^)0#>g5{K3)%i%O>SGXDN5}h5b-?$ibr8SpHS9e*+@%Q0-3-VvG}gS(?3mv-0y3_PC!`@T5M76{jT=KV^G{B1hmQrDsHq} zku9OML!qZE0E!HuvCU#2F!36{=ZZo3;%B)2LC_cP!E(42mJAW z(=bJyqRWf__uBmja`#sJuevY)M-0B-&GCQ0<^R9&s;RLuXb0G+4;1U+I2rxS=qL8< z2TCOjKHH8esG|&tJykl@qm!~u6}>v-m2V>o0aZzlT1V@H(F0DzPwNL7ca^pW+J8Cp z^hSaI+rl+uyGZRy!}o3q4rb`OD)gI^@N0)>1NI70Umu2gK_7FqTYVDEY#T$>&QJ#G z7e+*{*=zlyQLDDFKPU0P&%bbh?@c}YK+`WkW>F(OW^2PhGd-0{PJd=&q`8Y}cL1rU zQE6`k{*x@?zU{*AXeh77S7r6j?elahY@G@Ho#9>BcM9(}3Yhf?fF5X`)RgxQz(uig zYrt(aA*3;=rlv|RXeFblF*`=ez5VjwXMG-e2IbZw{P}@|Jl^roSP25^h5&e_KSNhO zF^t!Hil0l5zipVdN8EQN8_SJ|cWlkrHOO$Wpv+ldhJlJ# zZT#;0+zg)mJ^rrTmJw_sf_*SM(fAPqA=Bv5jcTTARk{d}iBDl$rjZH1H9uzf? zf7nxQ>Z^OXYLj3Jw2XWyCq~O)m@PXc>r~3eGxP1Wd=F^*B6h(nvHk;|?+SlE{ICKA zh&y!Xy-R>>XmcnDhwAU>7XAouO(^04(bMDG>FFHL?i_zVY2EuzkvIXZEdSkye@117 zCaF)A!B1%_b9g_xB*~RX=@{xsrKe6=UtVD?A3*U@7Yh8_>oQ5(0eP@;cH!GY7^8~a11aVbKKE6b6H{lKw!hU)^-mA*s`|4Xo z>TMjcL-9^p7naW_S~kQc&H&!sKA3U_VrPA(0bz=;BV3pm?_`pFOz(BQ|AT_Ida146 zo{u~737{=L8!oyA&{+4BUi(5;a3P?p)X(aj5&CMpwuwV@iIm}4te;am{QABJ`uwre z^pr;l)>{H{Y(JwhgSLqKhO!RPnD2MzEeLihP7sVy+UF>%QY6k4o7G;-4-`u-rAQ+% zx0!SJY9lpM?>aoGTWXsk=*YlX-WwB?_AmBoC0#qH-zyxu6lXa^2gi02cbx>vWxyae z#bI4>z6L(^W{l%_wvD*6x-i9CY%S{X9uctmK}K$`aRa z-Msg+IsRMxv86o)=6U+G)}XqxJf;0JsqKBi5G8(emTI`Ge2u`-3t-PYEe6{va zCU8w$CnarKX_TId)@w>ATZt1f<%vbo9<6K=jhIs;`ZRw(9cJ>p%KqnJ zN|C762M2Qki#SagfPyDy$($*&Jqp65Fh3nGeh5Tu!KU;a;pgN*>d-8f8O7cbp)VUx zu)5}2Ym+2rlkVHHwg=Q3yc&Aum7dxKbuBq4Xl zCU(9FaEZh9!BO;1$&>cw*}sfsZ(bC$CX7Qiao*NuLVMUG=TLq*Kn{PfOo&+$B6M7e z(gzQxvB^!52WL&>?F2e{UukUF@%%e0hRH>*cA8h)iT1UEHGz(XBgruRS2HjD`4xgM zEbjl=XDlN#v4Y5MNYG6lo4qSV+?!%TFqZ)jgL)GH1y{d+f0#iX*6{Jf({V$8_hesU zK3q}v^blKt^`)h$$?aARwGK^ke>iULp!}wopzUlt{vJ1_Hb^?GPjWmxX=tu37vDWZcijA#r9Qr z+i+r$%%a4y4=s&v7eIu2RT;RtB7LO;VWPf#7dm*X6ypA=x3aw7u?+L|{`mlr`=zOW zZM9~J$F|Gr*#$GJHUx4`xP1cpdNO6fOz*dH@3YX;ukh6>8hX?hdhRg^=;6-2iolq= zJb#w=N0y;eE}MmuIe6;|wkd`3rd-!u@7`QyQJd?rkAU#H$}L%Q``epEj6WUw$n)+R z#NIs`QB|&5s|nUM5QH+9sMXKE_SA+EqikE0*8v)Gc{RRz8g7dkkCAZ^gO*MI)3FcP z5rW|Bi7flRvxB9LNwSA2TP6kip;tAIc6u^ZPwds3W3gbY=v;NoS?9^j<#KJ-Ev<0^ z-#x)spzy|!lX+w(OCy$^7u!cEY@-IkYHHm-^44fcfM6P)`z$nRZ5~@cs!eqx4S$l4 zD53X)>>$@sjwMWeyW4i2Xy9`Zwr|JEu=VBEWCRdPtN4A=)+U~-Yf?@bLrCn2;@B3X zrK_J7UeflIcD~iQ>JwSe^E#}Di9FvYlLVwcks%jftO59b<@lI)g-vmHW{VboukF@i zm@%A^m{0MJIN;`F+#`YhmUFFCUxEcooVk2ouB18>8?1C3P@WA=+mo4fDbr{(>{R%3 z705+Wx_)`UP?-0pucZ(v}GNf{4=0r{D;k!>97oUAK4UjDF zhg@q11>x&E0>OUeBZN=?mdereKHSN8K+T9X6FK&r!JdQjbv@AlNPK$edH@B_zY)&=G_S>f8(`-ih3kk+=HoxV) zVO@OT03c{+)`^~+m)2KWWIw_Dk^UfhWd5Hyh zO5aI~s7NcF#Dg;J7A2&fO=ZCg*5D|no5!rnmJ-=^8PhK#(jGX;Sw2Kc(FJ;S`H|wI zk*0v`c0ia8T=y+XV#^&^4!L?qF7ShV&-BI3k5GEgr3^7h*=8wCogfos-6_NRO5=UU zJq6%x5(Q)87DuXFQLcoNj7R|V?-(P``U_CQ*VPR^gzp3LN3QB>(PszQquRw&e17Xf z`!p6$)E0M3^mTBa^F93;^Ns0Mp%nq>8UCkcqFC2iJZqg1C?CgTyq#9xM=(pw-);46 zvjS|ai%B|E+R%zfrhPcGYGAJmJP*c_cQsfL%jO=4>K+0N)GZ%4HMRlXIacQ34r2!o z&n(r42b&LcHL9%fEu!>D)_x_@RS15(8Tk#Y=89Cf?A9?#U*q1mtl|QT8)0n`-WJ?T zAZmYi4;S~(9&+TBd*r0hyQDY7oAI9D`=`>Qy9B^ZrO#rnRIFA>Tu>fAQC3xU(&Ft_ zlFJI(CaD~gY_ALKuP2b_Mn1B5doK{!M5Pao@R_T8+& zZi$8~3HJmVnB3eXy zyYN7$f)!P`i|hynx(l|zONj$F4VTv9UI6SytlJGN$z*4=Ss6|^l#UkRhZ$3(pV zh7D?O3O^z{-^^taM`y?-f9dq6)n>$tTNC;~H(uDXLo3@m@*W%apBZVWC~3)ygDh90lXW&eR8tSa%fosQ3Ve$44-Pt#@O~ME8Ef?wJEYY z7F%s#k?BTojHl(>6X3iw0}(eH_Z;j|j$p96#&b4f-f|3ZB|WL4bS5Z**VD=+h+&m* z^XYJ-@4CLtGMOKk3{|FkA=BOcjuRPpb4H*A_s&O}M1w2-d+c{Df-MDFqt-hkw-1Ts zZVhxu1wT|wI6ZS{LhxDFUai(s2=kLxgdf561+aBWHjUzl3vU7^C`-=h@gD+j2*Q zyz~fi=X#gF6aUU2_RS7UyR}{>YvEJ#O!IUZb@R26ynWW7eFj(Zsp(AGtCNjGMLBUd zfz&govQT0!w1s1yaIh0WXzitaiWa^PQLnJw*$H=^PT+|1+`{5K+2o%rvWzQfkC>h5 z2Ab9wzH1D0w1j5;_e5`BaDqkKl0)l;qkPNV9Xfb@MbONmj@cco53b*MjKT6ews%kc z(8}q3DEzdM3a80Ql3xbPEZ;_M zV33Rm#SY$&Vudnwum?XR@O~kHN#AbdP_N=E@AQ z+Jw%dx#3dfY%M>cUGRiq5%yuNeW{T~rg=#3fkNF4?|BbzRqyiE_{&5k_t@BBPM!7T z!!WW!qtb#)IfoQ}m%{iT0ZdX4|1u*&Btv84A)WaUeRB3x*n8{~_|$|i-!0Vx7+2u< zRPajg3-mg%N7R?;`)ZdUB4X%zLEsyKsf+LH@|F011}>NJJxh|)%T7R|?V6K-psopD z+tB@r>$_4tm{z^LI!$-;)C^i_T@K!V(Zf|eNBe~)j@eWeXe}4@#tB!l zJEwCtM5UhzS#5~@*z5-E6$$p@`NA`LC`%_;wL@XoxEOJhHd3ge9 zjgi9`|Cz3d=~?IJ)2`MYX!H*8{-6m?qs2QdD>K&wrl*I3c&Er-zlx|?&GY2`Kla`| zzNsqv|1Ka_tPl~UN}*8^DVHr4B8D-Zi*C6E!jgnY2~`AThCw*`RPbDrROqsC+! ziaLd-w&}$O_oc`JH*l3s1=yJm)mgvYyHkX~*xp&03FrP#9`+6J zp+r}$CT8IYp#twHnoV}Y&t%or>biLRN_>XbQ1;!W3ZpY$8~8XcaKFLXW!#UAj{)*# zo+zARhljitQQ=$ZDmR|$8DregmYojLPHo~>&UyIajT_$`${ail_C(*~t|&;8#~%bJ%kWRKNOQ+2(F9K|PY2RnH%QU{~hhJMZ65j2tX zua)38j*McTmzJL)1SWB^!e6<0P2J)T1j78BDLbS<5DGRXjjzKM@5V4 zj>XnkFFmPiT>zQjQ^)_iYWEXYFMvzB{L(yyE}%d1tQ>;Ir1H8N1+!8>j)MX+R) zK7cT6tk7;&^C`2|ueknl9`-<{K^{j#(pgj|%YCD^jvylR(*zX6B1KTF_~Xo-dTF)Z zf}dHqk06C%P6U&BjH#1g4vG8UlqE;n3|~`SKU;^&=@MJBrl;A(g{Go4+Ih_AI|dbK z`c7AQ-8i(I<3d;T&^r-8xC(^vkc^y#sxn*n-Ypfzko;Q6_1pj9hD~pmuiS(~%M>og zyz)0rJe;#Xw86eU<07uxT>18txFHq5^s$4$sT|VlvOohdQuDe)>*`L;^V0uVPv0B^ z7+HL1((C^@*3%>1vR^*t|BH;zfBE1eFWDoAqB%;x7^@puy#M*VUvbTD{PO?8cNQ)H zY!5wBiImFvl^Nre08co(B-VBR%{X{V{2-Y$qK4`tVHEigJzPvn^9I&1n}JUwbf8gV zwN0XlGZL-FKm81uMu@+ggF8v|5GD3C2=pLd1#O9iM?R4!Zdp@d1X;Mv`MSdMx|#~% zTpF1)vT|-+G1PQgsV}Hsa6t4*xQ<8M1=4JLJt7)7Rx`b;JMF;d2UXdlyMVhgx3G02 zgZ}v;U~mZ=jaFjyezz4y65&yPfbltAWRI~AZS#?AX*SYh-IBs0?otm zg)sPq6b48a(qi{9i9HkZy3xy{wJw(F%$rKCUp(+6`hmv=)w)oE#*(c0U|w3A1MyjC z)Jt01QPY{7Q8dR&;eAi>hqW$5g;9q)@I=DZA@~Cx%oj=PcSf@US~@6zDwd zTc(X{9IBq4c?R~%{!V)w=86*|R2az$AVn<4HJy7PM875F@E?_Rx4h7T8xXDYZ_XBe5NXno zsY0RVu+58vkn|q_Y82LNFvQZUGH^uMNb|%%CQgDYBO9wlDu4`xBtRH380kg=OGWsK z0zl5v#}R^M2ES(pE!}E6x~LYvWCT={8_5Z1*>x5CMDU{qrqQUITNcKgF$g87Gf(Hb zrr*$JB7FuzN`2nHnE$b4s4}NOcBP;fL9%FqlG2yO`stC-@^`Nlde;)?7~%U5j4V59 zJ?8aA2aXD*qmAyDSlX}FBt~%kQJJ8A!x+ZUODr9B2S8JC;Rc%Tf#5DP=7IJD-qTQ} zEjD5wliP-vTJ8lx_X72^%HY?^CZN&*z82Puz`RA^0}AeXH+-5WX>!HH8joI}+Zh%8 zmpKJT6T}CYp;Jur7QSr@FozK{jz%0Hm8J<%4@Mpuy^`l5-tJiGnMw;**`jRr=>;IK z$6Av?Y2jc6V5WO9E$AyMng*OEa$jNk54)l*2zCb3-pXbgD}gkh;5rO125%~nHkCM( zP9O_xm!diFkrl%VI0e=DOzL8l-^9Yk4*6a(dY{<$+9ZW7bRZ&~`qH}U$-wmkaj;7UbS3z=r1yd}6fIhMlYx5#Lg*k0 z5=|$dC1-othO3#nEL|X6yBq*PgNa%Z+*F!6%zT<_S z=X8(ZL~JWd(`_EOJU6I`?AC;a%mSo%zwpM$kObCF_duvUU4=Lt}BiOf}%Xo?&;rKZ)L9!1B@!(lX@}`mEXX>#U38?Ek2HI}G2<%@f}@ zu?C>g^kNGe23ie!XhejyPpH03&#|V^a?I4j#YDueE!~DLsP~A|zz9mXG$YQFR1qtZ|h!t{p z(mYVbY~b5hoO}$p)v%O*jfbLfG$??YlH`^o5d<*_7(0XV%^Cs~jQ@%v{5t)pkwF?5 z)|b!OrSR`kbZ;| z*$3=L$bK9c1*RAm3P?0b@u#gtpFwxiEXQ7?&VVo)m=d7@X>>uRM3rfN$pa@uBQ5rf zFovp)%~jPMRhw`vmvpL;njSsb*<;M`yAjXjXJ?P$z(P3MA;gP*ikCs1V~)a+HWkF0 zUuxWL{-^We(jvZl-Z)&#vmt6?a0-&8hHLkiMMtJrtZj>ywd8KP{ujmz$?n-8Z z?7jZ2+H7HGH;@J?fAO$4icBhEQC^MSCH=`Sj^?i;AxAH5;O!W z4}zVg6!cLQMoGdk|a=VyPNuFh|EQ{!7F*ElI{6!M~cZ{s(CyoY^c=;0J z*a+MK0=HIf@O^`0h0>I5a6UE+dQmr?!7#dx@oXyu!3PztQF(XWV4iri#cdsG!+5nX zmHQExT_C7picFUeRnCN3twvv4X}mp9_~H{1-UdEdYI@~P#4A%jivd(jmt1`z+4+Xj z^9ErRl27_VALaS)(SRr3VQ#35Yra8gKzIcUt^7tZZgleCbqi<$BaH;0{8~2x;bAA} zw37~a|Kb>S9RR!)0erp02tbNpHS{O?d2pZIKjB7*gz(_&snYaUfsKtZlqe@{Fjrz_;>lm^tv%V>RWt9Jc@5pFJeKD)5=WHtEP9PPevs@QV~pfKUF`r=~uL52T;1Qjh13}ND8ApuCkFV;ohL0G#P zM|&j_kbi1O`CuF%1p~yJc3P(6v&h7c3V_qujWmvX|zerlRny} z^LK-b`cOq$V~*4y0H=nnvzg>c1_&4d9pS&s3db8ft?A(5Is7EpXOf0$X*+|vH&3Qa zax7Mgrfj>ND=_36F5wpCa0%0$-ck}bcpjn-F(v}#cC83B<3{Hb!UNfCbWB4!!D7M^ zmjOp5|6@g{Qej4G-`n-BT=qJ;~)x5tJqYhAFS!U^Zl+Xn!<|3d<@ zf(c5PkQy09K{u}(*>M`Zcjd7BncL{^jWmA1$QZK8=b%R#50-`C+OtC7SurxKfUygk z>7@opIh(8w+>S$-Rp6S!%G~YCDvccVsPh>*A?y@pjkdeKV7pHWw@zZU2&pTJG(A;{ z77#u!+@iKs4i4odm`_eO1eT`v0N!xz9dJCAM#$?HW?1jb%ert()M<_d=R+DU0djsA zGr^yuC3s;3eM{E_*783Nhh^)_%F1e^Tpd8pQP-!c$HH|&0EpsV8kd%iGG}mRO*q$# z6V2`tELiee-_r{t-7f>K1mt zjtPN7 ztU#*4wdLdu6x_3z&yvXTlmaA8+#l)uUbu10oxvS+w4f#SaMk_*9ay%pz$Fe$h6v<25V@Gn%^9+5OKu>DkOtlU+fulF`}S3k!Tu{oqs_#?79x_5=oN$_M7#C9 zBOp)~jU~||{2XL1#pn4QEPsrKn8e?a#CN8SLPO%4fVGYxn z>ed43K<*8oRT|(u4n@zI_9wM1j#{L2JP_v}W@J66Lk#EFnd^n%?tF)Z5pK~D_aQKG zGp$HR`Dp^zXOOVa{(1MrCR1^E-$)`%EhoH+BJ(UwsFApqqIn?q^$5oX zu6!2etXPE(q$AOmRQYfpLD)NMuxsxT zKiZAWan;7@&`C_J5ASm_0>n7_@4q6{WHPgv))x zx#MVj<~}XQdkXy*U5t8Bm>Rk9z*x8~E!~_+5mSv$@vqRe>PHJEJc*Ej#qsu4u6tSX z9f)bLA(QD;@vg3!noqOPcC$8(-j*CWNkIS$Vw!J`_=+~xHc_>Hldtk2EbL$9WyEnT zMa(9Y{NE`7wQV)Ze9k`zhyMTtE@C_imJb2;y`t}nLZ}J(Basw;_v(lJpoV`z0~z$V zmkG(`FD~7C8cCL+V#7L1-(||n>+y!)a{Ab4{vA8(7`vyBhb@Kz4-=*_dQ%>ln4p4C zZh*vKKO$J0==(gnT)^G*me=LcrXN2Z_&77`9$#o!DVwTvy&5Mj1M*Mw+VW+Yypktlg*_leTVMF1GhK zEf~Gb*QG@p#(_lt>Y;wk99k|8g|WIY4Is{x=`(v#@d2GaP3TuZ{U3?9*u|_r1*iNM zk$Y*)kNOvS0(SuW3VwXU>~MUHiW#cFZ@Jr#F2CeQ1a1w{j2}e>jviE-$deBrqtgyx z!_T2fFNj0iUC!?>AIbPj`R~J#bpw#^K-G|=i?{4XalwKh2UG#Qv{1#V3Egz$u&@sO zi}K9lmGAdGfSA|_H^??h=b_MjSZ5nKWmiyHC_QgR;DwDioqqvZt~Y>N(3=c=!1Ler zQ>cWWBjL7-m%`$FKNh2x#^Lryj?U4iR|1&vXT94%a(|+rT5bd5&7#QF}8U-TaHx zgSUx-(7#k$ooQ_Dz|VJNfR~mF{Z%3vtYOJd*J$%VjGJ)N!TINKq#M3XIPi5T885YJ zu_t*l*Yfw4gIlO62Zpab(b#-df`2k?Px;fX{dp4y@BEEQY02L!I|Tl(1l?eQ<~Ira ztH~LHaJ`^)Hbb*hC9|n|Z)tJe_`c-4I|v=e#QOCcz>B~a1*R%wNmc*8 zD%w(#(^tmbMS(UZ1K&g2Nn0bkTCfjkug?%34pq{cbU0N*Qvh*kV>vDghA&Eo3a~j$ zWZDX|R?Zu5OcTt-kEF&*qlBS0Ve>6bpBt|~oOZk6mZBGV0>ZGj_+`Fn1%Eq?EYo90 zp(|G7A0MDzAF8r(IK9U>J9XrbIxBjx-0M8b!2{a?G9EcPPVh0r{$sIi$i|Hp-2d0; z?nii%yD^_0)6&!KonD{!kxOG3?+?_?{p*PYZu|3_L>vFh=(}+8@m-syf)tP*K@q1T z+c<@DlcL6k_1ZGRd-ZPSApM;}DOl9$XlXFoQ7;J9tGzL5C|_rAdou#vdBHbNRt`%UYws-hsO#x?V?FbnaC~VpYOmBhPHg%UY-;7c1M!^{blA{R1B8;0@WEsZb#vmumiESwoP z=NbE&DsIE)9dEz8ZflBd(E@JcKUyk{$da|v?nsZ=$6DrmEl-+B#v{k=Hi01=slGDZ zaJW5258f@_>JoP$k9wUyXk~Ys;+W0YC)}!jpQ!T+ZjsxAJ?A3Y4oZHqd5CXm*876PMK0MRVmuTEIa?IkE+coW<0`E>`O*GgOMOB%y_~H)I z!0GoK=@3>IF+>H7N9WOIm;0E33CM%a`25n9KZth4amZbSg%N%N3zT4uYXq)SQi_Lh zsq>oaQTXnwD`+J>*7*EV>G?GIM6RcnTfb|0984|3yX~FN-x>LBt54qHb4@ckrl~t; zRZqp;{djFZ-q%k%@u?rpiOf;=r)p*)Gs^G97uGQAec@pr@}t@SLO7m3C(dz2;lHwS zA+}vd^yn-H`JRId#la^IfGZWaN*#s~A9`$(dPYe;pln0i!F^TexY{YigbEh1hh@pt zSaU0kNUD-Cq{*Ns(tXga4aSq8QUmB>2=|1JE)?&eRb!Rafey{0o8FE1tA*V|Wqaqv zWncLdkTR`Az2(FHdmkfHrM=P88C=hFZhat?oaZ>{4&flONZaN2JpOEKZ zc_n;lf`XDT>kAWzX=1Hb!_^ILylfk1qdv??^8Hw_h`ru;=Gd}osK=bB;(cDZ$?VNO zqz8fotcxl}x~-0QJ}x>f!tcd{&ym9?jiwB~JEl?|n#_N&wC|&MpJLSM+1l7;(bE{i z$-KZTa^LrY)k4|+sOB=MHkEiFrfafJcV2Y5D^gW6j6fU%%q!;3358AyN zd&5tWHY{-7b|n2euCQAAHAMfnlGOzTSZ{$X6?3GDUtI4-_Rqv1Y*{>D!yB=`ccfSE z^<`E0dZOjN=*yQoy9KU^peiA1gw!auK|0+S%r-W8*nC}-%Z3p)sY4=t7M!|k3adv6 zph?*jruH@rB5eciia^<`217GkvSoj~a=K#uhFy$aQQn2DYf50=E&NcZ#qf zlQ^5{KzdE;zF)07dlWYuCGN-+ry`(8r`^o4n2{;6F>E$2{p3xK0($cSHc_u2#zJ2a zvoRKd(>KfeDytzw(#4jpLi#v5h42XaE_K8hz1izH!LaSd3fwNUmgT&FJH--%DfjR-kGx*-&>|=}0rn*%=a;9K7lBw0=P0`NHdVH}j z)M6y>U;9o&Np(YtyOT4h;%qBfB^}LbNmBQuIlpFCbEwx!zqgp>o-Wb3Q%R0p%HWIy z6%!*eUAeGWUNcW*yl-yH_Ma{YKM}jhRWJU`7pi2N-c9J<5=&U6-Ocg`bUJ}Cq?I-@ zrK5%JYrdXo%n%vz_k0~W$av?xnkIB6uYW%^fot^J1gGs?-sYD6y9J&PxpmIK%lrqIFOxP2HH^%Gu6s{w zHLhsay=?W}6dw$Q`dnp;gqHi(kkE4F=Sd?xP%R z>U4=5IL4m`UAoZbblKGtG~tDMgdd*4FVay>I$OE1yS&mEC%qi^%^N30pP_T|cMq__ zE>&&Vb?iL+&u5i7CIW|d&Qt3oiX1Fv1*Aat^)vv9UUR@`h zuN!Qa_m2@>t&ejQvjP)8xh9hHdyW*?f+q?=yRb`(MRUV-_TFTHT~YzD3oN&*dA z33Xoy`(*E4rq`P=T({=XW{c1sUM}~%$9?%J|AiFlX|yO|s5IPJxbgYe`|su0$~C)k z6jD%oDed7k8ViH7T_qltIQQkrup!^yqcCk6<;`(wSm8}-a*1(ww8fjuGiPA->7)F0 zDdD&1MZWs>RjDembN2J76Aa5MCHx(6#Z)}M|3zU_q-G!vbV`9Z>VB?2(^!|g%DtHg z!@>VQT%srD8%%o*+t-6^-%pd`k5MorFAbE4Y> z-Lw$o5|en1oIx+mXnAY2>zky6J=8sDR9NVILs$b^IELd;Gl z|F6XzCE-hvj_-1L-BpI`DTfQjQs*b+$T&oSu{Z?}j2Gbr0a5HVW*gja91>Z>W6*F#A)3$nJJT+(5#SlvHa#!o>OCLoB`y~`~ zE3$dr8)CzJwRw$E5gBaal7B9inl?$hVyK1lpVke&dD+~K^^ds{vo&xbCG^Kwx$6{6 za@`gkRl0CjUiYPBo^_V7J2LP$%)gfG{$9`p^j@wNt)BCY&gI0(ShfFfLGSqz z!4Z3?1BP}4fhh6L7=AXU6v(dfT&u9~3TYrl%4Mf>KVp(OLQ`6?KP%qq)eEsH{yc56T->=EocPm%A_LL-MBZV-8D6B3H=EFir765U4qQ)G9 zXWJ>S-NvSR*!%0E+`S54FL64PjEE2f#|xTb)O|6I&$IB%Ebg+RMfLO*;S9aR1jH6W z3(b6R#FtAWG@4!1c+(_O`BuP94qAeV17FVH{XX~vy z#n~xYV*tVtjZx8eyd_iP5RqkIeNDWk9-Q%pQl>eB%Jc58SpxBkAC4OOfdwFvhJQq} zjSmAfAc0%^?$8hVPRaM1*2jS=_04rl61dFabY>l*jpgIs{DgCZ5}s`m7ETl!3jzxb z-H!=djfPN(^gL5Kl}SA2z_S^Mig&#j+5$B!%w@v z?S5&CjhrV64ozxYpJ1swRw=fDxE0$NQaU}Dw^G`+4x65r5O^TrTr^pub!GaP?t2rs`0yvp-WqLe|6e7rrgQsIT0HeLb>M}4AK$HSJAfR{J~kG)tVt5HwQVo`(1j9LkIK+C0`hL7Zf^&pCx zlV_P5`gas<80EMm>?|vPY{+LRnJ*uRzBDz~`OHLzi?sqtw8uH0L4Oj6J>A{sI+cG0 zUGo4)`oBy#2e(ya$fax!$+B7i z-*6oi`p2yIX)BDkr@#s4&Rvo^Sel2FTbc_Ue3s{xgmaU0)NF-2p4nE~Ih-Y}nvh2hjL`ANwPN z|KRzbk^9(Kfze-PeDv{=UJccH2LhqEUoTl_FozAGdSp(|Mtg77S9T*p->#EreA`JV_S;0)jw5J=}~xuhR|c zDvDInL6s$+6UrCad|7GyLumsT^mz9reG^;6RM*g!-tQpU!T4aR|HNefG%id`Y-6HKa=xmASlZ} z%mOPmk5+uGmf3{;Jq6p9Vsitff);L)$L>%!h59blKc@Dc=7ZwS{3&@^fBB~Tk%*)C zyXn={PLO?rm)%}L)tWVrMHI_Z&GDG1b=Rbxz5G&JaNUk$)7^#GLRVeM0>1C!V!r)i z{LMd9p0g87JWD?w>KB*mv*s6f z9GX7=>EA`{S@i0$$Lqozw~G70>xZ51DV-M+RJ_m$M)!Nsiqo0Z7nRSkqWFib4RKV1 zBxC3r-w>$he3VjQnm_ZEKz=+{$Rw(SmLn%({5|^coZr=iIhJU{?xZ65%begrPF`eI z(@w4P5j^~&`a-#~E)Kt1;8r^2T;G!eE&JRng#IPO&^-ArVKA3v*}`;9mY!~wKJE+M zo&~|2F&VAm1K>iDuBx|8|#s&Qi(&-d1q5Z2JM z-)(1MVf(RJiMSt5yI&t5sCHAl8S*hy6JdQcclomS&yV_e*^!u4?*+Z zoEHRNiFIE4nc!D=r^5CnrSFnj8>)^J`c4XA3<=1vK~&bFp`!g7cYnk}7{5YSNI~$5 zplQB-6%cMHwtGisrd_7?byTyVRz{p{#&CLMoE~ zp^l!Iy$N6aJlDT`3?h>>YkohrI|k3JKc475Z^z%9YkPgYzrp_cCg-cJQ_PAtVfWD6 zQ%8NLv&G2=<3wnC;`OM?_Ux>AoWc1TFfxA+_l!yy?3pCeM^pFz@T0r4ZT(6u%AX>L ztL@kkJ5j3aULqjdMSD|`&bqgRTxfK&1+^i(CC-t>5GO}`aa5#Kh!YJKll^QJUYFy1 z0o$Tux{hQ1m(^|A#-j}B6<&|P9F`9)y?l6@t<>PiiSV}=gKrl95E9+Ienwz2JJWp} zbL>)kjv4bar2`>%r^ck?v+K3la z4y_1mD+u4UtcB<9yzLSYF?B*n+M?7%u?G_l>7=bnTV>cZ4R6zj`)m%9LI=Y^<#FZdrCEr8)0S@eDS_whQA_QI`TP%cdVkJ zqq?~R-Yg>UMr!&a0SxCmBK4?bP$$sJyS4r^JnDBEdx<8GUdKBFNKT3{J!7|}NKPO? zVyHjxtA#E4jORbc>(*m^`u%l!fato=ttH}DwG*ZD157m99-M54;hhF8NJHEwiKMJw z-T~4@lpyE>-Zy$cfaWN(k{F2sX{flrlz$s-U&B2{V?V)v9iXKv@4OJSVd zo60=hK8Ld%Ms}5whd6yJ@CUc)f<=tpeb6*O#jylcw&?+;!!2w>3!1L8MA-HX^-h-u z2E}N-8+!KJo5qC6LA;4gT(LRPdNxq+aJFN6QPpmB+Sa4WtXk&i?!GFXWuCl0Tjbc4 z1FuQ}vy3)U(FWYxs+^r$yW>@~HjuS-Q)Y)P2(p zyVfl@Y3n>MBV5a|J=nrfLBjP5$&M>Z&t8a|zs_wUxDKE3EQ=EW+JQdsTI^=xl>{z* zA-_^v<^P%;Ty5Pl>U90ZMVukMp(v?HI*!wKQL`r$?pZGXpk(rxJm-T$}w%JA3IgpMh2H8CqX{iI^0 z`#|2D1k0yqGOEnQ78bdcPG05Rj!q;ieJy|L<0vh-o0_oGbf zcG5M(?QSNo<%^3V@kLDkw+hJmP}%RBdGT#2)h5O!@HK(pt}iCr)l{=|w9@yn%5wW{ zwYY;phS|;xUhqZDU@KO9f_yjvCoV8;64JV^;yv4A()0cDxcN^rh7aT6#}Wc(jP6ZJ z;w3dPUfDJ#{Oc4XqT-u`W@DExO?@fb@JX}<)n$5I&#*GON;l)mP`l0qcjHwWiw7_4 zSEXC!cL=}o*1eh3S9y25r6bch8s~M)s zuvZygzzn^}%-sNB6XPKkAbop&y>OGO3PeQ(vS|kID#NGHrsLVTKbGQkrEdBjfV$dr zv7SmHn(X%-t$H&ey=vaLnx*W!e3G8_DJ6Ni%j3pu(@V#8!d!snh#=H1Fb^MY|EKZTo>eM%hd){U=<*n` z4l;v3ezkr}y!ui14>f4njo$d|fO8a=S_^eO?lx3vcD=y+!I3GwB*dOV>%fItDz{u8 zFUFAf`{Ez=`Y#J4DG{3Bc2u7-K=oOUb88PCB$i?Y`W1o8L#_;svf^+ZVNx8psJYlsT^0A z$QDgK8hb5T;W{QY8s*TeX%d{B)#;d$aBf3Eanl%CNFZATJ<7d0@d=@2`^l~!_P>4@ zq(DB*?Ne)0@koJdi{MO*CJ=Mefa`yj0^e-!eTGy0xUC;2`hhk?tu0otC^gF#iT6hy zZcS4M`_+qCFbyI7F%5elUb|KF8H@Y}%SwZC%DLhB%M^=Rqw)`{ou!(&Y4*fs>0DbT z70m1l?u))CAQ}Y@H#gvxUMi4I{`IO11X}W_KgoyUUfN3070$W&xxG%&cEg|Du5XQ#h>`k~L>{u3A$Iw4EMne>Pf;$KG-M zy<`Ru-Bv@^$~PZ;T3u52k0>TqZkOZ?$||HCG?cGH1CMEW@FCTi{^4`qpOE9p+E}BtBh~{l-&~+ zSSN`->lK}iB+nFP3|jZnAb>Zmo?8TTgJcs&H?C>r!48BX&!0M&JO~ zf0w~}-d-Qi@rK3e0%wxq%)B{?2bD52vsX?p!VonS16Wo6dVa4}57^ehYOE%*j(51bZpl6~)%C1X=QlWo9y+9M@Hy4Zxw2==zU|&lnmPpzZ6+~UNqwy9c~Ki% zlf937H0h{xE-Z&&cAj}=bgUNreJ9g|61%_$jPP9=cM_goAl_ME`f1LHWZ!|Pd1HVl z&nICRhE^H(MO!@rlc#bcIhsW^v2-e^mAPJM?mf^^ERR9dI)K|DO9El&2jQBGb@|3n zhtVwPM*|^&VqqXwZt}?yuW38hsFz*QJ69P&XeJ%Zl+RX@SCwtK*HL~4OnK>W9QOQe zD{j$cZuc~Ue_Dm@qy$ll?M~8OlbC6!rQ3N*Wc#5$KMS8|4|0A=O9`TpNsKdkw-_7F zn6cM6dD0sbE~MCxm5J#et+`Qg&4+i{MrH@6z<2kRKGk?l>cDv&$b=4RLk~v0TC5zjSo& znb@Wvj`^O8T@+xO-!ae z)Mq!>3$E7(3Xpq=x+_LAt^eW?l{BORf7jtMaPU1>CA(kf*(a%Vs_5dJvEaf16zkD<(Z%ph!Q`yTl7#PRKyS~@iE z@y3mMvyd%4&bAyBm=6ZjN`C@5``;C`jPeGSV>afjS8J2y3xz>PX3zH6Os~@4hLiVl zdLH5yMoyC~^o2~Af3m!9Cbxl&_vya5wm|$DuJPpse&qUpAG^8d73q(q@<*W3n{W9@|T6u#(o*Q{QXW=k+eaG-4FX_7faM7v_TgJ90Y%z zXZlq?e3Ovu2GUfjiX6@Ld|)@x;k^+`*e`XxU1~H1=;@=I6)b9^fhsa8dN(&Rh!)08 zWzC)ZCcx3}pH!ez? zi~NCQMFlQa%T2aagu1q>dgOFwl5NZzV@CCCY%E9?m9=BStpakq-R-s4($j~CC*5h^ z%kSO05aw;T!DKGX{rs|WMNcMC^KrL!I!={LQh4`A4f(RUem>j2Q!~EdY8;v5Z&Y9A2T_%@%Rgxc2ktwM;LhTS_UC+x;r&kUdijeav9Sb>T755{CWT&qCqENo1xI;zu7C2y$ zB$Q0nqk`6~*!Krk|D)pwUS-$$t2^X}#a-R){BWtr;Y`|$-&z*v2?WpxncUpRjrzF? zz!;9=k*nq1cFlgJ_!!gFV)Z1P6Ltloq#XkJIL2b{M#I2*9y-@ zV_nhi&SLUHiQ8lkn`oB!@dsnCT`Dm&b*RY>$4rH9rrKPEPiK9t^H=D)qvhA5D*`oa z_flhMDY7?FKN=Z|RNFe#adv8=U3Vh}zx|{#{G75i8rlSoDN6Sg^|O#=&;vf+RH1E} zVQ|_=vw`kbQx|l$B&;uq9u^)WJ6$D>P;mxdlLr{s;aBhVrN+*ldHkF%i+|{sZ3wy` zM=Aq97o|Gwv+xa#YnXhoS>FeI;7w`fBy8)OnL5;3yI3%Yu)!aTKJ6I#qui1fxs^4; zHw&7VGoBWDvl5THrjjNhXeXtGKFe~BZ@DOxDUp!n|2p83Gn$-n=3I(%5T| zzP0646KNaWr*dF=x0h|qn{rSm85BZr>Wh;zQRZlRv!X8ifoQm|phqSMuBn6zcfz?_ zuklV|>U{-pH*zJz$n=u-)xwly+6wn9TTc^kW#C12#-K;<+Cg{( z#C$uo%&zOuxI0|P>5f<@ZP3c@#=D|nUFBP4r-6)>4E zLUX%lmW==4FPq2oct}`~(f8=gk5FYls%q`rocfhS@^hwvfMuwx@gI}e0_OS+yKHfj zhT;o4Gv*xL((^^kj=`nQ9W)0X^Fl#Z2L>zOR+cy74Zo=CZik~baUUwgYq0G0s->1gt&*Ec8FHDsodS>JOD^`oZcYe^$uls zEUK>9m%1N71gT?Cgrxnm5jRA*SUp34-v-W`gr$w1#$l7@XT)* z=<0;UWXORL0=xt$nfFh;+^}H7U;orzmm+UYp$p~eFX>JQ8Z}5;C&{I+LT4(YP?9KZ zh?d>Wa^K4WReP%-+)7+#5D(+dG`t4CWDj4mJBy6|BBE75j7Oa%+K|P=Ua3R=SLAga zxjXPyTN>7v=03o3A5cH5aI99GMjlNtr?AR6cj=_|g#gGxetn<33PagLa7(_A>D_o(aNwG(0(wobV~~aUmQ&;@Q-WRH?T>zCg>H zYTHRuet(|8xe@27#Yp&I_kL=J0U=YrRgujn46Ts&eZ_4Uxt3xt zu5}?tqJ;2Fo()N3keSfPS;G>26e{Q zrRP1>W;DWv^dPtei8O@nps+TN=*e@Sxp6Evj;Ngk2@fe5Qk$1e@Je2VtOjoI*Yqi@g%@D@{wbF*@IGqCYxF^plcA}A&d;ViUa#o@a-CCuIh3f*_k1_{vsu&h!mcw|&WhB{rfbh=ZE;tweeYMk|Ln@Q zgtA@pV)W+^7E@1EmAVH`WL6qmwXwVJr#i|nRfI2fTv?>4ZCoCAQ*NC6??)xDA4)iZ zucYY2M>+6WDaS^AyE@VFg_8V2YWB&&5ujzdIGr0ZbDPt!o;3FaJ2^qxWRu^^7~UfA zZlRNJ#qbqHU3|4I{-6Ayw}_pnj^FU!@1_gBee+7h6a&LZR%l2_rl;eBTRG+^F0^lV zE>jWe4uKf`1%v9vxS;q;bwHYq0w1{pubS!n8o$Y zBCfH*Pcg6ywc;0lH6eXdE;pigqc;4r4-BkC`TJAEttqC<&NXy5h-&X5_jCk~@`6(} zfO-6Q-N~vCspK|8=BM;AKTqSBCpx!;2_uX*gPRfT+vxeMT-=*W&zg*MU4IHj@Jp=lpY>nSR7?c4o8#GgPC;)jw?9|BGetc1WEHtsJ{XPdWy^Ef)+HS4 zl54?zHt!07e}&YplCgZDuZ7{KX%W@!QbD8j4jr{kQ)^bf_SeT?du;NlzWs&4tIPWSws&?@c6sYJ zOzgN#3h?to-SU6`;RXM1dwsXO)muKg|0wGJ^ImRQ4s?kApR5V$51@5K7)R^Xb;@2u z8(B1k5$LZXKgcQ0dJr^iXOCYTOPUP0b*^6hVBF*q@LWw&g%{}8uhP{nMnyzBG|8!Rx5jo1g@~ZHjQ}FPUQ<*+3Jf7_hN=@IayXctS>gr zX1d#?Low26Ri#nt>+Hls49P*Bsg@sD9w92o6h|Y5mx2p0F$*F@Vr97e%#6upXO6{w zTMvVUtTy*Ih3rbxkww(OpoZob(bPJc6Z9C<^%#*B9MF;LV-dj)G1S(lr@Q=GZt=}^ zi$4ehLN`A{Dw+(AEAS)eijk~jHE$_Niy-_;LnDv4x{5egP(7NOMzix6p>G(F`PhA; ziz%`d0LvxEse65z#}$DrL47=K2|06g{-p*g&Ui#9DrH)?#0I}0(jdn{}1*G0KuyEVn%t<36({_WRamj&-3y2jW5`%FA%i~Wp~-#o0&C{njY;uWfcFxf3w_TuQh81n*BgCdmC{T zGn!|sfNhMR)Y7v#(t9wV;c015vI;P}31E_KLJY?MfYU{wvyo?ZZi=L7PCytpx)1?T zC8<(D^>R2Vut(>=kA7o7c;Z-=a6V8{L8VP#F2E?hCXiIzmWO#}y^F}wc$I_O zl%%D=GWPYnMgf<6fL9&<_}IldfCz%e8@Kf>z9_waZlGT)=$WaS_Gaz$*zf9-xz>lv z=7`dN`GL~J-VmVb+%E}+Us8i4h7FDL;brzZ`Wf#K>w*G1V9OH_*_ft=xmeWZk^LZN zQtx0|)3>8G`mQ__lY8dWi3!_2wVn*Uc9e6z>=5`GcOT>L$=5C(@%)P%FYTG43-rm7W1 z^C)=u0D$0sAA9YQDxN7UCr&`CK6IbKHP_yvFa~m@E|x4p;rmdaEA4bGhD|j!a84ml zBD){KYsPo5-Q$#=vFh{F4M(uXLaDyl5oBOdoT2%gg5=U%b(arcY!7_PY?5dOU^bSA z1>0%9EQ(P8r~1k#yjLxd<%J6pCKz|-pF=w>;Vrun6T+ecDJ#K*k;5qVe z-q4sg?QdGTzuZ(iKVv9!sAtNy_PDhX^nNMCFKy|xKa$A`^av%7tBhq&+;!mH_qHyW zr3m~j&vc$~rAH25wB+K0yAsSpGjdGRwa0eZLsvS0mVpdS01{wy9O|&#{S|(;fE+5>+%eQH zI+)5Y>qpUz-9KYvfRqPF% zZ^=H7x+z(4*Xa-KRAq+2%PUJ)K6*Fux7Vx>#sW*1f!T z74Xrp{j6|X+$kX0dddVJ?iH_2e1hqC9UFH#3AfdX4<|8`cHmNCj=;H!_k}&m7`(z- z0}Ka?}c=!LxL zt;wCN@if@@qQLqf%XxO%bEa&rPkGugUg7-l{S{F9khPjcM7uZey-xPXvT6OJ{3vmy zS=BUn9XT{`J1*a?39)b(YsqJ&tzTyte1`-1VJ~lOnbB%ABlPLQO+9zPXM#88_Rv^a z1o4Cjuh~Oyhf1LEho=DdPk0w}Ghh=>usbGn+G)l&N-iU4@?SyLR|P#r^D3@MC+gSZ z+cTxdVn$W?h$Ftx9^B&Q^qf++tl0-68bpfSQiTN?_dV&;i%x2R!E_>!WycNT7&sn!9?StZMksSv=dSf^Gt72>s^ z4%c2OOAB1!h5xJxRpEtewELV8kHj?2b7GC#{jbhws*+BO5oP|R@~W!lNech(fuYRb z4ONvfgM))ki2kwL(2w^PN0EWU!4&JO(2jDv%JRJ0V#^p(;keEm>suJXcg+wSsDGfsqmUsIa@noqP4l-!xI&WQp)8x+-svy$qIvtjf)&{`Gn zDk={0vhXCR#!2o4UNNjhbUG};$OLU3(?Vsm2n)UEt*X0RNA6OrAh)xwn~*MLaJ6#QDVnhre!p>Rs)h`gPcUb zXgx$FWCMW&MRFu0JK4#8*1bK{j{axny{`BDzh9mYb6wL`B-#5utb5&S{eEd1xJd>RI08$TK}S^b$v7wx-9_v zf%b9O=jmSt0xzy*g;<>N*=IF~{_RFz3&sZQY=FmMh%pqOHTA!KIrUPCeiIjEs%~ zGH&nc>k^xiUMRF5uP1M_v!C|$Jg{O*4l#BIv~xgpJt8LO#Pq!&{1RBccqhZA1(xz$ zhBB{4T7xUX-Yb$-|85-*s{l07Qx1$&>4NJ|%ozsKFyF44i0f9xPRR=`reyRlSVQ45 zqz5buPDDq&7u%)$8#_UH$UA(pYm~@0^FG~JxL4pW^~hWr!Pdg8%Tr>qF1>8%?a$sr zk%A?iZ~?F!Y9&@Z)^^|gFnLK?h9U;W^!0uHAL*2^oZ$Ta5Xqw9j{HfUeRf*Gqz@nCgc!1--3Iy?0Gp9wiF%aVqN8JbtKWc5%zh0v4FrGNS8;Zh+cO_}AZn(`4`A!(+`Z0Exo8!>uO( z7D;Yk`&pkJz4Icy^rb?%ahJ4iX(G#%z3R=+XJ)Jg>_A2Ei)E=}>QBY9_#I0*b#RIv zSm_9pv-Gzsikm95H=DIRMXity&cidg1PB2vflC?lZ-1tJLG5yvNIk~W&p_CEOKnAa zPFe4%mNHjeG-?>4qC>x7MNJ;^YD#{z8rb@7?)ptWMNeuO z(W1DlTD_veQo*M`0@K|TX@xR89FhogSDD%d?lTNlc|K6KD;|ui*?m8iB;=>@ETedu z6d)k~6_}?{*0#0{hM?A9LD_{_Qunw_vosb;(f{qiGZwrdW+13+Ktz5D&YsVrL+IVR zds-t3RtINdpS&L=g9`W?|24c>XNy`H98tHx`1G$JrOb zZ*QavG}$@P5{E|d8*USV!5RXAdd~nc{Z|Gw{iX|ThFb7;OoI>ruSbFfx2TJ2r^f1? z8o`cOlTV|)>{XuyCTpQ*Z@O?BR*F0@De@~RUCau&U`clG$pZ=5d$_?Lxfb-gfr4WF zNVo-Uw(S%39TWX~mE?1F9~#@tFG2uQDGEICs|#1fcN*ySmzbb#*33_}7fY8`LmL{L zOas$0gyGs`mh({b49V`o*$pASKZD*M%Gts3|C|DlB%#V3b$dmM-o8+@Eo#w&J0FV5{*ZuB`G!n6enXMHqL7h54tNRV>4OItaq(kup-dCe) zyKJ2*ik{ zGJO6^g3v<#t;76n%&{HG8~mq1E+hmh{lTcte?KdF642p^`I&w(y`C{i-HkP~4haH1 zgJtcS1Q-%KQ~ru5>N-J2G#*N``Oh1$_k>?^!~LZqis<0fDRBqy74J^7no3Uvco-@iMR!Qa|JaK@P2b{WnkqUY%;73+SI-i5 zyx)w=oktioQ`ykiWUJ?V{zs%_l1+S92X~husAm51-Fwn=LD2g9E zAt+J?{-OLZJG&^ucAjyi+=l2G#bZk{nx~r#GsJ;!#pl#ulKah23i8RXGXIR%UeALE zeiN%o!@8GA%A?|+spN#b1NF|9;ERz0?AB>IxP(}4*H3by!BZV`oW{$^5 ze-hO|2-{%nx<;y;!`@hZ9ZVe-(t};=>Jr_4Tpjp1cI$62PfZeK79Fs39xqmhE*E~7 zvEW}04_A=`!Zlm%nf*LuX!YAPEs zYc77;fzCZ9hg=Zif^=qPbGH7acwdrdv&g?$M%80H`h8?1@c+{gc$JZ++%*!Q^qmKNamIsGUogg^KyY#cthu=- zAg&0#vWa!&^Ppine_P}g&1Tdi_k+{FY=ah>mxAwJtu+C&h zgQzF-#FALn_?mRBX#&4*Y0SAg?P~lT=q{|tR|R`3-WxJ#&L7bczcYup`Qr10U2^Eb zNDw;;pIihz__T*))6=V6CStXerVo+j9DGSH&FPAMLHA+HBwZ?2+l*Y7n@V%Fz z`rzbz6tmT8SBLh^I{8J^-~Q#FJIC@n#`=pLWK412H?eD4*BM=$@5-nAG(GQh74}n{ z{8AhfW}+hyc#Wyl>sFM$x(+%nq8sk=;%Lo;QNPU znl+stUny&EI&d`q+~5zZ5Bes*cP!ed+IQmQ0;%FxS%;I5X+VN6qT}DL`S-74v0W(t z@-}DcD_Tdu_aSJ_gkaCf+}VIr>~&^_EuulAW}8R!&4VnIG>Y+@q2(5CbpYf) z?|gAWBg*Ejc6Ge@sI31lzw&SiM2*@TrMl5#?_zN+G99@E*HLoM1tHm083^(cIL-l_ z%0={=fIe;i%Pg}^Zq9Z13mk2NOU!Lj1&F3tyju&D9S;ZY_YO~sQbKVj<}Ls8+wIto zO*J!)j5rohf(ZDBA#;xIhKeb}>rY1D{HXH1}W)^de6^X z`sWoZ#jZ%)6RE9j(GPS4zjid%BS!RMhjTFl|9ZrO#Ew2xGeoC^&KW{a5q(#PnpC_x z71rAD+f;xXo?Soxc}s$zEuoxXDr|>9GYT+pu@NotEizb12QzTONUSf?GlvK@6U~UP z#jxj4bb+`4T1T#)tEOL88$$d(9seuDxYpiG)lI;u47`b;y@Y3_pl>BlBM9gO9n<*z z(|%J^HKwZ$3f~mn z>8Y{!WMdI6bVwz`H@np|LD3n7J-)${X7Dg|i8iG?7JZ*5Du0NPRHrEB^665(Dh2CJ ziELA1w36Hl^~MShSFx-1!vU3-iruf9Ek}tQhFeotB<`fHd$fjlEn5(55j2L(x7HEc z#K9`DWq;4Hb_E(%V)_qa12Er{+(U@a5GXUgTL38sy2Eb5y<2-eTK~)g-{{_8d(<0? z(gjoTc%v|L5`)bRDca((4M%$?nQx6TKefRDsZU!xelwnI7SPbXFr42veDAl=t$kJf z7(j3W3(G%q4I`kiu`)t;JzYCW94ZhuN6Km}=7 ziRcNZPH6x@tuyAja`9lU%&3${G4$2J?kCiJPXHWK{tDBx^x&Mmw^Tl2s!V5!@`Kvy zpmU%IZsl&K;uE-M5i#Y*%uke@@Mey6Bg4m6QSGYEOxl`x^K^kx(qM$#NZ$~D6F2-$ zOgAF7f4tC=jor!~;78=xVkbMf%}aE8!R-=Q$IL*Bz+x#UhI4#xn8{}IK2#P<^@%BH zWwj~J*`Vq?xSs*c|CdkRDD$LX(+KDTI}@ic#JMYQvf}pA8`U!$0kh*=9^RLifor5X zO}oFr!aSr0N?LI$5pMz&tGd5KU00#?RX7K!$U(ddxq4%+yR%-fkLlP~;K69TkqBQQ z+<)c<{|ppK{S)jy1~PUvR`#sgby{6zRMZ>YXH3~X-R1XZt+S5!uRlC;B?x@O1@-BA zbGm!0%DdGh@#OOmQY6RRo8#B2DIQvkfIEJ)VR49$xoZSp_skBDXTaEqJr7uX_bKQwP(1O7 zCrQ8aP&p>_C)Kd&H=wLkkuMH@Ew&&)Z8?zwq*o79FKkFwu({+^E?oOuPIsTJ=j{~J z?PA+Ty*&vpa0ILlfJOAAeOZHO6K=c1XP4bBk-y6Bd(lC@NQFdn0`Ak`^?4m}3Np@* zAX&oRlcIbzeN1fwBoFZ(sLBByaIdB3fjA@yN_w$1$bW^5gJZ3Gd1vAIY9RJs-WL70A%n8 z`ME^JQ<$d^tLBEo+{c`I)S*4RzM<@{QoOfRdPtMis)2jLtj9&+c_Qd+PscmcNxPU_ zrSB-hHyNeX#`b|yB2y*;{*8X0;_9Qu>!Y@rW_#^YbH_>ZCU&@+ZAFxtG9p&tk9Egk zt~jRq4wThID4*y$ENDASUJ;Os5ai*`CJ~qAiR+jv=$|{lZ&invD{dt!wu#8YqILy} zLv$&U1U>i(zhXY!n=Nq7rVl zJgtPgm8vv^S@i`R%Noo#lMf*AFAd22fmTuQfx;R&Rb?P+41qUT4497_XySHMU>z0Z zP=v^OH)ay~gD?D=8OVfNG?EGps|~$z|1N=lIKvZve%`t6ZNZtRcv^jE}`TkM=B=%4ntq@NoYnkE9m0N*{hkz;XT zKFQk?P^zYL8UOPhv9_2At0MKD$TFKf`^z^bY*vQ9QZ7IKu2Vdv0BBYj_!&dkZ}<_{ z80xG~g#Kx3HV2v|9*sQX-YEU0Q2l$F|Il*qh(GneTAl61>fIkVl*pS()*!ZxOXYIC zT0b`&Fbmqhj*Y23SroVSlXW`~S2IQ)7!OE|X6%%h%;Q?mhtgQ9q4!L7wh?JUFj4p zc&K&b{-PH4?wRP(B!A4awr@T-X~1gx@7qBXQq$QTr)f3BVV7POa*gE|xBvKl!|qdd?HngkDMJ(HdaF#jmqq8;e|SOU4vFFKQ)4#;QCnTsx$umAI>>MI7PH&3bwjM4dMcv&oH1u z;ag@wM-tsOTuMd@4<5We2=SAvbCiNjk+T7^Q{93cjPDnJbZ!Ug6>AjHqYnU>JIboI zlXGJ-?dtNMPFFUNqlDH#N*4mVM_CafQ$>Wb55%7?>}Q~JqUTQ{xzxdE6lD)*tRbf$VN=&!lfH&v?O3qtsdCF?~D3{SihrP^~)bi8&K?UwxT%2iC7M#d;_iK`0Us@p|B) zuij1{&Qi|wcF4|^$_IK*w-J_6g~eyUJuwliF2w%yIB}<-B_-r4$*_fwGW;B0HdT(d zXJEc+@@xwAys+zAV;9@uU&k=24T_`UHW)_3Hl*=FqkuV!M>srHj_O=lQ|{eGo0k92 z>msHj=o32#Drf6G;^3HTf_H#8Xr)qGh!)!@p&|2dB(3C%V{qk7NOL?RAuQ!+nXK_0KipvZTdsv6MOr+F8W6I|KkMa(~g zPmSd7N~s`+lo6sAZo_W*Fzuk5UgKHg?sUHvG}b*&!3ID zVeCEx4^%iE_*MjUaU-3HN!+?v*3H!qL0pc(LR-ss3(5%?&-K);iXSenL{Q`$-i0)( zKXOeyXnStO1%?(h&tENA#=Q8FB{Cl2tP`LWmR^iwLA4wGT3KDXY?3m;GZj|s?*-w{ z>?V9S7t3+j6iR_FPG)x~X5N3I)P4TzJ&NF3yfv>v+ddJ0(cu<4&eY3p)!$o!R{pys zNE|SaG#(sew$969y}#c;em`CuKAoWUPu(5WSsdJ1(z?ea7cIM*7uou* z1BeCxu+|`Na4qZD6P=SQD05O^`qEB>7_kDQDx`;6{tIE2bio+F+Fs!52O@MMqY+xM zzD4_6CK5Rqo~i2lR8;5jCeYy;9nn=s#Nm1X8T1)bqn9B8!!{sDwX~t8e84LNY@cHt$Wf%G2;TE+((Rz z>7J`_9hL6u5pHGx!3u1hgX2Q{Uzt4}8?Ky5cJMBkvkDQUiXm7UdK;p`;5qAMTgM-h z4`ilGFl1hdH^qg>D0d8(Y}Yna6FY4E37BU(b#5WHi6Q-h&|}yJ=5dAs*{)CH@By6u zr&LuL!PYct7||CcI-9NSOmu$B#u8(?`$27kHmCDk$FfpE9jsW}(Nsf&vo)x3W%@$1 zaI8h`?#!bri}=*RGDXkw`IF~+el+5vS(&#J4#pk`rV2Z56s9ece%PE4})Ly!MSF3(-u9loyeGyC ziQcc2p`TDazoS)p2)GfQrD6Xi0G8?^nK%z|ZZfGWDM)CvgV*2G==}TswH?tk!#<&0lpx`Xmc?^{KjA=v$4*mh?6#mr{VpeV-eN)B*iUsR&Rnol(So#GUpd z)tB9u;tLNI1tL{vZTkK=*B86EkO|moqxTrX2ica9V#y@Fb+p;Qv-!|cAk)>t<~5bMdqo`gI4iEac&7EnIoOeAh9aU(3qMg?M}8^~`A z?f1z&cQ@nAM{UMId1p{QPThY)T^FybkH5=7@PL%GXz%DHHTd&+Mk@YFH^93^r`n%zTY7jOoDJ4j(+Kf(Nb`aS7F_wmh|tm zen%+?8f6ATF~m^THzH=edkVZG#p`SV`L6KGUj>gQ1RMRF(0il&CS!Q{r#B&>QYgD5 zH9&)~esnB*B(i19(Jak+Mb#JWXin#M&+~s-82n4Iis4u{*yv&grn=b5($*WalZ`9Q zHE@ddxheSdvLOtH_XNlb5gzFAHgL@CQds2g&P&vu%U3tHODa_+GL<^=BXPq;-B|PX;t<@;q9hI_f55HI_p5f z=^`b0CA0k_b@p^~?|A-KaF7^x5C$gpC5H@(yGDv-laNY*z2VAFe}m$b!eaL_>gR=u z?Mm5ZgB6NvlA@-;vwX~1njCsd-y_yZ()k4oZ6Iqe26P$bDUOMnE+Spq9g{yITvpDZ&2MaDo3)go^rL@TnOCE7c+M zSrHwoiItt*WZSbv9X>S@+A*vVwm#@gP>1c6c?mzRwXCbU+>|2k*{07$R zj4#-dO@GDUMTf)$W@sHr*sBHub6(XTgQDjTf-}>L;(ib;>4?C>&C;5!QI-smtw^d+ z_&NH`Drtw$vq0Tfu65_Dt3%IhNuvA~JTJC5m}+g^RH(CGkz7jD4u5lVP!6Zl7nu{m zE|IrU`dyE3D|VDk)@qo^!gm5DH5j_0!)gk{Rk7}Jb!Z;1qchL*_nFy}I8^{}@Lxrs z4|5K}LSOJULH?;&5_b5laiRALnv(`K%YCyOct`yNl?&Jdl_`~;Ji&9!02HI~R4`l{lP~1*YY)1~I zyW@@u^>gy6DwR@pBXPxQoa1kpzA(p^l{?DN3(nSw_`2Fg-QT)a65rT1mk0Ho(faJ# zbp6_{AtF%3gf5ESq0We()>=}VBn|Z=ScqX%-~hlDb9siyKcb%RqBw?RL3_4zmD@E8s_$A8R?rKnI&Km6_Asdy$p@QPa+LAMJBIHrBsL}Mb(L&@QMUpvZ(AW$Z9-r4$5wi3`>kshSqMDXY* zD*-te#c(NA{!c~c8kYoL;b|tsnk^-wTx307C#Edmz3c=NBjdg}uL+E_egwDHP=DBv z%nMiew;TQM$7EYSk{lArZy`0EXOZmkMEPX3%+dpCzo16c2!7>yf0L2QipdBZksMNo zE*Y#qog1R>&&Rd{na4FpG{&J(<@Q|B9u20^*cJ*L3-6`djy)>fCTL-l zD@zGiFX&(IkFsvV(d+RWd|2Tq#6A(ZyK@(tJJ*3s`JUk9wEC(mOKou0#K0$(jpgaG4D^|Bi68<44_zX8bCXyt>YjPc`%=h}>hP zyK?aedS?qaAzvJB%f!W|sb zou=|fP)&L>B;IubvsE*KPDgMmr{3e)jMGyXGxfb!dVGl1XwdT6 z3FOCo`Xs*FW{af5MgH>G&oIr`&KzyR=kTk!R7a|3Dr`vql8PlE zI(YexNOSSZhQd%)tg9>odjPH5a(}TU{NI8`n*0Qw>SSt+%G<&@*CXgW%6J!9zbvQx%19ZW}=>sHJaq&$5u-oKf7XbO{l-WdBqbage+#Ax-f4*CBfIb z#tJH|>qJK_UZKvinY@JW_g@!9m)YN0y&_Tjf>QoQdzbLX$DBEYH-~{-;>e7(E^bLZ zeibqi^4v_9Y5uF{SFc#VDQbDs4j^?ZT?>;bA>>M7#>khPkVg1^l^ACCMQZ5LgfmT5 zj}~|mC3lNk4h9Wv!xg;=lH2)&MNac=ko(8=6xxrT_?%zlmM?-#Vi~Jp%RZ= z97=t1fHN%ceG6`1S#LD`TLZwi?=BdkzS9JgPR8;%_Y{;`N2Ba{vGYp}ti=C0n0q0z zdc3}m&EE<+l;_2vZeJ(T4}(J(orE2HOQC-!_=i``>VK`cMMZdnvRV%Il-Re|*WSzV z=vhcwa=<|rsoQfz_JtCND<&9v!FPZBpa3h7T}XG&#LSj!3^5w3U^Qy3j5t~*v| zS>9j%(G!*EQGlpBo^Yq^HG&|<1tt=K%kSuzCbXnW(ev5(%gN+^zz`8aA~#jm8J}~X zn)LFleMzo4M4(V~2B@kYSyyzJg&<`Ax_u%oYr3{G-T6C?_jj@jiSpmvJ0E)=aXv0? z5r=MxT@yIu1Z{IXK7>P$;v`Sl%v>zfj<9{u@W|Z1!u7AUGKv4~YR`~?8}bTw3P5*b zlpQHR;=XH%BMN$-+z@YnTYodSSR0tQ$lQ4{Te!Exbc4B)_*UJ*2rm~;QVv4|8!hgvWjXSmZ|8wr`P0mq2bnxC-HqxTgoNCNpeM+5q-4lWTiC3(C`EQiqbBG zql`GZXQDN479!!kx8haFp(|@^!S$d39KPPM^+~%%cocLICh_Rl?NIs@JeraYzt?x7 zIqT$d{oV}x&Y#}6Co$>S=)v}sx7&{h?ML?3ZN)>S_`tS6wAzOLH<0uQ!WjxO4ck}5 zkxk=x-{REv=r8u%QCy5Xkf4G@lUhP&?sI*a`Vv1PJD`$GKQ_Dcn~f(tdns4P!!zyuq35s=}Rnkb18v zR)4F|X^O%6)ufgP{!s{(bai(w%g|CMst3g z95Uc2qyFC{Ks6QYRhGdRZMl{z>O|)ixJ4EfMOm-5#(E~;q#pUzwxjf^wOk%{$VVh= z!J|?gO^*W@I}7q&>`<+yy{T@U%+({0GW14ZTXi?)bgBGfu?jN+3Sw{?AyT!6G zG2MwAUoUlexIULjyiO?*ZHo+C#i?P6o~tSw%DF+h6h!4HDOjFCFJ|t8u{^@xMYN$> zLVvN!A0zDeNVw@vJx|#2aP1}M=1LL05|t_mM^wM|gYf8pYo;6HUYg09Ic}QtgK-}e zMg3vesbBuj3vXSsZr<_vO5t;xSMR_7pCWgDzS17M6$%@t?gBA43#y(V@jmc)Gr-ze zY^i^){~}lm-=4rs;tUqwCf*(IT(3WFf_J=`h zaE(HzvF?c^&M>uisxC<4#Hv7EdhU{9(+iTuM18cFJevs;M(){PMYXG#ID^ybiiVS? zVvKUOcbG}s1n>s1W~1XOlMYQ12Dh0U_ie*Wyn!xZe?Oq|)A!vF*&YYeDeAlPwVGtN zzcx8;!zEFlT01<>!TZV}X;y!9qQ$Bp+)<(?vA`1LZz~LrU@nI7T4v!zt0h<7TIy`) z(xX=Nx0uA!$GCQ5?wjZ$nVDF?*ir!0{XfPfybG5yRc8yS_Sn}>ZdJ;=xNXPNgj=Ej zF#OR8`(|t)e94_AN3xw-sqaq~&9(3e?;haE|9?K* zlY@JcEy;9SGE=ZM?%(E-%JVH}OR(D|o>-iYm37z4e~VzDNk@0znILU(3Jf}2*LUpV ze^wRJ+v*p8^^!TXGEMtJ6nCFEtRGprTUuWD{gTYtWe%C!9~(Ehw@4$k6c=VUJXKkn zWKGqBpWuepl$Wwf%b$r=@m${x_Z^H=d%kB>Q0zWb8b=|cI0HmRva*X;SJo!d?)WvI z$afUt?b9X7LjUUXD})BT^H(>UniGi-uHoL_J^xNndt;us5=8JQYoaof@#scf?L8FN zl_Rw8k{+_wq1Wz2D-JuJd`@`lM#_gDr!)GpW1$ww!CdVuj&rK&Y({V+PX9p>eoIiZ zE;Y`z)`-uJUbhR^Ig+JtZnNxwVibeKtNwaV0hae&zN}Uy8;!{goHc9LY>5(h+)Fg_ z;ZcQYq$M89gO)F$tx60g#oMti6aPPE@+0I89K}mmhJllD>3HtyR%^cY1+n{w%+4uk z!u`Qe=sMnp^xfTmw~ai8&4X)%*OCeOFl@cg@pJulRO8TTJ(%m9lg4&1ewOR7C83Xx zzAi{qas(AUzIO{E(GfmIy}CMmH4gtK6Eg7O&yImkDo$P#zXnbr@jg`*iqNHGkLVy{ z3Ph7jNQ3@-ZZdE51_BSIz^N<96WRkQwi;>#cYSlC3nlPac;K?Cvm$IQ_5UL!utZ4K z86nh$yN~nCH2x(`APzNh+u^oDs)q~J4Pan{PS0CP!NGW+xT}$93zEjsS0mKEr_}X~ z;^wBvdGlVR^C%c-)}f}(v4n3dnTv-aq52WumzdDM;O-EEarpZXbI*VVe$*IeJatmCBU zhy@ODZj)W_Sp_kIc}pfnXWFU-;c8O3e&%ftD0r&0#w7U&=4#^a0c3%{SIA>99J%tQ z-lN7hp+WRs@~?0h-H9@bi>3(Z zZ#W%ks=gc`@U#B0K-!G5tZvA%F2=TFN^2dLN9K|zOF|=%bW&jy6K7I+9ZS(|<|u>V zKj#7tsl&SrZ4P@dBRt+gPgPaksRSTEbq~TaY^+9!e-^)Yj^v1zVnQLd-BUXR9W{ATFIgZ2gb>l>#SXC7) z!A)pNxFJ>bG7Dn;lu$#7=cl~najhuXHr+ve$a%a{Yz|~F!ZegdJ9o!~hK3PD7W8k> zo%6Y|$$IdEGuNW-6spYR+dsu?@fSlmCBcY7$?a_4wYw>$(F zhCve%LQ9bd8P_vuf2IeKaqmV7mLu(F23|7!H7!Z|+9zeJtNNy0yFQEyY4eLIuCYXL zEK{0~0PggTpWn(joL2y5_Xm$vXP(P2RE>dx!fUrOB@5n}wZ`5Zv+b#pIQ85wu&q;U zO8c^==}@Uql2<@~oZYju|7z4MzMyt7UOm|J9}BO-hsQi!SXOqr@@DPyS5MCqj(`VS z-lc<^&FgT^+Wc0mtmEp0+C;A1`(;7gwe!E~eeJ$UV9& zN`cCznW`t8oh>l?=9&xdKA}vc)Y6&lIz)gb{0YP#jhR#?lfT$<%9;=s~yFG|WzNW`8_0)9c z)+FzhX-soVap0R0xS{as4)E6PycgL$RD27i7QWzCPGAtjEf>)zL?u15?%gURUvPww@>phJEU*qjw`*G;}$kx)?tAR>>&y#q+uoL<@ z2;W0V8>}K#l{Hb+w@*})t!T@3?~THq#=}YY`9j6{LU+F-(9ft4ce0RafDwQ5$qd21 z6xo%O&SUZRj%vbIP5P5*iO_pexH}lS&IyfT{x+j5Xk@IkYMN4$KPk!A^<5zIDq2+z zw^3IY^Q<~P(1v~7tx-F-kzVZGd(c>NDlutCOAylr6ByfiL)9kr14BAT+^Ww zNBaazMGaNG)BB2w-mNm+=}+SZ{>kk~2|H7|(-K&WODbL;jCaF*-71r#6nXZL7C|4% z#SdafO9bAVGJ{6OR)vnJnjbL=1jNIci01dxwYQducIO`*qVOs)=nZB8Rw4{OLa5fy zl=w2+klJP@EF4~u%S(Q4pdL5dmzoP06N|Q?R5yqbC!}lb=}ryi)qqD&A0_Z-2+l4O zQd3NTh4E^bE)xZ*RP38(JkTxz5jE8_E0dVT4<-qU9xeQ_cC~W9$_+{wl1l7cuymhArrL0ZCAJV@8_6)xiyQ z94NOw&B`RCakNuISE5979)3CRQJodGhY!BkMUA3Y!zfB*XVMgpIoxK)87^(&dQi?% z7RtmD)cx

mrzYoVcvKxQ;x%HP8P&mwH9-Xu*|eNwsx;x69-hL^~fhdncK9E6K~s z_5?GX@Z8l1Ro~D%UW7Hs$IK{aL4320F_6I$7vZ#Fl#Qx#Py+xa(I)0K4-9@21lvas8|Q09rAl_;1zv;JV+`0yYLrY zlDG;LU@aVsZ$qnr|28+Y;#bSt-IbU3&>9+6zZWx!X;)ca(CZu{2#f)U9Aod{22ET` z5l$CLkECRekLey6Lq5eUL`0vCs7aHV(wu_@-oey)5&aDGnwlI9$&#s#zHc2hL76k? z9IEmUrEVE$D8_(i5hd@9a@C1lbu29ruj4>ll>d8i=#`jmlz@;n6w2RXhA`B1kI95u z5!Z5F|M-|j4OOZ6;iPV0Wrgla#TwtiOwR;WaDuj~NI!rj@GKKl+Z1k_I~{YUAK=OF z?uNhm+JgFr=V?0Mu~48C1X=|hs3&oj-1<6;2(Ba=nPT>4f^KF;BfHPd#-u{m72yss zRV)TCm3yIow~8LAu&J?Ja0u9a_brCNhL!ujNcJ<;W6*S~7|d}FD`W0kqMFV}Mz?>o%vxTtKq^Fkk-@&SYEp;evUDI&{7?M&BQog?&zW4J5J4xNR4 z!)3_5j3Sq_rCMw0YP_klE2e*~_IitU6timDup2hdEMxGo6M#o`>Y<+XK zdyOi*M%xn9N3$%iL4^<1Ly;VM*7&EGsn}~MJ4l3zh^~$1elM}7P#7xpu8zk@ic0@6}a;RXF_3TX821+uMvsaBc)Awf=9LAKkBVcWafz) zoJ6*Xc-O>PYut~6$;`VrhWa7WlaBk-WgWTlmzbWi9-XUa4Ia|Tg`OkAT|R1quX7pS zyUc%sOTMoUrs|7SbgRn7lq6&BNbF3mwm#RnS>)NQb){;j7$kvd{%_dyIMf|xQ;@c` z?!LE}nWqZuP<5at*nfS=poQkrTMDzT7e3a8B2asyZn7#oP<6gV*VD2FSAvzR-5&3- zBKuD{!6*hlaD+Z!dcKtMt@V%E!fg+mfv~EDs=mMIZ{_K?al<#b%hR(5Z&>pjX-X!S z@EsHRN?lU6wrZk&00)@vO+ne&;A$KxV1U}FPY{Jxikj1P;dB7xxT9pq3PbpH#My~> zU}6TYq8n7U69U%>ax)PecuW_5pHJxo2XL|xZ%+w#r(C;cVzni~4EAqU1qLd7Vnrd- zkB+4FlSy3BuDz6PI4Xnk+0k+oL!2xY5a1+ghO?;|rn&Zgl)q z_@fkigQ0c@^&+R=%&7xzAlrGZQ1a>nam>9ARjlBldv&^{6>bA$XR~EbGM%l!5@KC? zoe8KQQOR!`%V%v!4u6rnynG2`iv&g_PA8Ik)a0{@E~R4g`x9B>P?oqk8n2CJn&nN7 z4F1iGvf%j&(h9n0maw;0lplqC7iCLTw56626}Y#;v%MrChd}Dy3_({0*~AUJ!(txP zjYaxEi++F5UasW$&vV+)aAP5Fg>aUL!sPK=n9D40oL{5z+I6=-M8q#F#X->2wQ@54$t4dklSn{9A)T`#62qF)K^Qdu_1*C$|Y*lEh3W^az zh0Y{_CrR6wjo*P2a6)#DD@hPcqNwCBPbq~8-*3gt(U`Wy*`%N&BYZHL7sC%AF;qvb3w2VzN_egb6cU=T z)YNEoQ+^zAc8sRAsyuj}xr~HNEJ(D%l#)IGnWn<_c4gK~7D7tQg15hvxcKcKmNTI? zS~=$sz89X$-GL-!CTYvTr>p1mzH({i@sm16c=xM)?e2Cid1yO-#Gm?KZE}3@#J0%r zKi5v*6CW`fSlXk`uaB7LCmuQt-0wB{)%SkjEO!rS``|_izngxaF(*8{iT?lh-vn1) zdzz|ArXEM!{_AG5^@u2NM4A&PiTKrbyZe#DoQOH6nitWZgu@QH*q5dDbuiTOBPm(q z!$3Fr98swldaA%PO;9}~&h&_BvF-{zmw67N-njs@lLE~y$?VT3_C|ekvJNq$-Af7Y zQfywZCWU2woC;L?FnIW!bh~9R%^4GqA0gtJT%h9d+T{yV)-uU4;RtXx#Ui6!^5)|KIXK$9}^MXp?^6$-W+Nr07!T~-qATl zDSaBzplg(&Yf9G~0C)zx4_4mT^OA>lNVV*m}(={P7`Nef(9{DxcjMpKm+XBW7 z5~0b8Q`7Umm@|z1m-H@WxLYY{D3rZb+|sKdz?A`5V8G#Tk+mA-V}N7zt4Vn0n}t=3 zT7!Gx^?8VvXEIw7%sFwGC(edAV3HT!Ixut1CfoX!l^Rz7sAJ66<9HWv238)-G&ii6 zVd=Yh=@ENWN=Oi~VMS4C_zJv0=t0j0!9pt$Y9$MS#%gGrM7SrBNOfZFn@4;VlwVGi zk6?xmf^wXOuPCy`_)RH|!k z(GC&&){E;AU(BvP5uI5e~>Cg=d!+BnQ;Zbk*%%Ib^@pVlotI> zzy6$e$5E)1dGXlErg5M+d(=WkJpQ2a=U2neD;msvQq?*`g3k$4&&wD?Jmdj{N3`Vt z$+z6PKmFd{OV9VUW^xd3u8c^+`;tzaI8nWbE=Rn{Vt-BSTD(xYukeHH><2Os^b^dC zrN4n;DX@ls;zuI1tk5!Dn1dtQY=*#>fnFB5RplRKb`BxFygRi54tIpW839i~3y|Q0 z1UZKJp{P!Z^!)ARwDSkr@k^TrM|Gbr=vd|3l%ivm^ zp{O(O?f%mVh*``aOwQ__%Q?rUD%s9g)b3YI4)luAGIC1l-lb~VH!bb%H>Z_prgb%G zm}R*CpKR&tZ2g*u4MZ3yvb3SKPNV2=99D(xYwIx4pY7rE$ z#@owsxPiZOJ0dY>|&sSXl;^S$cd@JHtTc=h>EOaK)9yz43^DC`n6 zN$;#S0otMo_dPH2CL~)E_3qv8Ha)gM9CW3UjrG0mc`6dBw*nhE{sGLo!#D%nJCvQ} z3HCM6VoW%Pln3n<(xzoG)uq~Vr9YfJS<9ha91ogX2O*Q$>wk`QOpTl?~uApeXp6ClJwa+c@*Bqvmaq0)G{cY)H1IVf|?X{IGqG zXBg48el%ya*Nb$c?))_Fp7_g}<|SBYD(e1(1K7D5y-hdbegk>x?A(|$pPuYMUKIpO zB+qcE-^YZy6Fpy*@HV3v*ABW9PFdHURKMf(dBYI{(vYXSmghpxiFYAeZ_8$8k_^E2 zu@0}$QF~_%pfQ=jV5)N$L&ZmMO$Qxz zct8Yki*eleFxn?w1^OiP(7~Prgr{JI7C`N<`+}fxd5d|q_qpQbfP@SUV*&&8$lM6} z`pYI2iesUG#bAVo9WpyY{;xTK7guRrf8aypv?~)Jl^>+158i;@U0XJgw@Irwyj&k@ z!4Im*{pxn)8Df@=>X`o5!m&R#ACB?ROq6aCyi!-#}pI1R?i4)&{7z^s%&FqfSDM#o6Xz* z=M}oRp7hwH$iNc@Fz{d&`e!q;Yg1%zo;r2Pk7%gjlT&wn0X>Zn(vL$Sy#e6NLAdA* z9OYe@R_uD3;r+rz`peAGfu$i7@k&MCVdmVRfN*EOZN}3PN?0+0#)?KNI0t4HXs_kz zp4-=eEFnN*E&m89pR+A-ie(DW$L)rlNdUa!aF_HDoIpe!&F0(9e#TWn?iTI)B>=v= z#Y?rYozH@AhMz@ZGxV1ha0KTEdypPNTOF^BU@-!RYt=DzxN&B|0RaZ&+;p|>*|&HC z*nuz*(*{ZzC5jAmOB3%YS!um5)Q5MreSBs{o@4)H9l!xj)zZQ-itcte9$e@0n*}`Ia0G95}A6VVxI^_SbOf?8!?; zODrk)p4?V#ZF%c-0UhuA;r%$itvUuQKC;|ksFF>wV8%JBh8H^@k0H+{y642l$8Wcs zy?AlxPRWA5y?k=9(6&yPOTa0qt-N)0^$;WrLa&+rt;jV}R23^b6YE6MF4@nqa_H3u z>CQ0O7=8)%un6Xa@;tMgqVvELZHy=Fzj&K5ZKWCpx^kI0q@^E zG0;+E{DE8HY&}%uZ;NdGVL+uJ)Sn>pB`4%?=vyhj?~uwuY<|9o%*iHiv*k~7eIM4B z6TQAy$-P`MRoK&&>>29le~qQL28+*$!wv5t4ElDMkVV0zWv{!xR{4wvW?l50R0aP0 z$0hv_?@ytApKq3en#r)=JBF*y!e#k)C!zYqGn?6@4ZT~USN%0DEf)n8RN~opgjB8Q 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%0D7|t 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 Xb#}b->7J{-?2Xc7`zQg literal 0 HcmV?d00001 diff --git a/tests/0. Example book/main.typ b/tests/0. Example book/main.typ new file mode 100644 index 0000000..1191b9e --- /dev/null +++ b/tests/0. Example book/main.typ @@ -0,0 +1,70 @@ +#import "@preview/cram-snap:0.2.2": * +#import "../../src/lib.typ": * +#import "@preview/zebraw:0.5.5": * + +#set page(flipped: false, margin: 0.8cm) +#set text(size: 14pt) +#show raw: set text(font: "IBM Plex Sans", size: 11pt) +#show: set-group(grow-brackets:false, affect-layout:false) +#show: cram-snap.with( + title: [Example sheet], + subtitle: [ + #v(-1em) + For: 0.3.0\ + Written on: #datetime.today().display() + ], + column-number: 1, + fill-color: "d0e0d0", + stroke-color: "343434", +) + +#table(inset: 0.7em)[ + Effect +][ + Grammar +][ + #ce[H2O] +][ + ```typ #ce[H2O]``` or ```typ #ce("H2O")``` +][ + #ce[H+] +][ + ```typ #ce[H^+]``` or ```typ #ce[H+]``` +][ + #ce[H-] +][ + ```typ #ce[H^-]``` or ```typ #ce[H-]``` +][ + #ce[O^2-] +][ + ```typ #ce[O^2-]``` +][ + #ce("[Fe(CN)6]^4+") +][ + ```typ #ce("[Fe(CN)6]^4+")``` +][ + #ce[->] +][ + ```typ #ce("->")``` or ```typ #ce[->]``` +][ + #ce("->[600°C][200atm]") +][ + ```typ #ce("->[600°C][200atm]")``` +][ + #ce[Cu-2^^2] +][ + ```typ #ce[Cu-2^^2]``` +][ + roman-charge\ + roman-oxidation +][ + ```typ #show: set-element(roman-charge: true)``` \ ```typ #show: set-element(roman-oxidation: true)``` +][ + #ce[#text(red)[H2]] +][ + ```typ #ce[2#text(red)[H2]]``` +][ + #ce[$overbrace("H2O","water")$] +][ + ```typ #ce[$overbrace("H2O","water")$]``` +] diff --git a/tests/elembic/test.typ b/tests/elembic/test.typ index 4fc9a8f..178867f 100644 --- a/tests/elembic/test.typ +++ b/tests/elembic/test.typ @@ -3,12 +3,6 @@ #set page(width: auto, height: auto, margin: 0.5em) #[ - #lorem(5)\ - #lorem(5)\ - #lorem(10)\ - Hello World - #sym.bullet - #math.dot $ #string-to-element("H5+3").at(1)\ #reaction(string-to-reaction("(H5+3)5+3"))\ @@ -42,8 +36,5 @@ ),count: 2,), )) $ - - Hello World\ - #lorem(10)\ -R' ] + From 51a7707cf66c6fc98e949e54f1dea7f01e559e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B2-=E5=90=B2=E5=93=9A=E5=9F=BA=E4=B8=99=E6=B0=A8?= =?UTF-8?q?=E9=85=B8?= Date: Tue, 24 Jun 2025 21:52:07 +0800 Subject: [PATCH 17/20] remove local elembic change lib to "@preview/elembic:1.1.0" --- src/libs/elembic/.DS_Store | Bin 6148 -> 0 bytes src/libs/elembic/data.typ | 545 ---- src/libs/elembic/element.typ | 3875 ---------------------------- src/libs/elembic/fields.typ | 364 --- src/libs/elembic/lib.typ | 10 - src/libs/elembic/pub/constants.typ | 1 - src/libs/elembic/pub/data.typ | 1 - src/libs/elembic/pub/element.typ | 2 - src/libs/elembic/pub/filters.typ | 1 - src/libs/elembic/pub/leaky.typ | 8 - src/libs/elembic/pub/native.typ | 2 - src/libs/elembic/pub/parsing.typ | 1 - src/libs/elembic/pub/stateful.typ | 8 - src/libs/elembic/pub/types.typ | 5 - src/libs/elembic/types/base.typ | 582 ----- src/libs/elembic/types/custom.typ | 409 --- src/libs/elembic/types/native.typ | 341 --- src/libs/elembic/types/types.typ | 405 --- src/model/arrow-element.typ | 3 +- src/model/element-element.typ | 2 +- src/model/element-variable.typ | 2 +- src/model/group-element.typ | 2 +- src/model/molecule-element.typ | 2 +- src/model/molecule-variable.typ | 2 +- src/model/reaction-element.typ | 2 +- src/typing.typ | 2 +- tests/brackets/test.typ | 2 +- tests/content-to-reaction/test.typ | 2 +- 28 files changed, 11 insertions(+), 6570 deletions(-) delete mode 100644 src/libs/elembic/.DS_Store delete mode 100644 src/libs/elembic/data.typ delete mode 100644 src/libs/elembic/element.typ delete mode 100644 src/libs/elembic/fields.typ delete mode 100644 src/libs/elembic/lib.typ delete mode 100644 src/libs/elembic/pub/constants.typ delete mode 100644 src/libs/elembic/pub/data.typ delete mode 100644 src/libs/elembic/pub/element.typ delete mode 100644 src/libs/elembic/pub/filters.typ delete mode 100644 src/libs/elembic/pub/leaky.typ delete mode 100644 src/libs/elembic/pub/native.typ delete mode 100644 src/libs/elembic/pub/parsing.typ delete mode 100644 src/libs/elembic/pub/stateful.typ delete mode 100644 src/libs/elembic/pub/types.typ delete mode 100644 src/libs/elembic/types/base.typ delete mode 100644 src/libs/elembic/types/custom.typ delete mode 100644 src/libs/elembic/types/native.typ delete mode 100644 src/libs/elembic/types/types.typ diff --git a/src/libs/elembic/.DS_Store b/src/libs/elembic/.DS_Store deleted file mode 100644 index 4462432e9cf5017b0e924c09346d8594e47b64b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!A`?447J&iDskD7V~$++2Vp8-updBW2o9@SY1@HYK7p^|a}W|gz;kRw0)-<& z%$Abp#CF`is!2>lygF=_L<=Hn&;(hO36beZ(}_8Yk!6kL`ex|Ii6#E-lsx;4MmkWp zy(^!;nSWUNp>4bE&|wDt>Gk5}>9VwLez1=B<*JVeqf^?`19INduFmf8{atQ#G%W@Xz zQcFloFpP#t5f%t*C{RP$N(|O;%m?#}hDlMwiLLlx%luxvaL$hPLv|;QirzZ|&Opt; zOot0O|F7`NOcwcdh>x5BXW*YP!1H$9uJBTJwtjg%Ico#jHJXU{B~c*It4ja|vX7i+ dquPV$@Qa2?QC5-p91ir0KqkaHXW$nYcmtAaLw5iG diff --git a/src/libs/elembic/data.typ b/src/libs/elembic/data.typ deleted file mode 100644 index c48ae5c..0000000 --- a/src/libs/elembic/data.typ +++ /dev/null @@ -1,545 +0,0 @@ -// Functions to extract data from custom elements and types, as well as associated constants. - -// Type constants: - -// Used by typeinfos -#let type-key = "__elembic_type" -// To be used by any custom type instances -#let custom-type-key = "__elembic_custom_type" -// Used by custom types themselves -#let custom-type-data-key = "__elembic_custom_type_data" - -// Versions: -#let element-version = 2 // v1 = alphas 1 and 2, v2 = alpha 3+ -#let type-version = 1 -#let custom-type-version = 2 // v1 = alphas 1 and 2, v2 = alpha 3+ -#let current-field-version = 2 // v1 = alphas 1 and 2, v2 = alpha 3+ - -// Potential modes for configuration of styles. -// This defines how we declare a set rule (or similar) -// within a certain scope. -#let style-modes = ( - // Normal mode: we store metadata in a bibliography.title set rule. - // - // Before doing so, we retrieve the original value for bibliography.title, - // allowing us to restore it later. The effect is that the library is - // fully hygienic, that is, the change to bibliography.title is not perceptible. - // - // The downside is that retrieving the original value for bibliography.title costs - // an additional nested context { } call, of which there is a limit of 64. This means - // that, in this mode, you can have up to 32 non-consecutive set rules. - normal: 0, - - // leaky mode: similar to normal mode, but we don't try to preserve the value of bibliography.title - // after applying our changes to the document. This doubles the limit to up to 64 non-consecutive - // set rules since we no longer have an extra step to retrieve the old value, but, as a downside, - // we lose the original value of bibliography.title. While, in a future change, we might be able to - // preserve the FIRST known value, we can't generally preserve its value at later points, so the - // value of bibliography.title is effectively frozen before the first custom set rule. - // - // This mode should be used by package authors which know there won't be a bibliography (or, really, - // any custom user input) at some point to avoid consuming the set rule cost. End users can also use - // this mode if they hit a "max show rule depth exceeded" error. - // - // Note that this mode can only be enabled on individual set rules. - leaky: 1, - - // Stateful mode: this is entirely different from the other modes and should only be set by the end - // user (not by packages). This stores the style chain - and, thus, set rules' updated fields - in - // a 'state()'. This is more likely to be slower and lead to trouble as it triggers at least one - // document relayout. However, **this mode does not have a set rule limit.** Therefore, it can be - // used as a last resort by the end user if they can't fix the "max show rule depth exceeded error". - // - // Enabling this mode is as simple as using `#show: e.stateful.toggle(true)` at the beginning of the - // document. This will trigger a compatibility behavior where existing set rules will push to the - // state, even if they're not in the stateful mode. It will also push existing set rule data into - // the style 'state()'. Therefore, existing set rules are compatible with stateful mode, but this - // only effectively fixes the error if the set rules are individually switched to stateful mode - // with `e.stateful.set_` instead of `e.set_`. - stateful: 2 -) - - -// When on stateful mode, this state holds the sequence of 'data' for each scope. -// The last element on the list is the "current" data. -#let style-state-key = "__elembic_element_state" -#let style-state = state(style-state-key, ()) - -// Element constants: - -// Prefix for the labels added to shown elements. -#let lbl-show-head = "__elembic_element_shown_" - -// Prefix for the labels added to the metadata of each element. -// Used for querying. -#let lbl-meta-head = "__elembic_element_meta_" - -// Prefix for the labels added outside shown elements. -// This is used to be able to effectively apply show-set rules to them. -#let lbl-outer-head = "__elembic_element_outer_" - -// Prefix for counters of elements. -// This is only used if the element isn't 'refable'. -#let lbl-counter-head = "__elembic_element_counter_" - -// Prefix for the figure kind used by 'refable' elements. -// This is not to be confused with figures containing the elements. -// This is the kind for a hidden figure used for ref purposes. -#let lbl-ref-figure-kind-head = "__elembic_element_refable_" - -// Custom label applied to the hidden reference figure when the user specifies their own label. -#let lbl-ref-figure-label-head = "__elembic_element_ref_figure_label_" - -// Label for the hidden figure used for references. -#let lbl-ref-figure = <__elembic_element_ref_figure> - -// Label for context blocks which have access to the virtual stylechain. -#let lbl-get = <__elembic_element_get> - -// Label for metadata indicating an element's initial properties post-construction. -#let lbl-tag = <__elembic_element_tag> - -// Label for metadata indicating a rule's parameters. -#let lbl-rule-tag = <__elembic_element_rule_v2> - -// 'lbl-rule-tag' from older Elembic versions. -#let lbl-old-rule-tag = <__elembic_element_rule> - -// Label for other functions which access or modify the style chain, namely -// 'get', 'select', 'debug-get', 'stateful.toggle'. -// -// This is attached to metadata and the 'special-rule-key' property -// indicates which kind of rule this is. -#let lbl-special-rule-tag = <__elembic_element_special_rule> - -// Label for metadata which stores the global data. -// In practice, this label is never present in the document -// unless one accidentally leaks the 'bibliography.title' -// override from our workaround. -#let lbl-data-metadata = <__elembic_element_global_data_metadata> - -#let lbl-stateful-mode = <__elembic_element_stateful_mode> -#let lbl-normal-mode = <__elembic_element_normal_mode> -#let lbl-leaky-mode = <__elembic_element_leaky_mode> -#let lbl-auto-mode = <__elembic_element_auto_mode> - -// Prefix for labels added by 'select' to matched elements. -// These labels are not specific to eids. -#let lbl-global-select-head = "__elembic_element_global_where_" - -// Special dictionary key to indicate this is a prepared rule. -#let prepared-rule-key = "__elembic-prepared-rule" - -// Special dictionary key to indicate this is a "special rule" -// ('get', 'select', 'debug-get', 'stateful.toggle'). -#let special-rule-key = "__elembic-special-rule" - -// Special dictionary key which stores element context and other data. -#let stored-data-key = "__elembic_stored_element_data" - -// Special dictionary key to indicate this is query metadata for an element. -#let element-meta-key = "__elembic_element_meta" - -#let element-key = "__elembic_element" -#let element-data-key = "__elembic_element_data" -#let global-data-key = "__elembic_element_global_data" -#let filter-key = "__elembic_element_filter" - -#let sequence = [].func() - -// Special values that can be passed to a type or element's constructor to retrieve some data or show -// some behavior. -#let special-data-values = ( - // Indicate that the constructor should return the type or element's data. - get-data: 0, - // Indicate that the constructor should return an element filter. - get-where: 1, -) - -// Extract data from a type's or element's constructor, as well as convert -// a custom type or element instance into a dictionary with keys such as body (for elements only), -// fields and func, allowing you to access its fields and information when given content (for elements) -// or the type instance (for types). -// -// When given content that is not a custom element, 'body' will be the given value, -// 'fields' will be 'body.fields()' and 'func' will be 'body.func()'. -// -// The returned 'data-kind' indicates which kind of data was retrieved. -#let data(it) = { - if type(it) == function { - it(__elembic_data: special-data-values.get-data) - } else if type(it) == dictionary and element-key in it { - (data-kind: "element", ..it) - } else if type(it) == dictionary and custom-type-data-key in it { - (data-kind: "custom-type-data", ..it) - } else if type(it) == dictionary and custom-type-key in it { - it.at(custom-type-key) - } else if type(it) == dictionary and stored-data-key in it { - it.at(stored-data-key) - } else if type(it) != content { - (data-kind: "unknown", body: it, fields: (:), func: none, eid: none, fields-known: false, valid: false) - } else if ( - it.has("label") - and str(it.label).starts-with(lbl-show-head) - ) { - // Decomposing an element inside a show rule - it.children.at(1).value - } else if it.func() == sequence and it.children.len() >= 2 { - let last = it.children.last() - if ( - last.at("label", default: none) == lbl-tag - // Workaround for 0.11.0 weirdly placing some 'meta' element sometimes - or sys.version < version(0, 12, 0) and { - last = it.children.at(it.children.len() - 2) - last.at("label", default: none) == lbl-tag - } - ) { - // Decomposing a recently-constructed (but not placed) element - last.value - } else { - (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) - } - } else if ( - it.has("label") - and str(it.label).starts-with(lbl-outer-head) - ) { - (data-kind: "incomplete-element-instance", body: it, fields: (:), func: (:), eid: str(it.label).slice(lbl-outer-head.len()), fields-known: false, valid: false) - } else { - (data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false) - } -} - -// Obtain the fields of a type instance or element instance (custom or not). -// -// SAMPLE USAGE: -// -// #show e.selector(elem): it => { -// let fields = e.fields(it) -// [Hello #fields.name!] -// } -#let fields(it) = { - let info = data(it) - - if type(info) == dictionary and "data-kind" in info { - if info.data-kind in ("content", "element-instance", "type-instance") { - return info.fields - } - } - - (:) -} - -// Obtain context at an element's site. -// -// SAMPLE USAGE: -// -// 1. In show rules: -// -// #show e.selector(elem): it => { -// let (get, ..) = e.ctx(it) -// let other-elem-ctx = get(other-elem) -// [The other element field was set to #other-elem-ctx.field at that point!] -// } -// -// 2. In element declarations: -// -// #e.element.declare( -// ... -// synthesize: it => { -// // Get context for other element -// it.some-field = (e.ctx(it).get)(other-elem).field -// }, -// ... -// ) -#let ctx(it) = { - let info = data(it) - if type(info) == dictionary and "ctx" in info { - info.ctx - } else { - none - } -} - -// Obtain an element's or type's scope (usually a module with important definitions). -// -// SAMPLE USAGE: -// -// #let subelem = e.scope(elem).subelem -#let scope(it) = { - let info = data(it) - if type(info) == dictionary and "scope" in info { - info.scope - } else { - none - } -} - -/// Obtain an element's or custom type's constructor function. -/// For native elements, this will be equivalent to `it.func()`. -/// -/// Useful in custom element show rules, for example. -/// -/// This is equivalent to `e.data(it).func`. -/// -/// SAMPLE USAGE: -/// -/// ```typ -/// #show selector.or(e.selector(elem1), e.selector(elem2)): it => { -/// // Will be either elem1 or elem2 -/// let elem = e.func(it) -/// // 'elem == elem1' works, but comparing 'eid's is recommended -/// if e.eid(elem) == e.eid(elem1) { -/// [This is elem1] -/// } else { -/// [This is elem2] -/// } -/// } -/// ``` -/// -/// - it (any): element/custom type instance (or element/custom type itself) to get the constructor from -/// -> function | none -#let func(it) = { - let info = data(it) - if type(info) == dictionary and "func" in info { - info.func - } else { - none - } -} - -/// Obtain an element's eid. It uniquely distinguishes this element from others, -/// even if they have the same name, by including both its prefix and name. -/// -/// This is equivalent to `e.data(elem).eid`. -/// -/// - elem (any): custom element (or an instance of it) to get 'eid' from -/// -> function | none -#let eid(it) = { - let info = data(it) - if type(info) == dictionary and "eid" in info { - info.eid - } else { - none - } -} - -/// Obtain a custom type's tid. It uniquely distinguishes a custom type from -/// others, even if they have the same name, by including both its prefix and -/// name. Returns `none` on invalid input. -/// -/// This is equivalent to `e.data(typ).tid`. -/// -/// - typ (any): custom type (or an instance of it) to get 'tid' from -/// -> function | none -#let tid(it) = { - let info = data(it) - if type(info) == dictionary and "tid" in info { - info.tid - } else { - none - } -} - -// Obtain an element's counter. -// -// USAGE: -// -// #context { -// [The element counter value is #e.counter(elem).get().first()] -// } -#let counter_(elem) = { - let info = data(elem) - - if type(info) == dictionary and "data-kind" in info and (info.data-kind == "element" or info.data-kind == "element-instance") { - info.counter - } else { - assert(false, message: "elembic: e.counter: this is not an element") - } -} - -/// Get the name of a content's constructor function as a string. -/// -/// Returns 'none' on invalid input. -/// -/// USAGE: -/// -/// ```typ -/// assert.eq(func-name(my-elem()), "my-elem") -/// assert.eq(func-name([= abc]), "heading") -/// ``` -/// -/// - c (content): content to get the constructor function of -/// -> function | none -#let func-name(c) = { - if type(c) == function { - let func-data = data(c) - return if "name" in func-data { - func-data.name - } else { - none - } - } else if type(c) != content { - return none - } - let name = repr(c.func()) - if c.func() == sequence { - let element-data = data(c) - if "eid" in element-data and element-data.eid != none { - name = if "name" in element-data and type(element-data.name) == str { element-data.name } else { "unknown custom element" } - } - } - name -} - -#let _letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-" - -/// This is used to obtain a debug representation of custom elements and types. -/// -/// Also supports native types (just calls `repr()` for them). -/// -/// - value (any): value to represent -/// - depth (int): current depth (must start at 0, conservative limit of 10 for now) -/// -> str -#let repr_(value, depth: 0) = { - if depth >= 10 { - return repr(value) - } - let typename = "" - let value-type = type(value) - if value-type == content and value.func() == sequence { - let value-data = data(value) - if "eid" in value-data and value-data.eid != none { - value = value-data.fields - value-type = dictionary - typename = if "name" in value-data and type(value-data.name) == str { - value-data.name - } else { - "unknown-element" - } - } - } - - if value-type == dictionary { - let pairs = if typename != "" { - // Element fields => sort - value.pairs().sorted(key: ((k, _)) => k) - } else if custom-type-key in value { - let type-data = value.at(custom-type-key) - - let id = type-data.id - - typename = if "name" in id { - id.name - } else if id == "custom type" { - return if custom-type-data-key in value { - "custom-type(name: " + repr(value.name) + ", tid: " + repr(value.tid) + ")" - } else { - "custom-type()" - } - } else { - str(id) - } - - type-data.fields.pairs().sorted(key: ((k, _)) => k) - } else { - value.pairs() - } - - typename - "(" - pairs.map(((k, v)) => { - if k.codepoints().all(c => c in _letters) { - k - } else { - repr(k) - } - - ": " - - repr_(v, depth: depth + 1) - }).join(", ") - ")" - } else if value-type == array { - "(" - value.map(repr_.with(depth: depth + 1)).join(", ") - ")" - } else { - repr(value) - } -} - -/// Performs deep equality of values. -/// -/// This is necessary to reliably compare instances of the same element or -/// custom type, as well as data structures containing them such as arrays -/// or dictionary, between different versions of the same element or type, -/// by recursively comparing `eid(a) == eid(b) and fields(a) == fields(b)`. -/// However, this is notably slower than Typst's built-in equality check. -/// -/// - a (any): First value to compare. -/// - b (any): Second value to compare. -/// -> bool -#let eq(a, b) = { - if a == b { - return true - } - if type(a) != type(b) or type(a) not in (content, dictionary, array) { - return false - } - - // Recursively compare until we find a 'false' - let stack = ((a, b),) - let fuel = 3000 - while stack != () { - let (a, b) = stack.pop() - if a == b { - // Good! - continue - } - fuel -= 1 - if fuel == 0 { - return false - } - // Of course, the types must match - let a-type = type(a) - let b-type = type(b) - if a-type != b-type { - return false - } - - if a-type == array { - if a.len() != b.len() { - return false - } - stack += array.zip(a, b) - - // Only have special checks for composed types and custom types and elements - // of same type - } else if (a-type == content or a-type == dictionary) and eid(a) == eid(b) and tid(a) == tid(b) { - if eid(a) != none or tid(a) != none or a-type == content and a.func() == b.func() { - // Same element id, compare their fields - a = fields(a) - b = fields(b) - a-type = type(a) - if a-type != type(b) { - return false - } - } - if a-type != dictionary or a.len() != b.len() { - // Fields were invalid, or content didn't have the same func - return false - } - for (key, a-val) in a { - if key not in b { - return false - } - stack.push((a-val, b.at(key),)) - } - } else { - return false - } - } - - // No checks failed - true -} diff --git a/src/libs/elembic/element.typ b/src/libs/elembic/element.typ deleted file mode 100644 index d95b8fc..0000000 --- a/src/libs/elembic/element.typ +++ /dev/null @@ -1,3875 +0,0 @@ -#import "data.typ": data, lbl-show-head, lbl-meta-head, lbl-outer-head, lbl-counter-head, lbl-ref-figure-kind-head, lbl-ref-figure-label-head, lbl-ref-figure, lbl-get, lbl-tag, lbl-rule-tag, lbl-old-rule-tag, lbl-special-rule-tag, lbl-data-metadata, lbl-stateful-mode, lbl-leaky-mode, lbl-normal-mode, lbl-auto-mode, lbl-global-select-head, prepared-rule-key, stored-data-key, element-key, element-data-key, element-meta-key, global-data-key, filter-key, special-rule-key, special-data-values, custom-type-key, custom-type-data-key, type-key, element-version, style-modes, style-state -#import "fields.typ" as field-internals -#import "types/base.typ" -#import "types/types.typ" - -// Basic elements for our document tree analysis -#let sequence = [].func() -#let space = [ ].func() -#let styled = { set text(red); [a] }.func() -#let state-update-func = state(".").update(1).func() -#let counter-update-func = counter(".").update(1).func() - -// Default library-wide data. -#let default-global-data = ( - (global-data-key): true, - - // Keep track of versions in case we need some backwards-compatibility behavior - // in the future. - version: element-version, - - // If the style state should be read by set rules as the user has - // enabled stateful mode with `#shoW: e.stateful.toggle(true)`. - stateful: false, - - // First known bib title. - // This is used by leaky mode to attempt to preserve the correct bibliography.title - // property. Evidently, it's not perfect, and leaky mode should be avoided. - first-bib-title: (), - - // Identical to 'global.select-count', this is only here for compatibility - // with older elements. - where-rule-count: 0, - - // Some global settings changeable through set rules. - settings: ( - // Whether non-stateful rules should default to leaky mode. - prefer-leaky: false, - - // Additional elements for which ancestry should be tracked. - // Setting this to 'any' will enable ancestry tracking for all elements - // (POTENTIALLY SLOW!). - track-ancestry: (:), - - // Additional elements which should support ancestry-related filters when - // queried. - store-ancestry: (:), - ), - - // Shared state between elements. - // Differently from settings, this is not meant to be configurable by users. - global: ( - // Version that created the default global data. - version: element-version, - - // Amount of select rules in the style chain so far. - // Used to apply a unique label. - select-count: 0, - - // Current element ancestors, from outermost to innermost. - ancestry-chain: (), - ), - - // Per-element data (set rules and other style chain info). - elements: (:), -) - -// Default per-element data. -#let default-data = ( - (element-data-key): true, - - version: element-version, - - // Chain for foldable fields, that is, fields which have special behavior - // when changed through more than one set rule. By default, specifying the - // same field in two subsequent set rules will have the innermost set rule - // override the value from the previous one, but this can be overridden - // for certain types where it makes sense to combine the two values in - // some way instead. For example, stroke fields have custom folding: if - // you specify 4pt for a stroke field in one set rule and orange in another, - // the final stroke will be 4pt + orange, not orange. - // - // This data structure has an entry for each changed foldable field, laid out as follows: - // ( - // foldable-field-name: ( - // folder: auto or (outer, inner) => combined value // how to combine two values, auto = simple sum, equivalent to (a, b) => a + b - // default: stroke(), // default value for this field to begin folding. This is 'field.default' unless 'required = true'. - // // Then, it is the type's default. - // values: (4pt, orange, ...) // list of all set values for this field (length = amount of times this field was changed) - // // only 'values' is used if possible, for efficiency. E.g.: values.sum(default: stroke()) - // data: ( // list to associate each value with the real style chain index and name. - // (index: 3, name: none, names: (), value: 4pt), // If 'revoke' or 'reset' are used, this list is used instead - // (index: 5, name: none, names: (), value: orange), // so we can know which values were revoked. - // ... - // ) - // ), - // ... - // ) - // - // The final argument passed to the constructor, if any, also has to be folded with the latest folded value, - // or with the field's default value if nothing was changed. However, that step is done separately. So, if - // no set rules change a particular foldable field, it is not present in this dictionary at all. - fold-chain: (:), - - // The current accumulated styles (overridden values for arguments) for the element. - chain: (), - - // Maps each style in the chain to some data. - // This is used to assign names to styles, so they can be revoked later. - data-chain: (), - - // All known names, so we can be aware of invalid revoke rules. - names: (:), - - // List of active revokes, of the form: - // (index: last-chain-index, revoking: name-revoked, name: none / name of the revoke itself) - revoke-chain: (), - - // This is set to true when a rule with '.within(eid)' is used. - track-ancestry: false, - - // Data for filtering. - filters: ( - // Filters applying to this element. Each filter is associated with a rule below. - // While some filters might trivially apply to all instances of an element, - // others might require specific field values to match, for example. - all: (), - - // This is an array of rules to apply for each filter in the same index. - // These rules are applied whenever an element matching the given filter - // is found. - rules: (), - - // Data associated with each filter, such as its name(s). - // Format: - // ((names: ("name1", "name2", ...)), ...) - data: (), - ), - - // Conditional set rules, which only apply to matching instances of an - // element. - cond-sets: ( - // Filters to apply to each set rule. - filters: (), - // For each filter above, the associated set rule args with the changed - // fields. - args: (), - // Data associated with each conditional set rule. - // Format: - // ((names: ("name1", "name2", ...), index: 0), ...) - data: (), - ), - - // Show rules that might apply to this element. - show-rules: ( - // Filters for each show rule. - filters: (), - // For each filter above, the associated show rule function. - callbacks: (), - // Data associated with each show rule. - // Format: - // ((names: ("name1", "name2", ...), index: 0), ...) - data: (), - ), - - // Applied selectors (filters for labels). - selects: ( - filters: (), - labels: (), - data: (), - ) -) - -/// This is meant to be used in a show rule of the form `#show ref: e.ref` to ensure -/// references to custom elements work properly. -/// -/// Please use [`e.prepare`](#eprepare) as it does that automatically, and more if -/// necessary. -/// -/// - args (arguments): ref and extra arguments -/// -> content -#let ref_(..args) = { - assert(args.pos().len() > 0, message: "elembic: element.ref: expected at least one positional argument (reference or label)") - let first-arg = args.pos().first() - - set ref(..args.named()) - show ref: it => { - if ( - it.element == none - or it.element.has("label") and str(it.element.label).starts-with(lbl-ref-figure-label-head) - or type(it.target) != label - ) { - // This is known to be a reference to a custom element - // (or the target is not something we can deal with, i.e. not a label) - return it - } - - let info = data(it.element) - if type(info) == dictionary and "data-kind" in info and info.data-kind == "element-instance" { - let supplement = if it.has("supplement") and it.supplement != none { - (supplement: it.supplement) - } else { - (:) - } - - // Convert into a reference towards the reference figure - let converted-label = label(lbl-ref-figure-label-head + str(it.target)) - let reference = ref(converted-label, ..supplement) - - if "custom-ref" in info and info.custom-ref != none { - show ref.where(target: converted-label): [#info.custom-ref] - - reference - } else { - reference - } - } else { - it - } - } - - if type(first-arg) == content and first-arg.func() == ref { - first-arg - } else { - ref(..args) - } -} - -// Changes stateful mode settings within a certain scope. -// This function will sync all data between all modes (data from normal mode -// goes to state and data from stateful mode goes to normal mode). -// -// Setting it to 'true' tells all set rules to update the state, and also ensures -// getters retrieve the value from the state, even if not explicitly aware of -// stateful mode. -// -// By default, this function will not trigger any changes if one attempts to -// change the stateful mode to its current value. This behavior can be disabled -// with 'force: true', though that is not expected to make a difference in any way. -#let toggle-stateful-mode(enable, force: false) = doc => { - context { - let previous-bib-title = bibliography.title - [#context { - let (global-data, was-first-bib-title) = if ( - type(bibliography.title) == content - and bibliography.title.func() == metadata - and bibliography.title.at("label", default: none) == lbl-data-metadata - ) { - (bibliography.title.value, false) - } else { - ((..default-global-data, first-bib-title: previous-bib-title), true) - } - - set bibliography(title: previous-bib-title) - - if global-data.stateful != enable or force { - if not enable { - // Enabling stateful mode => use data from the style chain - // - // Disabling stateful mode => need to sync stateful with non-stateful, - // so we use data from the state - let chain = style-state.get() - global-data = if chain == () { - default-global-data - } else { - chain.last() - } - - // Store the first known bib title in the state as well - if global-data.first-bib-title == () and was-first-bib-title { - global-data.first-bib-title = previous-bib-title - } - } - - // Notify both modes about it (non-stateful and stateful) - global-data.stateful = enable - - let (show-normal, show-stateful) = if enable { - // TODO: Have a way to keep track of previous toggles and undo them - (none, it => it.value.body) - } else { - (it => it.value.body, none) - } - - show lbl-auto-mode: none - show lbl-normal-mode: show-normal - show lbl-stateful-mode: show-stateful - - // Sync data with style chain for non-stateful modes - show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) - - // Sync data with state for stateful mode - // Push at the start of the scope, pop at the end - [#style-state.update(chain => { - chain.push(global-data) - chain - })#doc#style-state.update(chain => { - _ = chain.pop() - chain - })] - } else { - // Nothing to do: it is already toggled to this value - doc - } - }#lbl-get] - } - - [#metadata(((special-rule-key): "toggle-stateful-mode", version: element-version, enable: enable, force: force))#lbl-special-rule-tag] -} - -// Check if an element instance satisfies a filter. -// -// Assumes this filter already accepts this element, so eid is not checked. -#let verify-filter(fields, eid: none, filter: none, ancestry: ()) = { - if filter == none { - return false - } - if eid == none { - assert(false, message: "elembic: element.verify-filter: eid must not be none") - } - - if "__future" in filter and element-version <= filter.__future.max-version { - return (filter.__future.call)(fields, eid: eid, filter: filter, ancestry: ancestry, __future-version: element-version) - } else if filter.kind == "where" { - return eid == filter.eid and filter.fields.pairs().all(((k, v)) => k in fields and fields.at(k) == v) - } else if filter.kind == "where-any" { - return eid in filter.fields-any and filter.fields-any.at(eid).any(f => f.pairs().all(((k, v)) => k in fields and fields.at(k) == v)) - } else if filter.kind == "custom" { - return (filter.elements == none or eid in filter.elements) and (filter.call)( - fields, eid: eid, ancestry: if filter.may-need-ancestry { ancestry } else { () }, __please-use-var-args: true - ) - } - - // Manually simulate a recursive algorithm. - // Normally, for OR(A, B), we could just call (verify(A), verify(B)), but - // recursive calls are limited and expensive. - // We instead do the following: - // - Have a stack of filters pending evaluation. - // - Have a stack of evaluation results (operands). This is only used for - // non-short circuiting operations (see below). - // - Each time a filter is pushed to the pending stack, we push its operands - // to the pending stack too, until the top of the stack has no further - // operands, and mark the filter as "visited" so we don't add its operands - // again. Note that operands are always pushed in reverse for short circuit - // to work, since we have to evaluate - thus pop from the end - each operand - // in its original order. - // - We evaluate each leaf filter (where or custom) and push their results to - // 'operands' (in reverse order, from last to first). - // - We then reach the filter that will use the latest N results from - // 'operands' and push that filter's evaluated result (e.g. AND of the latest - // two results) into 'operands'. - // - Repeat the process until the filter stack is empty (all evaluated) and - // operands has only a single element (for the root filter). - // - If operands is empty or has more than one element, something bad - // happened. Otherwise, its only remaining element is the evaluated result of - // the root filter. - // - // We also have an "op-stack" to indicate the latest operation whose operands - // were expanded into the filter stack. - // - // The idea is to allow short circuiting when the latest operation is an AND - // or OR. Otherwise, the operation in op-stack is only used to indicate the - // latest operation doesn't short-circuit. - // - // It works as follows: we store the filter stack state in "op-stack" - // whenever we push an operation, such as and, or, xor etc. When the latest - // pushed operation is an "and" and the current filter returned false, we - // immediately restore the filter state at the "and" (ignore its other - // operands) and assign its value to "false". If it was an "or", we do the - // same if the current filter returned true, assigning its value to true. - let filter-stack = (filter,) - let op-stack = () - let operands = () - while filter-stack != () { - let last = filter-stack.last() - - // Expand the latest filter's children into the evaluation stack. - while ( - last.at(filter-key) != "visited" - and ("__future" not in last or element-version > last.__future.max-version) - and "operands" in last - and last.operands != () - ) { - // Ensure we don't reach the parent operation until we have evaluated - // each child operation. - op-stack.push((last.kind, filter-stack.len() - 1)) - filter-stack.last().at(filter-key) = "visited" - if "__subject" in filter { - // Ensure children filters apply to the same subject. - filter-stack += last.operands.map(op => if "__subject" in op { op } else { (..op, __subject: filter.__subject) }).rev() - } else { - // In reverse order to pop the first operand first. - filter-stack += last.operands.rev() - } - last = filter-stack.last() - } - - let filter = filter-stack.pop() - let (kind,) = filter - let fields = fields - let eid = eid - let ancestry = ancestry - if "__subject" in filter { - (fields, eid, ancestry) = filter.__subject - } - - let value = if "__future" in filter and element-version <= filter.__future.max-version { - (filter.__future.call)(fields, eid: eid, filter: filter, ancestry: ancestry, __future-version: element-version) - } else if kind == "where" { - eid == filter.eid and filter.fields.pairs().all(((k, v)) => k in fields and fields.at(k) == v) - } else if kind == "where-any" { - eid in filter.fields-any and filter.fields-any.at(eid).any(f => f.pairs().all(((k, v)) => k in fields and fields.at(k) == v)) - } else if kind == "custom" { - (filter.elements == none or eid in filter.elements) and (filter.call)( - fields, eid: eid, ancestry: if filter.may-need-ancestry { ancestry } else { () }, __please-use-var-args: true - ) - } else if kind == "within" { - // Expand 'within' filter into - // (ancestor 1 matches OR ancestor 2 matches OR ...) - if filter.elements == none or eid in filter.elements { - let (ancestor-filter,) = filter - let matching-ancestors = if "depth" in filter and filter.depth != none and filter.depth > 0 { - let total-depth = ancestry.len() - if total-depth >= filter.depth { - ((total-depth - filter.depth, ancestry.at(total-depth - filter.depth)),) - } else { - () - } - } else if "max-depth" in filter and filter.max-depth != none and filter.max-depth > 0 { - let total-depth = ancestry.len() - if total-depth <= filter.max-depth { - ancestry.enumerate() - } else { - ancestry.enumerate().slice(total-depth - filter.max-depth) - } - } else { - ancestry.enumerate() - } - - filter-stack.push( - ( - (filter-key): true, - element-version: element-version, - kind: "or", - operands: matching-ancestors.map(((i, ancestor)) => ( - ..ancestor-filter, - - // TODO: maybe don't clone the ancestry for each ancestor... - __subject: (eid: ancestor.eid, fields: ancestor.fields, ancestry: ancestry.slice(0, i)) - )), - elements: ancestor-filter.elements, - - // Since this is an internal filter, doesn't matter - ancestry-elements: (:), - may-need-ancestry: true, - ) - ) - - // This filter won't be evaluated, but rather the pushed OR. - continue - } - - // Invalid - false - } else if kind == "and" { - // Due to short-circuiting, a false would have failed earlier. - true - } else if kind == "or" { - // Due to short-circuiting, a true would have succeeded earlier. - false - } else if "operands" in filter { - let first-applied-operand = operands.len() - filter.operands.len() - // Operation requires N operands => take N operands from the top of the - // stack. - let applied-operands = operands.slice(first-applied-operand) - operands = operands.slice(0, first-applied-operand) - - if kind == "not" { - assert(applied-operands.len() == 1, message: "elembic: element.verify-filter: expected one child filter for 'not'") - (filter.elements == none or eid in filter.elements) and not applied-operands.first() - } else if kind == "xor" { - assert(applied-operands.len() == 2, message: "elembic: element.verify-filter: expected two children filters for 'xor'") - // Here the order doesn't matter, since we always need to evaluate both - // XOR operands (no short-circuit). - applied-operands.first() != applied-operands.at(1) - } else { - assert(false, message: "elembic: element.verify-filter: unsupported filter kind '" + kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") - } - } else { - assert(false, message: "elembic: element.verify-filter: unsupported or invalid filter kind '" + kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") - } - - if op-stack != () and op-stack.last().at(1) == filter-stack.len() { - // We have just evaluated this operation. - _ = op-stack.pop() - } - - // Short-circuit: for certain operations, a specific value must stop all - // other operand filters from running. - let (current-op, op-pos) = if op-stack == () { (none, none) } else { op-stack.last() } - while current-op == "and" and not value or current-op == "or" and value { - filter-stack = filter-stack.slice(0, op-pos) - _ = op-stack.pop() - if op-stack == () { - current-op = none - op-pos = none - break - } else { - (current-op, op-pos) = op-stack.last() - } - } - - if current-op not in ("and", "or") { - operands.push(value) - } - } - - if operands.len() != 1 or op-stack != () { - assert(false, message: "elembic: element.verify-filter: filter didn't receive enough operands.") - } - - operands.first() -} - -#let multi-operand-filter(kind: "", arg-count: none) = (..args) => { - assert(args.named() == (:), message: "elembic: filters: invalid named arguments given to '" + kind + "' filter constructor.") - let filters = args.pos() - if arg-count != none and filters.len() != arg-count { - assert(false, message: "elembic: filters: must give exactly " + str(arg-count) + " arguments to a '" + kind + "' filter constructor.") - } - - filters = filters.map(filter => { - if type(filter) == function { - filter = filter(__elembic_data: special-data-values.get-where) - } - assert(type(filter) == dictionary and filter-key in filter, message: "elembic: filters: invalid filter passed to '" + kind + "' constructor, please use 'custom-element.with(...)' to generate a filter.") - - // Flatten "and", "or" - if filter.kind == kind and kind in ("and", "or") { - filter.operands - } else { - (filter,) - } - }).flatten() - - let elements = none - if kind == "and" { - // Merge where filters as an optimization - let where-fields = (:) - let where-eid = none - let may-merge-filters = filters != () - - // Intersect elements. - // Start accepting all elements and narrow it down from there. - for filter in filters { - assert("elements" in filter, message: "elembic: filters.and: this filter operand is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") - if elements == none { - elements = filter.elements - } else if filter.elements != none { - // Cannot add new elements, only remove non-shared elements. - for (eid, elem-data) in elements { - if eid not in filter.elements { - _ = elements.remove(eid) - } - } - } - - if filter.kind == "where" and may-merge-filters { - if where-eid == none { - where-eid = filter.eid - } else if where-eid != filter.eid { - // More than one element to check will never match. - may-merge-filters = false - continue - } - let (eid, fields) = filter - if where-fields == (:) { - where-fields = fields - } else { - for (field, value) in fields { - if field in where-fields and value != where-fields.at(field) { - // and(elem.with(a: 1), elem.with(a: 2)) - // impossible to match - may-merge-filters = false - break - } - - where-fields.insert(field, value) - } - } - } else if may-merge-filters { - // Has a custom filter, don't merge - may-merge-filters = false - } - } - - if may-merge-filters and where-eid != none { - // and(elem, elem.with(a: 0), elem.with(b: 1)) - return ( - (filter-key): true, - element-version: element-version, - kind: "where", - eid: where-eid, - fields: where-fields, - elements: ((where-eid): elements.at(where-eid)), - ancestry-elements: (:), - - // For optimizations - may-need-ancestry: false, - ) - } - - // Ensure the filters won't match on the wrong elements. - // Workaround for e.filters.and_(custom, element) - filters = filters.map(f => f + (elements: elements,)) - - elements - } else if kind == "or" or kind == "xor" { - // Join together. - elements = (:) - let may-merge-filters = kind == "or" and filters != () - let wheres = (:) - - for filter in filters { - if "elements" in filter and filter.elements == none { - // OR(Any, ...) is always Any. - // For XOR, we still have to check all operands so this would also be - // Any. - elements = none - } else if "elements" not in filter or type(filter.elements) != dictionary { - assert(false, message: "elembic: filters: invalid operand filter received by '" + kind + "' filter constructor\n\nhint: this filter was likely constructed with an old elembic version. Please update your packages.") - } else if elements != none { - elements += filter.elements - } - - if may-merge-filters and filter.kind in ("where", "where-any") { - for (eid, fields) in if filter.kind == "where-any" { filter.fields-any } else { ((filter.eid): (filter.fields,)) } { - if eid in wheres { - wheres.at(eid) += fields - } else { - wheres.insert(eid, fields) - } - } - } else if may-merge-filters { - // Not all "where" - may-merge-filters = false - } - } - - if may-merge-filters { - return ( - (filter-key): true, - element-version: element-version, - kind: "where-any", - fields-any: wheres, - elements: elements, - ancestry-elements: (:), - may-need-ancestry: false, - ) - } - - elements - } else if kind == "not" { - // No elements for NOT since it is unrestricted. - // User will have to restrict it manually. - none - } else { - assert(false, message: "elembic: filters: internal error: invalid kind '" + kind + "'") - } - - ( - (filter-key): true, - element-version: element-version, - kind: kind, - operands: filters, - elements: elements, - ancestry-elements: (:) + filters.map(f => f.at("ancestry-elements", default: none)).join(), - may-need-ancestry: filters.any(f => "may-need-ancestry" in f and f.may-need-ancestry), - ) -} - -#let or-filter = multi-operand-filter(kind: "or") -#let and-filter = multi-operand-filter(kind: "and") -#let not-filter = multi-operand-filter(kind: "not", arg-count: 1) -#let xor-filter = multi-operand-filter(kind: "xor", arg-count: 2) -#let custom-filter(callback) = { - assert(type(callback) == function, message: "elembic: filters.custom: 'callback' for custom filter must be a function (fields, eid: eid, ..) => bool.") - - ( - (filter-key): true, - element-version: element-version, - kind: "custom", - call: callback, - elements: none, - ancestry-elements: (:), - may-need-ancestry: true, - ) -} - -/// Filter that only matches when this element is inside another elembic element. -/// -/// For example: -/// -/// ```typ -/// #show: e.show_(e.filters.and(elem, e.filters.within(other-elem)), none) -/// -/// #other-elem(elem[This element will be matched and removed]) -/// #elem[This element stays, as it is not inside `other-elem`] -/// ``` -/// -/// - ancestor-filter (filter): a filter to match potential ancestors. -/// - depth (int): only match at this exact KNOWN depth. -/// - max-depth (int): only match up to this exact KNOWN depth. -/// -> filter -#let within-filter(ancestor-filter, depth: none, max-depth: none) = { - if type(ancestor-filter) == function { - ancestor-filter = ancestor-filter(__elembic_data: special-data-values.get-where) - } - assert(type(ancestor-filter) == dictionary and filter-key in ancestor-filter, message: "elembic: filters.within: invalid filter, please use 'custom-element.with(...)' to generate a filter.") - assert("elements" in ancestor-filter, message: "elembic: filters.within: the ancestor filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") - assert(ancestor-filter.elements != (:), message: "elembic: filters.within: the ancestor filter appears to not be restricted to any elements and is thus impossible to match. It must apply to at least one element (potential ancestor). Consider using a different filter.") - assert(ancestor-filter.elements != none, message: "elembic: filters.within: the ancestor filter appears to apply to any element. It must apply to exactly one element (the one receiving the set rule). Consider using an 'and' filter, e.g. 'e.filters.within(e.filters.and(wibble, e.not(wibble.with(a: 10))))' instead of just 'e.filters.within(e.not(wibble.with(a: 10)))', to restrict it.") - assert(depth == none or max-depth == none, message: "elembic: filters.within: cannot specify both depth and max-depth (please pick one).") - assert(depth == none or type(depth) == int and depth > 0, message: "elembic: filters.within: 'depth' parameter must be a positive integer or 'none'.") - assert(max-depth == none or type(max-depth) == int and max-depth > 0, message: "elembic: filters.within: 'max-depth' parameter must be a positive integer or 'none'.") - - ( - (filter-key): true, - element-version: element-version, - kind: "within", - ancestor-filter: ancestor-filter, - depth: depth, - max-depth: max-depth, - elements: none, - ancestry-elements: (:) + ancestor-filter.elements + ancestor-filter.at("ancestry-elements", default: (:)), - may-need-ancestry: true, - ) -} - -#let request-ancestry-tracking(elements, requests) = { - for (eid, elem-data) in requests { - if eid not in elements { - elements.insert(eid, elem-data.default-data) - } - elements.at(eid).track-ancestry = true - } - - elements -} - -// Apply set and revoke rules to the current per-element data. -#let apply-rules(rules, elements: none, settings: (:), global: (:), extra-output: (:)) = { - for rule in rules { - if "__future" in rule and element-version <= rule.__future.max-version { - let output = (rule.__future.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - let kind = rule.kind - if kind == "settings" { - let (write, transform) = rule - if write != none { - settings += write - } - if transform != none { - settings = transform(settings) - } - } else if kind == "set" { - let (element, args) = rule - let (eid, default-data, fields) = element - - // Forward-compatibility with newer elements - if ( - "__future-rules" in default-data - and "set" in default-data.__future-rules - and element-version <= default-data.__future-rules.set.max-version - ) { - let output = (default-data.__future-rules.set.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - if eid in elements { - elements.at(eid).chain.push(args) - } else { - elements.insert(eid, (..default-data, chain: (args,))) - } - - let names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } - let compat-name = none - if names != () { - let element-data = elements.at(eid) - let index = element-data.chain.len() - 1 - compat-name = names.last() - - // Lazily fill the data chain with 'none' - // Add 'name' for compatibility with older elembic versions - elements.at(eid).data-chain += (none,) * (index - element-data.data-chain.len()) - elements.at(eid).data-chain.push((kind: "set", name: compat-name, names: names)) - - for rule-name in names { - elements.at(eid).names.insert(rule-name, true) - } - } - - if fields.foldable-fields != (:) and args.keys().any(n => n in fields.foldable-fields) { - // A foldable field was specified in this set rule, so we need to record the fold - // data in the corresponding data structures separately for later. - let element-data = elements.at(eid) - let index = element-data.chain.len() - 1 - for (field-name, fold-data) in fields.foldable-fields { - if field-name in args { - let value = args.at(field-name) - let value-data = (index: index, name: compat-name, names: names, value: value) - if field-name in element-data.fold-chain { - elements.at(eid).fold-chain.at(field-name).values.push(value) - elements.at(eid).fold-chain.at(field-name).data.push(value-data) - } else { - elements.at(eid).fold-chain.insert( - field-name, - ( - folder: fold-data.folder, - default: fold-data.default, - values: (value,), - data: (value-data,) - ) - ) - } - } - } - } - } else if kind == "revoke" { - let rule-names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } - let compat-name = if rule-names == () { - none - } else { - rule-names.last() - } - - for (name, element-data) in elements { - // Forward-compatibility with newer elements - if ( - "__future-rules" in element-data - and "revoke" in element-data.__future-rules - and element-version <= element-data.__future-rules.revoke.max-version - ) { - let output = (element-data.__future-rules.revoke.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - // Can only revoke what's before us. - // If this element has no rules with this name, there is nothing to revoke; - // we shouldn't revoke names that come after us (inner rules). - // Note that this potentially includes named revokes as well. - if rule.revoking in element-data.names { - elements.at(name).revoke-chain.push((kind: "revoke", name: compat-name, names: rule-names, index: element-data.chain.len(), revoking: rule.revoking)) - - if rule-names != () { - for rule-name in rule-names { - elements.at(name).names.insert(rule-name, true) - } - } - } - } - } else if kind == "reset" { - // Whether the list of elements that this reset applies to is restricted. - let filtering = rule.eids != () - let rule-names = if "names" in rule { rule.names } else if "name" in rule and rule.name != none { (rule.name,) } else { () } - let compat-name = if rule-names == () { - none - } else { - rule-names.last() - } - - for (name, element-data) in elements { - // Forward-compatibility with newer elements - if ( - "__future-rules" in element-data - and "reset" in element-data.__future-rules - and element-version <= element-data.__future-rules.reset.max-version - ) { - let output = (element-data.__future-rules.reset.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - // Can only revoke what's before us. - // If this element has no rules, no need to add a reset. - if (not filtering or name in rule.eids) and element-data.chain != () { - elements.at(name).revoke-chain.push((kind: "reset", name: compat-name, names: rule-names, index: element-data.chain.len())) - - if rule-names != () { - for rule-name in rule-names { - elements.at(name).names.insert(rule-name, true) - } - } - } - } - } else if kind == "filtered" { - let (filter, rule: inner-rule, names) = rule - if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { - assert(false, message: "elembic: element.filtered: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") - } - let target-elements = filter.elements - if target-elements == none { - assert(false, message: "elembic: element.filtered: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") - } - let base-data = (names: names) - - if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { - elements = request-ancestry-tracking(elements, filter.ancestry-elements) - } - - for (eid, all-elem-data) in target-elements { - // Forward-compatibility with newer elements - if ( - "__future-rules" in all-elem-data.default-data - and "filtered" in all-elem-data.default-data.__future-rules - and element-version <= all-elem-data.default-data.__future-rules.filtered.max-version - ) { - let output = (all-elem-data.default-data.__future-rules.filtered.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - if eid not in elements { - elements.insert(eid, all-elem-data.default-data) - } - if "filters" not in elements.at(eid) { - // Old version - elements.at(eid).filters = default-data.filters - } - - let index = elements.at(eid).chain.len() - let data = (..base-data, index: index) - - elements.at(eid).filters.all.push(filter) - elements.at(eid).filters.rules.push(inner-rule) - elements.at(eid).filters.data.push(data) - - // Push an entry to the data chain so we have an index to assign to - // this filter rule. This allows us to reset() it later. - elements.at(eid).chain.push((:)) - - // Lazily fill the data chain with 'none' - elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) - - // Keep "name" for some compatibility with older versions... - elements.at(eid).data-chain.push( - (kind: "filtered", name: if data.names == () { none } else { data.names.last() }, names: data.names) - ) - - for name in data.names { - // Ensure the name is registered so revoke rules on this name are - // treated as valid. - elements.at(eid).names.insert(name, true) - } - } - } else if kind == "show" { - let (filter, callback, names) = rule - if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { - assert(false, message: "elembic: element.show_: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") - } - let target-elements = filter.elements - if target-elements == none { - assert(false, message: "elembic: element.show_: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") - } - let base-data = (names: names) - - if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { - elements = request-ancestry-tracking(elements, filter.ancestry-elements) - } - - for (eid, all-elem-data) in target-elements { - // Forward-compatibility with newer elements - if ( - "__future-rules" in all-elem-data.default-data - and "show" in all-elem-data.default-data.__future-rules - and element-version <= all-elem-data.default-data.__future-rules.show.max-version - ) { - let output = (all-elem-data.default-data.__future-rules.show.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - if eid not in elements { - elements.insert(eid, all-elem-data.default-data) - } - if "show-rules" not in elements.at(eid) { - // Old version - elements.at(eid).show-rules = default-data.show-rules - } - - let index = elements.at(eid).chain.len() - let data = (..base-data, index: index) - - elements.at(eid).show-rules.filters.push(filter) - elements.at(eid).show-rules.callbacks.push(callback) - elements.at(eid).show-rules.data.push(data) - - // Push an entry to the data chain so we have an index to assign to - // this show rule. This allows us to reset() it later. - elements.at(eid).chain.push((:)) - - // Lazily fill the data chain with 'none' - elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) - - // Keep "name" for some compatibility with older versions... - elements.at(eid).data-chain.push( - (kind: "show", name: if data.names == () { none } else { data.names.last() }, names: data.names) - ) - - for name in data.names { - // Ensure the name is registered so revoke rules on this name are - // treated as valid. - elements.at(eid).names.insert(name, true) - } - } - } else if kind == "cond-set" { - let (filter, args, names, element) = rule - if type(filter) != dictionary or "elements" not in filter or "kind" not in filter { - assert(false, message: "elembic: element.cond-set: invalid filter found while applying rule: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") - } - - if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { - elements = request-ancestry-tracking(elements, filter.ancestry-elements) - } - - let (eid,) = element - - // Forward-compatibility with newer elements - if ( - "__future-rules" in element.default-data - and "cond-set" in element.default-data.__future-rules - and element-version <= element.default-data.__future-rules.cond-set.max-version - ) { - let output = (element.default-data.__future-rules.cond-set.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - if eid not in elements { - elements.insert(eid, element.default-data) - } - if "cond-sets" not in elements.at(eid) { - // Old version - elements.at(eid).cond-sets = default-data.cond-sets - } - - let index = elements.at(eid).chain.len() - let data = (names: names, index: index) - elements.at(eid).cond-sets.filters.push(filter) - elements.at(eid).cond-sets.args.push(args) - elements.at(eid).cond-sets.data.push(data) - - // Push an entry to the data chain so we have an index to assign to - // this filter rule. This allows us to reset() it later. - elements.at(eid).chain.push((:)) - - // Lazily fill the data chain with 'none' - elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) - - // Keep "name" for some compatibility with older versions... - elements.at(eid).data-chain.push( - (kind: "cond-set", name: if data.names == () { none } else { data.names.last() }, names: data.names) - ) - - for name in data.names { - // Ensure the name is registered so revoke rules on this name are - // treated as valid. - elements.at(eid).names.insert(name, true) - } - } else if kind == "select" { - let (element-data: target-elements, names) = rule - let base-data = (names: names) - - for (eid, elem-data) in target-elements { - if "filters" not in elem-data or "labels" not in elem-data { - assert(false, message: "elembic: element.select: missing filters or labels for element " + repr(eid)) - } - let (filters, labels) = elem-data - assert(filters.len() == labels.len(), message: "elembic: element.select: differing lengths for filters and labels found (this is an internal error)") - if filters == () { - continue - } - - let sample-filter = filters.first() - assert( - type(sample-filter) == dictionary - and "kind" in sample-filter - and "elements" in sample-filter - and type(sample-filter.elements) == dictionary - and eid in sample-filter.elements, - message: "elembic: element.select: invalid filter found for element " + repr(eid) + ", it must contain the element's data.\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages." - ) - - let all-elem-data = sample-filter.elements.at(eid) - - // Forward-compatibility with newer elements - if ( - "__future-rules" in all-elem-data.default-data - and "select" in all-elem-data.default-data.__future-rules - and element-version <= all-elem-data.default-data.__future-rules.select.max-version - ) { - let output = (all-elem-data.default-data.__future-rules.select.call)(rule, elements: elements, settings: settings, global: global, extra-output: extra-output, __future-version: element-version) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - continue - } - - for filter in filters { - assert( - type(filter) == dictionary - and "kind" in filter - and "elements" in filter, - message: "elembic: element.select: invalid filter found for element " + repr(eid) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages." - ) - if "ancestry-elements" in filter and filter.ancestry-elements not in (none, (:)) { - elements = request-ancestry-tracking(elements, filter.ancestry-elements) - } - } - - if eid not in elements { - elements.insert(eid, all-elem-data.default-data) - } - if "selects" not in elements.at(eid) { - // Old version - elements.at(eid).selects = default-data.selects - } - - let index = elements.at(eid).chain.len() - let data = (..base-data, index: index) - - elements.at(eid).selects.filters += filters - elements.at(eid).selects.labels += labels - elements.at(eid).selects.data += (data,) * filters.len() - - // Push an entry to the data chain so we have an index to assign to - // this filter rule. This allows us to reset() it later. - elements.at(eid).chain += (((:),) * filters.len()) - - // Lazily fill the data chain with 'none' - elements.at(eid).data-chain += (none,) * (index - elements.at(eid).data-chain.len()) - - // Keep "name" for some compatibility with older versions... - elements.at(eid).data-chain.push( - (kind: "select", name: if data.names == () { none } else { data.names.last() }, names: data.names) - ) - - for name in data.names { - // Ensure the name is registered so revoke rules on this name are - // treated as valid. - elements.at(eid).names.insert(name, true) - } - } - } else if kind == "apply" { - // Mostly a fallback in case the rule is accidentally passed here... - let output = apply-rules(rule.rules, elements: elements, settings: settings, global: global, extra-output: extra-output) - extra-output += output - if "elements" in output { - elements = output.elements - } - if "settings" in output { - settings = output.settings - } - if "global" in output { - global = output.global - } - } else { - assert(false, message: "elembic: element: invalid rule kind '" + rule.kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.") - } - } - - (..extra-output, elements: elements, settings: settings) -} - -// Prepare rule(s), returning a function `doc => ...` to be used in -// `#show: rule`. The rule is attached as metadata to the returned -// content so it can still be accessed outside of a show rule. -// -// This is where we execute our main machinery to apply rules to the -// document, that is, modifications to the global data of custom -// elements. This is done in different ways depending on the mode: -// -// - In normal mode, we create 'get rule' points by annotating -// context blocks with `#lbl-get`. Any modifications to the global -// data are stored as 'set bibliography(title: metadata with data)' -// scoped to context blocks with that label. Therefore, we can access -// the data by retrieving bibliography.title inside those blocks. -// -// The downside is that the entire document is wrapped in context, -// so 'max show rule depth exceeded' errors can occur. -// -// - In leaky mode, it is similar, but we reset bibliography.title -// to an arbitrary value instead of having two context blocks to -// ensure it remains unchanged. -// -// - In stateful mode, we don't wrap anything around the document, -// removing the 'max show rule depth exceeded' problem. Rather, we -// place a state update at the start and another at the end of the -// scope, respectively updating the global data and then undoing -// the update, ensuring it only applies to that scope. -// -// The downside is that this uses 'state()', which can lead to -// relayouts (slower) and even diverging layout. -#let prepare-rule(rule) = { - let rules = if rule.kind == "apply" { rule.rules } else { (rule,) } - - doc => { - let rule = rule - let rules = rules - let mode = rule.mode - - // If there are two 'show:' in a row, flatten into a single set of rules - // instead of running this function multiple times, reducing the - // probability of accidental nested function limit errors. - // - // Note that all rules replace the document with - // [#context { ... doc .. }[#metadata(doc: doc, rule: rule)#lbl-rule-tag]] - // We get the second child to extract the original rule information. - // If 'doc' has the form above, this means the user wrote - // #show: rule1 - // #show: rule2 - // which we want to unify. So we check children len == 2 and unify if the tag is there. - // - // But we also want to accept some parbreaks before, i.e. - // - // #show: rule1 - // - // #show: rule2 - // - // This generates a doc of the form - // [#parbreak()[#context { ... doc .. }[#metadata(doc: doc, rule: rule)#lbl-rule-tag]]] - // So we also check for children len >= 2 (although == 2 is enough in that case) and - // strip any leading parbreaks / spaces / linebreaks, moving them to the new 'doc' (they - // now receive the rules, which is technically incorrect, but in practice is only a problem - // if you have a show rule on parbreak or space or something, which is odd). - // - // Note also that - // #show: rule1 - // - // // hello world! - // // hello world! - // // hello world! - // - // #show: rule2 - // - // produces - // [#parbreak()#space()#space()#parbreak()[... rule substructure with metadata... ]] - // which makes the need for stripping multiple kinds of whitespace explicit. - // We limit at 100 to prevent unbounded search. - // - // We also need to consider the case with - // #show: rule1 - // #set native(field: value) - // #show: rule2 - // - // in which case the document structure (from rule1's view) is - // - // styled(child: [... rule2 ...], styles: ..) - // - // Worse, there could be parbreaks around the set rule: - // - // #show: rule1 - // - // #set native(field: value) - // - // #show: rule2 - // - // leading to - // - // sequence(parbreak(), styled(child: sequence(parbreak(), [ ... rule2 ... ]), styles: ..)) - // - // so we need to perform a document tree walk to lift rule2 and transform this into - // - // #show: apply( - // rule1 - // rule2 - // ) - // - // #set native(field: value) - // ... - // - // Tree walk is performed as follows: - // - // this rule - // \ - // sequence - // \ space parbreak ... sequence - // \ space parbreak ... styled (styles = S) - // \ sequence - // \ space parbreak ... inner rule! - // \ (rule.doc, rule.rule) - // We store each tree level in 'wrappers' so we can reconstruct this document structure without 'rule!'. - // In the case above, that would correspond to - // wrappers = ((sequence, (space, parbreak, ...)), (sequence, space, parbreak, ...), (styled, S), (sequence, space, parbreak, ...)) - // and 'rule' would become 'potential-doc'. - // - // We would then wrap 'rule.doc' in reverse order, adding after the sequence prefix or - // making it the styled child, producing - // - // this rule + inner rule - // \ - // (sequence, apply(this rule, inner rule)) - // \ space parbreak ... sequence - // \ space parbreak ... styled (styles = S) - // \ sequence - // \ space parbreak ... rule.doc - // - // as desired. That is, we move the inner rule up into this rule in order to only consume 1 from - // the rule limit, which is valid since the rule won't apply to spaces, parbreaks, and styled. - // Of course, there could be show rules towards a different structure, but we assume that the user - // understands that show rules on spacing may cause unexpected behavior. - let potential-doc = [#doc] - let wrappers = () - let max-depth = 150 - // Acceptable content types for set rule lifting. - // These are content types that are leaves and we usually don't expect them to - // be replaced in a show rule by an actual custom element. - // If we find something that isn't here, e.g. a block, we stop searching as we can't lift any further rules. - // We also exclude anything with a label since that indicates there might be a show rule application incoming. - let whitespace-funcs = (parbreak, space, linebreak, h, v, state-update-func, counter-update-func) - // Content types we can peek at. - let recursing-funcs = (styled, sequence) - let loop-prefix = none - let loop-children = () - let loop-last = none - - while max-depth > 0 { - // Child is #{ - // set something(abc: def) - // show something: else - // [some stuff] - // } - if potential-doc.func() == styled { - max-depth -= 1 - wrappers.push((styled, potential-doc.styles)) - - // 'Recursively' check the child - potential-doc = [#potential-doc.child] - } else if ( - // Child is #[ - // (parbreak) - // (space) - // #[ sequence, rule or more styles ] - // ] - potential-doc.func() == sequence - and { loop-children = potential-doc.children; loop-children.len() >= 2 } // something like 'if let Sequence(children) = potential-doc { ... }' - and { loop-last = loop-children.last(); loop-last.func() in recursing-funcs } - and max-depth - loop-children.len() > 0 - and { - loop-prefix = loop-children.slice(0, -1); - loop-prefix.all(t => (t.func() in whitespace-funcs or t == []) and t.at("label", default: none) == none) - } - ) { - max-depth -= loop-children.len() - wrappers.push((sequence, loop-prefix)) - - // 'Recursively' check the last child - potential-doc = loop-last - } else { - break - } - } - - // Merge with the closest rule application below us, "moving" it upwards - // and reducing the rule count by 1 - let last-label = none - while ( - potential-doc.func() == sequence - and potential-doc.children.len() == 2 - and { - last-label = potential-doc.children.last().at("label", default: none) - last-label == lbl-rule-tag or last-label == lbl-old-rule-tag - } - ) { - let last = potential-doc.children.last() - let inner-rule = last.value.rule - - // Process all rules below us together with this one - if inner-rule.kind == "apply" { - // Note: apply should automatically distribute modes across its children, - // so it's okay if we don't inherit its own mode here. - rules += inner-rule.rules - } else { - rules.push(inner-rule) - } - - // We assume 'apply' already checked its own rules. - // Therefore, we only need to fold a single time. - // Don't check all rules every time again. - if ( - inner-rule.mode == style-modes.stateful - or mode != style-modes.stateful and inner-rule.mode == style-modes.leaky - or mode == auto - ) { - // Prioritize more explicit modes: - // stateful > leaky > normal - mode = inner-rule.mode - } - - // Convert this into an 'apply' rule - rule = ((prepared-rule-key): true, version: element-version, kind: "apply", rules: rules, mode: mode, name: none, names: ()) - - // Place what's inside, don't place the context block that would run our code again - doc = last.value.doc - - // Reconstruct the document structure. - // Must be in reverse (innermost wrapper to outermost). - for (func, data) in wrappers.rev() { - if func == styled { - doc = styled(doc, data) - } else { - // (sequence, prefix) - // Re-add stripped whitespace and stuff - doc = data.join() + doc - } - } - - if "__future" in last.value and element-version <= last.value.__future.max-version { - let res = (last.value.__future.call)(rule, doc, __future-version: element-version) - - if "doc" in res { - return res.doc - } - } - - if last-label == lbl-old-rule-tag { - // If we're merging with an older rule version, we may have to merge a - // newer version again - potential-doc = last.value.doc - } else { - break - } - } - - // Stateful mode: no context, just push in a state at the start of the scope - // and pop to previous data at the end. - let stateful = { - style-state.update(chain => { - let global-data = if chain == () { - default-global-data - } else { - chain.last() - } - - assert( - global-data.stateful, - message: "elembic: element rule: cannot use a stateful rule without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.toggle(true)' somewhere above this rule, or at the top of the document to apply to all" - ) - - if "settings" not in global-data { - global-data.settings = default-global-data.settings - } - - if "global" not in global-data { - global-data.global = default-global-data.global - } - - global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) - - chain.push(global-data) - chain - }) - doc - style-state.update(chain => { - _ = chain.pop() - chain - }) - } - - // Leaky mode: one context resetting bibliography.title. - let leaky = [#context { - let global-data = if ( - type(bibliography.title) == content - and bibliography.title.func() == metadata - and bibliography.title.at("label", default: none) == lbl-data-metadata - ) { - bibliography.title.value - } else { - // Bibliography title wasn't overridden, so we can use it - (..default-global-data, first-bib-title: bibliography.title) - } - - if mode == auto and ("settings" not in global-data or "prefer-leaky" not in global-data.settings or not global-data.settings.prefer-leaky) { - // User didn't want leaky. - return none - } - - let first-bib-title = global-data.first-bib-title - if first-bib-title == () { - // Nobody has seen the bibliography title (bug?) - first-bib-title = auto - } - - if global-data.stateful { - if mode == auto { - // User chose something else. - // Don't even place anything. - return none - } else { - // Use state instead! - return { - set bibliography(title: first-bib-title) - stateful - } - } - } - - if "settings" not in global-data { - global-data.settings = default-global-data.settings - } - - if "global" not in global-data { - global-data.global = default-global-data.global - } - - global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) - - set bibliography(title: first-bib-title) - show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) - doc - }#lbl-get] - - // Normal mode: two nested contexts: one retrieves the current bibliography title, - // and the other retrieves the title with metadata and restores the current title. - let normal = context { - let previous-bib-title = bibliography.title - [#context { - let global-data = if ( - type(bibliography.title) == content - and bibliography.title.func() == metadata - and bibliography.title.at("label", default: none) == lbl-data-metadata - ) { - bibliography.title.value - } else { - (..default-global-data, first-bib-title: previous-bib-title) - } - - if mode == auto and "settings" in global-data and "prefer-leaky" in global-data.settings and global-data.settings.prefer-leaky { - // User wants leaky. - return none - } - - if global-data.stateful { - if mode == auto { - // User chose something else. - // Don't even place anything. - return none - } else { - // Use state instead! - return { - set bibliography(title: previous-bib-title) - stateful - } - } - } - - if "settings" not in global-data { - global-data.settings = default-global-data.settings - } - - if "global" not in global-data { - global-data.global = default-global-data.global - } - - global-data += apply-rules(rules, elements: global-data.elements, settings: global-data.settings, global: global-data.global) - - set bibliography(title: previous-bib-title) - show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) - doc - }#lbl-get] - } - - let body = if mode == auto { - // Allow user to pick the mode through show rules. - [#metadata((body: stateful))#lbl-stateful-mode] - [#metadata((body: normal))#lbl-normal-mode] - [#leaky] - [#normal#lbl-auto-mode] - } else if mode == style-modes.normal { - normal - } else if mode == style-modes.leaky { - leaky - } else if mode == style-modes.stateful { - stateful - } else { - panic("element rule: unknown mode: " + repr(mode)) - } - - // Add the rule tag after each rule application. - // This allows extracting information about the rule before it is applied. - // It also allows combining the rule with an outer rule before application, - // as we do earlier. - [#body#metadata((version: element-version, routines: (prepare-rule: prepare-rule, apply-rules: apply-rules), doc: doc, rule: rule))#lbl-rule-tag] - } -} - -/// Apply a set rule to a custom element. Check out the Styling guide for more information. -/// -/// Note that this function only accepts non-required fields (that have a `default`). -/// Any required fields must always be specified at call site and, as such, are always -/// be prioritized, so it is pointless to have set rules for those. -/// -/// Keep in mind the limitations when using set rules, as well as revoke, reset and -/// apply rules. -/// -/// As such, when applying many set rules at once, please use `e.apply` instead -/// (or specify them consecutively so `elembic` does that automatically). -/// -/// USAGE: -/// -/// ```typ -/// #show: e.set_(superbox, fill: red) -/// #show: e.set_(superbox, optional-pos-arg1, optional-pos-arg2) -/// -/// // This call will be equivalent to: -/// // #superbox(required-arg, optional-pos-arg1, optional-pos-arg2, fill: red) -/// #superbox(required-arg) -/// ``` -/// -/// - elem (function): element to apply the set rule on -/// - fields (arguments): optional fields to set (positionally or named, depending on the field) -/// -> function -#let set_(elem, ..fields) = { - if type(elem) == function { - elem = data(elem) - } - assert(type(elem) == dictionary, message: "elembic: element.set_: please specify the element's constructor or data in the first parameter") - let (res, args) = (elem.parse-args)(fields, include-required: false) - if not res { - assert(false, message: args) - } - - prepare-rule( - ((prepared-rule-key): true, version: element-version, kind: "set", name: none, names: (), mode: auto, element: (eid: elem.eid, default-data: elem.default-data, fields: elem.fields), args: args) - ) -} - -/// Prepare a selector similar to 'element.where(..args)' -/// which can be used in "show sel: set". Receives a filter -/// generated by 'element.with(fields)' or '(element-data.where)(fields)'. -/// -/// This works by checking the filter within all element instances and, -/// if they match, they receive a unique label to be matched -/// by that selector. The label is then provided to the callback function -/// as the selector. -/// -/// Each requested selector is passed as a separate parameter to the callback. -/// You must wrap the remainder of the document that depends on those selectors -/// in this callback. -/// -/// USAGE: -/// -/// ```typ -/// #e.select(superbox.with(fill: red), prefix: "my first select", superbox.with(width: auto), (red-superbox, auto-superbox) => { -/// // Hide superboxes with red fill or auto width -/// show red-superbox: none -/// show auto-superbox: none -/// -/// // This one is hidden -/// #superbox(fill: red) -/// -/// // This one is hidden -/// #superbox(width: auto) -/// -/// // This one is kept -/// #superbox(fill: green, width: 5pt) -/// }) -/// ``` -/// -/// - args (function): filters in the format 'element.with(field-a: a, field-b: b)'. Note that you must write fields' names even if they are positional. -/// - receiver (function): receives one requested selector per filter as separate arguments, must return content. -/// - prefix (str): a unique prefix for selectors generated by this 'selector' to disambiguate from other calls to this function. -/// -> content -#let select(..args, receiver, prefix: 0) = { - assert(type(prefix) == str, message: "elembic: element.select: please pick a unique string 'prefix:' argument for the selectors generated by this call to 'select' to ensure they don't clash with other calls to 'select'.") - assert(args.named() == (:), message: "elembic: element.select: unexpected named arguments") - assert(type(receiver) == function, message: "elembic: element.select: last argument must be a function receiving each prepared selector as a separate argument") - - let filters = args.pos() - - // (eid: ((index, filter), ...)) - // The idea is to apply all filters for a given eid at once - let filters-by-eid = (:) - // (eid: sel) - let labels-by-eid = (:) - // Elements which still require explicit show rules. - let old-elements = (:) - let ordered-eids = () - - let i = 0 - for filter in filters { - if type(filter) == function { - filter = filter(__elembic_data: special-data-values.get-where) - } - - if type(filter) != dictionary or filter-key not in filter { - if type(filter) == selector { - assert(false, message: "elembic: element.select: Typst-native selectors cannot be specified here, only those of custom elements") - } - assert(false, message: "elembic: element.select: expected a valid filter, such as 'custom-element' or 'custom-element.with(field-name: value, ...)', got " + base.typename(filter)) - } - - if "elements" not in filter { - assert(false, message: "elembic: element.select: invalid filter found while applying rule, as it did not have an 'elements' field: " + repr(filter) + "\nPlease use 'elem.with(field: value, ...)' to create a filter.\n\nhint: it might come from a package's element made with an outdated elembic version. Please update your packages.") - } - - for (eid, elem-data) in filter.elements { - if "sel" not in elem-data { - assert(false, message: "elembic: element.select: filter did not have the element's selector") - } - if elem-data.eid in labels-by-eid and labels-by-eid.at(elem-data.eid) != elem-data.sel { - assert(false, message: "elembic: element.select: filter had a different selector from the others for the same element ID, check if you're not using conflicting library versions (could also be a bug)") - } - - if elem-data.eid not in labels-by-eid { - labels-by-eid.insert(elem-data.eid, elem-data.sel) - } - - if elem-data.eid in filters-by-eid { - filters-by-eid.at(elem-data.eid).push((i, filter)) - } else { - filters-by-eid.insert(elem-data.eid, ((i, filter),)) - ordered-eids.push(elem-data.eid) - } - - if ("version" not in elem-data or elem-data.version <= 1) and ("default-data" not in elem-data or "selects" not in elem-data.default-data) { - old-elements.insert(elem-data.eid, true) - } - } - i += 1 - } - - context { - let previous-bib-title = bibliography.title - [#context { - let global-data = if ( - type(bibliography.title) == content - and bibliography.title.func() == metadata - and bibliography.title.at("label", default: none) == lbl-data-metadata - ) { - bibliography.title.value - } else { - (..default-global-data, first-bib-title: previous-bib-title) - } - - if global-data.stateful { - let chain = style-state.get() - global-data = if chain == () { - default-global-data - } else { - chain.last() - } - } - - // Amount of 'select rules' so far, so we can - // assign a unique number to each query - let rule-counter = global-data.global.select-count - - // Generate labels by counting up, and update counter - let matching-labels = range(0, filters.len()).map(i => label(lbl-global-select-head + prefix + str(rule-counter + i))) - rule-counter += matching-labels.len() - global-data.select-count = rule-counter - - // Provide labels to the body, one per filter - // These labels only match the shown bodies of - // elements with matching field values - let body = receiver(..matching-labels) - - // Apply show rules to the body to add labels to matching elements - let styled-body = ordered-eids.filter(e => e in old-elements).fold(body, (acc, eid) => { - let filters = filters-by-eid.at(eid) - show labels-by-eid.at(eid): it => { - let data = data(it) - let tag = [#metadata(data)#lbl-tag] - let fields = data.fields - - let labeled-it = it - for (i, filter) in filters { - // Check if all positional and named arguments match - // Note: no ancestry support since newer elements don't run this - // code, they use 'select' rules instead - if verify-filter(fields, eid: eid, filter: filter, ancestry: ()) { - // Add corresponding label and preserve tag so 'data(it)' still works - labeled-it = [#[#labeled-it#tag]#matching-labels.at(i)] - } - } - - labeled-it - } - - acc - }) - - set bibliography(title: previous-bib-title) - - let pairs-by-eid = (:) - for eid in ordered-eids { - if eid in old-elements or filters-by-eid.at(eid, default: ()) == () { - continue - } - let pairs = filters-by-eid.at(eid).map(((i, f)) => (f, matching-labels.at(i))) - let (filters, labels) = array.zip(..pairs) - pairs-by-eid.insert(eid, (filters: filters, labels: labels)) - } - - if pairs-by-eid != (:) { - let select-rule = ( - ((prepared-rule-key): true, - version: element-version, - kind: "select", - name: none, - names: (), - mode: auto, - element-data: pairs-by-eid, - ) - ) - global-data += apply-rules( - (select-rule,), - elements: global-data.elements, - settings: global-data.at("settings", default: default-global-data.settings), - global: global-data.at("global", default: default-global-data.global) - ) - } - - // Increase select rule counter for further select rules - if global-data.stateful { - style-state.update(chain => { - chain.push(global-data) - chain - }) - - styled-body - - style-state.update(chain => { - _ = chain.pop() - chain - }) - } else { - show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) - styled-body - } - }#lbl-get] - } - - [#metadata( - ( - (special-rule-key): "select", - version: element-version, - filters: filters, - computed: (filters-by-eid: filters-by-eid, labels-by-eid: labels-by-eid, ordered-eids: ordered-eids), - receiver: receiver, - prefix: prefix - ) - )#lbl-special-rule-tag] -} - -/// Apply filtered rules to a custom element's descendants -/// (but not to itself; for that use `cond-set`). -/// -/// USAGE: -/// -/// ```typ -/// #show: e.filtered( -/// elem, -/// e.set_(elem3, fields: ...) -/// ) -/// ``` -/// -/// When applying many set rules at once, use 'apply' instead of 'set' on the last parameter. -/// -/// - filter (filter): filter specifying which element instances should create this set rule -/// for their children. -/// - rule (rule): which rule to create under matched elements. -/// -> function -#let filtered(filter, rule) = { - if type(filter) == function { - filter = filter(__elembic_data: special-data-values.get-where) - } - assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.filtered: invalid filter, please use 'custom-element.with(...)' to generate a filter.") - assert(type(rule) == function, message: "elembic: element.filtered: this is not a valid rule (not a function), please use functions such as 'set_' to create one.") - assert("elements" in filter, message: "elembic: element.filtered: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") - assert(filter.elements != (:), message: "elembic: element.filtered: this filter appears to not be restricted to any elements and is thus impossible to match. It must apply to exactly one element (the one receiving the set rule). Consider using a different filter.") - assert(filter.elements != none, message: "elembic: element.filtered: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") - - let rule = rule([]).children.last().value.rule - let filtered-rule = ((prepared-rule-key): true, version: element-version, kind: "filtered", filter: filter, rule: rule, name: none, names: (), mode: rule.at("mode", default: auto)) - if rule.kind == "apply" { - // Transpose filtered(filter, apply(a, b, c)) into apply(filtered(filter, a), filtered(filter, b), filtered(filter, c)) - let i = 0 - for inner-rule in rule.rules { - assert(inner-rule.kind in ("show", "set", "revoke", "reset", "cond-set", "filtered"), message: "elembic: element.filtered: can only filter apply, show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + inner-rule.kind + "'") - - rule.rules.at(i) = (..filtered-rule, rule: inner-rule, mode: inner-rule.at("mode", default: auto)) - - i += 1 - } - - // Keep the apply but with everything filtered. - prepare-rule(rule) - } else { - assert(rule.kind in ("show", "set", "revoke", "reset", "cond-set", "filtered"), message: "elembic: element.filtered: can only filter apply, show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + rule.kind + "'") - - prepare-rule(filtered-rule) - } -} - -/// Apply a conditional set rule to a custom element. The set rule is only applied if -/// the given filter matches for that element. -/// -/// Check out the Styling guide for more information. -/// -/// Note that this function only accepts non-required fields (that have a `default`). -/// Any required fields must always be specified at call site and, as such, are always -/// going to be prioritized, so it is pointless to have set rules for those. -/// -/// Keep in mind the limitations when using set rules, as well as revoke, reset and -/// apply rules. -/// -/// As such, when applying many set rules at once, please use `e.apply` instead -/// (or specify them consecutively so `elembic` does that automatically). -/// -/// USAGE: -/// -/// ```typ -/// #show: e.set_(superbox, fill: red) -/// #show: e.cond-set(superbox.with(data: 10), fill: blue) -/// -/// #superbox(data: 5) // this will have red fill -/// #superbox(data: 10) // this will have blue fill -/// ``` -/// -/// - filter (filter): filter specifying which element instances should receive this set rule. -/// - fields (arguments): optional fields to set (positionally or named, depending on the field) -/// -> function -#let cond-set(filter, ..fields) = { - if type(filter) == function { - filter = filter(__elembic_data: special-data-values.get-where) - } - assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.cond-set: invalid filter, please pass just 'custom-element' or use 'custom-element.with(...)' to generate a filter.") - assert("elements" in filter, message: "elembic: element.cond-set: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") - assert(filter.elements != (:), message: "elembic: element.cond-set: this filter appears to not be restricted to any elements and is thus impossible to match. It must apply to exactly one element (the one receiving the set rule). Consider using a different filter.") - assert(filter.elements != none, message: "elembic: element.cond-set: this filter appears to apply to any element. It must apply to exactly one element (the one receiving the set rule). Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") - assert(filter.elements.len() == 1, message: "elembic: element.cond-set: this filter appears to apply to more than one element. It must apply to exactly one element (the one receiving the set rule).") - let (eid, elem) = filter.elements.pairs().first() - - let (res, args) = (elem.parse-args)(fields, include-required: false) - if not res { - assert(false, message: args) - } - - prepare-rule( - ((prepared-rule-key): true, version: element-version, kind: "cond-set", name: none, names: (), mode: auto, filter: filter, element: (eid: elem.eid, default-data: elem.default-data, fields: elem.fields), args: args) - ) -} - -/// Applies a show rule through the elembic stylechain, thus making it -/// revokable and also allowing easy usage of filters. -/// -/// Show rules allow you to transform all occurrences of one or more elements, -/// replacing them with arbitrary document content. -/// -/// For example: -/// -/// ```typ -/// #show: e.show_(elem.with(fill: blue), it => [Hello *#it*!]) -/// -/// #elem(fill: red)[First] -/// #elem(fill: blue)[Second] // displays as "Hello *Second*!" -/// ``` -/// -/// - filter (filter): which element(s) to apply the rule to, with which fields etc. -/// - callback (function | content | str | none): replacement content or transformation function (content -> content) -/// receiving any matched elements and returning what to replace it with. -/// -> function -#let show_(filter, replacement, mode: auto) = { - if type(filter) == function { - filter = filter(__elembic_data: special-data-values.get-where) - } - assert(type(filter) == dictionary and filter-key in filter, message: "elembic: element.show_: invalid filter, please use 'custom-element.with(...)' to generate a filter.") - assert(replacement == none or type(replacement) in (function, str, content), message: "elembic: element.show_: second parameter is not a valid show rule replacement or callback. Must be either a function 'it => content', or the content to unconditionally replace by (if it does not depend on the matched element). For example, you can write 'show: e.show_(elem, it => [*#it*])' to make an element bold, or 'show: e.show_(elem, [Hi])' to always replace it with the word 'Hi'.") - - let callback = replacement - if type(replacement) != function { - replacement = [#replacement] - callback = _ => replacement - } - - prepare-rule(((prepared-rule-key): true, version: element-version, kind: "show", filter: filter, callback: callback, name: none, names: (), mode: mode)) -} - -/// Apply multiple rules (set rules, etc.) at once. -/// -/// These rules do not count towards the "set rule limit" observed in 'Limitations'; -/// `apply` itself will always count as a single rule regardless of the amount of rules -/// inside it (be it 5, 50, or 500). Therefore, -/// **it is recommended to group rules together under `apply` whenever possible.** -/// -/// Note that Elembic will automatically wrap consecutive rules (only whitespace -/// or native set/show rules inbetween) into a single `apply`, bringing the same benefit. -/// -/// USAGE: -/// -/// ```typ -/// #show: e.apply( -/// set_(elem, fields), -/// set_(elem, fields) -/// ) -/// ``` -/// -/// - mode (int): style mode given by the `style-modes` dictionary -/// - args (arguments): rules to apply -/// -> function -#let apply(mode: auto, ..args) = { - assert(args.named() == (:), message: "elembic: element.apply: unexpected named arguments") - assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.apply: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") - - let rules = args.pos().map( - rule => { - assert(type(rule) == function, message: "elembic: element.apply: invalid rule of type " + str(type(rule)) + ", please use 'set_' or some other function from this library to generate it") - - // Call it as if it we were in a show rule. - // It will have some trailing metadata indicating its arguments. - let inner = rule([]) - let rule-data = inner.children.last().value.rule - - if rule-data.kind == "apply" { - // Flatten 'apply' - rule-data.rules - } else { - (rule-data,) - } - } - ).sum(default: ()) - - if mode == auto { - mode = rules.fold(auto, (mode, rule) => { - if ( - rule.mode == style-modes.stateful - or mode != style-modes.stateful and rule.mode == style-modes.leaky - or mode == auto - ) { - // Prioritize more explicit modes: - // stateful > leaky > normal - rule.mode - } else { - mode - } - }) - } - - if mode != auto { - rules = rules.map(r => r + (mode: mode)) - } - - // Set this apply rule's mode as an optimization, but note that we have forcefully altered - // its children's modes above. - prepare-rule(((prepared-rule-key): true, version: element-version, kind: "apply", rules: rules, mode: mode)) -} - -#let settings(..args, mode: auto) = { - assert(args.pos() == (), message: "elembic: element.settings: unexpected positional args") - let args = args.named() - assert(args != (:), message: "elembic: element.settings: please specify some setting, e.g. e.settings(prefer-leaky: true)") - - let write = (:) - let transform = () - for (key, val) in args { - if key not in default-global-data.settings { - assert(false, message: "elembic: element.settings: invalid setting '" + key + "', valid keys are " + default-global-data.settings.keys().map(repr).join(", ")) - } - - let default-setting = default-global-data.settings.at(key) - if key in ("track-ancestry", "store-ancestry") and val != "any" { - if type(val) == array { - let new-elements = (:) - for elem in val { - if type(elem) == function { - elem = data(elem) - } - if type(elem) != dictionary or "eid" not in elem { - assert(false, message: "elembic: element.settings: expected array of elements or literal \"any\" (apply to any element) for setting '" + key + "', got array of '" + str(type(elem)) + "'") - } - - new-elements.insert(elem.eid, elem) - } - - if new-elements != (:) { - transform.push(s => { - let existing = if s != none and key in s { s.at(key) } else { (:) } - if existing == "any" { - // Nothing to change, already applies to all elements - s - } else { - (:..s, (key): (:) + existing + new-elements) - } - }) - } - } else { - assert(false, message: "elembic: element.settings: expected array of elements or literal \"any\" (apply to any element) for setting '" + key + "', got '" + str(type(val)) + "'") - } - } else if key == "prefer-leaky" and type(val) != type(default-setting) { - assert(false, message: "elembic: element.settings: expected type of '" + str(type(default-setting)) + "' for setting '" + key + "', got '" + str(type(val)) + "'") - } else { - write.insert(key, val) - } - } - - let transform = if transform == () { - none - } else if transform.len() == 1 { - transform.first() - } else { - s => transform.fold(s, (acc, fun) => fun(acc)) - } - - prepare-rule(((prepared-rule-key): true, version: element-version, kind: "settings", write: write, transform: transform, mode: mode)) -} - -/// Name a certain rule. Use `e.apply` to name a group of rules. -/// This is used to be able to revoke the rule later with `e.revoke`. -/// -/// Please note that, at the moment, each rule can only have -/// one name. This means that applying multiple `named` on -/// the same set of rules will simply replace the previous -/// names. -/// -/// However, more than one rule can have the same name, allowing both to be -/// revoked at once if needed. -/// -/// USAGE: -/// -/// ```typ -/// #show: e.named( -/// "cool set", -/// e.set_(elem, fields) -/// ) -/// ``` -/// -/// - name (str): The name to give to the rule. -/// - rule (function): The rule to apply this name to. -/// -> function -#let named(..names, rule) = { - assert(names.named() == (:), message: "elembic: element.named: unexpected named arguments") - let names = names.pos() - assert(names != (), message: "elembic: element.named: expected at least two arguments (one or more names and a rule)") - assert(type(rule) == function, message: "elembic: element.named: last parameter is not a valid rule (not a function), please use functions such as 'set_' to create one.") - for name in names { - assert(type(name) == str, message: "elembic: element.named: rule name must be a string, not " + str(type(name))) - assert(name != "", message: "elembic: element.named: name must not be empty") - } - - // For backwards compatibility when only one name was possible - let compat-name = names.last() - let rule = rule([]).children.last().value.rule - if rule.kind == "apply" { - let i = 0 - for inner-rule in rule.rules { - assert(inner-rule.kind in ("show", "set", "revoke", "reset", "filtered", "cond-set"), message: "elembic: element.named: can only name show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + inner-rule.kind + "'") - - rule.rules.at(i).name = compat-name - - if "names" in inner-rule { - rule.rules.at(i).names += names - } else { - rule.rules.at(i).names = names - } - - i += 1 - } - } else { - assert(rule.kind in ("show", "set", "revoke", "reset", "filtered", "cond-set"), message: "elembic: element.named: can only name show, set, revoke, reset, filtered and cond-set rules at this moment, not '" + rule.kind + "'") - rule.name = compat-name - - if "names" in rule { - rule.names += names - } else { - rule.names = names - } - } - - // Re-prepare the rule - prepare-rule(rule) -} - -/// Revoke all rules with a certain name. -/// -/// This is intended to be used in a specific scope, -/// and temporary. This means you are supposed to only revoke the rule -/// for a short portion of the document. If you wish to do the opposite, -/// that is, only apply the rule for a short portion for the document -/// (and have it never apply again afterwards), then please just scope -/// the set rule itself instead. -/// -/// USAGE: -/// -/// ```typ -/// #show: e.named("name", set_(element, fields)) -/// ... -/// #[ -/// #show: e.revoke("name") -/// // rule 'name' doesn't apply here -/// ... -/// ] -/// -/// // Applies here again -/// ... -/// ``` -/// -/// - name (str): name of rules to be revoked -/// - mode (int): style mode given by the `style-modes` dictionary -/// -> function -#let revoke(name, mode: auto) = { - assert(type(name) == str, message: "elembic: element.revoke: rule name must be a string, not " + str(type(name))) - assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.revoke: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") - - prepare-rule(((prepared-rule-key): true, version: element-version, kind: "revoke", revoking: name, name: none, names: (), mode: mode)) -} - -/// Temporarily revoke all active set rules for certain elements (or even all elements if none are specified). -/// Applies only to the current scope, like other rules. -/// -/// USAGE: -/// -/// ```typ -/// #show: e.set_(element, fill: red) -/// #[ -/// // Revoke all previous set rules on 'element' for this scope -/// #show: e.reset(element) -/// #element[This is using the default fill (not red)] -/// ] -/// -/// // Rules not revoked outside the scope -/// #element[This is using red fill] -/// ``` -/// -/// - args (arguments): elements whose rules should be reset, or none to reset all rules -/// - mode (int): style mode given by the `style-modes` dictionary -/// -> function -#let reset(..args, mode: auto) = { - assert(args.named() == (:), message: "elembic: element.reset: unexpected named arguments") - assert(mode == auto or mode == style-modes.normal or mode == style-modes.leaky or mode == style-modes.stateful, message: "elembic: element.reset: invalid mode, must be auto or e.style-modes.(normal / leaky / stateful)") - - let filters = args.pos().map(it => if type(it) == function { data(it) } else { it }) - assert(filters.all(x => type(x) == dictionary and "eid" in x), message: "elembic: element.reset: invalid arguments, please provide a function or element data with at least an 'eid'") - - prepare-rule(((prepared-rule-key): true, version: element-version, kind: "reset", eids: filters.map(x => x.eid), name: none, names: (), mode: mode)) -} - -// Stateful variants -#let stateful-set(..args) = { - apply(set_(..args), mode: style-modes.stateful) -} -#let stateful-cond-set(..args) = { - apply(cond-set(..args), mode: style-modes.stateful) -} -#let stateful-settings = settings.with(mode: style-modes.stateful) -#let stateful-apply = apply.with(mode: style-modes.stateful) -#let stateful-show = show_.with(mode: style-modes.stateful) -#let stateful-revoke = revoke.with(mode: style-modes.stateful) -#let stateful-reset = reset.with(mode: style-modes.stateful) - -// Leaky variants -#let leaky-set(..args) = { - apply(set_(..args), mode: style-modes.leaky) -} -#let leaky-cond-set(..args) = { - apply(cond-set(..args), mode: style-modes.leaky) -} -#let leaky-settings = settings.with(mode: style-modes.leaky) -#let leaky-apply = apply.with(mode: style-modes.leaky) -#let leaky-show = show_.with(mode: style-modes.leaky) -#let leaky-revoke = revoke.with(mode: style-modes.leaky) -#let leaky-reset = reset.with(mode: style-modes.leaky) - -#let leaky-toggle(enable) = leaky-settings(prefer-leaky: enable) - -// Apply revokes and other modifications to the chain and generate a final set -// of fields. -#let fold-styles(chain, data-chain, revoke-chain, fold-chain) = { - // Map name -> up to which index (exclusive) it is revoked. - // - // Importantly, a revoke at index B will apply to - // all rules with the revoked name before that index. - // If that revoke rule is, itself, revoked, that either - // completely eliminates the name from being revoked, - // or it simply leads the name to be revoked up to - // an index A < B. That, or it was also being revoked - // by another unrevoked revoke rule at index C > B, - // in which case the name is still revoked up to C. - // In all cases, the name is always revoked from the - // start until some end index. Otherwise, it isn't - // revoked at all (end index 0). - let active-revokes = (:) - - let first-active-index = 0 - - // Revoke revoked revokes by analyzing revokes in reverse - // order: a revoke that came later always takes priority. - for revoke in revoke-chain.rev() { - // This revoke will revoke rules named 'revoking' up to 'index' in the chain, which - // automatically revokes revoke rules before it as well, since they were added when - // the chain length was smaller (or the same), and 'index' is always the chain length - // at the moment the revoke rule was added. - // - // We don't explicitly add revoke rules to the chain as their order in the revoke-chain - // list is enough to know which revoke rules can revoke others, and the index indicates - // which set rules are revoked. - // - // Regarding the first part of the AND, note that, if a name is already revoked up to - // index C from a later revoke (since we're going in reverse, so this one appears earlier - // than the previous ones), then revoking it up to index B <= C for this revoke is - // unnecessary since the index interval [0, B) is already contained in [0, C). - // - // In other words, only the last revoke for a particular name matters, which is the - // first one we find in this loop. - // - // (As you can see, we assume above that, if revoke 1 comes before revoke 2 in the revoke-chain - // (before reversing), with revoke 1 applying up to chain index B and revoke 2 up to index C, - // then B <= C. This is enforced in 'prepare-rules' as we analyze revokes and push their - // information to the chain in order (outer to inner / earlier to later).) - let was-not-revoked = ( - ( - "names" not in revoke or revoke.names.all(n => n not in active-revokes) - ) - and ( - "names" in revoke or "name" not in revoke or revoke.name == none or revoke.name not in active-revokes - ) - ) - - if revoke.kind == "revoke" and revoke.revoking not in active-revokes and was-not-revoked { - active-revokes.insert(revoke.revoking, revoke.index) - } else if revoke.kind == "reset" and was-not-revoked { - // Applying a reset, so we delete everything before this index and stop revoking since - // any revokes before this reset won't count anymore. - first-active-index = revoke.index - - chain = if chain.len() <= first-active-index { - () - } else { - chain.slice(first-active-index) - } - - data-chain = if data-chain.len() <= first-active-index { - () - } else { - data-chain.slice(first-active-index) - } - - for (field-name, fold-data) in fold-chain { - let first-fold-index = fold-data.data.position(d => d.index >= first-active-index) - if first-fold-index == none { - // All folded values removed. - // The caller will be responsible for joining the default value with the - // final arguments (without any chain values inbetween) if that's necessary. - _ = fold-chain.remove(field-name) - } else { - fold-chain.at(field-name).values = fold-data.values.slice(first-fold-index) - fold-chain.at(field-name).data = fold-data.data.slice(first-fold-index) - } - } - - // No need to analyze any further revoke rules since everything was reset. - break - } - } - - if active-revokes != (:) { - let i = first-active-index - for data in data-chain { - if data != none and ( - "names" in data and data.names.any(n => n in active-revokes and i < active-revokes.at(n)) - or "names" not in data and "name" in data and data.name in active-revokes and i < active-revokes.at(data.name) - ) { - // Nullify changes at this stage - chain.at(i) = (:) - } - - i += 1 - } - - for (field-name, fold-data) in fold-chain { - let filtered-data = fold-data.data.filter(d => ( - // Only keep data without a name in the revoked name map, or, if the - // name is there, then data that came after the name was revoked. - ("names" not in d or d.names.all(n => n not in active-revokes or d.index >= active-revokes.at(n))) - and ("names" in d or "name" not in d or (d.name == none or d.name not in active-revokes or d.index >= active-revokes.at(d.name))) - )) - if filtered-data == () { - _ = fold-chain.remove(field-name) - } else { - fold-chain.at(field-name).data = filtered-data - fold-chain.at(field-name).values = filtered-data.map(d => d.value) - } - } - } - - let final-values = chain.sum(default: (:)) - - // Apply folds separately (their fields' values are meaningless in the above dict) - for (field-name, fold-data) in fold-chain { - final-values.at(field-name) = if fold-data.values == () { - fold-data.default - } else if fold-data.folder == auto { - fold-data.default + fold-data.values.sum() - } else { - fold-data.values.fold(fold-data.default, fold-data.folder) - } - } - - (folded: final-values, active-revokes: active-revokes, first-active-index: first-active-index) -} - -// Retrieves the final chain data for an element, after applying all set rules so far. -#let get-styles(element, elements: (:), use-routine: false) = { - if type(element) == function { - element = data(element) - } - let (eid, default-fields) = if type(element) == dictionary and "eid" in element and "default-fields" in element { - (element.eid, element.default-fields) - } else { - assert(false, message: "elembic: element.get: expected element (function / data dictionary), received " + str(type(element))) - } - - if ( - use-routine - and ("version" not in element or element.version != element-version) - and "routines" in element - and "get-styles" in element.routines - and type(element.routines.get-styles) == function - ) { - // Use the element's own "get styles". - return (element.routines.get-styles)(element, elements: elements) - } - - let element-data = elements.at(eid, default: default-data) - let folded-chain = if element-data.revoke-chain == default-data.revoke-chain and element-data.fold-chain == default-data.fold-chain { - element-data.chain.sum(default: (:)) - } else { - fold-styles(element-data.chain, element-data.data-chain, element-data.revoke-chain, element-data.fold-chain).folded - } - - // No need to do extra folding like in constructor: - // if a foldable field hasn't been specified, it is either equal to - // its default, or it is a required field which has no default and - // thus it is not returned here since it can't be set. - default-fields + folded-chain -} - -/// Reads the current values of element fields after applying set rules. -/// Must be in a context block. -/// -/// This is a stateful version, which doesn't require a callback, but only -/// works on stateful mode (less performant). -/// -/// USAGE: -/// ```typ -/// #show: e.set_(elem, fill: green) -/// // ... -/// #context { -/// // OK -/// assert(e.stateful.get(elem).fill == green) -/// } -/// ``` -/// -/// - receiver (function): function ('get' function) -> content -/// -> content -#let stateful-get(element) = { - let chain = style-state.get() - let global-data = if chain == () { - default-global-data - } else { - chain.last() - } - - assert( - global-data.stateful, - message: "elembic: stateful.get: cannot use this function without enabling the global stateful toggle\n hint: if you don't mind the performance hit, write '#show: e.stateful.toggle(true)' somewhere above the 'context {}' in which this call happens, or at the top of the document to apply to all rules as well" - ) - - get-styles(element, elements: global-data.elements, use-routine: true) -} - -#let prepare-ctx(receiver, include-global: false) = context { - let previous-bib-title = bibliography.title - [#context { - let global-data = if ( - type(bibliography.title) == content - and bibliography.title.func() == metadata - and bibliography.title.at("label", default: none) == lbl-data-metadata - ) { - bibliography.title.value - } else { - (..default-global-data, first-bib-title: previous-bib-title) - } - - if global-data.stateful { - let chain = style-state.get() - global-data = if chain == () { - default-global-data - } else { - chain.last() - } - } - - set bibliography(title: previous-bib-title) - - let getter = get-styles.with(elements: global-data.elements, use-routine: true) - if include-global { - receiver((:..global-data, ctx: (get: getter))) - } else { - receiver(getter) - } - }#lbl-get] -} - -/// Reads the current values of element fields after applying set rules. -/// -/// The callback receives a 'get' function which can be used to read the -/// values for a given element. The content returned by the function, which -/// depends on those values, is then placed into the document. -/// -/// USAGE: -/// ```typ -/// #show: e.set_(elem, fill: green) -/// // ... -/// #e.get(get => { -/// // OK -/// assert(get(elem).fill == green) -/// }) -/// ``` -/// -/// - receiver (function): function ('get' function) -> content -/// -> content -#let prepare-get(receiver) = { - let output = prepare-ctx(include-global: false, receiver) - [#output#metadata(((special-rule-key): "get", version: element-version, receiver: receiver))#lbl-special-rule-tag] -} - -#let prepare-debug(receiver) = { - let output = prepare-ctx(include-global: true, receiver) - [#output#metadata(((special-rule-key): "debug-get", version: element-version, receiver: receiver))#lbl-special-rule-tag] -} - -// Obtain a Typst selector to use to match this element in show rules or in the outline. -// Specify 'meta: true' to match this element in a query, as that selector is -// generated once regardless of show rules. -#let elem-selector(elem, outline: false, outer: false, meta: false) = { - if outline { - assert(not outer, message: "elembic: element.selector: cannot have 'outline: true' and 'outer: true' at the same time, please pick one selector") - assert(not meta, message: "elembic: element.selector: cannot have 'outline: true' and 'meta: true' at the same time, please pick one selector") - let elem-data = data(elem) - assert("outline-sel" in elem-data, message: "elembic: element.selector: this isn't a valid element") - assert(elem-data.outline-sel != none, message: "elembic: element.selector: this element isn't outlinable\n hint: try asking its author to define it as such with 'outline: auto', 'outline: (caption: [...])' or 'outline: (caption: it => ...)'") - elem-data.outline-sel - } else if outer { - assert(not meta, message: "elembic: element.selector: cannot have 'outer: true' and 'meta: true' at the same time, please pick one selector") - data(elem).outer-sel - } else if meta { - let elem-data = data(elem) - elem-data.at("meta-sel", default: elem-data.sel) - } else { - data(elem).sel - } -} - -#let elem-query(filter, before: none, after: none) = { - if type(filter) == function { - filter = filter(__elembic_data: special-data-values.get-where) - } - - if type(filter) != dictionary or filter-key not in filter { - if type(filter) == selector { - assert(false, message: "elembic: element.query: Typst-native selectors cannot be specified here, only those of custom elements") - } - assert(false, message: "elembic: element.query: expected a valid filter, such as 'custom-element' or 'custom-element.with(field-name: value, ...)', got " + base.typename(filter)) - } - - assert("elements" in filter, message: "elembic: element.query: this filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.") - assert(filter.elements != none, message: "elembic: element.query: this filter appears to apply to any element (e.g. it's a 'not' or 'custom' filter). It must match only within a certain set of elements. Consider using an 'and' filter, e.g. 'e.filters.and(wibble, e.not(wibble.with(a: 10)))' instead of just 'e.not(wibble.with(a: 10))', to restrict it.") - - let results = () - for (eid, elem-data) in filter.elements { - if "meta-sel" in elem-data { - let sel = elem-data.meta-sel - if before != none { - sel = selector(sel).before(before) - } - if after != none { - sel = selector(sel).after(after) - } - - results += query(sel).filter( - instance => ( - instance.func() == metadata - and { - let meta = data(instance.value) - - verify-filter( - meta.at("fields", default: (:)), - eid: eid, - filter: filter, - ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry and meta.at("ctx", default: none) != none and "ancestry" in meta.ctx { - meta.ctx.ancestry - } else { - () - } - ) and "rendered" in instance.value - } - ) - ).map( - instance => instance.value.rendered - ) - } else if "sel" in elem-data { - let sel = elem-data.sel - if before != none { - sel = selector(sel).before(before) - } - if after != none { - sel = selector(sel).after(after) - } - // This element is probably too outdated to have ancestry checks anyway, so we don't bother - results += query(sel).filter(instance => verify-filter(data(instance).at("fields", default: (:)), eid: eid, filter: filter, ancestry: ())) - } else { - assert(false, message: "elembic: element.query: filter did not have the element's meta selector") - } - } - - results -} - -/// Applies necessary show rules to the entire document so that custom elements behave -/// properly. This is usually only needed for elements which have custom references, -/// since, in that case, the document-wide rule `#show ref: e.ref` is required. -/// **It is recommended to always use `e.prepare` when using Elembic.** -/// -/// However, **some custom elements also have their own `prepare` functions.** (Read -/// their documentation to know if that's the case.) Then, you may specify their functions -/// as parameters to this function, and this function will run the `prepare` function of -/// each element. Not specifying any elements will just run the default rules, which may -/// still be important. -/// -/// As an example, an element may use its own `prepare` function to apply some special -/// behavior to its `outline`. -/// -/// USAGE: -/// ```rs -/// // Apply default rules + special rules for these elements (if they need it) -/// #show: e.prepare(elemA, elemB) -/// -/// // Apply default rules only -/// #show: e.prepare() -/// ``` -/// - args (arguments): element functions which need special preparation, or none to just apply default rules -/// -> function -#let prepare( - ..args -) = { - assert(args.named() == (:), message: "elembic: element.prepare: unexpected named arguments") - let default-rules = doc => { - show ref: ref_ - - doc - } - - if args.pos() == () { - return default-rules - } - - let elems = args.pos().map(data) - - if elems.len() == 1 and type(args.pos().first()) == content { - assert(false, message: "elembic: element.prepare: expected (optional) element functions as arguments, not the document\n hint: write '#show: e.prepare()', not '#show: e.prepare' - note the parentheses") - } - - assert(elems.all(it => it.data-kind == "element"), message: "elembic: element.prepare: positional arguments must be elements") - let prepares = elems.filter(elem => "prepare" in elem and elem.prepare != none).map(elem => elem.prepare.with(elem.func)) - - doc => { - show: default-rules - prepares.fold(doc, (acc, prepare) => prepare(acc)) - } -} - -/// Creates a new element, returning its constructor. Read the "Creating custom elements" -/// chapter for more information. -/// -/// USAGE: -/// -/// ```typ -/// #import "@preview/elembic:X.X.X" as e: field -/// -/// // For references to apply -/// #show: e.prepare() -/// -/// #let elem = e.element.declare( -/// "elem", -/// prefix: "@preview/my-package,v1", -/// display: it => { -/// [== #it.title] -/// block(fill: it.fill)[#it.inner] -/// }, -/// fields: ( -/// field("fill", e.types.option(e.types.paint)), -/// field("inner", content, default: [Hello!]), -/// field("title", content, default: [Hello!]), -/// ), -/// reference: ( -/// supplement: [Elem], -/// numbering: "1" -/// ), -/// outline: (caption: it => it.title), -/// ) -/// -/// #outline(target: e.selector(elem, outline: true)) -/// -/// #elem() -/// #elem(title: [abc], label: ) -/// @abc -/// ``` -/// -/// - name (str): The element's name. -/// - prefix (str): The element's prefix, used to distinguish it from elements with the same name. This is usually your package's name alongside a (major) version. -/// - display (function): Function `fields => content` to display the element. -/// - fields (array): Array with this element's fields. -/// - parse-args (auto | function): Optional override for the built-in argument parser -/// (or `auto` to keep as is). Must be in the form -/// `function(args, include-required: bool) => dictionary`, where `include-required: true` -/// means required fields are enforced (constructor), while `include-required: false` means -/// they are forbidden (set rules). -/// - typecheck (bool): Set to `false` to disable field typechecking. -/// - allow-unknown-fields (bool): Set to `true` to allow users to specify unknown -/// fields to your element. They are not typechecked and are simply forwarded to -/// the element's fields by the argument parser. -/// - template (none | function): Optional function displayed element => content to define overridable default set rules for your elements, such as paragraph settings. Users can override these settings with show-set rules on elements. -/// - prepare (none | function): Optional function (element, document) => content -/// to define show and set rules that should be applied to the whole document for your -/// element to properly function. -/// - construct (none | function): Optional function that overrides the default -/// element constructor, returning arbitrary content. This should be used over -/// manually wrapping the returned constructor as it ensures set rules and data -/// extraction from the constructor still work. -/// - scope (none | dictionary | module): Optional scope with associated data for your -/// element. This could be a module with constructors for associated elements, for -/// instance. This value can be accessed with `e.scope(elem)`, e.g. -/// `#import e.scope(elem): sub-elem`. -/// - count (none | function): Optional function `counter => (content | function fields => content)` -/// which inserts a counter step before the element. Ensures the element's display function has -/// updated context to get the latest counter value (after the step / update) with -/// `e.counter(it).get()`. Defaults to `counter.step` to step the counter once before -/// each element placed. -/// - labelable (bool): Defaults to `true`, allows specifying `#element(label: )`, which -/// not only ensures show rules on that label work and have access to the element's final fields, -/// but also allows referring to that element. When `false`, the element may have a field -/// named `label` instead, but it won't have these effects. -/// - reference (none | (supplement: none | str | content | function fields => str | content, numbering: none | str | function fields => str | function, custom: none | function fields => content)): -/// When not `none`, allows referring to the new element with Typst's built-in -/// `@ref` syntax. Requires the user to execute `#show: e.prepare()` at the top -/// of their document (it is part of the default rules, so `prepare` needs no -/// arguments there). Specify either a `supplement` and `numbering` for references -/// looking like "Name 2", and/or `custom` to show some fully customized content -/// for the reference instead. -/// - outline (none | auto | dictionary): -/// Accepts either `auto` or a dictionary of the form -/// `(caption: str | content | function fields => content)`. -/// When not `none`, allows creating an outline for the element's appearances -/// with `#outline(target: e.selector(elem, outline: true))`. When set to `auto`, -/// the entries will display "Name 2" based on reference information. When a caption -/// is specified, it will display as "Name 2: caption", unless supplement and numbering -/// for reference are both none. -/// - synthesize (none | function): Can be set to a function `fields => fields` to -/// override final values of fields, or create new fields based on final values of -/// fields, before the first show rule. When computing new fields based on other -/// fields, please specify those new fields in the fields array with -/// `synthesized: true`. This forbids the user from specifying them manually, -/// but allows them to filter based on that field. -/// - contextual (bool): When set to `true`, functions `fields => something` for -/// other options, including `display`, will be able to access the current -/// values of set rules with `(e.ctx(fields).get)(other-elem)`. In addition, -/// an additional context block is created, so that you may access the correct -/// values for `native-elem.field` in the context. In practice, this is a bit -/// expensive, and so this option shouldn't be enabled unless you need precisely -/// `bibliography.title`, or you really need to get set rule information from -/// other elements within functions such as `synthesize` or `display`. -/// -> function -#let declare( - name, - display: none, - fields: none, - prefix: none, - parse-args: auto, - typecheck: true, - allow-unknown-fields: false, - template: none, - prepare: none, - construct: none, - scope: none, - count: counter.step, - labelable: true, - reference: none, - outline: none, - synthesize: none, - contextual: false, -) = { - assert(type(display) == function, message: "elembic: element.declare: please specify a show rule in 'display:' to determine how your element is displayed.") - - let fields-hint = if type(fields) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single field: write 'fields: (field,)', not 'fields: (field)'" } else { "" } - assert(type(fields) == array, message: "elembic: element.declare: please specify an array of fields, creating each field with the 'field' function. It can be empty with '()'." + fields-hint) - assert(prefix != none, message: "elembic: element.declare: please specify a 'prefix: ...' for your type, to distinguish it from types with the same name. If you are writing a package or template to be used by others, please do not use an empty prefix.") - assert(type(prefix) == str, message: "elembic: element.declare: the prefix must be a string, not '" + str(type(prefix)) + "'") - assert(parse-args == auto or type(parse-args) == function, message: "elembic: element.declare: 'parse-args' must be either 'auto' (use built-in parser) or a function (default arg parser, fields: dictionary, typecheck: bool) => (user arguments, include-required: true (required fields must be specified - in constructor) / false (required fields must be omitted - in set rules)) => (bool (true on success, false on error), dictionary with parsed fields (or error message string if the bool is false)).") - assert(type(typecheck) == bool, message: "elembic: element.declare: the 'typecheck' argument must be a boolean (true to enable typechecking, false to disable).") - assert(type(allow-unknown-fields) == bool, message: "elembic: element.declare: the 'allow-unknown-fields' argument must be a boolean.") - assert(template == none or type(template) == function, message: "elembic: element.declare: 'template' must be 'none' or a function displayed element => content (usually set rules applied on the displayed element). This is used to add a set of overridable set rules to the element, such as paragraph settings.") - assert(prepare == none or type(prepare) == function, message: "elembic: element.declare: 'prepare' must be 'none' or a function (element, document) => styled document (used to apply show and set rules to the document).") - assert(count == none or type(count) == function, message: "elembic: element.declare: 'count' must be 'none', a function counter => counter step/update element, or a function counter => final fields => counter step/update element.") - assert(synthesize == none or type(synthesize) == function, message: "elembic: element.declare: 'synthesize' must be 'none' or a function element fields => element fields.") - assert(contextual == auto or type(contextual) == bool, message: "elembic: element.declare: 'contextual' must be 'auto' (true if using a contextual feature) or a boolean (true to wrap the output in a 'context { ... }', false to not).") - assert(construct == none or type(construct) == function, message: "elembic: element.declare: 'construct' must be 'none' (use default constructor) or a function receiving the original constructor and returning the new constructor.") - assert(scope == none or type(scope) in (dictionary, module), message: "elembic: element.declare: 'scope' must be either 'none', a dictionary or a module") - assert(type(labelable) == bool, message: "elembic: element.declare: 'labelable' must be a boolean (true to enable the special 'label' constructor argument, false to disable it)") - assert( - reference == none - or type(reference) == dictionary - and reference.keys().all(x => x in ("supplement", "numbering", "custom")) - and ("supplement" not in reference or reference.supplement == none or type(reference.supplement) in (str, content, function)) - and ("numbering" not in reference or reference.numbering == none or type(reference.numbering) in (str, function)) - and ("custom" not in reference or reference.custom == none or type(reference.custom) == function), - message: "elembic: element.declare: 'reference' must be 'none' or a dictionary (supplement: \"Name\" or [Name] or function fields => supplement, numbering: \"1.\" or function fields => (str / function numbers => content), custom (optional): none (default) or function fields => content)." - ) - assert( - reference == none or "supplement" in reference and "numbering" in reference or "custom" in reference, - message: "elembic: element.declare: reference must either have 'custom', or have both 'supplement' and 'numbering' (or all three, though 'custom' has priority when displaying references)." - ) - assert( - outline == none - or outline == auto - or type(outline) == dictionary - and "caption" in outline, - message: "elembic: element.declare: 'outline' must be 'none', 'auto' (to use data from 'reference') or a dictionary with 'caption'." - ) - assert(outline != auto or reference != none, message: "elembic: element.declare: if 'outline' is set to 'auto', 'reference' must be specified and not be 'none'.") - assert(labelable or reference == none, message: "elembic: element.declare: 'labelable' must be true for 'reference' to not be 'none'") - - // All element args as originally provided. - let elem-args = arguments( - name, - display: display, - fields: fields, - prefix: prefix, - parse-args: parse-args, - typecheck: typecheck, - allow-unknown-fields: allow-unknown-fields, - template: template, - prepare: prepare, - construct: construct, - scope: scope, - count: count, - labelable: labelable, - reference: reference, - outline: outline, - synthesize: synthesize, - contextual: contextual, - ) - - if contextual == auto { - // Provide separate context for synthesize. - // By default, assume it isn't needed. - contextual = synthesize != none - } - - let eid = base.unique-id("e", prefix, name) - let lbl-show = label(lbl-show-head + eid) - let lbl-meta = label(lbl-meta-head + eid) - let lbl-outer = label(lbl-outer-head + eid) - let ref-figure-kind = if reference == none and outline == none { none } else { lbl-ref-figure-kind-head + eid } - // Use same counter as hidden figure for ease of use - let counter-key = lbl-counter-head + eid - let element-counter = counter(counter-key) - let count = if count == none { none } else { count(element-counter) } - let count-needs-fields = type(count) == function - let custom-ref = if reference != none and "custom" in reference and type(reference.custom) == function { reference.custom } else { none } - - let supplement-type = if reference == none or "supplement" not in reference { - none - } else { - type(reference.supplement) - } - let numbering-type = if reference == none or "numbering" not in reference { - none - } else { - type(reference.numbering) - } - let caption-type = if outline == none or outline == auto { - none - } else { - type(outline.caption) - } - - let fields = field-internals.parse-fields(fields, allow-unknown-fields: allow-unknown-fields) - let (all-fields, user-fields, foldable-fields) = fields - - if labelable and "label" in all-fields { - assert(false, message: "elembic: element.declare: labelable element cannot have a conflicting 'label' field\n hint: you can set 'labelable: false' to disable the special label parameter, but note that it will then be impossible to refer to your element") - } - - let default-arg-parser = field-internals.generate-arg-parser( - fields: fields, - general-error-prefix: "elembic: element '" + name + "': ", - field-error-prefix: field-name => "field '" + field-name + "' of element '" + name + "': ", - typecheck: typecheck - ) - - let parse-args = if parse-args == auto { - default-arg-parser - } else { - let parse-args = parse-args(default-arg-parser, fields: fields, typecheck: typecheck) - if type(parse-args) != function { - assert(false, message: "elembic: element.declare: 'parse-args', when specified as a function, receives the default arg parser alongside `fields: fields dictionary` and `typecheck: bool`, and must return a function (the new arg parser), and not " + base.typename(parse-args)) - } - - parse-args - } - - let default-fields = fields.user-fields.values().map(f => if f.required { (:) } else { ((f.name): f.default) }).sum(default: (:)) - - let set-rule = set_.with((parse-args: parse-args, eid: eid, default-data: default-data, fields: fields)) - - let get-rule(receiver) = prepare-get(g => receiver(g((eid: eid, default-fields: default-fields)))) - - // Partial version of element data to store in filters. - let partial-element-data = ( - version: element-version, - name: name, - eid: eid, - parse-args: parse-args, - default-data: default-data, - default-global-data: default-global-data, - fields: fields, - sel: lbl-show, - meta-sel: lbl-meta, - routines: ( - prepare-rule: prepare-rule, - apply-rules: apply-rules, - get-styles: get-styles, - fold-styles: fold-styles, - verify-filter: verify-filter, - select: select, - toggle-stateful: toggle-stateful-mode, - settings: settings - ) - ) - - // Prepare a filter which should be passed to 'select()'. - // This function will specify which field values for this - // element should be matched. - let where(func) = (..args) => { - assert(args.pos().len() == 0, message: "elembic: unexpected positional arguments\nhint: here, specify positional fields as named arguments, using their names") - let args = args.named() - - if not allow-unknown-fields { - // Note: 'where' on synthesized fields is legal, - // so we check 'all-fields' rather than 'user-fields'. - let unknown-fields = args.keys().filter(k => k not in all-fields and (not labelable or k != "label")) - if unknown-fields != () { - let s = if unknown-fields.len() == 1 { "" } else { "s" } - assert(false, message: "elembic: element.where: element '" + name + "': unknown field" + s + " " + unknown-fields.map(f => "'" + f + "'").join(", ")) - } - } - - ( - (filter-key): true, - element-version: element-version, - kind: "where", - eid: eid, - fields: args, - sel: lbl-show, - elements: ((eid): (:..partial-element-data, func: func)), - ancestry-elements: (:), - may-need-ancestry: false, - ) - } - - let elem-data = ( - (element-key): true, - version: element-version, - name: name, - eid: eid, - scope: scope, - set_: set-rule, - get: get-rule, - where: none, // Filled later when func is known - sel: lbl-show, - meta-sel: lbl-meta, - outer-sel: lbl-outer, - outline-sel: if outline == none { none } else { figure.where(kind: ref-figure-kind) }, - counter: element-counter, - parse-args: parse-args, - default-data: default-data, - default-global-data: default-global-data, - default-fields: default-fields, - routines: partial-element-data.routines, - user-fields: user-fields, - all-fields: all-fields, - fields: fields, - typecheck: typecheck, - allow-unknown-fields: allow-unknown-fields, - template: template, - prepare: prepare, - default-constructor: none, - func: none, - elem-args: elem-args, - ) - - // Figure placed for referencing to work. - let ref-figure(tag, synthesized-fields, ref-label) = { - let numbering = if numbering-type == str { - reference.numbering - } else if numbering-type == function { - let numbering = (reference.numbering)(synthesized-fields) - assert(type(numbering) in (str, function), message: "elembic: element: 'reference.numbering' must be a function fields => numbering (a string or a function), but returned " + str(type(numbering))) - numbering - } else { - none - } - - let number = if numbering == none { none } else { element-counter.display(numbering) } - - let caption = if caption-type == function { - (caption: (outline.caption)(synthesized-fields)) - } else if caption-type in (str, content) { - (caption: [#outline.caption]) - } else if outline == auto { - if ( - "supplement" in reference and "numbering" in reference - or "custom-ref" not in tag - or tag.custom-ref == none - ) { - // Add some caption so it is displayed with the supplement and - // number, but remove useless separator - (caption: figure.caption(separator: "")[]) - } else { - // No supplement or number, but there are custom reference - // contents, so we display that - (caption: tag.custom-ref) - } - } else { - (:) - } - - let ref-figure = [#figure( - supplement: if supplement-type in (str, content) { - [#reference.supplement] - } else if supplement-type == function { - (reference.supplement)(synthesized-fields) - } else { - [] - }, - - numbering: if number == none { none } else { _ => number }, - - kind: ref-figure-kind, - - ..if sys.version >= version(0, 12, 0) { - (placement: none, scope: "column") - }, - - ..caption - )[#[]#metadata(tag)#lbl-tag]#ref-label] - - let tagged-figure = [#[#ref-figure#metadata(tag)#lbl-tag]#lbl-ref-figure] - - show figure: none - - tagged-figure - } - - let apply-show-rules(body, rule, show-rules) = { - if rule >= show-rules.len() { - rule = show-rules.len() - 1 - } else if rule < 0 { - assert(false, message: "elembic: internal error: show rule index cannot be negative") - } - - // Show rules are applied from last to first. - // The first is the base case. - if rule == 0 { - show: show-rules.at(rule) - body - } else { - // Don't recursively apply show rules immediately. - // Do it lazily through a matching show rule. - // This is so that a show rule that doesn't place down 'it' - // stops further show rules from executing. - // - // We could use 'context' for this, but then the show rule - // limit is lower even for 'it => it' (60 vs 30). It is always - // lower when the show rule is of the form 'it => element(it)', - // however, but it still feels like a waste to force it to be - // lower in all cases. - let lbl-tmp-show = label(str(lbl-show) + "-rule" + str(rule)) - (show-rules.at(rule))({ - // Take just the first child to remove the label. - // Add tag AFTER the show rule so data() can still pick it up. - show lbl-tmp-show: it => apply-show-rules(it.children.first(), rule - 1, show-rules) - [#[#body#[]]#lbl-tmp-show] - } + [#metadata(data(body))#lbl-tag]) - } - } - - // Sentinel for 'unspecified value' - let _missing() = {} - let std-label = label - - let default-constructor(..args, __elembic_data: none, __elembic_func: auto, __elembic_mode: auto, __elembic_settings: (:), label: _missing) = { - if __elembic_func == auto { - __elembic_func = default-constructor - } - - let default-constructor = default-constructor.with(__elembic_func: __elembic_func) - if __elembic_data != none { - return if __elembic_data == special-data-values.get-data { - (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)) - } else if __elembic_data == special-data-values.get-where { - if label == _missing { - where(__elembic_func)(..args) - } else { - where(__elembic_func)(..args, label: label) - } - } else { - assert(false, message: "elembic: element: invalid data key to constructor: " + repr(__elembic_data)) - } - } - - let labeling = false - let ref-label = none - if labelable { - if label == _missing { - label = none - } else if type(label) == std-label { - ref-label = std-label(lbl-ref-figure-label-head + str(label)) - labeling = true - } else if label != none { - assert(false, message: "elembic: element '" + name + "': expected label or 'none' for 'label', found " + base.typename(label)) - } - } else if label == _missing { - label = none - } else { - // Also parse label as a field if we don't want element to be labelable - args = arguments(..args, label: label) - } - - let (res, args) = parse-args(args, include-required: true) - if not res { - assert(false, message: args) - } - - // Step the counter early if we don't need additional context - let early-step = if not count-needs-fields { count } - - let inner = early-step + [#context { - let previous-bib-title = bibliography.title - [#context { - set bibliography(title: previous-bib-title) - - // Only update style chain if needed, e.g. filtered rules - let data-changed = false - let global-data = if ( - type(bibliography.title) == content - and bibliography.title.func() == metadata - and bibliography.title.at("label", default: none) == lbl-data-metadata - ) { - bibliography.title.value - } else { - (..default-global-data, first-bib-title: previous-bib-title) - } - - let is-stateful = global-data.stateful - if is-stateful { - let chain = style-state.get() - global-data = if chain == () { - default-global-data - } else { - chain.last() - } - } - - let ancestry = () - let synthesized-futures = () // forward-compat callbacks which need synthesized fields - if "global" in global-data { - if "ancestry-chain" in global-data.global { - ancestry = global-data.global.ancestry-chain - } - - // For set rules from the future... - if "__futures" in global-data.global and "global-data" in global-data.global.__futures { - for future in global-data.global.__futures.global-data { - if element-version <= future.max-version { - let res = (future.call)( - global-data: global-data, - element-data: global-data.elements.at(eid, default: default-data), - args: args, - all-element-data: (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)), - __future-version: element-version - ) - - if "global-data" in res { - global-data = res.global-data - if "data-changed" in res { - // Maybe we don't want to forward changes to children - // More efficient etc. - data-changed = data-changed or res.data-changed - } else { - // Assume we want to forward these changes to children - data-changed = true - } - } - - continue - } - } - } - } - - let element-data = global-data.elements.at(eid, default: default-data) - - if "__futures" in element-data { - if "construct" in element-data.__futures { - for future in element-data.__futures.construct { - if element-version <= future.max-version { - let res = (future.call)( - global-data: global-data, - element-data: element-data, - args: args, - all-element-data: (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)), - __future-version: element-version - ) - - if "construct" in res { - return res.construct - } - } - } - } - - if "element-data" in element-data.__futures { - for future in element-data.__futures.element-data { - if element-version <= future.max-version { - let res = (future.call)( - global-data: global-data, - element-data: element-data, - args: args, - all-element-data: (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)), - __future-version: element-version - ) - - if "construct" in res { - return res.construct - } - - if "element-data" in res { - element-data = res.element-data - } - } - } - } - - if "synthesized-fields" in element-data.__futures { - synthesized-futures += element-data.__futures.synthesized-fields.filter(f => element-version <= f.max-version) - } - } - - let has-synthesized-futures = synthesized-futures != () - let settings = if "settings" in global-data { global-data.settings } else { default-global-data.settings } - let filters = element-data.at("filters", default: default-data.filters) - let has-filters = filters.all != () - let cond-sets = element-data.at("cond-sets", default: default-data.cond-sets) - let has-cond-sets = cond-sets.args != () - let show-rules = element-data.at("show-rules", default: default-data.show-rules) - let has-show-rules = show-rules.callbacks != () - let selects = element-data.at("selects", default: default-data.selects) - let has-selects = selects.filters != () - let has-ancestry-tracking = ( - // Either a rule with a 'within(this element)' filter was used, or - // the user specifically requested ancestry tracking. - element-data.at("track-ancestry", default: default-data.track-ancestry) - or "track-ancestry" in settings and ( - settings.track-ancestry == "any" - or eid in settings.track-ancestry - ) - ) - - // Whether ancestry should be made available in a query() for this - // element, allowing usage of 'within()' rules for that element in a - // query. - let store-ancestry = has-ancestry-tracking or "store-ancestry" in settings and ( - settings.store-ancestry == "any" - or eid in settings.store-ancestry - ) - - let updates-stylechain-inside = has-filters or has-ancestry-tracking - - let (folded-fields, constructed-fields, active-revokes, first-active-index) = if ( - element-data.revoke-chain == default-data.revoke-chain - and ( - foldable-fields == (:) - or element-data.fold-chain == default-data.fold-chain - and args.keys().all(f => f not in foldable-fields) - ) - ) { - let folded-fields = default-fields + element-data.chain.sum(default: (:)) - // Sum the chain of dictionaries so that the latest value specified for - // each property wins. - (folded-fields, folded-fields + args, (:), 0) - } else { - // We can't just sum, we need to filter and fold first. - // Memoize this operation through a function. - let (folded, active-revokes, first-active-index) = fold-styles(element-data.chain, element-data.data-chain, element-data.revoke-chain, element-data.fold-chain) - let outer-chain = default-fields + folded - let finalized-chain = outer-chain + args - - // Fold received arguments with outer chain or defaults - for (field-name, fold-data) in foldable-fields { - if field-name in args { - let outer = outer-chain.at(field-name, default: fold-data.default) - if fold-data.folder == auto { - finalized-chain.insert(field-name, outer + args.at(field-name)) - } else { - finalized-chain.insert(field-name, (fold-data.folder)(outer, args.at(field-name))) - } - } - } - - (outer-chain, finalized-chain, active-revokes, first-active-index) - } - - let filter-revokes - let filter-first-active-index - let editable-global-data - if has-filters or has-synthesized-futures { - // The closures inside context {} below will capture global-data, - // reducing potential for memoization of their output, so, for - // performance reasons, we only pass the real global data if - // necessary due to filtering (which will update the data on a - // match). - editable-global-data = global-data - filter-revokes = active-revokes - filter-first-active-index = first-active-index - } else if has-cond-sets or has-show-rules or has-selects { - // No need for global data, but still need revokes to see which - // conditional sets were revoked - filter-revokes = active-revokes - filter-first-active-index = first-active-index - if updates-stylechain-inside { - editable-global-data = global-data - } - } else if updates-stylechain-inside { - editable-global-data = global-data - } - - let cond-set-foldable-fields - if has-cond-sets { - cond-set-foldable-fields = foldable-fields - } - - let all-elem-data-for-futures - let element-data-for-futures - if has-synthesized-futures { - all-elem-data-for-futures = (data-kind: "element", ..elem-data, func: __elembic_func, default-constructor: default-constructor, where: where(__elembic_func)) - element-data-for-futures = element-data - } - - let shown = { - let tag = ( - data-kind: "element-instance", - body: none, - fields: constructed-fields, - func: __elembic_func, - scope: scope, - default-constructor: default-constructor, - name: name, - eid: eid, - ctx: if contextual { - // Note: we add ancestry later if there is ancestry tracking - // to avoid interfering with memoization of other things - (get: get-styles.with(elements: global-data.elements), ancestry: ancestry) - } else { - (:) - }, - counter: element-counter, - reference: reference, - custom-ref: none, - fields-known: true, - valid: true - ) - - { - // Use context for synthesize as well - let synthesized-fields = if synthesize == none { - constructed-fields - } else { - // Pass contextual information to synthesize - // Remove it afterwards to ensure the final tag's 'fields' won't - // have its own copy of the tag - let new-fields = synthesize(constructed-fields + ((stored-data-key): tag)) - if type(new-fields) != dictionary { - assert(false, message: "elembic: element '" + name + "': 'synthesize' didn't return a dictionary, but rather " + repr(new-fields) + " (a(n) '" + str(type(new-fields)) + "') instead). Please contact the element author.") - } - if stored-data-key in new-fields { - _ = new-fields.remove(stored-data-key) - } - new-fields - } - - if labelable and label != none and label != _missing { - synthesized-fields.label = label - } - - // Update synthesized fields BEFORE applying filters! - if has-cond-sets { - let i = 0 - let new-synthesized-fields = folded-fields // only add args later (args must win) - let affected-fields = (:) - for filter in cond-sets.filters { - let data = cond-sets.data.at(i) - if ( - filter != none - and (data.index == none or data.index >= filter-first-active-index) - and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) - and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) - ) { - let cond-args = cond-sets.args.at(i) - - affected-fields += cond-args - - // Fold received arguments with existing fields or defaults - for (field-name, value) in cond-args { - if field-name in cond-set-foldable-fields { - let fold-data = cond-set-foldable-fields.at(field-name) - let outer = new-synthesized-fields.at(field-name, default: fold-data.default) - if fold-data.folder == auto { - new-synthesized-fields.insert(field-name, outer + value) - } else { - new-synthesized-fields.insert(field-name, (fold-data.folder)(outer, value)) - } - } else { - new-synthesized-fields.insert(field-name, value) - } - } - } - i += 1 - } - - // Fold args again (they must win). - for (field-name, value) in synthesized-fields { - if field-name not in args { - // Not an argument, and we already folded it with cond-sets - // before (not a synthesized field), so stop. - continue - } - - if ( - field-name in affected-fields - and field-name in cond-set-foldable-fields - // If field was changed due to synthetization, don't allow - // folding it further - and constructed-fields.at(field-name) == value - ) { - let fold-data = cond-set-foldable-fields.at(field-name) - if fold-data.folder == auto { - new-synthesized-fields.at(field-name) += args.at(field-name) - } else { - new-synthesized-fields.at(field-name) = (fold-data.folder)(new-synthesized-fields.at(field-name), args.at(field-name)) - } - } else { - // Undo (give precedence to already folded and synthesized argument) - new-synthesized-fields.insert(field-name, value) - } - } - - synthesized-fields = new-synthesized-fields - } - - let new-global-data = if data-changed { editable-global-data } else { none } - if has-synthesized-futures { - if new-global-data == none { - new-global-data = editable-global-data - } - let element-data-for-futures = element-data-for-futures - for future in synthesized-futures { - let res = (future.call)( - synthesized-fields: synthesized-fields, - global-data: new-global-data, - element-data: element-data-for-futures, - args: args, - all-element-data: all-elem-data-for-futures, - __future-version: element-version - ) - - if "construct" in res { - return res.construct - } - - if "global-data" in res { - new-global-data = res.global-data - } - - if "element-data" in res { - element-data-for-futures = res.element-data - } - - if "synthesized-fields" in res { - synthesized-fields = res.synthesized-fields - } - } - } - - let select-labels = () - if has-selects { - let i = 0 - for filter in selects.filters { - let data = selects.data.at(i) - if ( - filter != none - and (data.index == none or data.index >= filter-first-active-index) - and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) - and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) - ) { - select-labels.push(selects.labels.at(i)) - } - i += 1 - } - } - - let tag = tag - tag.fields = synthesized-fields - - // Store contextual information in synthesize - synthesized-fields.insert(stored-data-key, tag) - - if has-filters { - let i = 0 - let rules = () - for filter in filters.all { - let data = filters.data.at(i) - if ( - filter != none - and (data.index == none or data.index >= filter-first-active-index) - and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) - and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) - ) { - let rule = filters.rules.at(i) - if rule.kind == apply { - rules += rule.rules - } else { - rules.push(rule) - } - } - i += 1 - } - - if rules != () { - // Only update style chain if at least one filter matches - new-global-data = editable-global-data - - new-global-data += apply-rules( - rules, - elements: new-global-data.elements, - settings: new-global-data.at("settings", default: default-global-data.settings), - global: new-global-data.at("global", default: default-global-data.global) - ) - } - } - - if has-ancestry-tracking { - if new-global-data == none { - new-global-data = editable-global-data - } - - if "global" not in new-global-data { - new-global-data.global = default-global-data - } - if "ancestry-chain" in new-global-data.global { - new-global-data.global.ancestry-chain.push((eid: eid, fields: synthesized-fields)) - } else { - new-global-data.global.ancestry-chain = ((eid: eid, fields: synthesized-fields),) - } - } - - // Save updated styles from applied rules - show lbl-get: set bibliography(title: [#metadata(new-global-data)#lbl-data-metadata]) if new-global-data != none and not is-stateful - - if new-global-data != none and is-stateful { - // Popping after the if below - style-state.update(chain => { - chain.push(new-global-data) - chain - }) - } - - // Filter show rules - let show-rules = if has-show-rules { - let i = 0 - let final-rules = () - for filter in show-rules.filters { - let data = show-rules.data.at(i) - if ( - filter != none - and (data.index == none or data.index >= filter-first-active-index) - and data.names.all(n => n not in filter-revokes or data.index == none or data.index >= filter-revokes.at(n)) - and verify-filter(synthesized-fields, eid: eid, filter: filter, ancestry: if "may-need-ancestry" in filter and filter.may-need-ancestry { ancestry } else { () }) - ) { - final-rules.push(show-rules.callbacks.at(i)) - } - i += 1 - } - final-rules - } else { - () - } - - if count-needs-fields or contextual { - if count-needs-fields { - count(synthesized-fields) - } - - // Wrap in additional context so the counter step is detected - context { - let body = display(synthesized-fields) - let tag = tag - tag.body = body - - if custom-ref != none { - // Update with body - let synthesized-fields = synthesized-fields - synthesized-fields.at(stored-data-key) = tag - - tag.custom-ref = custom-ref(synthesized-fields) - } - - let tag-metadata = metadata(tag) - - if reference != none and ref-label != none or outline != none { - // Update with custom-ref - let synthesized-fields = synthesized-fields - synthesized-fields.at(stored-data-key) = tag - - ref-figure(tag, synthesized-fields, ref-label) - } - - if not contextual and store-ancestry { - tag.ctx = (ancestry: ancestry) - } - - let body = [#[#body#metadata(tag)#lbl-tag]#lbl-show] - - if select-labels != () { - body = select-labels.fold(body, (acc, lbl) => [#[#acc#metadata(tag)#lbl-tag]#lbl]) - } - - let shown-body = if show-rules == () { - body - } else { - apply-show-rules(body, show-rules.len() - 1, show-rules) - } - - // Include metadata for querying - let meta-body = [#shown-body#metadata(((element-meta-key): true, kind: "element-meta", eid: eid, rendered: body, (stored-data-key): tag))#lbl-meta#metadata(tag)#lbl-tag] - - if labeling { - [#[#meta-body#metadata(tag)#lbl-tag]#label] - } else { - meta-body - } - } - } else { - let body = display(synthesized-fields) - let tag = tag - tag.body = body - - if custom-ref != none { - // Update with body - synthesized-fields.at(stored-data-key) = tag - - tag.custom-ref = custom-ref(synthesized-fields) - } - - let tag-metadata = metadata(tag) - - if reference != none and ref-label != none or outline != none { - // Update with custom-ref - synthesized-fields.at(stored-data-key) = tag - - ref-figure(tag, synthesized-fields, ref-label) - } - - if not contextual and store-ancestry { - tag.ctx = (ancestry: ancestry) - } - - let body = [#[#body#metadata(tag)#lbl-tag]#lbl-show] - - if select-labels != () { - body = select-labels.fold(body, (acc, lbl) => [#[#acc#metadata(tag)#lbl-tag]#lbl]) - } - - let shown-body = if show-rules == () { - body - } else { - apply-show-rules(body, show-rules.len() - 1, show-rules) - } - - // Include metadata for querying - let meta-body = [#shown-body#metadata(((element-meta-key): true, kind: "element-meta", eid: eid, rendered: body, (stored-data-key): tag))#lbl-meta#metadata(tag)#lbl-tag] - - if labeling { - [#[#meta-body#metadata(tag)#lbl-tag]#label] - } else { - meta-body - } - } - - if new-global-data != none and is-stateful { - // Pushed before the if above - style-state.update(chain => { - _ = chain.pop() - chain - }) - } - } - } - - if data-changed and not updates-stylechain-inside { - if is-stateful { - [#style-state.update(chain => { - chain.push(global-data) - chain - })#shown#style-state.update(chain => { - _ = chain.pop() - chain - })] - } else { - show lbl-get: set bibliography(title: [#metadata(global-data)#lbl-data-metadata]) - shown - } - } else { - shown - } - }#lbl-get] - }#lbl-outer] - - let tag = [#metadata(( - data-kind: "element-instance", - body: inner, - scope: scope, - fields: args, - func: __elembic_func, - default-constructor: default-constructor, - name: name, - eid: eid, - ctx: none, - counter: element-counter, - reference: reference, - custom-ref: none, - fields-known: false, - valid: true - ))#lbl-tag] - - if template != none { - inner = template[#inner#tag] - } - - [#inner#tag] - } - - let final-constructor = if construct != none { - { - let test-construct = construct(default-constructor) - assert(type(test-construct) == function, message: "elembic: element.declare: the 'construct' function must receive original constructor and return the new constructor, a new function, not '" + str(type(test-construct)) + "'.") - } - - let final-constructor(..args, __elembic_data: none, __elembic_mode: auto, __elembic_settings: (:)) = { - if __elembic_data != none { - return if __elembic_data == special-data-values.get-data { - (data-kind: "element", ..elem-data, func: final-constructor, default-constructor: default-constructor.with(__elembic_func: final-constructor), where: where(final-constructor)) - } else if __elembic_data == special-data-values.get-where { - where(final-constructor)(..args) - } else { - assert(false, message: "elembic: element: invalid data key to constructor: " + repr(__elembic_data)) - } - } - - construct(default-constructor.with(__elembic_func: final-constructor, __elembic_mode: __elembic_mode, __elembic_settings: __elembic_settings))(..args) - } - - final-constructor - } else { - default-constructor - } - - final-constructor -} diff --git a/src/libs/elembic/fields.typ b/src/libs/elembic/fields.typ deleted file mode 100644 index e9fe51b..0000000 --- a/src/libs/elembic/fields.typ +++ /dev/null @@ -1,364 +0,0 @@ -#import "data.typ": type-key, custom-type-key, current-field-version, eq -#import "types/types.typ" - -#let field-key = "__elembic_field" -#let fields-key = "__elembic_fields" - -#let _missing() = {} - -// Specifies an element field's properties. -#let field( - name, - type_, - doc: none, - required: false, - named: auto, - synthesized: false, - default: _missing, - folds: true, - internal: false, - meta: (:), -) = { - assert(type(name) == str, message: "elembic: field: Field name must be a string, not " + str(type(name))) - - let error-prefix = "elembic: field '" + name + "': " - assert(doc == none or type(doc) == str, message: error-prefix + "'doc' must be none or a string (add documentation)") - assert(type(synthesized) == bool, message: error-prefix + "'synthesized' must be a boolean (true: field is automatically synthesized and cannot be specified or overridden by the user; false: field can be manually specified and overridden by the user)") - assert(type(required) == bool, message: error-prefix + "'required' must be a boolean") - assert(type(folds) == bool, message: error-prefix + "'folds' must be a boolean") - assert(type(internal) == bool, message: error-prefix + "'internal' must be a boolean") - assert(type(meta) == dictionary, message: error-prefix + "'meta' must be a dictionary") - assert(named == auto or type(named) == bool, message: error-prefix + "'named' must be a boolean or auto") - let typeinfo = { - let (res, value) = types.validate(type_) - assert(res, message: if not res { error-prefix + value } else { "" }) - value - } - - if not required and default == _missing { - let (res, value) = types.default(typeinfo) - assert(res, message: if not res { error-prefix + value } else { "" }) - - default = value - } - - default = if required or synthesized { - // This value should be ignored in that case - auto - } else { - let (success, value) = types.cast(default, typeinfo) - if not success { - assert(false, message: error-prefix + value + "\n hint: given default for field had an incompatible type") - } - - value - } - - let fold = if folds and not synthesized and "fold" in typeinfo and typeinfo.fold != none { - assert(typeinfo.fold == auto or type(typeinfo.fold) == function, message: error-prefix + "type '" + typeinfo.name + "' doesn't appear to have a valid fold field (must be auto or function)") - let fold-default = if required { - // No field default, use the type's own default to begin folding - let (res, value) = types.default(typeinfo) - assert(res, message: if not res { error-prefix + value } else { "" }) - - value - } else { - // Use the field default as starting point for folding - default - } - - ( - folder: typeinfo.fold, - default: fold-default, - ) - } else { - none - } - - if named == auto { - // Pos arg is generally required - named = not required - } - - if synthesized and (required or not named) { - assert(false, message: error-prefix + "synthesized field cannot be required or positional, since it cannot be specified by the user") - } - - ( - (field-key): true, - version: current-field-version, - name: name, - doc: doc, - typeinfo: typeinfo, - default: default, - required: required, - synthesized: synthesized, - named: named, - fold: fold, - folds: folds, - internal: internal, - meta: meta, - ) -} - -#let parse-fields(fields, allow-unknown-fields: false) = { - assert(type(allow-unknown-fields) == bool, message: "elembic: element.fields: 'allow-unknown-fields' must be a boolean, not " + str(type(allow-unknown-fields))) - - let required-pos-fields = () - let optional-pos-fields = () - let required-named-fields = () - let optional-named-fields = () - let all-fields = (:) - let user-named-fields = (:) - let foldable-fields = (:) - let user-fields = (:) - let synthesized-fields = (:) - - for field in fields { - assert(type(field) == dictionary and field.at(field-key, default: none) == true, message: "elembic: element.fields: Invalid field received, please use the 'e.fields.field' constructor.") - assert(field.named or not field.required or optional-pos-fields == (), message: "elembic: element.fields: field '" + field.name + "' cannot be positional and required and appear after other positional but optional fields. Ensure there are only optional fields after the first positional optional field.") - assert(field.name not in all-fields, message: "elembic: element.fields: duplicate field name '" + field.name + "'") - - if field.required { - if field.named { - required-named-fields.push(field) - } else { - required-pos-fields.push(field) - } - } else if field.named { - optional-named-fields.push(field) - } else { - optional-pos-fields.push(field) - } - - if field.fold != none { - foldable-fields.insert(field.name, field.fold) - } - - if field.synthesized { - synthesized-fields.insert(field.name, field) - } else { - user-fields.insert(field.name, field) - - if field.named { - user-named-fields.insert(field.name, field) - } - } - - all-fields.insert(field.name, field) - } - - ( - (fields-key): true, - version: current-field-version, - required-pos-fields: required-pos-fields, - optional-pos-fields: optional-pos-fields, - required-named-fields: required-named-fields, - optional-named-fields: optional-named-fields, - foldable-fields: foldable-fields, - user-named-fields: user-named-fields, - user-fields: user-fields, - all-fields: all-fields, - allow-unknown-fields: allow-unknown-fields, - ) -} - -// Generates an argument parser function with the given general error -// prefix (for listing missing fields) and per-field error prefix function -// (for an invalid field; receives the field name). -// -// You can customize 'field-term' to customize what the word "field" is -// in error messages. It should be either a string or a two-element -// array with (singular, plural). Setting 'typecheck: false' also fully -// disables typechecking. -// -// Parse arguments into a dictionary of fields and their casted values. -// By default, include required arguments and error if they are missing. -// Setting 'include-required' to false will error if they are present -// instead. -#let generate-arg-parser( - fields: none, - general-error-prefix: "", - field-error-prefix: _ => "", - field-term: "field", - typecheck: true, -) = { - assert(type(fields) == dictionary and fields-key in fields, message: "elembic: generate-arg-parser: please use 'parse-fields' to generate the fields input.") - assert(type(general-error-prefix) == str, message: "elembic: generate-arg-parser: 'general-error-prefix' must be a string") - assert(type(field-error-prefix) == function, message: "elembic: generate-arg-parser: 'field-error-prefix' must be a function receiving field name and returning string") - assert(type(typecheck) == bool, message: "elembic: generate-arg-parser: 'typecheck' must be a boolean, not " + str(type(typecheck))) - - let (field-singular, field-plural) = if type(field-term) == str { - (field-term, field-term + "s") - } else if type(field-term) == array and field-term.len() == 2 and field-term.all(term => type(term) == str) { - field-term - } else { - assert(false, message: "elembic: generate-arg-parser: 'field-term' must either be a string (plural with 's') or a two-element array of strings (singular, plural).") - } - - let (required-pos-fields, optional-pos-fields, required-named-fields, optional-named-fields, all-fields, user-fields, user-named-fields, allow-unknown-fields) = fields - let required-pos-fields-amount = required-pos-fields.len() - let optional-pos-fields-amount = optional-pos-fields.len() - let total-pos-fields-amount = required-pos-fields-amount + optional-pos-fields-amount - let all-pos-fields = required-pos-fields + optional-pos-fields - - let has-required-fields = required-pos-fields-amount + required-named-fields.len() != 0 - - // If we allow unknown named fields, we still need to check whether a - // positional or synthesized field was accidentally specified as a named field. - let is-unknown-named-field = if allow-unknown-fields { - f => f in all-fields and f not in user-named-fields - } else { - f => f not in user-named-fields - } - - // Disable typechecking anyway if all fields are 'any' - // - // Have a separate typecheck option so type information can be kept in fields - // even if typechecking is undesirable - // Note: we don't parse args for synthesized fields, so we can exclude them when - // checking whether we will typecheck when parsing args - let typecheck = typecheck and user-fields.values().any(f => f.typeinfo.type-kind != "any") - - // Parse args (no typechecking) - let parse-args-no-typechecking(args, include-required: true) = { - let pos = args.pos() - - if include-required and pos.len() < required-pos-fields-amount { - // Plural - let term = if required-pos-fields-amount - pos.len() == 1 { field-singular } else { field-plural } - - return (false, general-error-prefix + "missing positional " + term + " " + fields.required-pos-fields.slice(pos.len()).map(f => "'" + f.name + "'").join(", ")) - } - - if pos.len() > if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } { - let expected-arg-amount = if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } - let excluding-required-hint = if include-required { "" } else { "\n hint: only optional fields are accepted here" } - return (false, general-error-prefix + "too many positional arguments, expected " + str(expected-arg-amount) + excluding-required-hint) - } - - let named-args = args.named() - if include-required { - if required-named-fields.any(f => f.name not in named-args) { - let missing-fields = required-named-fields.filter(f => f.name not in named-args) - let term = if missing-fields.len() == 1 { field-singular } else { field-plural } - - return (false, general-error-prefix + "missing required named " + term + " " + missing-fields.map(f => "'" + f.name + "'").join(", ")) - } - } else if required-named-fields.any(f => f.name in named-args) { - let field = required-named-fields.find(f => f.name in named-args) - return (false, field-error-prefix(field.name) + "this " + field-singular + " cannot be specified here\n hint: only optional " + field-plural + " are accepted here") - } - - // Here we simultaneously check for unknown fields and for positional fields - // being wrongly specified as named. If there are no positional fields and - // unknown fields are allowed, there is no point in doing this check. - if (not allow-unknown-fields or total-pos-fields-amount > 0) and named-args.keys().any(is-unknown-named-field) { - let field-name = named-args.keys().find(is-unknown-named-field) - let field = all-fields.at(field-name, default: none) - let expected-pos-hint = if field == none or field.named { "" } else { "\n hint: this " + field-singular + " must be specified positionally" } - let is-synthesized-hint = if field != none and field.synthesized { "\n hint: this " + field-singular + " is synthesized and cannot be specified manually" } else { "" } - - return (false, general-error-prefix + "unknown named " + field-singular + " '" + field-name + "'" + expected-pos-hint + is-synthesized-hint) - } - - let pos-fields = if include-required { all-pos-fields } else { optional-pos-fields } - let i = 0 - for value in pos { - let pos-field = pos-fields.at(i) - named-args.insert(pos-field.name, value) - - i += 1 - } - - (true, named-args) - } - - // Parse args (with typechecking) - let parse-args(args, include-required: true) = { - let result = (:) - - let pos = args.pos() - if include-required and pos.len() < required-pos-fields-amount { - // Plural - let term = if required-pos-fields-amount - pos.len() == 1 { field-singular } else { field-plural } - - return (false, general-error-prefix + "missing positional " + term + " " + fields.required-pos-fields.slice(pos.len()).map(f => "'" + f.name + "'").join(", ")) - } - - let expected-arg-amount = if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } - - if pos.len() > expected-arg-amount { - let excluding-required-hint = if include-required { "" } else { "\n hint: only optional fields are accepted here" } - return (false, general-error-prefix + "too many positional arguments, expected " + str(expected-arg-amount) + excluding-required-hint) - } - - let named-args = args.named() - - if include-required and required-named-fields.any(f => f.name not in named-args) { - let missing-fields = required-named-fields.filter(f => f.name not in named-args) - let term = if missing-fields.len() == 1 { field-singular } else { field-plural } - - return (false, general-error-prefix + "missing required named " + term + " " + missing-fields.map(f => "'" + f.name + "'").join(", ")) - } - - for (field-name, value) in named-args { - if allow-unknown-fields and field-name not in all-fields { - continue - } - - let field = all-fields.at(field-name, default: none) - - if field == none or field.synthesized or not field.named { - let expected-pos-hint = if field == none or field.named { "" } else { "\n hint: this " + field-singular + " must be specified positionally" } - let is-synthesized-hint = if field != none and field.synthesized { "\n hint: this " + field-singular + " is synthesized and cannot be specified manually" } else { "" } - - return (false, general-error-prefix + "unknown named " + field-singular + " '" + field-name + "'" + expected-pos-hint + is-synthesized-hint) - } - - if not include-required and field.required { - return (false, field-error-prefix(field-name) + "this " + field-singular + " cannot be specified here\n hint: only optional " + field-plural + " are accepted here") - } - - let typeinfo = field.typeinfo - let kind = typeinfo.type-kind - - if kind != "any" { - let (res, casted) = types.cast(value, typeinfo) - if not res { - return (false, field-error-prefix(field-name) + casted) - } - named-args.insert(field-name, casted) - } - } - - let pos-fields = if include-required { all-pos-fields } else { optional-pos-fields } - let i = 0 - for value in pos { - let pos-field = pos-fields.at(i) - let typeinfo = pos-field.typeinfo - let kind = typeinfo.type-kind - let casted = value - - if kind != "any" { - let res - (res, casted) = types.cast(value, typeinfo) - if not res { - return (false, field-error-prefix(pos-field.name) + casted) - } - } - - named-args.insert(pos-field.name, casted) - - i += 1 - } - - (true, named-args) - } - - if typecheck { - parse-args - } else { - parse-args-no-typechecking - } -} diff --git a/src/libs/elembic/lib.typ b/src/libs/elembic/lib.typ deleted file mode 100644 index f902d66..0000000 --- a/src/libs/elembic/lib.typ +++ /dev/null @@ -1,10 +0,0 @@ -#import "element.typ": set_, data, apply, revoke, reset, named, filtered, cond-set, show_, style-modes, prepare-get as get, prepare-debug as debug-get, settings, elem-selector as selector, select, elem-query as query, ref_ as ref, prepare, within-filter as within -#import "fields.typ": field -#import "pub/data.typ": * -#import "pub/constants.typ" -#import "pub/element.typ" -#import "pub/filters.typ" -#import "pub/parsing.typ" -#import "pub/types.typ" -#import "pub/leaky.typ" -#import "pub/stateful.typ" diff --git a/src/libs/elembic/pub/constants.typ b/src/libs/elembic/pub/constants.typ deleted file mode 100644 index 27797dd..0000000 --- a/src/libs/elembic/pub/constants.typ +++ /dev/null @@ -1 +0,0 @@ -#import "../data.typ": element-version, type-version, custom-type-version, current-field-version, style-modes diff --git a/src/libs/elembic/pub/data.typ b/src/libs/elembic/pub/data.typ deleted file mode 100644 index f1b5c74..0000000 --- a/src/libs/elembic/pub/data.typ +++ /dev/null @@ -1 +0,0 @@ -#import "../data.typ": fields, counter_ as counter, ctx, scope, func, func-name, eid, tid, repr_ as repr, eq diff --git a/src/libs/elembic/pub/element.typ b/src/libs/elembic/pub/element.typ deleted file mode 100644 index 3b824dd..0000000 --- a/src/libs/elembic/pub/element.typ +++ /dev/null @@ -1,2 +0,0 @@ -// Public re-exports for element-related functions. -#import "../element.typ": declare diff --git a/src/libs/elembic/pub/filters.typ b/src/libs/elembic/pub/filters.typ deleted file mode 100644 index 88e0611..0000000 --- a/src/libs/elembic/pub/filters.typ +++ /dev/null @@ -1 +0,0 @@ -#import "../element.typ": or-filter as or_, and-filter as and_, not-filter as not_, xor-filter as xor, custom-filter as custom diff --git a/src/libs/elembic/pub/leaky.typ b/src/libs/elembic/pub/leaky.typ deleted file mode 100644 index 40c1106..0000000 --- a/src/libs/elembic/pub/leaky.typ +++ /dev/null @@ -1,8 +0,0 @@ -// Exports rules defaulting to leaky mode. -#import "../element.typ": leaky-set as set_, leaky-apply as apply, leaky-show as show_, leaky-revoke as revoke, leaky-reset as reset, leaky-cond-set as cond-set, leaky-settings as settings, leaky-toggle as toggle - -/// Enable leaky mode by default. -#let enable = toggle.with(true) - -/// Disable leaky mode by default. -#let disable = toggle.with(false) diff --git a/src/libs/elembic/pub/native.typ b/src/libs/elembic/pub/native.typ deleted file mode 100644 index d823aeb..0000000 --- a/src/libs/elembic/pub/native.typ +++ /dev/null @@ -1,2 +0,0 @@ -// Public re-exports for native type-related functions and constants. -#import "../types/native.typ": content_, auto_, none_, float_, function_, int_, array_, dict_, datetime_, duration_, color_, gradient_, str_, type_, bool_, relative_, ratio_, typeinfo, angle_, arguments_, bytes_, tiling, tiling_, version_, fraction_, length_, stroke_ diff --git a/src/libs/elembic/pub/parsing.typ b/src/libs/elembic/pub/parsing.typ deleted file mode 100644 index e4ff0b7..0000000 --- a/src/libs/elembic/pub/parsing.typ +++ /dev/null @@ -1 +0,0 @@ -#import "../fields.typ": parse-fields, generate-arg-parser diff --git a/src/libs/elembic/pub/stateful.typ b/src/libs/elembic/pub/stateful.typ deleted file mode 100644 index 9de9885..0000000 --- a/src/libs/elembic/pub/stateful.typ +++ /dev/null @@ -1,8 +0,0 @@ -// Exports rules defaulting to stateful mode. -#import "../element.typ": toggle-stateful-mode as toggle, stateful-set as set_, stateful-apply as apply, stateful-show as show_, stateful-revoke as revoke, stateful-reset as reset, stateful-cond-set as cond-set, stateful-get as get, stateful-settings as settings - -// Enable stateful mode. -#let enable = toggle.with(true) - -// Disable stateful mode. -#let disable = toggle.with(false) diff --git a/src/libs/elembic/pub/types.typ b/src/libs/elembic/pub/types.typ deleted file mode 100644 index 0c9d323..0000000 --- a/src/libs/elembic/pub/types.typ +++ /dev/null @@ -1,5 +0,0 @@ -// Public re-exports for type-related functions and constants. -#import "../types/base.typ": ok, err, is-ok, any, never, custom-type, typeid, typename, native-elem -#import "../types/types.typ": option, smart, union, paint, literal, exact, wrap, array_ as array, dict_ as dict, default, validate as typeinfo, typeof, cast, generate-cast-error -#import "../types/custom.typ": declare -#import "native.typ" diff --git a/src/libs/elembic/types/base.typ b/src/libs/elembic/types/base.typ deleted file mode 100644 index 768c9f8..0000000 --- a/src/libs/elembic/types/base.typ +++ /dev/null @@ -1,582 +0,0 @@ -// The shared fundamentals of the type system. -#import "../data.typ": data, type-key, custom-type-key, custom-type-data-key, repr_, func-name, type-version, eq - -// Typeinfo structure: -// - type-key: kind of type -// - version: 1 -// - name: type name -// - input: list of native types / custom types of input -// - output: list of native types / custom types of output -// - data: data specific for this type key -// - check: none (only check inputs) or function x => bool -// - cast: none (input is unchanged) or function to convert input to output -// - error: none or function x => string to customize check failure message -// - default: empty array (no default) or singleton array => default value for this type -// - fold: none, auto (equivalent to (a, b) => a + b but more efficient) or function (prev, next) => folded value: -// determines how to combine two consecutive values of this type in the stylechain -#let base-typeinfo = ( - (type-key): true, - type-kind: "base", - version: type-version, - name: "unknown", - input: (), - output: (), - data: none, - check: none, - cast: none, - error: none, - default: (), - fold: none, -) - -// Top type -// input and output have "any". -#let any = ( - ..base-typeinfo, - type-kind: "any", - name: "any", - input: ("any",), - output: ("any",), -) - -// Bottom type -// input and output are empty. -#let never = ( - ..base-typeinfo, - type-kind: "never", - name: "never", - input: (), - output: (), -) - -// Any custom type -#let custom-type = ( - ..base-typeinfo, - (type-key): "custom type", - name: "custom type", - input: ("custom type",), - output: ("custom type",), -) - -#let _sequence = [].func() - -#let element(name, eid) = ( - ..base-typeinfo, - type-kind: "element", - name: "element '" + name + "'", - input: (content,), - output: (content,), - check: c => c.func() == _sequence and data(c).eid == eid, - data: (name: name, eid: eid), - error: c => "expected element " + name + ", found " + func-name(c), -) - -#let native-elem(func) = { - assert(type(func) == function, message: "elembic: types.native-elem: expected native element constructor, got " + str(type(func))) - - ( - ..base-typeinfo, - type-kind: "native-element", - name: "native element '" + repr(func) + "'", - input: (content,), - output: (content,), - check: if func == _sequence { c => c.func() == _sequence and data(c).eid == none } else { c => c.func() == func }, - data: (func: func), - error: c => "expected native element " + repr(func) + ", found " + func-name(c), - ) -} - -// Get the type ID of a value. -// This is usually 'type(value)', unless value has a custom type. -// In that case, it has the format '(tid: ..., name: ...)'. -// This is the format expected by 'input' and 'output' arrays. -#let typeid(value) = { - let value-type = type(value) - if value-type == dictionary and custom-type-key in value { - value-type = value.at(custom-type-key).id - } - value-type -} - -// Returns the name of the value's type as a string. -#let typename(value) = { - let value-type = type(value) - if value-type == dictionary and custom-type-key in value { - let id = value.at(custom-type-key).id - if "name" in id { - id.name - } else { - str(id) - } - } else { - str(value-type) - } -} - -// Make a unique element or type ID based on prefix and name. -// -// Uses a separator and a "bit stuffing" technique to ensure -// the separator sequence doesn't appear in either of the -// prefix or the name in the final ID. -#let id-separator = "_---_" -#let trimmed-separator = id-separator.trim("_", at: end) -#let unique-id(kind, prefix, name) = { - ( - kind + "_" - ) + prefix.replace( - trimmed-separator, trimmed-separator + "-" - ) + id-separator + name.replace( - trimmed-separator, trimmed-separator + "-" - ) -} - -// Literal type -// Only accepted if value is equal to the literal. -// Input and output are equal to the value. -// -// Uses base typeinfo information for information such as casts and whatnot. -#let literal(value, typeinfo) = { - let represented = "'" + if type(value) == str { value } else { repr_(value) } + "'" - let value-type = typeid(value) - - let check = if typeinfo.check == none { x => eq(x, value) } else { x => eq(x, value) and (typeinfo.check)(x) } - - ( - ..typeinfo, - type-kind: "literal", - name: "literal " + represented, - data: (value: value, typeinfo: typeinfo, represented: represented), - check: check, - error: _ => "given value wasn't equal to literal " + represented, - default: (value,), - ) -} - -// Union type (one of many) -// Data is the list of typeinfos. -// Accepted if the value corresponds to one of the given types. -// Does not check the validity of typeinfos. -#let union(typeinfos) = { - // Flatten nested unions - let typeinfos = typeinfos.map(t => if t.type-kind == "union" { t.data } else { (t,) }).sum(default: ()).dedup() - if typeinfos == () { - // No inputs accepted... - return never - } - if typeinfos.len() == 1 { - // Simplify union if there's nothing else - return typeinfos.first() - } - if typeinfos.any(x => x.type-kind == "any") { - // Union with 'any' is just any - return any - } - - let name = typeinfos.map(t => t.name).join(", ", last: " or ") - let input = typeinfos.map(t => t.input).sum(default: ()).dedup() - let output = typeinfos.map(t => t.output).sum(default: ()).dedup() - - let has-any-input = "any" in input - let has-any-output = "any" in output - - if has-any-input { - input = ("any",) - } - - if has-any-output { - output = ("any",) - } - - // Try to optimize checks as much as possible - let check = if typeinfos.all(t => t.check == none) { - // If there are no checks, just checking inputs is enough - none - } else { - let checked-types = typeinfos.filter(t => t.check != none) - let unchecked-inputs = typeinfos.filter(t => t.check == none).map(t => t.input).sum(default: ()).dedup() - if input.all(t => t in unchecked-inputs) { - // Unchecked types include all possible input types, so some check will always succeed - // Note that this check also works for input reduced to just "any". If "any" is an - // unchecked input, then checks will never fail. - none - } else if checked-types.all(t => t.type-kind == "native-element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { - // From here onwards, we can assume unchecked-inputs doesn't contain "any", - // since it is a subset of input, therefore input would be just ("any",) and - // the check above would have had to pass in that case. - let all-funcs = checked-types.map(t => t.data.func) - let non-seq-funcs = all-funcs.filter(f => f != _sequence) - let has-seq = _sequence in all-funcs - - // Check sequence separately, as a sequence can also be a custom element, - // so we must tell them apart. - if has-seq { - if non-seq-funcs == () { - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - typ in unchecked-inputs or typ == content and x.func() == _sequence and data(x).eid == none - } - } else { - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - typ in unchecked-inputs or typ == content and (x.func() in non-seq-funcs or x.func() == _sequence and data(x).eid == none) - } - } - } else { - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - typ in unchecked-inputs or typ == content and x.func() in non-seq-funcs - } - } - } else if checked-types.all(t => t.type-kind == "element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { - let all-eids = checked-types.map(t => t.data.eid) - - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - typ in unchecked-inputs or typ == content and x.func() == _sequence and data(x).eid in all-eids - } - } else if checked-types.all(t => t.type-kind == "literal" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { - let values-inputs-and-checks = checked-types.map(t => (t.data.value, t.input, t.data.typeinfo.check)) - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - typ in unchecked-inputs or values-inputs-and-checks.any(((v, i, check)) => eq(x, v) and (typ in i or "any" in i) and (check == none or check(x))) - } - } else { - // If any check succeeds and the value has the correct input type, OK - let checks-and-inputs = checked-types.map(t => (t.input, t.check)) - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - // If one of the types without checks accepts this type as an input then we don't need - // to run any checks! - typ in unchecked-inputs or checks-and-inputs.any(((inp, check)) => (typ in inp or "any" in inp) and check(x)) - } - } - } - - // Try to optimize casts - let cast = if typeinfos.all(t => t.cast == none) { - none - } else { - let casting-types = typeinfos.filter(t => t.cast != none) - let first-casting-type = casting-types.first() - if ( - // If the casting types are all native, and none of the types before them - // accept their "cast-from" types, then we can fast track to a simple check: - // if within the 'cast-from' types, then cast, otherwise don't. - casting-types != () - and casting-types.all(t => t.type-kind == "native" and t.data in (float, content) and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) - and typeinfos.find(t => t.input.any(i => i == "any" or i in first-casting-type.input)) == first-casting-type - and (casting-types.len() == 1 or typeinfos.find(t => t.input.any(i => i == "any" or i in casting-types.at(1).input)) == casting-types.at(1)) - ) { - if casting-types.len() >= 2 { // just float and content - x => if type(x) == int { float(x) } else if x == none or type(x) in (str, symbol) [#x] else { x } - } else if first-casting-type.data == float { // just float - x => if type(x) == int { float(x) } else { x } - } else { // just content - x => if x == none or type(x) in (str, symbol) { [#x] } else { x } - } - } else { - // Generic case - x => { - let typ = type(x) - if typ == dictionary and custom-type-key in x { - // Custom type must be checked differently in inputs - typ = x.at(custom-type-key).id - } - let typeinfo = typeinfos.find(t => (typ in t.input or "any" in t.input) and (t.check == none or (t.check)(x))) - if typeinfo.cast == none { - x - } else { - (typeinfo.cast)(x) - } - } - } - } - - let error = if typeinfos.all(t => t.error == none) { - none - } else if typeinfos.all(t => t.type-kind == "literal" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { - let literals = typeinfos.map(t => str(t.data.represented)).join(", ", last: " or ") - let message = "given value wasn't equal to literals " + literals - x => message - } else if typeinfos.all(t => t.type-kind == "native-element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { - let funcs = typeinfos.map(t => repr(t.data.func)).join(", ", last: " or ") - let head = "expected native elements " + funcs + ", found " - x => head + { - if type(x) == content { func-name(x) } else { "a(n) " + typename(x) } - } - } else if typeinfos.all(t => (t.type-kind == "element" or t.type-kind == "native-element") and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) { - let funcs = typeinfos.map(t => if t.type-kind == "element" { t.data.name } else { repr(t.data.func) + " (native)" }).join(", ", last: " or ") - let head = "expected elements " + funcs + ", found " - x => head + { - if type(x) == content { func-name(x) } else { "a(n) " + typename(x) } - } - } else { - let error-types = typeinfos.filter(t => t.error != none) - x => { - "all typechecks for union failed" + error-types.map(t => "\n hint (" + t.name + "): " + (t.error)(x)).sum(default: "") - } - } - - let is-option = typeinfos.first().type-kind == "native" and typeinfos.first().data == type(none) - let is-smart = typeinfos.first().type-kind == "native" and typeinfos.first().data == type(auto) - - let default = if is-option or is-smart { - // Default of 'none' for option(...) - // Default of 'auto' for smart(...) - typeinfos.first().default - } else { - () - } - - // Match built-in behavior by only folding option(T) or smart(T) if T can fold and the inner isn't explicitly none/auto - let fold = if typeinfos.len() == 2 and typeinfos.at(1).fold != none { - let other-typeinfo = typeinfos.at(1) - let other-fold = other-typeinfo.fold - if is-option { - if other-fold == auto { - (outer, inner) => if inner != none and outer != none { outer + inner } else { inner } - } else { - (outer, inner) => if inner != none and outer != none { other-fold(outer, inner) } else { inner } - } - } else if is-smart { - if other-fold == auto { - (outer, inner) => if inner != auto and outer != auto { outer + inner } else { inner } - } else { - (outer, inner) => if inner != auto and outer != auto { other-fold(outer, inner) } else { inner } - } - } else { - none - } - } else { - // TODO: We could consider folding an arbitrary union iff the outputs are all disjoint, - // so we can easily distinguish the typeinfo for an output based on the type. - // Otherwise, can't do much if e.g. an int could be typeinfo A (say, positive integer) - // or typeinfo B (say, negative integer) because checks apply to inputs and not outputs - // (unless, of course, there is no casting). - none - } - - ( - ..base-typeinfo, - type-kind: "union", - name: name, - data: typeinfos, - input: input, - output: output, - check: check, - cast: cast, - error: error, - default: default, - fold: fold, - ) -} - -// A result to indicate success and return a value. -#let ok(value) = { - (true, value) -} - -// A result to indicate failure, with an error value indicating what happened. -#let err(error) = { - (false, error) -} - -// Whether this result was successful. -#let is-ok(result) = { - type(result) == array and result.len() == 2 and result.first() == true -} - -// Wrap a typeinfo with some other data. -// Mostly unchecked variant of 'types.wrap'. -#let wrap(typeinfo, overrides) = { - ( - (..typeinfo, type-kind: "wrapped", data: (base: typeinfo, extra: none)) - + for (key, default) in base-typeinfo { - if key == type-key or key == "type-kind" { - continue - } - - if key in overrides { - let override = overrides.at(key) - if type(override) == function { - override = override(typeinfo.at(key, default: default)) - } - - if key == "data" { - (data: (base: typeinfo, extra: override)) - } else { - ((key): override) - } - } - } - ) -} - -// A particular collection of types. -#let collection(name, base, parameters, check: none, cast: none, error: none, ..args) = { - if check == none { - check = base.check - } - - if cast == none { - cast = base.cast - } - - if check == none and error == none { - error = base.error - } - - let other-args = args.named() - let default = if "default" in other-args { - other-args.default - } else { - base.default - } - - ( - ..base, - type-kind: "collection", - name: name + if parameters != () { " of " + parameters.map(t => t.name).join(", ", last: " and ") }, - data: (base: base, parameters: parameters), - check: check, - cast: cast, - error: error, - default: default, - ) -} - -// Create an array collection with a uniform parameter typeinfo for its elements. -#let array_(base-type, param, error: none) = { - assert(array in base-type.input or "any" in base-type.input) - let kind = param.type-kind - - collection( - "array", - base-type, - (param,), - check: if param.check == none and "any" in param.input { - none - } else if param.input == () { - // Propagate 'never' - _ => false - } else if "any" in param.input { - // Only need to run checks - a => a.all(param.check) - } else { - // Some optimizations ahead - // The proper code is at the bottom - let input = param.input - let check = param.check - if kind == "native" and param.data == dictionary and ("__future_cast" not in param or param.__future_cast.max-version < type-version) { - a => a.all(x => type(x) == dictionary and custom-type-key not in x) - } else if param.input.all(i => type(i) == type) and dictionary not in param.input { - // No custom types accepted (the check above excludes '(tid: ..., name: ...)' as well as "any") - // If this is a custom type, it will return type(x) = dictionary, so it will fail - // (Also excludes "custom type": the type of custom types) - // So that suffices - if input.len() == 1 { - let input = input.first() - if check == none { - a => a.all(x => type(x) == input) - } else { - a => a.all(x => type(x) == input and check(x)) - } - } else if input.len() == 2 { - let first = input.first() - let second = input.at(1) - if check == none { - a => a.all(x => type(x) == first or type(x) == second) - } else { - a => a.all(x => (type(x) == first or type(x) == second) and check(x)) - } - } else if check == none { - a => a.all(x => type(x) in input) - } else { - a => a.all(x => type(x) in input and check(x)) - } - } else if param.check == none { - a => a.all(x => typeid(x) in param.input) - } else { - a => a.all(x => typeid(x) in param.input and check(x)) - } - }, - - cast: if param.cast == none { - none - } else if kind == "native" and param.data == content and ("__future_cast" not in param or param.__future_cast.max-version < type-version) { - a => a.map(x => [#x]) - } else { - a => a.map(param.cast) - }, - - error: error - ) -} - -// Create a dict with a uniform parameter typeinfo for its values. -// (Keys are always strings.) -#let dict_(base-type, param, error: none) = { - assert(dictionary in base-type.input or "any" in base-type.input) - let kind = param.type-kind - - collection( - "dict", - base-type, - (param,), - check: { - // Simply check the array of values - // (We can pass 'any' as the base type since that doesn't affect the 'check') - let array-check = array_(any, param).check - if array-check == none { - none - } else { - d => array-check(d.values()) - } - }, - - cast: if param.cast == none { - none - } else if kind == "native" and param.data == content and ("__future_cast" not in param or param.__future_cast.max-version < type-version) { - d => { - for (k, v) in d { - d.at(k) = [#v] - } - d - } - } else { - let cast = param.cast - d => { - for (k, v) in d { - d.at(k) = cast(v) - } - d - } - }, - - error: error - ) -} diff --git a/src/libs/elembic/types/custom.typ b/src/libs/elembic/types/custom.typ deleted file mode 100644 index 96058ec..0000000 --- a/src/libs/elembic/types/custom.typ +++ /dev/null @@ -1,409 +0,0 @@ -// Custom types! -#import "../data.typ": special-data-values, custom-type-key, custom-type-data-key, type-key, custom-type-version -#import "base.typ" -#import "types.typ" -#import "../fields.typ" as field-internals - -// Default folding procedure for custom types. -// Combines each inner type individually. -#let auto-fold(foldable-fields) = if foldable-fields == (:) { - // No fields to fold, so 'inner' always fully overwrites 'outer'. - // In that case, we can just sum inner with outer, adding its fields - // on top. - auto -} else { - (outer, inner) => { - let combined = outer + inner - - for (field-name, fold-data) in foldable-fields { - if field-name in inner { - let outer = outer.at(field-name, default: fold-data.default) - if fold-data.folder == auto { - combined.at(field-name) = outer + inner.at(field-name) - } else { - combined.at(field-name) = (fold-data.folder)(outer, inner.at(field-name)) - } - } - } - - combined - } -} - -#let auto-cast(from, fields: (:), constructor: none) = { - if from == dictionary { - value => constructor(..value) - } else { - assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.") - } -} - -#let auto-cast-check(from, fields: (:), parse-args: none) = { - if from == dictionary { - value => parse-args(arguments(..value)).first() - } else { - assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.") - } -} - -#let auto-cast-error(from, fields: (:), parse-args: none) = { - if from == dictionary { - value => parse-args(arguments(..value)).at(1) - } else { - assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.") - } -} - -#let declare( - name, - fields: none, - prefix: none, - default: none, - parse-args: auto, - typecheck: true, - allow-unknown-fields: false, - construct: none, - scope: none, - casts: none, - fold: none, -) = { - - let fields-hint = if type(fields) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single field: write 'fields: (field,)', not 'fields: (field)'" } else { "" } - let casts-hint = if type(casts) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single cast: write 'casts: ((from: ..., with: ...),)', not 'casts: ((from: ..., with: ...))'" } else { "" } - assert(type(fields) == array, message: "elembic: types.declare: please specify an array of fields, creating each field with the 'field' function." + fields-hint) - assert(prefix != none, message: "elembic: types.declare: please specify a 'prefix: ...' for your type, to distinguish it from types with the same name. If you are writing a package or template to be used by others, please do not use an empty prefix.") - assert(type(prefix) == str, message: "elembic: types.declare: the prefix must be a string, not '" + str(type(prefix)) + "'") - assert(parse-args == auto or type(parse-args) == function, message: "elembic: types.declare: 'parse-args' must be either 'auto' (use built-in parser) or a function (default arg parser, fields: dictionary, typecheck: bool) => (user arguments, include-required: true) => (bool (true on success, false on error), dictionary with parsed fields (or error message string if the bool is false)).") - assert(type(typecheck) == bool, message: "elembic: types.declare: the 'typecheck' argument must be a boolean (true to enable typechecking in the constructor, false to disable).") - assert(type(allow-unknown-fields) == bool, message: "elembic: types.declare: the 'allow-unknown-fields' argument must be a boolean.") - assert(construct == none or type(construct) == function, message: "elembic: types.declare: 'construct' must be 'none' (use default constructor) or a function receiving the original constructor and returning the new constructor.") - assert(default == none or type(default) == function, message: "elembic: types.declare: 'default' must be none or a function receiving the constructor and returning the default.") - assert(scope == none or type(scope) in (dictionary, module), message: "elembic: types.declare: 'scope' must be either 'none', a dictionary or a module") - assert( - casts == none - or type(casts) == array and casts.all( - d => ( - type(d) == dictionary - and "from" in d - and d.keys().all(k => k in ("from", "with", "check")) - and ("with" not in d or type(d.with) == function) - and ("check" not in d or d.check == none or type(d.check) == function) - ) - ), - message: "elembic: types.declare: 'casts' must be either 'none' or an array of dictionaries in the form (from: type, check (optional): none or casted value => bool, with (optional when 'from' is dictionary): constructor => casted value => your type)." + casts-hint - ) - assert(fold == none or fold == auto or type(fold) == function, message: "elembic: types.declare: 'fold' must be 'none' (no folding), 'auto' (fold each field individually) or a function 'default constructor => auto (same as (a, b) => a + b but more efficient) or function (outer, inner) => combined value'.") - - let tid = base.unique-id("t", prefix, name) - let fields = field-internals.parse-fields(fields, allow-unknown-fields: allow-unknown-fields) - let (all-fields, user-fields, foldable-fields) = fields - let auto-fold = if fold == auto { auto-fold(foldable-fields) } else { none } - - let default-arg-parser = field-internals.generate-arg-parser( - fields: fields, - general-error-prefix: "elembic: type '" + name + "': ", - field-error-prefix: field-name => "field '" + field-name + "' of type '" + name + "': ", - typecheck: typecheck - ) - - let parse-args = if parse-args == auto { - default-arg-parser - } else { - let parse-args = parse-args(default-arg-parser, fields: fields, typecheck: typecheck) - if type(parse-args) != function { - assert(false, message: "elembic: types.declare: 'parse-args', when specified as a function, receives the default arg parser alongside `fields: fields dictionary` and `typecheck: bool`, and must return a function (the new arg parser), and not " + base.typename(parse-args)) - } - - parse-args - } - - let default-fields = fields.user-fields.values().map(f => if f.required { (:) } else { ((f.name): f.default) }).sum(default: (:)) - - let typeid = (tid: tid, name: name) - - // We will specify default in a bit, once we declare the constructor - let typeinfo = ( - ..base.base-typeinfo, - type-kind: "custom", - name: name, - input: (typeid,), - output: (typeid,), - data: ( - id: typeid, - - // Original type before adding casts - // or none if this is already the type before casts - // (used for 'exact()') - pre-casts: none - ) - ) - - let type-data = ( - (custom-type-data-key): true, - (custom-type-key): ( - data-kind: "type-instance", - fields: ( - version: custom-type-version, - tid: tid, - id: typeid, - ), - func: declare, - default-constructor: declare, - tid: "b_custom type", - id: "custom type", - fields-known: true, - valid: true - ), - version: custom-type-version, - name: name, - tid: tid, - id: typeid, - // We will add this here once the constructor is declared - typeinfo: none, - scope: scope, - parse-args: parse-args, - default-fields: default-fields, - user-fields: user-fields, - all-fields: all-fields, - fields: fields, - typecheck: typecheck, - allow-unknown-fields: allow-unknown-fields, - default-constructor: none, - func: none, - ) - - let process-casts = if casts == none { - none - } else { - // Trick: We assign cast to each cast-from type and create a union, - // and use its generated check/cast functions as our own - default-constructor => { - let typeinfos = casts.map(cast => { - let (res, from) = types.validate(cast.from) - if not res { - assert(false, message: "elembic: types.declare: invalid cast-from type: " + from) - } - - let (cast-check, with, cast-error) = if "with" in cast { - (cast.at("check", default: none), (cast.with)(default-constructor), none) - } else if from.type-kind == "native" and from.data == dictionary { - assert(fields.required-pos-fields == (), message: "elembic: types.declare: cannot generate automatic cast from dict when there are required positional fields.") - - if "check" in cast { - ( - cast.check, - auto-cast(dictionary, fields: fields, constructor: default-constructor), - none, - ) - } else { - ( - auto-cast-check(dictionary, fields: fields, parse-args: parse-args), - auto-cast(dictionary, fields: fields, constructor: default-constructor), - auto-cast-error(dictionary, fields: fields, parse-args: parse-args), - ) - } - } else { - assert( - false, - message: "elembic: types.declare: cast 'with' can only be omitted for 'from: dictionary'. It must receive the default constructor and return a function 'casted value => your type'." - ) - } - - if type(with) != function { - assert( - false, - message: "elembic: types.declare: cast 'with' must receive the default constructor and return a function 'casted value => your type'. Received " + base.typename(with) - ) - } - - let from-cast = from.cast - - types.wrap( - from, - check: from-check => if from-check == none { - if cast-check == none { - none - } else if from.cast == none { - cast-check - } else { - value => cast-check(from-cast(value)) - } - } else if cast-check == none { - from-check - } else if from.cast == none { - value => from-check(value) and cast-check(value) - } else { - value => from-check(value) and cast-check(from-cast(value)) - }, - - output: (typeid,), - - cast: from-cast => if from-cast == none { - with - } else { - value => with(from-cast(value)) - }, - - default: (), - fold: none, - ..if cast-error == none { (:) } else { ( - error: if "check" not in from or from.check == none { - _ => cast-error - } else { - from-error => value => if from-error == none or from-check(value) { cast-error(value) } else { from-error(value) } - }, - ) } - ) - }) - - // Accept our own typeinfo first and foremost - let union = base.union((typeinfo,) + typeinfos) - - assert( - union.output == (typeid,) and union.default == () and union.fold == none, - message: "elembic: types.declare: internal error: cast generated invalid union: " + repr(union) - ) - - ( - input: union.input, - output: union.output, - check: union.check, - cast: union.cast, - error: if union.error == none { - _ => "failed to cast to custom type '" + name + "'" - } else { - x => (union.error)(x).replace("all typechecks for union failed", "all casts to custom type '" + name + "' failed") - }, - data: typeinfo.data + (pre-casts: typeinfo) - ) - } - } - - let default-constructor(..args, __elembic_data: none, __elembic_func: auto) = { - if __elembic_func == auto { - __elembic_func = default-constructor - } - - let default-constructor = default-constructor.with(__elembic_func: __elembic_func) - if __elembic_data != none { - return if __elembic_data == special-data-values.get-data { - let typeinfo = typeinfo + if process-casts != none { process-casts(default-constructor) } else { (:) } - if default != none { - typeinfo.default = (default(default-constructor),) - } - - if auto-fold != none { - typeinfo.fold = auto-fold - } else if type(fold) == function { - let fold = fold(default-constructor) - if fold != auto and type(fold) != function { - assert(false, message: "elembic: types: custom type did not specify a valid fold, must be a function default constructor => value, got " + base.typename(fold)) - } - typeinfo.fold = fold - } - - (data-kind: "custom-type-data", ..type-data, typeinfo: typeinfo, func: __elembic_func, default-constructor: default-constructor) - } else { - assert(false, message: "elembic: types: invalid data key to constructor: " + repr(__elembic_data)) - } - } - - let (res, args) = parse-args(args, include-required: true) - if not res { - assert(false, message: args) - } - - let final-fields = default-fields + args - - if foldable-fields != (:) { - // Fold received arguments with defaults - for (field-name, fold-data) in foldable-fields { - if field-name in args { - let outer = default-fields.at(field-name, default: fold-data.default) - if fold-data.folder == auto { - final-fields.at(field-name) = outer + args.at(field-name) - } else { - final-fields.at(field-name) = (fold-data.folder)(outer, args.at(field-name)) - } - } - } - } - - final-fields.insert( - custom-type-key, - ( - data-kind: "type-instance", - fields: final-fields, - func: __elembic_func, - default-constructor: default-constructor, - tid: tid, - id: (tid: tid, name: name), - scope: scope, - fields-known: true, - valid: true - ) - ) - - final-fields - } - - default = if default == none { - () - } else { - let default = default(default-constructor) - assert( - type(default) == dictionary and custom-type-key in default and default.at(custom-type-key).id == typeid, - message: "elembic: types.declare: the 'default' function must return an instance of the new type using the provided constructor, not " + repr(default) - ) - - - (default,) - } - - fold = if auto-fold != none { - auto-fold - } else if type(fold) == function { - let fold = fold(default-constructor) - if fold != auto and type(fold) != function { - assert(false, message: "elembic: types.declare: a valid fold was not specified, must be a function default constructor => value, got " + base.typename(fold)) - } - fold - } else { - none - } - - if process-casts != none { - typeinfo += process-casts(default-constructor) - } - typeinfo.default = default - typeinfo.fold = fold - type-data.typeinfo = typeinfo - - let final-constructor = if construct != none { - { - let test-construct = construct(default-constructor) - assert(type(test-construct) == function, message: "elembic: types.declare: the 'construct' function must receive the default constructor and return the new constructor, a new function, not '" + str(type(test-construct)) + "'.") - } - - let final-constructor(..args, __elembic_data: none) = { - if __elembic_data != none { - return if __elembic_data == special-data-values.get-data { - (data-kind: "custom-type-data", ..type-data, func: final-constructor, default-constructor: default-constructor.with(__elembic_func: final-constructor)) - } else { - assert(false, message: "elembic: types: invalid data key to constructor: " + repr(__elembic_data)) - } - } - - construct(default-constructor.with(__elembic_func: final-constructor))(..args) - } - - final-constructor - } else { - default-constructor - } - - type-data.default-constructor = default-constructor.with(__elembic_func: final-constructor) - type-data.func = final-constructor - - final-constructor -} diff --git a/src/libs/elembic/types/native.typ b/src/libs/elembic/types/native.typ deleted file mode 100644 index aead5c0..0000000 --- a/src/libs/elembic/types/native.typ +++ /dev/null @@ -1,341 +0,0 @@ -// Typst-native types. -#import "../data.typ": type-key -#import "base.typ": base-typeinfo, ok, err - -// Tiling type (renamed in Typst 0.13.0) -#let tiling = if sys.version < version(0, 13, 0) { pattern } else { tiling } - -#let native-base = ( - ..base-typeinfo, - type-kind: "native", -) - -// Generic typeinfo for a native type. -// PROPERTY: if type key is native, then output has the native type, -// and input has a list of native types that can be cast to it. -#let generic-typeinfo(native-type) = { - assert(type(native-type) == type(str), message: "elembic: internal error: not a type") - - ( - ..native-base, - name: str(native-type), - input: (native-type,), - output: (native-type,), - data: native-type, - ) -} - -// Castable types - -#let content_ = ( - ..native-base, - name: str(content), - input: (type(none), content, str, symbol), - output: (content,), - data: content, - cast: x => [#x], - default: ([],), -) -#let float_ = ( - ..native-base, - name: str(float), - input: (float, int), - output: (float,), - data: float, - cast: float, - default: (0.0,), -) -#let stroke-keys = ("paint", "thickness", "cap", "join", "dash", "miter-limit") -#let stroke_ = ( - ..native-base, - name: str(stroke), - input: (stroke, length, color, gradient, tiling, dictionary), - output: (stroke,), - data: stroke, - cast: stroke, - check: v => type(v) != dictionary or v.keys().all(k => k in stroke-keys), - default: (stroke(),), - // Allow specifying e.g. 4pt in one set rule, red in the other => 4pt + red in the end - fold: (outer, inner) => { - // Can't sum stroke with stroke, so can't optimize with 'fold: auto' :( - stroke( - paint: if inner.paint == auto { outer.paint } else { inner.paint }, - thickness: if inner.thickness == auto { outer.thickness } else { inner.thickness }, - cap: if inner.cap == auto { outer.cap } else { inner.cap }, - join: if inner.join == auto { outer.join } else { inner.join }, - dash: if inner.dash == auto { outer.dash } else { inner.dash }, - miter-limit: if inner.miter-limit == auto { outer.miter-limit } else { inner.miter-limit }, - ) - }, -) -#let relative_ = ( - ..native-base, - name: str(relative), - input: (relative, length, ratio), - output: (relative,), - data: relative, - cast: x => x + 0% + 0pt, - default: (0% + 0pt,), -) -#let function_ = ( - ..native-base, - name: str(function), - // Would add symbol as well, but missing a reliable way to check for callable symbols - input: (type, function), - output: (type, function,), - data: function, -) - -// Folding types (also includes stroke above) -#let array_ = ( - ..native-base, - name: str(array), - input: (array,), - output: (array,), - data: array, - default: ((),), - - // Array fields are added together by default. - fold: auto, -) - -#let alignment_ = ( - ..native-base, - name: str(alignment), - input: (alignment,), - output: (alignment,), - data: alignment, - fold: (outer, inner) => if inner.axis() == none or outer.axis() == inner.axis() { - // If axis A == axis B, we override. For example, left -> right. (No sum) - // Same if both are none (2D alignments), in which case inner fully overrides as well (left + top -> center + bottom). - // In addition, if inner axis is none (it is a 2D alignment), it overrides in both ways (left -> right + top). - inner - } else if outer.axis() == none { - // Here, we know that inner isn't 2D, so either outer is 2D or both have different axes. - // If outer is 2D and inner is 1D, inner replaces its axis in outer, but the other axis is kept. - if inner.axis() == "horizontal" { - inner + outer.y - } else { - outer.x + inner - } - } else { - // Both are 1D and have distinct axes, so we just sum. - // left and top => left + top - // bottom and right => right + bottom - inner + outer - } -) - -// Simple types (no casting) - -#let str_ = ( - ..native-base, - name: str(str), - input: (str,), - output: (str,), - data: str, - default: ("",) -) -#let bool_ = ( - ..native-base, - name: str(bool), - input: (bool,), - output: (bool,), - data: bool, - default: (false,) -) -#let dict_ = ( - ..native-base, - name: str(dictionary), - input: (dictionary,), - output: (dictionary,), - data: dictionary, - default: ((:),), -) -#let int_ = ( - ..native-base, - name: str(int), - input: (int,), - output: (int,), - data: int, - default: (0,), -) -#let color_ = ( - ..native-base, - name: str(color), - input: (color,), - output: (color,), - data: color, -) -#let gradient_ = ( - ..native-base, - name: str(gradient), - input: (gradient,), - output: (gradient,), - data: gradient, -) -#let tiling_ = ( - ..native-base, - name: str(tiling), - input: (tiling,), - output: (tiling,), - data: tiling, -) -#let datetime_ = ( - ..native-base, - name: str(datetime), - input: (datetime,), - output: (datetime,), - data: datetime, -) -#let angle_ = ( - ..native-base, - name: str(angle), - input: (angle,), - output: (angle,), - data: angle, - default: (0deg,), -) -#let ratio_ = ( - ..native-base, - name: str(ratio), - input: (ratio,), - output: (ratio,), - data: ratio, - default: (0%,), -) -#let length_ = ( - ..native-base, - name: str(length), - input: (length,), - output: (length,), - data: length, - default: (0pt,), -) -#let fraction_ = ( - ..native-base, - name: str(fraction), - input: (fraction,), - output: (fraction,), - data: fraction, - default: (0fr,), -) -#let duration_ = ( - ..native-base, - name: str(duration), - input: (duration,), - output: (duration,), - data: duration, - default: (duration(seconds: 0),), -) -#let type_ = ( - ..native-base, - name: str(type), - input: (type,), - output: (type,), - data: type, -) -#let arguments_ = ( - ..native-base, - name: str(arguments), - input: (arguments,), - output: (arguments,), - data: arguments, - default: (arguments(),), -) -#let bytes_ = ( - ..native-base, - name: str(bytes), - input: (bytes,), - output: (bytes,), - data: bytes, - default: (bytes(()),), -) -#let version_ = ( - ..native-base, - name: str(version), - input: (version,), - output: (version,), - data: version, - default: (version(0, 0, 0),), -) - -// None / auto - -#let none_ = ( - ..native-base, - name: "none", - input: (type(none),), - output: (type(none),), - data: type(none), - default: (none,) -) -#let auto_ = ( - ..native-base, - name: "auto", - input: (type(auto),), - output: (type(auto),), - data: type(auto), - default: (auto,) -) - -// Return the typeinfo for a native type. -#let typeinfo(t) = { - let out = if t == content { - content_ - } else if t == int { - int_ - } else if t == bool { - bool_ - } else if t == float { - float_ - } else if t == type(none) { - none_ - } else if t == type(auto) { - auto_ - } else if t == dictionary { - dict_ - } else if t == array { - array_ - } else if t == str { - str_ - } else if t == color { - color_ - } else if t == gradient { - gradient_ - } else if t == datetime { - datetime_ - } else if t == duration { - duration_ - } else if t == function { - function_ - } else if t == relative { - relative_ - } else if t == stroke { - stroke_ - } else if t == tiling { - tiling_ - } else if t == type { - type_ - } else if t == angle { - angle_ - } else if t == alignment { - alignment_ - } else if t == ratio { - ratio_ - } else if t == length { - length_ - } else if t == fraction { - fraction_ - } else if t == arguments { - arguments_ - } else if t == bytes { - bytes_ - } else if t == version { - version_ - } else { - generic-typeinfo(t) - } - - (true, out) -} diff --git a/src/libs/elembic/types/types.typ b/src/libs/elembic/types/types.typ deleted file mode 100644 index 51fd1fb..0000000 --- a/src/libs/elembic/types/types.typ +++ /dev/null @@ -1,405 +0,0 @@ -// The type system used by fields. -#import "../data.typ": data, special-data-values, type-key, custom-type-key, custom-type-data-key, eq, type-version -#import "base.typ" as base: ok, err -#import "native.typ" - -// The default value for a type. -#let default(type_) = { - if type_.default == () { - let prefix = if type_.type-kind in ("native", "union") { type_.type-kind + " " } else { "" } - err(prefix + "type '" + type_.name + "' has no known default, please specify an explicit 'default: value' or set 'required: true' for the field") - } else { - ok(type_.default.first()) - } -} - -#let sequence_ = [].func() -#let typeof(value) = { - let element-data - if type(value) == dictionary and custom-type-key in value { - if custom-type-data-key in value { - base.custom-type - } else { - (value.at(custom-type-key).func)(__elembic_data: special-data-values.get-data).typeinfo - } - } else if type(value) == content and value.func() == sequence_ and { - element-data = data(value) - element-data.eid != none - } { - if "name" in element-data and type(element-data.name) == str { - base.element(element-data.name, element-data.eid) - } else { - base.element("unknown-element", element-data.eid) - } - } else { - let (res, typeinfo) = native.typeinfo(type(value)) - if not res { - assert(false, message: "elembic: types.typeof: " + typeinfo) - } - - typeinfo - } -} - -// Literal type -// Only accepted if value is equal to the literal. -// Input and output are equal to the value. -// -// Uses base typeinfo information for information such as casts and whatnot. -#let literal(value) = { - if value == none { - native.none_ - } else if value == auto { - native.auto_ - } else { - base.literal(value, typeof(value)) - } -} - -// Obtain the typeinfo for a type. -// -// Returns ok(typeinfo), or err(error) if there is no corresponding typeinfo. -#let validate(type_) = { - if type(type_) == function { - let data = type_(__elembic_data: special-data-values.get-data) - let data-kind = data.at("data-kind", default: "unknown") - if data-kind == "custom-type-data" { - type_ = data.typeinfo - } else if data-kind == "element" { - type_ = base.element(data.name, data.eid) - } else { - return (false, "Received invalid type: " + repr(type_) + "\n hint: use 'types.literal(value)' to indicate only that particular value is valid") - } - } - - if type(type_) == type { - native.typeinfo(type_) - } else if type(type_) == dictionary and type-key in type_ { - (true, type_) - } else if type(type_) == dictionary and custom-type-data-key in type_ { - (true, type_.typeinfo) - } else if type(type_) == function { - (false, "A function is not a valid type. (You can use 'types.literal(func)' to only accept a particular function.)") - } else if type_ == none or type_ == auto { - // Accept none or auto to mean their types - native.typeinfo(type(type_)) - } else if type(type_) not in (dictionary, array, content) { - // Automatically accept literals - (true, literal(type_)) - } else { - (false, "Received invalid type: " + repr(type_) + "\n hint: use 'types.literal(value)' to indicate only that particular value is valid") - } -} - -// Error when a value doesn't conform to a certain cast -#let generate-cast-error(value, typeinfo, hint: none) = { - let message = if "any" not in typeinfo.input and base.typeid(value) not in typeinfo.input { - if typeinfo.input == () { - "type '" + typeinfo.name + "' does not accept any values" - } else { - ( - "expected " - + typeinfo.input.map(t => if type(t) == dictionary and "name" in t { t.name } else { str(t) }).join(", ", last: " or ") - + ", found " - + base.typename(value) - ) - } - } else if typeinfo.at("error", default: none) != none { - (typeinfo.error)(value) - } else { - "typecheck for " + typeinfo.name + " failed" - } - let given-hint = if hint == none { "" } else { "\n hint: " + hint } - - message + given-hint -} - -// Try to accept value via given typeinfo or return error -// Returns ok(value) a.k.a. (true, value) on success -// Returns err(value) a.k.a. (false, value) on error -#let cast(value, typeinfo) = { - if type(typeinfo) != dictionary or type-key not in typeinfo { - let (res, typeinfo-or-err) = validate(typeinfo) - if not res { - assert(false, message: "elembic: types.cast: " + typeinfo-or-err) - } - typeinfo = typeinfo-or-err - } - - let kind = typeinfo.type-kind - if kind == "any" { - (true, value) - } else { - let value-type = type(value) - if value-type == dictionary and custom-type-key in value { - value-type = value.at(custom-type-key).id - } - - if kind == "literal" and typeinfo.cast == none and ("__future_cast" not in typeinfo or typeinfo.__future_cast.max-version < type-version) { - if eq(value, typeinfo.data.value) and (value-type in typeinfo.input or "any" in typeinfo.input) and (typeinfo.data.typeinfo.check == none or (typeinfo.data.typeinfo.check)(value)) { - (true, value) - } else { - (false, generate-cast-error(value, typeinfo)) - } - } else if ( - value-type not in typeinfo.input and "any" not in typeinfo.input - or typeinfo.check != none and not (typeinfo.check)(value) - ) { - (false, generate-cast-error(value, typeinfo)) - } else if typeinfo.cast == none { - (true, value) - } else if kind == "native" and typeinfo.data == content and ("__future_cast" not in typeinfo or typeinfo.__future_cast.max-version < type-version) { - (true, [#value]) - } else { - (true, (typeinfo.cast)(value)) - } - } -} - -// Expected types for each typeinfo key. -#let overridable-typeinfo-types = ( - name: (check: a => type(a) == str, error: "string or function old name => new name"), - input: (check: a => type(a) == array and a.all(x => x == "any" or x == "custom type" or type(x) == type or (type(x) == dictionary and "tid" in x)), error: "array of \"any\", \"custom type\", type, or custom type id (tid: ...), or function old input => new input"), - output: (check: a => type(a) == array and a.all(x => x == "any" or x == "custom type" or type(x) == type or (type(x) == dictionary and "tid" in x)), error: "array of \"any\", \"custom type\", type, or custom type id (tid: ...), or function old output => new output"), - check: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function value => bool"), - cast: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function checked input => output"), - error: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function checked input => error string"), - default: (check: d => d == () or type(d) == array and d.len() == 1, error: "empty array for no default, singleton array for one default, or function old default => new default"), - fold: (check: f => f == none or f == auto or type(f) == function, error: "none for no folding, auto to fold with sum (same as (a, b) => a + b), or function receiving old fold and returning either none or auto, or a new function (outer, inner) => combined value"), -) - -// Wrap a type, altering its properties while keeping (or replacing) its input types and checks. -#let wrap(type_, ..data) = { - assert(data.pos() == (), message: "elembic: types.wrap: unexpected positional arguments") - let (res, typeinfo) = validate(type_) - if not res { - assert(false, message: "elembic: types.wrap: " + typeinfo) - } - - let overrides = data.named() - for (key, value) in overrides { - let (check: validate-value, error: key-error) = overridable-typeinfo-types.at(key, default: (check: none, error: none)) - if validate-value == none or key-error == none { - assert(false, message: "elembic: types.wrap: invalid key '" + key + "', must be one of " + overridable-typeinfo-types.keys().join(", ", last: " or ")) - } - - if type(value) == function { - value = value(typeinfo.at(key, default: base.base-typeinfo.at(key))) - } - - if type(value) != function and not validate-value(value) { - assert(false, message: "elembic: types.wrap: invalid value for key '" + key + "', expected " + key-error) - } - } - - if "any" not in typeinfo.output and "cast" in overrides and "output" not in overrides or "output" in overrides and "any" in overrides.output { - // - Collapse "any" + other types into just "any"; - // - If there is a cast and output is unknown, then set it to any for safety (should we error?) - overrides.output = ("any",) - } - - if typeinfo.cast != none and "output" in overrides and "cast" not in overrides and "any" not in overrides.output and typeinfo.output.any(o => o not in overrides.output) { - // If output was changed to a list which isn't 'any' and isn't a superset of the previous output, - // then remove casting as it is no longer safe (might produce something that is invalid) - // (TODO: Should we error?) - overrides.cast = none - } - - if "input" in overrides and "any" in overrides.input { - // - Collapse "any" + other types into just "any" - overrides.input = ("any",) - } - - if "default" not in overrides and typeinfo.default != () and ("check" in overrides or "output" in overrides and "any" not in overrides.output and typeinfo.output.any(o => o not in overrides.output)) { - // Not sure if default would fit those criteria anymore: - // 1. By overriding the check, it's possible that a type such as positive int (check: int > 0) would no longer - // have an acceptable default when changing its check to, say, negative int (check: int < 0). - // 2. By overriding the output and removing previous output types, it's possible the default no longer has a valid type (it must be a valid output). - overrides.default = () - } - - if ("check" in overrides or "output" in overrides) and "fold" not in overrides { - // Folding might not be valid anymore: - // 1. By overriding the check, it's possible a fold that, say, adds two numbers, would no longer be valid - // if, for example, the new check ensures each number is smaller than 59 (you might add up to that). - // In addition, the fold might now receive parameters that would fail the new check while being cast. - // 2. By overriding the output: - // a. and removing old output, it's possible the fold produces invalid output. - // b. and adding new output, it's possible the fold receives parameters of an unexpected type. - overrides.fold = none - } - - let new-default = overrides.at("default", default: typeinfo.default) - let new-output = overrides.at("output", default: typeinfo.output) - assert( - new-default == () - or "any" in new-output - or base.typeid(new-default.first()) in new-output, - - message: "elembic: types.wrap: new default (currently " + repr(if new-default == () { none } else { new-default.first() }) + ") must have a type within possible 'output' types of the new type (currently " + if new-output == () { "empty" } else { new-output.map(t => if type(t) == dictionary { t.name } else { str(t) }).join(", ", last: " or ") } + "), since it is itself an output\n hint: you can either change the default, or update possible output types with 'output: (new, list)' to indicate which native or custom types your wrapped type might end up as after casts (if there are casts)." - ) - - base.wrap(typeinfo, overrides) -} - -// Specifies that any from a given selection of types is accepted. -#let union(..args) = { - let types = args.pos() - assert(types != (), message: "elembic: types.union: please specify at least one type") - - let typeinfos = types.map(type_ => { - let (res, typeinfo-or-err) = validate(type_) - assert(res, message: if not res { "elembic: types.union: " + typeinfo-or-err } else { "" }) - - typeinfo-or-err - }) - - base.union(typeinfos) -} - -// An optional type (can be 'none'). -#let option(type_) = union(type(none), type_) - -// A type which can be 'auto'. -#let smart(type_) = union(type(auto), type_) - -#let array_(type_) = { - let (res, param) = validate(type_) - if not res { - assert(false, message: "elembic: types.array: " + param) - } - - base.array_( - native.array_, - param, - - error: if param.check == none { - a => { - let (count, message) = a.enumerate().fold((0, ""), ((count, message), (i, element)) => { - if "any" not in param.input and base.typeid(element) not in param.input { - (count + 1, message + "\n hint: at position " + str(i) + ": " + generate-cast-error(element, param)) - } else { - (count, message) - } - }) - - let n-elements = if count == 1 { "an element" } else { str(count) + " elements" } - n-elements + " in an array of " + param.name + " did not typecheck" + message - } - } else { - a => { - let (count, message) = a.enumerate().fold((0, ""), ((count, message), (i, element)) => { - if "any" not in param.input and base.typeid(element) not in param.input or not (param.check)(element) { - (count + 1, message + "\n hint: at position " + str(i) + ": " + generate-cast-error(element, param)) - } else { - (count, message) - } - }) - - let n-elements = if count == 1 { "an element" } else { str(count) + " elements" } - n-elements + " in an array of " + param.name + " did not typecheck" + message - } - } - ) -} - -#let dict_(type_) = { - let (res, param) = validate(type_) - if not res { - assert(false, message: "elembic: types.array: " + param) - } - - base.dict_( - native.dict_, - param, - - error: if param.check == none { - d => { - let (count, message) = d.pairs().fold((0, ""), ((count, message), (key, value)) => { - if "any" not in param.input and base.typeid(value) not in param.input { - (count + 1, message + "\n hint: at key " + repr(key) + ": " + generate-cast-error(value, param)) - } else { - (count, message) - } - }) - - let n-elements = if count == 1 { "a value" } else { str(count) + " values" } - n-elements + " in a dictionary of " + param.name + " did not typecheck" + message - } - } else { - d => { - let (count, message) = d.pairs().fold((0, ""), ((count, message), (key, value)) => { - if "any" not in param.input and base.typeid(value) not in param.input or not (param.check)(value) { - (count + 1, message + "\n hint: at key " + repr(key) + ": " + generate-cast-error(value, param)) - } else { - (count, message) - } - }) - - let n-elements = if count == 1 { "a value" } else { str(count) + " values" } - n-elements + " in a dictionary of " + param.name + " did not typecheck" + message - } - } - ) -} - -// Native paint type. Can be used for fills, strokes and so on. -#let paint = union(color, gradient, native.tiling_) - -// Force the type to only accept its outputs (disallow casting). -// Folding is kept if possible. -#let exact(type_) = { - let (res, type_) = validate(type_) - if not res { - assert(false, message: "elembic: types.exact: " + type_) - } - - let key = if type(type_) == dictionary and "type-kind" in type_ { type_.type-kind } else { none } - if key == "union" { - // exact(union(A, B)) === union(exact(A), exact(B)) - union(..type_.data.map(exact)) - } else if type(type_) == type or key == "native" { - // exact(float) => can only pass float, not int - // exact(stroke) => can only pass stroke, not length, gradient, dict, etc. - let native-type = type_.data - ( - ..native.generic-typeinfo(native-type), - default: if type_.default != () and type(type_.default.first()) == native-type { type_.default } else { () }, - - // Fold is an output => output function. The new output will be just (native-type,), - // so if fold previously accepted that native type, it will still accept it, so it - // can be kept. - fold: if native-type in type_.output { type_.fold } else { none }, - ) - } else if key == "literal" { - // exact(literal) => literal with base type modified to exact(base type) - assert(type(type_.data.value) not in (dictionary, array), message: "elembic: types.exact: exact literal types for custom types, dictionaries and arrays are not supported\n hint: consider customizing the check function to recursively check fields if the performance is acceptable") - - base.literal(type_.data.value, exact(type_.data.typeinfo)) - } else if key == "any" or key == "never" { - // exact(any) => any (same) - // exact(never) => never (same) - type_ - } else if key == "collection" { - if "base" in type_.data and "parameters" in type_.data { - let base-kind = type_.data.base.at("type-kind", default: none) - if base-kind == "native" and type_.data.base.data == array { - array_(..type_.data.parameters.map(exact)) - } else if base-kind == "native" and type_.data.base.data == dictionary { - dict_(..type_.data.parameters.map(exact)) - } else { - assert(false, message: "elembic: types.exact: unknown collection with type kind '" + base-kind + "'" + if base-kind == "native" { ", base native type '" + type_.data.base.name + "'" } else { "" }) - } - } else { - assert(false, message: "elembic: types.exact: invalid collection given") - } - } else if key == "custom" { - if type_.data.pre-casts == none { - type_ - } else { - type_.data.pre-casts - } - } else { - assert(false, message: "elembic: types.exact: unsupported type kind " + key + ", supported kinds include native types, literals, custom types, arrays, dicts, 'any' and 'never'") - } -} diff --git a/src/model/arrow-element.typ b/src/model/arrow-element.typ index 61ba41c..f9c88fa 100644 --- a/src/model/arrow-element.typ +++ b/src/model/arrow-element.typ @@ -1,4 +1,5 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e + #import "../utils.typ": ( get-arrow, ) diff --git a/src/model/element-element.typ b/src/model/element-element.typ index b66ab19..01dd19a 100644 --- a/src/model/element-element.typ +++ b/src/model/element-element.typ @@ -1,4 +1,4 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../utils.typ": ( count-to-content, charge-to-content, diff --git a/src/model/element-variable.typ b/src/model/element-variable.typ index d6e321c..87f1c3b 100644 --- a/src/model/element-variable.typ +++ b/src/model/element-variable.typ @@ -1,4 +1,4 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../utils.typ": ( // is-sequence, // is-kind, diff --git a/src/model/group-element.typ b/src/model/group-element.typ index 7916f96..cd4dd1b 100644 --- a/src/model/group-element.typ +++ b/src/model/group-element.typ @@ -1,4 +1,4 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../utils.typ": ( count-to-content, charge-to-content, diff --git a/src/model/molecule-element.typ b/src/model/molecule-element.typ index 96b4f19..54fc9c6 100644 --- a/src/model/molecule-element.typ +++ b/src/model/molecule-element.typ @@ -1,4 +1,4 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../utils.typ": ( count-to-content, charge-to-content, diff --git a/src/model/molecule-variable.typ b/src/model/molecule-variable.typ index 4d31591..f85e247 100644 --- a/src/model/molecule-variable.typ +++ b/src/model/molecule-variable.typ @@ -1,4 +1,4 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../utils.typ": ( // is-sequence, // is-kind, diff --git a/src/model/reaction-element.typ b/src/model/reaction-element.typ index 205c127..63ab1a1 100644 --- a/src/model/reaction-element.typ +++ b/src/model/reaction-element.typ @@ -1,5 +1,5 @@ -#import "../libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../utils.typ": get-arrow, is-default #let reaction( diff --git a/src/typing.typ b/src/typing.typ index 4e4356c..3a6bc40 100644 --- a/src/typing.typ +++ b/src/typing.typ @@ -1,4 +1,4 @@ -#import "libs/elembic/lib.typ" as e: selector +#import "@preview/elembic:1.1.0" as e: selector #import "model/arrow-element.typ": arrow #import "model/element-element.typ": element #import "model/group-element.typ": group diff --git a/tests/brackets/test.typ b/tests/brackets/test.typ index 8f4f7ac..ae659af 100644 --- a/tests/brackets/test.typ +++ b/tests/brackets/test.typ @@ -1,5 +1,5 @@ #import "../../src/lib.typ" : ce -#import "../../src/libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../../src/model/group-element.typ":* // #show: e.set_(group, grow-brackets:false, affect-layout:false) diff --git a/tests/content-to-reaction/test.typ b/tests/content-to-reaction/test.typ index 7f307bf..076b567 100644 --- a/tests/content-to-reaction/test.typ +++ b/tests/content-to-reaction/test.typ @@ -1,6 +1,6 @@ #import "../../src/lib.typ" : ce, define-molecule, get-element #import "../../src/utils.typ" : * -#import "../../src/libs/elembic/lib.typ" as e +#import "@preview/elembic:1.1.0" as e #import "../../src/model/group-element.typ":* #import "../../src/model/element-element.typ":* #import "../../src/parse-formula-intermediate-representation.typ": string-to-reaction, From 1917c241ef462c4d730382568c73c63632d7d99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B2-=E5=90=B2=E5=93=9A=E5=9F=BA=E4=B8=99=E6=B0=A8?= =?UTF-8?q?=E9=85=B8?= Date: Tue, 24 Jun 2025 22:01:42 +0800 Subject: [PATCH 18/20] Refine regex patterns Updated regex patterns for improved parsing of elements, groups, and reactions. Changed the default value of 'grow-brackets' to false in group-element.typ. --- src/model/group-element.typ | 2 +- ...se-formula-intermediate-representation.typ | 27 ++++++++++++++----- tests/brackets/test.typ | 7 +++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/model/group-element.typ b/src/model/group-element.typ index cd4dd1b..81e72b4 100644 --- a/src/model/group-element.typ +++ b/src/model/group-element.typ @@ -53,7 +53,7 @@ e.field("kind", int, default: 0), e.field("count", e.types.union(int, content), default: 1), e.field("charge", e.types.union(int, content), default: 0), - e.field("grow-brackets", bool, default: true), + e.field("grow-brackets", bool, default: false), e.field("affect-layout", bool, default: true), ), ) diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index b4aff58..3b2d21b 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -6,13 +6,26 @@ #import "model/arrow-element.typ": arrow #let patterns = ( - element: regex("^(?P[A-Z][a-z]?)(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?:(?P_?\d+)|(?P\^[+-]?[IV]+|\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\.?[+-]{1}\d?))?(?P\^\^[+-]?[IViv]{1,3}|\^\^[+-]?\d+)?"), - group: regex("^(?P\((?:[^()]|(?R))*\)|\{(?:[^{}]|(?R))*\}|\[(?:[^\[\]]|(?R))*\])(?:(?P_?\d+)|(?P\^[+-]?\d+[+-]?|\^[+-]{1}|[+-]{1}\d?))?(?:(?P_?\d+)|(?P\^[+-]?\d+[+-]?|\^[+-]{1}|[+-]{1}\d?))?"), - reaction-plus: regex("^(\s?\+\s?)"), - reaction-arrow: regex("^\s?(<->|↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-\/>|<\/-)(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?(?:\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\])?\s?"), - math: regex("^(\$[^$]*\$)"), - // Match physical states (s/l/g/aq) - state: regex("^\((s|l|g|aq|solid|liquid|gas|aqueous|aqua)\)"), + element: regex( + "^([A-Z][a-z]?)" + + "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|\\^?[+-]?[IV]+|\\.?[+-]{1}\\d?))?" + + "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|\\^?[+-]?[IV]+|\\.?[+-]{1}\\d?))?" + + "(\\^\\^[+-]?(?:[IViv]{1,3}|\\d+))?" + ), + group: regex( + "^((?:\\([^()]*(?:\\([^()]*\\)[^()]*)*\\))|(?:\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\})|(?:\\[[^\\[\\]]*(?:\\[[^\\[\\]]*\\][^\\[\\]]*)*\\]))" + + "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|[+-]{1}\\d?))?" + + "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|[+-]{1}\\d?))?" + ), + reaction-plus: regex("^\\s*\\+\\s*"), + reaction-arrow: regex( + "^\\s*(<->|↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-/?\\>| Date: Tue, 24 Jun 2025 22:16:44 +0800 Subject: [PATCH 19/20] Update parse-formula-intermediate-representation.typ --- ...se-formula-intermediate-representation.typ | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/parse-formula-intermediate-representation.typ b/src/parse-formula-intermediate-representation.typ index 3b2d21b..b7fcb54 100644 --- a/src/parse-formula-intermediate-representation.typ +++ b/src/parse-formula-intermediate-representation.typ @@ -8,24 +8,24 @@ #let patterns = ( element: regex( "^([A-Z][a-z]?)" + - "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|\\^?[+-]?[IV]+|\\.?[+-]{1}\\d?))?" + - "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|\\^?[+-]?[IV]+|\\.?[+-]{1}\\d?))?" + - "(\\^\\^[+-]?(?:[IViv]{1,3}|\\d+))?" + "(?:(_?\d+)|(\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\^?[+-]?[IV]+|\.?[+-]{1}\d?))?" + + "(?:(_?\d+)|(\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|\^?[+-]?[IV]+|\.?[+-]{1}\d?))?" + + "(\^\^[+-]?(?:[IViv]{1,3}|\d+))?" ), group: regex( - "^((?:\\([^()]*(?:\\([^()]*\\)[^()]*)*\\))|(?:\\{[^{}]*(?:\\{[^{}]*\\}[^{}]*)*\\})|(?:\\[[^\\[\\]]*(?:\\[[^\\[\\]]*\\][^\\[\\]]*)*\\]))" + - "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|[+-]{1}\\d?))?" + - "(?:(_?\\d+)|(\\^\\.?[+-]?\\d+[+-]?|\\^\\.?[+-.]{1}|[+-]{1}\\d?))?" + "^((?:\([^()]*(?:\([^()]*\)[^()]*)*\))|(?:\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})|(?:\[[^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*\]))" + + "(?:(_?\d+)|(\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|[+-]{1}\d?))?" + + "(?:(_?\d+)|(\^\.?[+-]?\d+[+-]?|\^\.?[+-.]{1}|[+-]{1}\d?))?" ), - reaction-plus: regex("^\\s*\\+\\s*"), + reaction-plus: regex("^\s*\+\s*"), reaction-arrow: regex( - "^\\s*(<->|↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-/?\\>||↔|<=>|⇔|->|→|<-|←|=>|⇒|<=|⇐|-/?\>| Date: Sun, 29 Jun 2025 10:04:12 +0800 Subject: [PATCH 20/20] Final commit Wrapping up the code for release. --- README.md | 21 ++- src/lib.typ | 3 + src/resources/arrow1.svg | 277 +++++++++++++++++++++++++++++++++ src/resources/arrow2.svg | 153 ++++++++++++++++++ tests/0. Example book/main.png | Bin 223548 -> 267541 bytes tests/0. Example book/main.typ | 50 +++++- tests/arrow-align/test.svg | 216 +++++++++++++++++++++++++ 7 files changed, 714 insertions(+), 6 deletions(-) create mode 100644 src/resources/arrow1.svg create mode 100644 src/resources/arrow2.svg create mode 100644 tests/arrow-align/test.svg diff --git a/README.md b/README.md index 81194e5..d149faa 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,30 @@ Enter your chemical formula or reaction into the `#ce"` method like this: ``` ![result](https://raw.githubusercontent.com/Typsium/typsium/main/tests/README-graphic1/ref/1.png) -You can also embed any kind of content into your chemical reactions like by using square brackets instead of a passing in a string. This will also apply any styling to the reaction. +You can also embed any kind of content into your chemical reactions like by using square brackets instead of a passing in a string. This will also apply any styling to the reaction. + +> **Warning:** Currently, brackets inside another bracket will not be parsed correctly. + ```typst #ce[...] ``` + ![result2](https://raw.githubusercontent.com/Typsium/typsium/main/tests/README-graphic1/ref/1.png) -There are many different kinds of arrows to choose from. And you can add additional arguments to them (such as the top or bottom text) by adding square brackets. +There are many different kinds of arrows to choose from. +```typst +#ce[->]\ +#ce[=>]\ +#ce[<=>]\ +#ce[<=]\ +#ce("<->")\ +#ce("<-")\ +``` + +And you can add additional arguments to them (such as the top or bottom text) by adding square brackets. + ```typst -//show arrows and how they look +#ce("->[top text][bottom text]") ``` The molecule parsing is flexible and allows many different ways of writing, so you can just copy paste in your formulas and they will probably work. Oxidation numbers can be added like this`^^`, radicals can be added like this`.` and hydration groups can be added like this`*`. diff --git a/src/lib.typ b/src/lib.typ index ba07897..d2d01fa 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -10,6 +10,9 @@ #import "model/reaction-element.typ": reaction #let ce(formula) = { + + show "*": sym.dot + if type(formula) == str{ let result = string-to-reaction(formula) if result.len() == 1{ diff --git a/src/resources/arrow1.svg b/src/resources/arrow1.svg new file mode 100644 index 0000000..6e5adca --- /dev/null +++ b/src/resources/arrow1.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/resources/arrow2.svg b/src/resources/arrow2.svg new file mode 100644 index 0000000..7a65867 --- /dev/null +++ b/src/resources/arrow2.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/0. Example book/main.png b/tests/0. Example book/main.png index 3626a5a4e3c3fcba3385e88084d69bb09a0b91be..810bc38f3ebc9ce7fb23b3250e9b68abcba33841 100644 GIT binary patch literal 267541 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

Q(ZgENNz-#xdNU8?w4p*ePJpZkx*Tj>%Lwrs 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

A3ct30cx3Jd$X@w6c~M!IYyk2HteHT4 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 literal 223548 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%0DEf)n8RN~opgjB8Q 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%0D7|t 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 Xb#}b->7J{-?2Xc7`zQg diff --git a/tests/0. Example book/main.typ b/tests/0. Example book/main.typ index 1191b9e..ab61e0c 100644 --- a/tests/0. Example book/main.typ +++ b/tests/0. Example book/main.typ @@ -1,8 +1,10 @@ #import "@preview/cram-snap:0.2.2": * #import "../../src/lib.typ": * +//#import "@preview/typsium:0.3.0": * #import "@preview/zebraw:0.5.5": * +#import "@preview/cuti:0.3.0":* -#set page(flipped: false, margin: 0.8cm) +#set page(margin: 0.8cm, height: auto) #set text(size: 14pt) #show raw: set text(font: "IBM Plex Sans", size: 11pt) #show: set-group(grow-brackets:false, affect-layout:false) @@ -18,6 +20,16 @@ stroke-color: "343434", ) +#align(center)[ + #text(size: 24pt, weight: "bold", fill: rgb("2c5aa0"))[ + #show raw: set text(size: 24pt) + Basic Usage + ] + #v(-0.9em) + #line(length: 60%, stroke: 1pt + rgb("2c5aa0")) + #v(-0.3em) +] + #table(inset: 0.7em)[ Effect ][ @@ -42,6 +54,10 @@ #ce("[Fe(CN)6]^4+") ][ ```typ #ce("[Fe(CN)6]^4+")``` +][ + #ce[CuSO4*5H2O] +][ + ```typ #ce[CuSO4*5H2O]``` ][ #ce[->] ][ @@ -59,12 +75,40 @@ roman-oxidation ][ ```typ #show: set-element(roman-charge: true)``` \ ```typ #show: set-element(roman-oxidation: true)``` +] +#align(center)[ + #text(size: 24pt, weight: "bold", fill: rgb("2c5aa0"))[ + #show raw: set text(size: 24pt) + Content in #raw("ce", lang: "typ") + ] + #v(-0.9em) + #line(length: 60%, stroke: 1pt + rgb("2c5aa0")) + #v(-0.3em) +] + +#table(inset: 0.7em)[ + effect +][ + content ][ #ce[#text(red)[H2]] ][ ```typ #ce[2#text(red)[H2]]``` ][ - #ce[$overbrace("H2O","water")$] + $overbrace(#ce("H2O"),"water")$ +][ + ```typ $overbrace(#ce("H2O"),"water")$ ``` +][ + #ce[*H2O*] +][ + ```typ #ce[*H2O*]``` +][ + #ce[#fakeitalic[H2O]] ][ - ```typ #ce[$overbrace("H2O","water")$]``` + ```typ + #import "@preview/cuti:0.3.0":* //or newer + #ce[#fakeitalic[H2O]] + + ``` ] +//... \ No newline at end of file diff --git a/tests/arrow-align/test.svg b/tests/arrow-align/test.svg new file mode 100644 index 0000000..1201db9 --- /dev/null +++ b/tests/arrow-align/test.svg @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +