flutterby 0.4.0 → 0.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 83e24d1e73103a5cef7b60efcf5a21951e474c66
4
- data.tar.gz: f25f5763a2a10b7190e9ea5a827ef716bba729f7
3
+ metadata.gz: 679b8fba9c9fda2c5e70df5bb73a16903be5181f
4
+ data.tar.gz: 5a27688b1713d0bd8bb49695afcb1bb9272682e6
5
5
  SHA512:
6
- metadata.gz: fc8ecf0efc0a1615d0149b1a94b0eb3fef7cd27db54147de26238646313f2dc679b74335cc69671364f1d04f43c96d72112ff7b596a27c883256808449622aa4
7
- data.tar.gz: d7ed0d77dde3726cc718c0420f1d1f3a5732c17a9906329d945b7eb607ccce3fa183659a7b92787edcb024bb39883f04cbcf6982a43675da21be31b576ba860d
6
+ metadata.gz: b24fcfd4c696520de83fcc2f6a0318f7e3895bf558bdc30145ccf8eca1af57845fca6c393ceac928edcfa3afaf5f99409324d9027269c88a89fd698d6512c35c
7
+ data.tar.gz: 8a2827f70bc5484a202d9f147e7c0cb0ba04ee3e95106041ceb8439e014321b01474a9273e3d2bcbbcba740eed03263c4d5b86bdb848a453cf13602647da2edb
data/.travis.yml CHANGED
@@ -1,7 +1,9 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
+ - 2.2.6
4
5
  - 2.3.3
5
6
  - 2.4.0
6
7
  before_install: gem install bundler -v 1.13.6
8
+ cache: bundler
7
9
  script: bundle exec rake
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --exclude lib/templates/
2
+ --embed-mixin "Flutterby::Node::*"
data/CHANGES.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Version History
2
2
 
3
+ ### 0.5.0 (2017-01-24)
4
+
5
+ - **NEW:** Nodes have two new attributes, `prefix` and `slug`, which are automatically generated from the node's name. If the name starts with a combination of decimals and dashes, these will become the `prefix`, and the remainder auf the name the `suffix`. For example, a name of `123-introduction` will result in a prefix of `123` and a slug of `introduction`. As before, a prefix that looks like a date (eg. `2017-04-01-introduction`) will automatically be parsed into `data[:date]`.
6
+ - **NEW:** When nodes are being spawned, their names will be changed to their slugs by default (ie. any prefix contained in the original name will be removed.) For example, a `123-foo.html.md` will be exported as just `foo.html`.
7
+ - **NEW:** Nodes now have first-class support of a node title through the new `title` attribute. This will either use `data[:title]`, when available, or generate a title from `slug` (eg. a node named `hello-world.html.md` will automatically have a title of `Hello World`.)
8
+ - **NEW:** You can now also access a node's data using a convenient dot syntax; eg. `node.data.foo.bar` will return `node.data[:foo][:bar]`. If you're on Ruby 2.3 or newer, this allows you to use the safe navigation operator; eg. `data.foo&.bar`.
9
+ - **BREAKING CHANGE:** The `_node.rb` mechanism is gone. In its stead, you can now add `_init.rb` files that will be evaluated automatically; those can use the new `extend_siblings` and `extend_parent` convenience methods to extend all available siblings (or the parent) with the specified module or block.
10
+ - **NEW:** These node extensions can now supply an `on_setup` block that will be executed after the tree has been fully spawned. You can use these setup blocks to further modify the tree.
11
+ - **NEW:** The `flutterby build` and `flutterby serve` CLI commands now provide additional debug output when started with the `--debug` option.
12
+ - **NEW:** Added `Node#create` as a convenience method for creating new child nodes below a given node.
13
+ - **CHANGE:** Some massive refactoring, the primary intent being to perform the rendering of nodes in a thread-safe manner.
14
+
15
+
3
16
  ### 0.4.0 (2017-01-21)
4
17
 
5
18
  - **NEW:** Flutterby views now have a `tag` helper method available that can generate HTML tags programatically.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Flutterby
1
+ # Flutterby 🦋
2
2
 
3
3
  ### A flexible, Ruby-powered static site generator.
4
4
 
@@ -17,8 +17,9 @@
17
17
 
