use super::*;
use std::fmt::{Debug, Display};
pub mod ast;
const CHARACTER: &str = "CHARACTER";
const CHARACTER_WIDTH: &str = "CHARWD";
const CHARACTER_HEIGHT: &str = "CHARHT";
const CHARACTER_DEPTH: &str = "CHARDP";
const CHARACTER_ITALIC: &str = "CHARIT";
const CHECKSUM: &str = "CHECKSUM";
const CODING_SCHEME: &str = "CODINGSCHEME";
const COMMENT: &str = "COMMENT";
const DESIGN_SIZE: &str = "DESIGNSIZE";
const FACE: &str = "FACE";
const FAMILY: &str = "FAMILY";
const HEADER: &str = "HEADER";
const SEVEN_BIT_SAFE: &str = "SEVENBITSAFEFLAG";
const FONT_DIMENSIONS: &str = "FONTDIMEN";
const SLANT: &str = "SLANT";
const SPACE: &str = "SPACE";
const STRETCH: &str = "STRETCH";
const SHRINK: &str = "SHRINK";
const XHEIGHT: &str = "XHEIGHT";
const QUAD: &str = "QUAD";
const EXTRA_SPACE: &str = "EXTRASPACE";
const NUM_1: &str = "NUM1";
const NUM_2: &str = "NUM2";
const NUM_3: &str = "NUM3";
const DENOM_1: &str = "DENOM1";
const DENOM_2: &str = "DENOM2";
const SUP_1: &str = "SUP1";
const SUP_2: &str = "SUP2";
const SUP_3: &str = "SUP3";
const SUB_1: &str = "SUB1";
const SUB_2: &str = "SUB2";
const SUP_DROP: &str = "SUPDROP";
const SUB_DROP: &str = "SUBDROP";
const DELIM_1: &str = "DELIM1";
const DELIM_2: &str = "DELIM2";
const AXIS_HEIGHT: &str = "AXISHEIGHT";
const DEFAULT_THICKNESS: &str = "DEFAULTRULETHICKNESS";
const BIG_OP_SPACING_1: &str = "BIGOPSPACING1";
const BIG_OP_SPACING_2: &str = "BIGOPSPACING2";
const BIG_OP_SPACING_3: &str = "BIGOPSPACING3";
const BIG_OP_SPACING_4: &str = "BIGOPSPACING4";
const BIG_OP_SPACING_5: &str = "BIGOPSPACING5";
const PARAMETER: &str = "PARAMETER";
pub fn format(file_name: &str, input: &str, style: Style) -> String {
let tree = ast::parse(file_name, input).unwrap();
ast::write(&tree, style)
}
pub fn parse<'a>(file_name: &'a str, input: &'a str) -> Result<File, ParseError<Word<'a>>> {
let tree = ast::parse(file_name, input)?;
let mut file: File = Default::default();
file.header.design_size = FixWord((1 << 20) * 10);
for node in tree.nodes() {
match node.key() {
CHECKSUM => {
file.header.checksum = node.try_into()?;
}
CODING_SCHEME => {
file.header.character_coding_scheme = Some(node.try_into()?);
}
COMMENT => {}
DESIGN_SIZE => {
file.header.design_size = node.try_into()?;
}
FACE => {
file.header.face = Some(Face(node.try_into()?));
}
FAMILY => {
file.header.font_family = Some(node.try_into()?);
}
FONT_DIMENSIONS => {
parse_font_dimensions(node, &mut file.params)?;
}
SEVEN_BIT_SAFE => {
file.header.seven_bit_safe = Some(node.try_into()?);
}
_ => {
return Err(ParseError::InvalidKey(node.key));
}
}
}
let output = write(&file, Style::default());
println!["{output}"];
Ok(file)
}
fn parse_font_dimensions<'a>(
node: &ast::Node<Word<'a>>,
params: &mut Params,
) -> Result<(), ParseError<Word<'a>>> {
for node in node.value.1.nodes() {
let i: usize = match node.key() {
SLANT => 0,
SPACE => 1,
STRETCH => 2,
SHRINK => 3,
XHEIGHT => 4,
QUAD => 5,
EXTRA_SPACE => 6,
COMMENT => continue,
PARAMETER => {
let (i, fix_word) = node.try_into()?;
params.set(i as usize, fix_word);
continue;
}
_ => {
return Err(ParseError::InvalidKey(node.key));
}
};
params.set(i, node.try_into()?);
}
Ok(())
}
#[derive(Debug)]
pub struct Style {
pub indent: usize,
pub closing_brace_style: ClosingBraceStyle,
}
impl Default for Style {
fn default() -> Self {
Self {
indent: 3,
closing_brace_style: Default::default(),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ClosingBraceStyle {
SameLine,
MatchingOpening,
ExtraIndent,
}
impl Default for ClosingBraceStyle {
fn default() -> Self {
ClosingBraceStyle::ExtraIndent
}
}
#[derive(Copy, Clone, Debug)]
pub struct Word<'a> {
pub file_name: &'a str,
pub file: &'a str,
pub start: usize,
pub end: usize,
}
impl<'a> AsRef<str> for Word<'a> {
fn as_ref(&self) -> &str {
&self.file[self.start..self.end]
}
}
impl<'a> Display for Word<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (line_number, line, word_start) = {
let mut line_number = 1;
let mut line_start = 0;
for (position, char) in self.file[..self.start].char_indices() {
if char == '\n' {
line_number += 1;
line_start = position + 1;
}
}
let tail = &self.file[line_start..];
let line = match tail.find('\n') {
None => tail,
Some(end) => &tail[..end],
};
(line_number, line, self.start - line_start)
};
let line_number_str = format!["{line_number}"];
let padding = " ".repeat(line_number_str.len());
writeln!(
f,
"{padding}--> {}:{line_number}:{}",
self.file_name,
word_start + 1,
)?;
writeln!(f, "{padding} |")?;
writeln!(f, "{line_number} | {line}")?;
writeln!(
f,
"{padding} | {}{}",
" ".repeat(word_start),
"^".repeat(self.end - self.start),
)?;
Ok(())
}
}
#[derive(Debug)]
pub enum ParseError<T> {
Parse(ast::ParseError<T>),
ConversionError(ast::ConversionError<T>),
InvalidKey(T),
}
impl<T: Display + AsRef<str>> Display for ParseError<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::Parse(_err) => todo!(),
ParseError::ConversionError(_) => todo!(),
ParseError::InvalidKey(key) => write!(f, "invalid key '{}'\n{key}", key.as_ref()),
}
}
}
impl<T> From<ast::ParseError<T>> for ParseError<T> {
fn from(err: ast::ParseError<T>) -> Self {
ParseError::Parse(err)
}
}
impl<T> From<ast::ConversionError<T>> for ParseError<T> {
fn from(err: ast::ConversionError<T>) -> Self {
ParseError::ConversionError(err)
}
}
pub fn to_ast(file: &File) -> ast::Tree<String> {
let mut builder = ast::Tree::builder();
if let Some(font_family) = &file.header.font_family {
builder.add(FAMILY).with_str(font_family);
}
if let Some(face) = file.header.face {
match face.try_into() {
Ok::<String, _>(s) => {
builder.add(FACE).with_str("F").with_string(s);
}
Err(_) => {
builder.add(FACE).with_octal(face.0 as u32);
}
}
}
for word in &file.header.additional_data {
builder.add(HEADER).with_octal(*word);
}
if let Some(coding_scheme) = &file.header.character_coding_scheme {
builder.add(CODING_SCHEME).with_str(coding_scheme);
}
builder
.add(DESIGN_SIZE)
.with_fix_word(file.header.design_size);
builder.add(COMMENT).with_str("DESIGNSIZE IS IN POINTS");
builder
.add(COMMENT)
.with_str("OTHER SIZES ARE MULTIPLES OF DESIGNSIZE");
builder.add(CHECKSUM).with_octal(file.header.checksum);
if file.header.seven_bit_safe == Some(true) {
builder.add(SEVEN_BIT_SAFE).with_str("TRUE");
}
{
let mut params_tree = ast::Tree::builder();
for (key, parameter_index, value) in convert_params(&file.params) {
let elem = params_tree.add(key);
if let Some(parameter_index) = parameter_index {
elem.with_integer(parameter_index);
}
elem .with_fix_word(value);
}
builder.add(FONT_DIMENSIONS).with_tree(params_tree.into());
}
for char_info in &file.char_infos {
let mut char_tree = ast::Tree::builder();
if char_info.width != FixWord::ZERO {
char_tree
.add(CHARACTER_WIDTH)
.with_fix_word(char_info.width);
}
if char_info.height != FixWord::ZERO {
char_tree
.add(CHARACTER_HEIGHT)
.with_fix_word(char_info.height);
}
if char_info.depth != FixWord::ZERO {
char_tree
.add(CHARACTER_DEPTH)
.with_fix_word(char_info.depth);
}
if char_info.italic_correction != FixWord::ZERO {
char_tree
.add(CHARACTER_ITALIC)
.with_fix_word(char_info.italic_correction);
}
builder
.add(CHARACTER)
.with_character(char_info.id)
.with_tree(char_tree.into());
}
builder.into()
}
pub fn write(file: &File, style: Style) -> String {
let tree: ast::Tree<String> = to_ast(file);
ast::write(&tree, style)
}
fn convert_params(params: &Params) -> Vec<(&'static str, Option<u8>, FixWord)> {
let v1 = vec![
(SLANT, params.slant),
(SPACE, params.space),
(STRETCH, params.space_stretch),
(SHRINK, params.space_shrink),
(XHEIGHT, params.x_height),
(QUAD, params.quad),
(EXTRA_SPACE, params.extra_space),
];
let v2 = match params.math_params {
MathParams::None => vec![],
MathParams::Symbols {
num_1,
num_2,
num_3,
denom_1,
denom_2,
sup_1,
sup_2,
sup_3,
sub_1,
sub_2,
sup_drop,
sub_drop,
delim_1,
delim_2,
axis_height,
} => vec![
(NUM_1, num_1),
(NUM_2, num_2),
(NUM_3, num_3),
(DENOM_1, denom_1),
(DENOM_2, denom_2),
(SUP_1, sup_1),
(SUP_2, sup_2),
(SUP_3, sup_3),
(SUB_1, sub_1),
(SUB_2, sub_2),
(SUP_DROP, sup_drop),
(SUB_DROP, sub_drop),
(DELIM_1, delim_1),
(DELIM_2, delim_2),
(AXIS_HEIGHT, axis_height),
],
MathParams::Extension {
default_thickness,
big_op_spacing,
} => vec![
(DEFAULT_THICKNESS, default_thickness),
(BIG_OP_SPACING_1, big_op_spacing[0]),
(BIG_OP_SPACING_2, big_op_spacing[1]),
(BIG_OP_SPACING_3, big_op_spacing[2]),
(BIG_OP_SPACING_4, big_op_spacing[3]),
(BIG_OP_SPACING_5, big_op_spacing[4]),
],
};
let mut v = vec![];
for (key, value) in v1 {
v.push((key, None, value));
}
for (key, value) in v2 {
v.push((key, None, value));
}
for additional_param in ¶ms.additional_params {
let index: u8 = match (v.len() + 1).try_into() {
Ok(u) => u,
Err(_) => u8::MAX,
};
v.push((PARAMETER, Some(index), *additional_param))
}
v
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! fix_word_round_trip_tests {
($($name:ident: $value:expr,)*) => {
$(
#[test]
fn $name() {
let value: i32 = $value;
let start = FixWord(value);
let output = format!("{}", start);
let output_ref: &str = &output;
let finish: FixWord = output_ref.try_into().unwrap();
assert_eq!(start, finish);
let start = FixWord(value.wrapping_mul(-1));
let output = format!("{}", start);
let output_ref: &str = &output;
let finish: FixWord = output_ref.try_into().unwrap();
assert_eq!(start, finish);
}
)*
}
}
fix_word_round_trip_tests!(
zero: 0,
one: 1,
two: 2,
three: 3,
four: 4,
five: 5,
ten: 10,
seventy: 70,
one40: 140,
pow10: 1 << 10,
pow15: 1 << 15,
pow18: 1 << 18,
pow19: 1 << 19,
pow20: 1 << 20,
pow20_times_10: 10 * 1 << 20,
pow21: 1 << 21,
pow21_plus_pow15: 1 << 21 + 1 << 15,
big: 15 * (1 << 20) + 1 << 15,
min: i32::MIN,
max: i32::MAX,
);
macro_rules! pl_round_trip_test {
($name: ident, $input: expr, $output: expr,) => {
#[test]
fn $name() {
let data = parse("", &$input).unwrap();
assert_eq!($output, data);
let ast_1 = ast::parse("", &$input).unwrap().into_string_tree();
let ast_2 = to_ast(&data);
assert_eq!(ast_1, ast_2);
}
};
}
pl_round_trip_test![
header,
"
(FAMILY CMR)
(FACE O 352)
(CODINGSCHEME TEX TEXT)
(DESIGNSIZE R 11.0)
(COMMENT DESIGNSIZE IS IN POINTS)
(COMMENT OTHER SIZES ARE MULTIPLES OF DESIGNSIZE)
(CHECKSUM O 77)
(SEVENBITSAFEFLAG TRUE)
(FONTDIMEN
(SLANT R 0.0)
(SPACE R 0.0)
(STRETCH R 0.0)
(SHRINK R 0.0)
(XHEIGHT R 0.0)
(QUAD R 0.0)
(EXTRASPACE R 0.0)
)
",
File {
header: Header {
checksum: 0o77,
design_size: FixWord((1 << 20) * 11),
character_coding_scheme: Some("TEX TEXT".to_string()),
font_family: Some("CMR".to_string()),
seven_bit_safe: Some(true),
face: Some(Face(0o352)),
additional_data: vec![],
},
char_infos: vec![],
params: Params::default(),
},
];
pl_round_trip_test![
params_basic,
"
(DESIGNSIZE R 10.0)
(COMMENT DESIGNSIZE IS IN POINTS)
(COMMENT OTHER SIZES ARE MULTIPLES OF DESIGNSIZE)
(CHECKSUM O 0)
(FONTDIMEN
(SLANT R 1.0)
(SPACE R 2.0)
(STRETCH R 3.0)
(SHRINK R 4.0)
(XHEIGHT R 5.0)
(QUAD R 6.0)
(EXTRASPACE R 7.0)
(PARAMETER D 8 R 8.0)
)
",
File {
header: Header {
checksum: 0,
design_size: FixWord((1 << 20) * 10),
character_coding_scheme: None,
font_family: None,
seven_bit_safe: None,
face: None,
additional_data: vec![],
},
char_infos: vec![],
params: Params {
slant: FixWord(1 * FixWord::UNITY.0),
space: FixWord(2 * FixWord::UNITY.0),
space_stretch: FixWord(3 * FixWord::UNITY.0),
space_shrink: FixWord(4 * FixWord::UNITY.0),
x_height: FixWord(5 * FixWord::UNITY.0),
quad: FixWord(6 * FixWord::UNITY.0),
extra_space: FixWord(7 * FixWord::UNITY.0),
math_params: MathParams::None,
additional_params: vec![FixWord(8 * FixWord::UNITY.0),],
},
},
];
}