mirror of
https://github.com/Garmelon/Arbeitszeitdokumentationsgenerator.git
synced 2026-04-12 16:55:04 +02:00
Render time sheets using typst
This commit is contained in:
parent
c423162a5f
commit
6524fb63c2
4 changed files with 2166 additions and 2 deletions
1956
Cargo.lock
generated
1956
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,5 +7,9 @@ edition = "2021"
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
clap = { version = "4.5.4", features = ["derive", "deprecated"] }
|
clap = { version = "4.5.4", features = ["derive", "deprecated"] }
|
||||||
|
comemo = "0.4.0"
|
||||||
|
fontdb = "0.16.2"
|
||||||
maud = { version = "0.26.0", features = ["axum"] }
|
maud = { version = "0.26.0", features = ["axum"] }
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
|
typst = "0.11.0"
|
||||||
|
typst-pdf = "0.11.0"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
mod render;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use maud::{html, Markup};
|
use maud::{html, Markup};
|
||||||
|
|
|
||||||
206
src/render.rs
Normal file
206
src/render.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
use comemo::Prehashed;
|
||||||
|
use typst::{
|
||||||
|
diag::{FileError, FileResult, SourceResult},
|
||||||
|
eval::Tracer,
|
||||||
|
foundations::{Bytes, Datetime, Smart},
|
||||||
|
model::Document,
|
||||||
|
syntax::{FileId, Source},
|
||||||
|
text::{Font, FontBook},
|
||||||
|
Library, World,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOGO: &str = include_str!("../kit_logo.svg");
|
||||||
|
const LOGO_NAME: &str = "kit_logo.svg";
|
||||||
|
|
||||||
|
const TEMPLATE: &str = include_str!("../kit_timesheet.typ");
|
||||||
|
const TEMPLATE_NAME: &str = "kit_timesheet.typ";
|
||||||
|
|
||||||
|
const ALIAS: &str = "ts";
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// Data //
|
||||||
|
//////////
|
||||||
|
|
||||||
|
pub enum WorkingArea {
|
||||||
|
Großforschung,
|
||||||
|
Unibereich,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Note {
|
||||||
|
Urlaub,
|
||||||
|
Krankheit,
|
||||||
|
Feiertag,
|
||||||
|
Sonstiges,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Entry {
|
||||||
|
pub task: String,
|
||||||
|
pub day: u32,
|
||||||
|
pub start: String,
|
||||||
|
pub end: String,
|
||||||
|
pub rest: Option<String>,
|
||||||
|
pub note: Option<Note>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Timesheet {
|
||||||
|
pub name: String,
|
||||||
|
pub staff_id: String,
|
||||||
|
pub department: String,
|
||||||
|
pub working_area: WorkingArea,
|
||||||
|
pub monthly_hours: u32,
|
||||||
|
pub hourly_wage: String,
|
||||||
|
pub validate: bool,
|
||||||
|
pub year: u32,
|
||||||
|
pub month: u32,
|
||||||
|
pub entries: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// Convert to source //
|
||||||
|
///////////////////////
|
||||||
|
|
||||||
|
fn fmt_str(s: &str) -> String {
|
||||||
|
// https://github.com/typst/typst/blob/69dcc89d84176838c293b2d59747cd65e28843ad/crates/typst-syntax/src/ast.rs#L1041-L1082
|
||||||
|
format!("\"{}\"", s.replace('\\', "\\\\"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_int(n: u32) -> String {
|
||||||
|
n.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_bool(b: bool) -> String {
|
||||||
|
match b {
|
||||||
|
true => "true",
|
||||||
|
false => "false",
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_area(area: WorkingArea) -> String {
|
||||||
|
let name = match area {
|
||||||
|
WorkingArea::Großforschung => "Großforschung",
|
||||||
|
WorkingArea::Unibereich => "Unibereich",
|
||||||
|
};
|
||||||
|
format!("{ALIAS}.areas.{name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_note(note: Note) -> String {
|
||||||
|
let name = match note {
|
||||||
|
Note::Urlaub => "Urlaub",
|
||||||
|
Note::Krankheit => "Krankheit",
|
||||||
|
Note::Feiertag => "Feiertag",
|
||||||
|
Note::Sonstiges => "Sonstiges",
|
||||||
|
};
|
||||||
|
format!("{ALIAS}.notes.{name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_entry(entry: Entry) -> String {
|
||||||
|
let mut args = vec![
|
||||||
|
fmt_str(&entry.task),
|
||||||
|
fmt_int(entry.day),
|
||||||
|
fmt_str(&entry.start),
|
||||||
|
fmt_str(&entry.end),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(rest) = entry.rest {
|
||||||
|
args.push(format!("rest: {}", fmt_str(&rest)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(note) = entry.note {
|
||||||
|
args.push(format!("note: {}", fmt_note(note)));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("{ALIAS}.entry({})", args.join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_timesheet(ts: Timesheet) -> String {
|
||||||
|
let mut lines = vec![];
|
||||||
|
|
||||||
|
lines.push(format!("#import {} as {ALIAS}", fmt_str(TEMPLATE_NAME)));
|
||||||
|
lines.push(format!("#{ALIAS}.timesheet("));
|
||||||
|
lines.push(format!(" name: {},", fmt_str(&ts.name)));
|
||||||
|
lines.push(format!(" staff_id: {},", fmt_str(&ts.staff_id)));
|
||||||
|
lines.push(format!(" department: {},", fmt_str(&ts.department)));
|
||||||
|
lines.push(format!(" working_area: {},", fmt_area(ts.working_area)));
|
||||||
|
lines.push(format!(" monthly_hours: {},", fmt_int(ts.monthly_hours)));
|
||||||
|
lines.push(format!(" hourly_wage: {},", fmt_str(&ts.hourly_wage)));
|
||||||
|
lines.push(format!(" validate: {},", fmt_bool(ts.validate)));
|
||||||
|
lines.push(format!(" year: {},", fmt_int(ts.year)));
|
||||||
|
lines.push(format!(" month: {},", fmt_int(ts.month)));
|
||||||
|
for entry in ts.entries {
|
||||||
|
lines.push(format!(" {},", fmt_entry(entry)));
|
||||||
|
}
|
||||||
|
lines.push(")".to_string());
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////
|
||||||
|
// Evaluate source //
|
||||||
|
/////////////////////
|
||||||
|
|
||||||
|
struct DummyWorld {
|
||||||
|
library: Prehashed<Library>,
|
||||||
|
book: Prehashed<FontBook>,
|
||||||
|
main: Source,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DummyWorld {
|
||||||
|
fn new(main: String) -> Self {
|
||||||
|
Self {
|
||||||
|
library: Prehashed::new(Library::builder().build()),
|
||||||
|
book: Prehashed::new(FontBook::new()),
|
||||||
|
main: Source::detached(main),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World for DummyWorld {
|
||||||
|
fn library(&self) -> &Prehashed<Library> {
|
||||||
|
&self.library
|
||||||
|
}
|
||||||
|
|
||||||
|
fn book(&self) -> &Prehashed<FontBook> {
|
||||||
|
&self.book
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(&self) -> Source {
|
||||||
|
self.main.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||||
|
let path = id.vpath().as_rootless_path();
|
||||||
|
match path.to_string_lossy().as_ref() {
|
||||||
|
TEMPLATE_NAME => Ok(Source::new(id, TEMPLATE.to_string())),
|
||||||
|
_ => Err(FileError::NotFound(path.to_path_buf())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||||
|
let path = id.vpath().as_rootless_path();
|
||||||
|
match path.to_string_lossy().as_ref() {
|
||||||
|
LOGO_NAME => Ok(Bytes::from_static(LOGO.as_bytes())),
|
||||||
|
_ => Err(FileError::NotFound(path.to_path_buf())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn font(&self, _index: usize) -> Option<Font> {
|
||||||
|
panic!("this should never be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
|
||||||
|
panic!("this should never be called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_timesheet(ts: Timesheet) -> SourceResult<Document> {
|
||||||
|
let world = DummyWorld::new(fmt_timesheet(ts));
|
||||||
|
let mut tracer = Tracer::new();
|
||||||
|
typst::compile(&world, &mut tracer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(ts: Timesheet) -> Result<Vec<u8>, Vec<String>> {
|
||||||
|
let document = compile_timesheet(ts)
|
||||||
|
.map_err(|es| es.iter().map(|e| e.message.to_string()).collect::<Vec<_>>())?;
|
||||||
|
|
||||||
|
Ok(typst_pdf::pdf(&document, Smart::Auto, None))
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue