flutterby 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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