Untwisting a Hypertext Narrative - PEG to the Rescue!

In this post you’ll learn why I think Parsing Expression Grammars are awesome and see an example of how I built one to scratch an itch.

The Itch

After spending some time writing Choose Your Own Adventure-style books in markdown, I quickly realized there were some tools missing that could greatly improve the writing process. A few missing items were:

  1. Knowing if there are any unreachable sections that have been orphaned in the writing process.
  2. Being able to see all the branches within a book.
  3. Knowing each branch is coherent by having an easy way to read through them.

“Never fear,” I say to myself, “I can just write some code to parse the markdown files and pluck out the paths. This will be easy.”

As a quick reminder, the format for a single section looks something like this:

1
2
3
4
5
6
7
# Something isn't right here. {#intro}

You hear a phone ringing.

- [pick up phone](#phone)
- [do not answer](#ignore-phone)
- [set yourself on fire](#fire)

(Headers specify new sections starting and have some anchor. Links direct you to new sections.)

There are plenty of ways to slurp in a story file and parse it. You could write a naive line-by-line loop that breaks it into sections based on the presence of a header and then parse the links within sections with substring matching. You could write some complicated regular expression because we all know how much fun regular expressions can become. Or you could do something saner like write a parsing expression grammar (hereafter PEG).

Why a PEG?

Generally, a regex makes for a beautiful collection of cryptic ascii art that you’ll either comment-to-death or be confused by when you stumble across it weeks or months later. PEGs take a different approach and instead seek define “a formal language in terms of a set of rules for recognizing strings in the language.” Because they’re a set of rules, you can slowly TDD your way up from parsing a single phrase to parsing an entire document (or at least the parts you care about).

(It is worth mentioning that because the format here is pretty trivial, either the naive line-by-line solution or a regex is fine. PEGs are without a doubt the right choice IMHO for complicated grammars.)

Show me some code

We’ll be using Parslet to write our PEG. Parslet provides a succinct syntax and exponentially better error messages than other competing ruby PEGs (parse_with_debug is my friend). My biggest complaint about Parslet is that the documentation was occasionally lacking, but it only slowed things down a bit – and there’s an IRC channel and mailing list.

Let’s start off simple, just parsing the links out of a single section of markdown. Being a TDD’er, we’ll write a few simple tests first (in MiniTest::Spec):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
describe LinkParser do
  def parse(input)
    LinkParser.new.parse(input)
  end

  it "can match a single link" do
    parsed = parse("[some link name](#some-href)").first

    assert_equal "some-href",
      parsed[:id]
  end

  it "can match a single link surrounded by content" do
    parsed = parse("
      hey there [some link name](#some-href)
      some content
    ").first

    assert_equal "some-href",
      parsed[:id]
  end

  it "can match a multiple links surrounded by content" do
    parsed = parse("
      hey there [some link name](#some-href)
      some content with a link [another](#new-href) and [another still](#last) ok?
    ")

    assert_equal ["some-href", "new-href", "last"],
      parsed.map{|s| s[:id].to_s}
  end
end

And the working implementation of LinkParser:

1
2
3
4
5
6
7
8
9
10
11
class LinkParser < Parslet::Parser
  rule(:link_text) { str("[") >> (str(']').absent? >> any).repeat >> str(']') }
  rule(:link_href) {
      str('(#') >> (str(')').absent? >> any).repeat.as(:id) >> str(')')
  }
  rule(:link)      { link_text >> link_href }
  rule(:non_link)  { (link.absent? >> any).repeat }
  rule(:content)   { (non_link >> link >> non_link).repeat }

  root(:content)
end

“Foul,” you cry, “this is much more complicated than a regular expression!” And I reply “Yes, but it is also more intelligible long-term as you build upon it.” You don’t look completely satisfied, but you’ll continue reading.

It is worth noting that everything has a name:

  • link_text encompasses everything between the two brackets in the markdown link.
  • link_href is the content within the parens. Because we are specifically linking only to anchors, we also include the # and then we’ll name the id we’re linking to via as.
  • link is just link_text + link_href
  • non_link is anything that isn’t a link. It could be other markdown or plain text. It may or may not actually contain any characters at all.
  • content is the whole markdown content. We can see it is made up of some number of the following: non_link + link + non_link

We’ve specified that “content” is our root so the parser starts there.

The Scratch: Adding the 3 missing features

Now we have an easy way to extract links from sections within a story. We’ll be able to leverage this to map the branches and solve all three problems.

But in order to break the larger story into sections we’ll need to write a StoryParser which can parse an entire story file (for an example file, see the previous post). Again, this was TDD’ed, but we’ll cut to the chase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StoryParser < Parslet::Parser
  rule(:space) { match('\s').repeat }
  rule(:newline) { match('\n') }

  rule(:heading) { match('^#') >> space.maybe >> (match['\n{'].absent? >> any).repeat.as(:heading) >> id.maybe }
  rule(:id)      { str('{#') >> (str('}').absent? >> any).repeat.as(:id) >> str('}') }
  rule(:content) { ((id | heading).absent? >> any).repeat }
  rule(:section) { (heading >> space.maybe >> content.as(:content) >> space.maybe).as(:section) }

  rule(:tile_block) { (str('%') >> (newline.absent? >> any).repeat >> newline).repeat }

  rule(:story) { space.maybe >> tile_block.maybe >> space.maybe >> section.repeat }

  root(:story)
end

Now we can parse out each section’s heading text, id, and content into a tree that looks something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
[
  {:section=>{
    :heading=>"Something isn't right here. "@51,
    :id=>"intro"@81,
    :content=>"You hear a phone ringing.\n\n- [pick up phone](#phone)..."@89}
  },
  {:section=>{
    :heading=>"You pick up the phone... "@210,
    :id=>"phone"@237,
    :content=>"It is your grandmother. You die.\n\n- [start over](#intro)"@245}
  },
  ...
]

“That’s well and good,” you say, “but how do we turn that into something useful?”

Enter Parslet’s Transform class (and exit your remaining skepticism). Parslet::Transform takes a tree and lets you convert it into whatever you want. The following code takes a section tree from above, cleans up some whitespace, and then returns an instantiated Section class based on the input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SectionTransformer < Parslet::Transform
  rule(section: subtree(:hash)) {
    hash[:content] = hash[:content].to_s.strip
    hash[:heading] = hash[:heading].to_s.strip

    if hash[:id].to_s.empty?
      hash.delete(:id)
    else
      hash[:id] = hash[:id].to_s
    end

    Section.new(hash)
  }
end

Example of an instantiated Section:

1
2
3
4
5
6
p SectionTransformer.new.apply(tree[0])
# <Section:0x007fd6e5853298
#  @content="You hear a phone ringing.\n\n- [pick up phone](#phone)\n- [do not answer](#ignore-phone)\n- [set yourself on fire](#fire)",
#  @heading="Something isn't right here.",
#  @id="intro",
#  @links=["phone", "ignore-phone", "fire"]>

So now we have the building blocks for parsing a story into sections and then our Section class internally uses the LinkParser from above to determine where the section branches outward.

Let’s finish this by encapsulating the entire story in a Story class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Story
  attr_reader :sections

  def initialize(file)
    @sections = parse_file(file)
  end

  def branches
    @_branches ||= BranchCruncher.new(@sections).traverse
  end

  def reachable
    branches.flatten.uniq
  end

  def unreachable
    @sections.map(&:id) - reachable
  end

  def split!(path)
    branches.each do |branch|
      File.open(path + branch.join('-') + '.md', 'w') do |f|
        branch.each do |id|
          section = sections.detect{|s| s.id == id}
          f.puts "# #{section.heading} {##{section.id}}\n"
          f.puts section.content
          f.puts "\n\n"
        end
      end
    end
  end

  private

  def parse_file(file)
    SectionTransformer.new.apply(StoryParser.new.parse(file.read))
  end
end

A few notes:

  • You instantiate the Story class with a File object pointing to your story.
  • It parses out the sections
  • Then you can call methods to fill in the missing pieces of functionality we identified at the beginning of this post.
1
2
3
4
5
6
7
8
9
10
11
12
13
# Which sections are orphaned?
p story.unreachable
# => ['some-unreachable-page-id']

# What branches are there in the book?
p story.branches
# => [ ["intro", "investigate", "help"], ["intro", "investigate", "rescue", "wake-up"], ["intro", "investigate", "grounded"], ["intro", "grounded"] ]

# Let me read each narrative branch by splitting each branch into files
story.split!('/tmp/')
# creates files in /tmp/ folder named for each section in a branch
# e.g. intro-investigate-help.md
# You can read through each branch and ensure you've maintained a cohesive narrative.

If you made it this far, you deserve a cookie and my undying affection. I’m all out of cookies and any I had would be gluten-free anyway, so how about I just link you to the example code instead and we call it even?

Here’s the cyoa-parser on github. It includes a hilariously bad speed-story I wrote for my son when he insisted on a CYOA bedtime story 10 minutes before bed.

If you’d like to learn more about Parslet from someone who knows it better than me, check out Jason Garber’s Wicked Good Ruby talk

Writing Hypertext Fiction in Markdown

Remember Choose Your Own Adventure books? I fondly remember finding new ways to get myself killed as I explored Aztec ruins or fought off aliens. Death or adventure waited just a few pages away and I was the one calling all the shots.

Introducing my son to Hypertext Fiction has rekindled my interest. I wondered how difficult it would be to throw something together to let me easily write CYOA-style books my kid could read on a kindle. I love markdown, so a toolchain built around it was definitely in order.

As it turns out, Pandoc fits the bill perfectly. You can write a story in markdown and easily export it to EPUB. From there you’re just a quick step through ebook-convert (via calibre’s commandline tools) to a well-formed .mobi file that reads beautifully on a kindle.

Here’s a quick example markdown story:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
% You're probably going to die.
% Jeffrey Chupp

# Something isn't right here. {#intro}

You hear a phone ringing.

- [pick up phone](#phone)
- [do not answer](#ignore-phone)
- [set yourself on fire](#fire)

# You pick up the phone... {#phone}

It is your grandmother. You die.

- [start over](#intro)

# You ignore the phone... {#ignore-phone}

It was your grandmother. You die.

- [start over](#intro)

# You set yourself on fire... {#fire}

Strangely, you don't die. Guess you better start getting ready for school.

- [pick up backpack and head out](#backpack)
- [decide to skip school](#skip)

# You decide to skip school {#skip}

A wild herd of dinosaurs bust in and kill you. Guess you'll never get to tell your friends about how you're immune to flame... or that you met live dinosaurs :(

- [start over](#intro)

# Going to school {#backpack}

You're on your way to school when a meteor lands on you, killing you instantly.

- [start over](#intro)

From the top, we have percent signs before the title and publishing date which Pandoc uses for the title page.

Then each chapter/section begins with an h1 header which has an id specified. This id is what we’ll use in our links to let a reader choose where to go next.

If you don’t specify a link, Pandoc will dasherize your header text, but it is probably easier to be specific since you need to reference it in your link choices anyway.

Save that as story.md and run the following to get your epub and mobi versions:

pandoc -o story.epub story.md && /usr/bin/ebook-convert story.epub story.mobi

BONUS: ebook-convert even complains if one of your links points to an invalid destination.

Here’s a preview as seen in Kindle Previewer

And here are the generated EPUB and .mobi files and the markdown source file.

Now, get writing!

A Proper API Proxy Written in Go

A little over a month ago, I blogged about a API proxy written in Go. This post contained a functioning but incredibly naive (not to mention unidiomatic) piece of Go code intended to allow proxying API requests while hiding your API keys. Here’s an updated version that makes better use of the Go standard library and works using layers like Ruby’s middleware (for more on this topic, see the excellent article here). It also improves upon the original in that it will work with all HTTP verbs.

When writing the first version, I tried using httputil.NewSingleHostReverseProxy since the name sounds like exactly what I was trying to do. There was an important piece missing by default, though, which made the library seem mysteriously broken. Being a newbie in a hurry, I went with the solution you can see in the previous post.

What was missing? httputil.NewSingleHostReverseProxy does not set the host of the request to the host of the destination server. If you’re proxying from foo.com to bar.com, requests will arrive at bar.com with the host of foo.com. Many webservers are configured to not serve pages if a request doesn’t appear from the same host.

Fortunately it isn’t too complicated to modify the chain to tweak the host.

1
2
3
4
5
6
func sameHost(handler http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      r.Host = r.URL.Host
      handler.ServeHTTP(w, r)
  })
}

And the usage:

1
2
3
4
5
// initialize our reverse proxy
reverseProxy := httputil.NewSingleHostReverseProxy(serverUrl)
// wrap that proxy with our sameHost function
singleHosted := sameHost(reverseProxy)
http.ListenAndServe(":5000", singleHosted)

Perfect. We’re now setting the host of the request to the host of the destination URL.

Continuing with this approach, let’s combine our secret query params with the existing request query.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func queryCombiner(handler http.Handler, addon string) http.Handler {
  // first parse the provided string to pull out the keys and values
  values, err := url.ParseQuery(addon)
  if err != nil {
      log.Fatal("addon failed to parse")
  }

  // now we apply our addon params to the existing query
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      query := r.URL.Query()

      for k, _ := range values {
          query.Add(k, values.Get(k))
      }

      r.URL.RawQuery = query.Encode()
      handler.ServeHTTP(w, r)
  })
}

And usage is similar to above. We just continue to chain together our handlers.

1
combined := queryCombiner(singleHosted, "key=value&name=bob")

Finally, we’ll need to allow CORS on our server.

1
2
3
4
5
6
7
func addCORS(handler http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      w.Header().Set("Access-Control-Allow-Origin", "*")
      w.Header().Set("Access-Control-Allow-Headers", "X-Requested-With")
      handler.ServeHTTP(w, r)
  })
}

And add that to our chain

1
2
cors := addCORS(combined)
http.ListenAndServe(":5000", cors)

The code is available on github and it runs quite well with the heroku go buildpack.

It has a couple tests. I should add some more, but I’m not totally happy with the current testing approach. Feedback is very welcome.

apis, go

A Simple API Proxy Written in Go

UPATE: see “A proper API proxy written in Go” for a better solution to this problem.

The problem:

Have you ever written a javascript app that needed to consume an API? What if the API requires you to pass your api key along in the query params? How do you hide your key?

This weekend I bumped into this issue once again. I was writing a simple app in angular to consume the last.fm api when it hit me.

This usually leaves me with two options:

  1. Decide my api key isn’t worth hiding and just embed it in the javascript.
  2. Make a call to the app server (I’m usually using Rails) that would then make the API call within the request lifecycle and return the json when the API call finishes.

Option 1 is also known as “giving up” – you don’t really want everyone to have your api key, do you? What happens when someone else starts using it to do nefarious things on your behalf or just decides to help you hit your rate limit faster?

Option 2 is safer, but now your poor app server pays the penalty of the API being slow. If the API call takes 3 seconds, your server process/thread is tied up for that time. Lame.

Imagine your rails app is built around an external API. Do you really want to spin up more and more instances to gain concurrency just to protect your key?

The solution: Move things out-of-band

For requests that could otherwise hit the api directly, your app server shouldn’t pay the penalties of keeping your key secure. So let’s move things out-of-band.

I’d been meaning to play with Go for some time but never had the right project. The implementation here was fairly simple but needed to be highly concurrent, so this felt like a good fit.

Borrowing from example Go http servers and http consumers, I came up with this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
  "fmt"
  "io/ioutil"
  "net/http"
  "os"
)

func errorOut(err error) {
  fmt.Printf("%s", err)
  os.Exit(1)
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Access-Control-Allow-Origin", "*")
  w.Header().Set("Access-Control-Allow-Headers", "X-Requested-With")

  if r.Method == "GET" {
      var newUrl string = os.Getenv("URL_ROOT") + r.URL.Path[1:] + "?" +
        r.URL.RawQuery + os.Getenv("URL_SUFFIX")

      fmt.Printf("fetching %s\n", newUrl)

      response, err := http.Get(newUrl)
      if err != nil {
          errorOut(err)
      } else {
          defer response.Body.Close()
          contents, err := ioutil.ReadAll(response.Body)
          if err != nil {
              errorOut(err)
          }
          fmt.Fprintf(w, "%s\n", contents)
      }
  }
}

The server takes incoming requests and will translate the url by substituting the provided URL_ROOT and appending the URL_SUFFIX (the api key). It fetches that foreign url and then returns the results.

So with the example config:

URL_ROOT=http://ws.audioscrobbler.com/2.0/ URL_SUFFIX=&api_key=XXXXXXXXXXXXX

A request to the go server at http://example.com/?method=user.getrecenttracks&user=violencenow&format=json would return the contents of http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=violencenow&format=json&api_key=XXXXXXXXXXXXX

This isn’t a solution for everything. Right now it only supports GET requests – this is probably all you’d ever want, lest someone start posting to your endpoint and doing things you don’t expect. These sorts of potentially destructive behaviors are perhaps better handled in-band where you can apply some sanity checks.

But if all you need to do is get content from an API without exposing your keys to the public, this might be a good solution for you.

Some numbers

This is very unscientific, but I setup a Go server on heroku http://sleepy-server.herokuapp.com/ that takes a request, waits 1 second, and then returns plain text.

The benchmark for that with ab -c 300 -n 600 "http://sleepy-server.herokuapp.com/"

Concurrency Level:      300
Time taken for tests:   5.046 seconds
Complete requests:      600
Failed requests:        0
Write errors:           0
Total transferred:      83400 bytes
HTML transferred:       2400 bytes
Requests per second:    118.91 [#/sec] (mean)
Time per request:       2522.907 [ms] (mean)
Time per request:       8.410 [ms] (mean, across all concurrent requests)
Transfer rate:          16.14 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       28  322 534.7    107    2257
Processing:  1040 1229 223.1   1148    2640
Waiting:     1038 1228 223.0   1148    2640
Total:       1069 1552 587.1   1309    3867

Now, let’s use our api_proxy to fetch requests from that server and serve them up by setting URL_ROOT=http://sleepy-server.herokuapp.com.

And we’ll use the same benchmark command: ab -c 300 -n 600 "http://some-fake-server-name-here.herokuapp.com/"

Concurrency Level:      300
Time taken for tests:   5.285 seconds
Complete requests:      600
Failed requests:        0
Write errors:           0
Total transferred:      132000 bytes
HTML transferred:       3000 bytes
Requests per second:    113.54 [#/sec] (mean)
Time per request:       2642.282 [ms] (mean)
Time per request:       8.808 [ms] (mean, across all concurrent requests)
Transfer rate:          24.39 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       28  324 550.9     75    2260
Processing:  1049 1406 325.2   1333    3012
Waiting:     1049 1405 325.1   1331    3012
Total:       1085 1730 609.4   1644    3875

Scientific or not, that’s performance I can live with. And hopefully those API endpoints aren’t quite taking a full second per request.

Unicorn Pukes Serving Large Files

Earlier today I was getting this weird unicorn error on heroku when trying to serve a retina-sized image.

ERROR -- : app error: undefined method `each' for nil:NilClass (NoMethodError)
ERROR -- : [..]/unicorn-4.6.3/lib/unicorn/http_response.rb:60:in `http_response_write'
ERROR -- : [..]/unicorn-4.6.3/lib/unicorn/http_server.rb:563:in `process_client'
ERROR -- : [..]/unicorn-4.6.3/lib/unicorn/http_server.rb:633:in `worker_loop'
ERROR -- : [..]/unicorn-4.6.3/lib/unicorn/http_server.rb:500:in `spawn_missing_workers'
ERROR -- : [..]/unicorn-4.6.3/lib/unicorn/http_server.rb:142:in `start'
ERROR -- : [..]/unicorn-4.6.3/bin/unicorn_rails:209:in `<top (required)>'

Weird, right? But sure enough, whenever I tried to view some-image@2x.png, everything went terribly wrong.

Googling took too long to find an answer, so I’m sharing my solution here in hopes that it helps someone else (oh, hai, google bot).

The issue is actually a bug in the version of rack-cache required by actionpack in Rails 3.2.14. Attempting to serve files larger than 1mb causes this error.

It has been fixed, but I had to require the master branch for rack-cache to resolve the problem.

Gemfile
1
2
gem "rack-cache", github: "rtomayko/rack-cache"
gem "unicorn"

No more error.

Now, the real solution is to not serve large images through unicorn on heroku. But hooking up a CDN is another problem for another time.

Letterpress Word Finder

In an attempt to start to blog more, here’s a quick follow-up post on the previous Letterpress article.

Background

As a reminder, here’s how I outlined steps in creating a Letterpress solver:

  1. Take screenshot of game and import it into solver
  2. Parse the board into a string of letters
  3. Reduce a dictionary of valid words against those characters to find playable words
  4. Optionally make recommendations of which word to play based on current board state and strategy. (i.e. don’t be naive)

We built step one (sort-of) and step two in the previous article, so let’s move on to step three.

Requirements

We want our script to fulfill the following requirements:

  1. Accept the board letters via STDIN or commandline arguments.
  2. Reduce the dictionary words against those letters.
  3. Dump out matching words (without regard to board state/strategy).

Implementation

We’ll take either an argument or read STDIN and downcase it.

1
letters = (ARGV[0] || STDIN.read).downcase

I don’t have the official Letterpress dictionary (a quick googling will get you on the right track if you insist), but every good unix-y system has a dictionary file.

$ cat /usr/share/dict/words | wc -l
235886

OK, that’s a lot of words. Let’s pull them in and downcase them too.

1
words = File.read("/usr/share/dict/words").downcase.split("\n")

Now, the only really interesting part: a method to determine if a word can be constructed from letters. I’ve shamelessly borrowed a perfectly fast solution from Stackoverflow.

1
2
3
def is_subset?(word, letters)
  !word.chars.find{|char| word.count(char) > letters.count(char)}
end

And now we reduce our words by those that match our letters

1
2
3
matching_words = words.select do |word|
  is_subset?(word, letters)
end

And there’s nothing left to do but dump them out.

1
puts matching_words.sort_by(&:length)

Here’s the entire word generating script.

And an example of using it with the board parser from the previous post:

$ ruby -r ./board_parser -e "puts BoardParser.new('light.png').tiles.join" | ruby letter.rb | tail -n 10
hermodactyl
typhlectomy
cryohydrate
polydactyle
pterodactyl
crymotherapy
hydrolyzable
acetylthymol
overthwartly
protractedly

Excellent. Of course, not all words in your system’s dictionary file may be playable, YMMV, etc.

Quick and Dirty OCR for Letterpress & Other Tile-based Games

I’ve been playing enough Letterpress lately to realize that I’m not great at it. This is super frustrating for me when this is a game that you could easily teach a computer to play.

I’m not the first person to have that thought. There are plenty of cheating programs for Letterpress (just google or search in the app store).

I haven’t investigated these solvers but in thinking about the problem, the basic approach would seem to be:

  • Take screenshot of game and import it into solver
  • Parse the board into a string of letters
  • Reduce a dictionary of valid words against those characters to find playable words
  • Optionally make recommendations of which word to play based on current board state and strategy.

I wondered how quickly I could throw something together to simply parse the game board into a string of letters. It turns out it is super easy. To get started I took a screenshot of a game in progress and downloaded it from my phone.

Read on →

Flashing Sprites Aren’t as Sexy as They Sound

My skills have traditionally skewed towards the back-end so I’m still finding lots of new tricks in front-end development that are new to me. Here’s a quick example…

I recently created a button with Compass sprites that had images for the default state, hover state, and active (pressed) state.

Here’s the gist of what I was doing:

special_button.sass
1
2
3
4
5
6
7
8
9
10
$button_sprites: sprite-map('/special-button/*.png')

.special-button
  background: sprite($button_sprites, "button-default")

  &:hover
    background: sprite($button_sprites, "button-hover")

  &:active
    background: sprite($button_sprites, "button-active")

which generates this css:

special_button.css
1
2
3
4
5
6
7
8
9
.special-button {
  background: url(/assets/special-button-sb6a3aa70c5.png) 0 -204px
}
.special-button:hover {
  background: url(/assets/special-button-sb6a3aa70c5.png) 0 -230px;
}
.special-button:active {
  background: url(/assets/special-button-sb6a3aa70c5.png) 0 -126px;
}

Pretty straight-forward, right? For each state I specify which of the sprite sub-images it should show.

Unfortunately, I was bumping into an issue in Chrome where the button would sometimes flash between image states on hover and active.

After a bit of googling, I found an answer. The problem is that I was specifying the URL redundantly, causing Chrome to show no background while it briefly loads that same sprite again to show it at the new position. After a little digging in the Compass docs, I found sprite-position and tweaked accordingly:

special_button_fixed.sass
1
2
3
4
5
6
7
8
9
10
$button_sprites: sprite-map('/special-button/*.png')

.special-button
  background: sprite($button_sprites, "button-default")

  &:hover
    background-position: sprite-position($button_sprites, "button-hover")

  &:active
    background-position: sprite-position($button_sprites, "button-active")

Now we’re using the same sprite specified in the original .special_button class and just changing the position in the other states. Our flashing problem is gone.