1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
use paste::paste;
use std::str::FromStr;
use thiserror::Error;

#[derive(Debug, Error)]
#[error("No such variant: {0}")]
pub struct NoSuchVariant(String);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WritingDirection {
	Horizontal = 0,
	Vertical = 1,
	Both = 2
}

impl FromStr for WritingDirection {
	type Err = NoSuchVariant;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		match s {
			"0" => Ok(Self::Horizontal),
			"1" => Ok(Self::Vertical),
			"2" => Ok(Self::Both),
			_ => Err(NoSuchVariant(s.into()))
		}
	}
}

macro_rules! tokens {
	(
		$(#[doc$($doc:tt)*])*
		$vis:vis enum $ident:ident {
			$(
				$(#[doc$($variant_doc:tt)*])*
				$(#[test($test_input:literal, $test_expected:expr)])*
				$variant:ident {
					$tag:literal $(,
						$($arg:ident: $arg_ty:ident),* $(,)?
						$(..$remaining:ident)?
					)?
				}
			),*
		}
	) => {
		paste! {
			$(#[doc$($doc)*])*
			#[allow(clippy::derive_partial_eq_without_eq)]
			#[derive(Clone, Debug, PartialEq)]
			$vis enum $ident {
				$(
					$(#[doc$($variant_doc)*])*
					$variant $({ $($arg: $arg_ty,)* $($remaining: String)? })?
				),*
			}

			#[derive(Debug, Error)]
			pub enum Error {
				$(
					$(
						$(
							#[error(
								"Failed to parse argument {} of tag {}: {0}",
								stringify!($arg),
								$tag
							)]
							[<$variant $arg:camel>](#[source] <$arg_ty as FromStr>::Err),
						)*
					)?
				)*

				#[error("Missing the argument {1} of tag {0}")]
				MissingArg(&'static str, &'static str),

				#[error("Extra tokens after tag {0}")]
				ExtraTokens(&'static str),

				#[error("Unknown tag: {0}")]
				UnknownTag(String)
			}

			impl $ident {
				#[allow(unused_assignments, unused_mut)]
				$vis fn parse_line(line: &str) -> Result<Self, Error> {
					let mut tokens = line
						.trim_end()
						.split(|ch: char| ch.is_ascii_whitespace())
						.peekable();

					$(
						if tokens.peek() == Some(&$tag) {
							tokens.next().unwrap();
							let mut empty = tokens.peek().is_none();
							$(
								$(
									let $arg: $arg_ty = tokens
										.next()
										.ok_or(Error::MissingArg($tag, stringify!($arg)))?
										.parse()
										.map_err(Error::[<$variant $arg:camel>])?;
									empty = tokens.peek().is_none();
								)*
								$(
									let $remaining = tokens
										.fold(String::new(), |mut rem, token| {
											if !rem.is_empty() {
												rem += " ";
											}
											rem += token;
											rem
										});
									empty = true;
								)?
							)?
							if !empty {
								return Err(Error::ExtraTokens($tag));
							}
							return Ok(Self::$variant $({ $($arg,)* $($remaining)? })?);
						}
					)*

					return Err(Error::UnknownTag(tokens.next().unwrap().into()));
				}
			}

			$(
				#[cfg(test)]
				#[test]
				fn [<test_ $ident:lower _parse_ $variant:lower>]() {
					$(
						let expected: $ident = {
							use $ident::*;
							$test_expected
						};
						assert_eq!(expected, $ident::parse_line($test_input).unwrap());
					)*
				}
			)*
		}
	};
}

// https://adobe-type-tools.github.io/font-tech-notes/pdfs/5005.BDF_Spec.pdf
tokens! {
	pub enum Token {
		/// `STARTFONT` is followed by a version number indicating the exact file format
		/// used (for example, 2.1).
		#[test("STARTFONT 2.1", StartFont { ver: "2.1".into() })]
		StartFont { "STARTFONT", ver: String },

		/// One or more lines beginning with the word `COMMENT`. These lines can be
		/// ignored by any program reading the file.
		#[test("COMMENT hello world", Comment { comment: "hello world".into() })]
		Comment { "COMMENT", ..comment },

		/// (Optional) The value of `CONTENTVERSION` is an integer which can be
		/// assigned by an installer program to keep track of the version of the included
		/// data. The value is intended to be valid only in a single environment, under
		/// the control of a single installer. The value of `CONTENTVERSION` should only
		/// reflect upgrades to the quality of the bitmap images, not to the glyph
		/// complement or encoding.
		#[test("CONTENTVERSION 1", ContentVersion { ver: 1 })]
		ContentVersion { "CONTENTVERSION", ver: i32 },

		/// `FONT` is followed by the font name, which should exactly match the
		/// Post-Script™ language **FontName** in the corresponding outline font program
		#[test("FONT font-name", Font { name: "font-name".into() })]
		Font { "FONT", name: String },

		/// `SIZE` is followed by the point size of the glyphs and the x and y resolutions
		/// of the device for which the font is intended.
		#[test("SIZE 16 75 75", Size { pt: 16, xres: 75, yres: 75 })]
		Size { "SIZE", pt: u32, xres: u32, yres: u32 },

		/// `FONTBOUNDINGBOX` is followed by the width in x and the height in y, and
		/// the x and y displacement of the lower left corner from origin 0 (for
		/// horizontal writing direction); all in integer pixel values.
		#[test(
			"FONTBOUNDINGBOX 16 16 0 -2",
			FontBoundingBox { fbbx: 16, fbby: 16, xoff: 0, yoff: -2 }
		)]
		FontBoundingBox { "FONTBOUNDINGBOX", fbbx: u32, fbby: u32, xoff: i32, yoff: i32 },

		/// (Optional) The integer value of `METRICSSET  may be 0, 1, or 2, which
		/// scorrepond to writing direction 0 only, 1 only, or both (respectively). If not
		/// present, `METRICSSET` 0 is implied. If `METRICSSET` is 1, `DWIDTH` and
		/// `SWIDTH` keywords are optional.
		#[test("METRICSSET 0", MetricsSet { dir: WritingDirection::Horizontal })]
		#[test("METRICSSET 1", MetricsSet { dir: WritingDirection::Vertical })]
		#[test("METRICSSET 2", MetricsSet { dir: WritingDirection::Both })]
		MetricsSet { "METRICSSET", dir: WritingDirection },

		/// `SWIDTH` is followed by swx0 and swy0, the scalable width of the glyph in x
		/// and y for writing mode 0. The scalable widths are of type Number and are in
		/// units of 1/1000th of the size of the glyph and correspond to the widths found
		/// in AFM files (for outline fonts). If the size of the glyph is p points, the
		/// width information must be scaled by p/1000 to get the width of the glyph in
		/// printer’s points. This width information should be regarded as a vector
		/// indicating the position of the next glyph’s origin relative to the origin of
		/// this glyph. `SWIDTH` is mandatory for all writing mode 0 fonts.
		///
		/// To convert the scalable width to the width in device pixels, multiply `SWIDTH`
		/// times p/1000 times r/72, where r is the device resolution in pixels per inch.
		/// The result is a real number giving the ideal width in device pixels. The
		/// actual device width must be an integral number of device pixels and is given
		/// by the `DWIDTH` entry.
		#[test("SWIDTH 1000 0", SWidth { swx0: 1000.0, swy0: 0.0 })]
		SWidth { "SWIDTH", swx0: f64, swy0: f64 },

		/// `DWIDTH` specifies the widths in x and y, dwx0 and dwy0, in device pixels.
		/// Like `SWIDTH`, this width information is a vector indicating the position of
		/// the next glyph’s origin relative to the origin of this glyph. `DWIDTH` is
		/// mandatory for all writing mode 0 fonts.
		#[test("DWIDTH 16 0", DWidth { dwx0: 16.0, dwy0: 0.0 })]
		DWidth { "DWIDTH", dwx0: f64, dwy0: f64 },

		/// `SWIDTH1` is followed by the values for swx1 and swy1, the scalable width of
		/// the glyph in x and y, for writing mode 1 (vertical direction). The values are
		/// of type Number, and represent the widths in glyph space coordinates.
		#[test("SWIDTH1 1000 0", SWidthVertical { swx1: 1000.0, swy1: 0.0 })]
		SWidthVertical { "SWIDTH1", swx1: f64, swy1: f64 },

		/// `DWIDTH1` specifies the integer pixel width of the glyph in x and y. Like
		/// `SWIDTH1`, this width information is a vector indicating the position of the
		/// next glyph’s origin relative to the origin of this glyph. `DWIDTH1` is
		/// mandatory for all writing mode 1 fonts.
		#[test("DWIDTH1 16 0", DWidthVertical { dwx1: 16.0, dwy1: 0.0 })]
		DWidthVertical { "DWIDTH1", dwx1: f64, dwy1: f64 },

		/// `VVECTOR` (optional) specifies the components of a vector from origin 0 (the
		/// origin for writing direction 0) to origin 1 (the origin for writing direction
		/// 1). If the value of `METRICSSET` is 1 or 2, `VVECTOR` must be specified either
		/// at the global level, or for each individual glyph. If specified at the global
		/// level, the `VVECTOR` is the same for all glyphs, though the inclusion of this
		/// keyword in an individual glyph has the effect of overriding the bal value for
		/// that specific glyph.
		#[test("VVECTOR 1 1", VVector { xoff: 1.0, yoff: 1.0 })]
		VVector { "VVECTOR", xoff: f64, yoff: f64 },

		/// The optional word `STARTPROPERTIES` may be followed by the number of
		/// properties (n) that follow. Within the properties list, there may be n lines
		/// consisting of a word for the property name followed by either an integer or
		/// string surrounded by ASCII double quotation marks (ASCII octal 042).
		/// Internal quotation characters are indicated (or “quoted”) by using two
		/// quotation characters in a row.
		#[test("STARTPROPERTIES 1", StartProperties { n: 1 })]
		StartProperties { "STARTPROPERTIES", n: usize },

		/// The word `ENDPROPERTIES` is used to delimit the end of the optional properties
		/// list in fonts files containing the word `STARTPROPERTIES`.
		#[test("ENDPROPERTIES", EndProperties {})]
		EndProperties { "ENDPROPERTIES" },

		/// `CHARS` is followed by *nglyphs*, the number of glyphs that follow. To make
		/// sure that the correct number of glyphs were actually read and processed, error
		/// checking is recommended at the end of the file
		#[test("CHARS 1", Chars { nglyphs: 1 })]
		Chars { "CHARS", nglyphs: usize },

		/// The word `STARTCHAR` followed by a string containing the name for the
		/// glyph. In base fonts, this should correspond to the name in the PostScript
		/// language outline font’s encoding vector. In a Composite font (Type 0), the
		/// value may be a numeric offset or glyph ID.
		#[test("STARTCHAR U+0041", StartChar { name: "U+0041".into() })]
		StartChar { "STARTCHAR", name: String },

		/// `ENCODING` is followed by a positive integer representing the Adobe Standard
		/// Encoding value. If the character is not in the Adobe Standard Encoding,
		/// `ENCODING` is followed by –1 and optionally by another integer specifying
		/// the glyph index for the non-standard encoding.
		// TODO represent the optional encoding value
		#[test("ENCODING 65", Encoding { enc: 65 })]
		Encoding { "ENCODING", enc: u32 },

		/// `BBX` is followed by BBw, the width of the black pixels in x, and BBh, the
		/// height in y. These are followed by the x and y displacement, BBxoff0 and
		/// BByoff0, of the lower left corner of the bitmap from origin 0. All values are
		/// are an integer number of pixels.
		///
		/// If the font specifies metrics for writing direction 1, `VVECTOR` specifies the
		/// offset from origin 0 to origin 1. For example, for writing direction 1, the
		/// offset from origin 1 to the lower left corner of the bitmap would be:
		///
		/// BBxoff1x,y = BBxoff0x,y – `VVECTOR`
		#[test("BBX 16 16 0 -2", BoundingBox { bbw: 16, bbh: 16, bbxoff: 0, bbyoff: -2 })]
		BoundingBox { "BBX", bbw: u32, bbh: u32, bbxoff: i32, bbyoff: i32 },

		/// `BITMAP` introduces the hexadecimal data for the character bitmap. From the
		/// `BBX` value for h, find h lines of hex-encoded bitmap, padded on the right
		/// with zero’s to the nearest byte (that is, multiple of 8). Hex data can be
		/// turned into binary by taking two bytes at a time, each of which represents 4
		/// bits of the 8-bit value. For example, the byte 01101101 is two hex digits: 6
		/// (0110 in hex) and D (1101 in hex).
		#[test("BITMAP", Bitmap {})]
		Bitmap { "BITMAP" },

		/// `ENDCHAR` delimits the end of the glyph description
		#[test("ENDCHAR", EndChar {})]
		EndChar { "ENDCHAR" },

		/// The entire file is terminated with the word `ENDFONT`. If this is encountered
		/// before all of the glyphs have been read, it is an error cond
		#[test("ENDFONT", EndFont {})]
		EndFont { "ENDFONT" }
	}
}