From 756bdb623c9deda1458506b1783a66d92f2d9414 Mon Sep 17 00:00:00 2001 From: damaxwell Date: Tue, 22 Aug 2023 02:23:55 -0800 Subject: [PATCH] Support selectors with and/or followed by before/after (#1883) Co-authored-by: Laurenz --- crates/typst/src/model/introspect.rs | 56 ++++++++--- crates/typst/src/model/selector.rs | 4 +- tests/ref/compiler/selector-logical.png | Bin 0 -> 3844 bytes tests/typ/compiler/selector-logical.typ | 127 ++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 tests/ref/compiler/selector-logical.png create mode 100644 tests/typ/compiler/selector-logical.typ diff --git a/crates/typst/src/model/introspect.rs b/crates/typst/src/model/introspect.rs index 42c1a9e1f..2b2693d96 100644 --- a/crates/typst/src/model/introspect.rs +++ b/crates/typst/src/model/introspect.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::num::NonZeroUsize; @@ -237,6 +237,15 @@ impl Introspector { .get_index_of(&elem.location().unwrap()) .unwrap_or(usize::MAX) } + + /// Perform a binary search for `elem` among the `list`. + fn binary_search( + &self, + list: &[Prehashed], + elem: &Content, + ) -> Result { + list.binary_search_by_key(&self.index(elem), |elem| self.index(elem)) + } } #[comemo::track] @@ -252,12 +261,9 @@ impl Introspector { Selector::Elem(..) | Selector::Label(_) | Selector::Regex(_) - | Selector::Can(_) - | Selector::Or(_) - | Selector::And(_) => { + | Selector::Can(_) => { self.all().filter(|elem| selector.matches(elem)).cloned().collect() } - Selector::Location(location) => { self.get(location).cloned().into_iter().collect() } @@ -265,9 +271,7 @@ impl Introspector { let mut list = self.query(selector); if let Some(end) = self.query_first(end) { // Determine which elements are before `end`. - let split = match list - .binary_search_by_key(&self.index(&end), |elem| self.index(elem)) - { + let split = match self.binary_search(&list, &end) { // Element itself is contained. Ok(i) => i + *inclusive as usize, // Element itself is not contained. @@ -281,10 +285,7 @@ impl Introspector { let mut list = self.query(selector); if let Some(start) = self.query_first(start) { // Determine which elements are after `start`. - let split = match list - .binary_search_by_key(&self.index(&start), |elem| { - self.index(elem) - }) { + let split = match self.binary_search(&list, &start) { // Element itself is contained. Ok(i) => i + !*inclusive as usize, // Element itself is not contained. @@ -294,6 +295,37 @@ impl Introspector { } list } + Selector::And(selectors) => { + let mut results: Vec<_> = + selectors.iter().map(|sel| self.query(sel)).collect(); + + // Extract the smallest result list and then keep only those + // elements in the smallest list that are also in all other + // lists. + results + .iter() + .enumerate() + .min_by_key(|(_, vec)| vec.len()) + .map(|(i, _)| i) + .map(|i| results.swap_remove(i)) + .iter() + .flatten() + .filter(|candidate| { + results + .iter() + .all(|other| self.binary_search(other, candidate).is_ok()) + }) + .cloned() + .collect() + } + Selector::Or(selectors) => selectors + .iter() + .flat_map(|sel| self.query(sel)) + .map(|elem| self.index(&elem)) + .collect::>() + .into_iter() + .map(|index| self.elems[index].0.clone()) + .collect(), }; self.queries.borrow_mut().insert(hash, output.clone()); diff --git a/crates/typst/src/model/selector.rs b/crates/typst/src/model/selector.rs index 12f7fa0e4..5e4f257b5 100644 --- a/crates/typst/src/model/selector.rs +++ b/crates/typst/src/model/selector.rs @@ -64,13 +64,13 @@ impl Selector { } /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::Or`] selector. + /// [`Selector::And`] selector. pub fn and(self, others: impl IntoIterator) -> Self { Self::And(others.into_iter().chain(Some(self)).collect()) } /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::And`] selector. + /// [`Selector::Or`] selector. pub fn or(self, others: impl IntoIterator) -> Self { Self::Or(others.into_iter().chain(Some(self)).collect()) } diff --git a/tests/ref/compiler/selector-logical.png b/tests/ref/compiler/selector-logical.png new file mode 100644 index 0000000000000000000000000000000000000000..eafa93c8118d33f261e1922b14b6543f6b9b4d48 GIT binary patch literal 3844 zcma)9XHXN|woQ-{dV*3!T7tr*h#*Q+iXk)$2oj2d0tzA>1VRhV&_YKk(n0uuPy*6R zAOyq!5|t_-z4y?oaJ~1~GEyi5Q9fK^8uVE_OC zX@B2Q&~E|&Vs4ZK0NA>95O)l{M%Ko>U69C{&zndIM%g@#hnIrMQRHm$h54d8G{L!a zAh~TYUkdL9!+y>A9J=V>>*3@VYC93EmY_$ympP!hJV8RQL2+OZ?e&R`V`1Q1WA}k# zWBkBRf`35X;ILwS(NX{KaeCzj;W*qxk}L9)RfUPVMEgLd240#YwOw)#n{i7iE>O zNOZRj=8ejlbMgevjy3iTsO3g~ z!RzgnOU*c?z8OCFYeD4IVK~x+hVFo!#c;`u`utxQJ@mSNms)reiQlxvhSYV?e z+8|vcB_+(|DRRFdrd+QYkvjvbJH7EO$Sh@r+Ull?pnohK?U@xgW!HQa zyzX}gMhvxVg}j>4JjA$hW;n03#KA#MSXWyzuV952%4*KP>jIdo5wem z^M9HC?lnXFBci?Av)o3IUbq>sS1_(1UCRPPR#|{>MM}B|K>J5C3r_;>92m@c|E9vf zxC>aOkcTKRcTOF8N>aStFa>slOA*99qP=Tkj6ZLsiPI($Gw1BTbi40;JS4T`K4)8v zJs4i0T{Rl{V<`U=R+cID2x_es?@gx>N!49`^j!wGVn%XuB+iU- z2_6g`47@aAuAvqMS^8)9JXSnbLtw?=wJEHys-T{uHRQqsE$U6`cZngncm ze?fHu51XD(ak1SVbiCE}?}KuhINIgZYJ9Wb@izibsUXHa8%n0^OG^6tG=5ETlmk?G z5Uo@X*DGjjGaF-Smv`ZXB0K>OH)y}UB4cCbPJn?k&pvwrv}1qxNx35M_vOLtP4NUKDV1AIcw2vv*c z^|E=%byFQ4@FR-I2>Kdv;HH26pc%crE^| zPN%sV%vJ#Z)6e&qb%9@&Id|NX@2Utk_(>41f{3!s*lv*W8{k1rejkYa3P7tt(Cy-s z$xH%4lSLqT@?d8q|Da@IJ!_cn!=GX>1mzp{>hgtnehsibt+^i<)rjwOeNB#&LR0oK zcvp|}lVPj4o6{>P)ur7%bmK_`)M)-TpiI@I=_+l&dIqTbUKt&gWfj1emf)Fep1T8R zPiEd5k%9izj%k8b@3oA*4AqmHJ!oF91}S&t&l;})rSgCgLv^mqWaUJZS(FsR<&~YL zUu6?pau{Y6M<$1!k}}_evYO`Fx>bU_>B!8$yt1s0v+?(9($eKcDm)5ox_dlh^*T)` z=xUNBo&2-!&QmWP<({|Ml6F{(e@)QP|75vJ}XZ8dBY&}yU{R!I4_&H*=pNc7KQd@lx4yAcOo!m=6rQ?i z%0kQ!FD%sdGF5ib9tC_^rmO!nE*|x*UjEyHO3h*(4Zioq&60ju4Nl95D4NMIZa-_G zC?vZdo_RaK*IlO*7y>S{|0Q5PBJXozl+GofC5vM3rRHM6Vh^^9*%CY$EENI8X)zPO z)LP&mnd?`bH&(5XOn!1RBPsJx=tn{!?8V?_L{G5~CM zj@I&-1OKJL6+}>Z(7Oe3o7CtD-q;ZibkhUu@6s*T(A9J%$Nf_4>{F96qp~Y7s`XOS zAF_t@G_b;~P2-!aPD=bD1p^O!dag|`<%L=1$IurV9l|?iIePSL@(zs&K<2p(;@zx< z-*@qa;j!B6qw}v@?0Ka+pvx6j>PLA`IXyBGC9cV zC9h2U$VO9-=#M!o@9tPva|cwAiu-?f6=t+fb5{~bPE21VRd9Ah!g1%;7`y&UQ!5) z^R@bjvpZIo59%-Mz~Q#_6hpy}Ds>5=(E>$J?B%Gnk3Cl8e`j5{N0#EjfRFw=T_iys zXqD-w7_@S8nT`*Q<6HA$!-e zf|m-^RW%{ZtxB*dN1CcVF#`xwW$jcPK@(+e&_pA5!7+}>;xOVEfDa#@}#@}y2LOIWg z99k2_4jB#wisH6{)8E+sV&R#Y2{Np^ZYJe>g~ZLle1~5Q6IWT+O$KtXe{W#pDNu~# z@4ZroPv4~kZB~5dCj6W?gJPbP1}B)7gn)ZfckfP_Z>WQ|fHle)=hOB@#=X567R_O4 z;4#+7T?NDvuSJPXrCKgk6BFtqU{l*j0F@T_t+>OxyME?{G(^0s*>aRR%a05MrV{yv z;cnS{lwEPmgP>;rfeE3(Grh#SMz#$cCgxk2T=y1`yssChpI^!3smd{#D`FO)N(hn) zr<hf!1N#U}t#9{*ZMw<`zH)m*&Vf;VaX!qG)0}llQ4G|$v#LmDF_&Ge z{P^7k!`lF2W#8esSrq(0e;+VgqdYAQ_Zdjd8TyLxWNovg<@ifw;A|~AAkLY6cqjGm zI{iO?vn1!pxGGVrfSprxW1qREf)N`s9-^jBMZ^Je$R1#o2NGJn5P$D~ z4Stt>^I9xfaR|bNC9~B+*kUC`VTm12-7Z2m(|%{8)_KgYZ*6^&AoFkQ{+EIOPdx}g gl>hfSxt?%FFZ8Gv9wSxa_^ +#figure([Frog], kind: "frog", supplement: none) +#figure([Giraffe], kind: "giraffe", supplement: none) +#figure([GiraffeCat], kind: "cat", supplement: [Other]) += H +#figure([Iguana], kind: "iguana", supplement: none) +== I + +#let test-selector(selector, ref) = locate(loc => { + let elems = query(selector, loc) + test(elems.map(e => e.body), ref) +}) + +// Test `or`. +#test-selector( + heading.where(level: 1).or(heading.where(level: 3)), + ([A], [D], [E], [H]), +) + +#test-selector( + heading.where(level: 1).or( + heading.where(level: 3), + figure.where(kind: "frog"), + ), + ([A], [D], [E], [Frog], [H]), +) + +#test-selector( + heading.where(level: 1).or( + heading.where(level: 2), + figure.where(kind: "frog"), + figure.where(kind: "cat"), + ), + ([A], [B], [Cat], [E], [Frog], [GiraffeCat], [H], [I]), +) + +#test-selector( + figure.where(kind: "dog").or(heading.where(level: 3)), + ([D],), +) + +#test-selector( + figure.where(kind: "dog").or(figure.where(kind: "fish")), + (), +) + +// Test `or` duplicates removal. +#test-selector( + heading.where(level: 1).or(heading.where(level: 1)), + ([A], [E], [H]), +) + +// Test `and`. +#test-selector( + figure.where(kind: "cat").and(figure.where(kind: "frog")), + (), +) + +// Test `or` with `before`/`after` +#test-selector( + selector(heading) + .before() + .or(selector(figure).before()), + ([A], [B], [Cat], [D], [E]), +) + +#test-selector( + heading.where(level: 2) + .after() + .or(selector(figure).after()), + ([Frog], [Giraffe], [GiraffeCat], [Iguana], [I]), +) + +// Test `and` with `after` +#test-selector( + figure.where(kind: "cat") + .and(figure.where(supplement: [Other])) + .after(), + ([GiraffeCat],), +) + +// Test `and` (with nested `or`) +#test-selector( + heading.where(level: 2) + .or(heading.where(level: 3)) + .and(heading.where(level: 2).or(heading.where(level: 1))), + ([B], [I]), +) + +#test-selector( + heading.where(level: 2) + .or(heading.where(level: 3), heading.where(level:1)) + .and( + heading.where(level: 2).or(heading.where(level: 1)), + heading.where(level: 3).or(heading.where(level: 1)), + ), + ([A], [E], [H]), +) + +// Test `and` with `or` and `before`/`after` +#test-selector( + heading.where(level: 1).before() + .or(heading.where(level: 3).before()) + .and( + heading.where(level: 1).before() + .or(heading.where(level: 2).before()) + ), + ([A], [E]), +) + +#test-selector( + heading.where(level: 1).before(, inclusive: false) + .or(selector(figure).after()) + .and(figure.where(kind: "iguana").or( + figure.where(kind: "frog"), + figure.where(kind: "cat"), + heading.where(level: 1).after(), + )), + ([Frog], [GiraffeCat], [Iguana]) +)