~raph/interp-toy

716b1801d2e062b89ef63b361054e0d9d5fe9c9d — Raph Levien 2 years ago 33881b8
Import glyphstool

I'm moving glyphstool into this repo as a subdirectory, so that it can
be used as a lib, rather than maintaining it as a separate repo. This
commit has the lib structure but doesn't change any functionality.
M .gitignore => .gitignore +1 -1
@@ 1,2 1,2 @@
/target
target
**/*.rs.bk

M Cargo.lock => Cargo.lock +18 -0
@@ 316,6 316,14 @@ dependencies = [
]

[[package]]
name = "glyphstool"
version = "0.1.0"
dependencies = [
 "kurbo 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "plist_derive 0.1.0",
]

[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 328,6 336,7 @@ name = "interp-toy"
version = "0.1.0"
dependencies = [
 "druid 0.3.0 (git+https://github.com/xi-editor/druid?rev=cfbde68ca16c67b20268a5999da469ed76999b82)",
 "glyphstool 0.1.0",
 "nalgebra 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "rbf-interp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]


@@ 602,6 611,15 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "plist_derive"
version = "0.1.0"
dependencies = [
 "proc-macro2 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
 "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "proc-macro-hack"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +2 -0
@@ 10,3 10,5 @@ edition = "2018"
druid = {git = "https://github.com/xi-editor/druid", rev = "cfbde68ca16c67b20268a5999da469ed76999b82" }
rbf-interp = "0.1.3"
nalgebra = "0.18"

glyphstool = { path = "glyphstool" }

A glyphstool/Cargo.lock => glyphstool/Cargo.lock +79 -0
@@ 0,0 1,79 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "arrayvec"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "glyphstool"
version = "0.1.0"
dependencies = [
 "kurbo 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 "plist_derive 0.1.0",
]

[[package]]
name = "kurbo"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "nodrop"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "plist_derive"
version = "0.1.0"
dependencies = [
 "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
 "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "proc-macro2"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "quote"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "syn"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
 "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
 "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
 "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]

[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"

[metadata]
"checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba"
"checksum kurbo 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2f0caeb26248a62abf92dea93aad4f8244f54668e2f1060ed9cd9fd1d5545723"
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afdc77cc74ec70ed262262942ebb7dac3d479e9e5cfa2da1841c0806f6cdabcc"
"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"

A glyphstool/Cargo.toml => glyphstool/Cargo.toml +12 -0
@@ 0,0 1,12 @@
[package]
name = "glyphstool"
version = "0.1.0"
license = "MIT/Apache-2.0"
authors = ["Raph Levien <raph.levien@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
kurbo = "0.5.1"
plist_derive = { path = "plist_derive" }

A glyphstool/plist_derive/Cargo.toml => glyphstool/plist_derive/Cargo.toml +15 -0
@@ 0,0 1,15 @@
[package]
name = "plist_derive"
version = "0.1.0"
authors = ["Raph Levien <raph.levien@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
syn = "1.0.5"
quote = "1.0.2"
proc-macro2 = "1.0"

A glyphstool/plist_derive/src/lib.rs => glyphstool/plist_derive/src/lib.rs +175 -0
@@ 0,0 1,175 @@
extern crate proc_macro;

use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields};

#[proc_macro_derive(FromPlist, attributes(rest))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    let deser = add_deser(&input.data);

    let expanded = quote! {
        impl crate::from_plist::FromPlist for #name {
            fn from_plist(plist: crate::plist::Plist) -> Self {
                let mut hashmap = plist.into_hashmap();
                #name {
                    #deser
                }
            }
        }
    };
    proc_macro::TokenStream::from(expanded)
}

#[proc_macro_derive(ToPlist, attributes(rest))]
pub fn derive_to(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    let ser_rest = add_ser_rest(&input.data);
    let ser = add_ser(&input.data);

    let expanded = quote! {
        impl crate::to_plist::ToPlist for #name {
            fn to_plist(self) -> crate::plist::Plist {
                #ser_rest
                #ser
                hashmap.into()
            }
        }
    };
    proc_macro::TokenStream::from(expanded)
}

fn add_deser(data: &Data) -> TokenStream {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => {
                let recurse = fields.named.iter().filter_map(|f| {
                    if !is_rest(&f.attrs) {
                        let name = &f.ident;
                        let name_str = name.as_ref().unwrap().to_string();
                        let snake_name = snake_to_camel_case(&name_str);
                        Some(quote_spanned! {f.span() =>
                            #name: crate::from_plist::FromPlistOpt::from_plist(
                                hashmap.remove(#snake_name)
                            ),
                        })
                    } else {
                        None
                    }
                });
                let recurse_rest = fields.named.iter().filter_map(|f| {
                    if is_rest(&f.attrs) {
                        let name = &f.ident;
                        Some(quote_spanned! {f.span() =>
                            #name: hashmap,
                        })
                    } else {
                        None
                    }
                });
                quote! {
                    #( #recurse )*
                    #( #recurse_rest )*
                }
            }
            _ => unimplemented!(),
        },
        _ => unimplemented!(),
    }
}

fn add_ser(data: &Data) -> TokenStream {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => {
                let recurse = fields.named.iter().filter_map(|f| {
                    if !is_rest(&f.attrs) {
                        let name = &f.ident;
                        let name_str = name.as_ref().unwrap().to_string();
                        let snake_name = snake_to_camel_case(&name_str);
                        Some(quote_spanned! {f.span() =>
                            if let Some(plist) = crate::to_plist::ToPlistOpt::to_plist(self.#name) {
                                hashmap.insert(#snake_name.to_string(), plist);
                            }
                        })
                    } else {
                        None
                    }
                });
                quote! {
                    #( #recurse )*
                }
            }
            _ => unimplemented!(),
        },
        _ => unimplemented!(),
    }
}

fn add_ser_rest(data: &Data) -> TokenStream {
    match *data {
        Data::Struct(ref data) => match data.fields {
            Fields::Named(ref fields) => {
                for f in fields.named.iter() {
                    if is_rest(&f.attrs) {
                        let name = &f.ident;
                        return quote_spanned! { f.span() =>
                            let mut hashmap = self.#name;
                        }
                    }
                }
                quote! { let mut hashmap = HashMap::new(); }
            }
            _ => unimplemented!(),
        },
        _ => unimplemented!(),
    }
}

fn is_rest(attrs: &[Attribute]) -> bool {
    attrs.iter().any(|attr| {
        attr.path
            .get_ident()
            .map(|ident| ident == "rest")
            .unwrap_or(false)
    })
}

fn snake_to_camel_case(id: &str) -> String {
    let mut result = String::new();
    let mut hump = false;
    for c in id.chars() {
        if c == '_' {
            hump = true;
        } else {
            if hump {
                result.push(c.to_ascii_uppercase());
            } else {
                result.push(c);
            }
            hump = false;
        }
    }
    result
}

/*
fn to_snake_case(id: &str) -> String {
    let mut result = String::new();
    for c in id.chars() {
        if c.is_ascii_uppercase() {
            result.push('_');
            result.push(c.to_ascii_lowercase());
        } else {
            result.push(c);
        }
    }
    result
}
*/

