This commit is contained in:
hkau 2024-02-04 23:01:49 -05:00
commit 253e7a7122
15 changed files with 857 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
/target
node_modules/
bun.lockb
Cargo.lock
out/
docs/
assets/css/

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "celestial"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pulldown-cmark = { version = "0.10.0" }
serde = { version = "1.0.196", features = ["derive"] }
toml = "0.8.9"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 hkau
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# celestial
Simple static site generator from Markdown files.

34
assets/js/link-header.js Normal file
View File

@ -0,0 +1,34 @@
// get link header
const link_header_import = document.getElementById("link-header-import");
const link_header = document.getElementById("link-header");
if (link_header_import) {
try {
// parse header
const header = JSON.parse(
link_header_import.getAttribute("data-header").replaceAll("'", '"')
);
// show header
link_header.style.display = "flex";
// build bottom
let bottom = "";
for (entry of header.links) {
bottom += `<a href="/${entry}" class="button ${
link_header_import.getAttribute("data-title") ===
entry.split(".html")[0]
? "active"
: ""
}">${entry.split(".html")[0]}</a>`;
}
// ...
link_header.innerHTML = `<div class="link-header-top"></div>
<div class="link-header-middle"><h1 class="no-margin">${
header.title || link_header_import.getAttribute("data-title")
}</h1></div>
<div class="link-header-bottom">${bottom}</div>`;
} catch {}
}

89
assets/js/theme.js Normal file
View File

@ -0,0 +1,89 @@
// https://code.stellular.org/SentryTwo/bundlrs/src/branch/master/static/ts/pages/Footer.ts
// theme manager
globalThis.SunIcon = document.getElementById("theme-icon-sun");
globalThis.MoonIcon = document.getElementById("theme-icon-moon");
globalThis.toggle_theme = () => {
if (
globalThis.localStorage.getItem("bundles:user.ForceClientTheme") ===
"true"
)
return;
const current = globalThis.localStorage.getItem("theme");
if (current === "dark") {
/* set light */
document.documentElement.classList.remove("dark-theme");
globalThis.localStorage.setItem("theme", "light");
globalThis.SunIcon.style.display = "flex";
globalThis.MoonIcon.style.display = "none";
} else {
/* set dark */
document.documentElement.classList.add("dark-theme");
globalThis.localStorage.setItem("theme", "dark");
globalThis.SunIcon.style.display = "none";
globalThis.MoonIcon.style.display = "flex";
}
};
/* prefer theme */
if (
globalThis.matchMedia("(prefers-color-scheme: dark)").matches &&
!globalThis.localStorage.getItem("theme")
) {
document.documentElement.classList.add("dark-theme");
globalThis.localStorage.setItem("theme", "dark");
globalThis.SunIcon.style.display = "none";
globalThis.MoonIcon.style.display = "flex";
} else if (
globalThis.matchMedia("(prefers-color-scheme: light)").matches &&
!globalThis.localStorage.getItem("theme")
) {
document.documentElement.classList.remove("dark-theme");
globalThis.localStorage.setItem("theme", "light");
globalThis.SunIcon.style.display = "flex";
globalThis.MoonIcon.style.display = "none";
} else if (globalThis.localStorage.getItem("theme")) {
/* restore theme */
const current = globalThis.localStorage.getItem("theme");
document.documentElement.className = `${current}-theme`;
if (current.includes("dark")) {
/* sun icon */
globalThis.SunIcon.style.display = "none";
globalThis.MoonIcon.style.display = "flex";
} else {
/* moon icon */
globalThis.SunIcon.style.display = "flex";
globalThis.MoonIcon.style.display = "none";
}
}
// global css string
if (
!globalThis.PASTE_USES_CUSTOM_THEME ||
globalThis.localStorage.getItem("bundles:user.ForceClientTheme") === "true"
) {
const style = document.createElement("style");
style.innerHTML = globalThis.localStorage.getItem(
"bundles:user.GlobalCSSString"
);
document.body.appendChild(style);
}
// localize dates
setTimeout(() => {
for (const element of Array.from(
document.querySelectorAll(".date-time-to-localize")
))
element.innerText = new Date(
parseInt(element.innerText)
).toLocaleString();
}, 50);
// default export
export default {};

280
assets/style.css Normal file
View File

