use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Span {
Column,
Wide,
}
impl Span {
pub fn latex_env(&self) -> &'static str {
match self {
Span::Column => "figure",
Span::Wide => "figure*",
}
}
pub fn latex_width(&self) -> &'static str {
match self {
Span::Column => "\\columnwidth",
Span::Wide => "\\textwidth",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
#[serde(default = "default_class")]
pub class: String,
#[serde(default)]
pub class_options: Vec<String>,
#[serde(default = "default_columns")]
pub columns: u8,
#[serde(default = "default_margins")]
pub margins: String,
}
fn default_class() -> String {
"IEEEtran".to_string()
}
fn default_columns() -> u8 {
2
}
fn default_margins() -> String {
"0.75in".to_string()
}
impl Default for Document {
fn default() -> Self {
Self {
class: default_class(),
class_options: vec!["conference".to_string()],
columns: default_columns(),
margins: default_margins(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Pos {
Top,
Bottom,
Both,
Page,
Here,
Force,
Barrier,
}
impl Pos {
pub fn parse(s: &str) -> Result<Pos, String> {
match s {
"top" => Ok(Pos::Top),
"bottom" => Ok(Pos::Bottom),
"both" => Ok(Pos::Both),
"page" => Ok(Pos::Page),
"here" => Ok(Pos::Here),
"force" => Ok(Pos::Force),
"barrier" => Ok(Pos::Barrier),
_ => Err(format!("pos must be top|bottom|both|page|here|force|barrier, got {s:?}")),
}
}
pub fn latex_spec(self) -> &'static str {
match self {
Pos::Top => "[t]",
Pos::Bottom => "[b]",
Pos::Both => "[!tb]",
Pos::Page => "[p]",
Pos::Here => "[h]",
Pos::Force => "[H]",
Pos::Barrier => "[t]",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CaptionPos {
Top,
Bottom,
}
impl CaptionPos {
pub fn parse(s: &str) -> Result<CaptionPos, String> {
match s {
"top" => Ok(CaptionPos::Top),
"bottom" => Ok(CaptionPos::Bottom),
_ => Err(format!("captionpos must be top|bottom, got {s:?}")),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FloatNeeds {
pub float_h: bool,
pub barrier: bool,
pub dblfloat: bool,
pub graphicx: bool,
}
impl FloatNeeds {
pub fn none() -> Self {
Self::default()
}
pub fn add(&mut self, o: FloatNeeds) {
self.float_h |= o.float_h;
self.barrier |= o.barrier;
self.dblfloat |= o.dblfloat;
self.graphicx |= o.graphicx;
}
pub fn packages(&self) -> Vec<String> {
let mut out = Vec::new();
if self.float_h {
out.push("\\usepackage{float}".to_string());
}
if self.barrier {
out.push("\\usepackage{placeins}".to_string());
}
if self.dblfloat {
out.push("\\usepackage{dblfloatfix}".to_string());
}
out
}
pub fn required_names(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.float_h {
out.push("float");
}
if self.barrier {
out.push("placeins");
}
if self.dblfloat {
out.push("dblfloatfix");
}
if self.graphicx {
out.push("graphicx");
}
out
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Spans {
#[serde(default = "span_wide")]
pub diagram: Span,
#[serde(default = "span_column")]
pub plot: Span,
#[serde(default = "span_column")]
pub table: Span,
#[serde(default)]
pub picture: Option<Span>,
#[serde(default = "span_column")]
pub default: Span,
}
fn span_wide() -> Span {
Span::Wide
}
fn span_column() -> Span {
Span::Column
}
impl Default for Spans {
fn default() -> Self {
Self {
diagram: Span::Wide,
plot: Span::Column,
table: Span::Column,
picture: None,
default: Span::Column,
}
}
}
impl Spans {
pub fn for_kind(&self, kind: &str) -> Span {
match kind {
"diagram" => self.diagram,
"plot" => self.plot,
"table" => self.table,
"picture" => self.picture.unwrap_or(self.diagram),
_ => self.default,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Template {
#[serde(default)]
pub preamble_file: String,
#[serde(default)]
pub preamble_append: Vec<String>,
#[serde(default)]
pub abstract_before_maketitle: bool,
#[serde(default)]
pub skeleton: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Floats {
#[serde(default = "pos_top")]
pub pos_default: Pos,
#[serde(default = "frac_top")]
pub topfraction: f64,
#[serde(default = "frac_bottom")]
pub bottomfraction: f64,
#[serde(default = "frac_text")]
pub textfraction: f64,
#[serde(default = "frac_floatpage")]
pub floatpagefraction: f64,
#[serde(default = "num_top")]
pub topnumber: i64,
#[serde(default = "num_bottom")]
pub bottomnumber: i64,
#[serde(default)]
pub barrier_sections: bool,
}
fn pos_top() -> Pos {
Pos::Top
}
fn frac_top() -> f64 {
0.7
}
fn frac_bottom() -> f64 {
0.3
}
fn frac_text() -> f64 {
0.2
}
fn frac_floatpage() -> f64 {
0.5
}
fn num_top() -> i64 {
2
}
fn num_bottom() -> i64 {
1
}
impl Default for Floats {
fn default() -> Self {
Self {
pos_default: Pos::Top,
topfraction: 0.7,
bottomfraction: 0.3,
textfraction: 0.2,
floatpagefraction: 0.5,
topnumber: 2,
bottomnumber: 1,
barrier_sections: false,
}
}
}
impl Floats {
pub fn setup_lines(&self) -> Vec<String> {
let mut lines = Vec::new();
let mut frac = |name: &str, val: f64, dflt: f64| {
if (val - dflt).abs() > 1e-9 {
lines.push(format!("\\renewcommand{{\\{name}}}{{{val}}}"));
}
};
frac("topfraction", self.topfraction, 0.7);
frac("bottomfraction", self.bottomfraction, 0.3);
frac("textfraction", self.textfraction, 0.2);
frac("floatpagefraction", self.floatpagefraction, 0.5);
if self.topnumber != 2 {
lines.push(format!("\\setcounter{{topnumber}}{{{}}}", self.topnumber));
}
if self.bottomnumber != 1 {
lines.push(format!("\\setcounter{{bottomnumber}}{{{}}}", self.bottomnumber));
}
lines
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub document: Document,
#[serde(default)]
pub spans: Spans,
#[serde(default)]
pub floats: Floats,
#[serde(default)]
pub template: Template,
}
impl Config {
pub fn from_toml(src: &str) -> Result<Self, String> {
toml::from_str(src).map_err(|e| format!("ikat.toml: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_favour_readable_diagrams() {
let cfg = Config::from_toml("").unwrap();
assert_eq!(cfg.document.class, "IEEEtran");
assert_eq!(cfg.document.columns, 2);
assert_eq!(cfg.spans.diagram, Span::Wide);
assert_eq!(cfg.spans.plot, Span::Column);
}
#[test]
fn spans_parse_and_envs_map() {
let cfg = Config::from_toml(
"[spans]\ndiagram = \"column\"\nplot = \"wide\"\n",
)
.unwrap();
assert_eq!(cfg.spans.for_kind("diagram").latex_env(), "figure");
assert_eq!(cfg.spans.for_kind("plot").latex_env(), "figure*");
assert_eq!(cfg.spans.for_kind("table").latex_env(), "figure");
assert_eq!(cfg.spans.for_kind("diagram").latex_width(), "\\columnwidth");
}
#[test]
fn template_table_parses() {
let cfg = Config::from_toml(
"[template]\npreamble_file = \"head.tex\"\npreamble_append = [\"\\\\usepackage{natbib}\"]\n",
)
.unwrap();
assert_eq!(cfg.template.preamble_file, "head.tex");
assert_eq!(cfg.template.preamble_append, vec!["\\usepackage{natbib}"]);
assert!(Config::from_toml("").unwrap().template.preamble_append.is_empty());
}
#[test]
fn template_flag_parses() {
let cfg = Config::from_toml("[template]\nabstract_before_maketitle = true\n").unwrap();
assert!(cfg.template.abstract_before_maketitle);
assert!(!Config::from_toml("").unwrap().template.abstract_before_maketitle);
}
#[test]
fn picture_falls_back_to_diagram() {
let cfg = Config::from_toml("").unwrap();
assert_eq!(cfg.spans.for_kind("picture"), Span::Wide);
let cfg = Config::from_toml("[spans]\npicture = \"column\"\n").unwrap();
assert_eq!(cfg.spans.for_kind("picture"), Span::Column);
assert_eq!(cfg.spans.for_kind("diagram"), Span::Wide);
}
#[test]
fn pos_parses_strictly() {
assert_eq!(Pos::parse("both"), Ok(Pos::Both));
assert_eq!(Pos::Both.latex_spec(), "[!tb]");
assert_eq!(Pos::Barrier.latex_spec(), "[t]");
assert!(Pos::parse("center").is_err());
}
#[test]
fn float_setup_lines_only_nondefaults() {
assert!(Config::from_toml("").unwrap().floats.setup_lines().is_empty());
let cfg = Config::from_toml("[floats]\ntopfraction = 0.9\nbottomnumber = 2\n").unwrap();
let lines = cfg.floats.setup_lines();
assert!(lines.contains(&"\\renewcommand{\\topfraction}{0.9}".to_string()));
assert!(lines.contains(&"\\setcounter{bottomnumber}{2}".to_string()));
assert_eq!(lines.len(), 2);
assert!(cfg.floats.barrier_sections == false);
}
#[test]
fn bad_toml_is_an_error_string() {
assert!(Config::from_toml("[spans\ndiagram = 42\n").is_err());
}
}