A glyphstool/src/font.rs => glyphstool/src/font.rs +164 -0
@@ 0,0 1,164 @@
//! The general strategy is just to use a plist for storage. Also, lots of
//! unwrapping.
//!
//! There are lots of other ways this could go, including something serde-like
//! where it gets serialized to more Rust-native structures, proc macros, etc.

use std::collections::HashMap;

use kurbo::{Affine, Point};

use crate::from_plist::FromPlist;
use crate::plist::Plist;
use crate::to_plist::ToPlist;

#[derive(Debug, FromPlist, ToPlist)]
pub struct Font {
    pub glyphs: Vec<Glyph>,
    #[rest]
    pub other_stuff: HashMap<String, Plist>,
}

#[derive(Debug, FromPlist, ToPlist)]
pub struct Glyph {
    pub layers: Vec<Layer>,
    pub glyphname: String,
    #[rest]
    pub other_stuff: HashMap<String, Plist>,
}

#[derive(Debug, FromPlist, ToPlist)]
pub struct Layer {
    pub layer_id: String,
    pub width: f64,
    pub paths: Option<Vec<Path>>,
    pub components: Option<Vec<Component>>,
    pub anchors: Option<Vec<Anchor>>,
    pub guide_lines: Option<Vec<GuideLine>>,
    #[rest]
    pub other_stuff: HashMap<String, Plist>,
}