@ -0,0 +1,280 @@
/* https://codeberg.org/hkau/fusion */
@import url("./css/fusion.css");
.tab-container {
background: var(--background-surface1);
transition: background 0.15s;
padding: 1.5rem !important;
height: 78dvh;
overflow-y: auto;
max-height: 90vh;
margin-bottom: 0.5rem;
max-width: 100vw;
min-height: 15rem;
}
.tabbar button:not(.full-normal),
.tabbar .button:not(.full-normal) {
border-radius: var(--u-02) var(--u-02) 0 0;
}
@media screen and (max-width: 900px) {
.tab-container {
max-height: 65vh;
padding: 1rem;
}
}
.-editor:not(.active) {
display: none;
}
#editor-tab-preview h1 {
width: 100%;
}
/* colors */
:root {
/* default colors (light) */
--base-hue: 0;
--base-sat: 0%;
--base-lit: 92%;
--mod: -;
--diff: 9%;
/* main colors */
--primary: hsl(0, 100%, 80%);
--primary-low: hsl(0, 100%, 76%);
--secondary: hsl(41, 100%, 62%);
--secondary-low: hsl(41, 100%, 58%);
}
.dark-theme {
/* default colors (dark) */
--base-hue: 0;
--base-sat: 0%;
--base-lit: 15%;
--mod: +;
--diff: 0%;
}
*.round {
border-radius: var(--u-02) !important;
}
/* svg */
svg {
fill: transparent;
stroke: currentColor;
}
/* button modifications */
button,
.button {
padding: var(--u-02) var(--u-08);
height: 35px !important;
user-select: none;
}
button.bundles-primary,
.button.bundles-primary {
background: var(--primary);
color: black;
}
button.bundles-primary:hover,
.button.bundles-primary:hover {
background: var(--primary-low);
}
button.bundles-secondary,
.button.bundles-secondary {
background: var(--secondary);
color: white;
}
button.bundles-secondary:hover,
.button.bundles-secondary:hover {
background: var(--secondary-low);
}
/* details */
details summary {
background: transparent;
border: solid 1px var(--background-surface2a);
border-radius: var(--u-02) !important;
}
details summary svg {
transition: transform 0.15s;
}
details[open] summary svg {
transform: rotate(90deg);
}
details[open] summary {
background: var(--background-surface1);
box-shadow: none;
margin-bottom: 0 !important;
border-radius: var(--u-02) var(--u-02) 0 0 !important;
}
details summary + .content {
display: none;
}
details[open] summary + .content {
border: solid 1px var(--background-surface2a);
border-top: none;
padding: var(--u-08);
border-radius: 0 0 var(--u-02) var(--u-02);
display: block;
}
/* hr */
hr {
border-color: var(--background-surface2a) !important;
}
/* input */
input,
textarea,
select {
background: var(--background-surface) !important;
border: solid 1px var(--background-surface2a);
}
input.round,
textarea.round,
select.round {
border-radius: var(--u-02) !important;
}
input:focus,
textarea:focus,
select:focus {
background: var(--background-surface0-5) !important;
}
/* notes */
.mdnote {
border-radius: var(--u-02) !important;
}
/* chips */
.chip.mention {
border-radius: var(--u-02);
background: var(--background-surface2a);
border: solid 1px var(--background-surface2);
color: var(--text-color);
}
/* sidebar */
.sidebar {
width: 325px;
background: var(--background-surface);
border-right: solid 1px var(--background-surface2a);
bottom: 0;
top: unset;
}
.sidebar.open {
left: 0;
}
@media screen and (max-width: 900px) {
.sidebar {
width: 100%;
}
}
/* toolbar */
:root {
--nav-height: 37.8px;
}
.toolbar {
width: 100%;
height: var(--nav-height);
padding: 0;
border-bottom: solid 1px var(--background-surface2a);
}
.toolbar button,
.toolbar .button,
.toolbar span {
padding: var(--u-10);
}
.toolbar button,
.toolbar .button {
height: var(--nav-height) !important;
display: flex;
justify-content: center;
background: transparent;
border-left: solid 1px var(--background-surface2a);
border-right: solid 1px var(--background-surface2a);
}
.toolbar button:hover,
.toolbar .button:hover {
background: var(--background-surface1a);
}
.toolbar button:hover *,
.toolbar .button:hover * {
justify-content: center;
align-items: center;
}
.sidebar-layout-wrapper,
.sidebar {
height: calc(100dvh - var(--nav-height));
}
/* link header */
#link-header {
padding: 0 calc(var(--u-10) * 4);
}
@media screen and (max-width: 900px) {
#link-header {
padding: 0 var(--u-10);
}
}
#link-header .link-header-middle {
padding: calc(var(--u-10) * 2) 0;
}
#link-header .link-header-bottom {
display: flex;
}
#link-header .link-header-bottom .button {
border-top-left-radius: var(--u-02);
border-top-right-radius: var(--u-02);
}
#link-header .link-header-bottom .button.active {
background: var(--background-surface);
box-shadow: none !important;
}
/* rendered content */
#content h1 {
width: 100%;
padding: 0 0 var(--u-10) 0;
border-bottom: solid 1px var(--background-surface2a);
}
#content pre {
background: var(--background-surface1);
padding: var(--u-04);
border-radius: var(--u-02);
border: solid 1px var(--background-surface2a);
}

