images_gallery 1.0.0.rc

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +674 -0
  4. data/README.md +84 -0
  5. data/Rakefile +53 -0
  6. data/bin/images_gallery +6 -0
  7. data/config.ru +8 -0
  8. data/lib/images_gallery/cli.rb +27 -0
  9. data/lib/images_gallery/collection.rb +15 -0
  10. data/lib/images_gallery/errors.rb +6 -0
  11. data/lib/images_gallery/generator.rb +66 -0
  12. data/lib/images_gallery/image.rb +19 -0
  13. data/lib/images_gallery/source.rb +104 -0
  14. data/lib/images_gallery/templates/_navigation.html.erb +5 -0
  15. data/lib/images_gallery/templates/_thumbnails.html.erb +13 -0
  16. data/lib/images_gallery/templates/layout.html.erb +61 -0
  17. data/lib/images_gallery/test_application.rb +14 -0
  18. data/lib/images_gallery/version.rb +3 -0
  19. data/lib/images_gallery/view.rb +55 -0
  20. data/lib/images_gallery/views/index.rb +26 -0
  21. data/lib/images_gallery/views/make.rb +28 -0
  22. data/lib/images_gallery/views/model.rb +29 -0
  23. data/lib/images_gallery.rb +10 -0
  24. data/spec/features/index_page_spec.rb +36 -0
  25. data/spec/features/makes_pages/canon_page_spec.rb +30 -0
  26. data/spec/features/makes_pages/leica_page_spec.rb +25 -0
  27. data/spec/features/models_pages/canon_eos_20d_page_spec.rb +14 -0
  28. data/spec/features/models_pages/lux_d_3_page_spec.rb +14 -0
  29. data/spec/fixtures/output-template.html +22 -0
  30. data/spec/fixtures/works.xml +596 -0
  31. data/spec/fixtures/works_large.xml +52780 -0
  32. data/spec/lib/images_gallery/cli_spec.rb +64 -0
  33. data/spec/lib/images_gallery/collection_spec.rb +14 -0
  34. data/spec/lib/images_gallery/generator_spec.rb +11 -0
  35. data/spec/lib/images_gallery/image_spec.rb +11 -0
  36. data/spec/lib/images_gallery/source_spec.rb +43 -0
  37. data/spec/lib/images_gallery/view_spec.rb +13 -0
  38. data/spec/lib/images_gallery/views/index_spec.rb +16 -0
  39. data/spec/lib/images_gallery/views/make_spec.rb +16 -0
  40. data/spec/lib/images_gallery/views/model_spec.rb +16 -0
  41. data/spec/spec_helper.rb +32 -0
  42. data/spec/support/capybara.rb +12 -0
  43. data/spec/support/helpers.rb +18 -0
  44. data/spec/support/spec_for_collection_interface.rb +7 -0
  45. data/spec/support/spec_for_generator_interface.rb +29 -0
  46. data/spec/support/spec_for_image_interface.rb +18 -0
  47. data/spec/support/spec_for_images_gallery.rb +28 -0
  48. data/spec/support/spec_for_make_page.rb +9 -0
  49. data/spec/support/spec_for_model_page.rb +13 -0
  50. data/spec/support/spec_for_parser_interface.rb +6 -0
  51. data/spec/support/spec_for_view_inerface.rb +19 -0
  52. data/spec/tmp/canon/canon_eos_20d.html +76 -0
  53. data/spec/tmp/canon/canon_eos_400d_digital.html +76 -0
  54. data/spec/tmp/canon.html +93 -0
  55. data/spec/tmp/fuji_photo_film_co_ltd/slp1000se.html +76 -0
  56. data/spec/tmp/fuji_photo_film_co_ltd.html +81 -0
  57. data/spec/tmp/fujifilm/finepix_s6500fd.html +76 -0
  58. data/spec/tmp/fujifilm.html +81 -0
  59. data/spec/tmp/index.html +181 -0
  60. data/spec/tmp/leica/d_lux_3.html +96 -0
  61. data/spec/tmp/leica.html +121 -0
  62. data/spec/tmp/nikon_corporation/nikon_d80.html +76 -0
  63. data/spec/tmp/nikon_corporation.html +81 -0
  64. data/spec/tmp/panasonic/dmc_fz30.html +81 -0
  65. data/spec/tmp/panasonic.html +91 -0
  66. data/spec/tmp/unknown_make/unknown_model.html +81 -0
  67. data/spec/tmp/unknown_make.html +91 -0
  68. metadata +283 -0
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'images_gallery/cli'
3
+
4
+ module ImagesGallery
5
+ describe 'CLI' do
6
+
7
+ let(:cli_class) { CLI }
8
+ let(:command_line_interface) { CLI.new }
9
+
10
+ it 'should respond to .start' do
11
+ expect(cli_class).to respond_to :start
12
+ end
13
+
14
+ it { expect(command_line_interface).to respond_to :generate }
15
+
16
+ describe '#generate' do
17
+
18
+ it 'outputs what the generator returns' do
19
+ stderr = double()
20
+ stdout = double()
21
+ allow(command_line_interface).to receive_message_chain(:generator, :run).and_return('/path/to/gallery/index.html')
22
+
23
+ expect(stdout).to receive_message_chain(:puts).with('/path/to/gallery/index.html')
24
+ command_line_interface.generate('spec/fixtures/works.xml', 'spec/tmp', stderr, stdout)
25
+ end
26
+
27
+ describe 'provides quick feedback on common errors' do
28
+
29
+ context 'on SourceFileNotFoundError' do
30
+
31
+ it 'outputs an error message about the missing source file' do
32
+ stderr = double()
33
+ allow(command_line_interface).to receive_message_chain(:generator, :run) { raise ImagesGallery::SourceFileNotFoundError }
34
+
35
+ expect(stderr).to receive_message_chain(:puts).with('Please make sure the specified source file exists.')
36
+ command_line_interface.generate('source', 'target', stderr)
37
+ end
38
+ end
39
+
40
+ context 'on TargetDirectoryNotFoundError' do
41
+
42
+ it 'outputs an error message about the missing target directory' do
43
+ stderr = double()
44
+ allow(command_line_interface).to receive_message_chain(:generator, :run) { raise ImagesGallery::TargetDirectoryNotFoundError }
45
+
46
+ expect(stderr).to receive_message_chain(:puts).with('Please make sure the specified target directory exists.')
47
+ command_line_interface.generate('source', 'target', stderr)
48
+ end
49
+ end
50
+
51
+ context 'on SourceFileInvalidError' do
52
+
53
+ it 'outputs an error message about the XML source file not being well-formed' do
54
+ stderr = double()
55
+ allow(command_line_interface).to receive_message_chain(:generator, :run) { raise ImagesGallery::SourceFileInvalidError }
56
+
57
+ expect(stderr).to receive_message_chain(:puts).with('The source file is invalid. Please check it is well-formed XML.')
58
+ command_line_interface.generate('source', 'target', stderr)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Collection' do
5
+
6
+ let(:collection) { Collection.new }
7
+
8
+ it_behaves_like 'a collection'
9
+
10
+ it { expect(collection).to respond_to :makes }
11
+ it { expect(collection).to respond_to :models }
12
+
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Generator' do
5
+
6
+ let(:generator) { Generator.new }
7
+
8
+ it_behaves_like 'a generator'
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Image' do
5
+
6
+ let(:image) { Image.new }
7
+
8
+ it_behaves_like 'an image'
9
+
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Source' do
5
+
6
+ let(:source) { Source.new('spec/fixtures/works.xml') }
7
+ let(:parser) { source }
8
+
9
+ it_behaves_like 'a parser'
10
+
11
+ it { expect(source).to respond_to :images }
12
+
13
+ describe '#images' do
14
+
15
+ context 'once the source has been parsed' do
16
+
17
+ describe 'is a collection of images' do
18
+
19
+ it 'is a kind of Array' do
20
+ source.parse
21
+ expect(source.images).to be_kind_of Array
22
+ end
23
+
24
+ it 'is composed of images' do
25
+ source.parse
26
+ source.images.each do |image|
27
+
28
+ # There is some duplication here that I couldn't remove.
29
+ # See spec/support/spec_for_image_interface.
30
+
31
+ #it_behaves_like 'an image'
32
+
33
+ expect(image).to respond_to :description
34
+ expect(image).to respond_to :make
35
+ expect(image).to respond_to :model
36
+ expect(image).to respond_to :src
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'View' do
5
+
6
+ let(:view) { View.new }
7
+
8
+ it 'does not define :template' do
9
+ expect { view.template }.to raise_error NotImplementedError
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Views::Index' do
5
+
6
+ let(:data) do
7
+ data = Collection.new
8
+ data << Image.new
9
+ end
10
+
11
+ let(:view) { Views::Model.new(data) }
12
+
13
+ it_behaves_like 'a view'
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Views::Make' do
5
+
6
+ let(:data) do
7
+ data = Collection.new
8
+ data << Image.new
9
+ end
10
+
11
+ let(:view) { Views::Model.new(data) }
12
+
13
+ it_behaves_like 'a view'
14
+
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ module ImagesGallery
4
+ describe 'Views::Model' do
5
+
6
+ let(:data) do
7
+ data = Collection.new
8
+ data << Image.new
9
+ end
10
+
11
+ let(:view) { Views::Model.new(data) }
12
+
13
+ it_behaves_like 'a view'
14
+
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ require 'images_gallery'
2
+
3
+ Dir["./spec/support/**/*.rb"].sort.each { |f| require f; puts f }
4
+
5
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
6
+ RSpec.configure do |config|
7
+ # make sure the deprecated RSpec 2 syntax is not used
8
+ config.raise_errors_for_deprecations!
9
+
10
+ # rspec-expectations config goes here. You can use an alternate
11
+ # assertion/expectation library such as wrong or the stdlib/minitest
12
+ # assertions if you prefer.
13
+ config.expect_with :rspec do |expectations|
14
+ # This option will default to `true` in RSpec 4. It makes the `description`
15
+ # and `failure_message` of custom matchers include text for helper methods
16
+ # defined using `chain`, e.g.:
17
+ # be_bigger_than(2).and_smaller_than(4).description
18
+ # # => "be bigger than 2 and smaller than 4"
19
+ # ...rather than:
20
+ # # => "be bigger than 2"
21
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
22
+ end
23
+
24
+ # rspec-mocks config goes here. You can use an alternate test double
25
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
26
+ config.mock_with :rspec do |mocks|
27
+ # Prevents you from mocking or stubbing a method that does not exist on
28
+ # a real object. This is generally recommended, and will default to
29
+ # `true` in RSpec 4.
30
+ mocks.verify_partial_doubles = true
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ require 'capybara/rspec'
2
+
3
+ require 'images_gallery/test_application'
4
+
5
+ test_target = ImagesGallery::TestApplication::DEFAULT_TARGET
6
+
7
+ FileUtils.mkdir_p(test_target) unless File.exists?(test_target)
8
+
9
+ Capybara.app = Rack::Builder.new do
10
+ use Rack::Static, urls: [""], root: test_target, index: 'index.html'
11
+ run ImagesGallery::TestApplication
12
+ end
@@ -0,0 +1,18 @@
1
+ module Selectors
2
+ def navigation_selector
3
+ 'nav'
4
+ end
5
+
6
+ def header_selector
7
+ 'header'
8
+ end
9
+
10
+ def title_selector
11
+ { 'head' => 'title', 'body' => 'h1' }
12
+ end
13
+ end
14
+
15
+ RSpec.configure do |config|
16
+ config.extend Selectors
17
+ config.include Selectors
18
+ end
@@ -0,0 +1,7 @@
1
+ RSpec.shared_examples 'a collection' do
2
+
3
+ it 'is a kind of Array' do
4
+ expect(collection).to be_kind_of Array
5
+ end
6
+
7
+ end
@@ -0,0 +1,29 @@
1
+ RSpec.shared_examples 'a generator' do
2
+
3
+ it 'responds to :run' do
4
+ expect(generator).to respond_to :run
5
+ end
6
+
7
+ describe '#run' do
8
+
9
+ context 'when called with a unexistent file' do
10
+ it { expect{ generator.run('missing.xml', 'spec/tmp/') }.to raise_error ImagesGallery::SourceFileNotFoundError }
11
+ end
12
+
13
+ context 'when called with an unexistent target directory' do
14
+ it { expect{ generator.run('spec/fixtures/works.xml', 'spec/tmp/missing') }.to raise_error ImagesGallery::TargetDirectoryNotFoundError }
15
+ end
16
+
17
+ context 'when called with a file path as second argument (instead of a driectory path)' do
18
+ it { expect{ generator.run('spec/fixtures/works.xml', 'spec/tmp/missing') }.to raise_error ImagesGallery::TargetDirectoryNotFoundError }
19
+ end
20
+
21
+ it 'accepts a file path and a directory path as arguments' do
22
+ expect{ generator.run('spec/fixtures/works.xml', 'spec/tmp/') }.not_to raise_error
23
+ end
24
+
25
+ it 'returns the path of the gallery index page' do
26
+ expect(generator.run('spec/fixtures/works.xml', 'spec/tmp/')).to eq 'spec/tmp/index.html'
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ RSpec.shared_examples 'an image' do
2
+
3
+ it 'responds to :description' do
4
+ expect(image).to respond_to :description
5
+ end
6
+
7
+ it 'responds to :make' do
8
+ expect(image).to respond_to :make
9
+ end
10
+
11
+ it 'responds to :model' do
12
+ expect(image).to respond_to :model
13
+ end
14
+
15
+ it 'responds to :src' do
16
+ expect(image).to respond_to :src
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ RSpec.shared_examples 'an images gallery' do |thumbnails, header_selector, navigation_selector, title_selector|
2
+
3
+ it 'has a <title/>' do
4
+ expect(page).to have_selector title_selector['head'], visible: false
5
+ end
6
+
7
+ it "displays #{thumbnails} thumbnails" do
8
+ expect(page).to have_selector 'img', count: thumbnails
9
+ end
10
+
11
+ describe 'header' do
12
+
13
+ it 'is contains the navigation' do
14
+ expect(page).to have_selector "#{header_selector} #{navigation_selector}"
15
+ end
16
+
17
+ it 'is contains the page title' do
18
+ expect(page).to have_selector "#{header_selector} #{title_selector['body']}"
19
+ end
20
+ end
21
+
22
+ describe 'navigation' do
23
+
24
+ it 'is contained by a <nav/> element' do
25
+ expect(page).to have_selector navigation_selector
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.shared_examples 'a make page' do |navigation_selector|
2
+
3
+ describe 'navigation' do
4
+
5
+ it 'contains a link to the index page' do
6
+ expect(page).to have_selector "#{navigation_selector} a[href='index.html']"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ RSpec.shared_examples 'a model page' do |make, navigation_selector|
2
+
3
+ describe 'navigation' do
4
+
5
+ it 'contains a link to the index page' do
6
+ expect(page).to have_selector "#{navigation_selector} a[href='../index.html']"
7
+ end
8
+
9
+ it 'contains a link to the make page' do
10
+ expect(page).to have_selector "#{navigation_selector} a[href='../#{make}.html']"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,6 @@
1
+ RSpec.shared_examples 'a parser' do
2
+
3
+ it 'responds to :parse' do
4
+ expect(parser).to respond_to :parse
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ RSpec.shared_examples 'a view' do
2
+
3
+ it 'responds to :render' do
4
+ expect(view).to respond_to :render
5
+ end
6
+
7
+ it 'responds to :template' do
8
+ expect(view).to respond_to :template
9
+ end
10
+
11
+ it 'defines :template' do
12
+ expect { view.template }.not_to raise_error
13
+ end
14
+
15
+ context 'when built with no data' do
16
+
17
+ it 'exits gracefully (?)'
18
+ end
19
+ end
@@ -0,0 +1,76 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Images by (Canon) Canon EOS 20D</title>
5
+ <style type="text/css">
6
+ html, body {
7
+ font-family: "adelle", "Helvetica Neue", Helvetica, Arial, sans-serif;
8
+ font-size: 1rem;
9
+ }
10
+ nav {
11
+ border: 1px solid #ccc;
12
+ border-width: 1px 0;
13
+ margin: 2rem 0;
14
+ }
15
+ .nav-link {
16
+ display: inline-block;
17
+ line-height: 3rem;
18
+ padding: 0 1rem;
19
+ text-decoration: none;
20
+ text-transform: uppercase;
21
+ }
22
+ .grid {
23
+ max-width: 960px;
24
+ }
25
+ .grid-item {
26
+ border: 1px solid #ccc;
27
+ display: inline-block;
28
+ padding: 10px 10px 5px;
29
+ position: relative;
30
+ margin: 5px;
31
+ }
32
+ .grid-item-details {
33
+ background-color: rgba(255, 255, 255, 0.9);
34
+ color: #333;
35
+ font-size: .8rem;
36
+ font-variant: small-caps;
37
+ height: 3rem;
38
+ left: 0;
39
+ line-height: 1.2rem;
40
+ opacity: 0;
41
+ padding: .2rem 0 10px;
42
+ position: absolute;
43
+ width: 100%;
44
+ bottom: 0;
45
+ text-align: center;
46
+ text-decoration: none;
47
+ }
48
+ .grid-item:hover .grid-item-details {
49
+ opacity: 1;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <header>
55
+ <h1>Images by (Canon) Canon EOS 20D</h1>
56
+ <nav>
57
+
58
+ <a class="nav-link" href="../index.html">Browse all the images</a>
59
+
60
+ <a class="nav-link" href="../canon.html">Browse all the Canon images</a>
61
+
62
+ </nav>
63
+
64
+ </header>
65
+
66
+ <div class="grid">
67
+
68
+ <div class="grid-item">
69
+ <img alt="Image 2041" src="http://ih1.redbubble.net/work.2041.1.flat,135x135,075,f.jpg"/>
70
+
71
+ </div>
72
+
73
+ </div>
74
+
75
+ </body>
76
+ </html>
@@ -0,0 +1,76 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Images by (Canon) Canon EOS 400D DIGITAL</title>
5
+ <style type="text/css">
6
+ html, body {
7
+ font-family: "adelle", "Helvetica Neue", Helvetica, Arial, sans-serif;
8
+ font-size: 1rem;
9
+ }
10
+ nav {
11
+ border: 1px solid #ccc;
12
+ border-width: 1px 0;
13
+ margin: 2rem 0;
14
+ }
15
+ .nav-link {
16
+ display: inline-block;
17
+ line-height: 3rem;
18
+ padding: 0 1rem;
19
+ text-decoration: none;
20
+ text-transform: uppercase;
21
+ }
22
+ .grid {
23
+ max-width: 960px;
24
+ }
25
+ .grid-item {
26
+ border: 1px solid #ccc;
27
+ display: inline-block;
28
+ padding: 10px 10px 5px;
29
+ position: relative;
30
+ margin: 5px;
31
+ }
32
+ .grid-item-details {
33
+ background-color: rgba(255, 255, 255, 0.9);
34
+ color: #333;
35
+ font-size: .8rem;
36
+ font-variant: small-caps;
37
+ height: 3rem;
38
+ left: 0;
39
+ line-height: 1.2rem;
40
+ opacity: 0;
41
+ padding: .2rem 0 10px;
42
+ position: absolute;
43
+ width: 100%;
44
+ bottom: 0;
45
+ text-align: center;
46
+ text-decoration: none;
47
+ }
48
+ .grid-item:hover .grid-item-details {
49
+ opacity: 1;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <header>
55
+ <h1>Images by (Canon) Canon EOS 400D DIGITAL</h1>
56
+ <nav>
57
+
58
+ <a class="nav-link" href="../index.html">Browse all the images</a>
59
+
60
+ <a class="nav-link" href="../canon.html">Browse all the Canon images</a>
61
+
62
+ </nav>
63
+
64
+ </header>
65
+
66
+ <div class="grid">
67
+
68
+ <div class="grid-item">
69
+ <img alt="Image 777577" src="http://ih1.redbubble.net/work.777577.1.flat,135x135,075,f.jpg"/>
70
+
71
+ </div>
72
+
73
+ </div>
74
+
75
+ </body>
76
+ </html>
@@ -0,0 +1,93 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Images by Canon</title>
5
+ <style type="text/css">
6
+ html, body {
7
+ font-family: "adelle", "Helvetica Neue", Helvetica, Arial, sans-serif;
8
+ font-size: 1rem;
9
+ }
10
+ nav {
11
+ border: 1px solid #ccc;
12
+ border-width: 1px 0;
13
+ margin: 2rem 0;
14
+ }
15
+ .nav-link {
16
+ display: inline-block;
17
+ line-height: 3rem;
18
+ padding: 0 1rem;
19
+ text-decoration: none;
20
+ text-transform: uppercase;
21
+ }
22
+ .grid {
23
+ max-width: 960px;
24
+ }
25
+ .grid-item {
26
+ border: 1px solid #ccc;
27
+ display: inline-block;
28
+ padding: 10px 10px 5px;
29
+ position: relative;
30
+ margin: 5px;
31
+ }
32
+ .grid-item-details {
33
+ background-color: rgba(255, 255, 255, 0.9);
34
+ color: #333;
35
+ font-size: .8rem;
36
+ font-variant: small-caps;
37
+ height: 3rem;
38
+ left: 0;
39
+ line-height: 1.2rem;
40
+ opacity: 0;
41
+ padding: .2rem 0 10px;
42
+ position: absolute;
43
+ width: 100%;
44
+ bottom: 0;
45
+ text-align: center;
46
+ text-decoration: none;
47
+ }
48
+ .grid-item:hover .grid-item-details {
49
+ opacity: 1;
50
+ }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <header>
55
+ <h1>Images by Canon</h1>
56
+ <nav>
57
+
58
+ <a class="nav-link" href="index.html">Browse all the images</a>
59
+
60
+ <a class="nav-link" href="canon/canon_eos_20d.html">Canon EOS 20D</a>
61
+
62
+ <a class="nav-link" href="canon/canon_eos_400d_digital.html">Canon EOS 400D DIGITAL</a>
63
+
64
+ </nav>
65
+
66
+ </header>
67
+
68
+ <div class="grid">
69
+
70
+ <div class="grid-item">
71
+ <img alt="Image 2041" src="http://ih1.redbubble.net/work.2041.1.flat,135x135,075,f.jpg"/>
72
+
73
+ <a class="grid-item-details" href="canon/canon_eos_20d.html" title="Browse all the Canon EOS 20D images.">
74
+ <div>Canon</div>
75
+ <div>Canon EOS 20D</div>
76
+ </a>
77
+
78
+ </div>
79
+
80
+ <div class="grid-item">
81
+ <img alt="Image 777577" src="http://ih1.redbubble.net/work.777577.1.flat,135x135,075,f.jpg"/>
82
+
83
+ <a class="grid-item-details" href="canon/canon_eos_400d_digital.html" title="Browse all the Canon EOS 400D DIGITAL images.">
84
+ <div>Canon</div>
85
+ <div>Canon EOS 400D DIGITAL</div>
86
+ </a>
87
+
88
+ </div>
89
+
90
+ </div>
91
+
92
+ </body>
93
+ </html>