1use std::{
2 cell::RefCell,
3 fs::File,
4 io::{BufRead, BufReader},
5};
6
7use libxml::tree::Node;
8use once_cell::sync::Lazy;
9use regex::Regex;
10use rustc_hash::FxHashSet as HashSet;
11
12use super::arena::SymHashMap;
13use crate::{
14 common::{
15 arena::{self, SymStr},
16 error::*,
17 relaxng::Relaxng,
18 xml::XML_NS,
19 },
20 document::Document,
21 pin,
22 util::pathname,
23};
24
25pub const LTX_NAMESPACE: &str = "http://dlmf.nist.gov/LaTeXML";
28pub type IndirectModel = SymHashMap<SymHashMap<SymStr>>;
29
30static PREFIXED_LOCALNAME_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^([^:]+):(.+)$").unwrap());
31static TAG_MODEL_LINE_RE: Lazy<Regex> =
32 Lazy::new(|| Regex::new(r"^([^\{]+)\{(.*?)\}\((.*?)\)$").unwrap());
33static CLASS_MODEL_LINE_RE: Lazy<Regex> =
37 Lazy::new(|| Regex::new(r"^([^:=]+):=\(?([^)]*?)\)?$").unwrap());
38static NAMESPACE_MODEL_LINE_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^([^=]+)=(.*?)$").unwrap());
39
40#[derive(Default, Debug)]
41pub struct TagFrame {
42 model: HashSet<SymStr>,
43 attributes: HashSet<SymStr>,
44}
45
46static DEFAULT_TAG_FRAME: Lazy<TagFrame> = Lazy::new(TagFrame::default);
47
48#[derive(Default, Debug)]
49pub struct Model {
50 pub schema: Option<Relaxng>,
51 pub schema_data: Option<Vec<SymStr>>,
52 pub schema_class: SymHashMap<HashSet<SymStr>>,
53 pub code_namespace_prefixes: SymHashMap<SymStr>,
54 pub code_namespaces: SymHashMap<SymStr>,
55 pub document_namespace_prefixes: SymHashMap<SymStr>,
56 pub document_namespaces: SymHashMap<SymStr>,
57 pub permissive: bool,
60 pub no_compiled: bool,
61 pub debug_mode: bool,
62 pub namespace_errors: u8,
63 pub tagprop: SymHashMap<TagFrame>,
64}
65
66#[thread_local]
67pub static MODEL: Lazy<RefCell<Model>> = Lazy::new(|| RefCell::new(Model::new()));
68
69macro_rules! model {
70 () => {
71 (*MODEL).borrow()
72 };
73}
74macro_rules! model_mut {
75 () => {
76 (*MODEL).borrow_mut()
77 };
78}
79
80pub fn initialize_model() {
81 let mut global_model = MODEL.borrow_mut();
82 *global_model = Model::new();
83}
84
85pub(crate) fn force_init() { Lazy::force(&MODEL); }
95
96impl Model {
97 pub fn new() -> Self {
98 let mut model = Model::default();
99 model.register_namespace("xml", Some(XML_NS));
101 model.register_document_namespace("xml", Some(XML_NS));
102 model
103 }
104 pub fn register_namespace(&mut self, codeprefix: &str, namespace_opt: Option<&str>) {
119 self.register_namespace_sym(arena::pin(codeprefix), namespace_opt.map(arena::pin))
120 }
121 pub fn register_namespace_sym(&mut self, codeprefix: SymStr, namespace_opt: Option<SymStr>) {
122 let namespace_opt_checked = namespace_opt.filter(|val| *val != pin!(""));
124 match namespace_opt_checked {
125 Some(namespace) => {
126 self
127 .code_namespace_prefixes
128 .insert_sym(namespace, codeprefix);
129 self.code_namespaces.insert_sym(codeprefix, namespace);
130 },
131 None => {
132 if let Some(prev) = self.code_namespaces.get_sym(codeprefix) {
133 self.code_namespace_prefixes.remove_sym(*prev);
134 };
135 self.code_namespaces.remove_sym(codeprefix);
136 },
137 };
138 }
139
140 pub fn register_document_namespace(&mut self, docprefix: &str, namespace_opt: Option<&str>) {
146 let default_sym = pin!("#default");
147 let docprefix_sym = if docprefix.is_empty() {
148 default_sym
149 } else {
150 arena::pin(docprefix)
151 };
152
153 match namespace_opt {
154 Some(namespace) => {
155 let ns_sym = arena::pin(namespace);
158 let regnamespace = if docprefix_sym == default_sym {
159 arena::pin(s!("DEFAULT#{namespace}"))
160 } else {
161 ns_sym
162 };
163 self
164 .document_namespace_prefixes
165 .insert_sym(regnamespace, docprefix_sym);
166 self.document_namespaces.insert_sym(docprefix_sym, ns_sym);
167 },
168 None => {
169 if let Some(prev) = self.document_namespaces.get_sym(docprefix_sym) {
170 self.document_namespace_prefixes.remove_sym(*prev);
171 };
172 self.document_namespaces.remove_sym(docprefix_sym);
173 },
174 };
175 }
176
177 pub fn set_relaxng_schema(&mut self, schema: &str) {
178 self.schema_data = Some(vec![pin!("RelaxNG"), arena::pin(schema)]);
179 }
180 pub fn load_compiled_schema(&mut self, path: &str) {
184 let compiled_fh = File::open(path).unwrap();
185 let compiled_reader = BufReader::new(&compiled_fh);
186 let content: String = compiled_reader
187 .lines()
188 .map_while(std::result::Result::ok)
189 .collect::<Vec<_>>()
190 .join("\n");
191 self.load_compiled_schema_str(&content, path);
192 }
193
194 pub fn load_compiled_schema_str(&mut self, content: &str, source: &str) {
200 note_begin(&s!("Loading compiled schema {}\n", source));
201 for line in content.lines() {
202 if let Some(caps) = TAG_MODEL_LINE_RE.captures(line) {
203 let tag = caps.get(1).map_or("", |m| m.as_str());
204 let attr = caps.get(2).map_or("", |m| m.as_str());
205 let children = caps.get(3).map_or("", |m| m.as_str());
206 self.add_tag_attribute(tag, attr.split(',').collect());
207 self.add_tag_content(tag, children.split(',').collect());
208 } else if let Some(caps) = CLASS_MODEL_LINE_RE.captures(line) {
209 let classname = caps.get(1).map_or("", |m| m.as_str());
210 let elements = caps.get(2).map_or("", |m| m.as_str());
211 let mut class_set = HashSet::default();
212 for set_element in elements.split(',') {
213 class_set.insert(arena::pin(set_element));
214 }
215 self.set_schema_class(classname, class_set);
216 } else if let Some(caps) = NAMESPACE_MODEL_LINE_RE.captures(line) {
217 let prefix = caps.get(1).map_or("", |m| m.as_str());
218 let namespace = caps.get(2).map_or("", |m| m.as_str());
219 self.register_document_namespace(prefix, Some(namespace));
220 } else {
221 panic!("Fatal:internal:{source} Compiled model '{source}' is malformatted at \"{line}\"");
222 }
223 }
224 note_end(&s!("Loading compiled schema {source}\n"));
225 }
226 pub fn add_tag_content(&mut self, tag: &str, elements: Vec<&str>) {
227 let frame = self.tagprop.entry(tag).or_default();
228
229 for element in elements {
230 frame.model.insert(arena::pin(element));
231 }
232 }
233
234 pub fn add_tag_attribute(&mut self, tag: &str, attributes: Vec<&str>) {
246 let frame = self.tagprop.entry(tag).or_default();
247
248 for attribute in attributes {
249 frame.attributes.insert(arena::pin(attribute));
250 }
251 }
252
253 pub fn set_schema_class(&mut self, classname: &str, content: HashSet<SymStr>) {
254 self.schema_class.insert(classname, content);
255 }
256
257 pub fn dump_compiled_schema(&self) -> String {
272 fn sym_to_string(sym: SymStr) -> String { arena::with(sym, |s| s.to_string()) }
273 fn syms_sorted(set: impl IntoIterator<Item = SymStr>) -> Vec<String> {
274 let mut v: Vec<String> = set.into_iter().map(sym_to_string).collect();
275 v.sort();
276 v
277 }
278 let mut out = String::new();
279 let prefixes = syms_sorted(self.document_namespaces.keys().copied());
280 for prefix in &prefixes {
281 let ns_opt = self
282 .document_namespaces
283 .get_sym(arena::pin(prefix.as_str()));
284 let ns = match ns_opt {
285 Some(v) => sym_to_string(*v),
286 None => continue,
287 };
288 out.push_str(prefix);
289 out.push('=');
290 out.push_str(&ns);
291 out.push('\n');
292 }
293 let classnames = syms_sorted(self.schema_class.keys().copied());
294 for classname in &classnames {
295 let elements = match self.schema_class.get_sym(arena::pin(classname.as_str())) {
296 Some(set) => set,
297 None => continue,
298 };
299 let elt_names = syms_sorted(elements.iter().copied());
300 out.push_str(classname);
301 out.push_str(":=(");
302 out.push_str(&elt_names.join(","));
303 out.push_str(")\n");
304 }
305 let tags = syms_sorted(self.tagprop.keys().copied());
306 for tag in &tags {
307 if tag.starts_with('!') {
308 continue;
309 }
310 let frame = match self.tagprop.get_sym(arena::pin(tag.as_str())) {
311 Some(f) => f,
312 None => continue,
313 };
314 let attrs = syms_sorted(frame.attributes.iter().copied());
315 let children = syms_sorted(frame.model.iter().copied());
316 out.push_str(tag);
317 out.push('{');
318 out.push_str(&attrs.join(","));
319 out.push_str("}(");
320 out.push_str(&children.join(","));
321 out.push_str(")\n");
322 }
323 out
324 }
325 pub fn describe_model(&self) {}
326 fn load_internal_extensions(&mut self) {
327 if !self.tagprop.contains_key("ltx:_CaptureBlock_") {
328 self.synthesize_element("ltx:_CaptureBlock_", &[
335 "ltx:block",
336 "ltx:logical-block",
337 "ltx:sectional-block",
338 "Caption",
339 "FrontMatter",
340 "BackMatter",
341 ]);
342 let cb_entry = self.tagprop.entry("ltx:_CaptureBlock_").or_default();
343 cb_entry.model.insert(pin!("svg:g"));
344 cb_entry.model.insert(pin!("svg:foreignObject"));
345 }
346 }
347
348 fn synthesize_element(&mut self, tag: &str, others: &[&str]) {
350 let mut to_add_in_model = Vec::new();
351 let mut to_add_in_attrs = Vec::new();
352 for other in others {
353 if let Some(content) = self.schema_class.get(other) {
354 for child in content {
355 to_add_in_model.push(*child);
356 }
357 } else if let Some(entry) = self.tagprop.get(other) {
358 for child in &entry.model {
359 to_add_in_model.push(*child);
360 }
361 for attr in &entry.attributes {
362 to_add_in_attrs.push(*attr);
363 }
364 }
365 }
366 let capture = self.tagprop.entry(tag).or_default();
367 for child in to_add_in_model {
368 capture.model.insert(child);
369 }
370 for attr in to_add_in_attrs {
371 capture.attributes.insert(attr);
372 }
373 }
374}
375
376pub fn set_relaxng_schema(schema: &str) { model_mut!().set_relaxng_schema(schema) }
377pub fn add_schema_declaration(document: &mut Document) {
378 if let Some(ref schema) = model!().schema {
379 schema.add_schema_declaration(document);
380 }
381}
382
383pub fn load_schema(search_paths: &[&str]) -> Result<()> {
384 let mut model = model_mut!();
386 if model.schema.is_some() {
387 return Ok(());
388 }
389 let mut name = String::new();
390 if model.schema_data.is_none() {
391 Warn!("expected", "<model>", "TODO");
393 model.register_document_namespace("ltx", Some(LTX_NAMESPACE));
397 model.set_relaxng_schema("LaTeXML");
398 model.register_namespace("ltx", Some(LTX_NAMESPACE));
399 model.register_namespace("svg", Some("http://www.w3.org/2000/svg"));
400 model.register_namespace("xlink", Some("http://www.w3.org/1999/xlink")); model.register_namespace("m", Some("http://www.w3.org/1998/Math/MathML"));
402 model.register_namespace("xhtml", Some("http://www.w3.org/1999/xhtml"));
403 model.permissive = true;
404 } if let Some(ref data) = model.schema_data {
407 if data[0] == pin!("RelaxNG") {
408 name = arena::to_string(data[1]);
409 model.schema = Some(Relaxng {
410 name: name.clone(),
411 ..Relaxng::default()
412 });
413 } else {
414 let message = arena::with(data[0], |schema_type_str| {
415 s!("Can't load a schema of type {schema_type_str:?}")
416 });
417 Error!("unknown", "schematype", message)
418 }
419 }
420
421 if !model.no_compiled && model.schema.is_some() {
422 let paths: Option<Vec<String>> = if search_paths.is_empty() {
423 None
424 } else {
425 Some(search_paths.iter().map(ToString::to_string).collect())
426 };
427 let pathname_opt = pathname::find(&name, pathname::PathnameFindOptions {
428 paths,
429 extensions: Some(vec![s!("model")]),
430 installation_subdir: Some(s!("resources/RelaxNG")),
431 });
432
433 match pathname_opt {
434 Some(compiled_path) => model.load_compiled_schema(&compiled_path),
435 None => {
436 let embed_key = format!("{}.model", name);
442 if let Some(content) = crate::common::relaxng::embedded::lookup(&embed_key) {
443 model.load_compiled_schema_str(content, &format!("<embedded>/{embed_key}"));
444 } else {
445 let paths: Vec<&std::path::Path> =
446 search_paths.iter().map(std::path::Path::new).collect();
447 let code_prefix_seed: Vec<(String, String)> = model
455 .code_namespace_prefixes
456 .iter()
457 .map(|(uri, prefix)| {
458 (
459 arena::with(*uri, |s| s.to_string()),
460 arena::with(*prefix, |s| s.to_string()),
461 )
462 })
463 .collect();
464 let schema = model.schema.as_mut().unwrap();
465 schema.code_namespace_prefixes = code_prefix_seed.into_iter().collect();
466 let schema_name = schema.name.clone();
467 match schema.load_schema(&schema_name, &paths) {
468 Err(err) => {
469 let msg = format!("load_schema failed for {}: {}", schema_name, err);
470 Warn!("expected", "RelaxNG", msg);
471 },
472 Ok(()) => {
479 let data = model.schema.as_ref().unwrap().compute_model_data();
480 for (tag, children) in &data.tag_contents {
481 model.add_tag_content(tag, children.iter().map(String::as_str).collect());
482 }
483 for (tag, attrs) in &data.tag_attributes {
484 model.add_tag_attribute(tag, attrs.iter().map(String::as_str).collect());
485 }
486 for (name, members) in &data.schema_classes {
487 model.set_schema_class(name, members.iter().map(arena::pin).collect());
488 }
489 for (prefix, uri) in &data.namespaces {
490 model.register_document_namespace(prefix, Some(uri));
491 }
492 let primary = model.schema.as_ref().unwrap().primary_namespace.clone();
501 if let Some(primary) = primary {
502 model.register_document_namespace("", Some(&primary));
503 }
504 },
505 }
506 }
507 },
508 };
509 }
510 model.load_internal_extensions();
511 if model.debug_mode {
512 model.describe_model()
513 }
514
515 Ok(())
516}
517
518pub fn get_document_namespace_prefix(
519 namespace: &str,
520 forattribute: bool,
521 probe: bool,
522) -> Option<SymStr> {
523 let mut docprefix = if !forattribute {
527 model!()
528 .document_namespace_prefixes
529 .get(&s!("DEFAULT#{namespace}"))
530 .copied()
531 } else {
532 None
533 };
534 let ns_sym = arena::pin(namespace);
535 if docprefix.is_none() {
536 docprefix = model!()
537 .document_namespace_prefixes
538 .get_sym(ns_sym)
539 .copied();
540 }
541
542 if docprefix.is_none() && namespace != LTX_NAMESPACE {
551 docprefix = model!().code_namespace_prefixes.get_sym(ns_sym).copied();
552 }
553
554 if docprefix.is_none() && !probe {
555 {
556 model_mut!().namespace_errors += 1;
557 }
558 let ns_err = s!("namespace{}", &model!().namespace_errors.to_string());
559 docprefix = Some(arena::pin(&ns_err));
560 {
561 model_mut!().register_document_namespace(&ns_err, Some(namespace));
562 }
563 let message2 = if let Some(dp) = docprefix {
564 arena::with(dp, |dp_str| s!("Using '{dp_str}' instead"))
565 } else {
566 String::from("No prefix to fall back on.")
567 };
568 Warn!(
569 "malformed",
570 namespace,
571 "No prefix has been registered for namespace (in document)",
572 message2
573 );
574 }
575 let default_sym = pin!("#default");
576 docprefix.filter(|p| p != &default_sym)
577}
578
579pub fn get_document_namespace(docprefix: &str, probe: bool) -> Option<String> {
580 let h_default_sym = pin!("#default");
581 let docprefix_sym = if docprefix.is_empty() {
582 h_default_sym
583 } else {
584 arena::pin(docprefix)
585 };
586 let ns_str = match model!().document_namespaces.get_sym(docprefix_sym) {
587 None => String::new(),
588 Some(sym) => arena::with(*sym, |s| {
589 if s.starts_with("DEFAULT#") {
590 s.replacen("DEFAULT#", "", 1)
591 } else {
592 s.to_string()
593 }
594 }),
595 };
596
597 if docprefix_sym != h_default_sym && ns_str.is_empty() && !probe {
598 {
599 model_mut!().namespace_errors += 1;
600 }
601 let ns_error = s!(
602 "http://example.com/namespace{}",
603 &model!().namespace_errors.to_string()
604 );
605 {
606 model_mut!().register_document_namespace(docprefix, Some(&ns_error));
607 }
608 let msg1 = arena::with(docprefix_sym, |dp_str| {
609 s!("No namespace has been registered for prefix '{dp_str}' (in document)")
610 });
611 let msg2 = s!("Using '{ns_str}' instead");
612 let err = || {
613 Error!("malformed", docprefix, msg1, msg2);
614 Ok(())
615 };
616 err().ok();
617 }
618 if ns_str.is_empty() {
619 None
620 } else {
621 Some(ns_str)
622 }
623}
624
625pub fn get_namespace_prefix(namespace: &str, _forattribute: bool, probe: bool) -> Option<SymStr> {
633 let mut codeprefix: Option<SymStr> = None;
634 let ns_sym = arena::pin(namespace);
635 let mut model = model_mut!();
636 if !namespace.is_empty() {
637 codeprefix = model.code_namespace_prefixes.get_sym(ns_sym).copied();
638
639 if codeprefix.is_some() && !probe {
640 {
641 let docprefix = model.document_namespace_prefixes.get_sym(ns_sym);
642 if docprefix.is_some() && !model.code_namespaces.contains_key_sym(docprefix.unwrap()) {
644 codeprefix = docprefix.copied();
645 }
646 }
647 } else {
648 model.namespace_errors += 1;
650 let auto_prefix = arena::pin(s!("namespace{}", &model.namespace_errors.to_string()));
651 codeprefix = Some(auto_prefix);
652 }
653 model.register_namespace_sym(codeprefix.unwrap(), Some(arena::pin(namespace)));
654 }
658
659 codeprefix
660}
661
662pub fn get_namespace(codeprefix: &str, probe: bool) -> Result<Option<SymStr>> {
663 let mut model = model_mut!();
664 let mut ns: Option<SymStr> = model.code_namespaces.get(codeprefix).copied();
665 if ns.is_none() && !probe {
666 model.namespace_errors += 1;
667 let example_namespace = s!(
668 "http://example.com/namespace{}",
669 &model.namespace_errors.to_string()
670 );
671 ns = Some(arena::pin(&example_namespace));
672 model.register_namespace(codeprefix, Some(&example_namespace));
673 Error!(
674 "malformed",
675 codeprefix,
676 s!("No namespace has been registered for prefix '{codeprefix}' (in code)"),
677 s!("Using '{example_namespace}' instead")
678 );
679 }
680 Ok(ns)
681}
682
683#[inline]
690fn prefixed_qname(prefix: &str, local: &str) -> String {
691 let mut q = String::with_capacity(prefix.len() + 1 + local.len());
692 q.push_str(prefix);
693 q.push(':');
694 q.push_str(local);
695 q
696}
697
698pub fn get_node_qname(node: &Node) -> SymStr {
699 use libxml::tree::NodeType::*;
700 let node_type = node.get_type();
701 if node_type.is_none() {
702 return pin!("#BrokenNode");
703 }
704 match node_type.unwrap() {
707 TextNode => pin!("#PCDATA"),
708 DocumentNode => pin!("#Document"),
709 CommentNode => pin!("#Comment"),
710 PiNode => pin!("#ProcessingInstruction"),
711 DTDNode => pin!("#DTD"),
712 NamespaceDecl => {
713 pin!("xmlns")
721 },
722 ElementNode | AttributeNode => {
723 let name_str = node.get_name();
724 if let Some(ns) = node.get_namespace() {
727 let prefix = ns.get_prefix();
728 if prefix.is_empty() {
729 arena::pin(prefixed_qname("ltx", &name_str))
738 } else {
739 arena::pin(prefixed_qname(&prefix, &name_str))
741 }
742 } else {
743 match name_str.as_str() {
745 "song" | "verse" => arena::pin(name_str),
746 regular => arena::pin(prefixed_qname("ltx", regular)),
747 }
748 }
749 },
750 _ => {
752 pin!("#BrokenNode")
759 },
760 }
761}
762
763pub fn get_foreign_node_qname(node: &Node) -> SymStr {
783 if let Some(ns) = node.get_namespace()
784 && ns.get_prefix().is_empty()
785 {
786 let href = ns.get_href();
787 if !href.is_empty() && href != LTX_NAMESPACE {
788 let registered = model!()
791 .code_namespace_prefixes
792 .get_sym(arena::pin(href.as_str()))
793 .copied();
794 if let Some(code_prefix) = registered {
797 let name_str = node.get_name();
798 return arena::pin(arena::with(code_prefix, |p| prefixed_qname(p, &name_str)));
799 }
800 }
801 }
802 get_node_qname(node)
803}
804
805pub fn with_node_qname<R, FnR>(node: &Node, caller: FnR) -> R
812where FnR: FnOnce(&str) -> R {
813 let qsym = get_node_qname(node);
814 arena::with(qsym, |qname_str| caller(qname_str))
815}
816
817pub fn get_node_document_qname(node: &Node) -> SymStr {
819 use libxml::tree::NodeType::*;
820 let node_type = node.get_type();
821 if node_type.is_none() {
822 return pin!("#BrokenNode");
823 }
824
825 match node_type.unwrap() {
826 TextNode => pin!("#PCDATA"),
827 DocumentNode => pin!("#Document"),
828 CommentNode => pin!("#Comment"),
829 PiNode => pin!("#ProcessingInstruction"),
830 DTDNode => pin!("#DTD"),
831
832 NamespaceDecl => pin!("xmlns"),
838
839 ElementNode | AttributeNode => {
840 let empty_sym = pin!("");
841 let mut prefix = empty_sym;
842 if let Some(ns) = node.get_namespace() {
843 let href = ns.get_href();
844 if !href.is_empty() {
845 prefix = get_document_namespace_prefix(&href, false, true).unwrap_or(empty_sym);
846 }
847 }
848 if prefix == empty_sym {
849 arena::pin(node.get_name())
850 } else {
851 arena::pin(arena::with(prefix, |prefix_str| {
852 s!("{}:{}", prefix_str, node.get_name())
853 }))
854 }
855 },
856 t => {
858 panic!("Fatal:misdefined:<caller> should not ask for qualified name for node of type {t:?}")
859 },
860 }
861}
862
863pub fn get_node_attribute(node: &Node, key: &str) -> Option<String> {
873 if let Some(ns_uri) = attribute_namespace(key) {
874 let local = key.split_once(':').map_or(key, |(_, l)| l);
875 if let Some(value) = node.get_attribute_ns(local, &ns_uri) {
876 return Some(value);
877 }
878 }
879 node.get_attribute(key)
880}
881
882pub fn remove_node_attribute(node: &mut Node, key: &str) -> std::result::Result<(), String> {
885 if let Some(ns_uri) = attribute_namespace(key) {
886 let local = key.split_once(':').map_or(key, |(_, l)| l).to_string();
887 if node.get_attribute_ns(&local, &ns_uri).is_some() {
888 return node
889 .remove_attribute_ns(&local, &ns_uri)
890 .map_err(|e| e.to_string());
891 }
892 }
893 node.remove_attribute(key).map_err(|e| e.to_string())
894}
895
896fn attribute_namespace(key: &str) -> Option<String> {
900 let (prefix, _) = key.split_once(':')?;
901 if prefix == "xml" {
902 return Some(XML_NS.to_string());
903 }
904 match decode_qname(key) {
905 Ok((ns_uri, _local)) => ns_uri,
906 Err(_) => None,
907 }
908}
909
910pub fn decode_qname(codetag: &str) -> Result<(Option<String>, String)> {
914 match PREFIXED_LOCALNAME_RE.captures(codetag) {
915 Some(captures) => {
916 let prefix = captures.get(1).map_or("", |m| m.as_str());
917 let localname = captures.get(2).map_or("", |m| m.as_str());
918
919 if prefix == "xml" {
920 Ok((None, codetag.to_string()))
921 } else {
922 Ok((
923 get_namespace(prefix, false)?.map(arena::to_string),
924 localname.to_string(),
925 ))
926 }
927 },
928 None => Ok((None, codetag.to_string())),
929 }
930}
931
932pub fn decode_qname_sym(sym: SymStr) -> Result<(Option<String>, String)> {
935 let codetag = arena::to_string(sym);
936 decode_qname(&codetag)
937}
938
939pub fn can_contain_sym(tag: SymStr, child: SymStr) -> bool {
964 if tag == pin!("#PCDATA") || tag == pin!("#Comment") || tag == pin!("") {
966 return false;
967 } else if tag == pin!("_WildCard_") {
968 return true;
969 }
970 if arena::with(tag, |t| t.ends_with("_Capture_"))
973 || arena::with(child, |c| {
974 c.ends_with("_Capture_") || c.ends_with("_CaptureBlock_")
975 })
976 {
977 return true;
978 }
979 if child == pin!("_WildCard_")
980 || child == pin!("#Comment")
981 || child == pin!("#ProcessingInstruction")
982 || child == pin!("#DTD")
983 {
984 return true;
985 }
986
987 let model = model!();
988 if model.permissive && tag == pin!("#Document") && child != pin!("#PCDATA") {
989 return true; }
991
992 let frame = model.tagprop.get_sym(tag);
997 let wildcard_frame = if frame.is_none() {
998 namespace_wildcard_sym(tag).and_then(|w| model.tagprop.get_sym(w))
999 } else {
1000 None
1001 };
1002 let content = &frame
1003 .or(wildcard_frame)
1004 .unwrap_or(&*DEFAULT_TAG_FRAME)
1005 .model;
1006 content.contains(&pin!("ANY")) || set_allows(content, child)
1007}
1008
1009pub fn can_contain(tag: &str, child: &str) -> bool {
1012 can_contain_sym(arena::pin(tag), arena::pin(child))
1013}
1014
1015fn namespace_wildcard_sym(tag: SymStr) -> Option<SymStr> {
1033 arena::with(tag, |t| t.split_once(':').map(|(ns, _)| s!("{ns}:*"))).map(|w| arena::pin(&w))
1034}
1035
1036fn set_allows(set: &HashSet<SymStr>, key: SymStr) -> bool {
1055 if set.contains(&key) {
1056 return true; }
1058 if set.is_empty() {
1059 return false;
1060 }
1061 if !arena::with(key, |k| k.contains(':')) {
1065 if !set.contains(&pin!("*")) || set.contains(&pin!("!*")) {
1067 return false;
1068 }
1069 let negated = arena::with(key, |k| s!("!{k}"));
1071 return !set.contains(&arena::pin(&negated));
1072 }
1073 let negated = arena::with(key, |k| s!("!{k}"));
1077 if set.contains(&arena::pin(&negated)) {
1078 return false;
1079 }
1080 let (ns_star, neg_ns_star) = arena::with(key, |k| {
1081 let ns = k.split_once(':').map_or("", |(ns, _)| ns);
1082 (s!("{ns}:*"), s!("!{ns}:*"))
1083 });
1084 if set.contains(&arena::pin(&ns_star)) {
1085 return true;
1086 }
1087 if set.contains(&arena::pin(&neg_ns_star)) {
1088 return false;
1089 }
1090 if set.contains(&pin!("!*:*")) {
1091 return false;
1092 }
1093 set.contains(&pin!("*:*"))
1094}
1095
1096pub fn can_have_attribute(tag: SymStr, attrib: SymStr) -> bool {
1097 if let Some(early_choice) = arena::with(tag, |tag_str| match tag_str {
1099 "#PCDATA" | "#Comment" | "#Document" | "#ProcessingInstruction" | "#DTD" => Some(false),
1100 "_WildCard_" => Some(true),
1101 other if other.ends_with("_Capture_") => Some(true),
1102 _ => None,
1103 }) {
1104 return early_choice;
1105 };
1106 if arena::with(attrib, |a| a.starts_with('_')) {
1109 return true;
1110 }
1111 let model = model!();
1112 if model.permissive {
1113 return true;
1114 }
1115
1116 let frame = model.tagprop.get_sym(tag);
1119 let wildcard_frame = if frame.is_none() {
1120 namespace_wildcard_sym(tag).and_then(|w| model.tagprop.get_sym(w))
1121 } else {
1122 None
1123 };
1124 let attributes = &frame
1125 .or(wildcard_frame)
1126 .unwrap_or(&*DEFAULT_TAG_FRAME)
1127 .attributes;
1128 set_allows(attributes, attrib)
1129}
1130
1131pub fn is_node_in_schema_class(class_name: &str, tag: &Node) -> bool {
1132 let tag = get_node_qname(tag);
1133 is_in_schema_class(arena::pin(class_name), tag)
1134}
1135pub fn is_in_schema_class(class_name: SymStr, tag: SymStr) -> bool {
1136 match model!().schema_class.get_sym(class_name) {
1137 Some(class) => class.contains(&tag),
1138 _ => false,
1139 }
1140}
1141
1142pub fn get_tags() -> Vec<SymStr> { model!().tagprop.keys().copied().collect() }
1147
1148pub fn get_tag_contents(tag: SymStr) -> Vec<SymStr> {
1149 match model!().tagprop.get_sym(tag) {
1150 Some(h) => h.model.iter().copied().collect(),
1151 None => Vec::new(),
1152 }
1153}
1154pub fn set_model(new_model: Model) {
1155 let mut model = model_mut!();
1156 *model = new_model;
1157}
1158pub fn is_permissive() -> bool { model!().permissive }
1159
1160pub fn with_schema_data<FnR, R>(caller: FnR) -> R
1161where FnR: FnOnce(Option<&Vec<SymStr>>) -> R {
1162 caller(model!().schema_data.as_ref())
1163}
1164pub fn set_schema(schema: Relaxng) {
1165 let mut model = model_mut!();
1166 model.schema = Some(schema);
1167}
1168pub fn set_schema_class(classname: &str, content: HashSet<SymStr>) {
1169 model_mut!().set_schema_class(classname, content)
1170}
1171pub fn add_tag_content(tag: &str, elements: Vec<&str>) {
1172 model_mut!().add_tag_content(tag, elements)
1173}
1174pub fn add_tag_attribute(tag: &str, attributes: Vec<&str>) {
1175 model_mut!().add_tag_attribute(tag, attributes)
1176}
1177
1178pub(crate) fn compute_indirect_model_aux(
1179 tag: SymStr,
1180 start_opt: Option<SymStr>,
1181 desirability: usize,
1182 openability: &mut SymHashMap<u32>,
1183 desc: &mut SymHashMap<SymHashMap<usize>>,
1184) {
1185 let start = match start_opt {
1186 Some(s) => s,
1187 None => pin!(""),
1188 };
1189
1190 let tag_contents: Vec<SymStr> = get_tag_contents(tag);
1194
1195 for kid in tag_contents {
1196 let prior = desc.entry_sym(kid).or_default().get_sym(start).copied();
1209 if let Some(prior_d) = prior
1210 && prior_d >= desirability
1211 {
1212 continue;
1213 }
1214
1215 if start != pin!("") {
1216 desc
1217 .entry_sym(kid)
1218 .or_default()
1219 .insert_sym(start, desirability);
1220 }
1221
1222 if kid != pin!("#PCDATA")
1223 && let Some(priority) = openability.get_sym(kid).copied()
1224 {
1225 let inner = if start != pin!("") { start } else { kid };
1226 let next_desirability = desirability * (priority as usize) / 100;
1229 compute_indirect_model_aux(kid, Some(inner), next_desirability, openability, desc);
1230 }
1231 }
1232}
1233pub fn register_document_namespace(docprefix: &str, namespace_opt: Option<&str>) {
1240 model_mut!().register_document_namespace(docprefix, namespace_opt)
1241}
1242
1243pub fn get_document_namespace_prefixes() -> Vec<(String, String)> {
1245 model!()
1246 .document_namespace_prefixes
1247 .iter()
1248 .map(|(ns_sym, prefix_sym)| {
1249 let prefix = arena::with(*prefix_sym, |s| s.to_string());
1250 let ns = arena::with(*ns_sym, |s| s.to_string());
1251 (prefix, ns)
1252 })
1253 .collect()
1254}
1255
1256pub fn register_namespace(codeprefix: &str, namespace_opt: Option<&str>) {
1263 model_mut!().register_namespace(codeprefix, namespace_opt)
1264}
1265
1266pub fn with_code_namespaces<FnR, R>(caller: FnR) -> R
1267where FnR: FnOnce(&SymHashMap<SymStr>) -> R {
1268 caller(&model!().code_namespaces)
1269}
1270
1271#[cfg(test)]
1272mod wildcard_resolution_tests {
1273 use super::*;
1278
1279 fn set_of(keys: &[&str]) -> HashSet<SymStr> { keys.iter().map(|k| arena::pin(*k)).collect() }
1280 fn allows(set: &HashSet<SymStr>, key: &str) -> bool { set_allows(set, arena::pin(key)) }
1283 fn wildcard(tag: &str) -> Option<String> {
1284 namespace_wildcard_sym(arena::pin(tag)).map(|w| arena::with(w, str::to_string))
1285 }
1286
1287 #[test]
1288 fn namespace_wildcard_tag_only_applies_to_prefixed_tags() {
1289 assert_eq!(wildcard("xhtml:p").as_deref(), Some("xhtml:*"));
1290 assert_eq!(wildcard("ltx:para").as_deref(), Some("ltx:*"));
1291 assert_eq!(wildcard("para"), None);
1293 assert_eq!(wildcard("#PCDATA"), None);
1294 }
1295
1296 #[test]
1297 fn exact_membership_wins_and_empty_sets_reject() {
1298 let s = set_of(&["class", "href"]);
1299 assert!(allows(&s, "class"));
1300 assert!(!allows(&s, "style"));
1301 assert!(!allows(&HashSet::default(), "class"));
1302 }
1303
1304 #[test]
1305 fn a_negation_beats_the_wildcard_that_would_otherwise_allow_it() {
1306 let s = set_of(&["!aria:*", "!xml:*", "*:*", "about"]);
1311 assert!(allows(&s, "about"), "exact entry allows");
1312 assert!(allows(&s, "data:foo"), "*:* allows an unexcluded namespace");
1313 assert!(!allows(&s, "aria:label"), "!aria:* excludes its namespace");
1314 assert!(!allows(&s, "xml:lang"), "!xml:* excludes its namespace");
1315
1316 let s2 = set_of(&["!svg:width", "svg:*"]);
1318 assert!(allows(&s2, "svg:height"));
1319 assert!(!allows(&s2, "svg:width"));
1320
1321 let s3 = set_of(&["!*:*", "svg:width"]);
1323 assert!(allows(&s3, "svg:width"));
1324 assert!(!allows(&s3, "svg:height"));
1325 }
1326
1327 #[test]
1328 fn unprefixed_keys_use_the_bare_star_not_the_namespaced_one() {
1329 let namespaced_only = set_of(&["*:*"]);
1331 assert!(!allows(&namespaced_only, "class"));
1332 assert!(allows(&namespaced_only, "svg:width"));
1333
1334 let html_wildcard = set_of(&["*", "*:*"]);
1337 assert!(allows(&html_wildcard, "class"));
1338 assert!(allows(&html_wildcard, "xlink:href"));
1339
1340 let denied = set_of(&["!*", "*:*"]);
1342 assert!(!allows(&denied, "class"));
1343 assert!(allows(&denied, "svg:width"));
1344
1345 let star_but_excluded = set_of(&["*", "!class"]);
1348 assert!(
1349 !allows(&star_but_excluded, "class"),
1350 "!class excludes despite *"
1351 );
1352 assert!(
1353 allows(&star_but_excluded, "id"),
1354 "* still allows an unexcluded name"
1355 );
1356 }
1357}
1358
1359#[cfg(test)]
1360mod schema_load_tests {
1361 use super::*;
1365
1366 #[test]
1367 fn raw_rng_scan_populates_tagprop_end_to_end() {
1368 let dir = std::env::temp_dir().join(format!("lxo652_{}", std::process::id()));
1371 std::fs::create_dir_all(&dir).expect("mkdir");
1372 let rng = "<grammar xmlns=\"http://relaxng.org/ns/structure/1.0\">\
1373 <start><ref name=\"document\"/></start>\
1374 <define name=\"document\"><element name=\"document\">\
1375 <zeroOrMore><ref name=\"para\"/></zeroOrMore></element></define>\
1376 <define name=\"para\"><element name=\"para\">\
1377 <attribute name=\"class\"/><text/></element></define>\
1378 </grammar>";
1379 std::fs::write(dir.join("lxo652schema.rng"), rng).expect("write rng");
1380
1381 initialize_model();
1382 model_mut!().set_relaxng_schema("lxo652schema");
1383 let dir_str = dir.to_str().unwrap();
1384 load_schema(&[dir_str]).expect("load_schema");
1385
1386 assert!(
1389 can_contain("#Document", "document"),
1390 "document root must be allowed after a raw .rng scan"
1391 );
1392 assert!(can_contain("document", "para"), "document may contain para");
1393 assert!(
1394 can_have_attribute(pin!("para"), pin!("class")),
1395 "para must allow its @class"
1396 );
1397 assert!(
1398 !can_have_attribute(pin!("para"), pin!("bogus")),
1399 "para must NOT allow an undeclared attribute"
1400 );
1401
1402 let _ = std::fs::remove_dir_all(&dir);
1403 }
1404
1405 #[test]
1411 fn default_ns_schema_uses_registered_code_prefix() {
1412 let dir = std::env::temp_dir().join(format!("lxo652def_{}", std::process::id()));
1413 std::fs::create_dir_all(&dir).expect("mkdir");
1414 let default_ns = "<grammar xmlns=\"http://relaxng.org/ns/structure/1.0\" \
1416 ns=\"http://dlmf.nist.gov/LaTeXML\">\
1417 <start><ref name=\"document\"/></start>\
1418 <define name=\"document\"><element name=\"document\">\
1419 <zeroOrMore><ref name=\"para\"/></zeroOrMore></element></define>\
1420 <define name=\"para\"><element name=\"para\">\
1421 <attribute name=\"class\"/><text/></element></define>\
1422 </grammar>";
1423 std::fs::write(dir.join("lxo652def.rng"), default_ns).expect("write rng");
1424
1425 initialize_model();
1426 model_mut!().register_namespace("ltx", Some(LTX_NAMESPACE));
1429 model_mut!().set_relaxng_schema("lxo652def");
1430 let dir_str = dir.to_str().unwrap();
1431 load_schema(&[dir_str]).expect("load_schema");
1432
1433 assert!(
1436 can_contain("#Document", "ltx:document"),
1437 "default-ns root must resolve to the registered ltx: prefix (#652)"
1438 );
1439 assert!(
1440 can_contain("ltx:document", "ltx:para"),
1441 "ltx:document may contain ltx:para"
1442 );
1443 assert!(
1444 !can_contain("#Document", "namespace1:document"),
1445 "must NOT invent a synthetic namespace1 prefix for a registered namespace (#652)"
1446 );
1447
1448 let _ = std::fs::remove_dir_all(&dir);
1449 }
1450
1451 #[test]
1455 fn schema_declared_prefix_survives_when_unregistered_in_code() {
1456 let dir = std::env::temp_dir().join(format!("lxo652decl_{}", std::process::id()));
1457 std::fs::create_dir_all(&dir).expect("mkdir");
1458 let rng = "<grammar xmlns=\"http://relaxng.org/ns/structure/1.0\" \
1460 ns=\"http://example.com/ex\" xmlns:ex=\"http://example.com/ex\">\
1461 <start><ref name=\"root\"/></start>\
1462 <define name=\"root\"><element name=\"root\"><empty/></element></define>\
1463 </grammar>";
1464 std::fs::write(dir.join("lxo652decl.rng"), rng).expect("write rng");
1465
1466 initialize_model();
1467 model_mut!().set_relaxng_schema("lxo652decl");
1468 load_schema(&[dir.to_str().unwrap()]).expect("load_schema");
1469
1470 assert!(
1471 can_contain("#Document", "ex:root"),
1472 "schema-declared xmlns:ex prefix must be used when code has none (#652)"
1473 );
1474 let _ = std::fs::remove_dir_all(&dir);
1475 }
1476
1477 #[test]
1481 fn schema_primary_ns_becomes_default_document_namespace() {
1482 let dir = std::env::temp_dir().join(format!("lxo652out_{}", std::process::id()));
1483 std::fs::create_dir_all(&dir).expect("mkdir");
1484 let rng = "<grammar xmlns=\"http://relaxng.org/ns/structure/1.0\" \
1485 ns=\"http://dlmf.nist.gov/LaTeXML\">\
1486 <start><ref name=\"document\"/></start>\
1487 <define name=\"document\"><element name=\"document\"><empty/></element></define>\
1488 </grammar>";
1489 std::fs::write(dir.join("lxo652out.rng"), rng).expect("write rng");
1490
1491 initialize_model();
1492 model_mut!().register_namespace("ltx", Some(LTX_NAMESPACE));
1493 model_mut!().set_relaxng_schema("lxo652out");
1494 load_schema(&[dir.to_str().unwrap()]).expect("load_schema");
1495
1496 assert_eq!(
1498 get_document_namespace("", true).as_deref(),
1499 Some(LTX_NAMESPACE),
1500 "schema primary ns must be the default document namespace (#652)"
1501 );
1502 assert_eq!(
1504 get_document_namespace_prefix(LTX_NAMESPACE, false, true),
1505 None,
1506 "the default document namespace serializes without a prefix (#652)"
1507 );
1508 }
1509
1510 #[test]
1515 fn foreign_and_runtime_namespaces_serialize_under_their_code_prefix() {
1516 initialize_model();
1517 model_mut!().register_namespace("svg", Some("http://www.w3.org/2000/svg"));
1519 model_mut!().register_namespace("bk", Some("http://bookml.example/ns"));
1521
1522 assert_eq!(
1523 get_document_namespace_prefix("http://www.w3.org/2000/svg", false, true),
1524 Some(pin!("svg")),
1525 "built-in foreign svg namespace serializes under its code prefix (#652)"
1526 );
1527 assert_eq!(
1528 get_document_namespace_prefix("http://bookml.example/ns", false, true),
1529 Some(pin!("bk")),
1530 "a runtime/third-party registered namespace serializes under its prefix (#652)"
1531 );
1532 }
1533}