aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--components/gfx/font.rs84
-rw-r--r--components/gfx/lib.rs2
-rw-r--r--components/gfx/text/shaping/harfbuzz.rs84
-rw-r--r--components/gfx/text/shaping/mod.rs3
-rw-r--r--components/gfx/text/text_run.rs29
-rw-r--r--components/layout/text.rs22
-rw-r--r--components/style/properties/mod.rs.mako23
-rw-r--r--components/util/geometry.rs2
-rw-r--r--tests/ref/basic.list1
-rw-r--r--tests/ref/letter_spacing_a.html17
-rw-r--r--tests/ref/letter_spacing_ref.html26
11 files changed, 246 insertions, 47 deletions
diff --git a/components/gfx/font.rs b/components/gfx/font.rs
index d7c28fb1d8c..c2d4504e720 100644
--- a/components/gfx/font.rs
+++ b/components/gfx/font.rs
@@ -13,9 +13,10 @@ use style::computed_values::{font_variant, font_weight};
use style::style_structs::Font as FontStyle;
use sync::Arc;
-use servo_util::geometry::Au;
+use collections::hash::Hash;
use platform::font_context::FontContextHandle;
use platform::font::{FontHandle, FontTable};
+use servo_util::geometry::Au;
use text::glyph::{GlyphStore, GlyphId};
use text::shaping::ShaperMethods;
use text::{Shaper, TextRun};
@@ -95,37 +96,85 @@ pub struct Font {
pub requested_pt_size: Au,
pub actual_pt_size: Au,
pub shaper: Option<Shaper>,
- pub shape_cache: HashCache<String, Arc<GlyphStore>>,
- pub glyph_advance_cache: HashCache<u32, FractionalPixel>,
+ pub shape_cache: HashCache<ShapeCacheEntry,Arc<GlyphStore>>,
+ pub glyph_advance_cache: HashCache<u32,FractionalPixel>,
+}
+
+bitflags! {
+ flags ShapingFlags: u8 {
+ #[doc="Set if the text is entirely whitespace."]
+ const IS_WHITESPACE_SHAPING_FLAG = 0x01,
+ #[doc="Set if we are to ignore ligatures."]
+ const IGNORE_LIGATURES_SHAPING_FLAG = 0x02
+ }
+}
+
+/// Various options that control text shaping.
+#[deriving(Clone, Eq, PartialEq, Hash)]
+pub struct ShapingOptions {
+ /// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
+ /// NB: You will probably want to set the `IGNORE_LIGATURES_SHAPING_FLAG` if this is non-null.
+ pub letter_spacing: Option<Au>,
+ /// Various flags.
+ pub flags: ShapingFlags,
+}
+
+/// An entry in the shape cache.
+#[deriving(Clone, Eq, PartialEq, Hash)]
+pub struct ShapeCacheEntry {
+ text: String,
+ options: ShapingOptions,
+}
+
+#[deriving(Clone, Eq, PartialEq, Hash)]
+struct ShapeCacheEntryRef<'a> {
+ text: &'a str,
+ options: &'a ShapingOptions,
+}
+
+impl<'a> Equiv<ShapeCacheEntry> for ShapeCacheEntryRef<'a> {
+ fn equiv(&self, other: &ShapeCacheEntry) -> bool {
+ self.text == other.text.as_slice() && *self.options == other.options
+ }
}
impl Font {
- pub fn shape_text(&mut self, text: &str, is_whitespace: bool) -> Arc<GlyphStore> {
- self.make_shaper();
+ pub fn shape_text(&mut self, text: &str, options: &ShapingOptions) -> Arc<GlyphStore> {
+ self.make_shaper(options);
+
let shaper = &self.shaper;
- match self.shape_cache.find_equiv(text) {
+ let lookup_key = ShapeCacheEntryRef {
+ text: text,
+ options: options,
+ };
+ match self.shape_cache.find_equiv(&lookup_key) {
None => {}
Some(glyphs) => return (*glyphs).clone(),
}
- let mut glyphs = GlyphStore::new(text.char_len() as int, is_whitespace);
- shaper.as_ref().unwrap().shape_text(text, &mut glyphs);
+ let mut glyphs = GlyphStore::new(text.char_len() as int,
+ options.flags.contains(IS_WHITESPACE_SHAPING_FLAG));
+ shaper.as_ref().unwrap().shape_text(text, options, &mut glyphs);
+
let glyphs = Arc::new(glyphs);
- self.shape_cache.insert(text.to_string(), glyphs.clone());
+ self.shape_cache.insert(ShapeCacheEntry {
+ text: text.to_string(),
+ options: *options,
+ }, glyphs.clone());
glyphs
}
- fn make_shaper<'a>(&'a mut self) -> &'a Shaper {
+ fn make_shaper<'a>(&'a mut self, options: &ShapingOptions) -> &'a Shaper {
// fast path: already created a shaper
match self.shaper {
- Some(ref shaper) => {
- let s: &'a Shaper = shaper;
- return s;
+ Some(ref mut shaper) => {
+ shaper.set_options(options);
+ return shaper
},
None => {}
}
- let shaper = Shaper::new(self);
+ let shaper = Shaper::new(self, options);
self.shaper = Some(shaper);
self.shaper.as_ref().unwrap()
}
@@ -149,7 +198,8 @@ impl Font {
self.handle.glyph_index(codepoint)
}
- pub fn glyph_h_kerning(&mut self, first_glyph: GlyphId, second_glyph: GlyphId) -> FractionalPixel {
+ pub fn glyph_h_kerning(&mut self, first_glyph: GlyphId, second_glyph: GlyphId)
+ -> FractionalPixel {
self.handle.glyph_h_kerning(first_glyph, second_glyph)
}
@@ -175,11 +225,11 @@ impl FontGroup {
}
}
- pub fn create_textrun(&self, text: String) -> TextRun {
+ pub fn create_textrun(&self, text: String, options: &ShapingOptions) -> TextRun {
assert!(self.fonts.len() > 0);
// TODO(Issue #177): Actually fall back through the FontGroup when a font is unsuitable.
- TextRun::new(&mut *self.fonts.get(0).borrow_mut(), text.clone())
+ TextRun::new(&mut *self.fonts.get(0).borrow_mut(), text.clone(), options)
}
}
diff --git a/components/gfx/lib.rs b/components/gfx/lib.rs
index 943de358ddd..c680233f7ac 100644
--- a/components/gfx/lib.rs
+++ b/components/gfx/lib.rs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-#![feature(globs, macro_rules, phase, unsafe_destructor)]
+#![feature(globs, macro_rules, phase, unsafe_destructor, default_type_params)]
#![deny(unused_imports)]
#![deny(unused_variables)]
diff --git a/components/gfx/text/shaping/harfbuzz.rs b/components/gfx/text/shaping/harfbuzz.rs
index 23c39a195ca..aec15f0014f 100644
--- a/components/gfx/text/shaping/harfbuzz.rs
+++ b/components/gfx/text/shaping/harfbuzz.rs
@@ -4,7 +4,8 @@
extern crate harfbuzz;
-use font::{Font, FontHandleMethods, FontTableMethods, FontTableTag};
+use font::{Font, FontHandleMethods, FontTableMethods, FontTableTag, IGNORE_LIGATURES_SHAPING_FLAG};
+use font::{ShapingOptions};
use platform::font::FontTable;
use text::glyph::{CharIndex, GlyphStore, GlyphId, GlyphData};
use text::shaping::ShaperMethods;
@@ -18,9 +19,11 @@ use harfbuzz::{hb_bool_t};
use harfbuzz::{hb_buffer_add_utf8};
use harfbuzz::{hb_buffer_destroy};
use harfbuzz::{hb_buffer_get_glyph_positions};
+use harfbuzz::{hb_buffer_get_length};
use harfbuzz::{hb_buffer_set_direction};
use harfbuzz::{hb_face_destroy};
use harfbuzz::{hb_face_t, hb_font_t};
+use harfbuzz::{hb_feature_t};
use harfbuzz::{hb_font_create};
use harfbuzz::{hb_font_destroy, hb_buffer_create};
use harfbuzz::{hb_font_funcs_create};
@@ -47,6 +50,9 @@ use std::ptr;
static NO_GLYPH: i32 = -1;
static CONTINUATION_BYTE: i32 = -2;
+static LIGA: u32 = ((b'l' as u32) << 24) | ((b'i' as u32) << 16) | ((b'g' as u32) << 8) |
+ (b'a' as u32);
+
pub struct ShapedGlyphData {
count: int,
glyph_infos: *mut hb_glyph_info_t,
@@ -131,10 +137,16 @@ impl ShapedGlyphData {
}
}
+struct FontAndShapingOptions {
+ font: *mut Font,
+ options: ShapingOptions,
+}
+
pub struct Shaper {
hb_face: *mut hb_face_t,
hb_font: *mut hb_font_t,
hb_funcs: *mut hb_font_funcs_t,
+ font_and_shaping_options: Box<FontAndShapingOptions>,
}
#[unsafe_destructor]
@@ -154,13 +166,18 @@ impl Drop for Shaper {
}
impl Shaper {
- pub fn new(font: &mut Font) -> Shaper {
+ pub fn new(font: &mut Font, options: &ShapingOptions) -> Shaper {
unsafe {
- // Indirection for Rust Issue #6248, dynamic freeze scope artificially extended
- let font_ptr = font as *mut Font;
- let hb_face: *mut hb_face_t = hb_face_create_for_tables(get_font_table_func,
- font_ptr as *mut c_void,
- None);
+ let mut font_and_shaping_options = box FontAndShapingOptions {
+ font: font,
+ options: *options,
+ };
+ let hb_face: *mut hb_face_t =
+ hb_face_create_for_tables(get_font_table_func,
+ (&mut *font_and_shaping_options)
+ as *mut FontAndShapingOptions
+ as *mut c_void,
+ None);
let hb_font: *mut hb_font_t = hb_font_create(hb_face);
// Set points-per-em. if zero, performs no hinting in that direction.
@@ -178,16 +195,21 @@ impl Shaper {
hb_font_funcs_set_glyph_func(hb_funcs, glyph_func, ptr::null_mut(), None);
hb_font_funcs_set_glyph_h_advance_func(hb_funcs, glyph_h_advance_func, ptr::null_mut(), None);
hb_font_funcs_set_glyph_h_kerning_func(hb_funcs, glyph_h_kerning_func, ptr::null_mut(), ptr::null_mut());
- hb_font_set_funcs(hb_font, hb_funcs, font_ptr as *mut c_void, None);
+ hb_font_set_funcs(hb_font, hb_funcs, font as *mut Font as *mut c_void, None);
Shaper {
hb_face: hb_face,
hb_font: hb_font,
hb_funcs: hb_funcs,
+ font_and_shaping_options: font_and_shaping_options,
}
}
}
+ pub fn set_options(&mut self, options: &ShapingOptions) {
+ self.font_and_shaping_options.options = *options
+ }
+
fn float_to_fixed(f: f64) -> i32 {
float_to_fixed(16, f)
}
@@ -200,7 +222,7 @@ impl Shaper {
impl ShaperMethods for Shaper {
/// Calculate the layout metrics associated with the given text when painted in a specific
/// font.
- fn shape_text(&self, text: &str, glyphs: &mut GlyphStore) {
+ fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore) {
unsafe {
let hb_buffer: *mut hb_buffer_t = hb_buffer_create();
hb_buffer_set_direction(hb_buffer, HB_DIRECTION_LTR);
@@ -211,15 +233,29 @@ impl ShaperMethods for Shaper {
0,
text.len() as c_int);
- hb_shape(self.hb_font, hb_buffer, ptr::null_mut(), 0);
- self.save_glyph_results(text, glyphs, hb_buffer);
+ let mut features = Vec::new();
+ if options.flags.contains(IGNORE_LIGATURES_SHAPING_FLAG) {
+ features.push(hb_feature_t {
+ _tag: LIGA,
+ _value: 0,
+ _start: 0,
+ _end: hb_buffer_get_length(hb_buffer),
+ })
+ }
+
+ hb_shape(self.hb_font, hb_buffer, features.as_mut_ptr(), features.len() as u32);
+ self.save_glyph_results(text, options, glyphs, hb_buffer);
hb_buffer_destroy(hb_buffer);
}
}
}
impl Shaper {
- fn save_glyph_results(&self, text: &str, glyphs: &mut GlyphStore, buffer: *mut hb_buffer_t) {
+ fn save_glyph_results(&self,
+ text: &str,
+ options: &ShapingOptions,
+ glyphs: &mut GlyphStore,
+ buffer: *mut hb_buffer_t) {
let glyph_data = ShapedGlyphData::new(buffer);
let glyph_count = glyph_data.len();
let byte_max = text.len() as int;
@@ -401,8 +437,9 @@ impl Shaper {
// (i.e., pretend there are no combining character sequences).
// 1-to-1 mapping of character to glyph also treated as ligature start.
let shape = glyph_data.get_entry_for_glyph(glyph_span.begin(), &mut y_pos);
+ let advance = self.advance_for_shaped_glyph(shape.advance, options);
let data = GlyphData::new(shape.codepoint,
- shape.advance,
+ advance,
shape.offset,
false,
true,
@@ -450,6 +487,13 @@ impl Shaper {
// lookup table for finding detailed glyphs by associated char index.
glyphs.finalize_changes();
}
+
+ fn advance_for_shaped_glyph(&self, advance: Au, options: &ShapingOptions) -> Au {
+ match options.letter_spacing {
+ None => advance,
+ Some(spacing) => advance + spacing,
+ }
+ }
}
/// Callbacks from Harfbuzz when font map and glyph advance lookup needed.
@@ -504,13 +548,19 @@ extern fn glyph_h_kerning_func(_: *mut hb_font_t,
}
// Callback to get a font table out of a font.
-extern fn get_font_table_func(_: *mut hb_face_t, tag: hb_tag_t, user_data: *mut c_void) -> *mut hb_blob_t {
+extern fn get_font_table_func(_: *mut hb_face_t,
+ tag: hb_tag_t,
+ user_data: *mut c_void)
+ -> *mut hb_blob_t {
unsafe {
- let font: *const Font = user_data as *const Font;
- assert!(font.is_not_null());
+ // NB: These asserts have security implications.
+ let font_and_shaping_options: *const FontAndShapingOptions =
+ user_data as *const FontAndShapingOptions;
+ assert!(font_and_shaping_options.is_not_null());
+ assert!((*font_and_shaping_options).font.is_not_null());
// TODO(Issue #197): reuse font table data, which will change the unsound trickery here.
- match (*font).get_table_for_tag(tag as FontTableTag) {
+ match (*(*font_and_shaping_options).font).get_table_for_tag(tag as FontTableTag) {
None => ptr::null_mut(),
Some(ref font_table) => {
let skinny_font_table_ptr: *const FontTable = font_table; // private context
diff --git a/components/gfx/text/shaping/mod.rs b/components/gfx/text/shaping/mod.rs
index 7fce60a3106..79e5452db06 100644
--- a/components/gfx/text/shaping/mod.rs
+++ b/components/gfx/text/shaping/mod.rs
@@ -7,6 +7,7 @@
//!
//! Currently, only harfbuzz bindings are implemented.
+use font::ShapingOptions;
use text::glyph::GlyphStore;
pub use text::shaping::harfbuzz::Shaper;
@@ -14,6 +15,6 @@ pub use text::shaping::harfbuzz::Shaper;
pub mod harfbuzz;
pub trait ShaperMethods {
- fn shape_text(&self, text: &str, glyphs: &mut GlyphStore);
+ fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore);
}
diff --git a/components/gfx/text/text_run.rs b/components/gfx/text/text_run.rs
index 4bb280ef261..08d09bb48b5 100644
--- a/components/gfx/text/text_run.rs
+++ b/components/gfx/text/text_run.rs
@@ -2,15 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-use font::{Font, RunMetrics, FontMetrics};
+use font::{Font, FontHandleMethods, FontMetrics, IS_WHITESPACE_SHAPING_FLAG, RunMetrics};
+use font::{ShapingOptions};
+use platform::font_template::FontTemplateData;
use servo_util::geometry::Au;
use servo_util::range::Range;
use servo_util::vec::{Comparator, FullBinarySearchMethods};
use std::slice::Items;
use sync::Arc;
use text::glyph::{CharIndex, GlyphStore};
-use font::FontHandleMethods;
-use platform::font_template::FontTemplateData;
/// A single "paragraph" of text in one font size and style.
#[deriving(Clone)]
@@ -117,8 +117,8 @@ impl<'a> Iterator<Range<CharIndex>> for LineIterator<'a> {
}
impl<'a> TextRun {
- pub fn new(font: &mut Font, text: String) -> TextRun {
- let glyphs = TextRun::break_and_shape(font, text.as_slice());
+ pub fn new(font: &mut Font, text: String, options: &ShapingOptions) -> TextRun {
+ let glyphs = TextRun::break_and_shape(font, text.as_slice(), options);
let run = TextRun {
text: Arc::new(text),
font_metrics: font.metrics.clone(),
@@ -129,7 +129,8 @@ impl<'a> TextRun {
return run;
}
- pub fn break_and_shape(font: &mut Font, text: &str) -> Vec<GlyphRun> {
+ pub fn break_and_shape(font: &mut Font, text: &str, options: &ShapingOptions)
+ -> Vec<GlyphRun> {
// TODO(Issue #230): do a better job. See Gecko's LineBreaker.
let mut glyphs = vec!();
let (mut byte_i, mut char_i) = (0u, CharIndex(0));
@@ -165,8 +166,14 @@ impl<'a> TextRun {
let slice = text.slice(byte_last_boundary, byte_i);
debug!("creating glyph store for slice {} (ws? {}), {} - {} in run {}",
slice, !cur_slice_is_whitespace, byte_last_boundary, byte_i, text);
+
+ let mut options = *options;
+ if !cur_slice_is_whitespace {
+ options.flags.insert(IS_WHITESPACE_SHAPING_FLAG);
+ }
+
glyphs.push(GlyphRun {
- glyph_store: font.shape_text(slice, !cur_slice_is_whitespace),
+ glyph_store: font.shape_text(slice, &options),
range: Range::new(char_last_boundary, char_i - char_last_boundary),
});
byte_last_boundary = byte_i;
@@ -182,8 +189,14 @@ impl<'a> TextRun {
let slice = text.slice_from(byte_last_boundary);
debug!("creating glyph store for final slice {} (ws? {}), {} - {} in run {}",
slice, cur_slice_is_whitespace, byte_last_boundary, text.len(), text);
+
+ let mut options = *options;
+ if cur_slice_is_whitespace {
+ options.flags.insert(IS_WHITESPACE_SHAPING_FLAG);
+ }
+
glyphs.push(GlyphRun {
- glyph_store: font.shape_text(slice, cur_slice_is_whitespace),
+ glyph_store: font.shape_text(slice, &options),
range: Range::new(char_last_boundary, char_i - char_last_boundary),
});
}
diff --git a/components/layout/text.rs b/components/layout/text.rs
index c12119f4510..ac96967522b 100644
--- a/components/layout/text.rs
+++ b/components/layout/text.rs
@@ -9,7 +9,8 @@
use fragment::{Fragment, ScannedTextFragmentInfo, UnscannedTextFragment};
use inline::InlineFragments;
-use gfx::font::{FontMetrics,RunMetrics};
+use gfx::font::{FontMetrics, IGNORE_LIGATURES_SHAPING_FLAG, RunMetrics, ShapingFlags};
+use gfx::font::{ShapingOptions};
use gfx::font_context::FontContext;
use gfx::text::glyph::CharIndex;
use gfx::text::text_run::TextRun;
@@ -105,6 +106,7 @@ impl TextRunScanner {
let fontgroup;
let compression;
let text_transform;
+ let letter_spacing;
{
let in_fragment = self.clump.front().unwrap();
let font_style = in_fragment.style().get_font_arc();
@@ -114,6 +116,7 @@ impl TextRunScanner {
white_space::pre => CompressNone,
};
text_transform = in_fragment.style().get_inheritedtext().text_transform;
+ letter_spacing = in_fragment.style().get_inheritedtext().letter_spacing;
}
// First, transform/compress text of all the nodes.
@@ -150,7 +153,22 @@ impl TextRunScanner {
self.clump = DList::new();
return last_whitespace
}
- Arc::new(box TextRun::new(&mut *fontgroup.fonts.get(0).borrow_mut(), run_text))
+
+ // Per CSS 2.1 § 16.4, "when the resultant space between two characters is not the same
+ // as the default space, user agents should not use ligatures." This ensures that, for
+ // example, `finally` with a wide `letter-spacing` renders as `f i n a l l y` and not
+ // `fi n a l l y`.
+ let options = ShapingOptions {
+ letter_spacing: letter_spacing,
+ flags: match letter_spacing {
+ Some(Au(0)) | None => ShapingFlags::empty(),
+ Some(_) => IGNORE_LIGATURES_SHAPING_FLAG,
+ },
+ };
+
+ Arc::new(box TextRun::new(&mut *fontgroup.fonts.get(0).borrow_mut(),
+ run_text,
+ &options))
};
// Make new fragments with the run and adjusted text indices.
diff --git a/components/style/properties/mod.rs.mako b/components/style/properties/mod.rs.mako
index e97db4c8224..2b9d9ae4971 100644
--- a/components/style/properties/mod.rs.mako
+++ b/components/style/properties/mod.rs.mako
@@ -1088,6 +1088,29 @@ pub mod longhands {
// TODO: initial value should be 'start' (CSS Text Level 3, direction-dependent.)
${single_keyword("text-align", "left right center justify")}
+ <%self:single_component_value name="letter-spacing">
+ pub type SpecifiedValue = Option<specified::Length>;
+ pub mod computed_value {
+ use super::super::Au;
+ pub type T = Option<Au>;
+ }
+ #[inline]
+ pub fn get_initial_value() -> computed_value::T {
+ None
+ }
+ #[inline]
+ pub fn to_computed_value(value: SpecifiedValue, context: &computed::Context)
+ -> computed_value::T {
+ value.map(|length| computed::compute_Au(length, context))
+ }
+ pub fn from_component_value(input: &ComponentValue, _: &Url) -> Result<SpecifiedValue,()> {
+ match input {
+ &Ident(ref value) if value.eq_ignore_ascii_case("normal") => Ok(None),
+ _ => specified::Length::parse_non_negative(input).map(|length| Some(length)),
+ }
+ }
+ </%self:single_component_value>
+
${new_style_struct("Text", is_inherited=False)}
<%self:longhand name="text-decoration">
diff --git a/components/util/geometry.rs b/components/util/geometry.rs
index 06b74c7c3a0..1dfa1fe0f9d 100644
--- a/components/util/geometry.rs
+++ b/components/util/geometry.rs
@@ -64,7 +64,7 @@ pub enum PagePx {}
// See https://bugzilla.mozilla.org/show_bug.cgi?id=177805 for more info.
//
// FIXME: Implement Au using Length and ScaleFactor instead of a custom type.
-#[deriving(Clone, PartialEq, PartialOrd, Eq, Ord, Zero)]
+#[deriving(Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Zero)]
pub struct Au(pub i32);
impl Default for Au {
diff --git a/tests/ref/basic.list b/tests/ref/basic.list
index f06e562d94b..f1de5d65c41 100644
--- a/tests/ref/basic.list
+++ b/tests/ref/basic.list
@@ -193,3 +193,4 @@ fragment=top != ../html/acid2.html acid2_ref.html
== text_transform_capitalize_a.html text_transform_capitalize_ref.html
== outlines_simple_a.html outlines_simple_ref.html
== outlines_wrap_a.html outlines_wrap_ref.html
+== letter_spacing_a.html letter_spacing_ref.html
diff --git a/tests/ref/letter_spacing_a.html b/tests/ref/letter_spacing_a.html
new file mode 100644
index 00000000000..f0169810c47
--- /dev/null
+++ b/tests/ref/letter_spacing_a.html
@@ -0,0 +1,17 @@
+<html>
+<head>
+<!-- Tests that `letter-spacing` works. -->
+<link rel="stylesheet" type="text/css" href="css/ahem.css">
+<style>
+* {
+ letter-spacing: 100px;
+ color: blue;
+}
+body {
+ margin: 0;
+}
+</style>
+<body>XXX</body>
+</head>
+
+
diff --git a/tests/ref/letter_spacing_ref.html b/tests/ref/letter_spacing_ref.html
new file mode 100644
index 00000000000..46c65aa8394
--- /dev/null
+++ b/tests/ref/letter_spacing_ref.html
@@ -0,0 +1,26 @@
+<html>
+<head>
+<!-- Tests that `letter-spacing` works. -->
+<style>
+section, nav, main {
+ background-color: blue;
+ width: 100px;
+ height: 100px;
+ position: absolute;
+ top: 0;
+}
+section {
+ left: 0;
+}
+nav {
+ left: 200px;
+}
+main {
+ left: 400px;
+}
+</style>
+<body><section></section><nav></nav><main></main></body>
+</head>
+
+
+