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}