forematter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # v0.1.0
2
+
3
+ * Initial release
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,9 @@
1
+ # Contributing
2
+
3
+ _You_ can make Forematter even awesomer!
4
+
5
+ 1. [Fork Forematter](https://github.com/bobthecow/forematter/fork)
6
+ 2. Create your feature branch: `git checkout -b feature/awesomesauce`
7
+ 3. Commit your changes: `git commit -am 'Add awesomesauce feature'`
8
+ 4. Push to the branch: `git push origin feature/awesomesauce`
9
+ 5. [Submit a new Pull Request](http://help.github.com/send-pull-requests/)!
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2013 Justin Hileman
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
20
+ OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Forematter
2
+
3
+ Forematter is the frontmatter-aware friend for your static site.
4
+
5
+
6
+ ## Install Forematter
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```rb
11
+ gem 'forematter'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ ```bash
17
+ $ bundle
18
+ ```
19
+
20
+ Or install it yourself, if that's what you're into:
21
+
22
+ ```bash
23
+ $ gem install forematter
24
+ ```
25
+
26
+ If you're not running a Ruby version manager, you might need to use `sudo gem install forematter` instead. But try it without the `sudo` first.
27
+
28
+
29
+ ## Use Forematter for Good
30
+
31
+ Forematter makes it easy to add tags I forgot:
32
+
33
+ ```bash
34
+ $ find . -name "*twitter*.markdown" | xargs fore add tags twitter
35
+ ```
36
+
37
+ And find all of my dumb:
38
+
39
+ ```bash
40
+ $ find . -name "*twitter*.markdown" | xargs fore list tags | grep -i tw
41
+ ```
42
+
43
+ > Twitpocalypse
44
+ > Twitter
45
+ > twitter
46
+
47
+ ... whoops. Let's fix that:
48
+
49
+ ```bash
50
+ $ fore merge tags Twitter twitter *.markdown
51
+ $ find . -name "*twitter*.markdown" | xargs fore list tags | grep -i tw
52
+ ```
53
+
54
+ > Twitpocalypse
55
+ > Twitter
56
+
57
+ Much better!
58
+
59
+
60
+ ## Use Forematter for Awesome
61
+
62
+ Forematter lets me automatically categorize all my blog posts, based on the tags I use:
63
+
64
+ ```bash
65
+ # Grab the 10 most common tags from my blog
66
+ for c in $(fore count tags *.markdown | tail -10 | sort -r | awk '{print($2)}'); do
67
+ # Set the category on each article with one of these tags
68
+ fore search -l tags "$c" *.markdown | xargs fore set category "$c"
69
+ end
70
+
71
+ # Format categories as title case
72
+ fore cleanup category --titlecase *.markdown
73
+
74
+ # Automatically classify all articles which don't already have a category
75
+ fore classify category *.markdown
76
+ ```
77
+
78
+ It does a lot more, too:
79
+
80
+ ```bash
81
+ fore add tags foo bar content/*.md
82
+ fore remove tags bacon content/*.md
83
+ fore merge tags foo Foo content/*.md
84
+ fore count tags content/*.md
85
+ fore search tags content/*.md
86
+ fore list tags content/*.md
87
+
88
+ fore set title 'title!' content/foo.md
89
+ fore unset title content/bar.md
90
+ fore list title content/*.md
91
+
92
+ fore touch updated_at content/baz.md
93
+
94
+ fore cleanup title --titlecase content/*.md
95
+ fore cleanup name --capitalize content/*.md
96
+ fore cleanup tags --downcase content/*.md
97
+ fore cleanup slug --url content/*.md
98
+ fore cleanup title --trim content/*.md
99
+ fore cleanup tags --sort content/*.md
100
+
101
+ fore classify category content/*.md
102
+ fore classify category --override content/*.md
103
+
104
+ fore --help
105
+ ```
106
+
107
+
108
+ ## Use it in your shell
109
+
110
+ Forematter tries to be a good *nix citizen. It plays nice with `find` and `grep` and `awk`. Mix and match with all your favorite command line tools!
111
+
112
+
113
+ ## Use it everywhere
114
+
115
+ Forematter isn't tied to any particular static site generator. If your files have [YAML frontmatter](http://jekyllrb.com/docs/frontmatter/), Forematter is the tool for you.
116
+
117
+ If you're looking for a great static site generator, go [check out this list — 210 of 'em and counting](http://staticsitegenerators.net)!
118
+
119
+ If Forematter doesn't work with your favorite, [let us know](https://github.com/bobthecow/forematter/issues/new).
120
+
121
+
122
+ ## Buyer beware
123
+
124
+ Forematter works by _editing your site's content files_. There is a nonzero chance that you'll do something you regret, and Forematter can't help you ⌘Z.
125
+
126
+ If you're not keeping things under version control, this would be a good time to start!
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # encoding: utf-8
2
+
3
+ require 'bundler/gem_tasks'
data/bin/fore ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ # Trap interrupts to quit cleanly while booting.
5
+ Signal.trap('INT') { exit 1 }
6
+
7
+ require 'forematter'
8
+
9
+ begin
10
+ Forematter.run(ARGV)
11
+ rescue Interrupt
12
+ $stderr.puts 'Quitting'
13
+ exit 1
14
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'forematter/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'forematter'
9
+ spec.version = Forematter::VERSION.gsub('-', '.')
10
+ spec.authors = ['Justin Hileman']
11
+ spec.email = 'justin@justinhileman.info'
12
+ spec.homepage = 'https://github.com/bobthecow/forematter'
13
+ spec.summary = 'the frontmatter-aware friend for your static site'
14
+ spec.description = 'Forematter is the frontmatter-aware friend for your static site'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = %w(CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md Rakefile forematter.gemspec)
18
+ spec.files += Dir['lib/**/*.rb']
19
+ spec.files += Dir['spec/**/*.rb']
20
+
21
+ spec.executables = %w(fore)
22
+
23
+ spec.add_dependency 'cri', '~> 2.4'
24
+ spec.add_dependency 'stringex'
25
+ spec.add_dependency 'titleize'
26
+
27
+ spec.add_development_dependency 'bundler'
28
+ spec.add_development_dependency 'rake'
29
+ end
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+
3
+ module Forematter
4
+ class CommandRunner < ::Cri::CommandRunner
5
+ def call
6
+ run
7
+ rescue UsageException
8
+ path = [command.supercommand]
9
+ path.unshift(path[0].supercommand) until path[0].nil?
10
+ super_usage = path[1..-1].map { |c| c.name + ' ' }.join
11
+ $stderr.puts "usage: #{super_usage}#{command.usage}"
12
+ exit 1
13
+ end
14
+
15
+ protected
16
+
17
+ def field
18
+ partition
19
+ fail UsageException, 'Missing field name' unless @field
20
+ @field
21
+ end
22
+
23
+ def value
24
+ fail 'ARGS!' unless command.value_args == :one
25
+ partition
26
+ fail UsageException, 'Missing argument' unless @value
27
+ @value
28
+ end
29
+
30
+ def values
31
+ fail 'ARGS!' unless command.value_args == :many
32
+ partition
33
+ fail UsageException, 'Missing argument' if @values.empty?
34
+ @values
35
+ end
36
+
37
+ def files
38
+ partition
39
+ fail UsageException, 'No file(s) specified' if @files.empty?
40
+ @files
41
+ end
42
+
43
+ def files_with(key)
44
+ files.select { |f| f.key?(key) }
45
+ end
46
+
47
+ def partition
48
+ return if @args_partitioned
49
+ args = arguments.dup
50
+ @field = args.shift
51
+ @value = args.shift if command.value_args == :one
52
+ @values, args = guess_split(args) if command.value_args == :many
53
+ @files = args.map { |f| Forematter::FileWrapper.new(f) }
54
+ @args_partitioned = true
55
+ end
56
+
57
+ def guess_split(args)
58
+ if (i = args.index('--'))
59
+ [args[0..i], args[i..-1]]
60
+ else
61
+ files = []
62
+ files.unshift(args.pop) while !args.empty? && File.exist?(args.last)
63
+ [args, files]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :many
4
+ summary 'add values to a field'
5
+ description <<-EOS
6
+ Add values to a frontmatter field in to a set of files.
7
+
8
+ By default, values will only be added if they don't already exist in a set. This
9
+ can be overridden with `--allow-dupes`.
10
+
11
+ If the specified frontmatter field is present on a file, but the value isn't a
12
+ YAML sequence (array), `fore add` will exit with an error.
13
+ EOS
14
+
15
+ flag nil, :'allow-dupes', 'allow duplicate values'
16
+
17
+ module Forematter::Commands
18
+ class Add < Forematter::CommandRunner
19
+ def run
20
+ files.each do |file|
21
+ old = file[field].to_ruby || []
22
+ fail "#{field} is not an array" unless old.is_a?(Array)
23
+ add = options[:'allow-dupes'] ? values : values.select { |v| !old.include?(v) }
24
+ next if add.empty?
25
+ add.each { |v| old << v }
26
+ file[field] = old
27
+ file.write
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ runner Forematter::Commands::Add
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ aliases :categorize
5
+ summary 'classify files by the given field'
6
+ description <<-EOS
7
+ Classify a set of files by the given frontmatter field.
8
+
9
+ For example, automatic classification could be used to select categories for
10
+ articles or blog posts:
11
+
12
+ fore classify category articles/*.md
13
+
14
+ By default, files which already have a value for the given field are used to
15
+ train the classifier, and files without a value are automatically classified.
16
+
17
+ If the `--override` option is passed, the classifier will be trained on the
18
+ available files as usual, and every file will be re-classified.
19
+
20
+ If the given frontmatter field is present on a file, but the value isn't a
21
+ string, `fore classify` will exit with an error.
22
+ EOS
23
+
24
+ flag nil, :override, 'Override existing values'
25
+
26
+ module Forematter::Commands
27
+ class Classify < Forematter::CommandRunner
28
+ def run
29
+ load_classifier
30
+
31
+ puts 'Getting categories'
32
+ categories_for(files_with(field)).each do |cat|
33
+ bayes.add_category(cat.to_sym)
34
+ end
35
+
36
+ puts 'Training classifier'
37
+ files_with(field).each { |file| train(file) }
38
+
39
+ puts 'Classifying files'
40
+ files_to_classify.each { |file| file.write if classify(file) }
41
+ end
42
+
43
+ protected
44
+
45
+ def load_classifier
46
+ require 'classifier'
47
+ rescue LoadError
48
+ $stderr.puts 'Install "classifier" gem to generate suggestions'
49
+ exit 1
50
+ end
51
+
52
+ def bayes
53
+ @bayes ||= Classifier::Bayes.new
54
+ end
55
+
56
+ def categories_for(files)
57
+ files.map { |file| file[field].to_ruby }.reject(&:nil?).uniq
58
+ end
59
+
60
+ def files_to_classify
61
+ files.reject do |file|
62
+ file.key?(field) unless options[:override]
63
+ end
64
+ end
65
+
66
+ def train(file)
67
+ val = file[field].to_ruby
68
+ fail 'Unable to classify by non-string fields' unless val.is_a?(String)
69
+ bayes.train(val.to_sym, file.content)
70
+ end
71
+
72
+ def classify(file)
73
+ old = file[field].to_ruby
74
+ file[field] = bayes.classify(file.content).to_s
75
+ file[field].to_ruby != old
76
+ end
77
+ end
78
+ end
79
+
80
+ runner Forematter::Commands::Classify
@@ -0,0 +1,67 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ summary 'clean up a field'
5
+ description <<-EOS
6
+ Normalize values in a frontmatter field in a set of files.
7
+
8
+ If the given frontmatter field is present on a file, but the value isn't a
9
+ string, `fore cleanup --sort` will exit with an error.
10
+ EOS
11
+
12
+ flag nil, :downcase, 'change to lower case'
13
+ flag nil, :capitalize, 'change to sentence case'
14
+ flag nil, :titlecase, 'change to title case'
15
+ flag nil, :sort, 'sort field values'
16
+ # flag nil, :translit, 'transliterate values'
17
+ flag nil, :trim, 'trim whitespace'
18
+ flag nil, :url, 'sluggify'
19
+
20
+ module Forematter::Commands
21
+ class Cleanup < Forematter::CommandRunner
22
+ def run
23
+ require 'stringex_lite'
24
+ require 'titleize'
25
+
26
+ files_with(field).each do |file|
27
+ old = file[field].to_ruby
28
+ val = cleanup(old)
29
+ unless val == old
30
+ file[field] = val
31
+ file.write
32
+ end
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ CLEANUP_MAP = {
39
+ downcase: :downcase,
40
+ capitalize: :capitalize,
41
+ titlecase: :titleize,
42
+ # translit: :translit,
43
+ trim: :strip,
44
+ url: :to_url,
45
+ }
46
+
47
+ def cleanup(val)
48
+ val = val.dup
49
+ return cleanup_array(val) if val.is_a?(Array)
50
+ fail 'Unable to sort non-array values' if options[:sort]
51
+ options.keys.each do |option|
52
+ val = val.method(CLEANUP_MAP[option]).call if CLEANUP_MAP.key?(option) && options[option]
53
+ end
54
+ val
55
+ end
56
+
57
+ def cleanup_array(val)
58
+ options.keys.each do |option|
59
+ val.map!(&CLEANUP_MAP[option]) if CLEANUP_MAP.key?(option) && options[option]
60
+ end
61
+ val.sort_by!(&:downcase) if options[:sort]
62
+ val
63
+ end
64
+ end
65
+ end
66
+
67
+ runner Forematter::Commands::Cleanup
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ summary 'count values in a field'
5
+ description <<-EOS
6
+ Count the number of times each value of a given field appears in a set of files.
7
+ EOS
8
+
9
+ module Forematter::Commands
10
+ class Count < Forematter::CommandRunner
11
+ def run
12
+ counts = tags.reduce({}) { |a, e| a.merge(e => (a[e] || 0) + 1) }
13
+ fmt = format(counts)
14
+
15
+ counts.sort_by { |tag, count| count }.each do |tag, count|
16
+ puts sprintf(fmt, count, tag)
17
+ end
18
+ end
19
+
20
+ protected
21
+
22
+ def tags
23
+ files_with(field).map { |file| file[field].to_ruby }.flatten.map(&:to_sym)
24
+ end
25
+
26
+ def format(counts)
27
+ "%#{counts.values.max.to_s.length}d %s"
28
+ end
29
+ end
30
+ end
31
+
32
+ runner Forematter::Commands::Count
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ usage 'fore command [options] [arguments]'
4
+ summary 'Forematter, the frontmatter-aware friend for your static site'
5
+
6
+ flag :h, :help, 'show the help message and quit' do |value, cmd|
7
+ puts cmd.help
8
+ exit 0
9
+ end
10
+
11
+ # flag :V, :verbose, 'enable verbose output' do
12
+ # Forematter.verbose = true
13
+ # end
14
+
15
+ flag :v, :version, 'show version' do
16
+ puts Forematter::VERSION
17
+ exit 0
18
+ end
19
+
20
+ run do |opts, args, cmd|
21
+ puts cmd.help
22
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ aliases :ls
5
+ summary 'list values for a field'
6
+ description <<-EOS
7
+ List the values for a given frontmatter field in a set of files.
8
+ EOS
9
+
10
+ module Forematter::Commands
11
+ class List < Forematter::CommandRunner
12
+ def run
13
+ puts tags.uniq.sort_by(&:downcase).join("\n")
14
+ end
15
+
16
+ protected
17
+
18
+ def tags
19
+ files_with(field).map { |file| file[field].to_ruby }.flatten
20
+ end
21
+ end
22
+ end
23
+
24
+ runner Forematter::Commands::List
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :many
4
+ summary 'combine multiple values for a field'
5
+ description <<-EOS
6
+ Combine multiple values for a frontmatter field in a set of files.
7
+
8
+ If the given frontmatter field is present on a file, but the value isn't a
9
+ string, `fore merge` will exit with an error.
10
+ EOS
11
+
12
+ module Forematter::Commands
13
+ class Merge < Forematter::CommandRunner
14
+ def run
15
+ dups = values.dup
16
+ canonical = dups.shift
17
+
18
+ files_with(field).each do |file|
19
+ old = file[field].to_ruby
20
+ fail "#{field} is not an array" unless old.is_a?(Array)
21
+
22
+ # Continue unless unless field had one of the values to remove
23
+ next if (old & dups).empty?
24
+ dups.each { |v| file[field].delete(v) }
25
+
26
+ # Save the original canonical for later
27
+ file[field] << canonical unless file[field].include?(canonical)
28
+ file.write
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ runner Forematter::Commands::Merge
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :many
4
+ aliases :rm
5
+ summary 'remove values from a field'
6
+ description <<-EOS
7
+ Remove values from a frontmatter field in a set of files.
8
+
9
+ If the given frontmatter field is present on a file, but the value isn't a
10
+ string, `fore remove` will exit with an error.
11
+ EOS
12
+
13
+ module Forematter::Commands
14
+ class Remove < Forematter::CommandRunner
15
+ def run
16
+ files_with(field).each do |file|
17
+ old = file[field].to_ruby
18
+ fail "#{field} is not an array" unless old.is_a?(Array)
19
+
20
+ # Continue unless old contains elements of values
21
+ next if (old & values).empty?
22
+ values.each { |v| file[field].delete(v) }
23
+
24
+ file.write
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ runner Forematter::Commands::Remove
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :one
4
+ aliases :mv
5
+ summary 'rename a field'
6
+ description <<-EOS
7
+ Rename a frontmatter field in a set of files.
8
+ EOS
9
+
10
+ module Forematter::Commands
11
+ class Rename < Forematter::CommandRunner
12
+ def run
13
+ files_with(field).each do |file|
14
+ file.rename(field, value)
15
+ file.write
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ runner Forematter::Commands::Rename
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :one
4
+ value_name 'pattern'
5
+ summary 'search for values in a field'
6
+ description <<-EOS
7
+ Search for values for a given frontmatter field in a set of files.
8
+
9
+ The search pattern may be a string or regular expression, e.g. `/foo\.bar/`.
10
+ EOS
11
+
12
+ flag :i, :'ignore-case', 'perform case insensitive matching'
13
+ flag :l, :'files-with-matches', 'only list the names of files with matches'
14
+ flag nil, :print0, 'list file names followed by an ASCII NUL character (for use with `xargs -0`)'
15
+
16
+ module Forematter::Commands
17
+ class Search < Forematter::CommandRunner
18
+ def run
19
+ files_with(field).sort_by(&:filename).each do |file|
20
+ field_val = file[field].to_ruby
21
+ if field_val.is_a? Array
22
+ next unless field_val.any? { |v| pattern =~ v }
23
+ else
24
+ next unless pattern =~ field_val
25
+ end
26
+ write(file.filename, field_val)
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ def write(filename, val)
33
+ if options[:print0]
34
+ print "#{filename}\0"
35
+ elsif options[:'files-with-matches']
36
+ puts filename
37
+ else
38
+ puts "#{filename}: #{val.to_json}"
39
+ end
40
+ end
41
+
42
+ def pattern
43
+ @pattern ||= parse_pattern
44
+ end
45
+
46
+ def parse_pattern
47
+ opts = options[:'ignore-case'] ? Regexp::IGNORECASE : 0
48
+
49
+ if %r{^/(.*)/([im]{0,2})$} =~ value
50
+ val = Regexp.last_match[1]
51
+ opts |= Regexp::IGNORECASE if Regexp.last_match[2].include? 'i'
52
+ opts |= Regexp::MULTILINE if Regexp.last_match[2].include? 'm'
53
+ else
54
+ val = Regexp.escape(value)
55
+ end
56
+
57
+ Regexp.new(val, opts)
58
+ end
59
+ end
60
+ end
61
+
62
+ runner Forematter::Commands::Search
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :one
4
+ summary 'set a field value'
5
+ description <<-EOS
6
+ Set a frontmatter field value on a set of files.
7
+ EOS
8
+
9
+ module Forematter::Commands
10
+ class Set < Forematter::CommandRunner
11
+ def run
12
+ files.each do |file|
13
+ file[field] = value
14
+ file.write
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ def value
21
+ val = super
22
+ YAML.load(val)
23
+ rescue Psych::SyntaxError
24
+ val
25
+ end
26
+ end
27
+ end
28
+
29
+ runner Forematter::Commands::Set
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ summary 'suggest values for a field'
5
+ description <<-EOS
6
+ Suggest values for a given frontmatter field based on other files in a set.
7
+ EOS
8
+
9
+ flag nil, :override, 'Override existing values'
10
+
11
+ module Forematter::Commands
12
+ class Suggest < Forematter::CommandRunner
13
+ def run
14
+ load_classifier
15
+
16
+ # Seed LSI index
17
+ files_with(field).each { |file| seed_index(file) }
18
+
19
+ # This takes days:
20
+ puts 'Building index... eesh'
21
+ lsi.build_index
22
+ puts "And we're done!"
23
+
24
+ files.each { |file| get_recs(file) }
25
+ end
26
+
27
+ protected
28
+
29
+ def load_classifier
30
+ require 'classifier'
31
+ rescue LoadError
32
+ $stderr.puts 'Install "classifier" gem to generate suggestions'
33
+ exit 1
34
+ end
35
+
36
+ def lsi
37
+ @lsi ||= Classifier::LSI.new(auto_rebuild: false)
38
+ end
39
+
40
+ def seed_index(file)
41
+ return unless file[:meta].key?(field)
42
+ lsi.add_item file[:file], *file[:meta][field].to_ruby { |i| file[:content] }
43
+ end
44
+
45
+ def get_recs(file)
46
+ return if file[:meta].key?(field) unless options[:override]
47
+
48
+ # TODO: something here :)
49
+ end
50
+ end
51
+ end
52
+
53
+ runner Forematter::Commands::Suggest
@@ -0,0 +1,65 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ summary 'update a field timestamp'
5
+
6
+ i = "\xC2\xA0" * 4
7
+ description <<-EOS
8
+ Update a frontmatter field timestamp for a set of files.
9
+
10
+ The new defaults to the current time, but may be overridden by passing
11
+ `--time`.
12
+
13
+ By default, an ISO-8601 string is used. Alternate formats may be specified:
14
+
15
+ --format iso8601
16
+
17
+ #{i}ISO-8601 date format, e.g. 2001-02-03T04:05:06+07:00 (default)
18
+
19
+ --format date
20
+
21
+ #{i}A YAML date literal.
22
+
23
+ --format db
24
+
25
+ #{i}ANSI SQL date format, e.g. 2001-02-03 04:05:06
26
+
27
+ --format '%Y-%m-%d'
28
+
29
+ #{i}Or any valid strftime format string
30
+ EOS
31
+
32
+ required :t, :time, 'new timestamp value (default: now)'
33
+ required :f, :format, 'timestamp format (default: iso8601)'
34
+
35
+ module Forematter::Commands
36
+ class Touch < Forematter::CommandRunner
37
+ def run
38
+ files_with(field).each do |file|
39
+ file[field] = now
40
+ file.write
41
+ end
42
+ end
43
+
44
+ protected
45
+
46
+ def now
47
+ @now ||= format_now
48
+ end
49
+
50
+ def format_now
51
+ format = options[:format] || 'iso8601'
52
+ now = options.key?(:time) ? Time.new(options[:time]) : Time.now
53
+
54
+ case format
55
+ when 'date' then now
56
+ when 'iso8601' then now.to_datetime.iso8601
57
+ when 'db' then now.strftime('%Y-%m-%d %H:%M:%S')
58
+ else
59
+ now.strftime(format)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ runner Forematter::Commands::Touch
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ value_args :none
4
+ summary 'remove a field'
5
+ description <<-EOS
6
+ Remove a frontmatter field from a set of files.
7
+ EOS
8
+
9
+ module Forematter::Commands
10
+ class Unset < Forematter::CommandRunner
11
+ def run
12
+ files_with(field).each do |file|
13
+ file.delete(field)
14
+ file.write
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ runner Forematter::Commands::Unset
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+
3
+ fail 'Forematter requires Psych' unless defined?(Psych)
4
+
5
+ # class String
6
+ # def translit
7
+ # convert_smart_punctuation.
8
+ # convert_accented_html_entities.
9
+ # convert_vulgar_fractions.
10
+ # convert_miscellaneous_html_entities.
11
+ # convert_miscellaneous_characters.
12
+ # to_ascii.
13
+ # convert_miscellaneous_characters.
14
+ # collapse
15
+ # end
16
+ # end
17
+
18
+ module Psych::Nodes
19
+ class Sequence
20
+ def include?(val)
21
+ children.any? { |c| c.to_ruby == val }
22
+ end
23
+
24
+ def delete(val)
25
+ return unless include?(val)
26
+ children.each_index do |i|
27
+ return children.delete_at(i) if children[i].to_ruby == val
28
+ end
29
+ end
30
+
31
+ def [](index)
32
+ fail "Unexpected index: #{index}" unless index.is_a? Integer
33
+ children[index]
34
+ end
35
+
36
+ def []=(index = nil, val)
37
+ return push(val) if index.nil?
38
+ fail "Unexpected index: #{index}" unless index.is_a? Integer
39
+ children[index] = YAML.parse(YAML.dump(val)).children.first
40
+ end
41
+
42
+ def push(val)
43
+ children.push(YAML.parse(YAML.dump(val)).children.first)
44
+ end
45
+ alias_method :<<, :push
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ module Cri
4
+ class Command
5
+ attr_accessor :value_args
6
+ attr_accessor :value_name
7
+ end
8
+
9
+ class CommandDSL
10
+ def value_args(count)
11
+ @command.value_args = count
12
+ end
13
+
14
+ def value_name(name)
15
+ @command.value_name = name
16
+ end
17
+
18
+ NBSP = "\xC2\xA0"
19
+
20
+ def auto_usage
21
+ name = @command.name
22
+ value = @command.value_name || 'value'
23
+ case @command.value_args
24
+ when :none
25
+ usage "#{name} [options] field file [file#{NBSP}...]"
26
+ when :one
27
+ usage "#{name} [options] field #{value} file [file#{NBSP}...]"
28
+ when :many
29
+ usage "#{name} [options] field #{value} [#{value}#{NBSP}...] file [file#{NBSP}...]"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+
3
+ module Forematter
4
+ class FileWrapper
5
+ extend Forwardable
6
+
7
+ def initialize(filename)
8
+ fail "File not found: #{filename}" unless File.exist?(filename)
9
+ @filename = filename
10
+ end
11
+
12
+ attr_reader :filename
13
+
14
+ def_delegators :meta, :key?, :has_key?, :[], :[]=, :delete, :rename
15
+
16
+ def to_s
17
+ "#{meta.to_yaml}---\n#{content}"
18
+ end
19
+
20
+ def write
21
+ File.open(filename, 'w+') { |f| f << to_s }
22
+ end
23
+
24
+ def content
25
+ parse_file
26
+ @content
27
+ end
28
+
29
+ protected
30
+
31
+ def meta
32
+ parse_file
33
+ @meta
34
+ end
35
+
36
+ def parse_file
37
+ return if @is_parsed
38
+
39
+ data = '--- {}'
40
+ content = IO.read(@filename)
41
+ if content =~ /\A(---\s*\n.*?\n?)^(?:(?:---|\.\.\.)\s*$\n?)/m
42
+ data = Regexp.last_match[1]
43
+ content = $POSTMATCH
44
+ end
45
+
46
+ @meta = Forematter::Frontmatter.new(data)
47
+ @content = content
48
+ @is_parsed = true
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+
3
+ module Forematter
4
+ class Frontmatter
5
+ def initialize(input)
6
+ init_stream(input)
7
+ end
8
+
9
+ def key?(key)
10
+ data.children.each_index do |i|
11
+ next unless i % 2
12
+ return true if data.children[i].to_ruby == key
13
+ end
14
+
15
+ false
16
+ end
17
+ alias_method :has_key?, :key?
18
+
19
+ def [](key)
20
+ data.children.each_index do |i|
21
+ next unless i % 2
22
+ return data.children[i + 1] if data.children[i].to_ruby == key
23
+ end
24
+ nil
25
+ end
26
+
27
+ def []=(key, val)
28
+ data.children.each_index do |i|
29
+ next unless i % 2
30
+ if data.children[i].to_ruby == key
31
+ data.children[i + 1] = thunk(val, data.children[i + 1])
32
+ return
33
+ end
34
+ end
35
+
36
+ data.children << Psych::Nodes::Scalar.new(key)
37
+ data.children << thunk(val)
38
+ end
39
+
40
+ def delete(key)
41
+ data.children.each_index do |i|
42
+ next unless i % 2
43
+ if data.children[i].to_ruby == key
44
+ val = data.children.delete_at(i + 1)
45
+ data.children.delete_at(i)
46
+ return val
47
+ end
48
+ end
49
+ end
50
+
51
+ def rename(key, new_key)
52
+ data.children.each_index do |i|
53
+ next unless i % 2
54
+ if data.children[i].to_ruby == key
55
+ data.children[i].value = new_key
56
+ return
57
+ end
58
+ end
59
+ end
60
+
61
+ def to_yaml
62
+ @stream.to_yaml
63
+ end
64
+
65
+ protected
66
+
67
+ def thunk(val, old = nil)
68
+ return val if val.is_a?(Psych::Nodes::Node)
69
+ val = parse_yaml(val)
70
+ if old.is_a?(Psych::Nodes::Sequence) && val.is_a?(Psych::Nodes::Sequence)
71
+ old.children.replace(val.children)
72
+ val = old
73
+ end
74
+ val
75
+ end
76
+
77
+ def parse_yaml(val)
78
+ YAML.parse(YAML.dump(val)).children.first
79
+ end
80
+
81
+ attr_reader :data
82
+
83
+ def init_stream(input)
84
+ doc = YAML.parse_stream(input).children.first
85
+ @data = doc.children.first
86
+ @stream = YAML.parse_stream('')
87
+ @stream.children << doc
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Forematter
4
+ VERSION = '0.1.0'
5
+ end
data/lib/forematter.rb ADDED
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ # stdlib
4
+ require 'date'
5
+ require 'English'
6
+ require 'forwardable'
7
+ require 'yaml'
8
+ require 'json'
9
+
10
+ # third party
11
+ require 'cri'
12
+
13
+ # forematter
14
+ require 'forematter/version'
15
+ require 'forematter/core_ext'
16
+ require 'forematter/cri_ext'
17
+ require 'forematter/frontmatter'
18
+ require 'forematter/file_wrapper'
19
+ require 'forematter/command_runner'
20
+
21
+ module Forematter
22
+ module Commands
23
+ end
24
+
25
+ class UsageException < Exception
26
+ end
27
+
28
+ class << self
29
+ attr_reader :root_command
30
+ # attr_accessor :verbose
31
+
32
+ def run(args)
33
+ # Remove the signal trap we set in the bin file.
34
+ Signal.trap('INT', 'DEFAULT')
35
+ setup
36
+ root_command.run(args)
37
+ end
38
+
39
+ def add_command(cmd)
40
+ root_command.add_command(cmd)
41
+ end
42
+
43
+ protected
44
+
45
+ def setup
46
+ root_cmd_filename = File.dirname(__FILE__) + '/forematter/commands/fore.rb'
47
+
48
+ # Add help and root commands
49
+ @root_command = load_command_at(root_cmd_filename)
50
+ add_command(Cri::Command.new_basic_help)
51
+
52
+ cmd_filenames.each do |filename|
53
+ add_command(load_command_at(filename)) unless filename == root_cmd_filename
54
+ end
55
+ end
56
+
57
+ def cmd_filenames
58
+ Dir[File.dirname(__FILE__) + '/forematter/commands/*.rb']
59
+ end
60
+
61
+ def load_command_at(filename, command_name = nil)
62
+ Cri::Command.define(File.read(filename), filename).modify do
63
+ name command_name || File.basename(filename, '.rb')
64
+ auto_usage
65
+ end
66
+ end
67
+ end
68
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forematter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Justin Hileman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: cri
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.4'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.4'
30
+ - !ruby/object:Gem::Dependency
31
+ name: stringex
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: titleize
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: bundler
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Forematter is the frontmatter-aware friend for your static site
95
+ email: justin@justinhileman.info
96
+ executables:
97
+ - fore
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - CHANGELOG.md
102
+ - CONTRIBUTING.md
103
+ - LICENSE.md
104
+ - README.md
105
+ - Rakefile
106
+ - forematter.gemspec
107
+ - lib/forematter/command_runner.rb
108
+ - lib/forematter/commands/add.rb
109
+ - lib/forematter/commands/classify.rb
110
+ - lib/forematter/commands/cleanup.rb
111
+ - lib/forematter/commands/count.rb
112
+ - lib/forematter/commands/fore.rb
113
+ - lib/forematter/commands/list.rb
114
+ - lib/forematter/commands/merge.rb
115
+ - lib/forematter/commands/remove.rb
116
+ - lib/forematter/commands/rename.rb
117
+ - lib/forematter/commands/search.rb
118
+ - lib/forematter/commands/set.rb
119
+ - lib/forematter/commands/suggest.rb
120
+ - lib/forematter/commands/touch.rb
121
+ - lib/forematter/commands/unset.rb
122
+ - lib/forematter/core_ext.rb
123
+ - lib/forematter/cri_ext.rb
124
+ - lib/forematter/file_wrapper.rb
125
+ - lib/forematter/frontmatter.rb
126
+ - lib/forematter/version.rb
127
+ - lib/forematter.rb
128
+ - bin/fore
129
+ homepage: https://github.com/bobthecow/forematter
130
+ licenses:
131
+ - MIT
132
+ post_install_message:
133
+ rdoc_options: []
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ segments:
143
+ - 0
144
+ hash: 2379320938142761946
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ! '>='
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ segments:
152
+ - 0
153
+ hash: 2379320938142761946
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 1.8.23
157
+ signing_key:
158
+ specification_version: 3
159
+ summary: the frontmatter-aware friend for your static site
160
+ test_files: []