~reesmichael1/burro

97b11fdb812ad8a883aeec04a1d9d6d4cb673148 — Michael Rees 1 year, 17 days ago 096123d
Add .ligatures command (used for disabling ligatures)
5 files changed, 86 insertions(+), 4 deletions(-)

A examples/no_ligatures.bur
M src/layout.rs
M src/parser/doc_config.rs
M src/parser/error.rs
M src/parser/mod.rs
A examples/no_ligatures.bur => examples/no_ligatures.bur +9 -0
@@ 0,0 1,9 @@
.family[Cardo]
.start
In this line, ligatures are enabled. Below, the word ``different'' and the ``ffi'' letter combination will look different than they do on this line.

.ligatures[off]
Now, ligatures are disabled. See how different looks different? Also note that I'm typing this in the office.

.ligatures[on]
Ligatures are officially once again enabled!

M src/layout.rs => src/layout.rs +39 -4
@@ 96,12 96,32 @@ struct Word {
}

impl Word {
    fn new(word: Arc<TextUnit>, face: &Face, font_id: u32, pt_size: f64) -> Self {
    fn new(word: Arc<TextUnit>, face: &Face, font_id: u32, pt_size: f64, ligatures: bool) -> Self {
        match &*word {
            TextUnit::Str(s) => {
                let mut in_buf = UnicodeBuffer::new();
                in_buf.push_str(&s);
                let out_buf = shape(&face, &vec![], in_buf);
                // If ligatures are currently disabled, turn them off here
                // liga = standard ligatures
                // dlig = discretionary ligatures
                // clig = contextual ligatures
                // We're not disabling rlig ("required ligatures") since those are, well, required
                // TODO: allow the user to control more of these features independently
                let lig_tags = [b"liga", b"dlig", b"clig", b"rlig"];
                let features: Vec<rustybuzz::Feature> = if !ligatures {
                    lig_tags
                        .iter()
                        // s.len() reports the number of bytes we need to format
                        // (NOT the number of graphemes), which is what rustybuzz::shape expects
                        .map(|t| {
                            rustybuzz::Feature::new(ttf_parser::Tag::from_bytes(t), 0, 0..s.len())
                        })
                        .collect()
                } else {
                    vec![]
                };

                let out_buf = shape(&face, &features, in_buf);
                let info = out_buf.glyph_infos();
                let positions = out_buf.glyph_positions();



@@ 170,6 190,7 @@ struct BurroParams {
    hyphenate: bool,
    consecutive_hyphens: u64,
    letter_space: f64,
    ligatures: bool,
}

#[derive(Debug)]


@@ 288,6 309,7 @@ impl<'a> LayoutBuilder<'a> {
            hyphenate: true,
            consecutive_hyphens: 3,
            letter_space: 0.,
            ligatures: true,
        };

        let font_data = load_font_data(font_map)?;


@@ 445,6 467,10 @@ impl<'a> LayoutBuilder<'a> {
            self.params.letter_space = space;
        }

        if let Some(ligatures) = config.ligatures {
            self.params.ligatures = ligatures;
        }

        if config.page_height.is_some() || config.page_width.is_some() {
            self.current_page = self.new_page();
            self.set_cursor_top_left();


@@ 867,6 893,7 @@ impl<'a> LayoutBuilder<'a> {
                    return Err(BurroError::NoTabsLoaded);
                }
            }
            Command::Ligatures(l) => self.params.ligatures = *l,
        }

        Ok(())


@@ 1010,6 1037,7 @@ impl<'a> LayoutBuilder<'a> {
                            &face,
                            font_id,
                            self.params.pt_size,
                            self.params.ligatures,
                        ));

                        if self.total_line_width(&current_line)


@@ 1069,12 1097,14 @@ impl<'a> LayoutBuilder<'a> {
                                            &face,
                                            font_id,
                                            self.params.pt_size,
                                            self.params.ligatures,
                                        );
                                        let rest = Word::new(
                                            Arc::new(rest),
                                            &face,
                                            font_id,
                                            self.params.pt_size,
                                            self.params.ligatures,
                                        );

                                        current_line.push(start.clone());


@@ 1204,8 1234,13 @@ impl<'a> LayoutBuilder<'a> {
            .font_map
            .font_id(&self.params.font_family, self.font.font_num());

        self.current_line
            .push(Word::new(word.clone(), &face, font_id, self.params.pt_size));
        self.current_line.push(Word::new(
            word.clone(),
            &face,
            font_id,
            self.params.pt_size,
            self.params.ligatures,
        ));

        Ok(())
    }

M src/parser/doc_config.rs => src/parser/doc_config.rs +6 -0
@@ 24,6 24,7 @@ pub struct DocConfig {
    pub letter_space: Option<f64>,
    pub tabs: Vec<Tab>,
    pub tab_lists: HashMap<String, Vec<String>>,
    pub ligatures: Option<bool>,
}

impl DocConfig {


@@ 101,6 102,11 @@ impl DocConfig {
        self
    }

    pub fn with_ligatures(mut self, ligatures: bool) -> Self {
        self.ligatures = Some(ligatures);
        self
    }

    pub fn add_tab(mut self, tab: Tab) -> Result<Self, ParseError> {
        let mut tab = tab;


M src/parser/error.rs => src/parser/error.rs +2 -0
@@ 70,4 70,6 @@ pub enum ParseError {
    DuplicateTab(String),
    #[error("repeated curly brace definition for '{0}'")]
    DuplicateCurlyBraceKey(String),
    #[error("malformed command with boolean argument")]
    MalformedBoolCommand,
}

M src/parser/mod.rs => src/parser/mod.rs +30 -0
@@ 47,6 47,7 @@ pub enum Command {
    NextTab,
    PreviousTab,
    QuitTabs,
    Ligatures(bool),
}

#[derive(Debug, PartialEq)]


@@ 120,6 121,28 @@ fn parse_bool_arg(val: &str) -> Result<bool, ParseError> {
    }
}

// There are times where we'll want to be generous with boolean arguments
// (for example, .ligatures[on] is nicer than .ligatures[true])
// This could maybe become a slippery slope, so we need to be careful
// As long as this remains clearly documented, this shouldn't cause too much confusion
// We can always revisit this in the future if it becomes a problem.
fn parse_relaxed_bool_arg(val: &str) -> Result<bool, ParseError> {
    match val {
        "true" | "on" | "yes" => Ok(true),
        "false" | "off" | "no" => Ok(false),
        _ => Err(ParseError::InvalidBool(val.to_string())),
    }
}

fn parse_bool_command(tokens: &[Token]) -> Result<(bool, &[Token]), ParseError> {
    match tokens {
        [Token::Command(_), Token::OpenSquare, Token::Word(arg), Token::CloseSquare, rest @ ..] => {
            Ok((parse_relaxed_bool_arg(arg)?, rest))
        }
        _ => Err(ParseError::MalformedBoolCommand),
    }
}

fn parse_node_list(tokens: &[Token]) -> Result<(Vec<Node>, &[Token]), ParseError> {
    fn get_paragraph(tokens: &[Token]) -> Result<(Vec<Node>, &[Token]), ParseError> {
        let (par, remaining) = parse_paragraph(tokens)?;


@@ 290,6 313,10 @@ fn parse_command(name: String, tokens: &[Token]) -> Result<(Node, &[Token]), Par
            pop_spaces(&tokens[1..]),
        )),
        "quit_tabs" => Ok((Node::Command(Command::QuitTabs), pop_spaces(&tokens[1..]))),
        "ligatures" => {
            let (arg, rem) = parse_bool_command(tokens)?;
            Ok((Node::Command(Command::Ligatures(arg)), rem))
        }
        _ => Err(ParseError::UnknownCommand(name)),
    }
}


@@ 720,6 747,9 @@ fn parse_config(tokens: &[Token]) -> Result<(DocConfig, &[Token]), ParseError> {
                        Node::Command(Command::TabList(list, name)) => {
                            config = config.add_tab_list(list, name);
                        }
                        Node::Command(Command::Ligatures(l)) => {
                            config = config.with_ligatures(l);
                        }
                        _ => return Err(ParseError::InvalidConfiguration),
                    }