use std::borrow::Cow; use anyhow::{anyhow, bail, Context}; use regex::Regex; use serde::Deserialize; use crate::typespec; #[derive(Deserialize, Debug)] pub struct ModuleInfoHeader { pub name: String, pub description: String, pub headers: Vec, } #[derive(Debug)] pub struct ModuleInfo<'a> { pub header: ModuleInfoHeader, pub details: &'a str, } pub fn parse_module(text: &str) -> anyhow::Result { let Some((mut header, mut details)) = text.split_once("-----") else { bail!("could not split module header"); }; header = header.trim(); details = details.trim(); let header = toml::from_str::(header).context("parsing module info header")?; Ok(ModuleInfo { header, details }) } #[derive(Debug)] pub struct ClassInfo<'a> { pub type_: ClassType, pub name: &'a str, pub qml_name: Option<&'a str>, pub superclass: Option<&'a str>, pub singleton: bool, pub uncreatable: bool, pub comment: Option<&'a str>, pub properties: Vec>, pub invokables: Vec>, pub signals: Vec>, } #[derive(Debug)] pub enum ClassType { Object, Gadget, } #[derive(Debug, Clone, Copy)] pub struct Property<'a> { pub type_: &'a str, pub name: &'a str, pub comment: Option<&'a str>, pub readable: bool, pub writable: bool, pub default: bool, } #[derive(Debug, Clone)] pub struct Invokable<'a> { pub name: &'a str, pub ret: &'a str, pub comment: Option<&'a str>, pub params: Vec>, } #[derive(Debug, Clone)] pub struct Signal<'a> { pub name: &'a str, pub comment: Option<&'a str>, pub params: Vec>, } #[derive(Debug, Clone, Copy)] pub struct InvokableParam<'a> { pub name: &'a str, pub type_: &'a str, } #[derive(Debug)] pub struct EnumInfo<'a> { pub namespace: &'a str, pub enum_name: &'a str, pub qml_name: &'a str, pub comment: Option<&'a str>, pub variants: Vec>, } #[derive(Debug, Clone, Copy)] pub struct Variant<'a> { pub name: &'a str, pub comment: Option<&'a str>, } pub struct Parser { pub class_regex: Regex, pub macro_regex: Regex, pub property_regex: Regex, pub signals_regex: Regex, pub fn_regex: Regex, pub fn_param_regex: Regex, pub defaultprop_classinfo_regex: Regex, pub enum_regex: Regex, pub enum_variant_regex: Regex, } #[derive(Debug)] pub struct ParseContext<'a> { pub classes: Vec>, pub enums: Vec>, } impl Default for ParseContext<'_> { fn default() -> Self { Self { classes: Vec::new(), enums: Vec::new(), } } } impl Parser { pub fn new() -> Self { Self { class_regex: Regex::new(r#"(?(\s*\/\/\/.*\n)+)?\s*class\s+(?\w+)(?:\s*:\s*public\s+(?\w+)(\s*,(\s*\w+)*)*)?\s*\{(?[\s\S]*?)};"#).unwrap(), macro_regex: Regex::new(r#"(?(\s*\/\/\/.*\n)+)?\s*(?(Q|QML)_\w+)\s*(\(\s*(?.*)\s*\))?;"#).unwrap(), property_regex: Regex::new(r#"^\s*(?(\w|::|<|>)+\*?)\s+(?\w+)(\s+(MEMBER\s+(?\w+)|READ\s+(?\w+)|WRITE\s+(?\w+)|NOTIFY\s+(?\w+)|(?CONSTANT)))+\s*$"#).unwrap(), fn_regex: Regex::new(r#"(?(\s*\/\/\/.*\n)+)?\s*(?Q_INVOKABLE)?\s+(?(\w|::|<|>)+\*?)\s+(?\w+)\((?[\s\S]*?)\);"#).unwrap(), fn_param_regex: Regex::new(r#"(?(\w|::|<|>)+\*?)\s+(?\w+)(,|$)"#).unwrap(), signals_regex: Regex::new(r#"signals:(?(\s*(\s*///.*\s*)*void .*;)*)"#).unwrap(), defaultprop_classinfo_regex: Regex::new(r#"^\s*"DefaultProperty", "(?.+)"\s*$"#).unwrap(), enum_regex: Regex::new(r#"(?(\s*\/\/\/.*\n)+)?\s*namespace (?\w+)\s*\{[\s\S]*?(QML_ELEMENT|QML_NAMED_ELEMENT\((?\w+)\));[\s\S]*?enum\s*(?\w+)\s*\{(?[\s\S]*?)\};[\s\S]*?\}"#).unwrap(), enum_variant_regex: Regex::new(r#"(?(\s*\/\/\/.*\n)+)?\s*(?\w+)\s*=\s*\d+,"#).unwrap(), } } pub fn parse_classes<'a>( &self, text: &'a str, ctx: &mut ParseContext<'a>, ) -> anyhow::Result<()> { for class in self.class_regex.captures_iter(text) { let comment = class.name("comment").map(|m| m.as_str()); let name = class.name("name").unwrap().as_str(); let superclass = class.name("super").map(|m| m.as_str()); let body = class.name("body").unwrap().as_str(); let mut classtype = None; let mut qml_name = None; let mut singleton = false; let mut uncreatable = false; let mut properties = Vec::new(); let mut default_property = None; let mut invokables = Vec::new(); let mut notify_signals = Vec::new(); let mut signals = Vec::new(); (|| { for macro_ in self.macro_regex.captures_iter(body) { let comment = macro_.name("comment").map(|m| m.as_str()); let type_ = macro_.name("type").unwrap().as_str(); let args = macro_.name("args").map(|m| m.as_str()); (|| { match type_ { "Q_OBJECT" => classtype = Some(ClassType::Object), "Q_GADGET" => classtype = Some(ClassType::Gadget), "QML_ELEMENT" => qml_name = Some(name), "QML_NAMED_ELEMENT" => { qml_name = Some(args.ok_or_else(|| { anyhow!("expected name for QML_NAMED_ELEMENT") })?) }, "QML_SINGLETON" => singleton = true, "QML_UNCREATABLE" => uncreatable = true, "Q_PROPERTY" => { let prop = self.property_regex .captures(args.ok_or_else(|| { anyhow!("expected args for Q_PROPERTY") })?) .ok_or_else(|| anyhow!("unable to parse Q_PROPERTY"))?; let member = prop.name("member").is_some(); let read = prop.name("read").is_some(); let write = prop.name("write").is_some(); let constant = prop.name("const").is_some(); if let Some(notify) = prop.name("notify").map(|v| v.as_str()) { notify_signals.push(notify); } properties.push(Property { type_: prop.name("type").unwrap().as_str(), name: prop.name("name").unwrap().as_str(), comment, readable: read || member, writable: !constant && (write || member), default: false, }); }, "Q_CLASSINFO" => { let classinfo = self.defaultprop_classinfo_regex.captures( args.ok_or_else(|| anyhow!("expected args for Q_CLASSINFO"))?, ); if let Some(classinfo) = classinfo { let prop = classinfo.name("prop").unwrap().as_str(); default_property = Some(prop); } }, _ => {}, } Ok::<_, anyhow::Error>(()) })() .with_context(|| { format!("while parsing macro `{}`", macro_.get(0).unwrap().as_str()) })?; } if let Some(prop) = default_property { let prop = properties .iter_mut() .find(|p| p.name == prop) .ok_or_else(|| anyhow!("could not find default property `{prop}`"))?; prop.default = true; } for invokable in self.fn_regex.captures_iter(body) { if invokable.name("invokable").is_none() { continue; } let comment = invokable.name("comment").map(|m| m.as_str()); let type_ = invokable.name("type").unwrap().as_str(); let name = invokable.name("name").unwrap().as_str(); let params_raw = invokable.name("params").unwrap().as_str(); let mut params = Vec::new(); for param in self.fn_param_regex.captures_iter(params_raw) { let type_ = param.name("type").unwrap().as_str(); let name = param.name("name").unwrap().as_str(); params.push(InvokableParam { type_, name }); } invokables.push(Invokable { name, ret: type_, comment, params, }); } for signal_set in self.signals_regex.captures_iter(body) { let signals_body = signal_set.name("signals").unwrap().as_str(); for signal in self.fn_regex.captures_iter(signals_body) { if signal.name("invokable").is_some() { continue; } let comment = signal.name("comment").map(|m| m.as_str()); let type_ = signal.name("type").unwrap().as_str(); let name = signal.name("name").unwrap().as_str(); let params_raw = signal.name("params").unwrap().as_str(); if notify_signals.contains(&name) { continue; } if type_ != "void" { bail!("non void return for signal {name}"); } let mut params = Vec::new(); for param in self.fn_param_regex.captures_iter(params_raw) { let type_ = param.name("type").unwrap().as_str(); let name = param.name("name").unwrap().as_str(); params.push(InvokableParam { type_, name }); } signals.push(Signal { name, comment, params, }); } } Ok::<_, anyhow::Error>(()) })() .with_context(|| format!("while parsing class `{name}`"))?; let Some(type_) = classtype else { continue }; ctx.classes.push(ClassInfo { type_, name, qml_name, superclass, singleton, uncreatable, comment, properties, invokables, signals, }); } Ok(()) } pub fn parse_enums<'a>(&self, text: &'a str, ctx: &mut ParseContext<'a>) -> anyhow::Result<()> { for enum_ in self.enum_regex.captures_iter(text) { let comment = enum_.name("comment").map(|m| m.as_str()); let namespace = enum_.name("namespace").unwrap().as_str(); let enum_name = enum_.name("enum_name").unwrap().as_str(); let qml_name = enum_ .name("qml_name") .map(|m| m.as_str()) .unwrap_or(namespace); let body = enum_.name("body").unwrap().as_str(); let mut variants = Vec::new(); for variant in self.enum_variant_regex.captures_iter(body) { let comment = variant.name("comment").map(|m| m.as_str()); let name = variant.name("name").unwrap().as_str(); variants.push(Variant { name, comment }); } ctx.enums.push(EnumInfo { namespace, enum_name, qml_name, comment, variants, }); } Ok(()) } pub fn parse<'a>(&self, text: &'a str, ctx: &mut ParseContext<'a>) -> anyhow::Result<()> { self.parse_classes(text, ctx)?; self.parse_enums(text, ctx)?; Ok(()) } } impl ParseContext<'_> { pub fn gen_typespec(&self, module: &str) -> typespec::TypeSpec { typespec::TypeSpec { typemap: self .classes .iter() .filter_map(|class| { Some(typespec::QmlTypeMapping { // filters gadgets name: class.qml_name?.to_string(), cname: class.name.to_string(), module: Some(module.to_string()), }) }) .collect(), classes: self .classes .iter() .filter_map(|class| { let (description, details) = class .comment .map(parse_details_desc) .unwrap_or((None, None)); Some(typespec::Class { name: class.name.to_string(), module: module.to_string(), description, details, // filters gadgets superclass: class.superclass?.to_string(), singleton: class.singleton, uncreatable: class.uncreatable, properties: class.properties.iter().map(|p| (*p).into()).collect(), functions: class.invokables.iter().map(|f| f.as_typespec()).collect(), signals: class.signals.iter().map(|s| s.as_typespec()).collect(), }) }) .collect(), gadgets: self .classes .iter() .filter_map(|class| match class.type_ { ClassType::Gadget => Some(typespec::Gadget { cname: class.name.to_string(), properties: class.properties.iter().map(|p| (*p).into()).collect(), }), _ => None, }) .collect(), enums: self .enums .iter() .map(|enum_| { let (description, details) = enum_ .comment .map(parse_details_desc) .unwrap_or((None, None)); typespec::Enum { name: enum_.qml_name.to_string(), module: Some(module.to_string()), cname: Some(format!("{}::{}", enum_.namespace, enum_.enum_name)), description, details, varaints: enum_.variants.iter().map(|v| (*v).into()).collect(), } }) .collect(), } } } impl From> for typespec::Property { fn from(value: Property) -> Self { Self { type_: value.type_.to_string(), name: value.name.to_string(), details: value.comment.map(parse_details), readable: value.readable, writable: value.writable, default: value.default, } } } impl From> for typespec::Variant { fn from(value: Variant<'_>) -> Self { Self { name: value.name.to_string(), details: value.comment.map(parse_details), } } } impl Invokable<'_> { fn as_typespec(&self) -> typespec::Function { typespec::Function { ret: self.ret.to_string(), name: self.name.to_string(), details: self.comment.map(parse_details), params: self.params.iter().map(|p| (*p).into()).collect(), } } } impl Signal<'_> { fn as_typespec(&self) -> typespec::Signal { typespec::Signal { name: self.name.to_string(), details: self.comment.map(parse_details), params: self.params.iter().map(|p| (*p).into()).collect(), } } } impl From> for typespec::FnParam { fn from(value: InvokableParam<'_>) -> Self { Self { type_: value.type_.to_string(), name: value.name.to_string(), } } } fn parse_details(text: &str) -> String { let mut seen_content = false; let mut callout = false; let mut str = text .lines() .map(|line| { line.trim() .strip_prefix("///") .map(|line| line.strip_prefix(' ').unwrap_or(line)) .unwrap_or(line) }) .filter(|line| { let any = !line.is_empty(); let filter = any || seen_content; seen_content |= any; filter }) .map(|line| match callout { true => { if line.starts_with('>') { Cow::Borrowed(line[1..].strip_prefix(' ').unwrap_or(&line[1..])) } else { callout = false; Cow::Owned(format!("{{{{< /callout >}}}}\n{line}")) } }, false => { if line.starts_with("> [!") { let code = line[4..].split_once(']'); if let Some((code, line)) = code { let code = code.to_lowercase(); callout = true; return Cow::Owned(format!("{{{{< callout type=\"{code}\" >}}}}\n{line}")) } } return Cow::Borrowed(line); }, }) .fold(String::new(), |accum, line| accum + line.as_ref() + "\n"); if callout { str += "\n{{< /callout >}}"; } str } fn parse_details_desc(text: &str) -> (Option, Option) { let details = parse_details(text); if details.starts_with('!') { match details[1..].split_once('\n') { Some((desc, details)) => ( Some(desc.strip_prefix(' ').unwrap_or(desc).to_string()), Some(details.to_string()), ), None => ( Some( details[1..] .strip_prefix(' ') .unwrap_or(&details[1..]) .to_string(), ), None, ), } } else { (None, Some(details)) } }