css_parser 0.9.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/css_parser.rb +1 -1
- data/lib/css_parser/parser.rb +1 -1
- data/lib/css_parser/regexps.rb +4 -4
- data/lib/css_parser/rule_set.rb +28 -17
- data/test/test_css_parser_regexps.rb +1 -0
- data/test/test_helper.rb +8 -8
- data/test/test_rule_set.rb +14 -4
- metadata +49 -45
- data/CHANGELOG +0 -13
- data/LICENSE +0 -21
- data/README +0 -58
data/lib/css_parser.rb
CHANGED
data/lib/css_parser/parser.rb
CHANGED
@@ -15,7 +15,7 @@ module CssParser
|
|
15
15
|
# [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
|
16
16
|
# [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
|
17
17
|
class Parser
|
18
|
-
USER_AGENT = "Ruby CSS Parser/#{
|
18
|
+
USER_AGENT = "Ruby CSS Parser/#{RUBY_VERSION} (http://code.dunae.ca/css_parser/)"
|
19
19
|
|
20
20
|
STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
|
21
21
|
STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
|
data/lib/css_parser/regexps.rb
CHANGED
@@ -2,17 +2,17 @@ module CssParser
|
|
2
2
|
# :stopdoc:
|
3
3
|
# Base types
|
4
4
|
RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
|
5
|
-
RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE) #[^\0-\177]
|
6
|
-
RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE)
|
5
|
+
RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE, 'n') #[^\0-\177]
|
6
|
+
RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
|
7
7
|
RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
|
8
|
-
RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE)
|
8
|
+
RE_IDENT = Regexp.new("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE, 'n')
|
9
9
|
|
10
10
|
# General strings
|
11
11
|
RE_STRING1 = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
|
12
12
|
RE_STRING2 = Regexp.new('(\'(.[^\n\r\f\\\']*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\')')
|
13
13
|
RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)
|
14
14
|
|
15
|
-
RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE)
|
15
|
+
RE_URI = Regexp.new('(url\([\s]*([\s]*' + RE_STRING.to_s + '[\s]*)[\s]*\))|(url\([\s]*([!#$%&*\-~]|' + RE_NON_ASCII.to_s + '|' + RE_ESCAPE.to_s + ')*[\s]*)\)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE, 'n')
|
16
16
|
URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im
|
17
17
|
|
18
18
|
# Initial parsing
|
data/lib/css_parser/rule_set.rb
CHANGED
@@ -14,6 +14,7 @@ module CssParser
|
|
14
14
|
@selectors = []
|
15
15
|
@specificity = specificity
|
16
16
|
@declarations = {}
|
17
|
+
@order = 0
|
17
18
|
parse_selectors!(selectors) if selectors
|
18
19
|
parse_declarations!(block)
|
19
20
|
end
|
@@ -48,11 +49,18 @@ module CssParser
|
|
48
49
|
#
|
49
50
|
# If the property already exists its value will be over-written.
|
50
51
|
def add_declaration!(property, value)
|
52
|
+
if value.nil? or value.empty?
|
53
|
+
@declarations.delete(property)
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
51
57
|
value.gsub!(/;\Z/, '')
|
52
58
|
is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
|
53
59
|
property = property.downcase.strip
|
54
60
|
#puts "SAVING #{property} #{value} #{is_important.inspect}"
|
55
|
-
@declarations[property] = {
|
61
|
+
@declarations[property] = {
|
62
|
+
:value => value, :is_important => is_important, :order => @order += 1
|
63
|
+
}
|
56
64
|
end
|
57
65
|
alias_method :[]=, :add_declaration!
|
58
66
|
|
@@ -76,7 +84,8 @@ module CssParser
|
|
76
84
|
|
77
85
|
# Iterate through declarations.
|
78
86
|
def each_declaration # :yields: property, value, is_important
|
79
|
-
@declarations.
|
87
|
+
decs = @declarations.sort { |a,b| a[1][:order] <=> b[1][:order] }
|
88
|
+
decs.each do |property, data|
|
80
89
|
value = data[:value]
|
81
90
|
#value += ' !important' if data[:is_important]
|
82
91
|
yield property.downcase.strip, value.strip, data[:is_important]
|
@@ -84,13 +93,14 @@ module CssParser
|
|
84
93
|
end
|
85
94
|
|
86
95
|
# Return all declarations as a string.
|
96
|
+
#--
|
97
|
+
# TODO: Clean-up regexp doesn't seem to work
|
98
|
+
#++
|
87
99
|
def declarations_to_s(options = {})
|
88
100
|
options = {:force_important => false}.merge(options)
|
89
101
|
str = ''
|
90
|
-
|
91
|
-
|
92
|
-
str += "#{prop}: #{val}#{importance}; "
|
93
|
-
end
|
102
|
+
importance = options[:force_important] ? ' !important' : ''
|
103
|
+
each_declaration { |prop, val| str += "#{prop}: #{val}#{importance}; " }
|
94
104
|
str.gsub(/^[\s]+|[\n\r\f\t]*|[\s]+$/mx, '').strip
|
95
105
|
end
|
96
106
|
|
@@ -138,6 +148,7 @@ private
|
|
138
148
|
@selectors = selectors.split(',')
|
139
149
|
end
|
140
150
|
|
151
|
+
public
|
141
152
|
# Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
|
142
153
|
# into their constituent parts.
|
143
154
|
def expand_dimensions_shorthand! # :nodoc:
|
@@ -147,6 +158,7 @@ private
|
|
147
158
|
|
148
159
|
value = @declarations[property][:value]
|
149
160
|
is_important = @declarations[property][:is_important]
|
161
|
+
order = @declarations[property][:order]
|
150
162
|
t, r, b, l = nil
|
151
163
|
|
152
164
|
matches = value.scan(CssParser::BOX_MODEL_UNITS_RX)
|
@@ -168,10 +180,11 @@ private
|
|
168
180
|
l = matches[3][0]
|
169
181
|
end
|
170
182
|
|
171
|
-
|
172
|
-
@declarations["#{property}-
|
173
|
-
@declarations["#{property}-
|
174
|
-
@declarations["#{property}-
|
183
|
+
values = { :is_important => is_important, :order => order }
|
184
|
+
@declarations["#{property}-top"] = values.merge(:value => t.to_s)
|
185
|
+
@declarations["#{property}-right"] = values.merge(:value => r.to_s)
|
186
|
+
@declarations["#{property}-bottom"] = values.merge(:value => b.to_s)
|
187
|
+
@declarations["#{property}-left"] = values.merge(:value => l.to_s)
|
175
188
|
@declarations.delete(property)
|
176
189
|
end
|
177
190
|
end
|
@@ -191,6 +204,7 @@ private
|
|
191
204
|
|
192
205
|
value = @declarations['font'][:value]
|
193
206
|
is_important = @declarations['font'][:is_important]
|
207
|
+
order = @declarations['font'][:order]
|
194
208
|
|
195
209
|
in_fonts = false
|
196
210
|
|
@@ -225,7 +239,7 @@ private
|
|
225
239
|
end
|
226
240
|
end
|
227
241
|
|
228
|
-
font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important} }
|
242
|
+
font_props.each { |font_prop, font_val| @declarations[font_prop] = {:value => font_val, :is_important => is_important, :order => order} }
|
229
243
|
|
230
244
|
@declarations.delete('font')
|
231
245
|
end
|
@@ -240,6 +254,7 @@ private
|
|
240
254
|
|
241
255
|
value = @declarations['background'][:value]
|
242
256
|
is_important = @declarations['background'][:is_important]
|
257
|
+
order = @declarations['background'][:order]
|
243
258
|
|
244
259
|
bg_props = {}
|
245
260
|
|
@@ -276,7 +291,7 @@ private
|
|
276
291
|
end
|
277
292
|
end
|
278
293
|
|
279
|
-
bg_props.each { |bg_prop, bg_val| @declarations[bg_prop] = {:value => bg_val, :is_important => is_important} }
|
294
|
+
bg_props.each { |bg_prop, bg_val| @declarations[bg_prop] = {:value => bg_val, :is_important => is_important, :order => order} }
|
280
295
|
|
281
296
|
@declarations.delete('background')
|
282
297
|
end
|
@@ -372,9 +387,5 @@ private
|
|
372
387
|
end
|
373
388
|
|
374
389
|
end
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
390
|
end
|
380
|
-
end
|
391
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../'))
|
2
|
-
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../lib/'))
|
3
|
-
require 'rubygems'
|
4
|
-
require 'test/unit'
|
5
|
-
require 'css_parser'
|
6
|
-
require 'net/http'
|
7
|
-
require 'open-uri'
|
8
|
-
require 'WEBrick'
|
1
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../'))
|
2
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__), '../lib/'))
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'css_parser'
|
6
|
+
require 'net/http'
|
7
|
+
require 'open-uri'
|
8
|
+
require 'WEBrick'
|
data/test/test_rule_set.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
require "set"
|
2
3
|
|
3
4
|
# Test cases for parsing CSS blocks
|
4
5
|
class RuleSetTests < Test::Unit::TestCase
|
@@ -32,7 +33,7 @@ class RuleSetTests < Test::Unit::TestCase
|
|
32
33
|
expected = [
|
33
34
|
{:selector => "#content p", :declarations => "color: #fff;", :specificity => 101},
|
34
35
|
{:selector => "a", :declarations => "color: #fff;", :specificity => 1}
|
35
|
-
]
|
36
|
+
]
|
36
37
|
|
37
38
|
actual = []
|
38
39
|
rs = RuleSet.new('#content p, a', 'color: #fff;')
|
@@ -44,13 +45,13 @@ class RuleSetTests < Test::Unit::TestCase
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def test_each_declaration
|
47
|
-
expected = [
|
48
|
+
expected = Set.new([
|
48
49
|
{:property => 'margin', :value => '1px -0.25em', :is_important => false},
|
49
50
|
{:property => 'background', :value => 'white none no-repeat', :is_important => true},
|
50
51
|
{:property => 'color', :value => '#fff', :is_important => false}
|
51
|
-
]
|
52
|
+
])
|
52
53
|
|
53
|
-
actual =
|
54
|
+
actual = Set.new
|
54
55
|
rs = RuleSet.new(nil, 'color: #fff; Background: white none no-repeat !important; margin: 1px -0.25em;')
|
55
56
|
rs.each_declaration do |prop, val, imp|
|
56
57
|
actual << {:property => prop, :value => val, :is_important => imp}
|
@@ -59,6 +60,15 @@ class RuleSetTests < Test::Unit::TestCase
|
|
59
60
|
assert_equal(expected, actual)
|
60
61
|
end
|
61
62
|
|
63
|
+
def test_each_declaration_respects_order
|
64
|
+
css_fragment = "margin: 0; padding: 20px; margin-bottom: 28px;"
|
65
|
+
rs = RuleSet.new(nil, css_fragment)
|
66
|
+
expected = %w(margin padding margin-bottom)
|
67
|
+
actual = []
|
68
|
+
rs.each_declaration { |prop, val, imp| actual << prop }
|
69
|
+
assert_equal(expected, actual)
|
70
|
+
end
|
71
|
+
|
62
72
|
def test_declarations_to_s
|
63
73
|
declarations = 'color: #fff; font-weight: bold;'
|
64
74
|
rs = RuleSet.new('#content p, a', declarations)
|
metadata
CHANGED
@@ -1,44 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.9.2
|
3
|
-
specification_version: 1
|
4
2
|
name: css_parser
|
5
3
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2008-11-12 00:00:00 -08:00
|
8
|
-
summary: A set of classes for parsing CSS.
|
9
|
-
require_paths:
|
10
|
-
- lib
|
11
|
-
email:
|
12
|
-
homepage: http://code.dunae.ca/css_parser
|
13
|
-
rubyforge_project:
|
14
|
-
description:
|
15
|
-
autorequire:
|
16
|
-
default_executable:
|
17
|
-
bindir: bin
|
18
|
-
has_rdoc: true
|
19
|
-
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
-
requirements:
|
21
|
-
- - ">"
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version: 0.0.0
|
24
|
-
version:
|
4
|
+
version: 1.0.0
|
25
5
|
platform: ruby
|
26
|
-
signing_key:
|
27
|
-
cert_chain:
|
28
|
-
post_install_message:
|
29
6
|
authors:
|
30
7
|
- Alex Dunae
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-21 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A set of classes for parsing CSS in Ruby.
|
17
|
+
email: code@dunae.ca
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
31
24
|
files:
|
32
25
|
- lib/css_parser.rb
|
33
26
|
- lib/css_parser/parser.rb
|
34
27
|
- lib/css_parser/regexps.rb
|
35
28
|
- lib/css_parser/rule_set.rb
|
36
|
-
- test/fixtures
|
37
29
|
- test/fixtures/import-circular-reference.css
|
38
30
|
- test/fixtures/import-with-media-types.css
|
39
31
|
- test/fixtures/import1.css
|
40
32
|
- test/fixtures/simple.css
|
41
|
-
- test/fixtures/subdir
|
42
33
|
- test/fixtures/subdir/import2.css
|
43
34
|
- test/test_css_parser_basic.rb
|
44
35
|
- test/test_css_parser_downloading.rb
|
@@ -50,9 +41,38 @@ files:
|
|
50
41
|
- test/test_rule_set.rb
|
51
42
|
- test/test_rule_set_creating_shorthand.rb
|
52
43
|
- test/test_rule_set_expanding_shorthand.rb
|
53
|
-
|
54
|
-
|
55
|
-
|
44
|
+
has_rdoc: true
|
45
|
+
homepage: http://github.com/alexdunae/css_parser
|
46
|
+
licenses: []
|
47
|
+
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options:
|
50
|
+
- --all
|
51
|
+
- --inline-source
|
52
|
+
- --line-numbers
|
53
|
+
- --charset
|
54
|
+
- utf-8
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.5
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Ruby CSS parser.
|
56
76
|
test_files:
|
57
77
|
- test/test_css_parser_basic.rb
|
58
78
|
- test/test_css_parser_downloading.rb
|
@@ -64,19 +84,3 @@ test_files:
|
|
64
84
|
- test/test_rule_set.rb
|
65
85
|
- test/test_rule_set_creating_shorthand.rb
|
66
86
|
- test/test_rule_set_expanding_shorthand.rb
|
67
|
-
rdoc_options:
|
68
|
-
- --all
|
69
|
-
- --inline-source
|
70
|
-
- --line-numbers
|
71
|
-
extra_rdoc_files:
|
72
|
-
- README
|
73
|
-
- CHANGELOG
|
74
|
-
- LICENSE
|
75
|
-
executables: []
|
76
|
-
|
77
|
-
extensions: []
|
78
|
-
|
79
|
-
requirements: []
|
80
|
-
|
81
|
-
dependencies: []
|
82
|
-
|
data/CHANGELOG
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
=== Ruby CSS Parser CHANGELOG
|
2
|
-
|
3
|
-
==== Version 0.9.1
|
4
|
-
* Fixed RuleSet#declaration_to_s so it would respect <tt>!important</tt>
|
5
|
-
rules (thanks to Dana - http://github.com/DanaDanger)
|
6
|
-
|
7
|
-
==== Version 0.9
|
8
|
-
* Initial version forked from Premailer project
|
9
|
-
|
10
|
-
==== TODO: Future
|
11
|
-
* border shorthand/folding support
|
12
|
-
* re-implement caching on CssParser.merge
|
13
|
-
* correctly parse http://www.webstandards.org/files/acid2/test.html
|
data/LICENSE
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
=== Ruby CSS Parser License
|
2
|
-
|
3
|
-
Copyright (c) 2007 Alex Dunae
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
13
|
-
all copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
-
THE SOFTWARE.
|
data/README
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
=== Ruby CSS Parser
|
2
|
-
|
3
|
-
Load, parse and cascade CSS rule sets in Ruby.
|
4
|
-
|
5
|
-
==== Setup
|
6
|
-
|
7
|
-
Install the gem from RubyGems.
|
8
|
-
|
9
|
-
gem install css_parser
|
10
|
-
|
11
|
-
Done.
|
12
|
-
|
13
|
-
==== An example
|
14
|
-
require 'css_parser'
|
15
|
-
include CssParser
|
16
|
-
|
17
|
-
parser = CssParser::Parser.new
|
18
|
-
parser.load_file!('http://example.com/styles/style.css')
|
19
|
-
|
20
|
-
# lookup a rule by a selector
|
21
|
-
parser.find('#content')
|
22
|
-
#=> 'font-size: 13px; line-height: 1.2;'
|
23
|
-
|
24
|
-
# lookup a rule by a selector and media type
|
25
|
-
parser.find('#content', [:screen, :handheld])
|
26
|
-
|
27
|
-
# iterate through selectors by media type
|
28
|
-
parser.each_selector(:screen) do |selector, declarations, specificity|
|
29
|
-
...
|
30
|
-
end
|
31
|
-
|
32
|
-
# add a block of CSS
|
33
|
-
css = <<-EOT
|
34
|
-
body { margin: 0 1em; }
|
35
|
-
EOT
|
36
|
-
|
37
|
-
parser.add_block!(css)
|
38
|
-
|
39
|
-
# output all CSS rules in a single stylesheet
|
40
|
-
parser.to_s
|
41
|
-
=> #content { font-size: 13px; line-height: 1.2; }
|
42
|
-
body { margin: 0 1em; }
|
43
|
-
|
44
|
-
==== Testing
|
45
|
-
|
46
|
-
You can run the suite of unit tests using <tt>rake test</tt>.
|
47
|
-
|
48
|
-
The download/import tests require that WEBrick is installed. The tests set up
|
49
|
-
a temporary server on port 12000 and pull down files from the <tt>test/fixtures/</tt>
|
50
|
-
directory.
|
51
|
-
|
52
|
-
==== Credits and code
|
53
|
-
|
54
|
-
By Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-08.
|
55
|
-
|
56
|
-
Thanks to Dana (http://github.com/DanaDanger) for the 0.9.1 update.
|
57
|
-
|
58
|
-
Made on Vancouver Island.
|