lumen_scene/source_parser.rs
1//! Injected source front-end - the markup + CSS parser boundary.
2//!
3//! Nothing under the runtime links the `.lmn` / CSS parser. That front-end
4//! stays in the compiler (`lumenc`), and `lumenc` depends on the runtime for
5//! its CLI `run` / `build` paths, so a direct parser dependency here would
6//! form a dependency cycle. The parser is injected instead:
7//!
8//! - The CLI (`lumenc run`), the Rust SDK, and the C-ABI dev paths hand a
9//! [`SourceParser`] to `RunOptions::parser`.
10//! - The dev source-load path and hot reload call it.
11//! - The precompiled-artifact path needs no parser at all and ignores this
12//! hook, which is also every path that runs an app in a browser or on a
13//! server.
14
15use bevy_ecs::prelude::Resource;
16use lumen_ir::css::Stylesheet;
17use lumen_ir::fragment::FragmentTable;
18use lumen_ir::layout_ir::{LayoutIR, ScriptRefs};
19use std::path::{Path, PathBuf};
20use std::sync::Arc;
21
22/// The markup/CSS front-end, injected by whoever drives the runtime from
23/// source. Implemented in `lumenc` (`LumencParser`) over its `roxmltree`
24/// markup parser, the hand-rolled CSS parser, the `<include>` / `@import`
25/// resolver, and the real-filesystem `FsLoader`.
26///
27/// Errors are surfaced as `String` (rendered from the front-end's own error
28/// types) so the trait carries no dependency on the parser's error model.
29pub trait SourceParser: Send + Sync {
30 /// Parse markup text (with `<include>` directives already spliced away)
31 /// into a [`LayoutIR`], instantiating any of `fragments` it names.
32 fn parse_html(&self, src: &str, fragments: &FragmentTable) -> Result<LayoutIR, String>;
33
34 /// Parse CSS text into a [`Stylesheet`].
35 fn parse_css(&self, src: &str) -> Result<Stylesheet, String>;
36
37 /// Resolve `<include src="...">` directives in `src` against
38 /// `self_path`'s directory (real filesystem). Every resolved file path is
39 /// appended to `out` so the hot-reload watcher can poll it.
40 fn resolve_includes(
41 &self,
42 src: &str,
43 self_path: &Path,
44 out: &mut Vec<PathBuf>,
45 ) -> Result<String, String>;
46
47 /// Resolve `@import "..."` directives in `src` against `self_path`.
48 /// Every imported file path is appended to `out`.
49 fn resolve_css_imports(
50 &self,
51 src: &str,
52 self_path: &Path,
53 out: &mut Vec<PathBuf>,
54 ) -> Result<String, String>;
55
56 /// Parse markup, resolving `<include>` directives against `self_path`'s
57 /// directory via the real-filesystem loader (the file-based-pages
58 /// assembly path).
59 fn parse_html_with_loader(
60 &self,
61 src: &str,
62 self_path: &Path,
63 fragments: &FragmentTable,
64 ) -> Result<LayoutIR, String>;
65
66 /// Read the fragments `src` declares, without building its tree. An app
67 /// collects these from every one of its `.lmn` files so a fragment
68 /// declared in one is usable from all of them.
69 fn collect_fragments(&self, src: &str, self_path: &Path) -> Result<FragmentTable, String>;
70
71 /// Build the fragments the markup blocks of one script declare. The
72 /// blocks are what the script's language read out of `src` (candela's
73 /// `lmn!`); this front-end turns each block's markup into a fragment.
74 /// `uri` is where the script came from, and lands on each fragment's
75 /// origin, at the line `src` holds the block on. An app collects these
76 /// alongside what its markup declares, so a shipped artifact carries every
77 /// fragment and parses no markup itself.
78 fn block_fragments(
79 &self,
80 src: &str,
81 uri: &str,
82 blocks: Vec<lumen_script::MarkupBlock>,
83 ) -> Result<FragmentTable, String>;
84
85 /// Read the `<script>` elements `src` names, without building its tree.
86 /// Markup names a script component by writing the function as a tag, so
87 /// the scripts are read before the tree and their blocks are in the table
88 /// the parse instantiates against.
89 fn script_refs(&self, src: &str, self_path: &Path) -> Result<ScriptRefs, String>;
90
91 /// Expand the use sites the fragment bodies hold against each other, so
92 /// every body is the whole subtree it stands for. An artifact carries the
93 /// linked table, which is what lets a shipped app instantiate with no
94 /// parser.
95 fn link_fragments(&self, table: FragmentTable) -> Result<FragmentTable, String>;
96}
97
98/// World-resource wrapper so an exclusive system (one taking `&mut World`,
99/// which cannot take the parser as a param) can reach the injected
100/// [`SourceParser`]. Hot reload and `set_inner_markup` are the two.
101///
102/// Put in by the host that has a parser to give. An app with none, which is
103/// every app running from a compiled artifact, carries no such resource and
104/// the callers report the limitation rather than assuming one.
105#[derive(Resource, Clone)]
106pub struct RuntimeParser(pub Arc<dyn SourceParser>);