#[derive(Debug, FromPlist, ToPlist)]
pub struct Path {
    pub closed: bool,
    pub nodes: Vec<Node>,
}

#[derive(Debug)]
pub struct Node {
    pub pt: Point,
    pub node_type: NodeType,
}

#[derive(Debug)]
pub enum NodeType {
    Line,
    OffCurve,
    Curve,
    CurveSmooth,
}

#[derive(Debug, FromPlist, ToPlist)]
pub struct Component {
    pub name: String,
    pub transform: Option<Affine>,
    #[rest]
    pub other_stuff: HashMap<String, Plist>,
}

#[derive(Debug, FromPlist, ToPlist)]
pub struct Anchor {
    pub name: String,
    pub position: Point,
}

#[derive(Debug, FromPlist, ToPlist)]
pub struct GuideLine {
    pub angle: Option<f64>,
    pub position: Point,
}

impl FromPlist for Node {
    fn from_plist(plist: Plist) -> Self {
        let mut spl = plist.as_str().unwrap().split(' ');
        let x = spl.next().unwrap().parse().unwrap();
        let y = spl.next().unwrap().parse().unwrap();
        let pt = Point::new(x, y);
        let node_type = spl.next().unwrap().parse().unwrap();
        Node { pt, node_type }
    }
}

impl std::str::FromStr for NodeType {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "LINE" => Ok(NodeType::Line),
            "OFFCURVE" => Ok(NodeType::OffCurve),
            "CURVE" => Ok(NodeType::Curve),
            "CURVE SMOOTH" => Ok(NodeType::CurveSmooth),
            _ => Err(format!("unknown node type {}", s)),
        }
    }
}

impl NodeType {
    fn glyphs_str(&self) -> &'static str {
        match self {
            NodeType::Line => "LINE",
            NodeType::OffCurve => "OFFCURVE",
            NodeType::Curve => "CURVE",
            NodeType::CurveSmooth => "CURVE SMOOTH",
        }
    }
}

impl ToPlist for Node {
    fn to_plist(self) -> Plist {
        format!(
            "{} {} {}",
            self.pt.x,
            self.pt.y,
            self.node_type.glyphs_str()
        )
        .into()
    }
}

impl FromPlist for Affine {
    fn from_plist(plist: Plist) -> Self {
        let raw = plist.as_str().unwrap();
        let raw = &raw[1..raw.len() - 1];
        let coords: Vec<f64> = raw.split(", ").map(|c| c.parse().unwrap()).collect();
        Affine::new([
            coords[0], coords[1], coords[2], coords[3], coords[4], coords[5],
        ])
    }
}

impl ToPlist for Affine {
    fn to_plist(self) -> Plist {
        let c = self.as_coeffs();
        format!(
            "{{{}, {}, {}, {}, {}, {}}}",
            c[0], c[1], c[2], c[3], c[4], c[5]
        )
        .into()
    }
}

impl FromPlist for Point {
    fn from_plist(plist: Plist) -> Self {
        let raw = plist.as_str().unwrap();
        let raw = &raw[1..raw.len() - 1];
        let coords: Vec<f64> = raw.split(", ").map(|c| c.parse().unwrap()).collect();
        Point::new(coords[0], coords[1])
    }
}

impl ToPlist for Point {
    fn to_plist(self) -> Plist {
        format!("{{{}, {}}}", self.x, self.y).into()
    }
}

A glyphstool/src/from_plist.rs => glyphstool/src/from_plist.rs +60 -0
@@ 0,0 1,60 @@
pub use plist_derive::FromPlist;

use crate::plist::Plist;

pub trait FromPlist {
    // Consider using result type; just unwrap for now.
    fn from_plist(plist: Plist) -> Self;
}

pub trait FromPlistOpt {
    // Consider using result type; just unwrap for now.
    fn from_plist(plist: Option<Plist>) -> Self;
}

impl FromPlist for String {
    fn from_plist(plist: Plist) -> Self {
        plist.into_string()
    }
}

impl FromPlist for bool {
    fn from_plist(plist: Plist) -> Self {
        // TODO: maybe error or warn on values other than 0, 1
        plist.as_i64().expect("expected integer") != 0
    }
}

