1use crate::interpreter::RuntimeValue;
18use std::cell::RefCell;
19use std::collections::BTreeMap;
20
21pub const FETCH_REQUEST_BODY_CAP: usize = 4 * 1024 * 1024;
25
26pub const FETCH_RESPONSE_BODY_CAP: usize = 4 * 1024 * 1024;
29
30pub const FETCH_ALLOWED_REQUEST_HEADERS: &[&str] = &[
35 "accept",
36 "accept-language",
37 "authorization",
38 "cache-control",
39 "content-type",
40 "idempotency-key",
41 "if-match",
42 "if-none-match",
43 "x-csrf-token",
44 "x-expected-version",
45 "x-requested-with",
46];
47
48pub const CSRF_META_NAME: &str = logicaffeine_system::csrf::CSRF_META_NAME;
51
52pub fn changes_state(method: &str) -> bool {
55 ["POST", "PUT", "PATCH", "DELETE"]
56 .iter()
57 .any(|m| m.eq_ignore_ascii_case(method))
58}
59
60pub fn to_own_server(url: &str) -> bool {
63 !url.contains("://") && !url.starts_with("//")
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum DomOp {
71 CreateElement {
72 node: u32,
73 tag: String,
74 },
75 CreateTextNode {
76 node: u32,
77 text: String,
78 },
79 SetText {
80 node: u32,
81 text: String,
82 },
83 SetHtml {
84 node: u32,
85 html: String,
86 },
87 ClearChildren {
88 node: u32,
89 },
90 SetAttribute {
91 node: u32,
92 name: String,
93 value: String,
94 },
95 AppendChild {
96 parent: u32,
97 child: u32,
98 },
99 SetValue {
100 node: u32,
101 value: String,
102 },
103 SetStream {
106 node: u32,
107 stream: i64,
108 },
109 RtcPeerConnection {
119 pc: u32,
120 },
121 RtcDataChannel {
122 pc: u32,
123 channel: u32,
124 label: String,
125 },
126 RtcCreateOffer {
127 pc: u32,
128 },
129 RtcCreateAnswer {
130 pc: u32,
131 },
132 RtcSetLocal {
133 pc: u32,
134 sdp: String,
135 },
136 RtcSetRemote {
137 pc: u32,
138 sdp: String,
139 },
140 RtcAddIce {
141 pc: u32,
142 candidate: String,
143 },
144 RtcSend {
145 channel: u32,
146 bytes: u64,
147 },
148 AddEventListener {
149 node: u32,
150 event: String,
151 },
152 SetChecked {
161 node: u32,
162 checked: bool,
163 },
164 PreventDefault {
172 node: u32,
173 event: String,
174 },
175 RemoveNode {
179 node: u32,
180 },
181 InsertBefore {
187 parent: u32,
188 child: u32,
189 before: u32,
190 },
191 RemoveAttribute {
193 node: u32,
194 name: String,
195 },
196 Adopt {
200 parent: u32,
201 node: u32,
202 tag: String,
203 },
204 AdoptText {
208 parent: u32,
209 node: u32,
210 text: String,
211 },
212 Listen {
217 node: u32,
218 event: String,
219 flags: u8,
220 },
221 Unlisten {
223 node: u32,
224 event: String,
225 },
226 FreeNode {
233 node: u32,
234 },
235 Hydrate {
239 root: u32,
240 },
241}
242
243#[derive(Clone, Debug, PartialEq, Eq)]
247pub enum RouteOp {
248 Navigate {
249 path: String,
250 replace: bool,
251 },
252 Load {
254 path: String,
255 },
256}
257
258#[derive(Clone, Debug, Default)]
259struct Node {
260 tag: String,
261 text: String,
262 html: String,
264 value: String,
265 checked: bool,
268 attrs: BTreeMap<String, String>,
269 children: Vec<u32>,
270 is_text: bool,
271 user_edited: bool,
274 bad_input: bool,
277}
278
279impl Node {
280 fn focusable(&self) -> bool {
282 if self.is_text {
283 return false;
284 }
285 let has = |name: &str| self.attrs.contains_key(name);
286 let enabled = !has("disabled");
287 if has("tabindex") {
288 return true;
289 }
290 if self
291 .attrs
292 .get("contenteditable")
293 .is_some_and(|v| !v.eq_ignore_ascii_case("false"))
294 {
295 return true;
296 }
297 match self.tag.to_ascii_lowercase().as_str() {
298 "button" | "select" | "textarea" => enabled,
299 "input" => {
300 enabled
301 && !self
302 .attrs
303 .get("type")
304 .is_some_and(|t| t.eq_ignore_ascii_case("hidden"))
305 }
306 "a" | "area" => has("href"),
307 "iframe" | "summary" => true,
308 "audio" | "video" => has("controls"),
309 _ => false,
310 }
311 }
312}
313
314use logicaffeine_language::lgx::selector::{self, Compound};
318
319#[derive(Clone, Debug)]
322pub struct DomRecorder {
323 nodes: Vec<Node>,
324 streamed: Vec<Option<Node>>,
326 log: Vec<DomOp>,
327 listeners: Vec<(u32, String, RuntimeValue)>,
330 registrations: BTreeMap<(u32, String), u8>,
335 head: Vec<HeadTag>,
340 clipboard: String,
347 downloads: Vec<(String, String)>,
348 notifications: Vec<(String, String)>,
349 published: Vec<(String, String)>,
352 scrolled_to: u32,
360 selected: u32,
361 focused: u32,
364 raised: Vec<(String, &'static str)>,
367 dropped_file: Option<DroppedFile>,
370 popped: bool,
373 reload_requested: bool,
376 load_requested: Option<String>,
380 current_event: Option<CurrentEvent>,
382 fetches: BTreeMap<i64, PendingFetch>,
386 next_fetch: i64,
388 fetch_stubs: Vec<((String, String), (i64, String))>,
395 fetch_responses: BTreeMap<i64, FetchResponse>,
400 current_fetch: Option<i64>,
402 route_path: String,
410 route_query: String,
411 route_ops: Vec<RouteOp>,
412 route_history: Vec<String>,
417 route_handlers: Vec<RuntimeValue>,
418 hydration: Option<Hydration>,
420 forwarded: BTreeMap<u32, u32>,
424}
425
426#[derive(Clone, Debug, Default)]
428struct Hydration {
429 cursors: BTreeMap<u32, Option<u32>>,
432 texts: Vec<(u32, u32, String)>,
435 raw: std::collections::BTreeSet<u32>,
437 written: BTreeMap<u32, std::collections::BTreeSet<String>>,
441 mismatches: i64,
443}
444
445#[derive(Clone, Debug)]
446struct CurrentEvent {
447 target: u32,
448 value: String,
449 key: String,
452}
453
454#[derive(Clone, Debug)]
456struct PendingFetch {
457 method: String,
458 url: String,
459 body: String,
460 headers: Vec<(String, String)>,
461 closure: RuntimeValue,
462 timeout_ms: i64,
463 aborted: bool,
464}
465
466#[derive(Clone, Debug, Default)]
469struct FetchResponse {
470 status: i64,
471 headers: Vec<(String, String)>,
472 body: String,
473 error: String,
474}
475
476#[derive(Clone, Debug, PartialEq, Eq)]
479pub struct FetchRequest {
480 pub req: i64,
481 pub method: String,
482 pub url: String,
483 pub body: String,
484 pub headers: Vec<(String, String)>,
485 pub timeout_ms: i64,
486 pub aborted: bool,
487}
488
489impl Default for DomRecorder {
490 fn default() -> Self {
491 DomRecorder {
494 head: Vec::new(),
495 focused: u32::MAX,
496 clipboard: String::new(),
497 downloads: Vec::new(),
498 notifications: Vec::new(),
499 published: Vec::new(),
500 scrolled_to: u32::MAX,
501 selected: u32::MAX,
502 raised: Vec::new(),
503 dropped_file: None,
504 popped: false,
505 reload_requested: false,
506 load_requested: None,
507 fetch_stubs: Vec::new(),
508 nodes: vec![Node {
509 tag: "body".to_string(),
510 ..Node::default()
511 }],
512 streamed: Vec::new(),
513 log: Vec::new(),
514 listeners: Vec::new(),
515 registrations: BTreeMap::new(),
516 current_event: None,
517 fetches: BTreeMap::new(),
518 next_fetch: 1,
519 fetch_responses: BTreeMap::new(),
520 current_fetch: None,
521 route_path: "/".to_string(),
523 route_query: String::new(),
524 route_ops: Vec::new(),
525 route_history: vec!["/".to_string()],
526 route_handlers: Vec::new(),
527 hydration: None,
528 forwarded: BTreeMap::new(),
529 }
530 }
531}
532
533impl DomRecorder {
534 fn get(&self, h: u32) -> Option<&Node> {
535 let h = self.resolve(h);
536 match h.checked_sub(STREAM_ID_BASE) {
537 Some(i) => self.streamed.get(i as usize).and_then(Option::as_ref),
538 None => self.nodes.get(h as usize),
539 }
540 }
541 fn get_mut(&mut self, h: u32) -> Option<&mut Node> {
542 let h = self.resolve(h);
543 match h.checked_sub(STREAM_ID_BASE) {
544 Some(i) => self.streamed.get_mut(i as usize).and_then(Option::as_mut),
545 None => self.nodes.get_mut(h as usize),
546 }
547 }
548
549 fn resolve(&self, h: u32) -> u32 {
551 self.forwarded.get(&h).copied().unwrap_or(h)
552 }
553
554 fn body(&self) -> u32 {
555 0
556 }
557
558 fn create_as(&mut self, node: u32, fresh: Node) -> Result<(), String> {
561 match node.checked_sub(STREAM_ID_BASE) {
562 Some(i) => {
563 let i = i as usize;
564 if self.streamed.len() <= i {
565 self.streamed.resize_with(i + 1, || None);
566 }
567 if self.streamed[i].is_some() {
568 return Err(format!(
569 "the stream creates node {node}, which is still live"
570 ));
571 }
572 self.streamed[i] = Some(fresh);
573 }
574 None if node as usize == self.nodes.len() => self.nodes.push(fresh),
575 None => {
576 return Err(format!(
577 "the stream names node {node} for a new node, and this host mints its own ids \
578 below {STREAM_ID_BASE}: the next is {}",
579 self.nodes.len()
580 ))
581 }
582 }
583 Ok(())
584 }
585
586 fn create_element_as(&mut self, node: u32, tag: &str) -> Result<(), String> {
587 self.create_as(
588 node,
589 Node {
590 tag: tag.to_string(),
591 ..Node::default()
592 },
593 )?;
594 self.log.push(DomOp::CreateElement {
595 node,
596 tag: tag.to_string(),
597 });
598 Ok(())
599 }
600
601 fn create_text_node_as(&mut self, node: u32, text: &str) -> Result<(), String> {
602 self.create_as(
603 node,
604 Node {
605 text: text.to_string(),
606 is_text: true,
607 ..Node::default()
608 },
609 )?;
610 self.log.push(DomOp::CreateTextNode {
611 node,
612 text: text.to_string(),
613 });
614 Ok(())
615 }
616
617 fn create_element(&mut self, tag: &str) -> u32 {
618 let h = self.nodes.len() as u32;
619 self.nodes.push(Node {
620 tag: tag.to_string(),
621 ..Node::default()
622 });
623 self.log.push(DomOp::CreateElement {
624 node: h,
625 tag: tag.to_string(),
626 });
627 h
628 }
629
630 fn create_text_node(&mut self, text: &str) -> u32 {
631 let h = self.nodes.len() as u32;
632 self.nodes.push(Node {
633 text: text.to_string(),
634 is_text: true,
635 ..Node::default()
636 });
637 self.log.push(DomOp::CreateTextNode {
638 node: h,
639 text: text.to_string(),
640 });
641 h
642 }
643
644 fn set_text(&mut self, node: u32, text: &str) {
645 if let Some(n) = self.get_mut(node) {
646 n.text = text.to_string();
647 }
648 self.log.push(DomOp::SetText {
649 node,
650 text: text.to_string(),
651 });
652 }
653
654 fn set_html(&mut self, node: u32, html: &str) {
655 if self.adopting(node) {
656 self.hydration.as_mut().map(|h| h.raw.insert(node));
657 }
658 if let Some(n) = self.get_mut(node) {
659 n.html = html.to_string();
660 }
661 self.log.push(DomOp::SetHtml {
662 node,
663 html: html.to_string(),
664 });
665 }
666
667 fn clear_children(&mut self, node: u32) {
670 if let Some(n) = self.get_mut(node) {
671 n.children.clear();
672 n.html.clear();
673 }
674 self.log.push(DomOp::ClearChildren { node });
675 }
676
677 fn set_attribute(&mut self, node: u32, name: &str, value: &str) {
678 if let Some(written) = self
679 .hydration
680 .as_mut()
681 .and_then(|h| h.written.get_mut(&node))
682 {
683 written.insert(name.to_string());
684 }
685 if let Some(n) = self.get_mut(node) {
686 n.attrs.insert(name.to_string(), value.to_string());
687 }
688 self.log.push(DomOp::SetAttribute {
689 node,
690 name: name.to_string(),
691 value: value.to_string(),
692 });
693 }
694
695 fn append_child(&mut self, parent: u32, child: u32) {
696 let child = self.resolve(child);
697 if let Some(n) = self.get_mut(parent) {
698 n.children.push(child);
699 }
700 self.log.push(DomOp::AppendChild { parent, child });
701 }
702
703 fn set_value(&mut self, node: u32, value: &str) {
704 if self.adopting(node) {
705 if self.get(node).is_some_and(|n| n.user_edited) {
706 self.keep_the_visitors(node);
707 self.log.push(DomOp::SetValue {
708 node,
709 value: value.to_string(),
710 });
711 return;
712 }
713 }
714 if let Some(n) = self.get_mut(node) {
715 if n.value != value {
716 n.user_edited = false;
717 n.bad_input = false;
718 }
719 n.value = value.to_string();
720 }
721 self.log.push(DomOp::SetValue {
722 node,
723 value: value.to_string(),
724 });
725 }
726
727 fn options_of(&self, select: u32) -> Vec<(String, bool)> {
730 let mut out = Vec::new();
731 let mut stack: Vec<u32> = self
732 .get(select)
733 .map(|n| n.children.iter().rev().copied().collect())
734 .unwrap_or_default();
735 while let Some(h) = stack.pop() {
736 let Some(n) = self.get(h) else { continue };
737 if n.tag.eq_ignore_ascii_case("option") {
738 let value = n
739 .attrs
740 .get("value")
741 .cloned()
742 .unwrap_or_else(|| self.text_content(h));
743 out.push((value, n.attrs.contains_key("selected")));
744 } else {
745 stack.extend(n.children.iter().rev().copied());
746 }
747 }
748 out
749 }
750
751 fn default_choice(&self, select: u32) -> Option<String> {
754 let options = self.options_of(select);
755 options
756 .iter()
757 .find(|(_, selected)| *selected)
758 .or(options.first())
759 .map(|(v, _)| v.clone())
760 }
761
762 fn option_offering(&self, select: u32, wanted: &str) -> Option<String> {
764 if !self
765 .get(select)
766 .is_some_and(|n| n.tag.eq_ignore_ascii_case("select"))
767 {
768 return None;
769 }
770 let options = self.options_of(select);
771 if options.iter().any(|(v, _)| v == wanted) {
772 return Some(wanted.to_string());
773 }
774 let mut stack: Vec<u32> = self
775 .get(select)
776 .map(|n| n.children.clone())
777 .unwrap_or_default();
778 while let Some(h) = stack.pop() {
779 let Some(n) = self.get(h) else { continue };
780 if n.tag.eq_ignore_ascii_case("option") && self.text_content(h) == wanted {
781 return Some(
782 n.attrs
783 .get("value")
784 .cloned()
785 .unwrap_or_else(|| wanted.to_string()),
786 );
787 }
788 stack.extend(n.children.iter().copied());
789 }
790 None
791 }
792
793 fn set_checked(&mut self, node: u32, checked: bool) {
794 if self.adopting(node) {
795 if self
796 .get(node)
797 .is_some_and(|n| n.checked != n.attrs.contains_key("checked"))
798 {
799 self.keep_the_visitors(node);
800 self.log.push(DomOp::SetChecked { node, checked });
801 return;
802 }
803 }
804 if let Some(n) = self.get_mut(node) {
805 n.checked = checked;
806 }
807 self.log.push(DomOp::SetChecked { node, checked });
808 }
809
810 fn set_stream(&mut self, node: u32, stream: i64) {
811 self.log.push(DomOp::SetStream { node, stream });
812 }
813
814 fn rtc_peer_connection(&mut self) -> u32 {
815 let h = self.nodes.len() as u32;
816 self.nodes.push(Node::default());
817 self.log.push(DomOp::RtcPeerConnection { pc: h });
818 h
819 }
820
821 fn rtc_data_channel(&mut self, pc: u32, label: &str) -> u32 {
822 let h = self.nodes.len() as u32;
823 self.nodes.push(Node::default());
824 self.log.push(DomOp::RtcDataChannel {
825 pc,
826 channel: h,
827 label: label.to_string(),
828 });
829 h
830 }
831
832 fn rtc_create_offer(&mut self, pc: u32) {
833 self.log.push(DomOp::RtcCreateOffer { pc });
834 }
835
836 fn rtc_create_answer(&mut self, pc: u32) {
837 self.log.push(DomOp::RtcCreateAnswer { pc });
838 }
839
840 fn rtc_set_local(&mut self, pc: u32, sdp: &str) {
841 self.log.push(DomOp::RtcSetLocal {
842 pc,
843 sdp: sdp.to_string(),
844 });
845 }
846
847 fn rtc_set_remote(&mut self, pc: u32, sdp: &str) {
848 self.log.push(DomOp::RtcSetRemote {
849 pc,
850 sdp: sdp.to_string(),
851 });
852 }
853
854 fn rtc_add_ice(&mut self, pc: u32, candidate: &str) {
855 self.log.push(DomOp::RtcAddIce {
856 pc,
857 candidate: candidate.to_string(),
858 });
859 }
860
861 fn rtc_send(&mut self, channel: u32, bytes: u64) {
862 self.log.push(DomOp::RtcSend { channel, bytes });
863 }
864
865 fn get_text(&self, node: u32) -> String {
866 self.get(node).map(|n| n.text.clone()).unwrap_or_default()
867 }
868
869 fn get_value(&self, node: u32) -> String {
870 self.get(node).map(|n| n.value.clone()).unwrap_or_default()
871 }
872
873 fn get_checked(&self, node: u32) -> bool {
874 self.get(node).is_some_and(|n| n.checked)
875 }
876
877 fn toggle_control(&self, node: u32) -> Option<ToggleControl> {
882 let n = self.get(node)?;
883 if n.tag != "input" {
884 return None;
885 }
886 match n.attrs.get("type").map(String::as_str) {
887 Some("checkbox") => Some(ToggleControl::Checkbox),
888 Some("radio") => Some(ToggleControl::Radio),
889 _ => None,
890 }
891 }
892
893 fn text_content(&self, node: u32) -> String {
897 let mut out = String::new();
898 fn go(dom: &DomRecorder, h: u32, out: &mut String) {
899 let Some(n) = dom.get(h) else { return };
900 out.push_str(&n.text);
901 for &c in &n.children {
902 go(dom, c, out);
903 }
904 }
905 go(self, node, &mut out);
906 out
907 }
908
909 fn query_selector_all(&self, selector: &str) -> Vec<u32> {
915 if let Some(place) = selector.strip_prefix(PLACE) {
916 return self.at_place(place).into_iter().collect();
917 }
918 let Some(parts) = selector::parse(selector) else {
919 return Vec::new();
920 };
921 let mut out = Vec::new();
922 let mut ancestors = Vec::new();
923 self.collect(0, &parts, &mut ancestors, &mut out);
924 out
925 }
926
927 fn collect(&self, h: u32, parts: &[Compound], ancestors: &mut Vec<u32>, out: &mut Vec<u32>) {
930 let Some(n) = self.get(h) else { return };
931 if !n.is_text && self.matches_chain(h, parts, ancestors) {
932 out.push(h);
933 }
934 ancestors.push(h);
935 for &c in &n.children {
936 self.collect(c, parts, ancestors, out);
937 }
938 ancestors.pop();
939 }
940
941 fn ancestors_of(&self, target: u32) -> Option<Vec<u32>> {
943 fn walk(d: &DomRecorder, target: u32, h: u32, path: &mut Vec<u32>) -> bool {
944 if h == target {
945 return true;
946 }
947 let Some(n) = d.get(h) else { return false };
948 path.push(h);
949 for &c in &n.children {
950 if walk(d, target, c, path) {
951 return true;
952 }
953 }
954 path.pop();
955 false
956 }
957 let mut path = Vec::new();
958 walk(self, target, 0, &mut path).then_some(path)
959 }
960
961 fn place_of(&self, target: u32) -> Option<(Vec<usize>, String)> {
964 let node = self.get(target).filter(|n| !n.is_text && target != 0)?;
965 let chain = self.ancestors_of(target)?;
966 let mut indices = Vec::with_capacity(chain.len());
967 for (i, &parent) in chain.iter().enumerate() {
968 let child = chain.get(i + 1).copied().unwrap_or(target);
969 indices.push(
970 self.get(parent)?
971 .children
972 .iter()
973 .position(|&c| c == child)?,
974 );
975 }
976 Some((indices, node.tag.to_ascii_lowercase()))
977 }
978
979 fn element_at(&self, indices: &[usize], tag: &str) -> Option<u32> {
981 let mut at = 0u32;
982 for &i in indices {
983 at = *self.get(at)?.children.get(i)?;
984 }
985 self.get(at)
986 .filter(|n| !n.is_text && n.tag.eq_ignore_ascii_case(tag))
987 .map(|_| at)
988 }
989
990 fn at_place(&self, place: &str) -> Option<u32> {
992 let (path, tag) = place.rsplit_once(':')?;
993 let indices: Vec<usize> = match path {
994 "" => Vec::new(),
995 path => path
996 .split('/')
997 .map(str::parse)
998 .collect::<Result<_, _>>()
999 .ok()?,
1000 };
1001 self.element_at(&indices, tag)
1002 }
1003
1004 fn drop_detached_focus(&mut self) {
1007 if self.focused != u32::MAX && self.ancestors_of(self.focused).is_none() {
1008 self.focused = u32::MAX;
1009 }
1010 }
1011
1012 fn holds(&self, outer: u32, inner: u32) -> bool {
1014 let mut stack = vec![outer];
1015 while let Some(h) = stack.pop() {
1016 if h == inner {
1017 return true;
1018 }
1019 if let Some(n) = self.get(h) {
1020 stack.extend(n.children.iter().copied());
1021 }
1022 }
1023 false
1024 }
1025
1026 fn leaving(&mut self, node: u32) {
1032 if self.focused != u32::MAX && self.holds(node, self.focused) {
1033 self.focused = u32::MAX;
1034 }
1035 }
1036
1037 fn shown(&self, target: u32) -> bool {
1051 let Some(chain) = self.ancestors_of(target) else {
1052 return false;
1053 };
1054 let hiding = self.hiding_rules();
1055 for h in chain.iter().copied().chain(std::iter::once(target)) {
1056 if h == 0 {
1057 continue;
1058 }
1059 let Some(n) = self.get(h) else { return false };
1060 if n.is_text {
1061 continue;
1062 }
1063 if n.attrs.contains_key("hidden") {
1064 return false;
1065 }
1066 if n.attrs.get("style").is_some_and(|v| declares_hidden(v)) {
1067 return false;
1068 }
1069 if hiding.iter().any(|parts| {
1070 self.ancestors_of(h)
1071 .is_some_and(|a| self.matches_chain(h, parts, &a))
1072 }) {
1073 return false;
1074 }
1075 }
1076 true
1077 }
1078
1079 fn hiding_rules(&self) -> Vec<Vec<Compound>> {
1084 let mut out = Vec::new();
1085 let mut sheets = Vec::new();
1086 self.collect_stylesheets(0, &mut sheets);
1087 for css in sheets {
1088 let Ok(items) = logicaffeine_language::lgx::css::parse_items(&css) else {
1089 continue;
1090 };
1091 for item in items {
1092 let logicaffeine_language::lgx::css::CssItem::Rule { prelude, body } = item else {
1093 continue;
1094 };
1095 if !declares_hidden(&body) {
1096 continue;
1097 }
1098 for one in prelude.split(',') {
1101 if let Some(parts) = selector::parse(one.trim()) {
1102 out.push(parts);
1103 }
1104 }
1105 }
1106 }
1107 out
1108 }
1109
1110 fn collect_stylesheets(&self, h: u32, out: &mut Vec<String>) {
1113 let Some(n) = self.get(h) else { return };
1114 if n.tag == "style" {
1115 out.push(self.text_content(h));
1116 }
1117 for &c in &n.children {
1118 self.collect_stylesheets(c, out);
1119 }
1120 }
1121
1122 fn matches_chain(&self, h: u32, parts: &[Compound], ancestors: &[u32]) -> bool {
1124 let Some((last, rest)) = parts.split_last() else {
1125 return false;
1126 };
1127 if !self.matches_compound(h, last) {
1128 return false;
1129 }
1130 let mut i = ancestors.len();
1132 for want in rest.iter().rev() {
1133 let mut found = false;
1134 while i > 0 {
1135 i -= 1;
1136 if self.matches_compound(ancestors[i], want) {
1137 found = true;
1138 break;
1139 }
1140 }
1141 if !found {
1142 return false;
1143 }
1144 }
1145 true
1146 }
1147
1148 fn matches_compound(&self, h: u32, c: &Compound) -> bool {
1149 let Some(n) = self.get(h) else { return false };
1150 if let Some(tag) = &c.tag {
1151 if &n.tag != tag {
1152 return false;
1153 }
1154 }
1155 if let Some(id) = &c.id {
1156 if n.attrs.get("id") != Some(id) {
1157 return false;
1158 }
1159 }
1160 for want in &c.classes {
1161 let has = n
1162 .attrs
1163 .get("class")
1164 .is_some_and(|v| v.split_whitespace().any(|k| k == want));
1165 if !has {
1166 return false;
1167 }
1168 }
1169 for (name, value) in &c.attrs {
1170 match (n.attrs.get(name), value) {
1171 (None, _) => return false,
1172 (Some(_), None) => {}
1173 (Some(have), Some(want)) if have == want => {}
1174 _ => return false,
1175 }
1176 }
1177 true
1178 }
1179
1180 fn query_selector(&self, selector: &str) -> u32 {
1182 self.query_selector_all(selector)
1183 .first()
1184 .copied()
1185 .unwrap_or(u32::MAX)
1186 }
1187
1188 fn add_event_listener(&mut self, node: u32, event: &str, closure: RuntimeValue) {
1189 self.register(node, event);
1190 self.listeners.push((node, event.to_string(), closure));
1191 self.log.push(DomOp::AddEventListener {
1192 node,
1193 event: event.to_string(),
1194 });
1195 }
1196
1197 fn listener_for(&self, node: u32, event: &str) -> Option<RuntimeValue> {
1198 self.listeners
1199 .iter()
1200 .find(|(n, e, _)| *n == node && e == event)
1201 .map(|(_, _, c)| c.clone())
1202 }
1203
1204 fn fetch_send(
1211 &mut self,
1212 method: &str,
1213 url: &str,
1214 body: &str,
1215 headers: Vec<(String, String)>,
1216 closure: RuntimeValue,
1217 ) -> i64 {
1218 let headers = self.request_headers(method, url, headers);
1219 let req = self.next_fetch;
1220 self.next_fetch += 1;
1221 let mut prefail: Option<String> = None;
1222 if body.len() > FETCH_REQUEST_BODY_CAP {
1223 prefail = Some("toolarge".to_string());
1224 } else if let Some((name, _)) = headers.iter().find(|(n, _)| {
1225 !FETCH_ALLOWED_REQUEST_HEADERS.contains(&n.to_ascii_lowercase().as_str())
1226 }) {
1227 prefail = Some(format!(
1228 "network: header not allowed: {}",
1229 name.to_ascii_lowercase()
1230 ));
1231 }
1232 self.fetches.insert(
1233 req,
1234 PendingFetch {
1235 method: method.to_string(),
1236 url: url.to_string(),
1237 body: body.to_string(),
1238 headers,
1239 closure,
1240 timeout_ms: 0,
1241 aborted: false,
1242 },
1243 );
1244 if let Some(error) = prefail {
1245 self.fetch_responses.insert(
1246 req,
1247 FetchResponse {
1248 error,
1249 ..FetchResponse::default()
1250 },
1251 );
1252 }
1253 req
1254 }
1255
1256 fn request_headers(
1261 &self,
1262 method: &str,
1263 url: &str,
1264 mut headers: Vec<(String, String)>,
1265 ) -> Vec<(String, String)> {
1266 let header = logicaffeine_language::parser::lgx_lower::CSRF_HEADER;
1267 let sets_one = headers.iter().any(|(n, _)| n.eq_ignore_ascii_case(header));
1268 if !changes_state(method) || !to_own_server(url) || sets_one {
1269 return headers;
1270 }
1271 let token = self.head.iter().find(|t| {
1272 t.tag == "meta"
1273 && t.attrs
1274 .get("name")
1275 .is_some_and(|n| n.eq_ignore_ascii_case(CSRF_META_NAME))
1276 });
1277 if let Some(token) = token.and_then(|t| t.attrs.get("content")) {
1278 headers.push((header.to_string(), token.clone()));
1279 }
1280 headers
1281 }
1282
1283 fn fetch_abort(&mut self, req: i64) {
1286 if let Some(p) = self.fetches.get_mut(&req) {
1287 p.aborted = true;
1288 }
1289 self.fetch_responses
1290 .entry(req)
1291 .or_insert_with(|| FetchResponse {
1292 error: "abort".to_string(),
1293 ..FetchResponse::default()
1294 });
1295 }
1296
1297 fn fetch_set_timeout(&mut self, req: i64, millis: i64) {
1301 if let Some(p) = self.fetches.get_mut(&req) {
1302 p.timeout_ms = millis.max(0);
1303 }
1304 }
1305
1306 fn fetch_resolve(
1311 &mut self,
1312 req: i64,
1313 status: i64,
1314 headers: &[(String, String)],
1315 body: &str,
1316 ) -> Option<RuntimeValue> {
1317 if self.fetch_responses.contains_key(&req) {
1318 return None;
1319 }
1320 let closure = self.fetches.get(&req).map(|p| p.closure.clone())?;
1321 let response = if body.len() > FETCH_RESPONSE_BODY_CAP {
1322 FetchResponse {
1323 error: "toolarge".to_string(),
1324 ..FetchResponse::default()
1325 }
1326 } else {
1327 FetchResponse {
1328 status,
1329 headers: headers.to_vec(),
1330 body: body.to_string(),
1331 error: String::new(),
1332 }
1333 };
1334 self.fetch_responses.insert(req, response);
1335 self.current_fetch = Some(req);
1336 Some(closure)
1337 }
1338
1339 fn fetch_fail(&mut self, req: i64, error: &str) -> Option<RuntimeValue> {
1343 if self.fetch_responses.contains_key(&req) {
1344 return None;
1345 }
1346 let closure = self.fetches.get(&req).map(|p| p.closure.clone())?;
1347 self.fetch_responses.insert(
1348 req,
1349 FetchResponse {
1350 error: error.to_string(),
1351 ..FetchResponse::default()
1352 },
1353 );
1354 self.current_fetch = Some(req);
1355 Some(closure)
1356 }
1357
1358 fn fetch_response(&self, req: i64) -> Option<&FetchResponse> {
1359 self.fetch_responses.get(&req)
1360 }
1361
1362 fn fetch_requests(&self) -> Vec<FetchRequest> {
1363 self.fetches
1364 .iter()
1365 .map(|(&req, p)| FetchRequest {
1366 req,
1367 method: p.method.clone(),
1368 url: p.url.clone(),
1369 body: p.body.clone(),
1370 headers: p.headers.clone(),
1371 timeout_ms: p.timeout_ms,
1372 aborted: p.aborted,
1373 })
1374 .collect()
1375 }
1376
1377 fn navigate(&mut self, path: &str, replace: bool) {
1383 self.route_ops.push(RouteOp::Navigate {
1384 path: path.to_string(),
1385 replace,
1386 });
1387 self.route_path = path.to_string();
1388 if replace {
1391 let last = self.route_history.len() - 1;
1392 self.route_history[last] = path.to_string();
1393 } else {
1394 self.route_history.push(path.to_string());
1395 }
1396 }
1397
1398 fn go_back(&mut self) -> Option<String> {
1401 if self.route_history.len() <= 1 {
1402 return None;
1403 }
1404 self.route_history.pop();
1405 let path = self
1406 .route_history
1407 .last()
1408 .cloned()
1409 .unwrap_or_else(|| "/".to_string());
1410 self.route_path = path.clone();
1411 self.popped = true;
1412 Some(path)
1413 }
1414
1415 fn route_query_value(&self, name: &str) -> String {
1418 for pair in self.route_query.split('&') {
1419 if let Some((k, v)) = pair.split_once('=') {
1420 if k == name {
1421 return v.to_string();
1422 }
1423 }
1424 }
1425 String::new()
1426 }
1427
1428 fn render(&self) -> String {
1431 let mut out = String::new();
1432 self.render_node(0, &mut out, Spelling::Recorded);
1433 out
1434 }
1435
1436 fn render_served(&self) -> String {
1437 let mut out = String::new();
1438 self.render_node(0, &mut out, Spelling::Served { chosen: None });
1439 out
1440 }
1441
1442 fn option_value(&self, h: u32) -> String {
1444 self.get(h)
1445 .and_then(|n| n.attrs.get("value").cloned())
1446 .unwrap_or_else(|| self.text_content(h))
1447 }
1448
1449 fn render_node(&self, h: u32, out: &mut String, spelling: Spelling) {
1450 let Some(n) = self.get(h) else { return };
1451 if n.is_text {
1452 escape_text(&n.text, out);
1453 return;
1454 }
1455 let served = matches!(spelling, Spelling::Served { .. });
1456 let option = n.tag.eq_ignore_ascii_case("option");
1457 let chosen = match spelling {
1458 Spelling::Served { chosen: Some(v) } if option => Some(self.option_value(h) == v),
1459 _ => None,
1460 };
1461 out.push('<');
1462 out.push_str(&n.tag);
1463 for (k, v) in &n.attrs {
1464 if chosen.is_some() && k.eq_ignore_ascii_case("selected") {
1465 continue;
1466 }
1467 out.push_str(&format!(" {k}=\""));
1468 escape_attr(v, out);
1469 out.push('"');
1470 }
1471 if chosen == Some(true) {
1472 out.push_str(" selected");
1473 }
1474 if n.checked {
1475 out.push_str(" checked");
1478 }
1479 let select = n.tag.eq_ignore_ascii_case("select");
1483 let textarea = n.tag.eq_ignore_ascii_case("textarea");
1484 let shown = match select {
1485 true => self.default_choice(h).as_deref() != Some(n.value.as_str()),
1486 false => true,
1487 };
1488 if shown && !n.value.is_empty() && !(served && (select || textarea)) {
1491 out.push_str(" value=\"");
1492 escape_attr(&n.value, out);
1493 out.push('"');
1494 }
1495 out.push('>');
1496 let inner = match spelling {
1497 Spelling::Served { .. } if select && shown && !n.value.is_empty() => Spelling::Served {
1498 chosen: Some(n.value.as_str()),
1499 },
1500 other => other,
1501 };
1502 if logicaffeine_language::lgx::html::is_void(&n.tag) {
1510 return;
1511 }
1512 let raw = logicaffeine_language::lgx::html::is_raw_text(&n.tag);
1517 let drops_newline = served
1520 && ["pre", "listing", "textarea"]
1521 .iter()
1522 .any(|t| n.tag.eq_ignore_ascii_case(t));
1523 if served && textarea && !n.value.is_empty() {
1524 if drops_newline && n.value.starts_with('\n') {
1525 out.push('\n');
1526 }
1527 escape_text(&n.value, out);
1528 out.push_str(&format!("</{}>", n.tag));
1529 return;
1530 }
1531 let leading = if n.text.is_empty() {
1532 n.children
1533 .first()
1534 .and_then(|&c| self.get(c))
1535 .filter(|t| t.is_text)
1536 .map(|t| t.text.as_str())
1537 } else {
1538 Some(n.text.as_str())
1539 };
1540 if drops_newline && leading.is_some_and(|t| t.starts_with('\n')) {
1541 out.push('\n');
1542 }
1543 if raw {
1544 out.push_str(&n.text);
1545 } else {
1546 escape_text(&n.text, out);
1547 }
1548 out.push_str(&n.html);
1549 for &c in &n.children {
1550 match self.get(c) {
1551 Some(t) if raw && t.is_text => out.push_str(&t.text),
1552 _ => self.render_node(c, out, inner),
1553 }
1554 }
1555 out.push_str(&format!("</{}>", n.tag));
1556 }
1557}
1558
1559#[derive(Clone, Copy)]
1562enum Spelling<'v> {
1563 Recorded,
1564 Served {
1567 chosen: Option<&'v str>,
1568 },
1569}
1570
1571fn declares_hidden(css: &str) -> bool {
1578 let flat: String = css
1579 .chars()
1580 .filter(|c| !c.is_whitespace())
1581 .collect::<String>()
1582 .to_ascii_lowercase();
1583 flat.contains("display:none") || flat.contains("visibility:hidden")
1584}
1585
1586fn escape_text(s: &str, out: &mut String) {
1587 out.push_str(&logicaffeine_language::lgx::html::escape_text(s));
1588}
1589
1590fn escape_attr(s: &str, out: &mut String) {
1592 out.push_str(&logicaffeine_language::lgx::html::escape_attr(s));
1593}
1594
1595thread_local! {
1596 static DOM: RefCell<DomRecorder> = RefCell::new(DomRecorder::default());
1598 static SHELL: RefCell<Vec<HeadTag>> = const { RefCell::new(Vec::new()) };
1601}
1602
1603pub fn reset() {
1606 DOM.with(|d| {
1607 *d.borrow_mut() = DomRecorder {
1608 head: SHELL.with(|s| s.borrow().clone()),
1609 ..DomRecorder::default()
1610 }
1611 });
1612 HEAD_HTML.with(|h| *h.borrow_mut() = None);
1616}
1617
1618pub fn reload() {
1621 let shell = SHELL.with(|s| s.borrow().clone());
1622 DOM.with(|d| {
1623 let mut d = d.borrow_mut();
1624 let before = std::mem::take(&mut *d);
1625 *d = before.next_document(shell);
1626 });
1627 HEAD_HTML.with(|h| *h.borrow_mut() = None);
1628}
1629
1630pub fn assign_location(url: &str) {
1636 DOM.with(|d| {
1637 let mut d = d.borrow_mut();
1638 let before = std::mem::take(&mut *d);
1639 *d = before.next_document(Vec::new());
1640 });
1641 HEAD_HTML.with(|h| *h.borrow_mut() = None);
1642 move_location(url);
1643 DOM.with(|d| {
1644 d.borrow_mut().route_ops.push(RouteOp::Load {
1645 path: url.to_string(),
1646 })
1647 });
1648}
1649
1650fn move_location(url: &str) {
1653 DOM.with(|d| {
1654 let mut d = d.borrow_mut();
1655 d.route_history.push(url.to_string());
1656 d.route_path = url.to_string();
1657 d.route_query = url
1658 .split_once('?')
1659 .map(|(_, q)| q.split('#').next().unwrap_or("").to_string())
1660 .unwrap_or_default();
1661 });
1662}
1663
1664pub fn follow_response(url: &str, content_type: &str, body: &str) {
1672 let media = content_type
1673 .split(';')
1674 .next()
1675 .unwrap_or("")
1676 .trim()
1677 .to_ascii_lowercase();
1678 let lower = body.to_ascii_lowercase();
1679 let section = |tag: &str| {
1680 let open = lower
1681 .find(&format!("<{tag}"))
1682 .and_then(|at| lower[at..].find('>').map(|e| at + e + 1))?;
1683 let close = lower.find(&format!("</{tag}>")).filter(|c| *c >= open)?;
1684 Some(body[open..close].to_string())
1685 };
1686 let head = match media.as_str() {
1687 "text/html" => section("head").map(|h| parse_head(&h)).unwrap_or_default(),
1688 "application/json" => {
1689 parse_head(r#"<meta name="color-scheme" content="light dark"><meta charset="utf-8">"#)
1690 }
1691 _ => parse_head(r#"<meta name="color-scheme" content="light dark">"#),
1692 };
1693 DOM.with(|d| {
1694 let mut d = d.borrow_mut();
1695 let before = std::mem::take(&mut *d);
1696 *d = before.next_document(head);
1697 });
1698 HEAD_HTML.with(|h| *h.borrow_mut() = None);
1699 move_location(url);
1700 let root = self::body();
1701 let preformatted = |style: Option<&str>| {
1702 let pre = create_element("pre");
1703 if let Some(style) = style {
1704 set_attribute(pre, "style", style);
1705 }
1706 let text = create_text_node(body);
1707 append_child(pre, text);
1708 append_child(root, pre);
1709 };
1710 match media.as_str() {
1711 "text/html" => set_html(root, section("body").as_deref().unwrap_or(body).trim()),
1712 "application/json" => {
1713 preformatted(None);
1714 let viewer = create_element("div");
1715 set_attribute(viewer, "class", "json-formatter-container");
1716 append_child(root, viewer);
1717 }
1718 _ => preformatted(Some("word-wrap: break-word; white-space: pre-wrap;")),
1719 }
1720}
1721
1722pub fn request_load(url: &str) {
1727 DOM.with(|d| d.borrow_mut().load_requested = Some(url.to_string()));
1728}
1729
1730pub fn take_load_request() -> Option<String> {
1733 DOM.with(|d| d.borrow_mut().load_requested.take())
1734}
1735
1736pub fn request_reload() {
1738 DOM.with(|d| d.borrow_mut().reload_requested = true);
1739}
1740
1741pub fn take_reload_request() -> bool {
1744 DOM.with(|d| std::mem::take(&mut d.borrow_mut().reload_requested))
1745}
1746
1747pub fn take_popped() -> bool {
1751 DOM.with(|d| std::mem::take(&mut d.borrow_mut().popped))
1752}
1753
1754impl DomRecorder {
1755 fn next_document(self, head: Vec<HeadTag>) -> DomRecorder {
1764 let DomRecorder {
1765 fetch_stubs,
1767 route_path,
1768 route_query,
1769 route_history,
1770 nodes: _,
1772 streamed: _,
1773 log: _,
1774 listeners: _,
1775 registrations: _,
1776 head: _,
1777 clipboard: _,
1778 downloads: _,
1779 notifications: _,
1780 published: _,
1781 scrolled_to: _,
1782 selected: _,
1783 focused: _,
1784 raised: _,
1785 current_event: _,
1786 fetches: _,
1787 next_fetch: _,
1788 fetch_responses: _,
1789 current_fetch: _,
1790 route_ops: _,
1791 route_handlers: _,
1792 dropped_file: _,
1793 popped: _,
1794 reload_requested: _,
1795 load_requested: _,
1796 hydration: _,
1797 forwarded: _,
1798 } = self;
1799 DomRecorder {
1800 fetch_stubs,
1801 route_path,
1802 route_query,
1803 route_history,
1804 head,
1805 ..DomRecorder::default()
1806 }
1807 }
1808}
1809
1810pub fn set_dropped_file(file: DroppedFile) {
1815 DOM.with(|d| d.borrow_mut().dropped_file = Some(file));
1816}
1817
1818pub fn dropped_file() -> DroppedFile {
1821 DOM.with(|d| d.borrow().dropped_file.clone())
1822 .unwrap_or_default()
1823}
1824
1825const BROWSER_PRIMARY_TYPES: &[(&str, &str)] = &[
1834 ("video/webm", "webm"),
1835 ("audio/mpeg", "mp3"),
1836 ("application/wasm", "wasm"),
1837 ("application/x-chrome-extension", "crx"),
1838 ("application/xhtml+xml", "xhtml,xht,xhtm"),
1839 ("audio/flac", "flac"),
1840 ("audio/matroska", "mka"),
1841 ("audio/mp3", "mp3"),
1842 ("audio/ogg", "ogg,oga,opus"),
1843 ("audio/wav", "wav"),
1844 ("audio/webm", "webm"),
1845 ("audio/x-m4a", "m4a"),
1846 ("image/avif", "avif"),
1847 ("image/gif", "gif"),
1848 ("image/jpeg", "jpeg,jpg,jpe"),
1849 ("image/jxl", "jxl"),
1850 ("image/png", "png"),
1851 ("image/apng", "png,apng"),
1852 ("image/svg+xml", "svg,svgz"),
1853 ("image/webp", "webp"),
1854 ("multipart/related", "mht,mhtml"),
1855 ("text/css", "css"),
1856 ("text/html", "html,htm,shtml,shtm"),
1857 ("text/javascript", "js,mjs"),
1858 ("text/xml", "xml"),
1859 ("video/matroska", "mkv"),
1860 ("video/mp4", "mp4,m4v"),
1861 ("video/ogg", "ogv,ogm"),
1862 ("text/csv", "csv"),
1863];
1864
1865const BROWSER_SECONDARY_TYPES: &[(&str, &str)] = &[
1866 ("image/x-icon", "ico"),
1867 ("application/epub+zip", "epub"),
1868 ("application/font-woff", "woff"),
1869 ("application/gzip", "gz,tgz"),
1870 ("application/javascript", "js"),
1871 ("application/json", "json"),
1872 ("application/msword", "doc,dot"),
1873 ("application/octet-stream", "bin,exe,com"),
1874 ("application/pdf", "pdf"),
1875 ("application/pkcs7-mime", "p7m,p7c,p7z"),
1876 ("application/pkcs7-signature", "p7s"),
1877 ("application/postscript", "ps,eps,ai"),
1878 ("application/rdf+xml", "rdf"),
1879 ("application/rss+xml", "rss"),
1880 ("application/rtf", "rtf"),
1881 ("application/vnd.android.package-archive", "apk"),
1882 ("application/vnd.mozilla.xul+xml", "xul"),
1883 ("application/vnd.ms-excel", "xls"),
1884 ("application/vnd.ms-powerpoint", "ppt"),
1885 (
1886 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1887 "pptx",
1888 ),
1889 (
1890 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1891 "xlsx",
1892 ),
1893 (
1894 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1895 "docx",
1896 ),
1897 ("application/x-gzip", "gz,tgz"),
1898 ("application/x-mpegurl", "m3u8"),
1899 ("application/x-shockwave-flash", "swf,swl"),
1900 ("application/x-tar", "tar"),
1901 ("application/x-x509-ca-cert", "cer,crt"),
1902 ("application/zip", "zip"),
1903 ("audio/webm", "weba"),
1904 ("image/bmp", "bmp"),
1905 ("image/jpeg", "jfif,pjpeg,pjp"),
1906 ("image/tiff", "tiff,tif"),
1907 ("image/vnd.microsoft.icon", "ico"),
1908 ("image/x-png", "png"),
1909 ("image/x-xbitmap", "xbm"),
1910 ("message/rfc822", "eml"),
1911 ("text/calendar", "ics"),
1912 ("text/html", "ehtml"),
1913 ("text/markdown", "md"),
1914 ("text/plain", "txt,text"),
1915 ("text/vtt", "vtt"),
1916 ("text/x-sh", "sh"),
1917 ("text/xml", "xsl,xbl,xslt"),
1918 ("video/mpeg", "mpeg,mpg,mpe"),
1919];
1920
1921pub fn browser_mime_type(name: &str) -> &'static str {
1928 let Some(dot) = name.rfind('.') else {
1929 return "";
1930 };
1931 let ext = &name[dot + 1..];
1932 if ext.is_empty() {
1933 return "";
1934 }
1935 BROWSER_PRIMARY_TYPES
1936 .iter()
1937 .chain(BROWSER_SECONDARY_TYPES)
1938 .find(|(_, extensions)| {
1939 extensions
1940 .split(',')
1941 .any(|known| known.eq_ignore_ascii_case(ext))
1942 })
1943 .map_or("", |(mime_type, _)| mime_type)
1944}
1945
1946#[derive(Clone, Debug, Default, PartialEq, Eq)]
1951pub struct DroppedFile {
1952 pub name: String,
1953 pub declared_type: Option<String>,
1954 pub bytes: Vec<u8>,
1955}
1956
1957impl DroppedFile {
1958 pub fn named(name: &str) -> DroppedFile {
1960 DroppedFile {
1961 name: name.to_string(),
1962 declared_type: None,
1963 bytes: Vec::new(),
1964 }
1965 }
1966
1967 pub fn declared(
1973 name: String,
1974 size: Option<i64>,
1975 declared_type: Option<String>,
1976 bytes: Option<Vec<i64>>,
1977 ) -> Result<DroppedFile, String> {
1978 let content = match (bytes, size) {
1979 (Some(bytes), size) => {
1980 if let Some(bad) = bytes.iter().find(|b| !(0..=255).contains(*b)) {
1981 return Err(format!(
1982 "the file {name:?} declares the byte {bad}, and a byte is a number from 0 \
1983 to 255"
1984 ));
1985 }
1986 if let Some(size) = size.filter(|s| *s != 0 && *s != bytes.len() as i64) {
1987 return Err(format!(
1988 "the file {name:?} declares size {size} and {} bytes; a file's size is the \
1989 count of its bytes, so declare one or the other",
1990 bytes.len()
1991 ));
1992 }
1993 bytes.into_iter().map(|b| b as u8).collect()
1994 }
1995 (None, Some(size)) if size < 0 => {
1996 return Err(format!(
1997 "the file {name:?} declares size {size}, and a size is never negative"
1998 ))
1999 }
2000 (None, Some(size)) => vec![0; size as usize],
2001 (None, None) => Vec::new(),
2002 };
2003 Ok(DroppedFile {
2004 name,
2005 declared_type,
2006 bytes: content,
2007 })
2008 }
2009
2010 pub fn mime_type(&self) -> &str {
2013 self.declared_type
2014 .as_deref()
2015 .unwrap_or_else(|| browser_mime_type(&self.name))
2016 }
2017
2018 pub fn from_value(value: &RuntimeValue) -> Result<DroppedFile, String> {
2022 let RuntimeValue::Struct(record) = value else {
2023 return Ok(DroppedFile::named(&value.to_display_string()));
2024 };
2025 if record.type_name != "File" {
2026 return Err(format!(
2027 "a dropped file is a `File` or its name as Text, and this is a {}",
2028 record.type_name
2029 ));
2030 }
2031 let field = |name: &str| {
2032 record
2033 .fields
2034 .get(name)
2035 .filter(|v| !matches!(v, RuntimeValue::Nothing))
2036 };
2037 let text = |name: &str| field(name).map(RuntimeValue::to_display_string);
2038 let size = match field("size") {
2039 Some(RuntimeValue::Int(n)) => Some(*n),
2040 Some(other) => {
2041 return Err(format!(
2042 "a `File`'s size is an Int, and this one is {}",
2043 other.type_name()
2044 ))
2045 }
2046 None => None,
2047 };
2048 let bytes = match field("bytes") {
2049 Some(RuntimeValue::List(list)) => {
2050 let list = list.borrow();
2051 let mut out = Vec::with_capacity(list.len());
2052 for i in 0..list.len() {
2053 match list.get(i) {
2054 Some(RuntimeValue::Int(n)) => out.push(n),
2055 other => {
2056 return Err(format!(
2057 "a `File`'s bytes are Ints, and byte {} is {}",
2058 i + 1,
2059 other.map_or("missing".to_string(), |v| v.type_name().to_string())
2060 ))
2061 }
2062 }
2063 }
2064 Some(out)
2065 }
2066 Some(other) => {
2067 return Err(format!(
2068 "a `File`'s bytes are a Seq of Int, and these are {}",
2069 other.type_name()
2070 ))
2071 }
2072 None => None,
2073 };
2074 DroppedFile::declared(text("name").unwrap_or_default(), size, text("type"), bytes)
2075 }
2076
2077 pub fn size(&self) -> i64 {
2079 self.bytes.len() as i64
2080 }
2081
2082 pub fn to_value(&self) -> RuntimeValue {
2084 let mut fields = std::collections::HashMap::new();
2085 fields.insert(
2086 "name".to_string(),
2087 RuntimeValue::Text(std::rc::Rc::new(self.name.clone())),
2088 );
2089 fields.insert("size".to_string(), RuntimeValue::Int(self.size()));
2090 fields.insert(
2091 "type".to_string(),
2092 RuntimeValue::Text(std::rc::Rc::new(self.mime_type().to_string())),
2093 );
2094 fields.insert(
2095 "bytes".to_string(),
2096 crate::semantics::builtins::bytes_to_seq(&self.bytes),
2097 );
2098 RuntimeValue::Struct(Box::new(crate::interpreter::StructValue {
2099 type_name: "File".to_string(),
2100 fields,
2101 }))
2102 }
2103}
2104
2105pub fn arrive(document: &str) {
2110 let lower = document.to_ascii_lowercase();
2111 let head = match (lower.find("<head>"), lower.find("</head>")) {
2112 (Some(open), Some(close)) if open < close => {
2113 parse_head(&document[open + "<head>".len()..close])
2114 }
2115 _ => Vec::new(),
2116 };
2117 SHELL.with(|s| *s.borrow_mut() = head);
2118 reset();
2119}
2120
2121#[derive(Default)]
2124pub struct DomState {
2125 dom: DomRecorder,
2126 head: Option<String>,
2127 shell: Vec<HeadTag>,
2128}
2129
2130pub fn take() -> DomState {
2135 DomState {
2136 dom: DOM.with(RefCell::take),
2137 head: HEAD_HTML.with(RefCell::take),
2138 shell: SHELL.with(RefCell::take),
2139 }
2140}
2141
2142pub fn restore(state: DomState) {
2144 DOM.with(|d| *d.borrow_mut() = state.dom);
2145 HEAD_HTML.with(|h| *h.borrow_mut() = state.head);
2146 SHELL.with(|s| *s.borrow_mut() = state.shell);
2147}
2148
2149pub fn atomically<T>(change: impl FnOnce() -> Result<T, String>) -> Result<T, String> {
2152 let before = DOM.with(|d| d.borrow().clone());
2153 let result = change();
2154 if result.is_err() {
2155 DOM.with(|d| *d.borrow_mut() = before);
2156 }
2157 result
2158}
2159
2160pub fn op_log() -> Vec<DomOp> {
2162 DOM.with(|d| d.borrow().log.clone())
2163}
2164
2165pub fn render_children(node: i64) -> String {
2169 DOM.with(|d| {
2170 let dom = d.borrow();
2171 let mut out = String::new();
2172 let kids = dom
2173 .get(node as u32)
2174 .map(|n| n.children.clone())
2175 .unwrap_or_default();
2176 for child in kids {
2177 dom.render_node(child, &mut out, Spelling::Recorded);
2178 }
2179 out
2180 })
2181}
2182
2183pub fn render() -> String {
2184 DOM.with(|d| d.borrow().render())
2185}
2186
2187pub fn render_served() -> String {
2192 DOM.with(|d| d.borrow().render_served())
2193}
2194
2195thread_local! {
2196 static HEAD_HTML: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
2204}
2205
2206pub fn set_head_html(html: &str) {
2215 let mut declared: Vec<(Option<String>, &str)> = Vec::new();
2216 let mut rest = html;
2217 while let Some(open) = rest.find('<') {
2218 let tag = &rest[open..];
2219 let end = if tag.to_ascii_lowercase().starts_with("<title") {
2220 tag.to_ascii_lowercase()
2221 .find("</title>")
2222 .map(|e| e + "</title>".len())
2223 } else {
2224 tag.find('>').map(|e| e + 1)
2225 };
2226 let Some(end) = end else { break };
2227 let markup = &tag[..end];
2228 let identity = parse_head(markup).first().and_then(HeadTag::identity);
2229 if let Some(identity) = &identity {
2230 declared.retain(|(held, _)| held.as_ref() != Some(identity));
2231 }
2232 declared.push((identity, markup));
2233 rest = &tag[end..];
2234 }
2235 let merged: String = declared.iter().map(|(_, markup)| *markup).collect();
2236 HEAD_HTML.with(|h| *h.borrow_mut() = Some(merged));
2237 DOM.with(|d| {
2238 let head = &mut d.borrow_mut().head;
2239 for tag in parse_head(html) {
2240 if let Some(identity) = tag.identity() {
2241 head.retain(|t| t.identity().as_deref() != Some(identity.as_str()));
2242 }
2243 head.push(tag);
2244 }
2245 });
2246}
2247
2248pub fn head_html() -> Option<String> {
2252 HEAD_HTML.with(|h| h.borrow().clone())
2253}
2254
2255pub fn document_head() -> Option<String> {
2259 DOM.with(|d| {
2260 let markup: String = d.borrow().head.iter().map(HeadTag::markup).collect();
2261 (!markup.is_empty()).then_some(markup)
2262 })
2263}
2264
2265#[derive(Clone, Debug, Default, PartialEq, Eq)]
2268pub struct HeadTag {
2269 pub tag: String,
2270 pub attrs: BTreeMap<String, String>,
2272 pub text: String,
2273}
2274
2275impl HeadTag {
2276 pub fn identity(&self) -> Option<String> {
2283 let attr = |name: &str| self.attrs.get(name).map(|v| v.to_ascii_lowercase());
2284 match self.tag.as_str() {
2285 "title" => Some("title".to_string()),
2286 "base" => Some("base".to_string()),
2287 "meta" if self.attrs.contains_key("charset") => Some("meta:charset".to_string()),
2288 "meta" => match (attr("name"), attr("property")) {
2289 (Some(name), _) => Some(match attr("media") {
2290 Some(media) => format!("meta:name={name}@{media}"),
2291 None => format!("meta:name={name}"),
2292 }),
2293 (None, Some(property)) => Some(format!("meta:property={property}")),
2294 (None, None) => None,
2295 },
2296 "link" => attr("rel").map(|rel| format!("link:rel={rel}")),
2297 _ => None,
2298 }
2299 }
2300
2301 pub fn markup(&self) -> String {
2304 use logicaffeine_language::lgx::html::{is_raw_text, is_void};
2305 let mut out = format!("<{}", self.tag);
2306 for (name, value) in &self.attrs {
2307 out.push_str(&format!(" {name}=\""));
2308 escape_attr(value, &mut out);
2309 out.push('"');
2310 }
2311 out.push('>');
2312 if is_void(&self.tag) {
2313 return out;
2314 }
2315 if is_raw_text(&self.tag) {
2316 out.push_str(&self.text);
2317 } else {
2318 escape_text(&self.text, &mut out);
2319 }
2320 out.push_str(&format!("</{}>", self.tag));
2321 out
2322 }
2323}
2324
2325pub fn parse_head(markup: &str) -> Vec<HeadTag> {
2331 use logicaffeine_language::lgx::html::{decode_entities, is_raw_text, is_void};
2332 let mut out = Vec::new();
2333 let mut rest = markup;
2334 while let Some(lt) = rest.find('<') {
2335 rest = &rest[lt + 1..];
2336 if let Some(comment) = rest.strip_prefix("!--") {
2337 rest = comment.find("-->").map_or("", |end| &comment[end + 3..]);
2338 continue;
2339 }
2340 if rest.starts_with(['/', '!', '?']) {
2341 rest = rest.find('>').map_or("", |end| &rest[end + 1..]);
2342 continue;
2343 }
2344 let name_end = rest
2345 .find(|c: char| c.is_ascii_whitespace() || c == '>' || c == '/')
2346 .unwrap_or(rest.len());
2347 let tag = rest[..name_end].to_ascii_lowercase();
2348 rest = &rest[name_end..];
2349 let mut attrs = BTreeMap::new();
2350 loop {
2351 rest = rest.trim_start_matches(|c: char| c.is_ascii_whitespace());
2352 if let Some(after) = rest.strip_prefix('>') {
2353 rest = after;
2354 break;
2355 }
2356 if let Some(after) = rest.strip_prefix('/') {
2357 rest = after;
2358 continue;
2359 }
2360 if rest.is_empty() {
2361 break;
2362 }
2363 let name_end = rest
2364 .find(|c: char| c.is_ascii_whitespace() || c == '=' || c == '>' || c == '/')
2365 .unwrap_or(rest.len());
2366 let name = rest[..name_end].to_ascii_lowercase();
2367 rest = rest[name_end..].trim_start_matches(|c: char| c.is_ascii_whitespace());
2368 let value = match rest.strip_prefix('=') {
2369 None => String::new(),
2370 Some(after) => {
2371 let after = after.trim_start_matches(|c: char| c.is_ascii_whitespace());
2372 match after.chars().next() {
2373 Some(quote @ ('"' | '\'')) => {
2374 let body = &after[1..];
2375 let end = body.find(quote).unwrap_or(body.len());
2376 rest = body.get(end + 1..).unwrap_or("");
2377 decode_entities(&body[..end])
2378 }
2379 _ => {
2380 let end = after
2381 .find(|c: char| c.is_ascii_whitespace() || c == '>')
2382 .unwrap_or(after.len());
2383 rest = &after[end..];
2384 decode_entities(&after[..end])
2385 }
2386 }
2387 }
2388 };
2389 if !name.is_empty() {
2390 attrs.entry(name).or_insert(value);
2391 }
2392 }
2393 let mut text = String::new();
2394 if !is_void(&tag) {
2395 let close = format!("</{tag}");
2396 let end = rest.to_ascii_lowercase().find(&close).unwrap_or(rest.len());
2397 text = if is_raw_text(&tag) {
2398 rest[..end].to_string()
2399 } else {
2400 decode_entities(&rest[..end])
2401 };
2402 rest = &rest[end..];
2403 rest = rest.find('>').map_or("", |e| &rest[e + 1..]);
2404 }
2405 if tag != "html" && !tag.is_empty() {
2406 out.push(HeadTag { tag, attrs, text });
2407 }
2408 }
2409 out
2410}
2411
2412pub fn body() -> i64 {
2415 DOM.with(|d| d.borrow().body() as i64)
2416}
2417pub fn create_element(tag: &str) -> i64 {
2418 DOM.with(|d| d.borrow_mut().create_element(tag) as i64)
2419}
2420pub fn create_text_node(text: &str) -> i64 {
2421 DOM.with(|d| d.borrow_mut().create_text_node(text) as i64)
2422}
2423pub fn create_element_as(node: i64, tag: &str) -> Result<(), String> {
2425 let id = u32::try_from(node)
2426 .map_err(|_| format!("the stream names node {node}, which is no node id"))?;
2427 DOM.with(|d| d.borrow_mut().create_element_as(id, tag))
2428}
2429pub fn create_text_node_as(node: i64, text: &str) -> Result<(), String> {
2431 let id = u32::try_from(node)
2432 .map_err(|_| format!("the stream names node {node}, which is no node id"))?;
2433 DOM.with(|d| d.borrow_mut().create_text_node_as(id, text))
2434}
2435pub fn set_text(node: i64, text: &str) {
2436 DOM.with(|d| d.borrow_mut().set_text(node as u32, text));
2437}
2438pub fn set_html(node: i64, html: &str) {
2439 DOM.with(|d| d.borrow_mut().set_html(node as u32, html));
2440}
2441pub fn clear_children(node: i64) {
2442 DOM.with(|d| d.borrow_mut().clear_children(node as u32));
2443}
2444pub fn set_attribute(node: i64, name: &str, value: &str) {
2445 DOM.with(|d| d.borrow_mut().set_attribute(node as u32, name, value));
2446}
2447pub fn set_attribute_value(node: i64, name: &str, value: &RuntimeValue) {
2459 if name.eq_ignore_ascii_case("checked") {
2465 set_checked(node, !matches!(value, RuntimeValue::Bool(false)));
2468 return;
2469 }
2470 if logicaffeine_language::lgx::html::is_boolean_attribute(name) {
2471 if let RuntimeValue::Bool(on) = value {
2472 if *on {
2473 set_attribute(node, name, "");
2474 }
2475 return;
2476 }
2477 }
2478 let text = value.to_display_string();
2479 if logicaffeine_language::lgx::html::is_url_attribute(name)
2489 && logicaffeine_language::lgx::html::is_executable_url(&text)
2490 {
2491 set_attribute(node, name, "about:blank#blocked");
2492 return;
2493 }
2494 set_attribute(node, name, &text);
2495}
2496pub fn append_child(parent: i64, child: i64) {
2497 DOM.with(|d| d.borrow_mut().append_child(parent as u32, child as u32));
2498}
2499pub fn set_value(node: i64, value: &str) {
2500 DOM.with(|d| d.borrow_mut().set_value(node as u32, value));
2501}
2502#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2504pub enum ToggleControl {
2505 Checkbox,
2507 Radio,
2509}
2510
2511pub fn has_attribute(node: i64, name: &str) -> bool {
2515 DOM.with(|d| {
2516 d.borrow()
2517 .get(node as u32)
2518 .is_some_and(|n| n.attrs.contains_key(name))
2519 })
2520}
2521pub fn tag_of(node: i64) -> Option<String> {
2525 DOM.with(|d| {
2526 d.borrow()
2527 .get(node as u32)
2528 .map(|n| n.tag.to_ascii_lowercase())
2529 })
2530}
2531
2532pub fn attribute(node: i64, name: &str) -> Option<String> {
2534 DOM.with(|d| {
2535 d.borrow()
2536 .get(node as u32)
2537 .and_then(|n| n.attrs.get(name).cloned())
2538 })
2539}
2540
2541pub fn form_fields(node: i64) -> Vec<(String, String)> {
2546 DOM.with(|d| {
2547 let dom = d.borrow();
2548 let mut out = Vec::new();
2549 let mut stack = vec![node as u32];
2550 while let Some(id) = stack.pop() {
2551 let Some(n) = dom.get(id) else { continue };
2552 if let Some(name) = n.attrs.get("name") {
2553 if !name.is_empty() {
2554 let value = if matches!(n.tag.to_ascii_lowercase().as_str(), "input")
2555 && matches!(
2556 n.attrs
2557 .get("type")
2558 .map(|t| t.to_ascii_lowercase())
2559 .as_deref(),
2560 Some("checkbox") | Some("radio")
2561 ) {
2562 if !dom.get_checked(id) {
2566 for c in n.children.iter().rev() {
2567 stack.push(*c);
2568 }
2569 continue;
2570 }
2571 n.attrs
2572 .get("value")
2573 .cloned()
2574 .unwrap_or_else(|| "on".to_string())
2575 } else {
2576 dom.control_value(id)
2577 };
2578 out.push((name.clone(), value));
2579 }
2580 }
2581 for c in n.children.iter().rev() {
2582 stack.push(*c);
2583 }
2584 }
2585 out
2586 })
2587}
2588
2589pub fn get_checked(node: i64) -> bool {
2590 DOM.with(|d| d.borrow().get_checked(node as u32))
2591}
2592pub fn toggle_control(node: i64) -> Option<ToggleControl> {
2593 DOM.with(|d| d.borrow().toggle_control(node as u32))
2594}
2595pub fn set_checked(node: i64, checked: bool) {
2596 DOM.with(|d| d.borrow_mut().set_checked(node as u32, checked));
2597}
2598pub fn prevent_default(node: i64, event: &str) {
2605 DOM.with(|d| {
2606 let mut d = d.borrow_mut();
2607 let flags = d
2608 .registrations
2609 .entry((node as u32, event.to_string()))
2610 .or_insert(0);
2611 *flags |= LISTEN_PREVENT;
2612 d.log.push(DomOp::PreventDefault {
2613 node: node as u32,
2614 event: event.to_string(),
2615 })
2616 });
2617}
2618pub fn set_stream(node: i64, stream: i64) {
2619 DOM.with(|d| d.borrow_mut().set_stream(node as u32, stream));
2620}
2621pub fn rtc_peer_connection() -> i64 {
2622 DOM.with(|d| d.borrow_mut().rtc_peer_connection() as i64)
2623}
2624pub fn rtc_data_channel(pc: i64, label: &str) -> i64 {
2625 DOM.with(|d| d.borrow_mut().rtc_data_channel(pc as u32, label) as i64)
2626}
2627pub fn rtc_create_offer(pc: i64) -> String {
2628 DOM.with(|d| d.borrow_mut().rtc_create_offer(pc as u32));
2629 "offer".to_string()
2630}
2631pub fn rtc_create_answer(pc: i64) -> String {
2632 DOM.with(|d| d.borrow_mut().rtc_create_answer(pc as u32));
2633 "answer".to_string()
2634}
2635pub fn rtc_set_local(pc: i64, sdp: &str) {
2636 DOM.with(|d| d.borrow_mut().rtc_set_local(pc as u32, sdp));
2637}
2638pub fn rtc_set_remote(pc: i64, sdp: &str) {
2639 DOM.with(|d| d.borrow_mut().rtc_set_remote(pc as u32, sdp));
2640}
2641pub fn rtc_add_ice(pc: i64, candidate: &str) {
2642 DOM.with(|d| d.borrow_mut().rtc_add_ice(pc as u32, candidate));
2643}
2644pub fn rtc_send(channel: i64, bytes: u64) {
2645 DOM.with(|d| d.borrow_mut().rtc_send(channel as u32, bytes));
2646}
2647pub fn get_text(node: i64) -> String {
2648 DOM.with(|d| d.borrow().get_text(node as u32))
2649}
2650pub fn get_value(node: i64) -> String {
2651 DOM.with(|d| d.borrow().get_value(node as u32))
2652}
2653pub fn is_shown(selector: &str) -> bool {
2655 DOM.with(|d| {
2656 let d = d.borrow();
2657 match d.query_selector_all(selector).first() {
2658 Some(&h) => d.shown(h),
2659 None => false,
2660 }
2661 })
2662}
2663
2664pub fn query_selector(selector: &str) -> i64 {
2665 DOM.with(|d| handle(d.borrow().query_selector(selector)))
2666}
2667fn handle(node: u32) -> i64 {
2670 if node == u32::MAX {
2671 -1
2672 } else {
2673 node as i64
2674 }
2675}
2676pub fn query_selector_all(selector: &str) -> Vec<i64> {
2678 DOM.with(|d| {
2679 d.borrow()
2680 .query_selector_all(selector)
2681 .into_iter()
2682 .map(i64::from)
2683 .collect()
2684 })
2685}
2686pub fn text_content(node: i64) -> String {
2688 DOM.with(|d| d.borrow().text_content(node as u32))
2689}
2690pub fn set_title(title: &str) {
2693 DOM.with(|d| {
2694 let head = &mut d.borrow_mut().head;
2695 match head.iter_mut().find(|t| t.tag == "title") {
2696 Some(element) => element.text = title.to_string(),
2697 None => head.push(HeadTag {
2698 tag: "title".to_string(),
2699 text: title.to_string(),
2700 ..HeadTag::default()
2701 }),
2702 }
2703 });
2704}
2705pub fn title() -> String {
2708 DOM.with(|d| {
2709 d.borrow()
2710 .head
2711 .iter()
2712 .find(|t| t.tag == "title")
2713 .map(|t| {
2714 t.text
2715 .split_ascii_whitespace()
2716 .collect::<Vec<_>>()
2717 .join(" ")
2718 })
2719 .unwrap_or_default()
2720 })
2721}
2722pub fn write_clipboard(text: &str) {
2725 DOM.with(|d| d.borrow_mut().clipboard = text.to_string());
2726}
2727
2728pub fn clipboard() -> String {
2730 DOM.with(|d| d.borrow().clipboard.clone())
2731}
2732
2733pub fn offer_download(name: &str, content: &str) {
2735 DOM.with(|d| {
2736 d.borrow_mut()
2737 .downloads
2738 .push((name.to_string(), content.to_string()))
2739 });
2740}
2741
2742pub fn downloads() -> Vec<(String, String)> {
2744 DOM.with(|d| d.borrow().downloads.clone())
2745}
2746
2747pub fn notify(message: &str, tone: &str) {
2749 DOM.with(|d| {
2750 d.borrow_mut()
2751 .notifications
2752 .push((message.to_string(), tone.to_string()))
2753 });
2754}
2755
2756pub fn notifications() -> Vec<(String, String)> {
2758 DOM.with(|d| d.borrow().notifications.clone())
2759}
2760
2761pub fn publish(topic: &str, message: &str) {
2769 DOM.with(|d| {
2770 d.borrow_mut()
2771 .published
2772 .push((topic.to_string(), message.to_string()))
2773 });
2774}
2775
2776pub fn published() -> Vec<(String, String)> {
2778 DOM.with(|d| d.borrow().published.clone())
2779}
2780
2781pub fn scroll_to(node: i64) {
2783 DOM.with(|d| d.borrow_mut().scrolled_to = node as u32);
2784}
2785
2786pub fn scrolled_to() -> i64 {
2788 DOM.with(|d| handle(d.borrow().scrolled_to))
2789}
2790
2791pub fn select_text(node: i64) {
2793 DOM.with(|d| d.borrow_mut().selected = node as u32);
2794}
2795
2796pub fn selected() -> i64 {
2798 DOM.with(|d| handle(d.borrow().selected))
2799}
2800
2801pub fn set_focus(node: i64) {
2802 DOM.with(|d| {
2803 let mut d = d.borrow_mut();
2804 d.focused = node as u32;
2805 });
2806}
2807pub fn focused() -> i64 {
2809 DOM.with(|d| {
2810 let mut d = d.borrow_mut();
2811 d.drop_detached_focus();
2812 handle(d.focused)
2813 })
2814}
2815
2816pub fn focusable(node: i64) -> bool {
2821 DOM.with(|d| d.borrow().get(node as u32).is_some_and(Node::focusable))
2822}
2823
2824pub fn focus_target(verb: crate::semantics::drive::Verb, node: i64) -> Option<i64> {
2828 use crate::semantics::drive::FocusTarget;
2829 match verb.focus_target() {
2830 FocusTarget::Unmoved => None,
2831 FocusTarget::Element => focusable(node).then_some(node),
2832 FocusTarget::PressedAncestor => DOM.with(|d| {
2833 let d = d.borrow();
2834 let chain = d.ancestors_of(node as u32).unwrap_or_default();
2835 let pressed = std::iter::once(node as u32).chain(chain.into_iter().rev());
2836 Some(
2837 pressed
2838 .filter(|&h| h != 0)
2839 .find(|&h| d.get(h).is_some_and(Node::focusable))
2840 .map_or(-1, i64::from),
2841 )
2842 }),
2843 }
2844}
2845
2846pub fn move_focus(target: i64) -> Option<(i64, i64)> {
2850 let before = focused();
2851 let target = if target < 0 { -1 } else { target };
2852 if before == target {
2853 return None;
2854 }
2855 set_focus(target);
2856 Some((before, target))
2857}
2858
2859pub fn focus_for(verb: crate::semantics::drive::Verb, node: i64) -> Option<(i64, i64)> {
2862 focus_target(verb, node).and_then(move_focus)
2863}
2864
2865pub fn element_focus(node: i64) {
2870 if !focusable(node) {
2871 return;
2872 }
2873 if let Some((blurred, gained)) = move_focus(node) {
2874 raise_focus_events(blurred, gained);
2875 }
2876}
2877
2878pub fn element_blur(node: i64) {
2881 if node >= 0 && focused() == node {
2882 if let Some((blurred, gained)) = move_focus(-1) {
2883 raise_focus_events(blurred, gained);
2884 }
2885 }
2886}
2887
2888fn raise_focus_events(blurred: i64, gained: i64) {
2889 let events: Vec<(String, &'static str)> = [(blurred, "blur"), (gained, "focus")]
2890 .into_iter()
2891 .filter(|(element, _)| *element >= 0)
2892 .map(|(element, event)| (node_address(element), event))
2893 .collect();
2894 DOM.with(|d| d.borrow_mut().raised.extend(events));
2895}
2896
2897pub fn take_raised_events() -> Vec<(i64, &'static str)> {
2903 let raised = DOM.with(|d| std::mem::take(&mut d.borrow_mut().raised));
2904 raised
2905 .into_iter()
2906 .filter_map(|(address, event)| {
2907 let element = query_selector(&address);
2908 (element >= 0).then_some((element, event))
2909 })
2910 .collect()
2911}
2912
2913pub fn node_address(node: i64) -> String {
2920 DOM.with(|d| d.borrow().address(node as u32))
2921}
2922
2923const PLACE: char = '\u{0}';
2926
2927pub fn enter_value(verb: crate::semantics::drive::Verb, selector: &str, value: &str) {
2934 use crate::semantics::drive::Verb;
2935 DOM.with(|d| {
2936 let mut d = d.borrow_mut();
2937 let node = d.query_selector(selector);
2938 if !d.takes(verb, node, value) {
2939 return;
2940 }
2941 match verb {
2942 Verb::Type => {
2943 let Some(n) = d.get_mut(node) else { return };
2944 let (held, bad_input) = typed_value(n, value);
2945 n.value = held;
2946 n.bad_input = bad_input;
2947 n.user_edited = true;
2948 }
2949 Verb::Choose => {
2950 if let Some(chosen) = d.option_offering(node, value) {
2951 if let Some(n) = d.get_mut(node) {
2952 n.value = chosen;
2953 n.user_edited = true;
2954 }
2955 }
2956 }
2957 _ => {}
2958 }
2959 });
2960}
2961
2962pub fn answers_by_itself(verb: crate::semantics::drive::Verb, selector: &str, value: &str) -> bool {
2969 DOM.with(|d| {
2970 let d = d.borrow();
2971 d.takes(verb, d.query_selector(selector), value)
2972 })
2973}
2974
2975const TEXT_ENTRY_TYPES: [&str; 8] = [
2977 "", "text", "search", "url", "tel", "email", "password", "number",
2978];
2979
2980impl DomRecorder {
2981 fn takes(&self, verb: crate::semantics::drive::Verb, node: u32, value: &str) -> bool {
2985 use crate::semantics::drive::Verb;
2986 let Some(n) = self.get(node) else {
2987 return false;
2988 };
2989 let reachable = !self.barred(node);
2990 match verb {
2991 Verb::Type => {
2992 reachable
2993 && (n.tag.eq_ignore_ascii_case("textarea")
2994 || (n.tag.eq_ignore_ascii_case("input")
2995 && TEXT_ENTRY_TYPES.contains(&input_type(n).as_str())))
2996 }
2997 Verb::Choose => reachable && self.option_offering(node, value).is_some(),
2998 Verb::Click | Verb::Submit | Verb::Press | Verb::Hover => false,
2999 }
3000 }
3001}
3002
3003fn typed_value(n: &Node, text: &str) -> (String, bool) {
3009 let tag = n.tag.to_ascii_lowercase();
3010 let ty = input_type(n);
3011 let mut held: String = match (tag.as_str(), ty.as_str()) {
3012 ("textarea", _) => text.to_string(),
3013 (_, "number") => text
3014 .chars()
3015 .filter(|c| c.is_ascii_digit() || matches!(c, '+' | '-' | '.' | 'e' | 'E'))
3016 .collect(),
3017 _ => text.chars().filter(|c| *c != '\n' && *c != '\r').collect(),
3018 };
3019 if let Some(max) = n
3020 .attrs
3021 .get("maxlength")
3022 .and_then(|m| m.trim().parse::<usize>().ok())
3023 {
3024 let mut units = 0;
3025 held = held
3026 .chars()
3027 .take_while(|c| {
3028 units += c.len_utf16();
3029 units <= max
3030 })
3031 .collect();
3032 }
3033 let trim = |s: &str| {
3034 s.trim_matches(|c: char| c.is_ascii_whitespace())
3035 .to_string()
3036 };
3037 match ty.as_str() {
3038 "email" if n.attrs.contains_key("multiple") => (
3039 held.split(',').map(trim).collect::<Vec<_>>().join(","),
3040 false,
3041 ),
3042 "email" | "url" => (trim(&held), false),
3043 "number" if valid_float(&held) => (held, false),
3044 "number" => (String::new(), !held.is_empty()),
3045 _ => (held, false),
3046 }
3047}
3048
3049fn input_type(n: &Node) -> String {
3051 match n.tag.eq_ignore_ascii_case("input") {
3052 true => n
3053 .attrs
3054 .get("type")
3055 .map(|t| t.trim().to_ascii_lowercase())
3056 .unwrap_or_else(|| "text".to_string()),
3057 false => String::new(),
3058 }
3059}
3060
3061pub fn focus_by(verb: crate::semantics::drive::Verb, selector: &str) -> Vec<(i64, &'static str)> {
3066 let node = query_selector(selector);
3067 if node < 0 {
3068 return Vec::new();
3069 }
3070 match focus_for(verb, node) {
3071 Some((blurred, gained)) => [(blurred, "blur"), (gained, "focus")]
3072 .into_iter()
3073 .filter(|(element, _)| *element >= 0)
3074 .collect(),
3075 None => Vec::new(),
3076 }
3077}
3078#[derive(Clone, Debug, Default, PartialEq, Eq)]
3081pub struct DispatchPath {
3082 pub stops: Vec<String>,
3086 pub prevented: bool,
3089}
3090
3091pub fn dispatch_path(target: i64, event: &str) -> DispatchPath {
3097 DOM.with(|d| d.borrow().dispatch_path(target as u32, event))
3098}
3099
3100pub const NON_BUBBLING_EVENTS: &[&str] = &[
3103 "focus",
3104 "blur",
3105 "mouseenter",
3106 "mouseleave",
3107 "pointerenter",
3108 "pointerleave",
3109 "load",
3110 "unload",
3111 "error",
3112 "abort",
3113 "scroll",
3114 "scrollend",
3115 "resize",
3116 "invalid",
3117 "toggle",
3118];
3119
3120pub fn event_bubbles(event: &str) -> bool {
3122 !NON_BUBBLING_EVENTS.contains(&event)
3123}
3124
3125#[derive(Clone, Debug, PartialEq, Eq)]
3128pub struct Link {
3129 pub href: String,
3130 pub here: bool,
3134}
3135
3136pub fn link_of(node: i64) -> Option<Link> {
3138 DOM.with(|d| {
3139 let d = d.borrow();
3140 let chain = d.ancestors_of(node as u32)?;
3141 let a = std::iter::once(node as u32)
3142 .chain(chain.into_iter().rev())
3143 .filter_map(|h| d.get(h))
3144 .find(|n| {
3145 !n.is_text && n.tag.eq_ignore_ascii_case("a") && n.attrs.contains_key("href")
3146 })?;
3147 let target = a
3148 .attrs
3149 .get("target")
3150 .map(|t| t.trim().to_ascii_lowercase())
3151 .unwrap_or_default();
3152 Some(Link {
3153 href: a.attrs.get("href").cloned().unwrap_or_default(),
3154 here: !a.attrs.contains_key("download")
3155 && matches!(target.as_str(), "" | "_self" | "_top" | "_parent"),
3156 })
3157 })
3158}
3159
3160pub fn add_class(node: i64, class: &str) {
3162 let current = DOM.with(|d| {
3163 d.borrow()
3164 .get(node as u32)
3165 .and_then(|n| n.attrs.get("class").cloned())
3166 .unwrap_or_default()
3167 });
3168 if current.split_whitespace().any(|c| c == class) {
3169 return;
3170 }
3171 let next = if current.is_empty() {
3172 class.to_string()
3173 } else {
3174 format!("{current} {class}")
3175 };
3176 set_attribute(node, "class", &next);
3177}
3178pub fn remove_class(node: i64, class: &str) {
3180 let current = DOM.with(|d| {
3181 d.borrow()
3182 .get(node as u32)
3183 .and_then(|n| n.attrs.get("class").cloned())
3184 .unwrap_or_default()
3185 });
3186 let next: Vec<&str> = current.split_whitespace().filter(|c| *c != class).collect();
3187 set_attribute(node, "class", &next.join(" "));
3188}
3189pub fn add_event_listener(node: i64, event: &str, closure: RuntimeValue) {
3190 DOM.with(|d| {
3191 d.borrow_mut()
3192 .add_event_listener(node as u32, event, closure)
3193 });
3194}
3195
3196pub fn note_event_listener(node: i64, event: &str) {
3204 DOM.with(|d| {
3205 let mut d = d.borrow_mut();
3206 d.register(node as u32, event);
3207 d.log.push(DomOp::AddEventListener {
3208 node: node as u32,
3209 event: event.to_string(),
3210 })
3211 });
3212}
3213pub fn event_key() -> String {
3220 DOM.with(|d| {
3221 d.borrow()
3222 .current_event
3223 .as_ref()
3224 .map(|e| e.key.clone())
3225 .unwrap_or_default()
3226 })
3227}
3228pub fn event_value() -> String {
3229 DOM.with(|d| {
3230 d.borrow()
3231 .current_event
3232 .as_ref()
3233 .map(|e| e.value.clone())
3234 .unwrap_or_default()
3235 })
3236}
3237pub fn event_target() -> i64 {
3238 DOM.with(|d| {
3239 d.borrow()
3240 .current_event
3241 .as_ref()
3242 .map(|e| e.target as i64)
3243 .unwrap_or(-1)
3244 })
3245}
3246
3247pub fn begin_dispatch(node: i64, event: &str, value: &str) -> Option<RuntimeValue> {
3252 begin_dispatch_keyed(node, event, value, "")
3253}
3254
3255pub fn begin_dispatch_keyed(
3257 node: i64,
3258 event: &str,
3259 value: &str,
3260 key: &str,
3261) -> Option<RuntimeValue> {
3262 DOM.with(|d| {
3263 let mut dom = d.borrow_mut();
3264 let closure = dom.listener_for(node as u32, event)?;
3265 dom.current_event = Some(CurrentEvent {
3266 target: node as u32,
3267 value: value.to_string(),
3268 key: key.to_string(),
3269 });
3270 Some(closure)
3271 })
3272}
3273pub fn end_dispatch() {
3274 DOM.with(|d| d.borrow_mut().current_event = None);
3275}
3276
3277pub fn fetch_send(
3285 method: &str,
3286 url: &str,
3287 body: &str,
3288 headers: Vec<(String, String)>,
3289 closure: RuntimeValue,
3290) -> i64 {
3291 DOM.with(|d| {
3292 d.borrow_mut()
3293 .fetch_send(method, url, body, headers, closure)
3294 })
3295}
3296
3297pub fn fetch_start(url: &str, closure: RuntimeValue) {
3299 DOM.with(|d| {
3300 d.borrow_mut()
3301 .fetch_send("GET", url, "", Vec::new(), closure);
3302 });
3303}
3304
3305pub fn fetch_abort(req: i64) {
3308 DOM.with(|d| d.borrow_mut().fetch_abort(req));
3309}
3310
3311pub fn fetch_set_timeout(req: i64, millis: i64) {
3313 DOM.with(|d| d.borrow_mut().fetch_set_timeout(req, millis));
3314}
3315
3316pub fn response_status(req: i64) -> i64 {
3319 DOM.with(|d| {
3320 d.borrow()
3321 .fetch_response(req)
3322 .map(|r| r.status)
3323 .unwrap_or(0)
3324 })
3325}
3326
3327pub fn response_body(req: i64) -> String {
3329 DOM.with(|d| {
3330 d.borrow()
3331 .fetch_response(req)
3332 .map(|r| r.body.clone())
3333 .unwrap_or_default()
3334 })
3335}
3336
3337pub fn response_header(req: i64, name: &str) -> String {
3340 DOM.with(|d| {
3341 d.borrow()
3342 .fetch_response(req)
3343 .and_then(|r| {
3344 r.headers
3345 .iter()
3346 .find(|(n, _)| n.eq_ignore_ascii_case(name))
3347 .map(|(_, v)| v.clone())
3348 })
3349 .unwrap_or_default()
3350 })
3351}
3352
3353pub fn response_error(req: i64) -> String {
3356 DOM.with(|d| {
3357 d.borrow()
3358 .fetch_response(req)
3359 .map(|r| r.error.clone())
3360 .unwrap_or_default()
3361 })
3362}
3363
3364pub fn response_text() -> String {
3367 DOM.with(|d| {
3368 let dom = d.borrow();
3369 dom.current_fetch
3370 .and_then(|req| dom.fetch_response(req).map(|r| r.body.clone()))
3371 .unwrap_or_default()
3372 })
3373}
3374
3375pub const STUB_CONTENT_TYPE: &str = "application/json";
3379
3380pub fn stub_fetch(method: &str, url: &str, status: i64, body: &str) {
3383 DOM.with(|d| {
3384 let mut d = d.borrow_mut();
3385 let key = (method.to_ascii_uppercase(), url.to_string());
3386 let value = (status, body.to_string());
3387 match d.fetch_stubs.iter_mut().find(|(k, _)| *k == key) {
3388 Some(slot) => slot.1 = value,
3389 None => d.fetch_stubs.push((key, value)),
3390 }
3391 });
3392}
3393
3394pub fn stubbed_answer(req: i64) -> Option<(i64, String)> {
3396 DOM.with(|d| {
3397 let d = d.borrow();
3398 let r = d.fetch_requests().into_iter().find(|r| r.req == req)?;
3399 let key = (r.method.to_ascii_uppercase(), r.url);
3400 d.fetch_stubs
3401 .iter()
3402 .find(|(k, _)| *k == key)
3403 .map(|(_, v)| v.clone())
3404 })
3405}
3406
3407pub fn fetch_requests() -> Vec<FetchRequest> {
3408 DOM.with(|d| d.borrow().fetch_requests())
3409}
3410
3411pub fn begin_fetch_resolve(
3416 req: i64,
3417 status: i64,
3418 headers: &[(String, String)],
3419 body: &str,
3420) -> Option<RuntimeValue> {
3421 DOM.with(|d| d.borrow_mut().fetch_resolve(req, status, headers, body))
3422}
3423
3424pub fn fail_fetch(req: i64, error: &str) -> Option<RuntimeValue> {
3426 DOM.with(|d| d.borrow_mut().fetch_fail(req, error))
3427}
3428
3429pub fn end_fetch_resolve() {
3430 DOM.with(|d| d.borrow_mut().current_fetch = None);
3431}
3432
3433pub fn route_path() -> String {
3442 DOM.with(|d| d.borrow().route_path.clone())
3443}
3444
3445pub fn route_query(name: &str) -> String {
3447 DOM.with(|d| d.borrow().route_query_value(name))
3448}
3449
3450pub fn navigate(path: &str) {
3452 DOM.with(|d| d.borrow_mut().navigate(path, false));
3453}
3454
3455pub fn replace_route(path: &str) {
3458 DOM.with(|d| d.borrow_mut().navigate(path, true));
3459}
3460
3461pub fn on_route(handler: RuntimeValue) {
3464 DOM.with(|d| d.borrow_mut().route_handlers.push(handler));
3465}
3466
3467pub fn route_ops() -> Vec<RouteOp> {
3469 DOM.with(|d| d.borrow().route_ops.clone())
3470}
3471
3472pub fn init_route(path: &str, query: &str) {
3475 DOM.with(|d| {
3476 let mut dom = d.borrow_mut();
3477 dom.route_path = path.to_string();
3478 dom.route_query = query.to_string();
3479 dom.route_history = vec![path.to_string()];
3481 });
3482}
3483
3484pub fn go_back() -> Option<(String, Vec<RuntimeValue>)> {
3488 DOM.with(|d| {
3489 let mut dom = d.borrow_mut();
3490 let path = dom.go_back()?;
3491 Some((path, dom.route_handlers.clone()))
3492 })
3493}
3494
3495pub fn deliver_pop(path: &str) -> Vec<RuntimeValue> {
3500 DOM.with(|d| {
3501 let mut dom = d.borrow_mut();
3502 dom.route_path = path.to_string();
3503 dom.route_handlers.clone()
3504 })
3505}
3506
3507pub const LISTEN_PREVENT: u8 = 1 << 0;
3515pub const LISTEN_STOP: u8 = 1 << 1;
3516pub const LISTEN_ONCE: u8 = 1 << 2;
3517pub const LISTEN_CAPTURE: u8 = 1 << 3;
3518pub const LISTEN_PASSIVE: u8 = 1 << 4;
3519
3520pub const STREAM_ID_BASE: u32 = 1 << 31;
3523
3524impl DomRecorder {
3525 fn parent_of(&self, node: u32) -> Option<u32> {
3530 let minted = self
3531 .nodes
3532 .iter()
3533 .enumerate()
3534 .map(|(i, n)| (i as u32, Some(n)));
3535 let streamed = self
3536 .streamed
3537 .iter()
3538 .enumerate()
3539 .map(|(i, n)| (i as u32 + STREAM_ID_BASE, n.as_ref()));
3540 minted
3541 .chain(streamed)
3542 .find(|(_, n)| n.is_some_and(|n| n.children.contains(&node)))
3543 .map(|(id, _)| id)
3544 }
3545
3546 fn remove_node(&mut self, node: u32) {
3547 let node = self.resolve(node);
3548 self.leaving(node);
3549 if let Some(parent) = self.parent_of(node) {
3550 if let Some(p) = self.get_mut(parent) {
3551 p.children.retain(|&c| c != node);
3552 }
3553 }
3554 self.log.push(DomOp::RemoveNode { node });
3555 }
3556
3557 fn insert_before(&mut self, parent: u32, child: u32, before: u32) {
3558 let (child, before) = (self.resolve(child), self.resolve(before));
3559 let moved = self.parent_of(child).is_some();
3560 let blurred = (moved && self.focused != u32::MAX && self.holds(child, self.focused))
3561 .then_some(self.focused);
3562 self.leaving(child);
3563 if let Some(cur) = self.parent_of(child) {
3564 if let Some(p) = self.get_mut(cur) {
3565 p.children.retain(|&c| c != child);
3566 }
3567 }
3568 if let Some(p) = self.get_mut(parent) {
3569 let at = if before == u32::MAX {
3570 p.children.len()
3571 } else {
3572 p.children
3573 .iter()
3574 .position(|&c| c == before)
3575 .unwrap_or(p.children.len())
3576 };
3577 p.children.insert(at, child);
3578 }
3579 if let Some(element) = blurred {
3582 let at = self.address(element);
3583 self.raised.push((at, "blur"));
3584 }
3585 self.log.push(DomOp::InsertBefore {
3586 parent,
3587 child,
3588 before,
3589 });
3590 }
3591
3592 fn remove_attribute(&mut self, node: u32, name: &str) {
3593 if let Some(n) = self.get_mut(node) {
3594 n.attrs.remove(name);
3595 }
3596 self.log.push(DomOp::RemoveAttribute {
3597 node,
3598 name: name.to_string(),
3599 });
3600 }
3601
3602 fn free_node(&mut self, node: u32) {
3608 let node = self.resolve(node);
3609 self.leaving(node);
3610 if let Some(parent) = self.parent_of(node) {
3611 if let Some(p) = self.get_mut(parent) {
3612 p.children.retain(|&c| c != node);
3613 }
3614 }
3615 let mut stack = vec![node];
3616 while let Some(n) = stack.pop() {
3617 if let Some(existing) = self.get(n) {
3618 stack.extend(existing.children.iter().copied());
3619 }
3620 self.listeners.retain(|(ln, _, _)| *ln != n);
3621 self.registrations.retain(|(rn, _), _| *rn != n);
3622 match n.checked_sub(STREAM_ID_BASE) {
3623 Some(i) => {
3624 if let Some(slot) = self.streamed.get_mut(i as usize) {
3625 *slot = None;
3626 }
3627 }
3628 None => {
3629 if let Some(nn) = self.get_mut(n) {
3630 *nn = Node::default();
3631 }
3632 }
3633 }
3634 }
3635 self.log.push(DomOp::FreeNode { node });
3636 }
3637
3638 fn listen(&mut self, node: u32, event: &str, flags: u8) {
3639 let node = self.resolve(node);
3640 self.registrations.insert((node, event.to_string()), flags);
3641 self.log.push(DomOp::Listen {
3642 node,
3643 event: event.to_string(),
3644 flags,
3645 });
3646 }
3647
3648 fn unlisten(&mut self, node: u32, event: &str) {
3649 let node = self.resolve(node);
3650 self.registrations.remove(&(node, event.to_string()));
3651 self.log.push(DomOp::Unlisten {
3652 node,
3653 event: event.to_string(),
3654 });
3655 }
3656
3657 fn register(&mut self, node: u32, event: &str) {
3659 let node = self.resolve(node);
3660 self.registrations
3661 .entry((node, event.to_string()))
3662 .or_insert(0);
3663 }
3664
3665 fn registration(&self, node: u32, event: &str) -> Option<u8> {
3667 self.registrations.get(&(node, event.to_string())).copied()
3668 }
3669
3670 fn dispatch_path(&self, target: u32, event: &str) -> DispatchPath {
3672 let ancestors = self.ancestors_of(target).unwrap_or_default();
3673 let mut stops = vec![];
3674 let mut prevented = false;
3675 let mut reach = |node: u32, flags: u8| {
3676 stops.push(node);
3677 prevented |= flags & LISTEN_PREVENT != 0 && flags & LISTEN_PASSIVE == 0;
3678 flags & LISTEN_STOP != 0
3679 };
3680 let capturing = ancestors.iter().filter_map(|&a| {
3681 self.registration(a, event)
3682 .filter(|f| f & LISTEN_CAPTURE != 0)
3683 .map(|f| (a, f))
3684 });
3685 let bubbling = ancestors.iter().rev().filter_map(|&a| {
3686 self.registration(a, event)
3687 .filter(|f| f & LISTEN_CAPTURE == 0)
3688 .map(|f| (a, f))
3689 });
3690 let at_target = std::iter::once((target, self.registration(target, event).unwrap_or(0)));
3691 let walk: Vec<(u32, u8)> = match event_bubbles(event) {
3692 true => capturing.chain(at_target).chain(bubbling).collect(),
3693 false => capturing.chain(at_target).collect(),
3694 };
3695 for (node, flags) in walk {
3696 if reach(node, flags) {
3697 break;
3698 }
3699 }
3700 let addresses = stops.into_iter().map(|n| self.address(n)).collect();
3701 DispatchPath {
3702 stops: addresses,
3703 prevented,
3704 }
3705 }
3706
3707 fn address(&self, node: u32) -> String {
3709 let id = self
3710 .get(node)
3711 .and_then(|n| n.attrs.get("id"))
3712 .filter(|id| !id.is_empty());
3713 if let Some(id) = id.filter(|id| self.query_selector(&format!("#{id}")) == node) {
3714 return format!("#{id}");
3715 }
3716 if node == self.body() {
3717 return "body".to_string();
3718 }
3719 match self.place_of(node) {
3720 Some((indices, tag)) => {
3721 let path: Vec<String> = indices.iter().map(usize::to_string).collect();
3722 format!("{PLACE}{}:{tag}", path.join("/"))
3723 }
3724 None => String::new(),
3725 }
3726 }
3727}
3728
3729impl DomRecorder {
3730 fn adopting(&self, node: u32) -> bool {
3733 self.hydration
3734 .as_ref()
3735 .is_some_and(|h| h.written.contains_key(&node))
3736 }
3737
3738 fn keep_the_visitors(&mut self, node: u32) {
3741 let at = self.address(node);
3742 self.raised.push((at, "input"));
3743 }
3744
3745 fn hydrate(&mut self, root: u32) -> Result<(), String> {
3746 if self.hydration.is_some() {
3747 return Err(format!(
3748 "the stream hydrates node {root} while an earlier hydration in it is still open"
3749 ));
3750 }
3751 if let Some(n) = self.get_mut(root) {
3752 n.attrs
3753 .remove(logicaffeine_language::parser::lgx_lower::PRERENDERED_ATTR);
3754 }
3755 let mut h = Hydration::default();
3756 h.cursors.insert(root, None);
3757 self.hydration = Some(h);
3758 self.log.push(DomOp::Hydrate { root });
3759 Ok(())
3760 }
3761
3762 fn open_hydration(&self, op: &str) -> Result<(), String> {
3763 match self.hydration {
3764 Some(_) => Ok(()),
3765 None => Err(format!("the stream sends `{op}` outside a hydration: only a stream that opened one with `Hydrate` claims nodes the markup holds")),
3766 }
3767 }
3768
3769 fn mismatch(&mut self) {
3770 if let Some(h) = self.hydration.as_mut() {
3771 h.mismatches += 1;
3772 }
3773 }
3774
3775 fn unclaimed_from(&self, parent: u32, last: Option<u32>) -> usize {
3778 let kids = self
3779 .get(parent)
3780 .map(|n| n.children.as_slice())
3781 .unwrap_or(&[]);
3782 match last {
3783 Some(l) => kids
3784 .iter()
3785 .position(|&c| c == l)
3786 .map_or(kids.len(), |i| i + 1),
3787 None => 0,
3788 }
3789 }
3790
3791 fn host_own(&self, h: u32) -> bool {
3793 self.get(h)
3794 .is_some_and(|n| !n.is_text && n.attrs.contains_key("data-logos-host"))
3795 }
3796
3797 fn next_element(&self, parent: u32, from: usize) -> Option<usize> {
3799 let kids = self
3800 .get(parent)
3801 .map(|n| n.children.clone())
3802 .unwrap_or_default();
3803 (from..kids.len())
3804 .find(|&i| self.get(kids[i]).is_some_and(|n| !n.is_text) && !self.host_own(kids[i]))
3805 }
3806
3807 fn take_texts(&mut self, parent: u32) -> Vec<(u32, String)> {
3809 let Some(h) = self.hydration.as_mut() else {
3810 return Vec::new();
3811 };
3812 let (mine, rest): (Vec<_>, Vec<_>) = std::mem::take(&mut h.texts)
3813 .into_iter()
3814 .partition(|(p, ..)| *p == parent);
3815 h.texts = rest;
3816 mine.into_iter()
3817 .map(|(_, node, text)| (node, text))
3818 .collect()
3819 }
3820
3821 fn rebind(&mut self, parent: u32, old: u32, new: u32) -> Result<(), String> {
3824 let old = self.resolve(old);
3825 let node = match old.checked_sub(STREAM_ID_BASE) {
3826 Some(i) => self.streamed.get_mut(i as usize).and_then(Option::take),
3827 None => self.nodes.get_mut(old as usize).map(std::mem::take),
3828 }
3829 .ok_or_else(|| format!("the stream adopts node {old}, which holds nothing"))?;
3830 self.create_as(new, node)?;
3831 if let Some(p) = self.get_mut(parent) {
3832 for c in p.children.iter_mut().filter(|c| **c == old) {
3833 *c = new;
3834 }
3835 }
3836 for target in self.forwarded.values_mut().filter(|t| **t == old) {
3837 *target = new;
3838 }
3839 self.forwarded.insert(old, new);
3840 for held in [&mut self.focused, &mut self.scrolled_to, &mut self.selected] {
3841 if *held == old {
3842 *held = new;
3843 }
3844 }
3845 let moved: Vec<(String, u8)> = self
3846 .registrations
3847 .iter()
3848 .filter(|((n, _), _)| *n == old)
3849 .map(|((_, e), f)| (e.clone(), *f))
3850 .collect();
3851 for (event, flags) in moved {
3852 self.registrations.remove(&(old, event.clone()));
3853 self.registrations.insert((new, event), flags);
3854 }
3855 for (n, ..) in self.listeners.iter_mut().filter(|(n, ..)| *n == old) {
3856 *n = new;
3857 }
3858 Ok(())
3859 }
3860
3861 fn retire_stream(&mut self) {
3865 let mut renamed = BTreeMap::new();
3866 for (i, slot) in std::mem::take(&mut self.streamed).into_iter().enumerate() {
3867 if let Some(node) = slot {
3868 renamed.insert(STREAM_ID_BASE + i as u32, self.nodes.len() as u32);
3869 self.nodes.push(node);
3870 }
3871 }
3872 if renamed.is_empty() {
3873 return;
3874 }
3875 let name = |h: &mut u32| {
3876 if let Some(&minted) = renamed.get(h) {
3877 *h = minted;
3878 }
3879 };
3880 for n in &mut self.nodes {
3881 n.children.iter_mut().for_each(name);
3882 }
3883 self.forwarded.values_mut().for_each(name);
3884 for held in [&mut self.focused, &mut self.scrolled_to, &mut self.selected] {
3885 name(held);
3886 }
3887 if let Some(event) = self.current_event.as_mut() {
3888 name(&mut event.target);
3889 }
3890 for (n, ..) in &mut self.listeners {
3891 name(n);
3892 }
3893 self.registrations = std::mem::take(&mut self.registrations)
3894 .into_iter()
3895 .map(|((mut n, event), flags)| {
3896 name(&mut n);
3897 ((n, event), flags)
3898 })
3899 .collect();
3900 }
3901
3902 fn fresh_text(&mut self, node: u32, text: &str) -> Result<u32, String> {
3904 self.create_as(
3905 node,
3906 Node {
3907 text: text.to_string(),
3908 is_text: true,
3909 ..Node::default()
3910 },
3911 )?;
3912 Ok(node)
3913 }
3914
3915 fn settle_texts(
3919 &mut self,
3920 parent: u32,
3921 from: usize,
3922 to: usize,
3923 run: Vec<(u32, String)>,
3924 ) -> Result<usize, String> {
3925 let kids = self
3926 .get(parent)
3927 .map(|n| n.children.clone())
3928 .unwrap_or_default();
3929 let region = &kids[from..to];
3930 let gap: Vec<u32> = region
3931 .iter()
3932 .copied()
3933 .filter(|&c| self.get(c).is_some_and(|n| n.is_text))
3934 .collect();
3935 let held: Vec<String> = gap
3936 .iter()
3937 .map(|&c| self.get(c).map(|n| n.text.clone()).unwrap_or_default())
3938 .collect();
3939 let wanted: Vec<&str> = run.iter().map(|(_, t)| t.as_str()).collect();
3940 let mut settled: Vec<u32> = region
3941 .iter()
3942 .copied()
3943 .filter(|&c| self.host_own(c))
3944 .collect();
3945 match text_tiles(&held, &wanted) {
3946 Some(tiles) => {
3947 for (g, tile) in gap.iter().zip(&tiles) {
3948 let mut kept = false;
3949 for k in tile.clone() {
3950 let (node, text) = &run[k];
3951 if !kept && !text.is_empty() {
3952 self.rebind(parent, *g, *node)?;
3953 if let Some(n) = self.get_mut(*node) {
3954 n.text = text.clone();
3955 }
3956 kept = true;
3957 settled.push(*node);
3958 } else {
3959 settled.push(self.fresh_text(*node, text)?);
3960 }
3961 }
3962 }
3963 let trailing = tiles.last().map_or(0, |t| t.end);
3964 for (node, text) in &run[trailing..] {
3965 settled.push(self.fresh_text(*node, text)?);
3966 }
3967 }
3968 None => {
3969 for (node, text) in &run {
3970 settled.push(self.fresh_text(*node, text)?);
3971 }
3972 }
3973 }
3974 let end = from + settled.len();
3975 if let Some(p) = self.get_mut(parent) {
3976 p.children.splice(from..to, settled);
3977 }
3978 Ok(end)
3979 }
3980
3981 fn adopt(&mut self, parent: u32, node: u32, tag: &str) -> Result<(), String> {
3982 self.open_hydration("Adopt")?;
3983 self.log.push(DomOp::Adopt {
3984 parent,
3985 node,
3986 tag: tag.to_string(),
3987 });
3988 let Some(last) = self
3989 .hydration
3990 .as_ref()
3991 .and_then(|h| h.cursors.get(&parent).copied())
3992 else {
3993 self.mismatch();
3994 return Ok(());
3995 };
3996 let from = self.unclaimed_from(parent, last);
3997 let Some(at) = self.next_element(parent, from) else {
3998 self.mismatch();
3999 return Ok(());
4000 };
4001 let claimed = self.get(parent).map(|n| n.children[at]).unwrap_or(u32::MAX);
4002 if !self
4003 .get(claimed)
4004 .is_some_and(|n| n.tag.eq_ignore_ascii_case(tag))
4005 {
4006 self.mismatch();
4007 return Ok(());
4008 }
4009 let run = self.take_texts(parent);
4010 self.settle_texts(parent, from, at, run)?;
4011 self.rebind(parent, claimed, node)?;
4012 if let Some(h) = self.hydration.as_mut() {
4013 h.cursors.insert(parent, Some(node));
4014 h.cursors.insert(node, None);
4015 h.written.insert(node, Default::default());
4016 }
4017 Ok(())
4018 }
4019
4020 fn adopt_text(&mut self, parent: u32, node: u32, text: &str) -> Result<(), String> {
4021 self.open_hydration("AdoptText")?;
4022 self.log.push(DomOp::AdoptText {
4023 parent,
4024 node,
4025 text: text.to_string(),
4026 });
4027 match self.hydration.as_mut() {
4028 Some(h) if h.cursors.contains_key(&parent) => {
4029 h.texts.push((parent, node, text.to_string()))
4030 }
4031 _ => self.mismatch(),
4032 }
4033 Ok(())
4034 }
4035
4036 fn end_hydration(&mut self) -> Result<i64, String> {
4040 let Some(h) = self.hydration.clone() else {
4041 return Ok(0);
4042 };
4043 for (&parent, &last) in &h.cursors {
4044 if h.raw.contains(&parent) {
4045 continue;
4046 }
4047 let from = self.unclaimed_from(parent, last);
4048 let len = self.get(parent).map_or(0, |n| n.children.len());
4049 let to = self.next_element(parent, from).unwrap_or(len);
4050 let run = self.take_texts(parent);
4051 let end = self.settle_texts(parent, from, to, run)?;
4052 let rest: Vec<u32> = self
4053 .get(parent)
4054 .map(|n| n.children[end..].to_vec())
4055 .unwrap_or_default();
4056 let mut stays = Vec::new();
4057 for c in rest {
4058 if self.host_own(c) {
4059 stays.push(c);
4060 continue;
4061 }
4062 self.leaving(c);
4063 }
4064 if let Some(p) = self.get_mut(parent) {
4065 p.children.truncate(end);
4066 p.children.extend(stays);
4067 }
4068 }
4069 for (&el, written) in &h.written {
4070 if let Some(n) = self.get_mut(el) {
4071 n.attrs.retain(|name, _| written.contains(name));
4072 }
4073 }
4074 self.hydration = None;
4075 Ok(h.mismatches)
4076 }
4077}
4078
4079fn text_tiles(held: &[String], wanted: &[&str]) -> Option<Vec<std::ops::Range<usize>>> {
4083 let mut tiles = Vec::with_capacity(held.len());
4084 let mut k = 0;
4085 for g in held {
4086 let start = k;
4087 let mut spelled = String::new();
4088 while spelled.len() < g.len() && k < wanted.len() {
4089 spelled.push_str(wanted[k]);
4090 k += 1;
4091 }
4092 if spelled != *g {
4093 return None;
4094 }
4095 tiles.push(start..k);
4096 }
4097 wanted[k..].iter().all(|t| t.is_empty()).then_some(tiles)
4098}
4099
4100pub fn remove_node(node: i64) {
4101 DOM.with(|d| d.borrow_mut().remove_node(node as u32));
4102}
4103
4104pub fn insert_before(parent: i64, child: i64, before: i64) {
4105 DOM.with(|d| {
4106 d.borrow_mut()
4107 .insert_before(parent as u32, child as u32, before as u32)
4108 });
4109}
4110
4111pub fn remove_attribute(node: i64, name: &str) {
4112 DOM.with(|d| d.borrow_mut().remove_attribute(node as u32, name));
4113}
4114
4115pub fn listen(node: i64, event: &str, flags: u8) {
4119 DOM.with(|d| d.borrow_mut().listen(node as u32, event, flags));
4120}
4121
4122pub fn unlisten(node: i64, event: &str) {
4123 DOM.with(|d| d.borrow_mut().unlisten(node as u32, event));
4124}
4125
4126pub fn free_node(node: i64) {
4127 DOM.with(|d| d.borrow_mut().free_node(node as u32));
4128}
4129
4130pub fn hydrate(root: i64) -> Result<(), String> {
4133 DOM.with(|d| d.borrow_mut().hydrate(root as u32))
4134}
4135
4136pub fn adopt(parent: i64, node: i64, tag: &str) -> Result<(), String> {
4138 DOM.with(|d| d.borrow_mut().adopt(parent as u32, node as u32, tag))
4139}
4140
4141pub fn adopt_text(parent: i64, node: i64, text: &str) -> Result<(), String> {
4143 DOM.with(|d| d.borrow_mut().adopt_text(parent as u32, node as u32, text))
4144}
4145
4146pub fn retire_stream() {
4149 DOM.with(|d| d.borrow_mut().retire_stream());
4150}
4151
4152pub fn end_hydration() -> Result<i64, String> {
4155 DOM.with(|d| d.borrow_mut().end_hydration())
4156}
4157
4158#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4165pub struct Validity {
4166 pub value_missing: bool,
4167 pub type_mismatch: bool,
4168 pub too_short: bool,
4169 pub too_long: bool,
4170 pub pattern_mismatch: bool,
4171 pub range_underflow: bool,
4172 pub range_overflow: bool,
4173 pub step_mismatch: bool,
4174 pub bad_input: bool,
4175}
4176
4177impl Validity {
4178 pub fn valid(&self) -> bool {
4180 *self == Validity::default()
4181 }
4182}
4183
4184impl DomRecorder {
4185 fn control_value(&self, h: u32) -> String {
4190 let Some(n) = self.get(h) else {
4191 return String::new();
4192 };
4193 if matches!(input_type(n).as_str(), "checkbox" | "radio") {
4194 return n
4195 .attrs
4196 .get("value")
4197 .cloned()
4198 .unwrap_or_else(|| "on".to_string());
4199 }
4200 if n.user_edited || !n.value.is_empty() {
4201 return n.value.clone();
4202 }
4203 match n.tag.eq_ignore_ascii_case("select") {
4204 true => self.default_choice(h).unwrap_or_default(),
4205 false => n.attrs.get("value").cloned().unwrap_or_default(),
4206 }
4207 }
4208
4209 fn barred(&self, h: u32) -> bool {
4213 let Some(n) = self.get(h) else { return true };
4214 let has = |name: &str| n.attrs.contains_key(name);
4215 let ty = input_type(n);
4216 let readonly_applies = n.tag.eq_ignore_ascii_case("textarea")
4217 || matches!(
4218 ty.as_str(),
4219 "text"
4220 | "search"
4221 | "url"
4222 | "tel"
4223 | "email"
4224 | "password"
4225 | "date"
4226 | "month"
4227 | "week"
4228 | "time"
4229 | "datetime-local"
4230 | "number"
4231 );
4232 if has("disabled") || (readonly_applies && has("readonly")) || ty == "hidden" {
4233 return true;
4234 }
4235 let chain = self.ancestors_of(h).unwrap_or_default();
4236 for (i, &a) in chain.iter().enumerate() {
4237 let Some(anc) = self.get(a) else { continue };
4238 if anc.tag.eq_ignore_ascii_case("datalist") {
4239 return true;
4240 }
4241 if anc.tag.eq_ignore_ascii_case("fieldset") && anc.attrs.contains_key("disabled") {
4242 let first_legend = anc.children.iter().copied().find(|&c| {
4243 self.get(c)
4244 .is_some_and(|x| !x.is_text && x.tag.eq_ignore_ascii_case("legend"))
4245 });
4246 let inside_legend = first_legend.is_some_and(|l| chain.get(i + 1) == Some(&l));
4247 if !inside_legend {
4248 return true;
4249 }
4250 }
4251 }
4252 false
4253 }
4254
4255 fn form_owner(&self, h: u32) -> Option<u32> {
4258 if let Some(id) = self.get(h).and_then(|n| n.attrs.get("form")) {
4259 let found = self.query_selector(&format!("#{id}"));
4260 return self
4261 .get(found)
4262 .filter(|f| f.tag.eq_ignore_ascii_case("form"))
4263 .map(|_| found);
4264 }
4265 let chain = self.ancestors_of(h)?;
4266 chain.into_iter().rev().find(|&a| {
4267 self.get(a)
4268 .is_some_and(|n| n.tag.eq_ignore_ascii_case("form"))
4269 })
4270 }
4271
4272 fn radio_group_ticked(&self, h: u32) -> bool {
4274 let Some(n) = self.get(h) else { return false };
4275 let Some(name) = n.attrs.get("name").filter(|x| !x.is_empty()) else {
4276 return n.checked;
4277 };
4278 let owner = self.form_owner(h);
4279 let mut stack = vec![0u32];
4280 while let Some(at) = stack.pop() {
4281 let Some(x) = self.get(at) else { continue };
4282 if !x.is_text
4283 && input_type(x) == "radio"
4284 && x.attrs.get("name") == Some(name)
4285 && self.form_owner(at) == owner
4286 && x.checked
4287 {
4288 return true;
4289 }
4290 stack.extend(x.children.iter().rev().copied());
4291 }
4292 false
4293 }
4294
4295 fn validity(&self, h: u32) -> Result<Validity, String> {
4296 let mut v = Validity::default();
4297 let Some(n) = self.get(h) else { return Ok(v) };
4298 let tag = n.tag.to_ascii_lowercase();
4299 if n.is_text || !matches!(tag.as_str(), "input" | "select" | "textarea") || self.barred(h) {
4300 return Ok(v);
4301 }
4302 let ty = input_type(n);
4303 let value = self.control_value(h);
4304 if n.attrs.contains_key("required") {
4305 v.value_missing = match ty.as_str() {
4306 "checkbox" => !n.checked,
4307 "radio" => !self.radio_group_ticked(h),
4308 _ => value.is_empty(),
4309 };
4310 }
4311 let text_like = tag == "textarea"
4312 || matches!(
4313 ty.as_str(),
4314 "text" | "search" | "url" | "tel" | "email" | "password"
4315 );
4316 if text_like && n.user_edited && !value.is_empty() {
4317 let units = value.encode_utf16().count();
4318 let limit = |name: &str| {
4319 n.attrs
4320 .get(name)
4321 .and_then(|x| x.trim().parse::<usize>().ok())
4322 };
4323 v.too_long = limit("maxlength").is_some_and(|max| units > max);
4324 v.too_short = limit("minlength").is_some_and(|min| units < min);
4325 }
4326 let values: Vec<&str> = match (ty.as_str(), n.attrs.contains_key("multiple")) {
4327 ("email", true) => value
4328 .split(',')
4329 .map(|x| x.trim_matches(|c: char| c.is_ascii_whitespace()))
4330 .collect(),
4331 _ => vec![value.as_str()],
4332 };
4333 if !value.is_empty() {
4334 match ty.as_str() {
4335 "email" => v.type_mismatch = values.iter().any(|x| !valid_email(x)),
4336 "url" => v.type_mismatch = !valid_url(&value),
4337 _ => {}
4338 }
4339 if tag == "input"
4340 && matches!(
4341 ty.as_str(),
4342 "text" | "search" | "url" | "tel" | "email" | "password"
4343 )
4344 {
4345 if let Some(pattern) = n.attrs.get("pattern") {
4346 for x in &values {
4347 if !pattern_matches(pattern, x)? {
4348 v.pattern_mismatch = true;
4349 }
4350 }
4351 }
4352 }
4353 }
4354 if ty == "number" {
4355 v.bad_input = n.bad_input;
4356 let num = |name: &str| {
4357 n.attrs
4358 .get(name)
4359 .filter(|x| valid_float(x))
4360 .and_then(|x| x.parse::<f64>().ok())
4361 };
4362 if let Some(x) = value.parse::<f64>().ok().filter(|_| valid_float(&value)) {
4363 v.range_underflow = num("min").is_some_and(|min| x < min);
4364 v.range_overflow = num("max").is_some_and(|max| x > max);
4365 let step = match n.attrs.get("step").map(|s| s.trim().to_ascii_lowercase()) {
4366 Some(any) if any == "any" => None,
4367 Some(s) => Some(
4368 Some(s.as_str())
4369 .filter(|s| valid_float(s))
4370 .and_then(|s| s.parse::<f64>().ok())
4371 .filter(|s| *s > 0.0)
4372 .unwrap_or(1.0),
4373 ),
4374 None => Some(1.0),
4375 };
4376 if let Some(step) = step {
4377 let base = num("min").or_else(|| num("value")).unwrap_or(0.0);
4378 let q = (x - base) / step;
4379 v.step_mismatch = (q - q.round()).abs() > 1e-9 * q.abs().max(1.0);
4380 }
4381 }
4382 }
4383 Ok(v)
4384 }
4385}
4386
4387fn valid_float(s: &str) -> bool {
4390 let b = s.as_bytes();
4391 let mut i = usize::from(b.first() == Some(&b'-'));
4392 let digits = |i: &mut usize| {
4393 let start = *i;
4394 while *i < b.len() && b[*i].is_ascii_digit() {
4395 *i += 1;
4396 }
4397 *i > start
4398 };
4399 let whole = digits(&mut i);
4400 let fraction = if b.get(i) == Some(&b'.') {
4401 i += 1;
4402 if !digits(&mut i) {
4403 return false;
4404 }
4405 true
4406 } else {
4407 false
4408 };
4409 if !whole && !fraction {
4410 return false;
4411 }
4412 if matches!(b.get(i), Some(b'e') | Some(b'E')) {
4413 i += 1;
4414 if matches!(b.get(i), Some(b'+') | Some(b'-')) {
4415 i += 1;
4416 }
4417 if !digits(&mut i) {
4418 return false;
4419 }
4420 }
4421 i == b.len()
4422}
4423
4424fn valid_email(s: &str) -> bool {
4428 let Some((local, domain)) = s.split_once('@') else {
4429 return false;
4430 };
4431 let local_ok = !local.is_empty()
4432 && local
4433 .chars()
4434 .all(|c| c.is_ascii_alphanumeric() || ".!#$%&'*+/=?^_`{|}~-".contains(c));
4435 let label_ok = |l: &str| {
4436 let b = l.as_bytes();
4437 (1..=63).contains(&b.len())
4438 && b[0].is_ascii_alphanumeric()
4439 && b[b.len() - 1].is_ascii_alphanumeric()
4440 && b.iter().all(|c| c.is_ascii_alphanumeric() || *c == b'-')
4441 };
4442 local_ok && domain.split('.').all(label_ok)
4443}
4444
4445fn valid_url(s: &str) -> bool {
4449 let Some((scheme, rest)) = s.split_once(':') else {
4450 return false;
4451 };
4452 let mut chars = scheme.chars();
4453 let scheme_ok = chars.next().is_some_and(|c| c.is_ascii_alphabetic())
4454 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
4455 if !scheme_ok {
4456 return false;
4457 }
4458 if !matches!(
4459 scheme.to_ascii_lowercase().as_str(),
4460 "http" | "https" | "ws" | "wss" | "ftp"
4461 ) {
4462 return true;
4463 }
4464 let authority = rest
4465 .trim_start_matches(['/', '\\'])
4466 .split(['/', '\\', '?', '#'])
4467 .next()
4468 .unwrap_or("");
4469 let host_port = authority.rsplit_once('@').map_or(authority, |(_, h)| h);
4470 let (host_ok, port) = match host_port.strip_prefix('[') {
4471 Some(inner) => match inner.split_once(']') {
4472 Some((addr, tail)) => (
4473 !addr.is_empty()
4474 && addr
4475 .chars()
4476 .all(|c| c.is_ascii_hexdigit() || matches!(c, ':' | '.')),
4477 tail.strip_prefix(':'),
4478 ),
4479 None => return false,
4480 },
4481 None => {
4482 let (host, port) = match host_port.rsplit_once(':') {
4483 Some((h, p)) => (h, Some(p)),
4484 None => (host_port, None),
4485 };
4486 let forbidden =
4487 |c: char| c.is_control() || c.is_whitespace() || "#%/:<>?@[\\]^|".contains(c);
4488 (!host.is_empty() && !host.chars().any(forbidden), port)
4489 }
4490 };
4491 host_ok && port.is_none_or(|p| p.chars().all(|c| c.is_ascii_digit()))
4492}
4493
4494pub fn pattern_matches(pattern: &str, value: &str) -> Result<bool, String> {
4501 let refuse = |what: &str| {
4502 Err(format!(
4503 "`pattern=\"{pattern}\"` uses {what}, which the in-process tiers cannot check; a browser \
4504 can — write the constraint without it, or check the value in an action"
4505 ))
4506 };
4507 const STRING_PROPERTIES: &[&str] = &[
4508 "Basic_Emoji",
4509 "Emoji_Keycap_Sequence",
4510 "RGI_Emoji_Modifier_Sequence",
4511 "RGI_Emoji_Flag_Sequence",
4512 "RGI_Emoji_Tag_Sequence",
4513 "RGI_Emoji_ZWJ_Sequence",
4514 "RGI_Emoji",
4515 ];
4516 const JS_SPACE: &str = "\\t\\n\\x0B\\x0C\\r \\u{A0}\\u{1680}\\u{2000}-\\u{200A}\\u{2028}\\u{2029}\\u{202F}\\u{205F}\\u{3000}\\u{FEFF}";
4517 let mut out = String::from("^(?:");
4518 let chars: Vec<char> = pattern.chars().collect();
4519 let mut i = 0;
4520 let mut depth = 0usize;
4521 let mut class_atoms = 0usize;
4522 while i < chars.len() {
4523 let c = chars[i];
4524 if c == '\\' {
4525 let Some(&e) = chars.get(i + 1) else {
4526 return Ok(true);
4527 };
4528 if e == 'q' && chars.get(i + 2) == Some(&'{') {
4529 return refuse("a `\\q{…}` string literal (the `v` flag's set notation)");
4530 }
4531 if matches!(e, 'p' | 'P' | 'u') && chars.get(i + 2) == Some(&'{') {
4532 let name: String = chars[i + 3..].iter().take_while(|c| **c != '}').collect();
4533 if e != 'u' && STRING_PROPERTIES.contains(&name.as_str()) {
4534 return refuse(&format!("the string property `\\p{{{name}}}`"));
4535 }
4536 let end = i + 3 + name.chars().count();
4537 if chars.get(end) != Some(&'}') {
4538 return Ok(true);
4539 }
4540 out.extend(&chars[i..=end]);
4541 class_atoms += 1;
4542 i = end + 1;
4543 continue;
4544 }
4545 let in_class = depth > 0;
4546 let class = |set: &str, negated: bool| match (in_class, negated) {
4547 (true, false) => set.to_string(),
4548 (false, false) => format!("[{set}]"),
4549 (false, true) => format!("[^{set}]"),
4550 (true, true) => String::new(),
4551 };
4552 let translated = match e {
4553 'd' => Some(class("0-9", false)),
4554 'D' => Some(class("0-9", true)),
4555 'w' => Some(class("A-Za-z0-9_", false)),
4556 'W' => Some(class("A-Za-z0-9_", true)),
4557 's' => Some(class(JS_SPACE, false)),
4558 'S' => Some(class(JS_SPACE, true)),
4559 _ => None,
4560 };
4561 match translated {
4562 Some(t) if t.is_empty() => {
4563 return refuse(&format!("a negated `\\{e}` inside a class"))
4564 }
4565 Some(t) => out.push_str(&t),
4566 None => {
4567 out.push(c);
4568 out.push(e);
4569 }
4570 }
4571 class_atoms += 1;
4572 i += 2;
4573 continue;
4574 }
4575 if depth > 0 {
4576 match c {
4577 ']' => {
4578 depth -= 1;
4579 class_atoms += 1;
4580 out.push(c);
4581 }
4582 '[' => {
4583 depth += 1;
4584 class_atoms = 0;
4585 out.push(c);
4586 if chars.get(i + 1) == Some(&'^') {
4587 out.push('^');
4588 i += 1;
4589 }
4590 }
4591 '-' | '&' if chars.get(i + 1) == Some(&c) => {
4592 out.push(c);
4593 out.push(c);
4594 class_atoms = 0;
4595 i += 2;
4596 continue;
4597 }
4598 '-' if class_atoms == 0 || chars.get(i + 1) == Some(&']') => return Ok(true),
4599 '(' | ')' | '{' | '}' | '/' | '|' => return Ok(true),
4600 _ => {
4601 out.push(c);
4602 class_atoms += 1;
4603 }
4604 }
4605 i += 1;
4606 continue;
4607 }
4608 if c == '[' {
4609 depth = 1;
4610 class_atoms = 0;
4611 out.push(c);
4612 if chars.get(i + 1) == Some(&'^') {
4613 out.push('^');
4614 i += 1;
4615 }
4616 i += 1;
4617 continue;
4618 }
4619 out.push(c);
4620 i += 1;
4621 }
4622 if depth > 0 {
4623 return Ok(true);
4624 }
4625 out.push_str(")$");
4626 match fancy_regex::Regex::new(&out) {
4627 Ok(re) => re.is_match(value).map_err(|e| {
4628 format!("`pattern=\"{pattern}\"` could not be checked against {value:?}: {e}")
4629 }),
4630 Err(_) => Ok(true),
4631 }
4632}
4633
4634pub fn control_value(node: i64) -> String {
4636 DOM.with(|d| d.borrow().control_value(node as u32))
4637}
4638
4639pub fn validity(node: i64) -> Result<Validity, String> {
4642 DOM.with(|d| d.borrow().validity(node as u32))
4643}
4644
4645pub fn form_of(node: i64) -> Option<i64> {
4647 DOM.with(|d| {
4648 let d = d.borrow();
4649 match d.get(node as u32) {
4650 Some(n) if !n.is_text && n.tag.eq_ignore_ascii_case("form") => Some(node),
4651 Some(_) => d.form_owner(node as u32).map(i64::from),
4652 None => None,
4653 }
4654 })
4655}
4656
4657pub fn form_constraints(form: i64, submitter: Option<i64>) -> Result<Vec<i64>, String> {
4661 DOM.with(|d| {
4662 let d = d.borrow();
4663 let has = |h: i64, name: &str| d.get(h as u32).is_some_and(|n| n.attrs.contains_key(name));
4664 if has(form, "novalidate") || submitter.is_some_and(|s| has(s, "formnovalidate")) {
4665 return Ok(Vec::new());
4666 }
4667 let mut invalid = Vec::new();
4668 let mut stack = vec![0u32];
4669 while let Some(at) = stack.pop() {
4670 let Some(x) = d.get(at) else { continue };
4671 if !x.is_text
4672 && matches!(
4673 x.tag.to_ascii_lowercase().as_str(),
4674 "input" | "select" | "textarea"
4675 )
4676 && d.form_owner(at) == Some(form as u32)
4677 && !d.validity(at)?.valid()
4678 {
4679 invalid.push(i64::from(at));
4680 }
4681 stack.extend(x.children.iter().rev().copied());
4682 }
4683 Ok(invalid)
4684 })
4685}
4686
4687pub fn submits_its_form(node: i64) -> bool {
4690 DOM.with(|d| {
4691 let d = d.borrow();
4692 let Some(n) = d.get(node as u32).filter(|n| !n.is_text) else {
4693 return false;
4694 };
4695 let ty = n.attrs.get("type").map(|t| t.trim().to_ascii_lowercase());
4696 match n.tag.to_ascii_lowercase().as_str() {
4697 "button" => {
4698 ty.is_none_or(|t| t == "submit" || !matches!(t.as_str(), "reset" | "button"))
4699 }
4700 "input" => matches!(ty.as_deref(), Some("submit") | Some("image")),
4701 _ => false,
4702 }
4703 })
4704}