Repository: typst/typst
File: crates/typst-library/src/text/mod.rs (Rust)

Code before:
    v: Str => {
        // Tags must: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#data-types
        // - be one to four bytes in length
        // - be representable as printable ASCII (0x20..=0x7E)
        // - contain at least one character that isn't padding (0x20, space)
        // - padding may only appear at the end of a tag

        if !(1..=4).contains(&v.len()) {
            let e = error!(
                "feature tag must be one to four bytes in length";
                hint: "found {} bytes", v.len()
            );

            return Err(match v.grapheme_indices(true).find(|(_, v)| v.len() > 1) {
                Some((i,v)) => e.with_hint(
                    eco_format!("found multi-byte cluster '{v}' in grapheme {}", i+1)
                ),
                None => e
            })
        }

        let mut within_padding = false;
        for (i, &v) in v.as_bytes().iter().enumerate() {
            match v {
                0x20 if i == 0 => {
                    bail!("feature tag must contain at least one non-space character")
                }
                0x20 => within_padding = true,
                0x21 ..= 0x7E => if within_padding {
                    bail!("spaces may only appear as padding at the end of a feature tag")
                },
                .. 0x20 | 0x7F .. => bail!(
                    "feature tag must be printable ASCII";
                    hint: "found: U+{v:04X}"
                )
            }
        }

        Self::from_bytes_lossy(v.as_bytes())
    }
}

cast! {
    FontFeatures,
    self => self.0
        .into_iter()
        .map(|(tag, num)| {
            let bytes = tag.to_bytes();
            let key = std::str::from_utf8(&bytes).unwrap_or_default();
            (key.into(), num.into_value())
        })

Review comment (suggestion):
this would also be thrown for `" abc"` which is in conflict with the error message. I think the message "spaces may only appear as padding at the end of a feature tag" would be good here, too.

Code after:
        // - padding may only appear at the end of a tag

        if let Some(cluster) = v.graphemes(true).find(|v| {
            !v.as_bytes().iter().all(|v| (0x20..=0x7E).contains(v))
        }) {
            bail!(
                "feature tag may contain only printable ASCII characters";
                hint: "found invalid cluster `{}`", cluster.repr();
            )
        }

        if !(1..=4).contains(&v.len()) {
            bail!(
                "feature tag must be one to four characters in length";
                hint: "found {} characters", v.len();
            );
        }

        let mut within_padding = false;
        for (i, &v) in v.as_bytes().iter().enumerate() {
            if (within_padding && v != b' ') || (i == 0 && v == b' ') {
                bail!("spaces may only appear as padding following a feature tag")
            }
            within_padding |= b' ' == v;
        }

        Self::from_bytes_lossy(v.as_bytes())
    }
}

cast! {
    FontFeatures,
    self => self.0
        .into_iter()
        .map(|(tag, num)| {
            let bytes = tag.to_bytes();
            let key = std::str::from_utf8(&bytes).unwrap_or_default();
            (key.into(), num.into_value())
        })
        .collect::<Dict>()
        .into_value(),
    values: Array => Self(values
        .into_iter()
        .enumerate()
        .map(|(i, v)| Ok((
            v.clone().cast::<Tag>().hint(tag_hint_helper(i, &v)).map_err(|e| {
                // Append a hint if the value is a string containing the
                // assignment operator `=` or another type was supplied.
                if v.cast::<Str>().map_or(true, |v| v.as_str().contains('=')) {
                    e.with_hint("to set features with custom values, consider supplying a dictionary")
                } else {