dropdown 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +23 -0
  3. data/.travis.yml +4 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +194 -0
  7. data/Rakefile +41 -0
  8. data/dropdown.gemspec +37 -0
  9. data/lib/dropdown.rb +23 -0
  10. data/lib/dropdown/blog.rb +23 -0
  11. data/lib/dropdown/configuration.rb +12 -0
  12. data/lib/dropdown/constants.rb +4 -0
  13. data/lib/dropdown/dropbox.rb +1 -0
  14. data/lib/dropdown/dropbox/session.rb +18 -0
  15. data/lib/dropdown/errors.rb +4 -0
  16. data/lib/dropdown/inflector.rb +11 -0
  17. data/lib/dropdown/iterators.rb +3 -0
  18. data/lib/dropdown/iterators/dropbox_iterator.rb +31 -0
  19. data/lib/dropdown/iterators/file_iterator.rb +18 -0
  20. data/lib/dropdown/iterators/iterator_factory.rb +12 -0
  21. data/lib/dropdown/markdown_renderer.rb +21 -0
  22. data/lib/dropdown/output_stores.rb +3 -0
  23. data/lib/dropdown/output_stores/dropbox_store.rb +24 -0
  24. data/lib/dropdown/output_stores/file_store.rb +23 -0
  25. data/lib/dropdown/output_stores/output_store_factory.rb +12 -0
  26. data/lib/dropdown/parsers/excerpt_extractor.rb +38 -0
  27. data/lib/dropdown/parsers/metadata_parser.rb +30 -0
  28. data/lib/dropdown/parsers/parser.rb +12 -0
  29. data/lib/dropdown/post.rb +48 -0
  30. data/lib/dropdown/processor.rb +39 -0
  31. data/lib/dropdown/readers.rb +3 -0
  32. data/lib/dropdown/readers/dropbox_reader.rb +32 -0
  33. data/lib/dropdown/readers/file_reader.rb +13 -0
  34. data/lib/dropdown/readers/reader_factory.rb +12 -0
  35. data/lib/dropdown/renderer_factory.rb +10 -0
  36. data/lib/dropdown/version.rb +3 -0
  37. data/spec/dropdown/blog_spec.rb +62 -0
  38. data/spec/dropdown/configuration_spec.rb +12 -0
  39. data/spec/dropdown/dropbox/session_spec.rb +26 -0
  40. data/spec/dropdown/inflector_spec.rb +18 -0
  41. data/spec/dropdown/iterators/dropbox_iterator_spec.rb +53 -0
  42. data/spec/dropdown/iterators/file_iterator_spec.rb +43 -0
  43. data/spec/dropdown/iterators/iterator_factory_spec.rb +10 -0
  44. data/spec/dropdown/markdown_renderer_spec.rb +31 -0
  45. data/spec/dropdown/output_stores/dropbox_store_spec.rb +46 -0
  46. data/spec/dropdown/output_stores/file_store_spec.rb +31 -0
  47. data/spec/dropdown/output_stores/output_store_factory_spec.rb +9 -0
  48. data/spec/dropdown/parsers/excerpt_extractor_spec.rb +54 -0
  49. data/spec/dropdown/parsers/metadata_parser_spec.rb +35 -0
  50. data/spec/dropdown/parsers/parser_spec.rb +26 -0
  51. data/spec/dropdown/post_spec.rb +66 -0
  52. data/spec/dropdown/processor_spec.rb +80 -0
  53. data/spec/dropdown/readers/dropbox_reader_spec.rb +43 -0
  54. data/spec/dropdown/readers/file_reader_spec.rb +30 -0
  55. data/spec/dropdown/readers/reader_factory_spec.rb +9 -0
  56. data/spec/dropdown/renderer_factory_spec.rb +18 -0
  57. data/spec/dropdown_spec.rb +13 -0
  58. data/spec/features/processing_a_markdown_directory_spec.rb +60 -0
  59. data/spec/fixtures/blog/my-trip-to-africa.md +10 -0
  60. data/spec/fixtures/processed/my-trip-to-africa.html +16 -0
  61. data/spec/fixtures/sample_post.md +8 -0
  62. data/spec/spec_helper.rb +3 -0
  63. data/spec/support/dummy_dropbox.rb +63 -0
  64. metadata +278 -0
