mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 22:26:49 -06:00
199 lines
7.8 KiB
Rust
199 lines
7.8 KiB
Rust
//! Text attributes. Every attribute exists in two forms generated by one macro:
|
|
//! a sparse `…Attrs` (all `Option`, used by styles and local overrides) and a complete `…Props`
|
|
//! (resolved values used by the composer). Resolution layers sparse attrs over defaults.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Preserve a present nullable override; a missing field still uses the sparse default.
|
|
fn deserialize_explicit_value<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
T: Deserialize<'de>,
|
|
{
|
|
T::deserialize(deserializer).map(Some)
|
|
}
|
|
|
|
macro_rules! attr_set {
|
|
(
|
|
$(#[$m:meta])*
|
|
$attrs:ident / $props:ident {
|
|
$( $(#[$fm:meta])* $field:ident : $ty:ty = $default:expr $(=> $deserialize:literal)? ),* $(,)?
|
|
}
|
|
) => {
|
|
$(#[$m])*
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default, rename_all = "camelCase")]
|
|
pub struct $attrs {
|
|
$( $(#[$fm])* $(#[serde(deserialize_with = $deserialize)])? #[serde(skip_serializing_if = "Option::is_none")] pub $field: Option<$ty>, )*
|
|
}
|
|
|
|
/// Fully resolved values.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default, rename_all = "camelCase")]
|
|
pub struct $props {
|
|
$( $(#[$fm])* pub $field: $ty, )*
|
|
}
|
|
|
|
impl Default for $props {
|
|
fn default() -> Self {
|
|
Self { $( $field: $default, )* }
|
|
}
|
|
}
|
|
|
|
impl $attrs {
|
|
/// Overlay `other` on top of `self` (fields set in `other` win).
|
|
pub fn merge(&mut self, other: &$attrs) {
|
|
$( if other.$field.is_some() { self.$field = other.$field.clone(); } )*
|
|
}
|
|
pub fn is_empty(&self) -> bool {
|
|
true $( && self.$field.is_none() )*
|
|
}
|
|
/// Number of fields set (for "+" override indicators).
|
|
pub fn count(&self) -> usize {
|
|
0 $( + self.$field.is_some() as usize )*
|
|
}
|
|
/// Remove local values already supplied by the applied style, keeping real differences.
|
|
pub fn retain_differences(&mut self, base: &$props) {
|
|
$( if self.$field.as_ref() == Some(&base.$field) { self.$field = None; } )*
|
|
}
|
|
/// Field names that are set (camelCase).
|
|
pub fn keys(&self) -> Vec<&'static str> {
|
|
let mut v = Vec::new();
|
|
$( if self.$field.is_some() { v.push(attr_key(stringify!($field))); } )*
|
|
v
|
|
}
|
|
/// Set one field from JSON by camelCase or snake_case name.
|
|
pub fn set_json(&mut self, key: &str, value: &serde_json::Value) -> Result<(), String> {
|
|
let k: String = key.chars().filter(|c| *c != '_').collect::<String>().to_ascii_lowercase();
|
|
$(
|
|
if k == stringify!($field).replace('_', "") {
|
|
if value.is_null() {
|
|
self.$field = None;
|
|
} else {
|
|
self.$field = Some(serde_json::from_value(value.clone()).map_err(|e| format!("{key}: {e}"))?);
|
|
}
|
|
return Ok(());
|
|
}
|
|
)*
|
|
Err(unknown_attribute(key, &[ $( stringify!($field), )* ]))
|
|
}
|
|
/// Fields where `self` differs from `base`'s resolved value (used for "clear overrides").
|
|
pub fn diff_from(&self, base: &$props) -> $attrs {
|
|
let mut out = $attrs::default();
|
|
$( if let Some(v) = &self.$field { if *v != base.$field { out.$field = Some(v.clone()); } } )*
|
|
out
|
|
}
|
|
/// Names of every attribute.
|
|
pub fn all_keys() -> &'static [&'static str] {
|
|
&[ $( stringify!($field), )* ]
|
|
}
|
|
}
|
|
|
|
impl $props {
|
|
pub fn apply(&mut self, a: &$attrs) {
|
|
$( if let Some(v) = &a.$field { self.$field = v.clone(); } )*
|
|
}
|
|
/// The attributes where `self` and `other` differ, with `other`'s values.
|
|
pub fn delta(&self, other: &$props) -> $attrs {
|
|
let mut out = $attrs::default();
|
|
$( if self.$field != other.$field { out.$field = Some(other.$field.clone()); } )*
|
|
out
|
|
}
|
|
/// Keep only values common to all (`None` where they differ) — for mixed selections.
|
|
pub fn common<'a>(items: impl IntoIterator<Item = &'a $props>) -> $attrs {
|
|
let mut it = items.into_iter();
|
|
let Some(first) = it.next() else { return $attrs::default() };
|
|
let mut out = $attrs { $( $field: Some(first.$field.clone()), )* };
|
|
for p in it {
|
|
$( if out.$field.as_ref().is_some_and(|v| *v != p.$field) { out.$field = None; } )*
|
|
}
|
|
out
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
mod enums;
|
|
mod language;
|
|
mod rules_lists;
|
|
mod tabs;
|
|
|
|
pub use enums::{
|
|
Align, Capitalization, ChangeMark, Composer, Digits, GrepStyle, GridAlign, Kerning, Leading, NestedLineStyle, NestedStyle, NestedUntil, Position,
|
|
SpanColumns, StartParagraph, TextDirection,
|
|
};
|
|
pub use language::{capitalizes, language_subtag, language_tag};
|
|
use rules_lists::list_level_index;
|
|
pub use rules_lists::{
|
|
BoxBottom, BoxTop, BoxWidth, LIST_LEVELS, ListAlign, ListCounter, ListType, NumberStyle, Rule, TEXT_COLOR, expand_list_levels, expand_list_text,
|
|
};
|
|
use tabs::sanitized_tabs;
|
|
pub use tabs::{
|
|
MAX_MEASURE, MAX_TAB_LEADER, MAX_TAB_POSITION, MAX_TAB_STOPS, MAX_TYPE_SIZE, MIN_TYPE_SIZE, SAME_TAB_POSITION, TabAlign, TabStop,
|
|
imported_measure, imported_type_size,
|
|
};
|
|
|
|
mod char_attrs;
|
|
mod para_attrs;
|
|
|
|
pub use char_attrs::{CharAttrs, CharProps};
|
|
pub use para_attrs::{ParaAttrs, ParaProps};
|
|
|
|
fn attr_key(snake: &'static str) -> &'static str {
|
|
snake
|
|
}
|
|
|
|
/// `snake_case` → `camelCase`, as commands name attributes.
|
|
fn camel(snake: &str) -> String {
|
|
let mut out = String::with_capacity(snake.len());
|
|
let mut up = false;
|
|
for c in snake.chars() {
|
|
if c == '_' {
|
|
up = true;
|
|
} else if up {
|
|
out.push(c.to_ascii_uppercase());
|
|
up = false;
|
|
} else {
|
|
out.push(c);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Edits (insert, delete, replace) that turn `a` into `b`, ignoring case and underscores.
|
|
fn edit_distance(a: &str, b: &str) -> usize {
|
|
let norm = |s: &str| s.chars().filter(|c| *c != '_').map(|c| c.to_ascii_lowercase()).collect::<Vec<_>>();
|
|
let (a, b) = (norm(a), norm(b));
|
|
let mut prev: Vec<usize> = (0..=b.len()).collect();
|
|
for (i, ca) in a.iter().enumerate() {
|
|
let mut row = vec![i + 1];
|
|
for (j, cb) in b.iter().enumerate() {
|
|
let d = (prev[j] + usize::from(ca != cb)).min(prev[j + 1] + 1).min(row[j] + 1);
|
|
row.push(d);
|
|
}
|
|
prev = row;
|
|
}
|
|
prev.last().copied().unwrap_or(0)
|
|
}
|
|
|
|
/// The error for an attribute `key` that isn't one of `fields` (snake_case): the nearest one
|
|
/// (a typo, or a name it starts or ends: `font` → `fontFamily`), or else all of them.
|
|
fn unknown_attribute(key: &str, fields: &[&str]) -> String {
|
|
let k = key.to_ascii_lowercase().replace('_', "");
|
|
let flat = |f: &str| f.replace('_', "");
|
|
let near = fields
|
|
.iter()
|
|
.map(|f| (edit_distance(key, f), *f))
|
|
.filter(|(d, _)| *d <= (k.len() / 3).max(1))
|
|
.min()
|
|
.map(|(_, f)| f)
|
|
.or_else(|| fields.iter().copied().find(|f| k.len() >= 3 && (flat(f).starts_with(&k) || flat(f).ends_with(&k))));
|
|
match near {
|
|
Some(f) => format!("unknown attribute `{key}`; did you mean `{}`?", camel(f)),
|
|
None => format!("unknown attribute `{key}`; the attributes are: {}", fields.iter().map(|f| camel(f)).collect::<Vec<_>>().join(", ")),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|