Skip to main content

fusor/
authoring.rs

1//! The native-module bridge used by generated HTML bindings.
2
3/// Associate package-relative HTML with this ordinary Rust module.
4///
5/// The include path below is a contract with `fusor-build`, which writes the
6/// file and, in `app::includes`, exempts this shape from disabling development
7/// refresh. Change them together.
8///
9/// `compile_app()` compiles the default entry (`web/index.html`), reusable
10/// templates discovered under `web/components/`, and configured sources before
11/// macro expansion. HTML for this mode contains no Rust script. Generated impls
12/// stay in this lexical scope, including access to private state and imports.
13/// Call once per HTML file. Multiple component declarations in one file share
14/// this module. Use `/` separators and the exact configured package-relative path.
15#[macro_export]
16macro_rules! template {
17    (backend = $backend:literal, $path:literal) => {
18        include!(concat!(
19            env!("OUT_DIR"),
20            "/fusor_backends/",
21            $backend,
22            "/",
23            $path,
24            ".rs"
25        ));
26    };
27    ($path:literal) => {
28        include!(concat!(env!("OUT_DIR"), "/fusor_templates/", $path, ".rs"));
29    };
30}
31
32/// Include an HTML module's generated bindings in the ordinary Rust module
33/// declared by its script's `rust:module` attribute. The argument is the entry
34/// registration (`app`) or a component registration from Cargo metadata.
35///
36/// As with [`template!`], the include path below is a contract with
37/// `fusor-build`; change them together.
38///
39/// The authored Rust file remains in Cargo's normal module tree. Only generated
40/// implementations are included here; private fields retain their visibility.
41#[macro_export]
42macro_rules! bindings {
43    ($name:ident) => {
44        const __FUSOR_BINDINGS_ORIGIN: (&str, &str) = (file!(), env!("CARGO_MANIFEST_DIR"));
45        include!(concat!(
46            env!("OUT_DIR"),
47            "/fusor_",
48            stringify!($name),
49            ".rs"
50        ));
51    };
52}
53
54/// Compare native `file!()` origins with the explicitly registered source.
55/// Paths may contain native separators and `#[path]`'s lexical `..` components.
56#[doc(hidden)]
57pub const fn source_matches(origin: (&str, &str), expected: &str) -> bool {
58    let file = native_path(origin.0.as_bytes());
59    let manifest = native_path(origin.1.as_bytes());
60    let expected = PathParts(b"", native_path(expected.as_bytes()));
61    if same_path(PathParts(b"", file), expected) {
62        return true;
63    }
64    // Cargo's file!() is either absolute or relative to the workspace/package
65    // invocation directory. Only ancestors of this package are candidates;
66    // accepting arbitrary suffixes would allow a different nested src/app.rs.
67    let mut end = manifest.len();
68    loop {
69        if same_path(PathParts(manifest.split_at(end).0, file), expected) {
70            return true;
71        }
72        if end == 0 {
73            return false;
74        }
75        end -= 1;
76        while end > 0 && !separator(manifest[end]) {
77            end -= 1;
78        }
79    }
80}
81
82// std::fs::canonicalize produces verbatim Windows paths; Cargo's source and
83// manifest paths generally do not include this prefix.
84const fn native_path(path: &[u8]) -> &[u8] {
85    if path.len() >= 4
86        && separator(path[0])
87        && separator(path[1])
88        && path[2] == b'?'
89        && separator(path[3])
90    {
91        let path = path.split_at(4).1;
92        if path.len() >= 4
93            && path[0] == b'U'
94            && path[1] == b'N'
95            && path[2] == b'C'
96            && separator(path[3])
97        {
98            return path.split_at(4).1;
99        }
100        return path;
101    }
102    path
103}
104
105const fn separator(byte: u8) -> bool {
106    byte == b'/' || byte == b'\\'
107}
108
109#[derive(Clone, Copy)]
110struct PathParts<'a>(&'a [u8], &'a [u8]);
111impl PathParts<'_> {
112    const fn len(self) -> usize {
113        self.0.len() + 1 + self.1.len()
114    }
115    const fn at(self, index: usize) -> u8 {
116        if index < self.0.len() {
117            self.0[index]
118        } else if index == self.0.len() {
119            b'/'
120        } else {
121            self.1[index - self.0.len() - 1]
122        }
123    }
124}
125
126const fn previous(path: PathParts<'_>, mut end: usize) -> Option<(usize, usize)> {
127    let mut parents = 0;
128    while end > 0 {
129        while end > 0 && separator(path.at(end - 1)) {
130            end -= 1;
131        }
132        if end == 0 {
133            break;
134        }
135        let mut start = end;
136        while start > 0 && !separator(path.at(start - 1)) {
137            start -= 1;
138        }
139        if end - start == 2 && path.at(start) == b'.' && path.at(start + 1) == b'.' {
140            parents += 1;
141        } else if end - start == 1 && path.at(start) == b'.' {
142        } else if parents > 0 {
143            parents -= 1;
144        } else {
145            return Some((start, end));
146        }
147        end = start;
148    }
149    None
150}
151
152const fn same_path(left: PathParts<'_>, right: PathParts<'_>) -> bool {
153    let mut a = left.len();
154    let mut b = right.len();
155    loop {
156        match (previous(left, a), previous(right, b)) {
157            (None, None) => return true,
158            (Some((start_a, end_a)), Some((start_b, end_b))) => {
159                if end_a - start_a != end_b - start_b {
160                    return false;
161                }
162                let mut index = 0;
163                while index < end_a - start_a {
164                    if left.at(start_a + index) != right.at(start_b + index) {
165                        return false;
166                    }
167                    index += 1;
168                }
169                a = start_a;
170                b = start_b;
171            }
172            _ => return false,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::source_matches;
180
181    #[test]
182    fn source_identity_handles_cargo_and_native_module_paths() {
183        for (origin, expected) in [
184            (("src/app.rs", "/app"), "/app/src/app.rs"),
185            (("/app/src/app.rs", "/app"), "/app/src/app.rs"),
186            (
187                ("examples/app/src/app.rs", "/workspace/examples/app"),
188                "/workspace/examples/app/web/../src/app.rs",
189            ),
190            (("src/../web/page.rs", "/app"), "/app/web/page.rs"),
191            (("src\\app.rs", "C:\\app"), "C:/app/src/app.rs"),
192            (("src\\app.rs", "C:\\app"), "\\\\?\\C:\\app\\src\\app.rs"),
193            (
194                ("src\\app.rs", "\\\\server\\app"),
195                "\\\\?\\UNC\\server\\app\\src\\app.rs",
196            ),
197            (
198                ("./src/nested/../../web/page.rs", "/app"),
199                "/app/web/page.rs",
200            ),
201        ] {
202            assert!(source_matches(origin, expected));
203            assert!(!source_matches(origin, "/another/page.rs"));
204            assert!(!source_matches(origin, "/app/other/src/app.rs"));
205        }
206    }
207}