Skip to main content

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>);