use crate::error::FontmergeError;
use babelfont::SmolStr;
use clap::Parser;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodepointArgs(pub Vec<char>);
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
pub font_1: String,
pub font_2: String,
#[arg(short, long)]
pub output: String,
#[arg(long)]
pub fea_include_dir: Option<String>,
#[command(flatten)]
pub verbosity: clap_verbosity_flag::Verbosity,
#[command(flatten)]
pub glyph_selection: GlyphSelection,
#[clap(
short,
long,
default_value_t,
value_enum,
help_heading = "Existing glyph handling"
)]
pub existing_handling: ExistingGlyphHandling,
#[clap(
short,
long,
default_value_t,
value_enum,
help_heading = "Layout handling"
)]
pub layout_handling: LayoutHandling,
#[clap(
short,
long,
default_value_t,
value_enum,
help_heading = "Layout handling"
)]
pub duplicate_lookups: DuplicateLookupHandling,
#[command(flatten)]
pub fixups: Fixups,
}
#[derive(clap::Args, Debug)]
#[command(next_help_heading = "Glyph selection")]
pub struct GlyphSelection {
#[arg(short, long, value_delimiter = ' ')]
pub glyphs: Vec<SmolStr>,
#[arg(short = 'G', long)]
pub glyphs_file: Option<String>,
#[arg(short = 'u', long, value_parser = crate::args::parse_codepoints)]
pub codepoints: Option<CodepointArgs>,
#[arg(short = 'U', long)]
pub codepoints_file: Option<String>,
#[arg(short = 'x', long, value_delimiter = ' ')]
pub exclude_glyphs: Vec<SmolStr>,
#[arg(short = 'X', long)]
pub exclude_glyphs_file: Option<String>,
}
#[derive(clap::ValueEnum, Debug, Clone, Default, Copy, PartialEq, Eq)]
pub enum ExistingGlyphHandling {
#[default]
Skip,
Replace,
}
#[derive(clap::ValueEnum, Clone, Default, Debug, Copy, PartialEq, Eq)]
pub enum LayoutHandling {
#[default]
Subset,
Closure,
Ignore,
}
#[derive(clap::ValueEnum, Debug, Clone, Default, Copy, PartialEq, Eq)]
pub enum DuplicateLookupHandling {
#[default]
First,
Both,
}
impl std::fmt::Display for DuplicateLookupHandling {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DuplicateLookupHandling::First => write!(f, "first"),
DuplicateLookupHandling::Both => write!(f, "both"),
}
}
}
#[derive(clap::Args, Debug)]
#[command(next_help_heading = "Specialist fixups")]
pub struct Fixups {
#[arg(long, default_value = "true", action = clap::ArgAction::Set)]
pub dotted_circle: bool,
#[arg(long, default_value = "false", action = clap::ArgAction::Set)]
pub skip_avar_masters: bool,
}
fn parse_codepoint(input: &str) -> Result<char, String> {
if input.len() == 1 {
#[allow(clippy::unwrap_used)] return Ok(input.chars().next().unwrap());
}
let input = input.to_lowercase();
let input = input.trim_start_matches("u+").trim_start_matches("0x");
let cp = u32::from_str_radix(input, 16).map_err(|_| format!("Invalid codepoint: {}", input))?;
char::from_u32(cp).ok_or_else(|| format!("Invalid Unicode codepoint: U+{:04X}", cp))
}
pub fn parse_codepoints(input: &str) -> Result<CodepointArgs, String> {
let mut result = Vec::new();
for item in input.split(",") {
if item.contains('-') {
let parts: Vec<&str> = item.split('-').collect();
if parts.len() != 2 {
return Err(format!("Invalid codepoint range: {}", item));
}
#[allow(clippy::indexing_slicing)] let start = parse_codepoint(parts[0])?;
#[allow(clippy::indexing_slicing)] let end = parse_codepoint(parts[1])?;
let start_u32 = start as u32;
let end_u32 = end as u32;
if start_u32 > end_u32 {
return Err(format!(
"Invalid codepoint range: {} > {}",
start_u32, end_u32
));
}
for cp in start_u32..=end_u32 {
if let Some(c) = char::from_u32(cp) {
result.push(c);
}
}
} else {
result.push(parse_codepoint(item)?);
}
}
Ok(CodepointArgs(result))
}
impl GlyphSelection {
pub fn get_include_glyphs(&self) -> Result<Vec<SmolStr>, FontmergeError> {
let mut glyphs = self.glyphs.clone();
if let Some(ref filename) = self.glyphs_file {
let content = std::fs::read_to_string(filename)?;
for line in content.lines() {
let glyph_name = line.trim();
if !glyph_name.is_empty() && !glyph_name.starts_with('#') {
glyphs.push(glyph_name.into());
}
}
}
Ok(glyphs)
}
pub fn get_exclude_glyphs(&self) -> Result<Vec<SmolStr>, FontmergeError> {
let mut glyphs = self.exclude_glyphs.clone();
if let Some(ref filename) = self.exclude_glyphs_file {
let content = std::fs::read_to_string(filename)?;
for line in content.lines() {
let glyph_name = line.trim();
if !glyph_name.is_empty() && !glyph_name.starts_with('#') {
glyphs.push(glyph_name.into());
}
}
}
Ok(glyphs)
}
pub fn get_codepoints(&self) -> Result<Vec<char>, FontmergeError> {
let mut codepoints = self
.codepoints
.clone()
.unwrap_or_else(|| CodepointArgs(Vec::new()))
.0;
if let Some(ref filename) = self.codepoints_file {
let content = std::fs::read_to_string(filename)?;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let cp_args = parse_codepoints(line).map_err(FontmergeError::Parse)?;
codepoints.extend(cp_args.0);
}
}
Ok(codepoints)
}
}