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