maneki 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,105 @@
1
+ Maneki
2
+ ======
3
+
4
+ Maneki is a simple file-based model for your Ruby projects. Give it a directory and it will parse any relevant files within.
5
+
6
+ For example you might have a model like this:
7
+
8
+ class Document < Maneki
9
+ path_is '../content/documents'
10
+ end
11
+
12
+ ...and a directory, `documents`, that contains a bunch of markdown documents:
13
+
14
+ documents/
15
+ first-document.text
16
+ second-document.text
17
+ third-document.text
18
+
19
+ ...and each file might be in a form similar to:
20
+
21
+ # This is the title
22
+
23
+ - published: yes
24
+ - authors: Aristoclea Cattington, Sebastian Fluffybottom
25
+
26
+ This is the content of the document. It is written in Markdown.
27
+
28
+ This example parsed document would have the following properties:
29
+
30
+ - Title: This is the title
31
+ - Headers:
32
+ - Published: True
33
+ - Authors:
34
+ - 'Aristoclea Cattington'
35
+ - 'Sebastian Fluffybottom'
36
+ - Body: This is the content of the document. It is written in Markdown.
37
+
38
+
39
+ Getting some documents
40
+ ----------------------
41
+
42
+ If you want all of the documents.
43
+
44
+ Document.all
45
+
46
+ Or you can find items by their slug.
47
+
48
+ Document.find :slug => 'test-document'
49
+
50
+ Or you can also search for the inclusion of certain headers.
51
+
52
+ Document.find :headers => { :published => true }
53
+ Document.find :headers => { :authors => 'Aristoclea Cattington' }
54
+
55
+ You can also grab any documents from a given category (read on).
56
+
57
+
58
+ Document categories
59
+ -------------------
60
+
61
+ If documents are found within folders then they will also be added to a categorised list.
62
+
63
+ For example, given this directory structure:
64
+
65
+ content/
66
+ documents/
67
+ first-document.text
68
+ second-document.text
69
+ third-document.text
70
+ projects/
71
+ a-project.text
72
+ here-is-another-project.text
73
+
74
+ Now assume that Maneki has its path set to `content`:
75
+
76
+ Maneki.path = 'content'
77
+
78
+ Here if we want to grab any posts we can just ask for the category.
79
+
80
+ Maneki.documents
81
+
82
+ Here we will get:
83
+
84
+ - First document
85
+ - Second document
86
+ - Third document
87
+
88
+ Whereas if you did:
89
+
90
+ Maneki.projects
91
+
92
+ You would get:
93
+
94
+ - A Project
95
+ - Here is another project
96
+
97
+ If you want to get a list of the found categories then ask for it.
98
+
99
+ Maneki.categories
100
+
101
+
102
+ Suggestions?
103
+ ------------
104
+
105
+ [Find me on Twitter](http://twitter.com/nathanhoad)
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ task :default => :test
2
+
3
+
4
+ ### TESTING ###
5
+ desc 'Run unit tests'
6
+ task 'test' do |t|
7
+ sh 'ruby test/test_all.rb'
8
+ end
9
+
10
+
11
+ ### PACKAGING ###
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |gemspec|
15
+ gemspec.name = "maneki"
16
+ gemspec.summary = "A simple file-based model for your Ruby projects"
17
+ gemspec.description = "Maneki loads and parses any relevant text files in a given directory"
18
+ gemspec.email = "nathan@nathanhoad.net"
19
+ gemspec.homepage = "http://github.com/nathanhoad/maneki"
20
+ gemspec.authors = ["Nathan Hoad"]
21
+ gemspec.files = `git ls-files`.split("\n").sort.reject{ |file| file =~ /^\./ }
22
+ end
23
+ rescue LoadError
24
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+
28
+ ### DOCUMENTATION ###
29
+ require 'rake/rdoctask'
30
+ Rake::RDocTask.new do |rdoc|
31
+ if File.exist? 'VERSION'
32
+ version = File.read('VERSION')
33
+ else
34
+ version = ""
35
+ end
36
+
37
+ rdoc.rdoc_dir = 'rdoc'
38
+ rdoc.title = "Maneki #{version}"
39
+ rdoc.rdoc_files.include('README*')
40
+ rdoc.rdoc_files.include('lib/**/*.rb')
41
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/lib/maneki.rb ADDED
@@ -0,0 +1,241 @@
1
+ # = Maneki
2
+ #
3
+ # Load documents from a directory and parse them. The idea is that
4
+ # any of the given documents (Markdown only for now) should be readable
5
+ # in their own format without parsing but be formatted in such way as
6
+ # to present meta information (title, headers, etc) in an easily parsable
7
+ # way.
8
+ class Maneki
9
+ # Set the search path
10
+ def self.path= (value)
11
+ path value
12
+ end
13
+
14
+ # Set or return the search path
15
+ def self.path (value = nil)
16
+ @path = value if value
17
+ @category = File.basename(value) unless @category
18
+ @path
19
+ end
20
+
21
+
22
+ # Set the general category for these documents
23
+ def self.category (value = nil)
24
+ @category = value
25
+ @category
26
+ end
27
+
28
+
29
+ # Get a list of all categories
30
+ def self.categories
31
+ prepare
32
+ @categorised.keys
33
+ end
34
+
35
+
36
+ # Grab all documents
37
+ def self.all
38
+ prepare
39
+ end
40
+
41
+
42
+ # Search for any documents
43
+ def self.find (args = {})
44
+ if args[:slug] or args.is_a? String
45
+ return find_by_slug(args[:slug] || args)
46
+ end
47
+
48
+ match = args[:match] || :any
49
+ args.delete(:match)
50
+
51
+ all.select do |item|
52
+ matches = 0
53
+
54
+ if args[:title]
55
+ if item.title == args[:title]
56
+ matched ||= true if match == :any
57
+ matches += 1 if match == :all
58
+ else
59
+ matched ||= false if match == :all
60
+ end
61
+ end
62
+
63
+ if args[:body]
64
+ if item.body.downcase.include? args[:body].downcase
65
+ matched ||= true if match == :any
66
+ matches += 1 if match == :all
67
+ else
68
+ matched ||= false if match == :all
69
+ end
70
+ end
71
+
72
+ if args[:headers]
73
+ args[:headers].each_pair do |header, value|
74
+ if item.headers[header] and (item.headers[header] == value or item.headers[header].include? value)
75
+ matched ||= true if match == :any
76
+ matches += 1 if match == :all
77
+ else
78
+ matched ||= false if match == :all
79
+ end
80
+ end
81
+ end
82
+
83
+ matched || (matches >= args.size)
84
+ end
85
+ end
86
+
87
+
88
+ # The previous document before a given document
89
+ def self.previous_before (document, args = {})
90
+ prepare
91
+ if args[:category]
92
+ index = @categorised[args[:category]].index(document)
93
+ else
94
+ index = @documents.index(document)
95
+ end
96
+ @documents[index - 1] unless index == 0
97
+ end
98
+
99
+ # The next document after a given document
100
+ def self.next_after (document, args = {})
101
+ prepare
102
+ if args[:category]
103
+ index = @categorised[args[:category]].index(document)
104
+ else
105
+ index = @documents.index(document)
106
+ end
107
+ @documents[index + 1] unless index == @documents.length
108
+ end
109
+
110
+
111
+ # Look up a document category
112
+ def self.method_missing (method, *args)
113
+ prepare
114
+ @categorised[method.to_s] unless @categorised.nil?
115
+ end
116
+
117
+
118
+ attr_accessor :filename, :category, :slug, :headers, :title, :body
119
+
120
+ # Create a new document
121
+ def initialize (args = {})
122
+ @filename = File.expand_path(args[:filename])
123
+
124
+ @body = ''
125
+ if File.exists? @filename
126
+ @category = File.dirname(@filename).split('/').last unless @category
127
+ @slug = @filename.gsub(File.expand_path(self.class.path) + '/', '').gsub(/\..+$/, '')
128
+
129
+ body = File.open(@filename).readlines
130
+ body.each do |line|
131
+ line.gsub!("\r", '') # fix up any DOS EOLs
132
+ end
133
+
134
+ title = body.find { |item| /^\#.+/.match item }
135
+ body = body[2..body.length-1] if title # consume title
136
+
137
+ # check for headers
138
+ @headers = Hash.new
139
+ if /^\s?\-.+/.match(body[0])
140
+ headers_end = body.index("\n") || 1
141
+ headers = body[0..headers_end-1] if headers_end # next blank line is end of headers
142
+
143
+ body_start = 1
144
+ body_start = body.index(headers.last) + 1 if headers
145
+ body = body[body_start..body.length - 1] if title
146
+
147
+ headers.each do |header|
148
+ unless header.strip == '' or /^\#.+/.match header
149
+ values = header.gsub(/^\s?\-/, '').strip.split(/:\s/)
150
+ key = values.shift.downcase.gsub(/[^\w\_]+/, '_').to_sym
151
+ value = values.join(': ').strip
152
+ # check for special header values (true, false, lists, etc)
153
+ if ['true', 't', 'yes', 'y'].include? value
154
+ value = true
155
+ elsif ['false', 'f', 'no', 'n'].include? value
156
+ value = false
157
+ elsif value.include? ','
158
+ value = value.split(/\,\s?/)
159
+ end
160
+ @headers[key] = value
161
+ end
162
+ end if headers
163
+ end
164
+
165
+ # title
166
+ @title = title.gsub(/^\#\s/, '').strip if title
167
+
168
+ # content
169
+ @body = body.join("").strip
170
+ end
171
+ end
172
+
173
+
174
+ # A callback to see if this document should be added to the list
175
+ def valid?
176
+ true
177
+ end
178
+
179
+
180
+ # Documents are the same if their IDs are the same
181
+ def == (rhs)
182
+ @slug == rhs.slug
183
+ end
184
+
185
+
186
+ # Sort by filename
187
+ def <=> (rhs)
188
+ @filename <=> rhs.filename
189
+ end
190
+
191
+
192
+ # Stringify this document
193
+ def to_s
194
+ title
195
+ end
196
+
197
+
198
+ protected
199
+
200
+ # Grab the @path with a slash if needed
201
+ def self.path_with_slash
202
+ files_path = File.expand_path(@path)
203
+ files_path += '/' unless files_path.end_with? '/'
204
+ files_path
205
+ end
206
+
207
+
208
+ # Find a document by its slug
209
+ def self.find_by_slug (slug)
210
+ filename = path_with_slash + slug + '.text'
211
+ if File.exists? filename
212
+ document = new(:filename => filename)
213
+ document if document.valid?
214
+ end
215
+ end
216
+
217
+
218
+ # Load the any documents in the files path
219
+ def self.prepare (args = {})
220
+ if @documents.nil? or args[:force]
221
+ files_path = path_with_slash
222
+
223
+ @documents = []
224
+ @categorised = {}
225
+
226
+ # find some documents
227
+ Dir.glob(["#{files_path}*", "#{files_path}*/*"]).each do |filename|
228
+ if filename.split('.').last == 'text'
229
+ document = new(:filename => filename)
230
+ if document.valid?
231
+ @documents << document
232
+ @categorised[document.category] ||= []
233
+ @categorised[document.category] << document
234
+ end
235
+ end
236
+ end
237
+ @documents.sort!
238
+ end
239
+ @documents
240
+ end
241
+ end
data/maneki.gemspec ADDED
@@ -0,0 +1,58 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{maneki}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Nathan Hoad"]
12
+ s.date = %q{2010-05-22}
13
+ s.description = %q{Maneki loads and parses any relevant text files in a given directory}
14
+ s.email = %q{nathan@nathanhoad.net}
15
+ s.extra_rdoc_files = [
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ "README.markdown",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "lib/maneki.rb",
23
+ "maneki.gemspec",
24
+ "test/chapter.rb",
25
+ "test/content/chapters/01-the-first-chapter.text",
26
+ "test/content/chapters/02-chapter-2.text",
27
+ "test/content/chapters/03-chapter-with-headers.text",
28
+ "test/content/chapters/04-different-chapter.text",
29
+ "test/content/documents/first-document.text",
30
+ "test/content/documents/second-document.text",
31
+ "test/content/documents/third-document.text",
32
+ "test/test_all.rb",
33
+ "test/test_chapters.rb",
34
+ "test/test_maneki.rb"
35
+ ]
36
+ s.homepage = %q{http://github.com/nathanhoad/maneki}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.6}
40
+ s.summary = %q{A simple file-based model for your Ruby projects}
41
+ s.test_files = [
42
+ "test/chapter.rb",
43
+ "test/test_all.rb",
44
+ "test/test_chapters.rb",
45
+ "test/test_maneki.rb"
46
+ ]
47
+
48
+ if s.respond_to? :specification_version then
49
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
53
+ else
54
+ end
55
+ else
56
+ end
57
+ end
58
+
data/test/chapter.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Chapter < Maneki
2
+ path File.dirname(__FILE__) + '/content/chapters'
3
+ end
@@ -0,0 +1,3 @@
1
+ # Chapter 1: The first chapter
2
+
3
+ This is the content of this chapter.
@@ -0,0 +1,3 @@
1
+ # Chapter 2: Crazy cats
2
+
3
+ This is the second chapter.
@@ -0,0 +1,8 @@
1
+ # Chapter 3: Chapter with Headers
2
+
3
+ - Author: Nathan Hoad
4
+ - List values: These, are, list, items
5
+ - Boolean value: yes
6
+ - Another boolean: no
7
+
8
+ Here is a chapter written by a guest author. It has some headers.
@@ -0,0 +1,6 @@
1
+ # Alpha sorted first title
2
+
3
+ - Boolean value: yes
4
+ - Facts: This chapter should be sorted first if by title
5
+
6
+ Here is some text.
@@ -0,0 +1,6 @@
1
+ # This is the first document
2
+
3
+ - published: yes
4
+ - author: Nathan Hoad
5
+
6
+ Here is some content for this document.
@@ -0,0 +1,3 @@
1
+ # Here is the second document
2
+
3
+ The content for this document is this.
@@ -0,0 +1,3 @@
1
+ # Third document
2
+
3
+ Yep. This is the third one.
data/test/test_all.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'test/unit'
2
+
3
+ Dir.glob('test/test_*.rb').each do |filename|
4
+ require filename if filename != __FILE__
5
+ end
@@ -0,0 +1,50 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/maneki'
3
+ require File.dirname(__FILE__) + '/chapter'
4
+
5
+
6
+ class TestChapters < Test::Unit::TestCase
7
+ def test_document_load
8
+ chapter = Chapter.new :filename => File.dirname(__FILE__) + '/content/chapters/01-the-first-chapter.text'
9
+ assert_kind_of Chapter, chapter
10
+ assert_equal '01-the-first-chapter', chapter.slug
11
+ assert_equal 'Chapter 1: The first chapter', chapter.title
12
+ assert_equal 'This is the content of this chapter.', chapter.body
13
+ assert_equal({}, chapter.headers)
14
+ end
15
+
16
+ def test_chapters_load
17
+ assert_not_equal 0, Chapter.all.size
18
+ end
19
+
20
+ def test_find
21
+ chapter = Chapter.find '01-the-first-chapter'
22
+ assert_kind_of Chapter, chapter
23
+ assert_equal 'Chapter 1: The first chapter', chapter.title
24
+
25
+ chapters = Chapter.find :headers => { :boolean_value => true }, :body => 'some headers', :match => :all
26
+ assert_equal 1, chapters.size
27
+
28
+ chapters = Chapter.find :headers => { :boolean_value => true }, :body => 'some headers', :match => :any
29
+ assert_equal 2, chapters.size
30
+
31
+ chapters = Chapter.find :headers => { :boolean_value => true }
32
+ assert_not_equal 0, chapters.size
33
+
34
+ chapters = Chapter.find :headers => { :list_values => 'are' }
35
+ assert_not_equal 0, chapters.size
36
+ end
37
+
38
+ def test_headers
39
+ chapter = Chapter.find '01-the-first-chapter'
40
+ assert_equal({}, chapter.headers)
41
+
42
+ chapter = Chapter.find '03-chapter-with-headers'
43
+ assert_not_equal({}, chapter.headers)
44
+ assert_equal 'Here is a chapter written by a guest author. It has some headers.', chapter.body
45
+
46
+ assert_kind_of Array, chapter.headers[:list_values]
47
+ assert_kind_of TrueClass, chapter.headers[:boolean_value]
48
+ assert_kind_of FalseClass, chapter.headers[:another_boolean]
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/maneki'
3
+
4
+
5
+ class TestManeki < Test::Unit::TestCase
6
+ def setup
7
+ @content_path = File.dirname(__FILE__) + '/content'
8
+ Maneki.path = @content_path
9
+ end
10
+
11
+ def test_documents_categories
12
+ assert Maneki.categories.include?('documents')
13
+ assert_not_equal 0, Maneki.documents.size
14
+
15
+ documents = Maneki.documents
16
+ assert_kind_of Array, documents
17
+ end
18
+
19
+ def test_previous_and_next
20
+ documents = Maneki.documents
21
+ first = documents[0]
22
+ second = documents[1]
23
+ other_second = Maneki.find 'documents/second-document'
24
+ third = documents[2]
25
+
26
+ assert_not_nil Maneki.previous_before(first)
27
+ assert_nil Maneki.previous_before(first, :category => 'documents')
28
+ assert_equal second, Maneki.next_after(first)
29
+ assert_equal other_second, Maneki.next_after(first)
30
+ assert_equal third, Maneki.next_after(second)
31
+ assert_equal third, Maneki.next_after(other_second)
32
+ assert_nil Maneki.next_after(third)
33
+
34
+ assert_equal first, Maneki.next_after(Maneki.previous_before(first))
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: maneki
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Nathan Hoad
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-22 00:00:00 +10:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Maneki loads and parses any relevant text files in a given directory
22
+ email: nathan@nathanhoad.net
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.markdown
29
+ files:
30
+ - README.markdown
31
+ - Rakefile
32
+ - VERSION
33
+ - lib/maneki.rb
34
+ - maneki.gemspec
35
+ - test/chapter.rb
36
+ - test/content/chapters/01-the-first-chapter.text
37
+ - test/content/chapters/02-chapter-2.text
38
+ - test/content/chapters/03-chapter-with-headers.text
39
+ - test/content/chapters/04-different-chapter.text
40
+ - test/content/documents/first-document.text
41
+ - test/content/documents/second-document.text
42
+ - test/content/documents/third-document.text
43
+ - test/test_all.rb
44
+ - test/test_chapters.rb
45
+ - test/test_maneki.rb
46
+ has_rdoc: true
47
+ homepage: http://github.com/nathanhoad/maneki
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.6
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: A simple file-based model for your Ruby projects
76
+ test_files:
77
+ - test/chapter.rb
78
+ - test/test_all.rb
79
+ - test/test_chapters.rb
80
+ - test/test_maneki.rb