use std::path::Path;
#[derive(Debug)]
pub enum XmlSchemaCodegenError {
ReadSource(String, std::io::Error),
EmptyScan(String),
}
impl core::fmt::Display for XmlSchemaCodegenError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::ReadSource(p, e) => write!(f, "read source {p}: {e}"),
Self::EmptyScan(what) => write!(f, "empty scan for {what}"),
}
}
}
impl std::error::Error for XmlSchemaCodegenError {}
pub fn generate_xml_namespace_schema_source(
xsd_path: &Path,
) -> Result<String, XmlSchemaCodegenError> {
let xsd = std::fs::read_to_string(xsd_path)
.map_err(|e| XmlSchemaCodegenError::ReadSource(xsd_path.display().to_string(), e))?;
generate_xml_namespace_schema_from_source(&xsd)
}
pub fn generate_xml_namespace_schema_from_source(
xsd: &str,
) -> Result<String, XmlSchemaCodegenError> {
let names = scan_xsd_attribute_names(xsd);
if names.is_empty() {
return Err(XmlSchemaCodegenError::EmptyScan(
"xml.xsd <xs:attribute name=...>".to_string(),
));
}
let mut out = String::new();
out.push_str(
"// Generated by pr4xis::codegen::xml_schemas::generate_xml_namespace_schema_source.\n",
);
out.push_str(
"// Source: W3C xml.xsd — the W3C-published xml namespace schema.\n\
// Citation: Bray, Hollander, Layman, Tobin & Thompson (2009) Namespaces in XML 1.0\n\
// (Third Edition), W3C Recommendation; Bray et al. (2008) XML 1.0 Fifth Edition\n\
// §2.10 (xml:space) + §2.12 (xml:lang); Marsh & Tobin (2009) XML Base.\n",
);
out.push_str(&format!("// Attributes loaded: {}\n", names.len()));
out.push_str("pub const XML_NAMESPACE_ATTRIBUTES: &[&str] = &[\n");
for name in &names {
out.push_str(&format!(" {:?},\n", name));
}
out.push_str("];\n");
Ok(out)
}
fn scan_xsd_attribute_names(xsd: &str) -> Vec<String> {
const NEEDLE: &str = "<xs:attribute name=\"";
let mut out = std::collections::BTreeSet::new();
let mut rest = xsd;
while let Some(idx) = rest.find(NEEDLE) {
let after = &rest[idx + NEEDLE.len()..];
if let Some(end) = after.find('"') {
out.insert(after[..end].to_string());
rest = &after[end..];
} else {
break;
}
}
out.into_iter().collect()
}
pub fn generate_xml_infoset_source(xhtml_path: &Path) -> Result<String, XmlSchemaCodegenError> {
let xhtml = std::fs::read_to_string(xhtml_path)
.map_err(|e| XmlSchemaCodegenError::ReadSource(xhtml_path.display().to_string(), e))?;
generate_xml_infoset_from_source(&xhtml)
}
pub fn generate_xml_infoset_from_source(xhtml: &str) -> Result<String, XmlSchemaCodegenError> {
let items = scan_infoset_items(xhtml);
if items.is_empty() {
return Err(XmlSchemaCodegenError::EmptyScan(
"xml-infoset.xhtml <h3><a name=\"infoitem.*\">...</a></h3>".to_string(),
));
}
let mut out = String::new();
out.push_str("// Generated by pr4xis::codegen::xml_schemas::generate_xml_infoset_source.\n");
out.push_str(
"// Source: W3C XML Information Set (Second Edition), W3C Recommendation\n\
// 4 February 2004. Editors: Cowan & Tobin.\n\
// Information items per §2 — the 11-item conceptual taxonomy.\n",
);
out.push_str(&format!("// Items loaded: {}\n", items.len()));
out.push_str(
"/// One information item extracted from the published rec's section\n\
/// hierarchy. `section` is the dotted number (e.g. `\"2.1\"`),\n\
/// `anchor` is the `<a name=\"...\">` identifier (e.g.\n\
/// `\"infoitem.document\"`), `english_name` is the canonical\n\
/// English head-noun phrase (e.g. `\"Document\"`),\n\
/// `variant_ident` is the Rust enum-variant identifier (e.g.\n\
/// `\"DocumentItem\"`).\n\
#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n\
pub struct InformationItemEntry {\n \
pub section: &'static str,\n \
pub anchor: &'static str,\n \
pub english_name: &'static str,\n \
pub variant_ident: &'static str,\n\
}\n\n",
);
out.push_str("pub const XML_INFOSET_INFORMATION_ITEMS: &[InformationItemEntry] = &[\n");
for it in &items {
out.push_str(&format!(
" InformationItemEntry {{ section: {:?}, anchor: {:?}, english_name: {:?}, variant_ident: {:?} }},\n",
it.section, it.anchor, it.english_name, it.variant_ident,
));
}
out.push_str("];\n");
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScannedItem {
pub section: String,
pub anchor: String,
pub english_name: String,
pub variant_ident: String,
}
pub fn scan_infoset_items(xhtml: &str) -> Vec<ScannedItem> {
const ANCHOR_PREFIX: &str = "<a name=\"infoitem.";
let mut out = Vec::new();
let mut rest = xhtml;
while let Some(idx) = rest.find(ANCHOR_PREFIX) {
let after = &rest[idx + ANCHOR_PREFIX.len()..];
let Some(quote_end) = after.find('"') else {
break;
};
let anchor_suffix = &after[..quote_end];
let Some(gt_end) = after.find('>') else {
break;
};
let body_start = gt_end + 1;
let body = &after[body_start..];
let Some(close_idx) = body.find("</a>") else {
break;
};
let title_raw = body[..close_idx].trim();
rest = &body[close_idx + "</a>".len()..];
let (section, title_tail) = match split_section_number(title_raw) {
Some(p) => p,
None => continue,
};
let english_name = reduce_to_head_noun(&title_tail);
let variant_ident = make_variant_ident(&english_name);
let anchor = format!("infoitem.{}", anchor_suffix);
out.push(ScannedItem {
section,
anchor,
english_name,
variant_ident,
});
}
out
}
fn split_section_number(s: &str) -> Option<(String, String)> {
let mut chars = s.char_indices();
let mut last_digit_or_dot = None;
for (i, c) in chars.by_ref() {
if c.is_ascii_digit() || c == '.' {
last_digit_or_dot = Some(i);
continue;
}
last_digit_or_dot?;
let end = last_digit_or_dot.unwrap() + 1;
let raw_num = s[..end].trim_end_matches('.');
let tail = s[end..].trim_start();
return Some((raw_num.to_string(), tail.to_string()));
}
None
}
fn reduce_to_head_noun(s: &str) -> String {
let trimmed = s.trim();
let without_suffix = trimmed
.trim_end_matches("Information Items")
.trim_end_matches("Information Item")
.trim();
without_suffix
.strip_prefix("The ")
.unwrap_or(without_suffix)
.trim()
.to_string()
}
fn make_variant_ident(english_name: &str) -> String {
let mut out = String::new();
let mut capitalize_next = true;
for c in english_name.chars() {
if c.is_ascii_whitespace() || c == '-' {
capitalize_next = true;
continue;
}
if capitalize_next {
out.extend(c.to_uppercase());
capitalize_next = false;
} else {
out.push(c);
}
}
out.push_str("Item");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[crate::praxis_value(Verifiable)]
#[test]
fn scan_xsd_attribute_names_finds_lang_space_base_id() {
let xsd = r#"<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:attribute name="lang" />
<xs:attribute name="space" />
<xs:attribute name="base" type="xs:anyURI" />
<xs:attribute name="id" type="xs:ID" />
</xs:schema>"#;
let names = scan_xsd_attribute_names(xsd);
assert_eq!(names, vec!["base", "id", "lang", "space"]);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn scan_xsd_attribute_names_empty_on_empty_input() {
assert!(scan_xsd_attribute_names("").is_empty());
}
#[crate::praxis_value(Verifiable)]
#[test]
fn split_section_number_handles_dot_form() {
assert_eq!(
split_section_number("2.1. The Document Information Item"),
Some((
"2.1".to_string(),
"The Document Information Item".to_string()
))
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn split_section_number_handles_no_trailing_dot() {
assert_eq!(
split_section_number("2.6 Character Information Items"),
Some(("2.6".to_string(), "Character Information Items".to_string()))
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn reduce_to_head_noun_drops_the_and_suffix() {
assert_eq!(
reduce_to_head_noun("The Document Information Item"),
"Document"
);
assert_eq!(reduce_to_head_noun("Element Information Items"), "Element");
assert_eq!(
reduce_to_head_noun("The Document Type Declaration Information Item"),
"Document Type Declaration"
);
assert_eq!(
reduce_to_head_noun("Unexpanded Entity Reference Information Items"),
"Unexpanded Entity Reference"
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn make_variant_ident_camel_cases_with_item_suffix() {
assert_eq!(make_variant_ident("Document"), "DocumentItem");
assert_eq!(
make_variant_ident("Document Type Declaration"),
"DocumentTypeDeclarationItem"
);
assert_eq!(
make_variant_ident("Unexpanded Entity Reference"),
"UnexpandedEntityReferenceItem"
);
}
#[crate::praxis_value(Explainable, Verifiable)]
#[test]
fn scan_infoset_items_yields_eleven_in_section_order() {
let sample = r#"
<h3><a name="infoitem.document">2.1. The Document Information Item</a></h3>
<h3><a name="infoitem.element">2.2. Element Information Items</a></h3>
<h3><a name="infoitem.attribute">2.3. Attribute Information Items</a></h3>
<h3><a name="infoitem.pi">2.4. Processing Instruction Information Items</a></h3>
<h3><a name="infoitem.rse">2.5. Unexpanded Entity Reference Information Items</a></h3>
<h3><a name="infoitem.character">2.6. Character Information Items</a></h3>
<h3><a name="infoitem.comment">2.7. Comment Information Items</a></h3>
<h3><a name="infoitem.doctype">2.8. The Document Type Declaration Information Item</a></h3>
<h3><a name="infoitem.entity.unparsed">2.9. Unparsed Entity Information Items</a></h3>
<h3><a name="infoitem.notation">2.10. Notation Information Items</a></h3>
<h3><a name="infoitem.namespace">2.11. Namespace Information Items</a></h3>
"#;
let items = scan_infoset_items(sample);
assert_eq!(items.len(), 11);
assert_eq!(items[0].english_name, "Document");
assert_eq!(items[0].variant_ident, "DocumentItem");
assert_eq!(items[0].section, "2.1");
assert_eq!(items[10].english_name, "Namespace");
assert_eq!(items[10].variant_ident, "NamespaceItem");
let dtd = items
.iter()
.find(|i| i.anchor == "infoitem.doctype")
.unwrap();
assert_eq!(dtd.english_name, "Document Type Declaration");
assert_eq!(dtd.variant_ident, "DocumentTypeDeclarationItem");
let rse = items.iter().find(|i| i.anchor == "infoitem.rse").unwrap();
assert_eq!(rse.english_name, "Unexpanded Entity Reference");
assert_eq!(rse.variant_ident, "UnexpandedEntityReferenceItem");
}
}