1
celestial.toml Normal file
View File

@ -0,0 +1 @@
site_name = "Celestial"

11
package.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "celestial",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}

11
scripts/css.sh Executable file
View File

@ -0,0 +1,11 @@
wget https://codeberg.org/api/packages/hkau/npm/fusion/-/1.0.11/fusion-1.0.11.tgz -O fusion.tgz
mv ./fusion.tgz ./assets/fusion.tgz
cd assets
tar -xzf fusion.tgz
mv package/src/css ./css
sed -i -e 's/"\/utility.css"/"\/assets\/css\/utility.css"/' ./css/fusion.css
rm -r package
rm ./fusion.tgz

5
src/config.rs Normal file
View File

@ -0,0 +1,5 @@
use std::env;
pub fn collect_arguments() -> Vec<String> {
return env::args().collect::<Vec<String>>();
}

174
src/main.rs Normal file
View File

@ -0,0 +1,174 @@
use std::{collections::HashMap, fs, process};
mod config;
mod templater;
#[derive(serde::Deserialize, Clone)]
pub struct Config {
// global template variables
pub site_name: String,
}
fn main() {
let args: Vec<String> = config::collect_arguments();
// get input dir
let input = args
.get(1)
.expect("expected input directory (use './' for cwd)");
// get config
// (default)
let mut config: Config = Config {
site_name: String::from("celestial"),
};
// (from file)
let config_file = fs::read_to_string("celestial.toml");
if config_file.is_ok() {
config = toml::from_str::<Config>(config_file.unwrap().as_str())
.expect("failed to deserialize config");
}
// read directory
read_dir(input.to_string(), config, true); // TODO: maybe don't clone here
}
fn read_dir(dir: String, config: Config, create_structure: bool) -> () {
// create required directories
if create_structure == true {
fs::create_dir("out").expect("failed to create 'out' directory");
process::Command::new("cp")
.arg("-r")
.arg("assets")
.arg("out/assets")
.spawn()
.expect("failed to run 'cp' command");
}
// ...
let files = fs::read_dir(dir.clone());
if files.is_err() {
return;
}
let files = files.unwrap();
// build file listing
let mut file_link_list: String = String::new();
for file in files {
if file.is_err() {
continue;
}
let file = file.unwrap();
// get file name
let file_name = &file.file_name();
let file_name = file_name.to_str().unwrap();
// make sure file is a markdown file
if !file_name.ends_with(".md") {
continue;
}
let file_name = file_name.replace(".md", "");
// push to list
file_link_list += &format!(
"<a href=\"./{}.html\" class=\"button round border full justify-start\">{}</a>",
file_name, file_name
);
}
// "ReadDir" does not derive Clone, so we have to actually call it again
let files = fs::read_dir(dir.clone()).expect("failed to read specified directory");
// build page
for file in files {
if file.is_err() {
continue;
}
let file = file.unwrap();
// get file name
let file_name = &file.file_name();
let file_name = file_name.to_str().unwrap();
// get file type
let file_type = file.file_type().expect("failed to get file type");
// if file is a directory, call read_dir and continue
if file_type.is_dir() {
read_dir(format!("{}{}", dir, file_name), config.clone(), false);
continue;
}
// make sure file is a markdown file
if !file_name.ends_with(".md") {
continue;
}
// read file
let mut file_contents = fs::read_to_string(format!("{}/{}", &dir, file_name))
.expect("failed to read file contents");
let without_ext = format!(
"{}{}",
if dir != String::from("./") {
dir.clone() + "/"
} else {
String::new()
},
&file_name.replace(".md", "")
);
// get link header
let file_contents_c = file_contents.clone();
let header_split = file_contents_c.split("\n{{LH}}\n").collect::<Vec<&str>>();
if header_split.get(1).is_some() {
file_contents = header_split.get(1).unwrap().to_string();
}
// parse contents
let parser = pulldown_cmark::Parser::new(&file_contents);
let mut html_out: String = String::new();
pulldown_cmark::html::push_html(&mut html_out, parser);
// load template
let mut replacements: HashMap<String, &str> = HashMap::new();
replacements.insert(String::from("site_name"), &config.site_name);
replacements.insert(String::from("page_title"), &without_ext);
replacements.insert(String::from("rendered"), &html_out);
replacements.insert(String::from("sidebar"), &file_link_list);
let lh = header_split.get(0).unwrap().replace("\"", "'");
if header_split.get(1).is_some() {
// include link header
replacements.insert(String::from("link_header"), &lh);
}
let replaced_template =
templater::load_template(String::from("markdown_simple"), replacements);
// create directory in output
if dir != String::from("./") {
let existing_dir = fs::read_dir(format!("out/{}", &dir));
if existing_dir.is_err() {
fs::create_dir(format!("out/{}", &dir)).expect("failed to create directory");
}
}
// write file
fs::write(
format!("out/{}/{}", dir, file_name.replace(".md", ".html")),
replaced_template,
)
.expect("failed to write completed file");
}
}

