1use std::ops::Range;
2
3use fea_rs::{
4 NodeOrToken,
5 typed::{AstNode as _, Number, Os2TableItem},
6};
7use smol_str::SmolStr;
8
9use crate::{AsFea, Comment, FeaTable, Table};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Os2Field {
13 Comment(Comment),
14 FSType(u8),
15 Panose(Vec<u8>),
16 UnicodeRange(Vec<u16>),
17 CodePageRange(Vec<u16>),
18 TypoAscender(i16),
19 TypoDescender(i16),
20 TypoLineGap(i16),
21 WinAscent(u16),
22 WinDescent(u16),
23 XHeight(i16),
24 CapHeight(i16),
25 WeightClass(u16),
26 WidthClass(u16),
27
28 Vendor(SmolStr),
29}
30impl AsFea for Os2Field {
31 fn as_fea(&self, indent: &str) -> String {
32 match self {
33 Os2Field::Comment(cmt) => cmt.as_fea(indent),
34 Os2Field::FSType(x) => format!("{}FSType {};", indent, x),
35 Os2Field::WeightClass(x) => format!("{}WeightClass {};", indent, x),
36 Os2Field::WidthClass(x) => format!("{}WidthClass {};", indent, x),
37 Os2Field::Panose(vals) => format!(
38 "{}Panose {};",
39 indent,
40 vals.iter()
41 .map(|v| v.to_string())
42 .collect::<Vec<_>>()
43 .join(" ")
44 ),
45 Os2Field::Vendor(x) => format!("{}Vendor \"{}\";", indent, x),
46 Os2Field::TypoAscender(x) => format!("{}TypoAscender {};", indent, x),
47 Os2Field::TypoDescender(x) => format!("{}TypoDescender {};", indent, x),
48 Os2Field::TypoLineGap(x) => format!("{}TypoLineGap {};", indent, x),
49 Os2Field::UnicodeRange(items) => format!(
50 "{}UnicodeRange {};",
51 indent,
52 items
53 .iter()
54 .map(|v| v.to_string())
55 .collect::<Vec<_>>()
56 .join(" ")
57 ),
58 Os2Field::CodePageRange(items) => format!(
59 "{}CodePageRange {};",
60 indent,
61 items
62 .iter()
63 .map(|v| v.to_string())
64 .collect::<Vec<_>>()
65 .join(" ")
66 ),
67 Os2Field::WinAscent(x) => format!("{}winAscent {};", indent, x),
68 Os2Field::WinDescent(x) => format!("{}winDescent {};", indent, x),
69 Os2Field::XHeight(x) => format!("{}XHeight {};", indent, x),
70 Os2Field::CapHeight(x) => format!("{}CapHeight {};", indent, x),
71 }
72 }
73}
74
75impl From<Os2TableItem> for Os2Statement {
76 fn from(val: Os2TableItem) -> Self {
77 match val {
78 Os2TableItem::Number(number_record) => {
79 let keyword = number_record
80 .iter()
81 .next()
82 .and_then(|t| t.as_token())
83 .unwrap();
84 let number = number_record
85 .iter()
86 .find_map(Number::cast)
87 .unwrap()
88 .text()
89 .parse::<i64>()
90 .unwrap();
91 let field = match keyword.as_str() {
92 "FSType" => Os2Field::FSType(number as u8),
93 "WeightClass" => Os2Field::WeightClass(number as u16),
94 "WidthClass" => Os2Field::WidthClass(number as u16),
95 _ => panic!("Unknown OS/2 number field: {}", keyword.as_str()),
96 };
97 Os2Statement {
98 field,
99 location: number_record.range(),
100 }
101 }
102 Os2TableItem::NumberList(os2_number_list) => {
103 let keyword = os2_number_list
104 .iter()
105 .next()
106 .and_then(|t| t.as_token())
107 .unwrap();
108 let numbers: Vec<u16> = os2_number_list
109 .iter()
110 .filter_map(Number::cast)
111 .map(|n| n.text().parse::<u16>().unwrap())
112 .collect();
113 let field = match keyword.as_str() {
114 "Panose" => Os2Field::Panose(numbers.iter().map(|&n| n as u8).collect()),
115 "UnicodeRange" => Os2Field::UnicodeRange(numbers),
116 "CodePageRange" => Os2Field::CodePageRange(numbers),
117 _ => panic!("Unknown OS/2 number list field: {}", keyword.as_str()),
118 };
119 Os2Statement {
120 field,
121 location: os2_number_list.range(),
122 }
123 }
124 Os2TableItem::Metric(metric_record) => {
125 let keyword = metric_record
126 .iter()
127 .next()
128 .and_then(|t| t.as_token())
129 .unwrap();
130 let metric = metric_record
131 .iter()
132 .find_map(fea_rs::typed::Metric::cast)
133 .unwrap();
134
135 let value = match metric {
136 fea_rs::typed::Metric::Scalar(number) => number.text().parse::<i16>().unwrap(),
137 _ => unimplemented!(),
138 };
139 let field = match keyword.as_str() {
140 "TypoAscender" => Os2Field::TypoAscender(value),
141 "TypoDescender" => Os2Field::TypoDescender(value),
142 "TypoLineGap" => Os2Field::TypoLineGap(value),
143 "XHeight" => Os2Field::XHeight(value),
144 "CapHeight" => Os2Field::CapHeight(value),
145 "winAscent" => Os2Field::WinAscent(value as u16),
146 "winDescent" => Os2Field::WinDescent(value as u16),
147 _ => panic!("Unknown OS/2 metric field: {}", keyword.as_str()),
148 };
149 Os2Statement {
150 field,
151 location: metric_record.range(),
152 }
153 }
154 Os2TableItem::Vendor(vendor_record) => {
155 let vendor = vendor_record
156 .iter()
157 .find(|t| t.kind() == fea_rs::Kind::String)
158 .and_then(|t| t.as_token())
159 .unwrap();
160 Os2Statement {
161 field: Os2Field::Vendor(vendor.text.trim_matches('"').to_string().into()),
162 location: vendor_record.range(),
163 }
164 }
165 Os2TableItem::FamilyClass(_os2_family_class) => todo!(),
166 }
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct Os2Statement {
172 field: Os2Field,
173 location: Range<usize>,
174}
175impl AsFea for Os2Statement {
176 fn as_fea(&self, indent: &str) -> String {
177 self.field.as_fea(indent)
178 }
179}
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct Os2;
182impl FeaTable for Os2 {
183 type Statement = Os2Statement;
184 const TAG: &'static str = "OS/2";
185 type FeaRsTable = fea_rs::typed::Os2Table;
186 #[allow(clippy::manual_map)]
187 fn to_statement(child: &NodeOrToken) -> Option<Os2Statement> {
188 if child.kind() == fea_rs::Kind::Comment {
189 Some(Os2Statement {
190 field: Os2Field::Comment(Comment::from(child.token_text().unwrap())),
191 location: child.range(),
192 })
193 } else if let Some(fr) = fea_rs::typed::Os2TableItem::cast(child) {
194 Some(fr.into())
195 } else {
196 None
197 }
198 }
199}
200
201impl From<fea_rs::typed::Os2Table> for Table<Os2> {
202 fn from(val: fea_rs::typed::Os2Table) -> Self {
203 Self {
204 statements: Os2::statements_from_node(val.node()),
205 }
206 }
207}