unreact/lib.rs
1//! # Unreact
2//!
3//! [Unreact](https://github.com/darccyy/unreact) is a simple static site generation framework
4//!
5//! ## Quick Start
6//!
7//! See also: [examples](https://github.com/darccyy/unreact/tree/main/examples)
8//! and [unreact template](https://github.com/darccyy/unreact-template)
9//!
10//! Create an site with a single index page
11//!
12//! ```rust,no_run
13//! use unreact::prelude::*;
14//!
15//! fn main() -> Result<(), Error> {
16//! // Create the app
17//! // Using default config, not in dev mode, and an example url
18//! let mut app = Unreact::new(Config::default(), false, "https://example.com")?;
19//! // Create an index route
20//! // This uses the template 'page.hbs' in 'templates/'
21//! // A json object with a value for 'foo' is passed into the template
22//! app.index("page", object! { foo: "World!" });
23//! // Compile it!
24//! app.run()
25//! }
26//! ```
27//!
28//! Your workspace should look something like this:
29//!
30//! ```txt
31//! unreact-app/
32//! ├─ Cargo.toml
33//! ├─ src/
34//! │ └─ main.rs
35//! │
36//! └─ assets/
37//! ├─ templates/
38//! │ └─ page.hbs
39//! │
40//! ├─ styles/
41//! └─ public/
42//! ```
43//!
44//! This is the contents of `assets/templates/page.hbs`:
45//!
46//! ```hbs
47//! <h1> Hello {{foo}} </h1>
48//! ```
49//!
50//! This will render `build/index.html`:
51//!
52//! ```html
53//! <h1> Hello World! </h1>
54//! ```
55//!
56//! A larger project could look something like this:
57//!
58//! ```txt
59//! unreact-app/
60//! ├─ Cargo.toml
61//! ├─ src/
62//! │ └─ main.rs
63//! │
64//! └─ assets/
65//! ├─ templates/
66//! │ ├─ boilerplate.hbs
67//! │ ├─ hello.hbs
68//! │ ├─ other/
69//! │ │ ├─ another/
70//! │ │ │ └─ something.hbs
71//! │ │ └─ article.hbs
72//! │ └─ page.hbs
73//! │
74//! ├─ styles/
75//! │ ├─ global.scss
76//! │ └─ scoped/
77//! │ └─ stylish.scss
78//! │
79//! └─ public/
80//! └─ favicon.ico
81//! ```
82
83#![doc(html_logo_url = "https://raw.githubusercontent.com/darccyy/unreact/main/icon.png")]
84#![doc(html_favicon_url = "https://raw.githubusercontent.com/darccyy/unreact/main/icon.png")]
85
86/// Private macros module
87#[macro_use]
88mod macros;
89/// `Unreact` struct implementations
90mod app;
91/// `Config` struct
92mod config;
93/// Convert and render filetypes, .hbs and .scss
94mod convert;
95/// Unreact `Error` type
96mod error;
97/// Handle file system logic
98mod files;
99
100/// Dev server and websockets
101#[cfg(feature = "dev")]
102mod server;
103
104use handlebars::Handlebars;
105use std::collections::HashMap;
106
107pub use crate::{
108 config::Config,
109 error::{Error, IoError},
110};
111
112pub use handlebars;
113pub use serde_json::{json, Value};
114
115/// Prelude for `Unreact`
116///
117/// ## Contains
118///
119/// - [`Unreact`] struct
120/// - [`Config`] struct
121/// - [`object`] macro
122/// - [`is_dev`](fn.is_dev.html) function
123/// - [`Error`] enum
124pub mod prelude {
125 pub use crate::{is_dev, object, Config, Error, Unreact};
126}
127
128/// Represents json-like object
129/// A map of string keys to json values
130///
131/// A type alias for `serde_json::Map<String, serde_json::Value>`
132///
133/// See also: [`object`]
134pub type Object = serde_json::Map<String, Value>;
135
136/// Map a filepath to file contents
137type FileMap = HashMap<String, String>;
138/// Map a path to a `Page` enum
139type RouteMap = HashMap<String, Page>;
140
141/// Build directory for *dev mode*
142///
143/// Overrides any build directory given
144const DEV_BUILD_DIR: &str = ".devbuild";
145
146/// A page that will be rendered
147///
148/// ## Variants
149///
150/// - `Raw`: Raw string
151/// - `Template`: Render a template, with data
152#[derive(Debug)]
153enum Page {
154 /// Raw string
155 Raw(String),
156 /// Render a template, with data
157 Template { template: String, data: Object },
158}
159
160/// Unreact app
161///
162/// Create a new app with `Unreact::new()`
163///
164/// # Examples
165///
166/// ```rust,no_run
167/// use unreact::prelude::*;
168///
169/// const URL: &str = "https://example.com";
170///
171/// fn main() -> Result<(), Error> {
172/// let mut app = Unreact::new(Config::default(), false, URL)?;
173///
174/// app
175/// .index("page", object! {})?
176/// .route("hi", "hello", object! {
177/// world: "World!"
178/// })?;
179///
180/// app.run()
181/// }
182#[derive(Debug)]
183pub struct Unreact<'a> {
184 /// Configuration for app
185 config: Config,
186 /// Map paths to pages
187 routes: RouteMap,
188 /// Global variables for templates
189 globals: Object,
190 /// Whether *dev mode* is active
191 is_dev: bool,
192 /// [`Handlebars`](handlebars) registry
193 ///
194 /// Access with `.handlebars()` method
195 handlebars: Handlebars<'a>,
196 /// Url of app (overridden in *dev mode*)
197 ///
198 /// Access with `.url()` method
199 url: String,
200}
201
202/// Check if `--dev` or `-d` argument was passed on `cargo run`
203///
204/// # Examples
205///
206/// This will run in production mode
207///
208/// ```ps1
209/// cargo run
210/// ```
211///
212/// This will run in development mode
213///
214/// ```ps1
215/// cargo run -- --dev
216/// cargo run -- -d
217/// ```
218pub fn is_dev() -> bool {
219 let args = std::env::args().collect::<Vec<_>>();
220 args.contains(&"--dev".to_string()) || args.contains(&"-d".to_string())
221}
222
223/// Alias for u16
224type Port = u16;
225
226/// Local port to host dev server (on localhost)
227const DEFAULT_PORT: Port = 3000;
228/// Local port to host websocket hub (on localhost)
229const DEFAULT_PORT_WS: Port = 3001;
230
231/// Get package name from `Cargo.toml` file in workspace
232///
233/// Returns `None` if any errors are found, or no package name is found
234#[cfg(feature = "dev")]
235fn get_package_name() -> Option<String> {
236 // Read Cargo.toml or return
237 let file = std::fs::read_to_string("./Cargo.toml").ok()?;
238
239 // Current category is 'package'
240 let mut is_package = false;
241 // Loop lines
242 for line in file.lines() {
243 let line = line.trim();
244
245 // Change category
246 if line.starts_with('[') && line.ends_with(']') {
247 is_package = line == "[package]";
248 }
249 // Continue if not package category
250 if !is_package {
251 continue;
252 }
253
254 // Check key is 'name'
255 let mut split = line.split('=');
256 if split.next().map(|x| x.trim()) != Some("name") {
257 continue;
258 }
259 let rest: Vec<_> = split.collect();
260
261 // Get rest of line
262 let name = rest.join("=");
263 let name = name.trim();
264
265 // Remove first and last characters, break if not quotes
266 let mut chars = name.chars();
267 if chars.next() != Some('"') {
268 break;
269 }
270 if chars.next_back() != Some('"') {
271 break;
272 }
273
274 // Return value
275 return Some(chars.as_str().to_string());
276 }
277
278 // No name found
279 None
280}