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(¤t_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),
}