Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Bright-Shard committed Oct 25, 2024
0 parents commit b07b49b
Show file tree
Hide file tree
Showing 22 changed files with 1,262 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "webby"
version = "0.1.0"
edition = "2021"

[dependencies]
base64 = "0.22.1"
boml = "0.3.1"

[features]
default = []
log = []
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# webby

> The smol web compiler
As seen in [my website](https://github.com/bright-shard/website).

**webby** is a small and efficient compiler for making static sites. It adds macros, minifiers, and translators to compile your project into a tiny static site.

> Note: Webby is WIP. The above is a summary of what I want it to do when it's finished. For the current project status, see [todo](#todo).
# macros

webby adds a few simple macros to make writing HTML simpler. Macros open with `#!`, followed by the macro name, followed by arguments in parentheses, like so:

```
#!MACRO_NAME(args)
```

Macros can be combined, like this:

```
#!MACRO1(#!MACRO2(args))
```

- `#!INCLUDE(path/to/file)`: Webby will compile the given file, then embed it at the macro's location. The file must contain valid UTF-8 text.
- `#!BASE64(text)`: Base64-encode the given text.
- `#!INCLUDE_BASE64(path/to/file)`: Base64-encode the given file. This differs from `#!BASE64(#!INCLUDE(path/to/file))` because it can also base64-encode binary files.

# minifiers

webby will automatically strip comments and unneeded whitespace from your code to make it as small as possible.

# translators

Translators cross-compile between languages - for example, Markdown to HTML, or Gemtext to HTML.



# usage

webby projects have a `webby.toml` in the root of their project, just like Rust projects have a `Cargo.toml` in the root of theirs. The format of `webby.toml` is given in [config](#config).

To install webby, just install it with Cargo:

```sh
cargo install --git https://github.com/bright-shard/webby
```

Then just run `webby` in your webby project.

# config

In its simplest form, the `webby.toml` file will look like this:

```toml
# For every file you want to compile with webby, add a `[[target]]` section
[[target]]
# The path to the file to compile
path = "index.html"

[[target]]
path = "blog.html"
```

However, webby allows customising more if you need it:

```toml
# (Optional) the directory to put the output files at
# If this isn't specified it defaults to `webby`
# The path is relative to the webby.toml file
output = "my/custom/build/dir"

[[target]]
# The path to the file, relative to the webby.toml file
# If you list a folder instead of a file, webby will compile all of the files
# in that folder
path = "path/to/file.html"
# (Optional) Where to put the compiled file
# If this isn't specified it defaults to the name of the file given in path
# The path is relative to the output directory
output = "file.out.html"
# (Optional) The compilation mode
# This can be "compile", "copy", or "link". Compile will compile the file. Copy
# will just copy the file as-is and will not compile it at all. Link is the same
# as copy, but it creates a hard link (not a symlink) to the file instead of
# copying it.
# If this isn't specified, webby will infer if it should compile or copy the
# file based on the file's ending.
mode = "compile"
```

# todo

- [x] Macros
- [x] INCLUDE
- [x] BASE64
- [x] BASE64_INCLUDE
- [x] HTML minifier
- [x] CSS minifier
- [ ] JS minifier
- [x] Gemtext translator
- [ ] Markdown translator
- [ ] Redo macro compiler... it's old and has bugs
121 changes: 121 additions & 0 deletions src/compiler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use {
crate::{line_number_of_offset, Cow},
base64::{engine::general_purpose::STANDARD, Engine},
std::{fs, path::Path},
};

pub fn compile_macros<'a>(original: &'a str, source_path: &'a Path) -> Cow<'a> {
let mut output = String::default();
let mut offset = 0;

while let Some(start_idx) = original[offset..].find("#!") {
if original[offset..]
.as_bytes()
.get(start_idx.saturating_sub(1))
.copied()
== Some(b'\\')
{
if !output.is_empty() {
output += &original[offset..offset + start_idx + 1]
}
offset += start_idx + 1;
continue;
}

output += &original[offset..offset + start_idx];
offset += start_idx;

let macro_src = &original[offset..];
let paren_open = macro_src.find('(').unwrap_or_else(|| {
panic!(
"Expected ( in macro invocation at {source_path:?}:{}",
line_number_of_offset(original, offset)
)
});
let mut paren_close = macro_src.find(')').unwrap_or_else(|| {
panic!(
"Expected ) to end macro invocation at {source_path:?}:{}",
line_number_of_offset(original, offset)
)
});
while macro_src.as_bytes().get(paren_close + 1).copied() == Some(b')') {
paren_close += 1;
}

let macro_name = &macro_src[2..paren_open];
let macro_args = &macro_src[paren_open + 1..paren_close];
let macro_args = compile_macros(macro_args, source_path);
let macro_args = macro_args.as_ref();

match macro_name {
"INCLUDE" => {
let path = source_path.parent().unwrap().join(macro_args);
let src = fs::read_to_string(&path).unwrap_or_else(|err| {
panic!(
"Error in INCLUDE macro at {source_path:?}:{}: {err}",
line_number_of_offset(original, offset)
)
});
let compiled = compile_macros(&src, &path);
output += compiled.as_ref();
}
"BASE64" => {
output += STANDARD.encode(macro_args).as_str();
}
"INCLUDE_BASE64" => {
let path = source_path.parent().unwrap().join(macro_args);
let src = fs::read(&path).unwrap_or_else(|err| {
panic!(
"Error in INCLUDE_BASE64 macro at {source_path:?}:{}: {err}",
line_number_of_offset(original, offset)
)
});
output += STANDARD.encode(&src).as_str();
}
other => panic!(
"Unknown macro '{other}' in macro invocation at {source_path:?}:{}",
line_number_of_offset(original, offset)
),
}

offset += paren_close + 1;
}

if output.is_empty() {
Cow::Borrowed(original)
} else {
output += &original[offset..];
Cow::Owned(output)
}
}

pub fn copy_batch_target(src: &Path, dest: &Path) {
if dest.is_file() {
fs::remove_file(dest).unwrap_or_else(|err| {
panic!("Failed to copy batch target {src:?}. There was already a file where its output should go ({dest:?}), which couldn't be removed: {err}");
});
}
if !dest.exists() {
fs::create_dir_all(dest).unwrap_or_else(|err| {
panic!("Failed to copy batch target {src:?}. Couldn't create its output folder at {dest:?} because: {err}");
});
}

let src = src.read_dir().unwrap_or_else(|err| {
panic!(
"Failed to copy batch target {dest:?}. Couldn't open its source directory because: {err}"
);
});

for dir_entry in src.filter_map(|dir_entry| dir_entry.ok()) {
let dir_entry = &dir_entry.path();

if dir_entry.is_file() {
fs::copy(dir_entry, dest.join(dir_entry.file_name().unwrap())).unwrap_or_else(|err| {
panic!("Failed to copy batch target {dest:?}. Couldn't copy file at {dir_entry:?} because: {err}");
});
} else {
copy_batch_target(dir_entry, &dest.join(dir_entry.file_name().unwrap()));
}
}
}
71 changes: 71 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
pub mod compiler;
pub mod minifier;
pub mod translator;

use std::{fs, path::PathBuf};

type Cow<'a> = std::borrow::Cow<'a, str>;

pub enum Mode {
Compile,
Copy,
Link,
}

pub struct Target {
pub path: PathBuf,
pub output: PathBuf,
pub mode: Mode,
}

pub fn build_target(target: Target) -> Result<(), Cow<'static>> {
match target.mode {
Mode::Copy => {
if target.path.is_file() | target.path.is_symlink() {
fs::copy(target.path, target.output).unwrap();
} else {
compiler::copy_batch_target(&target.path, &target.output);
}
}
Mode::Link => {
if target.output.exists() {
fs::remove_file(&target.output)
.unwrap_or_else(|err| panic!("Failed to link target {:?}: {err}", &target.path))
}
fs::hard_link(&target.path, target.output)
.unwrap_or_else(|err| panic!("Failed to link target {:?}: {err}", &target.path));
}
Mode::Compile => {
let original = fs::read_to_string(&target.path).unwrap_or_else(|err| {
panic!(
"Failed to compile target {:?}: Error occurred while reading the source file: {err}",
&target.path
)
});
let compiled_macros = compiler::compile_macros(&original, &target.path);

let output = match target.path.extension().and_then(|val| val.to_str()) {
Some("gmi") => Cow::Owned(translator::translate_gemtext(
&target.path,
compiled_macros.as_ref(),
)?),
Some("html") => Cow::Owned(minifier::minify_html(
target.path.to_str().unwrap(),
&compiled_macros,
&original,
)?),
Some("css") => Cow::Owned(minifier::minify_css(&compiled_macros)),
_ => compiled_macros,
};

fs::write(&target.output, output.as_ref())
.unwrap_or_else(|err| panic!("Failed to compile target {:?}: Error occured while writing the compiled file: {err}", &target.path));
}
}

Ok(())
}

fn line_number_of_offset(src: &str, offset: usize) -> usize {
src[..offset].bytes().filter(|byte| *byte == b'\n').count()
}
Loading

0 comments on commit b07b49b

Please sign in to comment.