@@ -0,0 +1,11 @@
1
+ module Dropdown
2
+ class Inflector
3
+ def initialize(word)
4
+ @word = word
5
+ end
6
+
7
+ def constant_name
8
+ @word.to_s.split('_').map! { |word| word.capitalize }.join
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'iterators/iterator_factory'
2
+ require_relative 'iterators/file_iterator'
3
+ require_relative 'iterators/dropbox_iterator'
@@ -0,0 +1,31 @@
1
+ require 'dropbox_sdk'
2
+ require_relative '../constants'
3
+
4
+ module Dropdown
5
+ module Iterators
6
+ class DropboxIterator
7
+ def initialize(source)
8
+ raise ArgumentError if source.nil?
9
+ @source = source.sub(/(\/)+$/, '')
10
+ end
11
+
12
+ def each
13
+ contents = client.metadata(@source)['contents']
14
+ contents.each do |child|
15
+ unless child['is_dir']
16
+ path = child['path']
17
+ if Dropdown::MARKDOWN_EXTENSIONS.include? File.extname(path)
18
+ yield path
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def client
27
+ @client ||= DropboxClient.new(Dropdown.configuration.dropbox_access_token)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../constants'
2
+
3
+ module Dropdown
4
+ module Iterators
5
+ class FileIterator
6
+ def initialize(source)
7
+ raise ArgumentError if source.nil?
8
+ @source = source.sub(/(\/)+$/, '')
9
+ end
10
+
11
+ def each
12
+ Dir.glob("#{@source}/**/*{#{Dropdown::MARKDOWN_EXTENSIONS.join(',')}}") do |markdown_file|
13
+ yield markdown_file
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../inflector'
2
+
3
+ module Dropdown
4
+ module Iterators
5
+ class IteratorFactory
6
+ def self.create(iterator, *args)
7
+ constant_name = Inflector.new("#{iterator}_iterator").constant_name
8
+ Dropdown::Iterators.const_get(constant_name).new *args
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ require 'redcarpet'
2
+
3
+ module Dropdown
4
+ class MarkdownRenderer
5
+ attr_reader :file_path
6
+ attr_accessor :reader
7
+
8
+ def initialize(file_path)
9
+ @file_path = file_path
10
+ @markdown = Redcarpet::Markdown.new Redcarpet::Render::HTML
11
+ end
12
+
13
+ def render
14
+ @markdown.render reader.read(file_path)
15
+ end
16
+
17
+ def output_filename
18
+ File.basename(file_path, '.md') + '.html'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'output_stores/output_store_factory'
2
+ require_relative 'output_stores/file_store'
3
+ require_relative 'output_stores/dropbox_store'
@@ -0,0 +1,24 @@
1
+ require 'dropbox_sdk'
2
+
3
+ module Dropdown
4
+ module OutputStores
5
+ class DropboxStore
6
+ attr_reader :path
7
+
8
+ def initialize(output_path)
9
+ @output_path = output_path
10
+ end
11
+
12
+ def save(contents, file_name)
13
+ @path = File.join @output_path, file_name
14
+ client.put_file(@path, contents, true)
15
+ end
16
+
17
+ private
18
+
19
+ def client
20
+ @client ||= DropboxClient.new(Dropdown.configuration.dropbox_access_token)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module Dropdown
2
+ module OutputStores
3
+ class FileStore
4
+ attr_reader :path
5
+
6
+ def initialize(output_path)
7
+ @output_path = output_path
8
+ end
9
+
10
+ def save(contents, file_name)
11
+ create_directory @output_path
12
+ @path = "#{@output_path}/#{file_name}"
13
+ File.open(@path, 'w+') { |f| f.write contents }
14
+ end
15
+
16
+ private
17
+
18
+ def create_directory(path)
19
+ FileUtils.mkdir_p(path)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../inflector'
2
+
3
+ module Dropdown
4
+ module OutputStores
5
+ class OutputStoreFactory
6
+ def self.create(store, *args)
7
+ constant_name = Inflector.new("#{store}_store").constant_name
8
+ Dropdown::OutputStores.const_get(constant_name).new *args
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ require 'nokogiri'
2
+
3
+ module Dropdown
4
+ module Parsers
5
+ class ExcerptExtractor
6
+ include Parser
7
+
8
+ def extract
9
+ @results = ''
10
+ doc = Nokogiri::HTML.parse(@content)
11
+ process_element doc
12
+ @results.chomp
13
+ end
14
+
15
+ private
16
+
17
+ def process_element(element)
18
+ if element.text?
19
+ @results << element.text.gsub(/\n/, '')
20
+ else
21
+ unless element.description.nil?
22
+ # save the seperator for later
23
+ seperator = element.description.block? ? "\n" : ""
24
+ end
25
+ end
26
+
27
+ element.children.map { |el| process_element(el) }
28
+
29
+ # Display a seperator if we are done processing the children
30
+ if !element.children.empty? &&
31
+ !seperator.nil? &&
32
+ !['html', 'body'].include?(element.name)
33
+ @results << seperator unless seperator.nil?
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ require 'nokogiri'
2
+
3
+ module Dropdown
4
+ module Parsers
5
+
6
+ class MetadataParser
7
+ include Parser
8
+
9
+ attr_accessor :headers
10
+
11
+ def initialize(content)
12
+ super
13
+ @headers = {}
14
+ end
15
+
16
+ def parse
17
+ doc = Nokogiri::HTML.parse(@content)
18
+ doc.xpath('//comment()').each { |comment| extract_header(comment.content) }
19
+ end
20
+
21
+ private
22
+
23
+ def extract_header(line)
24
+ pair = line.split(/:(.+)?/).map(&:strip)
25
+ @headers[pair[0].downcase.to_sym] = pair[1]
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module Dropdown
2
+ module Parsers
3
+ module Parser
4
+ attr_accessor :content
5
+
6
+ def initialize(content)
7
+ raise ArgumentError if content.nil?
8
+ @content = content
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,48 @@
1
+ module Dropdown
2
+ class Post
3
+ attr_reader :source, :reader
4
+
5
+ def title
6
+ metadata[:title]
7
+ end
8
+
9
+ def author
10
+ metadata[:author]
11
+ end
12
+
13
+ def date
14
+ Date.parse metadata[:date]
15
+ end
16
+
17
+ def body
18
+ content
19
+ end
20
+
21
+ def excerpt
22
+ unless @excerpt
23
+ extractor = Dropdown::Parsers::ExcerptExtractor.new(content)
24
+ @excerpt = extractor.extract
25
+ end
26
+ @excerpt
27
+ end
28
+
29
+ def initialize(source, reader)
30
+ @source = source
31
+ @reader = reader
32
+ end
33
+
34
+ private
35
+
36
+ def metadata
37
+ unless @metadata
38
+ @metadata = Dropdown::Parsers::MetadataParser.new(content)
39
+ @metadata.parse
40
+ end
41
+ @metadata.headers
42
+ end
43
+
44
+ def content
45
+ @content ||= reader.read(@source)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'renderer_factory'
2
+
3
+ module Dropdown
4
+ class Processor
5
+ attr_accessor :source,
6
+ :destination,
7
+ :storage,
8
+ :reader,
9
+ :markdown_iterator,
10
+ :output_store,
11
+ :renderer
12
+
13
+ def initialize
14
+ @renderer = Dropdown.configuration.renderer
15
+ end
16
+
17
+ def storage=(value)
18
+ @storage = value
19
+ self.reader = Dropdown::Readers::ReaderFactory.create(value)
20
+ self.markdown_iterator = Dropdown::Iterators::IteratorFactory.create(value, self.source)
21
+ self.output_store = Dropdown::OutputStores::OutputStoreFactory.create(value, self.destination)
22
+ end
23
+
24
+ def process
25
+ markdown_iterator.each do |file|
26
+ renderer = renderer_for file
27
+ output_store.save renderer.render, renderer.output_filename
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def renderer_for(file)
34
+ r = RendererFactory.create(renderer, file)
35
+ r.reader = reader
36
+ r
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'readers/reader_factory'
2
+ require_relative 'readers/file_reader'
3
+ require_relative 'readers/dropbox_reader'
@@ -0,0 +1,32 @@
1
+ require 'dropbox_sdk'
2
+ require_relative '../constants'
3
+
4
+ module Dropdown
5
+ module Readers
6
+ class DropboxReader
7
+ def read(path)
8
+ client.get_file(path)
9
+ end
10
+
11
+ def find_html_files(path)
12
+ files = []
13
+ contents = client.metadata(path)['contents']
14
+ contents.each do |child|
15
+ unless child['is_dir']
16
+ path = child['path']
17
+ if Dropdown::HTML_EXTENSIONS.include? File.extname(path)
18
+ files << path
19
+ end
20
+ end
21
+ end
22
+ files
23
+ end
24
+
25
+ private
26
+
27
+ def client
28
+ @client ||= DropboxClient.new(Dropdown.configuration.dropbox_access_token)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ module Dropdown
2
+ module Readers
3
+ class FileReader
4
+ def read(path)
5
+ File.read path
6
+ end
7
+
8
+ def find_html_files(path)
9
+ Dir.glob("#{path}/**/*.html")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ require_relative '../inflector'
2
+
3
+ module Dropdown
4
+ module Readers
5
+ class ReaderFactory
6
+ def self.create(reader, *args)
7
+ constant_name = Inflector.new("#{reader}_reader").constant_name
8
+ Dropdown::Readers.const_get(constant_name).new *args
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'inflector'
2
+
3
+ module Dropdown
4
+ class RendererFactory
5
+ def self.create(renderer, *args)
6
+ constant_name = Inflector.new(renderer).constant_name
7
+ Dropdown.const_get(constant_name).new *args
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Dropdown
2
+ VERSION = "0.8.1"
3
+ end
@@ -0,0 +1,62 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../support/dummy_dropbox'
3
+ require_relative '../../lib/dropdown'
4
+
5
+ describe Dropdown::Blog do
6
+ it 'can be initialized with a source directory' do
7
+ directory = 'foo'
8
+ blog = Dropdown::Blog.new directory
9
+ blog.source.should == directory
10
+ end
11
+
12
+ describe '#name' do
13
+ it 'can be set from an attribribute' do
14
+ blog = Dropdown::Blog.new
15
+ blog.name = 'Personal'
16
+ blog.name.should == 'Personal'
17
+ end
18
+ end
19
+
20
+ describe 'in a local directory' do
21
+ describe '#posts' do
22
+ let(:current_directory) { File.expand_path File.dirname(__FILE__) }
23
+ let(:source_directory) { File.join current_directory, '../fixtures/processed' }
24
+
25
+ it 'is initialized to empty' do
26
+ Dropdown::Blog.new.posts.should be_empty
27
+ end
28
+
29
+ it 'creates a post for each file in the source' do
30
+ html_files = Dir[File.join(source_directory, '**', '*')]
31
+ blog = Dropdown::Blog.new source_directory
32
+ blog.posts.length.should == html_files.length
33
+ end
34
+ end
35
+ end
36
+
37
+ describe 'in a Dropbox directory' do
38
+ include DummyDropbox
39
+
40
+ describe '#posts' do
41
+ let(:access_token) { 'blah' }
42
+ let(:path) { 'blog' }
43
+ let(:reader) { Dropdown::Readers::DropboxReader.new }
44
+ let(:contents) do
45
+ [{id_dir: false, path: "#{path}/file1.html"},
46
+ {id_dir: false, path: "#{path}/file1.txt"},
47
+ {id_dir: false, path: "#{path}/file2.html"}]
48
+ end
49
+
50
+ before do
51
+ Dropdown.configure { |c| c.dropbox_access_token = access_token }
52
+ stub_dropbox_metadata access_token, path, contents
53
+ end
54
+ after { remove_stubbed_dropbox_files }
55
+
56
+ it 'returns all of the html files within a directory' do
57
+ blog = Dropdown::Blog.new path, reader
58
+ blog.posts.length.should == 2
59
+ end
60
+ end
61
+ end
62
+ end