impl FromPlist for i64 {
    fn from_plist(plist: Plist) -> Self {
        plist.as_i64().expect("expected integer")
    }
}

impl FromPlist for f64 {
    fn from_plist(plist: Plist) -> Self {
        plist.as_f64().expect("expected float")
    }
}

impl<T: FromPlist> FromPlist for Vec<T> {
    fn from_plist(plist: Plist) -> Self {
        let mut result = Vec::new();
        for element in plist.into_vec() {
            result.push(FromPlist::from_plist(element));
        }
        result
    }
}

impl<T: FromPlist> FromPlistOpt for T {
    fn from_plist(plist: Option<Plist>) -> Self {
        FromPlist::from_plist(plist.unwrap())
    }
}

impl<T: FromPlist> FromPlistOpt for Option<T> {
    fn from_plist(plist: Option<Plist>) -> Self {
        plist.map(FromPlist::from_plist)
    }
}

A glyphstool/src/lib.rs => glyphstool/src/lib.rs +13 -0
@@ 0,0 1,13 @@
//! Lightweight library for reading and writing Glyphs font files.

mod font;
mod from_plist;
mod plist;
mod stretch;
mod to_plist;

pub use font::Font;
pub use from_plist::FromPlist;
pub use plist::Plist;
pub use stretch::stretch;
pub use to_plist::ToPlist;

A glyphstool/src/main.rs => glyphstool/src/main.rs +39 -0
@@ 0,0 1,39 @@
use std::env;
use std::fs;

use glyphstool::{stretch, Font, FromPlist, Plist, ToPlist};

fn usage() {
    eprintln!("usage: glyphstool font.glyphs");
}

fn main() {
    let mut filename = None;
    for arg in env::args().skip(1) {
        if filename.is_none() {
            filename = Some(arg);
        }
    }
    if filename.is_none() {
        usage();
        return;
    }
    let filename = filename.unwrap();
    let contents = fs::read_to_string(filename).expect("error reading font");
    let plist = Plist::parse(&contents).expect("parse error");
    //println!("Plist: {:?}", plist);
    /*
    let font = Font::from_plist(plist);
    for glyph in font.glyphs() {
        println!("glyphname: {}", glyph.glyphname());
        for layer in glyph.layers() {
            println!("  layer: {}, width = {}", layer.layer_id(), layer.width());
        }
    }
    */
    let mut font: Font = FromPlist::from_plist(plist);
    //println!("{:?}", font);
    stretch::stretch(&mut font, 0.5, "051EFAE4-8BBE-4FBB-A016-4335C3E52F59");
    let plist = font.to_plist();
    println!("{}", plist.to_string());
}

A glyphstool/src/plist.rs => glyphstool/src/plist.rs +403 -0
@@ 0,0 1,403 @@
use std::borrow::Cow;
use std::collections::HashMap;

/// An enum representing a property list.
#[derive(Clone, Debug)]
pub enum Plist {
    Dictionary(HashMap<String, Plist>),
    Array(Vec<Plist>),
    String(String),
    Integer(i64),
    Float(f64),
}

#[derive(Debug)]
pub enum Error {
    UnexpectedChar(char),
    UnclosedString,
    UnknownEscape,
    NotAString,
    ExpectedEquals,
    ExpectedComma,
    ExpectedSemicolon,
    SomethingWentWrong,
}