18
18
  - [Blog post introducing Flutterby](http://hmans.io/posts/2017/01/11/flutterby.html)
19
19
  - [New project template](https://github.com/hmans/flutterby/tree/master/lib/templates/new_project) (example code)
20
+ - [Code Reference Documentation](http://www.rubydoc.info/github/hmans/flutterby)
20
21
  - [Version History](https://github.com/hmans/flutterby/blob/master/CHANGES.md)
21
- - [Roadmap](https://github.com/hmans/flutterby/projects/1)
22
+ - [Issues/Roadmap](https://github.com/hmans/flutterby/issues)
22
23
  - [Sites built with Flutterby](https://github.com/hmans/flutterby/wiki/Sites-built-with-Flutterby) (add yours!)
23
24
 
24
25
 
data/bin/yard ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'yard' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("yard", "yard")
data/bin/yardoc ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'yardoc' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("yard", "yardoc")
data/bin/yri ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'yri' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("yard", "yri")
data/flutterby.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency 'awesome_print', '~> 0'
31
31
  spec.add_development_dependency 'gem-release', '~> 0'
32
32
  spec.add_development_dependency 'pry', '~> 0.10'
33
+ spec.add_development_dependency 'yard', '~> 0.9'
33
34
 
34
35
  spec.add_dependency 'erubis', '~> 2.7'
35
36
  spec.add_dependency 'erubis-auto', '~> 1.0'
data/lib/flutterby.rb CHANGED
@@ -3,7 +3,9 @@ require 'toml'
3
3
  require 'mime-types'
4
4
  require 'json'
5
5
 
6
+ require "flutterby/dotaccess"
6
7
  require "flutterby/version"
8
+ require "flutterby/tree_walker"
7
9
  require "flutterby/node"
8
10
  require "flutterby/filters"
9
11
  require "flutterby/view"
data/lib/flutterby/cli.rb CHANGED
@@ -21,10 +21,13 @@ module Flutterby
21
21
  end
22
22
 
23
23
  desc "build", "Build your static site"
24
- option :in, default: "./site/", aliases: [:i]
25
- option :out, default: "./_build/", aliases: [:o]
24
+ option :in, default: "./site/", aliases: ["-i"]
25
+ option :out, default: "./_build/", aliases: ["-o"]
26
+ option :debug, default: false, aliases: ["-d"], type: :boolean
26
27
 
27
28
  def build
29
+ Flutterby.logger.level = options.debug ? Logger::DEBUG : Logger::INFO
30
+
28
31
  # Simplify logger output
29
32
  Flutterby.logger.formatter = proc do |severity, datetime, progname, msg|
30
33
  " • #{msg}\n"
@@ -49,10 +52,13 @@ module Flutterby
49
52
 
50
53
 
51
54
  desc "serve", "Serve your site locally"
52
- option :in, default: "./site/", aliases: [:i]
53
- option :port, default: 4004, aliases: [:p], type: :numeric
55
+ option :in, default: "./site/", aliases: ["-i"]
56
+ option :port, default: 4004, aliases: ["-p"], type: :numeric
57
+ option :debug, default: false, aliases: ["-d"], type: :boolean
54
58
 
55
59
  def serve
60
+ Flutterby.logger.level = options.debug ? Logger::DEBUG : Logger::INFO
61
+
56
62
  say_hi
57
63
 
58
64
  say color("📚 Importing site...", :bold)
@@ -61,8 +67,8 @@ module Flutterby
61
67
  say color("🌲 Read #{root.tree_size} nodes.", :green, :bold)
62
68
 
63
69
  say color("🌤 Serving your Flutterby site on http://localhost:#{options.port} - enjoy! \\o/", :bold)
64
- server = Flutterby::Server.new(root, port: options.port)
65
- server.run!
70
+ server = Flutterby::Server.new(root)
71
+ server.run!(port: options.port)
66
72
  end
67
73
 
68
74
 
@@ -0,0 +1,31 @@
1
+ module Dotaccess
2
+ class Proxy
3
+ def initialize(hash)
4
+ @hash = hash
5
+ end
6
+
7
+ def method_missing(meth, *args)
8
+ if @hash.respond_to?(meth)
9
+ @hash.send(meth, *args)
10
+ elsif meth =~ %r{\A(.+)=\Z}
11
+ @hash[$1] = args.first
12
+ elsif v = (@hash[meth] || @hash[meth.to_s])
13
+ v.is_a?(Hash) ? Proxy.new(v) : v
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ def [](k)
20
+ @hash[k]
21
+ end
22
+
23
+ def ==(o)
24
+ @hash == o
25
+ end
26
+ end
27
+
28
+ def self.[](hash)
29
+ Proxy.new(hash)
30
+ end
31
+ end
@@ -5,25 +5,27 @@ module Flutterby
5
5
  end
6
6
 
7
7
  def export!(into:)
8
- @root.paths.each do |path, node|
9
- if node.should_publish?
10
- path = ::File.expand_path(::File.join(into, node.url))
8
+ export_node(@root, into: into)
9
+ end
10
+
11
+ private
12
+
13
+ def export_node(node, into:)
14
+ return unless node.should_publish?
11
15
 
12
- if node.file?
13
- # Make sure directory exists
14
- FileUtils.mkdir_p(::File.dirname(path))
16
+ path = ::File.expand_path(::File.join(into, node.full_name))
15
17
 
16
- # Write file
17
- ::File.write(path, node.render(layout: true))
18
- logger.info "Exported #{node.url}"
19
- end
18
+ if node.file?
19
+ ::File.write(path, node.render(layout: true))
20
+ logger.info "Exported #{node.url}"
21
+ else
22
+ FileUtils.mkdir_p(path)
23
+ node.children.each do |child|
24
+ export_node(child, into: path)
20
25
  end
21
26
  end
22
27
  end
23
28
 
24
-
25
- private
26
-
27
29
  def logger
28
30
  @logger ||= Flutterby.logger
29
31
  end
@@ -8,19 +8,19 @@ require 'flutterby/markdown_formatter'
8
8
 
9
9
  module Flutterby
10
10
  module Filters
11
- def self.apply!(node)
12
- node.body = node.source.try(:html_safe)
11
+ def self.apply!(view)
12
+ view._body = view.source.try(:html_safe)
13
13
 
14
14
  # Apply all filters
15
- node.filters.each do |filter|
15
+ view.node.filters.each do |filter|
16
16
  meth = "process_#{filter}!"
17
17
 
18
18
  if Filters.respond_to?(meth)
19
- Filters.send(meth, node)
20
- elsif template = tilt(filter, node.body)
21
- node.body = template.render(node.view).html_safe
19
+ Filters.send(meth, view)
20
+ elsif template = tilt(filter, view._body)
21
+ view._body = template.render(view).html_safe
22
22
  else
23
- Flutterby.logger.warn "Unsupported filter '#{filter}' for #{node.url}"
23
+ Flutterby.logger.warn "Unsupported filter '#{filter}' for #{view.node.url}"
24
24
  end
25
25
  end
26
26
  end
@@ -43,23 +43,23 @@ module Flutterby
43
43
  end
44
44
  end
45
45
 
46
- Flutterby::Filters.add("rb") do |node|
47
- node.instance_eval(node.body)
46
+ Flutterby::Filters.add("rb") do |view|
47
+ view._body = view.instance_eval(view._body)
48
48
  end
49
49
 
50
- Flutterby::Filters.add(["md", "markdown"]) do |node|
51
- node.body = Flutterby::MarkdownFormatter.new(node.body).complete.to_s.html_safe
50
+ Flutterby::Filters.add(["md", "markdown"]) do |view|
51
+ view._body = Flutterby::MarkdownFormatter.new(view._body).complete.to_s.html_safe
52
52
  end
53
53
 
54
- Flutterby::Filters.add("scss") do |node|
54
+ Flutterby::Filters.add("scss") do |view|
55
55
  sass_options = {
56
56
  syntax: :scss,
57
57
  load_paths: []
58
58
  }
59
59
 
60
- if node.fs_path
61
- sass_options[:load_paths] << File.dirname(node.fs_path)
60
+ if view.node.fs_path
61
+ sass_options[:load_paths] << File.dirname(view.node.fs_path)
62
62
  end
63
63
 
64
- node.body = Sass::Engine.new(node.body, sass_options).render
64
+ view._body = Sass::Engine.new(view._body, sass_options).render
65
65
  end
@@ -1,14 +1,16 @@
1
- require 'benchmark'
1
+ require 'flutterby/node_extension'
2
2
 
3
3
  module Flutterby
4
4
  class Node
5
5
  attr_accessor :name, :ext, :source
6
- attr_writer :body
7
- attr_reader :filters, :parent, :fs_path, :children, :paths
6
+ attr_reader :filters, :parent, :fs_path, :children
7
+ attr_reader :prefix, :slug
8
+ attr_reader :_setup_procs
8
9
 
9
10
  def initialize(name, parent: nil, fs_path: nil, source: nil)
10
11
  @fs_path = fs_path ? ::File.expand_path(fs_path) : nil
11
12
  @source = source
13
+ @_setup_procs = []
12
14
 
13
15
  # Extract name, extension, and filters from given name
14
16
  parts = name.split(".")
@@ -24,21 +26,66 @@ module Flutterby
24
26
  reload!
25
27
  end
26
28
 
27
- concerning :Children do
28
- def register_url!
29
- if file? && should_publish?
30
- root.paths[url] = self
31
- end
29
+ module Paths
30
+ # Returns the node's URL.
31
+ #
32
+ def url
33
+ ::File.join(parent ? parent.url : "/", full_name)
32
34
  end
35
+ end
33
36
 
34
- def find_child(name)
35
- if name.include?(".")
36
- @children.find { |c| c.full_name == name }
37
- else
38
- @children.find { |c| c.name == name }
37
+ include Paths
38
+
39
+
40
+ module Tree
41
+ # Returns the tree's root node.
42
+ #
43
+ def root
44
+ parent ? parent.root : self
45
+ end
46
+
47
+ # Returns true if this node is also the tree's root node.
48
+ #
49
+ def root?
50
+ root == self
51
+ end
52
+
53
+ def sibling(name)
54
+ parent && parent.find(name)
55
+ end
56
+
57
+ # Returns this node's siblings (ie. other nodes within the
58
+ # same folder node.)
59
+ #
60
+ def siblings
61
+ parent && (parent.children - [self])
62
+ end
63
+
64
+ # Among this node's children, find a node by its name. If the
65
+ # name passed as an argument includes a dot, the name will match against
66
+ # the full name of the children; otherwise, just the base name.
67
+ #
68
+ # Examples:
69
+ #
70
+ # # returns the first child called "index"
71
+ # find_child("index")
72
+ #
73
+ # # returns the child called "index" with extension "html"
74
+ # find_child("index.html")
75
+ #
76
+ def find_child(name, opts = {})
77
+ name_attr = name.include?(".") ? "full_name" : "name"
78
+
79
+ @children.find do |c|
80
+ (c.should_publish? || !opts[:public_only]) &&
81
+ (c.send(name_attr) == name)
39
82
  end
40
83
  end
41
84
 
85
+ def emit_child(name)
86
+ # Override this to dynamically create child nodes.
87
+ end
88
+
42
89
  def tree_size
43
90
  children.inject(children.length) do |count, child|
44
91
  count + child.tree_size
@@ -46,109 +93,89 @@ module Flutterby
46
93
  end
47
94
 
48
95
  def parent=(new_parent)
96
+ # Remove from previous parent
49
97
  if @parent
50
98
  @parent.children.delete(self)
51
99
  end
52
100
 
101
+ # Assign new parent (it may be nil)
53
102
  @parent = new_parent
54
103
 
55
- @parent.children << self
104
+ # Notify new parent
105
+ if @parent
106
+ @parent.children << self
107
+ end
56
108
  end
57
109
 
58
110
  # Returns all children that will compile to a HTML page.
59
111
  #
60
112
  def pages
61
- children.select { |c| c.ext == "html" && c.should_publish? }
62
- end
63
- end
64
-
65
- concerning :Paths do
66
- def path
67
- parent ? ::File.join(parent.path, full_name) : full_name
68
- end
69
-
70
- def url
71
- ::File.join(parent ? parent.url : "/", full_name)
72
- end
73
-
74
- def full_fs_path(base:)
75
- ::File.expand_path(::File.join(base, full_name))
76
- end
77
- end
78
-
79
-
80
- concerning :Tree do
81
- def root
82
- parent ? parent.root : self
83
- end
84
-
85
- def root?
86
- root == self
87
- end
88
-
89
- def sibling(name)
90
- parent && parent.find(name)
113
+ children.select { |c| c.page? }
91
114
  end
92
115
 
93
- def siblings
94
- parent && parent.children
116
+ # Creates a new node, using the specified arguments, as a child
117
+ # of this node.
118
+ #
119
+ def create(name, **args)
120
+ args[:parent] = self
121
+ Node.new(name.to_s, **args)
95
122
  end
96
123
 
97
- def find(path)
98
- return self if path.nil? || path.empty?
124
+ def find(path, opts = {})
125
+ path = path.to_s
126
+ return self if path.empty?
99
127
 
100
128
  # remove duplicate slashes
101
- path.gsub!(%r{/+}, "/")
129
+ path = path.gsub(%r{/+}, "/")
102
130
 
103
131
  case path
132
+ # ./foo/...
104
133
  when %r{^\./?} then
105
- parent ? parent.find($') : root.find($')
134
+ parent ? parent.find($', opts) : root.find($', opts)
135
+
136
+ # /foo/...
106
137
  when %r{^/} then
107
- root.find($')
138
+ root.find($', opts)
139
+
140
+ # foo/...
108
141
  when %r{^([^/]+)/?} then
109
- child = find_child($1)
110
- $'.empty? ? child : child.find($')
142
+ # Use the next path part to find a child by that name.
143
+ # If no child can't be found, try to emit a child, but
144
+ # not if the requested name starts with an underscore.
145
+ if child = find_child($1, opts) || (emit_child($1) unless $1.start_with?("_"))
146
+ # Depending on the tail of the requested find expression,
147
+ # either return the found node, or ask it to find the tail.
148
+ $'.empty? ? child : child.find($', opts)
149
+ end
111
150
  end
112
151
  end
152
+ end
113
153
 
114
- # Walk the tree up, invoking the passed block for every node
115
- # found on the way, passing the node as its only argument.
116
- #
117
- def walk_up(val = nil, &blk)
118
- val = blk.call(self, val)
119
- parent ? parent.walk_up(val, &blk) : val
120
- end
154
+ include Tree
121
155
 
122
- # Walk the graph from the root to this node. Just like walk_up,
123
- # except the block will be called on higher level nodes first.
124
- #
125
- def walk_down(val = nil, &blk)
126
- val = parent ? parent.walk_up(val, &blk) : val
127
- blk.call(self, val)
128
- end
129
156
 
130
- # Walk the entire tree, top to bottom.
157
+ module Reading
158
+ # (Re-)loads the node from the filesystem, if it's a filesystem based
159
+ # node.
131
160
  #
132
- def walk_tree(val = nil, &blk)
133
- val = blk.call(self, val)
134
- children.each do |child|
135
- val = child.walk_tree(val, &blk)
136
- end
137
-
138
- val
139
- end
140
- end
141
-
142
- concerning :Reading do
143
161
  def reload!
144
- @body = nil
145
162
  @data = nil
163
+ @data_proxy = nil
164
+ @prefix = nil
165
+ @slug = nil
146
166
  @children = []
147
- @paths = {}
148
167
 
149
168
  load_from_filesystem! if @fs_path
169
+
170
+ extract_data!
171
+ end
172
+
173
+ def data
174
+ @data_proxy ||= Dotaccess[@data]
150
175
  end
151
176
 
177
+ private
178
+
152
179
  def load_from_filesystem!
153
180
  if @fs_path
154
181
  if ::File.directory?(fs_path)
@@ -161,32 +188,41 @@ module Flutterby
161
188
  end
162
189
  end
163
190
  end
164
- end
165
191
 
166
- concerning :Data do
167
- def data
168
- extract_data! if @data.nil?
169
- @data
170
- end
192
+ private
171
193
 
172
194
  def extract_data!
173
- @data ||= {}
195
+ @data ||= {}.with_indifferent_access
196
+
197
+ # Extract prefix and slug
198
+ if name =~ %r{\A([\d-]+)-(.+)\Z}
199
+ @prefix = $1
200
+ @slug = $2
201
+ else
202
+ @slug = name
203
+ end
174
204
 
175
- # Extract date from name
176
- if name =~ %r{^(\d\d\d\d\-\d\d?\-\d\d?)\-}
205
+ # Change this node's name to the slug. This may be made optional
206
+ # in the future.
207
+ @name = @slug
208
+
209
+ # Extract date from prefix if possible
210
+ if prefix =~ %r{\A(\d\d\d\d\-\d\d?\-\d\d?)\Z}
177
211
  @data['date'] = Date.parse($1)
178
212
  end
179
213
 
180
214
  # Read remaining data from frontmatter. Data in frontmatter
181
215
  # will always have precedence!
182
- parse_frontmatter!
216
+ extract_frontmatter!
183
217
 
184
- # Do some extra processing depending on extension
218
+ # Do some extra processing depending on extension. This essentially
219
+ # means that your .json etc. files will be rendered at least once at
220
+ # bootup.
185
221
  meth = "read_#{ext}!"
186
- send(meth) if respond_to?(meth)
222
+ send(meth) if respond_to?(meth, true)
187
223
  end
188
224
 
189
- def parse_frontmatter!
225
+ def extract_frontmatter!
190
226
  @data || {}
191
227
 
192
228
  if @source
@@ -203,11 +239,11 @@ module Flutterby
203
239
  end
204
240
 
205
241
  def read_json!
206
- @data.merge!(JSON.parse(body))
242
+ @data.merge!(JSON.parse(render))
207
243
  end
208
244
 
209
245
  def read_yaml!
210
- @data.merge!(YAML.load(body))
246
+ @data.merge!(YAML.load(render))
211
247
  end
212
248
 
213
249
  def read_yml!
@@ -215,85 +251,95 @@ module Flutterby
215
251
  end
216
252
 
217
253
  def read_toml!
218
- @data.merge!(TOML.parse(body))
254
+ @data.merge!(TOML.parse(render))
219
255
  end
220
256
  end
221
257
 
222
- concerning :Staging do
258
+ include Reading
259
+
260
+
261
+
262
+ module Staging
223
263
  def stage!
224
- # First of all, we want to make sure all nodes have their
225
- # available extensions loaded.
264
+ # First of all, we want to make sure all initializers
265
+ # (`_init.rb` files) are executed, starting at the top of the tree.
226
266
  #
227
- walk_tree do |node|
228
- node.load_extension! unless node.name == "_node"
267
+ TreeWalker.walk_tree(self) do |node|
268
+ if node.full_name == "_init.rb"
269
+ logger.debug "Executing initializer #{node.url}"
270
+ node.instance_eval(node.render)
271
+ end
229
272
  end
230
273
 
231
- # Now do another pass, prerendering stuff where necessary,
232
- # extracting data, registering URLs to be exported, etc.
274
+ # In a second pass, walk the tree to invoke any available
275
+ # setup methods.
233
276
  #
234
- walk_tree do |node|
235
- node.render_body! if node.should_prerender?
236
- node.register_url! if node.should_publish?
277
+ TreeWalker.walk_tree(self) do |node|
278
+ node.perform_setup!
237
279
  end
238
280
  end
239
281
 
240
- def load_extension!
241
- if extension = sibling("_node.rb")
242
- instance_eval(extension.body)
282
+ # Perform setup for this node. The setup step is run after the
283
+ # tree has been built up completely. It allows you to perform one-time
284
+ # setup operations that, for example, modify the tree (like sorting blog
285
+ # posts into date-specific subnodes.)
286
+ #
287
+ # Your nodes (or their extensions) may overload this method, but you
288
+ # may also simply use the `setup { ... }` syntax in a node extension
289
+ # to define a block of code to be run at setup time.
290
+ #
291
+ def perform_setup!
292
+ _setup_procs.each do |p|
293
+ instance_exec(&p)
243
294
  end
244
295
  end
245
296
 
246
- def should_prerender?
247
- !folder? &&
248
- (["json", "yml", "yaml", "rb", "toml"] & filters).any?
297
+ # Extend all of this node's siblings. See {#extend_all}.
298
+ #
299
+ def extend_siblings(*mods, &blk)
300
+ extend_all(siblings, *mods, &blk)
249
301
  end
250
- end
251
302
 
252
-
253
- concerning :Rendering do
254
- def view
255
- @view ||= View.for(self)
303
+ # Extend this node's parent. See {#extend_all}.
304
+ #
305
+ def extend_parent(*mods, &blk)
306
+ extend_all([parent], *mods, &blk)
256
307
  end
257
308
 
258
- def render_body!
259
- time = Benchmark.realtime do
260
- Filters.apply!(self)
309
+ # Extend all of the specified `nodes` with the specified module(s). If
310
+ # a block is given, the nodes will be extended with the code found
311
+ # in the block.
312
+ #
313
+ def extend_all(nodes, *mods, &blk)
314
+ if block_given?
315
+ mods << NodeExtension.new(&blk)
261
316
  end
262
317
 
263
- logger.info "Rendered #{url} in #{sprintf "%.1f", time * 1000}ms"
264
- end
265
-
266
- def body
267
- if @body.nil?
268
- data # make sure data is lazy-loaded
269
- render_body!
318
+ nodes.each do |n|
319
+ n.extend(*mods)
270
320
  end
271
-
272
- @body
273
321
  end
322
+ end
274
323
 
275
- def render(opts = {})
276
- layout = opts[:layout]
277
- view.opts.merge!(opts)
278
- (layout && apply_layout?) ? apply_layout(body) : body
279
- end
324
+ include Staging
280
325
 
281
- def apply_layout(input)
282
- walk_up(input) do |node, current|
283
- if layout = node.sibling("_layout")
284
- tilt = Flutterby::Filters.tilt(layout.ext, layout.source)
285
- tilt.render(view) { current }.html_safe
286
- else
287
- current
288
- end
289
- end
326
+
327
+ module Rendering
328
+ # Returns a freshly created {View} instance for this node.
329
+ #
330
+ def view(opts = {})
331
+ View.for(self, opts)
290
332
  end
291
333
 
292
- def apply_layout?
293
- page?
334
+ # Creates a new {View} instance through {#view} and uses it to
335
+ # render this node. Returns the rendered page as a string.
336
+ #
337
+ def render(opts = {})
338
+ view(opts).render!
294
339
  end
295
340
  end
296
341
 
342
+ include Rendering
297
343
 
298
344
 
299
345
 
@@ -302,8 +348,16 @@ module Flutterby
302
348
  # Misc
303
349
  #
304
350
 
351
+ # Returns the node's title. If there is a `:title` key in {#data}, its
352
+ # value will be used; otherwise, as a fallback, it will generate a
353
+ # human-readable title from {#slug}.
354
+ #
355
+ def title
356
+ data[:title] || slug.try(:titleize)
357
+ end
358
+
305
359
  def to_s
306
- "<#{self.class} #{self.path}>"
360
+ "<#{self.class} #{self.url}>"
307
361
  end
308
362
 
309
363
  def full_name
@@ -315,11 +369,11 @@ module Flutterby
315
369
  end
316
370
 
317
371
  def file?
318
- !folder?
372
+ !folder? && should_publish?
319
373
  end
320
374
 
321
375
  def page?
322
- !folder? && ext == "html"
376
+ file? && ext == "html"
323
377
  end
324
378
 
325
379
  def should_publish?
@@ -330,9 +384,10 @@ module Flutterby
330
384
  Flutterby.logger
331
385
  end
332
386
 
333
- def copy(new_name)
387
+ def copy(new_name, data = {})
334
388
  dup.tap do |c|
335
389
  c.name = new_name
390
+ c.data.merge!(data)
336
391
  parent.children << c
337
392
  end
338
393
  end
@@ -0,0 +1,23 @@
1
+ module Flutterby
2
+ # NodeExtension is a subclass of Module that also provides a convenient
3
+ # `setup` method for quick creation of initialization code. It's used
4
+ # to wrap the blocks of code passed to {Node#extend_all} and friends,
5
+ # but can also be used directly through `NodeExtension.new { ... }`.
6
+ #
7
+ class NodeExtension < Module
8
+ def initialize(*args)
9
+ @_setup_procs = []
10
+ super
11
+ end
12
+
13
+ def extended(base)
14
+ if @_setup_procs.any?
15
+ base._setup_procs.append(*@_setup_procs)
16
+ end
17
+ end
18
+
19
+ def on_setup(&blk)
20
+ @_setup_procs << blk
21
+ end
22
+ end
23
+ end
@@ -4,12 +4,11 @@ require 'better_errors'
4
4
 
5
5
  module Flutterby
6
6
  class Server
7
- def initialize(root, port: 4004)
7
+ def initialize(root)
8
8
  @root = root
9
- @port = port
10
9
  end
11
10
 
12
- def run!
11
+ def run!(port: 4004)
13
12
  # Set up listener
14
13
  listener = Listen.to(@root.fs_path) do |modified, added, removed|
15
14
  # puts "modified absolute path: #{modified}"
@@ -40,11 +39,11 @@ module Flutterby
40
39
 
41
40
  # Go!
42
41
  listener.start
43
- server.run app, Port: @port, Logger: Flutterby.logger
42
+ server.run app, Port: port, Logger: Flutterby.logger
44
43
  end
45
44
 
46
45
  def call(env)
47
- req = Rack::Request.new(env)
46
+ req = Rack::Request.new(env)
48
47
  res = Rack::Response.new([], 200, {})
49
48
 
50
49
  # Look for target node in path registry
@@ -65,9 +64,11 @@ module Flutterby
65
64
  end
66
65
 
67
66
  def find_node_for_path(path)
68
- @root.paths[path] ||
69
- @root.paths[path + ".html"] ||
70
- @root.paths[::File.join(path, "index.html")]
67
+ if node = @root.find(path, public_only: true)
68
+ # If the node is a folder, try and find its "index" node.
69
+ # Otherwise, use the node directly.
70
+ node.folder? ? node.find('index') : node
71
+ end
71
72
  end
72
73
  end
73
74
  end
@@ -0,0 +1,37 @@
1
+ module Flutterby
2
+ # A helper module with methods to walk across a node tree in various
3
+ # directions and variations and perform a block of code on each passed node.
4
+ #
5
+ module TreeWalker
6
+ extend self
7
+
8
+ # Walk the tree up, invoking the passed block for every node
9
+ # found on the way, passing the node as its only argument.
10
+ #
11
+ def walk_up(node, val = nil, &blk)
12
+ val = blk.call(node, val)
13
+ node.parent ? walk_up(node.parent, val, &blk) : val
14
+ end
15
+
16
+ # Walk the graph from the root to the specified node. Just like {#walk_up},
17
+ # except the block will be called on higher level nodes first.
18
+ #
19
+ def walk_down(node, val = nil, &blk)
20
+ val = node.parent ? walk_up(node.parent, val, &blk) : val
21
+ blk.call(node, val)
22
+ end
23
+
24
+ # Walk the entire tree, top to bottom, starting with its root, and then
25
+ # descending into its child layers.
26
+ #
27
+ def walk_tree(node, val = nil, &blk)
28
+ val = blk.call(node, val)
29
+
30
+ node.children.each do |child|
31
+ val = walk_tree(child, val, &blk)
32
+ end
33
+
34
+ val
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module Flutterby
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -1,6 +1,9 @@
1
+ require 'benchmark'
2
+
1
3
  module Flutterby
2
4
  class View
3
- attr_reader :node, :opts
5
+ attr_reader :node, :opts, :source
6
+ attr_accessor :_body
4
7
  alias_method :page, :node
5
8
 
6
9
  # Include ERB::Util from ActiveSupport. This will provide
@@ -10,9 +13,41 @@ module Flutterby
10
13
  #
11
14
  include ERB::Util
12
15
 
13
- def initialize(node)
16
+ def initialize(node, opts = {})
14
17
  @node = node
15
- @opts = {}
18
+ @opts = opts
19
+ @source = node.source
20
+ @_body = nil
21
+ end
22
+
23
+ def render!
24
+ time = Benchmark.realtime do
25
+ Filters.apply!(self)
26
+
27
+ # Apply layouts
28
+ if opts[:layout] && node.page?
29
+ @_body = apply_layout!(@_body)
30
+ end
31
+ end
32
+
33
+ logger.info "Rendered #{node.url} in #{sprintf "%.1f", time * 1000}ms"
34
+
35
+ @_body
36
+ end
37
+
38
+ def apply_layout!(input)
39
+ TreeWalker.walk_up(node, input) do |node, current|
40
+ if layout = node.sibling("_layout")
41
+ tilt = Flutterby::Filters.tilt(layout.ext, layout.source)
42
+ tilt.render(self) { current }.html_safe
43
+ else
44
+ current
45
+ end
46
+ end
47
+ end
48
+
49
+ def to_s
50
+ @_body ||= render!
16
51
  end
17
52
 
18
53
  def date_format(date, fmt)
@@ -24,7 +59,11 @@ module Flutterby
24
59
  end
25
60
 
26
61
  def render(expr, *args)
27
- find(expr).render(*args)
62
+ if expr.is_a?(Node)
63
+ expr.render(*args)
64
+ else
65
+ find(expr).render(*args)
66
+ end
28
67
  end
29
68
 
30
69
  def find(*args)
@@ -35,7 +74,7 @@ module Flutterby
35
74
  node.siblings(*args)
36
75
  end
37
76
 
38
- def tag(name, attributes)
77
+ def tag(name, attributes = {})
39
78
  ActiveSupport::SafeBuffer.new.tap do |output|
40
79
  attributes_str = attributes.keys.sort.map do |k|
41
80
  %{#{h k}="#{h attributes[k]}"}
@@ -61,16 +100,20 @@ module Flutterby
61
100
  tag(:pre, class: "debug") { h obj.to_yaml }
62
101
  end
63
102
 
103
+ def logger
104
+ @logger ||= Flutterby.logger
105
+ end
106
+
64
107
  class << self
65
108
  # Factory method that returns a newly created view for the given node.
66
109
  # It also makes sure all available _view.rb extensions are loaded.
67
110
  #
68
- def for(file)
111
+ def for(node, *args)
69
112
  # create a new view instance
70
- view = new(file)
113
+ view = new(node, *args)
71
114
 
72
115
  # walk the tree up to dynamically extend the view
73
- file.walk_down do |e|
116
+ TreeWalker.walk_down(node) do |e|
74
117
  if view_node = e.sibling("_view.rb")
75
118
  case view_node.ext
76
119
  when "rb" then
@@ -0,0 +1,9 @@
1
+ # This is your site's configuration file.
2
+
3
+ site:
4
+ title: My Flutterby Site
5
+ description: >
6
+ This is my new <a href="https://github.com/hmans/flutterby">Flutterby</a> Site. I should probably
7
+ change this description in my site's configuration file,
8
+ found at ./site/_config.yaml. Or I can just leave it as is.
9
+ Isn't choice wonderful?
@@ -2,7 +2,8 @@ doctype html
2
2
  html
3
3
  head
4
4
  meta charset="utf-8"
5
- title = config["site"]["title"]
5
+ title = config.site.title
6
+ meta name="viewport" content="width=device-width, initial-scale=1.0"
6
7
 
7
8
  // highlight.js for syntax highlighting
8
9
  link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/default.min.css"
@@ -17,4 +18,4 @@ html
17
18
  = yield
18
19
 
19
20
  footer role="main"
20
- == config["site"]["description"]
21
+ == config.site.description
@@ -0,0 +1,11 @@
1
+ # A _init.rb file contains Ruby code that will be executed when
2
+ # your application boots up. Use it to extend and modify other nodes!
3
+ #
4
+ # In this simple example, we're simply adding some convenience methods to
5
+ # all available blog posts for easier access to specific pieces of data.
6
+
7
+ extend_siblings do
8
+ def date
9
+ data.date
10
+ end
11
+ end
@@ -4,4 +4,4 @@ article.post
4
4
 
5
5
  h1 = page.title
6
6
 
7
- = page.body
7
+ = yield
@@ -8,7 +8,7 @@ date: <%= Date.today.to_s %>
8
8
 
9
9
  This sample project is set up as a simple blog, but of course you can do so much more with Flutterby. Just head straight into your project's `site` directory and mix things up. Some files you should look at:
10
10
 
11
- | `/_config.toml` | Your site configuration. |
11
+ | `/_config.yaml` | Your site configuration. |
12
12
  | `/css/styles.css.scss` | Your stylesheet. [Sass]-powered, of course! |
13
13
  | `/_layout.slim` | Your global site layout. |
14
14
  | `/posts/_layout.slim` | Your post-specific layout. |
@@ -1,4 +1,4 @@
1
- h1 = config["site"]["title"]
1
+ h1 = config.site.title
2
2
 
3
3
  p This is my new website! #{link_to "Find out more", "/about.html"}.
4
4
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flutterby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hendrik Mans
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-21 00:00:00.000000000 Z
11
+ date: 2017-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0.10'
111
+ - !ruby/object:Gem::Dependency
112
+ name: yard
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.9'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.9'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: erubis
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -343,6 +357,7 @@ files:
343
357
  - ".gitignore"
344
358
  - ".rspec"
345
359
  - ".travis.yml"
360
+ - ".yardopts"
346
361
  - CHANGES.md
347
362
  - Gemfile
348
363
  - LICENSE.txt
@@ -352,28 +367,34 @@ files:
352
367
  - bin/rake
353
368
  - bin/rspec
354
369
  - bin/setup
370
+ - bin/yard
371
+ - bin/yardoc
372
+ - bin/yri
355
373
  - exe/flutterby
356
374
  - flutterby.gemspec
357
375
  - lib/flutterby.rb
358
376
  - lib/flutterby/cli.rb
377
+ - lib/flutterby/dotaccess.rb
359
378
  - lib/flutterby/exporter.rb
360
379
  - lib/flutterby/filters.rb
361
380
  - lib/flutterby/markdown_formatter.rb
362
381
  - lib/flutterby/node.rb
382
+ - lib/flutterby/node_extension.rb
363
383
  - lib/flutterby/server.rb
384
+ - lib/flutterby/tree_walker.rb
364
385
  - lib/flutterby/version.rb
365
386
  - lib/flutterby/view.rb
366
387
  - lib/templates/new_project/.gitignore
367
388
  - lib/templates/new_project/Gemfile.tt
368
389
  - lib/templates/new_project/README.md
369
390
  - lib/templates/new_project/bin/flutterby
370
- - lib/templates/new_project/site/_config.toml
391
+ - lib/templates/new_project/site/_config.yaml
371
392
  - lib/templates/new_project/site/_layout.slim
372
393
  - lib/templates/new_project/site/_view.rb
373
394
  - lib/templates/new_project/site/about.html.md
395
+ - lib/templates/new_project/site/blog/_init.rb
374
396
  - lib/templates/new_project/site/blog/_layout.slim
375
397
  - lib/templates/new_project/site/blog/_list.html.slim
376
- - lib/templates/new_project/site/blog/_node.rb
377
398
  - lib/templates/new_project/site/blog/_view.rb
378
399
  - lib/templates/new_project/site/blog/hello-world.html.md.tt
379
400
  - lib/templates/new_project/site/css/styles.css.scss
@@ -1,10 +0,0 @@
1
- # This is your site's configuration file.
2
-
3
- [site]
4
- title = "My Flutterby Site"
5
- description = """
6
- This is my new <a href="https://github.com/hmans/flutterby">Flutterby</a> Site. I should probably
7
- change this description in my site's configuration file,
8
- found at ./site/_config.toml. Or I can just leave it as is.
9
- Isn't choice wonderful?
10
- """
@@ -1,14 +0,0 @@
1
- # A _node.rb will be run against all nodes from the same directory -- use it
2
- # to enhance the Ruby objects representing these nodes with extra methods
3
- # or behavior.
4
- #
5
- # In this example, we're simply adding some convenience methods for easier
6
- # access to specific pieces of data.
7
-
8
- def date
9
- data["date"]
10
- end
11
-
12
- def title
13
- data["title"]
14
- end