Compare commits
1 Commits
bfc908f075
...
1cc0efcd13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cc0efcd13 |
@ -46,14 +46,6 @@
|
|||||||
cp -r ./* $out/
|
cp -r ./* $out/
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
packages.themes = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "themes";
|
|
||||||
phases = [ "installPhase" ];
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out
|
|
||||||
cp ${helix}/runtime/themes/*.toml $out/
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
packages.karnaugh = pkgs.rustPlatform.buildRustPackage (final: {
|
packages.karnaugh = pkgs.rustPlatform.buildRustPackage (final: {
|
||||||
pname = "karnaugh";
|
pname = "karnaugh";
|
||||||
version = "0.1.5";
|
version = "0.1.5";
|
||||||
|
|||||||
@ -26,7 +26,6 @@ use tracing_subscriber::FmtSubscriber;
|
|||||||
use crate::typst::TypstContext;
|
use crate::typst::TypstContext;
|
||||||
|
|
||||||
mod syntax;
|
mod syntax;
|
||||||
mod themes;
|
|
||||||
mod typst;
|
mod typst;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -65,10 +64,6 @@ struct AppConfig {
|
|||||||
#[arg(short = 'g', long, value_name = "DIR", default_value = None)]
|
#[arg(short = 'g', long, value_name = "DIR", default_value = None)]
|
||||||
syntax_root: Option<PathBuf>,
|
syntax_root: Option<PathBuf>,
|
||||||
|
|
||||||
/// The directory where Helix theme `.toml` files are stored.
|
|
||||||
#[arg(short = 't', long, value_name = "DIR", default_value = None)]
|
|
||||||
themes_root: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Grammars to load, separated by commas.
|
/// Grammars to load, separated by commas.
|
||||||
/// Useful to avoid the memory required to load all languages.
|
/// Useful to avoid the memory required to load all languages.
|
||||||
#[arg(short = 'L', long = "lang", value_name = "NAME", value_delimiter = ',')]
|
#[arg(short = 'L', long = "lang", value_name = "NAME", value_delimiter = ',')]
|
||||||
@ -110,7 +105,6 @@ async fn main() {
|
|||||||
.init();
|
.init();
|
||||||
|
|
||||||
syntax::init(&state.config);
|
syntax::init(&state.config);
|
||||||
themes::init(&state.config);
|
|
||||||
|
|
||||||
// TODO: Replace with config option
|
// TODO: Replace with config option
|
||||||
let mut favicon = state.config.get_full_assets_path();
|
let mut favicon = state.config.get_full_assets_path();
|
||||||
|
|||||||
@ -20,7 +20,7 @@ use typst::{
|
|||||||
foundations::{Content, Func, NativeElement, NativeFunc, func},
|
foundations::{Content, Func, NativeElement, NativeFunc, func},
|
||||||
text::TextElem,
|
text::TextElem,
|
||||||
};
|
};
|
||||||
use typst_html::{HtmlAttr, HtmlElem, attr, tag};
|
use typst_html::{HtmlElem, attr, tag};
|
||||||
|
|
||||||
use crate::AppConfig;
|
use crate::AppConfig;
|
||||||
|
|
||||||
@ -194,15 +194,9 @@ fn highlight(
|
|||||||
let highlighted = highlight_code(&text, lang.as_deref()).unwrap_or(TextElem::packed(text));
|
let highlighted = highlight_code(&text, lang.as_deref()).unwrap_or(TextElem::packed(text));
|
||||||
|
|
||||||
if block {
|
if block {
|
||||||
HtmlElem::new(tag::pre)
|
HtmlElem::new(tag::pre).with_body(Some(highlighted)).pack()
|
||||||
.with_body(Some(highlighted))
|
|
||||||
.with_attr(HtmlAttr::constant("class"), "code-block")
|
|
||||||
.pack()
|
|
||||||
} else {
|
} else {
|
||||||
HtmlElem::new(tag::code)
|
highlighted
|
||||||
.with_body(Some(highlighted))
|
|
||||||
.with_attr(HtmlAttr::constant("class"), "code-inline")
|
|
||||||
.pack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
300
src/themes.rs
300
src/themes.rs
@ -1,300 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
error::Error,
|
|
||||||
fs,
|
|
||||||
path::Path,
|
|
||||||
sync::OnceLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use toml::{Table, Value};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
use typst::{
|
|
||||||
foundations::{Content, Func, NativeElement, NativeFunc, func},
|
|
||||||
text::TextElem,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::AppConfig;
|
|
||||||
|
|
||||||
static REGISTRY: OnceLock<ThemeRegistry> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ThemeRegistry {
|
|
||||||
themes: HashMap<String, Theme>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ThemeRegistry {
|
|
||||||
fn new(theme_root: &Path) -> Self {
|
|
||||||
let mut helix_themes = HashMap::new();
|
|
||||||
for entry in fs::read_dir(theme_root).into_iter().flatten().flatten() {
|
|
||||||
let name = entry
|
|
||||||
.path()
|
|
||||||
.file_prefix()
|
|
||||||
.unwrap()
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned();
|
|
||||||
if let Ok(text) = fs::read_to_string(entry.path())
|
|
||||||
&& let Ok(toml) = toml::from_str::<Table>(&text)
|
|
||||||
{
|
|
||||||
helix_themes.insert(name, toml);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve inheritance
|
|
||||||
while helix_themes
|
|
||||||
.values()
|
|
||||||
.any(|table| table.contains_key("inherits"))
|
|
||||||
{
|
|
||||||
let old_themes = helix_themes.clone();
|
|
||||||
old_themes
|
|
||||||
.keys()
|
|
||||||
.filter(|name| old_themes.get(*name).unwrap().contains_key("inherits"))
|
|
||||||
.for_each(|name| {
|
|
||||||
let parent_name = old_themes
|
|
||||||
.get(name)
|
|
||||||
.unwrap()
|
|
||||||
.get("inherits")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap();
|
|
||||||
let parent = old_themes.get(parent_name).unwrap();
|
|
||||||
if !parent.contains_key("inherits") {
|
|
||||||
let table = helix_themes.get_mut(name).unwrap();
|
|
||||||
parent.into_iter().for_each(|(k, v)| {
|
|
||||||
if k == "palette" {
|
|
||||||
let parent_palette = v.as_table();
|
|
||||||
let child_palette = table
|
|
||||||
.entry(k.clone())
|
|
||||||
.or_insert_with(|| Value::Table(Table::new()))
|
|
||||||
.as_table_mut();
|
|
||||||
if let (Some(parent_palette), Some(child_palette)) =
|
|
||||||
(parent_palette, child_palette)
|
|
||||||
{
|
|
||||||
for (pk, pv) in parent_palette {
|
|
||||||
child_palette
|
|
||||||
.entry(pk.clone())
|
|
||||||
.or_insert_with(|| pv.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !table.contains_key(k) {
|
|
||||||
table.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
table.remove("inherits");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let themes = helix_themes
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(name, table)| Theme::from_toml(table).map(|t| (name, t)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self { themes }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
struct Color(String);
|
|
||||||
|
|
||||||
impl Color {
|
|
||||||
fn from_str(color: &str) -> Option<Self> {
|
|
||||||
if color.starts_with('#') {
|
|
||||||
Some(Self(color.into()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_palette(color: &str, palette: &HashMap<String, Color>) -> Option<Self> {
|
|
||||||
if color.starts_with('#') {
|
|
||||||
Self::from_str(color)
|
|
||||||
} else {
|
|
||||||
palette.get(color).cloned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
struct Style {
|
|
||||||
fg: Option<Color>,
|
|
||||||
bold: bool,
|
|
||||||
italic: bool,
|
|
||||||
underlined: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Style {
|
|
||||||
fn from_toml(style: Value, palette: &HashMap<String, Color>) -> Option<Self> {
|
|
||||||
let resolve = |s: &str| Color::from_palette(s, palette);
|
|
||||||
match style {
|
|
||||||
Value::String(s) => Some(Self {
|
|
||||||
fg: resolve(&s),
|
|
||||||
bold: false,
|
|
||||||
italic: false,
|
|
||||||
underlined: false,
|
|
||||||
}),
|
|
||||||
Value::Table(table) => {
|
|
||||||
let fg = table.get("fg").and_then(Value::as_str).and_then(resolve);
|
|
||||||
let modifiers: HashSet<&str> = table
|
|
||||||
.get("modifiers")
|
|
||||||
.and_then(Value::as_array)
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.flat_map(Value::as_str)
|
|
||||||
.collect();
|
|
||||||
Some(Self {
|
|
||||||
fg,
|
|
||||||
bold: modifiers.contains("bold"),
|
|
||||||
italic: modifiers.contains("italic"),
|
|
||||||
underlined: modifiers.contains("underlined"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Theme {
|
|
||||||
foreground: Option<Color>,
|
|
||||||
background: Option<Color>,
|
|
||||||
styles: HashMap<String, Style>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static IGNORED_SCOPES: &[&str] = &["warning", "error", "info", "hint"];
|
|
||||||
|
|
||||||
impl Theme {
|
|
||||||
fn from_toml(theme: Table) -> Option<Self> {
|
|
||||||
let palette = theme
|
|
||||||
.get("palette")
|
|
||||||
.and_then(Value::as_table)
|
|
||||||
.cloned()
|
|
||||||
.map(|table| {
|
|
||||||
table
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(k, v)| v.as_str().and_then(Color::from_str).map(|v| (k, v)))
|
|
||||||
.collect::<HashMap<String, Color>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let resolve = |c: &str| Color::from_palette(c, &palette);
|
|
||||||
|
|
||||||
let ui_background = theme.get("ui.background").and_then(Value::as_table)?;
|
|
||||||
let foreground = ui_background
|
|
||||||
.get("fg")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.and_then(resolve);
|
|
||||||
let background = ui_background
|
|
||||||
.get("bg")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.and_then(resolve);
|
|
||||||
|
|
||||||
let styles = theme
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(k, _)| {
|
|
||||||
!k.starts_with("ui.")
|
|
||||||
&& !k.starts_with("diagnostic.")
|
|
||||||
&& !IGNORED_SCOPES.contains(&k.as_str())
|
|
||||||
})
|
|
||||||
.flat_map(|(k, v)| Style::from_toml(v, &palette).map(|v| (k, v)))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(Self {
|
|
||||||
foreground,
|
|
||||||
background,
|
|
||||||
styles,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(config: &AppConfig) {
|
|
||||||
match &config.themes_root {
|
|
||||||
Some(themes_root) => {
|
|
||||||
info!("Loading themes from {}", themes_root.display());
|
|
||||||
REGISTRY.get_or_init(|| ThemeRegistry::new(themes_root));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
warn!("No themes root has been configured, theme-based colouring will not be possible.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn theme_css_func() -> Func {
|
|
||||||
theme_css::func()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scope_to_selector(scope: &str) -> String {
|
|
||||||
format!(".hl-{}", scope.replace('.', "-"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_declarations(style: &Style) -> String {
|
|
||||||
let mut parts = Vec::new();
|
|
||||||
if let Some(Color(hex)) = &style.fg {
|
|
||||||
parts.push(format!("color:{hex}"));
|
|
||||||
}
|
|
||||||
parts.push(format!(
|
|
||||||
"font-weight:{}",
|
|
||||||
if style.bold { "bold" } else { "normal" }
|
|
||||||
));
|
|
||||||
parts.push(format!(
|
|
||||||
"font-style:{}",
|
|
||||||
if style.italic { "italic" } else { "normal" }
|
|
||||||
));
|
|
||||||
parts.push(format!(
|
|
||||||
"text-decoration:{}",
|
|
||||||
if style.underlined {
|
|
||||||
"underline"
|
|
||||||
} else {
|
|
||||||
"none"
|
|
||||||
}
|
|
||||||
));
|
|
||||||
parts.join(";")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_css(scopes: &[&str], theme: &Theme) -> String {
|
|
||||||
let mut groups: HashMap<&Style, Vec<&str>> = HashMap::new();
|
|
||||||
for &scope in scopes {
|
|
||||||
if let Some(style) = theme.styles.get(scope) {
|
|
||||||
groups.entry(style).or_default().push(scope);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groups
|
|
||||||
.iter()
|
|
||||||
.map(|(style, scopes)| {
|
|
||||||
let selectors = scopes
|
|
||||||
.iter()
|
|
||||||
.map(|s| scope_to_selector(s))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(",");
|
|
||||||
format!("{}{{{}}}", selectors, style_declarations(style))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[func]
|
|
||||||
fn theme_css(light: String, dark: String) -> String {
|
|
||||||
let Some(registry) = REGISTRY.get() else {
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
let Some(light_theme) = registry.themes.get(&light) else {
|
|
||||||
warn!("Unknown light theme: {light}");
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
let Some(dark_theme) = registry.themes.get(&dark) else {
|
|
||||||
warn!("Unknown dark theme: {dark}");
|
|
||||||
return String::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut shared: Vec<&str> = light_theme
|
|
||||||
.styles
|
|
||||||
.keys()
|
|
||||||
.filter(|k| dark_theme.styles.contains_key(*k))
|
|
||||||
.map(String::as_str)
|
|
||||||
.collect();
|
|
||||||
shared.sort();
|
|
||||||
|
|
||||||
let light_css = generate_css(&shared, light_theme);
|
|
||||||
let dark_css = generate_css(&shared, dark_theme);
|
|
||||||
|
|
||||||
format!("{light_css}\n@media (prefers-color-scheme:dark){{{dark_css}}}")
|
|
||||||
}
|
|
||||||
@ -30,12 +30,6 @@ impl TypstContext {
|
|||||||
Value::Func(crate::syntax::highlight_func()),
|
Value::Func(crate::syntax::highlight_func()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if config.themes_root.is_some() {
|
|
||||||
inputs.insert(
|
|
||||||
"theme_css".into(),
|
|
||||||
Value::Func(crate::themes::theme_css_func()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let library = Library::builder()
|
let library = Library::builder()
|
||||||
.with_inputs(inputs)
|
.with_inputs(inputs)
|
||||||
.with_features([typst::Feature::Html].into_iter().collect())
|
.with_features([typst::Feature::Html].into_iter().collect())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user