enum Token<'a> {
    Eof,
    OpenBrace,
    OpenParen,
    String(Cow<'a, str>),
    Atom(&'a str),
}

fn is_numeric(b: u8) -> bool {
    (b >= b'0' && b <= b'9') || b == b'.' || b == b'-'
}

fn is_alnum(b: u8) -> bool {
    is_numeric(b) || (b >= b'A' && b <= b'Z') || (b >= b'a' && b <= b'z') || b == b'_'
}

// Used for serialization; make sure UUID's get quoted
fn is_alnum_strict(b: u8) -> bool {
    is_alnum(b) && b != b'-'
}

fn is_ascii_whitespace(b: u8) -> bool {
    b == b' ' || b == b'\t' || b == b'\r' || b == b'\n'
}

fn numeric_ok(s: &str) -> bool {
    if s.is_empty() {
        return false;
    }
    if s.len() > 1 && s.as_bytes()[0] == b'0' {
        return !s.as_bytes().iter().all(|&b| b >= b'0' && b <= b'9');
    }
    true
}

fn skip_ws(s: &str, mut ix: usize) -> usize {
    while ix < s.len() && is_ascii_whitespace(s.as_bytes()[ix]) {
        ix += 1;
    }
    ix
}

fn escape_string(buf: &mut String, s: &str) {
    if !s.is_empty() && s.as_bytes().iter().all(|&b| is_alnum_strict(b)) {
        buf.push_str(s);
    } else {
        buf.push('"');
        let mut start = 0;
        let mut ix = start;
        while ix < s.len() {
            let b = s.as_bytes()[ix];
            match b {
                b'"' | b'\\' => {
                    buf.push_str(&s[start..ix]);
                    buf.push('\\');
                    start = ix;
                }
                _ => (),
            }
            ix += 1;
        }
        buf.push_str(&s[start..]);
        buf.push('"');
    }
}

impl Plist {
    pub fn parse(s: &str) -> Result<Plist, Error> {
        let (plist, _ix) = Plist::parse_rec(s, 0)?;
        // TODO: check that we're actually at eof
        Ok(plist)
    }

    #[allow(unused)]
    pub fn as_dict(&self) -> Option<&HashMap<String, Plist>> {
        match self {
            Plist::Dictionary(d) => Some(d),
            _ => None,
        }
    }

    #[allow(unused)]
    pub fn get(&self, key: &str) -> Option<&Plist> {
        match self {
            Plist::Dictionary(d) => d.get(key),
            _ => None,
        }
    }

    #[allow(unused)]
    pub fn as_array(&self) -> Option<&[Plist]> {
        match self {
            Plist::Array(a) => Some(a),
            _ => None,
        }
    }

    #[allow(unused)]
    pub fn as_str(&self) -> Option<&str> {
        match self {
            Plist::String(s) => Some(s),
            _ => None,
        }
    }

    pub fn as_i64(&self) -> Option<i64> {
        match self {
            Plist::Integer(i) => Some(*i),
            _ => None,
        }
    }

    pub fn as_f64(&self) -> Option<f64> {
        match self {
            Plist::Integer(i) => Some(*i as f64),
            Plist::Float(f) => Some(*f),
            _ => None,
        }
    }

    pub fn into_string(self) -> String {
        match self {
            Plist::String(s) => s,
            _ => panic!("expected string"),
        }
    }

    pub fn into_vec(self) -> Vec<Plist> {
        match self {
            Plist::Array(a) => a,
            _ => panic!("expected array"),
        }
    }

    pub fn into_hashmap(self) -> HashMap<String, Plist> {
        match self {
            Plist::Dictionary(d) => d,
            _ => panic!("expected dictionary"),
        }
    }

    fn parse_rec(s: &str, ix: usize) -> Result<(Plist, usize), Error> {
        let (tok, mut ix) = Token::lex(s, ix)?;
        match tok {
            Token::Atom(s) => Ok((Plist::parse_atom(s), ix)),
            Token::String(s) => Ok((Plist::String(s.into()), ix)),
            Token::OpenBrace => {
                let mut dict = HashMap::new();
                loop {
                    if let Some(ix) = Token::expect(s, ix, b'}') {
                        return Ok((Plist::Dictionary(dict), ix));
                    }
                    let (key, next) = Token::lex(s, ix)?;
                    let key_str = Token::try_into_string(key)?;
                    let next = Token::expect(s, next, b'=');
                    if next.is_none() {
                        return Err(Error::ExpectedEquals);
                    }
                    let (val, next) = Self::parse_rec(s, next.unwrap())?;
                    dict.insert(key_str, val);
                    if let Some(next) = Token::expect(s, next, b';') {
                        ix = next;
                    } else {
                        return Err(Error::ExpectedSemicolon);
                    }
                }
            }
            Token::OpenParen => {
                let mut list = Vec::new();
                if let Some(ix) = Token::expect(s, ix, b')') {
                    return Ok((Plist::Array(list), ix));
                }
                loop {
                    let (val, next) = Self::parse_rec(s, ix)?;
                    list.push(val);
                    if let Some(ix) = Token::expect(s, next, b')') {
                        return Ok((Plist::Array(list), ix));
                    }
                    if let Some(next) = Token::expect(s, next, b',') {
                        ix = next;
                    } else {
                        return Err(Error::ExpectedComma);
                    }
                }
            }
            _ => Err(Error::SomethingWentWrong),
        }
    }

    fn parse_atom(s: &str) -> Plist {
        if numeric_ok(s) {
            if let Ok(num) = s.parse() {
                return Plist::Integer(num);
            }
            if let Ok(num) = s.parse() {
                return Plist::Float(num);
            }
        }
        Plist::String(s.into())
    }

    pub fn to_string(&self) -> String {
        let mut s = String::new();
        self.push_to_string(&mut s);
        s
    }

    fn push_to_string(&self, s: &mut String) {
        match self {
            Plist::Array(a) => {
                s.push_str("(");
                let mut delim = "\n";
                for el in a {
                    s.push_str(delim);
                    el.push_to_string(s);
                    delim = ",\n";
                }
                s.push_str("\n)");
            }
            Plist::Dictionary(a) => {
                s.push_str("{\n");
                // TODO: probably want to sort keys
                for (k, el) in a {
                    // TODO: quote if needed?
                    escape_string(s, k);
                    s.push_str(" = ");
                    el.push_to_string(s);
                    s.push_str(";\n");
                }
                s.push_str("}");
            }
            Plist::String(st) => escape_string(s, st),
            Plist::Integer(i) => {
                s.push_str(&format!("{}", i));
            }
            Plist::Float(f) => {
                s.push_str(&format!("{}", f));
            }
        }
    }
}

impl<'a> Token<'a> {
    fn lex(s: &'a str, ix: usize) -> Result<(Token<'a>, usize), Error> {
        let start = skip_ws(s, ix);
        if start == s.len() {
            return Ok((Token::Eof, start));
        }
        let b = s.as_bytes()[start];
        match b {
            b'{' => Ok((Token::OpenBrace, start + 1)),
            b'(' => Ok((Token::OpenParen, start + 1)),
            b'"' => {
                let mut ix = start + 1;
                let mut cow_start = ix;
                let mut buf = String::new();
                while ix < s.len() {
                    let b = s.as_bytes()[ix];
                    match b {
                        b'"' => {
                            // End of string
                            let string = if buf.is_empty() {
                                s[cow_start..ix].into()
                            } else {
                                buf.push_str(&s[cow_start..ix]);
                                buf.into()
                            };
                            return Ok((Token::String(string), ix + 1));
                        }
                        b'\\' => {
                            buf.push_str(&s[cow_start..ix]);
                            ix += 1;
                            if ix == s.len() {
                                return Err(Error::UnclosedString);
                            }
                            let b = s.as_bytes()[ix];
                            match b {
                                b'"' | b'\\' => cow_start = ix,
                                b'n' => {
                                    buf.push('\n');
                                    cow_start = ix + 1;
                                }
                                b'r' => {
                                    buf.push('\r');
                                    cow_start = ix + 1;
                                }
                                _ => {
                                    if b >= b'0' && b <= b'3' && ix + 2 < s.len() {
                                        // octal escape
                                        let b1 = s.as_bytes()[ix + 1];
                                        let b2 = s.as_bytes()[ix + 2];
                                        if b1 >= b'0' && b1 <= b'7' && b2 >= b'0' && b2 <= b'7' {
                                            let oct =
                                                (b - b'0') * 64 + (b1 - b'0') * 8 + (b2 - b'0');
                                            buf.push(oct as char);
                                            ix += 2;
                                            cow_start = ix + 1;
                                        } else {
                                            return Err(Error::UnknownEscape);
                                        }
                                    } else {
                                        return Err(Error::UnknownEscape);
                                    }
                                }
                            }
                            ix += 1;
                        }
                        _ => ix += 1,
                    }
                }
                Err(Error::UnclosedString)
            }
            _ => {
                if is_alnum(b) {
                    let mut ix = start + 1;
                    while ix < s.len() {
                        if !is_alnum(s.as_bytes()[ix]) {
                            break;
                        }
                        ix += 1;
                    }
                    Ok((Token::Atom(&s[start..ix]), ix))
                } else {
                    Err(Error::UnexpectedChar(s[start..].chars().next().unwrap()))
                }
            }
        }
    }

    fn try_into_string(self) -> Result<String, Error> {
        match self {
            Token::Atom(s) => Ok(s.into()),
            Token::String(s) => Ok(s.into()),
            _ => Err(Error::NotAString),
        }
    }

    fn expect(s: &str, ix: usize, delim: u8) -> Option<usize> {
        let ix = skip_ws(s, ix);
        if ix < s.len() {
            let b = s.as_bytes()[ix];
            if b == delim {
                return Some(ix + 1);
            }
        }
        None
    }
}

impl From<String> for Plist {
    fn from(x: String) -> Plist {
        Plist::String(x)
    }
}

impl From<i64> for Plist {
    fn from(x: i64) -> Plist {
        Plist::Integer(x)
    }
}

impl From<f64> for Plist {
    fn from(x: f64) -> Plist {
        Plist::Float(x)
    }
}

impl From<Vec<Plist>> for Plist {
    fn from(x: Vec<Plist>) -> Plist {
        Plist::Array(x)
    }
}

impl From<HashMap<String, Plist>> for Plist {
    fn from(x: HashMap<String, Plist>) -> Plist {
        Plist::Dictionary(x)
    }
}

A glyphstool/src/stretch.rs => glyphstool/src/stretch.rs +54 -0
@@ 0,0 1,54 @@
//! A little logic to apply horizontal stretching to a font.

use kurbo::Affine;

use crate::font::{Font, Glyph, Layer};

fn affine_stretch(stretch: f64) -> Affine {
    Affine::new([stretch, 0., 0., 1., 0., 0.])
}

fn stretch_layer(layer: &mut Layer, stretch: f64) {
    let a = affine_stretch(stretch);
    let a_inv = affine_stretch(stretch.recip());
    layer.width = (layer.width * stretch).round();
    if let Some(ref mut paths) = layer.paths {
        for path in paths {
            for node in &mut path.nodes {
                node.pt = (a * node.pt).round();
            }
        }
    }
    if let Some(ref mut anchors) = layer.anchors {
        for anchor in anchors {
            anchor.position = (a * anchor.position).round();
        }
    }
    if let Some(ref mut guide_lines) = layer.guide_lines {
        for guide_line in guide_lines {
            guide_line.position = (a * guide_line.position).round();
        }
    }
    if let Some(ref mut components) = layer.components {
        for component in components {
            if let Some(ref mut transform) = component.transform {
                // TODO: round the translation component
                *transform = a * *transform * a_inv;
            }
        }
    }
}

fn stretch_glyph(glyph: &mut Glyph, stretch: f64, layer_id: &str) {
    for layer in &mut glyph.layers {
        if layer.layer_id == layer_id {
            stretch_layer(layer, stretch);
        }
    }
}

pub fn stretch(font: &mut Font, stretch: f64, layer_id: &str) {
    for glyph in &mut font.glyphs {
        stretch_glyph(glyph, stretch, layer_id);
    }
}

A glyphstool/src/to_plist.rs => glyphstool/src/to_plist.rs +57 -0
@@ 0,0 1,57 @@
pub use plist_derive::ToPlist;

use crate::plist::Plist;

pub trait ToPlist {
    fn to_plist(self) -> Plist;
}

pub trait ToPlistOpt {
    fn to_plist(self) -> Option<Plist>;
}

impl ToPlist for String {
    fn to_plist(self) -> Plist {
        self.into()
    }
}

impl ToPlist for bool {
    fn to_plist(self) -> Plist {
        (self as i64).into()
    }
}

impl ToPlist for i64 {
    fn to_plist(self) -> Plist {
        self.into()
    }
}

impl ToPlist for f64 {
    fn to_plist(self) -> Plist {
        self.into()
    }
}

impl<T: ToPlist> ToPlist for Vec<T> {
    fn to_plist(self) -> Plist {
        let mut result = Vec::new();
        for element in self {
            result.push(ToPlist::to_plist(element));
        }
        result.into()
    }
}

impl<T: ToPlist> ToPlistOpt for T {
    fn to_plist(self) -> Option<Plist> {
        Some(ToPlist::to_plist(self))
    }
}

impl<T: ToPlist> ToPlistOpt for Option<T> {
    fn to_plist(self) -> Option<Plist> {
        self.map(ToPlist::to_plist)
    }
}