123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148moduleCss=Cascade.Css(* Client-side CSS injection.
Maintains [<style data-tw="runtime">] elements in [<head>] containing CSS for
all utilities registered via [use]. Each utility's class name is tracked to
avoid re-injection.
A full [Tw.to_css] over every utility seen so far, run on every newly
discovered class, costs 1 + 2 + ... + n compilations over a mount pass that
introduces n distinct classes - O(n^2). Rebuilds are coalesced onto a
microtask (a mount pass compiles once, not once per [use] call), but that
alone does not bound the *total* work over the app's lifetime: a class
trickling in on its own microtask on every tick still hits the same pattern,
one full recompile per tick. [flush] instead keeps one "consolidated" element
holding a single from-scratch, fully sorted and deduplicated compile, and
injects each tick's new classes as their own small "delta" element compiled
from just that tick's batch - O(that batch) instead of O(everything so far).
Deltas fold back into the consolidated element once they have grown to match
it, the same growth-factor rule a dynamic array doubles by, which keeps the
*total* cost of every [Tw.to_css] call across the app's lifetime O(n) in
distinct classes rather than O(n^2). Base (Preflight) is never recompiled per
delta - it does not depend on which utilities are present, so it is folded in
once, at each consolidation. *)openBrrletregistered:(string,unit)Hashtbl.t=Hashtbl.create128(* Every utility ever registered, newest-first (prepended in O(1)); reversed
only at a consolidation. *)letall_styles:Tw.tlistref=ref[]letall_styles_count=ref0(* Styles [use] has registered since the last [flush], newest-first. *)letsince_flush:Tw.tlistref=ref[](* The prefix of [all_styles] (by count) already folded into [main_el] by a
from-scratch compile. *)letconsolidated_count=ref0letmain_el:El.toptionref=refNoneletdelta_els:El.tlistref=ref[]letinclude_base:boolref=reftrueletmk_style_el()=letel=El.vEl.Name.style[]inEl.set_at(Jstr.v"data-tw")(Some(Jstr.v"runtime"))el;elletensure_style_el()=match!main_elwith|Someel->el|None->lethead=Document.headG.documentinletel=mk_style_el()inEl.append_childrenhead[el];main_el:=Someel;elletset_text_contentels=El.set_childrenel[El.txt(Jstr.vs)](* Fold everything registered so far into [main_el] with one from-scratch
compile, matching exactly what a full rebuild produces today, and drop the
delta elements it now supersedes. *)letconsolidate()=letel=ensure_style_el()inletstyles=List.rev!all_stylesinletcss=Tw.to_css~base:!include_basestylesinset_text_contentel(Css.to_string~minify:truecss);List.iterEl.remove!delta_els;delta_els:=[];consolidated_count:=!all_styles_count(* Compile and inject just [batch] (this tick's new styles) as its own small
element, appended after everything already in the document. [base] is never
included here - see the module comment - so a batch this only picks up
utility rules, folded into the canonical, deduplicated compile at the next
consolidation. *)letinject_deltabatch=lethead=Document.headG.documentinletcss=Tw.to_css~base:falsebatchinletel=mk_style_el()inEl.append_childrenhead[el];set_text_contentel(Css.to_string~minify:truecss);delta_els:=!delta_els@[el]letpending:boolref=reffalseletflush()=if!pendingthen(pending:=false;letbatch=List.rev!since_flushinsince_flush:=[];ifbatch<>[]then(inject_deltabatch;letpending_count=!all_styles_count-!consolidated_countinifpending_count>=max1!consolidated_countthenconsolidate()))(* Coalesced onto a microtask, which the browser drains at the end of the
current task and before it paints, so a mount pass calling [use] many times
still injects once. *)letschedule_rebuild()=ifnot!pendingthen(pending:=true;Fut.await(Fut.return())flush)letinit?(base=true)()=include_base:=base;ignore(ensure_style_el())letusestyles=letnew_found=reffalseinList.iter(funs->letcls=Tw.ppsinifnot(Hashtbl.memregisteredcls)then(Hashtbl.addregisteredcls();all_styles:=s::!all_styles;incrall_styles_count;since_flush:=s::!since_flush;new_found:=true))styles;if!new_foundthenschedule_rebuild();Tw.to_classesstyles(* Names [use_str] was given that are no utility, newest-first and each recorded
once. Rendering must not stop for one: a class attribute legitimately carries
names this library knows nothing about, and a browser cannot tell those from
a typo any better than the parser can. The name reaches the element either
way; this list is what makes the typo findable. *)letunknown_seen:(string,unit)Hashtbl.t=Hashtbl.create16letunknown_rev:stringlistref=ref[]letunknown_classes()=List.rev!unknown_revletuse_strs=letstyles,unknown=Tw.of_classessinignore(usestyles);List.iter(funcls->ifnot(Hashtbl.memunknown_seencls)then(Hashtbl.addunknown_seencls();unknown_rev:=cls::!unknown_rev))unknown;sletcss()=letstyles=List.rev!all_stylesinletcss=Tw.to_css~base:!include_basestylesinCss.to_string~minify:truecss