15
src/templater.rs Normal file
View File

@ -0,0 +1,15 @@
use std::{collections::HashMap, fs};
pub fn load_template(template: String, replacements: HashMap<String, &str>) -> String {
// load file
let mut file = fs::read_to_string(format!("templates/{}.html", template))
.expect("failed to read template file");
// run replacements
for replacement in replacements.into_iter() {
file = file.replace(&format!("{{{{{}}}}}", replacement.0), replacement.1);
}
// return
return file;
}

View File

@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- identifier -->
<title>{{page_title}} - {{site_name}}</title>
<meta property="og:site_name" content="{{site_name}}" />
<meta name="theme-color" content="#fcda4f" />
<meta property="og:type" content="website" />
<meta property="og:title" content="{{page_title}}" />
<meta property="og:description" content="{{page_description}}" />
<!-- resources -->
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<!-- top nav -->
<div class="toolbar flex justify-space-between">
<!-- left -->
<div class="flex">
<a class="button" href="/" style="border-left: 0">
<b>{{site_name}}</b>
</a>
<a class="button" href="/{{page_title}}" style="border-left: 0">
{{page_title}}
</a>
</div>
<!-- right -->
<div class="flex">
<button
onclick="globalThis.toggle_sidebar()"
class="device:mobile mobile:flex"
style="border-right: 0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-menu flex"
>
<line x1="4" x2="20" y1="12" y2="12" />
<line x1="4" x2="20" y1="6" y2="6" />
<line x1="4" x2="20" y1="18" y2="18" />
</svg>
</button>
<button
onclick="globalThis.toggle_theme()"
id="theme_button"
style="border-right: 0"
>
<div id="theme-icon-sun">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-sun"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
</div>
<div id="theme-icon-moon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-moon"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
</div>
</button>
</div>
</div>
<!-- main layout-->
<div class="sidebar-layout-wrapper">
<!-- sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-content">
<div
class="card flex g-4"
style="flex-direction: column-reverse"
>
{{sidebar}}
</div>
<hr />
<div class="flex justify-center">
<p style="opacity: 75%">
powered by
<a
href="https://code.stellular.org/hkau/celestial"
target="_blank"
>
celestial
</a>
</p>
</div>
</div>
</div>
<!-- content -->
<div
style="
height: calc(100dvh - var(--nav-height));
width: 100%;
overflow: hidden auto;
"
>
<div
id="link-header"
style="display: none"
class="flex-column bg-1"
></div>
<main class="small" id="content">{{rendered}}</main>
</div>
</div>
<!-- scripts -->
<script src="/assets/js/theme.js" type="module"></script>
<script
src="/assets/js/link-header.js"
id="link-header-import"
data-header="{{link_header}}"
data-title="{{page_title}}"
></script>
<script>
globalThis.toggle_sidebar = () => {
const sidebar = document.getElementById("sidebar");
sidebar.classList.toggle("open");
};
</script>
</body>
</html>

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}