Skip to main content

renox_core/components/
plugin.rs

1//! Components that modules and plugins register (`Registry::component`): an owned, public
2//! description turned into the compiler's [`Contract`] at boot.
3
4use super::attrs::Kind;
5use super::contracts::{Contract, Module, Prop as Compiled, Render, Slot};
6
7/// One attribute of a [`Component`].
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct Prop {
11    name: String,
12    kind: Kind,
13    required: bool,
14    values: Vec<String>,
15    doc: String,
16}
17
18impl Prop {
19    fn new(name: &str, kind: Kind) -> Self {
20        Self {
21            name: name.to_owned(),
22            kind,
23            required: false,
24            values: Vec::new(),
25            doc: String::new(),
26        }
27    }
28
29    /// Text, possibly with `{{ }}`: `title="Hello {{ name }}"`.
30    pub fn text(name: &str) -> Self {
31        Self::new(name, Kind::Text)
32    }
33
34    /// A flag: a bare attribute, `"true"`/`"false"`, or `:name="expr"`.
35    pub fn bool(name: &str) -> Self {
36        Self::new(name, Kind::Bool)
37    }
38
39    /// A number.
40    pub fn number(name: &str) -> Self {
41        Self::new(name, Kind::Number)
42    }
43
44    /// Data from the page's context: only `:name="expr"` is accepted.
45    pub fn data(name: &str) -> Self {
46        Self::new(name, Kind::Data)
47    }
48
49    /// One of a fixed list of words.
50    pub fn choice(name: &str, values: &[&str]) -> Self {
51        let mut prop = Self::new(name, Kind::Enum);
52        prop.values = values.iter().map(|v| (*v).to_owned()).collect();
53        prop
54    }
55
56    /// The attribute must be given.
57    pub fn required(mut self) -> Self {
58        self.required = true;
59        self
60    }
61
62    /// One line about it, for tooling.
63    pub fn doc(mut self, doc: &str) -> Self {
64        self.doc = doc.to_owned();
65        self
66    }
67}
68
69/// A component a module registers with `Registry::component`: a tag that
70/// compiles to a call of one of the module's template macros.
71///
72/// ```
73/// use renox::view::{Component, Prop};
74///
75/// let hello = Component::macro_call("rx-hello", "hello/ui.html", "hello")
76///     .doc("Greets someone.")
77///     .prop(Prop::text("name").required());
78/// # let _ = hello;
79/// ```
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[non_exhaustive]
82pub struct Component {
83    tag: String,
84    template: String,
85    macro_name: String,
86    doc: String,
87    props: Vec<Prop>,
88    content: bool,
89    attrs: bool,
90}
91
92impl Component {
93    /// `<tag …>` becomes `macro_name(…)` of the template `template`, which the
94    /// module adds with [`add_template`](crate::view::add_template). The tag
95    /// starts with `rx-`.
96    pub fn macro_call(tag: &str, template: &str, macro_name: &str) -> Self {
97        Self {
98            tag: tag.to_owned(),
99            template: template.to_owned(),
100            macro_name: macro_name.to_owned(),
101            doc: String::new(),
102            props: Vec::new(),
103            content: true,
104            attrs: false,
105        }
106    }
107
108    /// One line about the component, for tooling.
109    pub fn doc(mut self, doc: &str) -> Self {
110        self.doc = doc.to_owned();
111        self
112    }
113
114    /// Adds an attribute.
115    pub fn prop(mut self, prop: Prop) -> Self {
116        self.props.push(prop);
117        self
118    }
119
120    /// Whether the tag takes content (the macro's `caller()`). On by default.
121    pub fn content(mut self, takes: bool) -> Self {
122        self.content = takes;
123        self
124    }
125
126    /// Lets other HTML attributes (`data-*`, `@click`, `title`…) through as the
127    /// macro's `attrs`.
128    pub fn attrs(mut self, pass: bool) -> Self {
129        self.attrs = pass;
130        self
131    }
132
133    /// The tag, such as `rx-hello`.
134    pub fn tag(&self) -> &str {
135        &self.tag
136    }
137
138    /// Why this can't be registered, if so.
139    pub(crate) fn problem(&self) -> Option<String> {
140        if !self.tag.starts_with("rx-") || self.tag.len() <= 3 {
141            return Some(format!("the tag \"{}\" must start with \"rx-\"", self.tag));
142        }
143        if super::contracts::BUILTIN.iter().any(|c| c.tag == self.tag) {
144            return Some(format!("<{}> is a built-in component", self.tag));
145        }
146        None
147    }
148
149    /// The compiler's form. The strings are leaked: components are registered once
150    /// at boot and live as long as the process.
151    pub(crate) fn contract(&self) -> Contract {
152        fn leak(s: &str) -> &'static str {
153            Box::leak(s.to_owned().into_boxed_str())
154        }
155        let props: Vec<Compiled> = self
156            .props
157            .iter()
158            .map(|p| Compiled {
159                name: leak(&p.name),
160                kind: p.kind,
161                required: p.required,
162                values: Box::leak(
163                    p.values
164                        .iter()
165                        .map(|v| leak(v))
166                        .collect::<Vec<_>>()
167                        .into_boxed_slice(),
168                ),
169                doc: leak(&p.doc),
170            })
171            .collect();
172        let alias: String = self
173            .template
174            .chars()
175            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
176            .collect();
177        let slots: &'static [Slot] = if self.content {
178            Box::leak(Box::new([Slot {
179                name: "",
180                into: None,
181                args: &[],
182                optional: true,
183                doc: "The content.",
184            }]))
185        } else {
186            &[]
187        };
188        Contract {
189            tag: leak(&self.tag),
190            doc: leak(&self.doc),
191            render: Render::Macro {
192                module: Module::Custom {
193                    path: leak(&self.template),
194                    alias: leak(&format!("__rx_p_{alias}")),
195                },
196                name: leak(&self.macro_name),
197            },
198            props: Box::leak(props.into_boxed_slice()),
199            slots,
200            events: &[],
201            route_prop: None,
202            attrs: self.attrs,
203            parent: None,
204        }
205    }
206}