├── .gitignore ├── .rspec ├── .travis.yml ├── .yardopts ├── CHANGES.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── rake ├── rspec ├── setup ├── yard ├── yardoc └── yri ├── exe └── flutterby ├── flutterby.gemspec ├── lib ├── flutterby.rb ├── flutterby │ ├── cli.rb │ ├── config.rb │ ├── dotaccess.rb │ ├── event.rb │ ├── exporter.rb │ ├── filters.rb │ ├── layout.rb │ ├── livereload_server.rb │ ├── markdown_formatter.rb │ ├── node.rb │ ├── node │ │ ├── deletion.rb │ │ ├── event_handling.rb │ │ ├── reading.rb │ │ ├── rendering.rb │ │ ├── staging.rb │ │ ├── tree.rb │ │ └── url.rb │ ├── server.rb │ ├── tree_walker.rb │ ├── version.rb │ └── view.rb └── templates │ ├── 404.html │ └── new_project │ ├── .gitignore │ ├── Gemfile.tt │ ├── README.md │ ├── Rakefile │ ├── bin │ ├── flutterby │ └── rake │ ├── lib │ └── .keep │ └── site │ ├── _config.yaml │ ├── _layout.html.slim │ ├── _view.rb │ ├── about.html.md │ ├── blog │ ├── _init.rb │ ├── _layout.html.slim │ ├── _list.html.slim │ ├── _view.rb │ └── hello-world.html.md.tt │ ├── css │ └── styles.css.scss │ ├── index.html.slim │ └── js │ └── app.js └── spec ├── data_file_spec.rb ├── data_spec.rb ├── date_in_filename_spec.rb ├── dotaccess_spec.rb ├── emitters_spec.rb ├── event_spec.rb ├── exporter_spec.rb ├── filters ├── builder_spec.rb ├── markdown_spec.rb ├── ruby_node_spec.rb ├── sass_spec.rb ├── tilt_spec.rb └── unsupported_filter_spec.rb ├── find_spec.rb ├── frontmatter_spec.rb ├── html_escaping_spec.rb ├── initializer_spec.rb ├── layout_spec.rb ├── names_extensions_filters_spec.rb ├── node ├── deletion_spec.rb ├── event_handling_spec.rb ├── render_spec.rb └── url_spec.rb ├── node_spec.rb ├── prefix_and_slug_spec.rb ├── site ├── css │ ├── _partial.scss │ └── styles.css.scss ├── json_data.json ├── json_with_erb.json.erb ├── markdown.html.md ├── posts │ └── 2017-01-04-hello-world.html.md └── yaml_data.yaml ├── spec_helper.rb ├── title_spec.rb ├── tree_walker_spec.rb └── view_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /out/ 2 | /.bundle/ 3 | /.yardoc 4 | Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | /.sass-cache/ 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.6 5 | - 2.3.3 6 | - 2.4.0 7 | before_install: gem install bundler -v 1.13.6 8 | cache: bundler 9 | script: bundle exec rake 10 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --exclude lib/templates/ 2 | --embed-mixin "Flutterby::Node::*" 3 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Version History 2 | 3 | ### 0.7.0 (in development) 4 | 5 | - **NEW:** **LiveReload integration!** When viewing your site through `flutterby serve`, it will now be automatically reloaded when you make changes to a file. Flutterby will even try to apply changed stylesheets transparently, without reloading the page you're on. 6 | - **BREAKING CHANGE:** There has been a minor, but potentially breaking, change to how Flutterby deals with file names. Where previously Flutterby would always use the _first_ extension of a file as the target extension, it will now use the first extension suffix it considers to be static (eg. `js`, `html`, `txt` etc.) This allows for input files like `jquery.min.js` or `jquery.min.js.erb` to compile to `jquery.min.js` (where they would previously compile to `jquery.min`.) (#35) 7 | - **NEW:** **Event system!** (TODO: add link to tutorial.) 8 | - **NEW:** `flutterby build` now has a new `--prefix` option. When supplied, the specified URL will be used to prefix all URLs generated by `Node#url` (and paths generated by the new `Node#path`.) 9 | - **NEW:** `flutterby build` now has a new `--threads` option. Use it to specify the number of threads Flutterby should use when exporting your site. Depending on the nature of your site, using multiple threads may give exporting a considerable speed boost. 10 | - **NEW:** `flutterby serve` now has a new `--address` option you can use to configure the IP address/host name the server will bind to. (thanks @localhost) 11 | - **NEW:** If your project has a `/404.html(.*)` page, it will be used by `flutterby serve` to render 404 errors. If it doesn't, Flutterby now has a somewhat nicer to look at internal template that it will use. 12 | - **NEW:** Flutterby will now add your project's `lib` directory to Ruby's load path. Use it to house any Ruby modules that you want out of your site directory. 13 | 14 | 15 | ### 0.6.2 (2017-01-29) 16 | 17 | - **FIXED:** Images and other binaries would crash Flutterby when trying to extract frontmatter from them. Woops! ([#29](https://github.com/hmans/flutterby/issues/29)) 18 | 19 | 20 | ### 0.6.1 (2017-01-29) 21 | 22 | - **FIXED:** Front matter is now extracted using a non-greedy regular expression, fixing the problem with `---` horizontal rules in Markdown bodies. 23 | 24 | 25 | ### 0.6.0 (2017-01-26) 26 | 27 | - **NEW:** Within a view context, you can now invoke `render(node, as: "foo")`, and it will use a `_foo` partial in the same folder as `node`, passing `node` as a `foo` local. This allows you to easily apply decorator partials to nodes. 28 | - **NEW:** When invoking `Node#render`, you now have additional control over the layout behavior through the `layout` argument. Like before, when `true`, the default page layouts will be applied; when `false`, no layout will be applied whatsoever; but now you can also pass one or more nodes (or node selectors) that will be applied as layouts. 29 | - **BREAKING:** If you want to pass locals to `render`, you now need to use the `locals:` key. Example: `node.render(locals: { foo: "bar" })` 30 | - **IMPROVED:** The new project template has received some minor improvements, including a `Rakefile` containing an example `deploy` task. 31 | 32 | 33 | ### 0.5.2 (2017-01-25) 34 | 35 | - **NEW:** Just like `find`, there is now also a `find!` that will raise an exception when the specified node could not be found. 36 | - **NEW:** Nodes can now control the layout(s) that will be applied to them in their front matter through the `layout` keyword. 37 | 38 | 39 | ### 0.5.1 (2017-01-24) 40 | 41 | - **NEW:** Views now provide an `extend_view` method that you can (and should) use in `_view.rb` extensions. 42 | - **NEW:** Improved log output, especially when using `--debug`. 43 | 44 | 45 | ### 0.5.0 (2017-01-24) 46 | 47 | - **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]`. 48 | - **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`. 49 | - **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`.) 50 | - **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`. 51 | - **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. 52 | - **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. 53 | - **NEW:** The `flutterby build` and `flutterby serve` CLI commands now provide additional debug output when started with the `--debug` option. 54 | - **NEW:** Added `Node#create` as a convenience method for creating new child nodes below a given node. 55 | - **CHANGE:** Some massive refactoring, the primary intent being to perform the rendering of nodes in a thread-safe manner. 56 | 57 | 58 | ### 0.4.0 (2017-01-21) 59 | 60 | - **NEW:** Flutterby views now have a `tag` helper method available that can generate HTML tags programatically. 61 | - **NEW:** Flutterby views now have a `link_to` helper method available that renders link tags. You can use a URL string as the link target, eg. `link_to "Home", "/"`, or any Flutterby node, eg. `link_to "Blog", blog_node`. 62 | - **NEW:** Flutterby views now have a `debug` helper that will dump its argument's YAML representation into a `
` HTML tag (similar to Rails.)
63 | 
64 | 
65 | ### 0.3.1 (2017-01-15)
66 | 
67 | - **NEW:** Flutterby now uses ActiveSupport. It's a big dependency, but there's just so much useful goodness in there -- let's ride on the shoulders of that giant! This allows you to use all the neat little ActiveSupport toys you may know from Rails in your Flutterby project.
68 | - **CHANGE:** Thanks to the new inclusion of ActiveSupport, Flutterby now properly deals with HTML escaping (by way of `ActiveSupport::SafeBuffer`). This may not be critically important to static sites, but since Flutterby aspires to also power live sites, better make this change now than later. To sum things up, Flutterby now deals with HTML escaping pretty much like Rails does. Hooray!
69 | 
70 | 
71 | ### 0.2.0 (2017-01-13)
72 | 
73 | - **BREAKING CHANGE:** The default for `Node#render` is now to _not_ render a layout. Pass `layout: true` if you do want the node to be rendered within a layout.
74 | - **BREAKING CHANGE:** The behavior of `find` has now changed with regard to relative paths. `find(".")` will now return _the node's parent_ (ie. it's folder); before, a single dot would return the node itself. This change was made to make `find` behave more like what you would expect from a file system.
75 | - **CHANGE:** Stop the [Slodown] library from picking up Coderay et al to perform server-side syntax highlighting. This will probably be made configurable at some point in the future. For the time being, it is recommended to perform syntax highlighting through client-side libraries like [highlight.js].
76 | - **NEW:** You can now pass options to rendered partials. For example, when you invoke `<%= render("_foo.html.erb", name: "bar") %>`, `_foo.html.erb` can use `opts[:name]`.
77 | - **NEW:** `flutterby serve` now properly catches and displays exceptions via the [better_errors](https://github.com/charliesome/better_errors) gem.
78 | - **NEW:** For filters not natively supported by Flutterby, it will now fall back to [Tilt]. This means you can just add any gem supported by Tilt to your project to use it as a template language, with no Flutterby-specific plugin required. Hooray!
79 | - **NEW:** the `flutterby` CLI now has a `version` command that will print the version of Flutterby you're using.
80 | 
81 | 
82 | ### 0.1.0 (2017-01-11)
83 | 
84 | - First release!
85 | 
86 | 
87 | 
88 | 
89 | [Tilt]: https://github.com/rtomayko/tilt
90 | [Slodown]: http://github.com/hmans/slodown
91 | [highlight.js]: https://highlightjs.org/
92 | 


--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | source "https://rubygems.org"
3 | gemspec
4 | 


--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
 1 | The MIT License (MIT)
 2 | 
 3 | Copyright (c) 2017 Hendrik Mans
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # Flutterby 🦋
 2 | 
 3 | ### A flexible, Ruby-powered static site generator.
 4 | 
 5 | [![Gem Version](https://badge.fury.io/rb/flutterby.svg)](https://badge.fury.io/rb/flutterby) [![Build Status](https://travis-ci.org/hmans/flutterby.svg?branch=master)](https://travis-ci.org/hmans/flutterby) [![license](https://img.shields.io/github/license/hmans/flutterby.svg)](https://github.com/hmans/flutterby/blob/master/LICENSE.txt) ![Status](https://img.shields.io/badge/status-active-brightgreen.svg)
 6 | 
 7 | 
 8 | #### Links:
 9 | 
10 | - [Official Flutterby Website](http://www.flutterby.run/)
11 | - [Flutterby Documentation](http://www.flutterby.run/docs/) and [Reference](http://www.rubydoc.info/github/hmans/flutterby)
12 | - [Version History](https://github.com/hmans/flutterby/blob/master/CHANGES.md)
13 | - [Issues/Roadmap](https://github.com/hmans/flutterby/issues)
14 | 
15 | #### Examples:
16 | 
17 | - [Sites built with Flutterby](https://github.com/hmans/flutterby/wiki/Sites-built-with-Flutterby) (add yours!)
18 | - [New project template](https://github.com/hmans/flutterby/tree/master/lib/templates/new_project/site) (used with `flutterby new`)
19 | - [Source of hmans.io](https://github.com/hmans/hmans_me/tree/master/site), the Flutterby main author's blog
20 | 
21 | #### Blog Posts:
22 | 
23 | - [Blog post introducing Flutterby](http://hmans.io/posts/2017/01/11/flutterby.html)
24 | 
25 | 
26 | #### Installation & Basic Usage:
27 | 
28 | Please refer to the [Flutterby Documentation](http://www.flutterby.run/docs/).
29 | 
30 | 
31 | #### Contributing to Flutterby's Development:
32 | 
33 | Please refer to [Contributing to Flutterby](http://www.flutterby.run/docs/flutterby-development/contributing-to-flutterby.html) for details.
34 | 
35 | 
36 | 
37 | #### License:
38 | 
39 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
40 | 


--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rspec/core/rake_task"
3 | 
4 | RSpec::Core::RakeTask.new(:spec)
5 | 
6 | task :default => :spec
7 | 


--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env ruby
 2 | 
 3 | require "bundler/setup"
 4 | require "flutterby"
 5 | 
 6 | # You can add fixtures and/or initialization code here to make experimenting
 7 | # with your gem easier. You can also use a different console, if you like.
 8 | 
 9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 | 
13 | require "irb"
14 | IRB.start
15 | 


--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env ruby
 2 | # frozen_string_literal: true
 3 | #
 4 | # This file was generated by Bundler.
 5 | #
 6 | # The application 'rake' 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("rake", "rake")
18 | 


--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env ruby
 2 | # frozen_string_literal: true
 3 | #
 4 | # This file was generated by Bundler.
 5 | #
 6 | # The application 'rspec' 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("rspec-core", "rspec")
18 | 


--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 | 
6 | bundle install
7 | 
8 | # Do any other automated setup that you need to do here
9 | 


--------------------------------------------------------------------------------
/bin/yard:
--------------------------------------------------------------------------------
 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")
18 | 


--------------------------------------------------------------------------------
/bin/yardoc:
--------------------------------------------------------------------------------
 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")
18 | 


--------------------------------------------------------------------------------
/bin/yri:
--------------------------------------------------------------------------------
 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")
18 | 


--------------------------------------------------------------------------------
/exe/flutterby:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env ruby
 2 | if File.file?("Gemfile")
 3 |   require "bundler"
 4 |   Bundler.require
 5 | else
 6 |   require 'rubygems'
 7 | end
 8 | 
 9 | require 'flutterby/cli'
10 | Flutterby::CLI.start(ARGV)
11 | 


--------------------------------------------------------------------------------
/flutterby.gemspec:
--------------------------------------------------------------------------------
 1 | # coding: utf-8
 2 | lib = File.expand_path('../lib', __FILE__)
 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
 4 | require 'flutterby/version'
 5 | 
 6 | Gem::Specification.new do |spec|
 7 |   spec.name          = "flutterby"
 8 |   spec.version       = Flutterby::VERSION
 9 |   spec.authors       = ["Hendrik Mans"]
10 |   spec.email         = ["hendrik@mans.de"]
11 | 
12 |   spec.summary       = %q{A flexible, Ruby-powered website creation framework.}
13 |   spec.description   = %q{Flutterby is a flexible, Ruby-powered, routing graph-based web application framework that will serve your website dynamically or export it as a static site.}
14 |   spec.homepage      = "https://github.com/hmans/flutterby"
15 |   spec.license       = "MIT"
16 | 
17 |   spec.post_install_message = %q{Please note that Flutterby is still under heavy development. If you use it to build a website, please expect breakage in future versions! Please keep an eye (or two) on the official website at .}
18 | 
19 |   spec.required_ruby_version = '~> 2.2'
20 | 
21 |   spec.files         = `git ls-files -z`.split("\x0").reject do |f|
22 |     f.match(%r{^(test|spec|features)/})
23 |   end
24 |   spec.bindir        = "exe"
25 |   spec.executables   = ["flutterby"]
26 |   spec.require_paths = ["lib"]
27 | 
28 |   spec.add_development_dependency "bundler", "~> 1.13"
29 |   spec.add_development_dependency "rake", "~> 10.0"
30 |   spec.add_development_dependency "rspec", "~> 3.0"
31 |   spec.add_development_dependency "rspec-its", "~> 1.2"
32 |   spec.add_development_dependency 'awesome_print', '~> 1.7'
33 |   spec.add_development_dependency 'gem-release', '~> 0.7'
34 |   spec.add_development_dependency 'pry', '~> 0.10'
35 |   spec.add_development_dependency 'yard', '~> 0.9'
36 | 
37 |   spec.add_dependency 'colorize', '~> 0.8'
38 |   spec.add_dependency 'erubis', '~> 2.7'
39 |   spec.add_dependency 'erubis-auto', '~> 1.0'
40 |   spec.add_dependency 'json', '~> 2.0'
41 |   spec.add_dependency 'thor', '~> 0.19'
42 |   spec.add_dependency 'highline', '~> 1.7'
43 |   spec.add_dependency 'slodown', '~> 0.4'
44 |   spec.add_dependency 'toml-rb', '~> 0.3'
45 |   spec.add_dependency 'rack', '~> 2.0'
46 |   spec.add_dependency 'listen', '~> 3.1'
47 |   spec.add_dependency 'mime-types', '~> 3.1'
48 |   spec.add_dependency 'better_errors', '~> 2.1'
49 |   spec.add_dependency 'activesupport', '~> 5.0'
50 | 
51 |   # LiveReload related
52 |   spec.add_dependency 'rack-livereload', '~> 0.3.16'
53 |   spec.add_dependency 'em-websocket', '~> 0.5.1'
54 | 
55 |   # We support some template engines out of the box.
56 |   # There's a chance these will be extracted/made optional
57 |   # at some point in the future.
58 |   spec.add_dependency 'sass', '~> 3.4'
59 |   spec.add_dependency 'builder', '~> 3.2'
60 |   spec.add_dependency 'slim', '~> 3.0'
61 |   spec.add_dependency 'tilt', '~> 2.0'
62 | end
63 | 


--------------------------------------------------------------------------------
/lib/flutterby.rb:
--------------------------------------------------------------------------------
 1 | require 'active_support/all'
 2 | require 'toml'
 3 | require 'mime-types'
 4 | require 'json'
 5 | require 'colorize'
 6 | 
 7 | require "flutterby/dotaccess"
 8 | require "flutterby/version"
 9 | require "flutterby/tree_walker"
10 | require "flutterby/event"
11 | require "flutterby/node"
12 | require "flutterby/filters"
13 | require "flutterby/view"
14 | require "flutterby/config"
15 | 
16 | module Flutterby
17 |   extend self
18 | 
19 |   attr_writer :logger
20 | 
21 |   def logger
22 |     @logger ||= Logger.new(STDOUT)
23 |   end
24 | 
25 |   def config
26 |     @config ||= Flutterby::Config.new
27 |   end
28 | 
29 |   def reset_config!
30 |     @config = nil
31 |   end
32 | end
33 | 
34 | # Add local lib directory of project using this gem to load path
35 | $:.unshift File.join(Dir.getwd, "/lib")
36 | 


--------------------------------------------------------------------------------
/lib/flutterby/cli.rb:
--------------------------------------------------------------------------------
  1 | require 'flutterby'
  2 | require 'flutterby/exporter'
  3 | require "flutterby/server"
  4 | 
  5 | require 'thor'
  6 | require 'thor/group'
  7 | require 'highline/import'
  8 | require 'benchmark'
  9 | 
 10 | Flutterby.logger.level = Logger::INFO
 11 | 
 12 | module Flutterby
 13 |   class CLI < Thor
 14 |     include Thor::Actions
 15 | 
 16 |     desc "version", "Displays Flutterby's version"
 17 |     map "-v" => :version
 18 |     map "--version" => :version
 19 |     def version
 20 |       say_hi
 21 |     end
 22 | 
 23 | 
 24 |     desc "build", "Build your static site"
 25 | 
 26 |     option :in,
 27 |       default: "./site/",
 28 |       aliases: ["-i"],
 29 |       desc: "Input directory."
 30 | 
 31 |     option :out,
 32 |       default: "./_build/",
 33 |       aliases: ["-o"],
 34 |       desc: "Output directory."
 35 | 
 36 |     option :prefix,
 37 |       type: :string,
 38 |       aliases: ["-p"],
 39 |       desc: "URL or path prefix (used when generating URLs for nodes.)"
 40 | 
 41 |     option :threads,
 42 |       type: :numeric,
 43 |       default: 1,
 44 |       aliases: ["-t"],
 45 |       desc: "Number of threads to use."
 46 | 
 47 |     option :debug,
 48 |       default: false,
 49 |       aliases: ["-d"],
 50 |       type: :boolean,
 51 |       desc: "Print extra debugging information."
 52 | 
 53 |     def build
 54 |       Flutterby.config.prefix = options.prefix
 55 |       Flutterby.logger.level = options.debug ? Logger::DEBUG : Logger::INFO
 56 | 
 57 |       # Simplify logger output
 58 |       Flutterby.logger.formatter = proc do |severity, datetime, progname, msg|
 59 |         " • #{msg}\n"
 60 |       end
 61 | 
 62 |       say_hi
 63 | 
 64 |       time = Benchmark.realtime do
 65 |         # Import site
 66 |         say color("📚  Importing site...", :bold)
 67 |         root = Flutterby::Node.new("/", fs_path: options.in)
 68 |         root.stage!
 69 |         say color("🌲  Read #{root.size} nodes.", :green, :bold)
 70 | 
 71 |         # Export site
 72 |         say color("💾  Exporting site#{options.threads > 1 ? " (using #{options.threads} threads)" : ""}...", :bold)
 73 |         Flutterby::Exporter.new(root).export!(into: options.out, threads: options.threads)
 74 |       end
 75 | 
 76 |       say color("✅  Done. (took #{sprintf "%.2f", time}s)", :green, :bold)
 77 |     end
 78 | 
 79 | 
 80 |     desc "serve", "Serve your site locally"
 81 | 
 82 |     option :in,
 83 |       default: "./site/",
 84 |       aliases: ["-i"],
 85 |       desc: "Input directory."
 86 | 
 87 |     option :address,
 88 |       default: "localhost",
 89 |       aliases: ["-a"]
 90 | 
 91 |     option :port,
 92 |       default: 4004,
 93 |       aliases: ["-p"],
 94 |       type: :numeric
 95 | 
 96 |     option :debug,
 97 |       default: false,
 98 |       aliases: ["-d"],
 99 |       type: :boolean
100 | 
101 |     def serve
102 |       Flutterby.logger.level = options.debug ? Logger::DEBUG : Logger::INFO
103 | 
104 |       say_hi
105 | 
106 |       say color("📚  Importing site...", :bold)
107 |       root = Flutterby::Node.new("/", fs_path: options.in)
108 |       root.stage!
109 |       say color("🌲  Read #{root.size} nodes.", :green, :bold)
110 | 
111 |       say color("🌤  Serving your Flutterby site on http://#{options.address}:#{options.port} - enjoy! \\o/", :bold)
112 |       server = Flutterby::Server.new(root)
113 |       server.run!(address: options.address, port: options.port)
114 |     end
115 | 
116 | 
117 |     desc "new PATH", "Create a new Flutterby project"
118 |     def new(path)
119 |       say_hi
120 | 
121 |       path = File.expand_path(path)
122 |       self.destination_root = path
123 | 
124 |       say color("🏗  Creating a new Flutterby project in #{path}...", :bold)
125 |       directory("new_project", path)
126 |       chmod("bin/flutterby", 0755)
127 |       chmod("bin/rake", 0755)
128 |       in_root { bundle_install }
129 |     end
130 | 
131 |     private
132 | 
133 |     def bundle_install
134 |       if defined?(Bundler)
135 |         Bundler.with_clean_env do
136 |           run "bundle install"
137 |         end
138 |       else
139 |         run "bundle install"
140 |       end
141 |     end
142 | 
143 |     def color(*args)
144 |       $terminal.color(*args)
145 |     end
146 | 
147 |     def say_hi
148 |       say color("🦋  Flutterby #{Flutterby::VERSION}", :bold, :blue)
149 |     end
150 | 
151 |     def self.source_root
152 |       File.expand_path("../templates/", File.dirname(__FILE__))
153 |     end
154 |   end
155 | end
156 | 


--------------------------------------------------------------------------------
/lib/flutterby/config.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   class Config
 3 |     attr_reader :prefix, :prefix_uri
 4 | 
 5 |     def prefix=(prefix)
 6 |       @prefix_uri = prefix && URI(prefix)
 7 |       @prefix = prefix
 8 |     end
 9 |   end
10 | end
11 | 


--------------------------------------------------------------------------------
/lib/flutterby/dotaccess.rb:
--------------------------------------------------------------------------------
 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
32 | 


--------------------------------------------------------------------------------
/lib/flutterby/event.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   # An Event instance wraps an event emitted by nodes through {Node#emit}.
 3 |   # It will always be passed back into event handlers as their first argument.
 4 |   #
 5 |   class Event
 6 |     attr_reader :name, :time, :args
 7 |     attr_accessor :source
 8 |     alias_method :node, :source
 9 | 
10 |     def initialize(name, source: nil, args: {})
11 |       @name = name.to_sym
12 |       @source = source
13 |       @args = args
14 |       @time = Time.now
15 |     end
16 | 
17 |     def to_sym
18 |       name
19 |     end
20 | 
21 |     def ==(o)
22 |       name == o.to_sym
23 |     end
24 |   end
25 | end
26 | 


--------------------------------------------------------------------------------
/lib/flutterby/exporter.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   class Exporter
 3 |     def initialize(root)
 4 |       @root = root
 5 |     end
 6 | 
 7 |     def export!(into:, threads: 1)
 8 |       # Set up queue
 9 |       q = Queue.new
10 |       @root.all_nodes.each { |node| q.push(node) }
11 | 
12 |       # Work through queue
13 |       workers = (1..threads).map do
14 |         Thread.new do
15 |           begin
16 |             while node = q.pop(true)
17 |               export_node(node, into: into)
18 |             end
19 |           rescue ThreadError
20 |           end
21 |         end
22 |       end.map(&:join)
23 |     end
24 | 
25 |     private
26 | 
27 |     def export_node(node, into:)
28 |       return unless node.should_publish?
29 | 
30 |       path = ::File.expand_path(::File.join(into, node.internal_path))
31 | 
32 |       if node.file?
33 |         FileUtils.mkdir_p(File.dirname(path))
34 |         File.write(path, node.render(layout: true))
35 |         logger.info "Exported #{node.internal_path.colorize(:light_white)}"
36 |       else
37 |         FileUtils.mkdir_p(path)
38 |       end
39 |     end
40 | 
41 |     def logger
42 |       @logger ||= Flutterby.logger
43 |     end
44 |   end
45 | end
46 | 


--------------------------------------------------------------------------------
/lib/flutterby/filters.rb:
--------------------------------------------------------------------------------
 1 | require 'erubis'
 2 | require 'erubis/auto'
 3 | require 'sass'
 4 | require 'tilt'
 5 | require 'slim'
 6 | require 'builder'
 7 | require 'flutterby/markdown_formatter'
 8 | 
 9 | module Flutterby
10 |   module Filters
11 |     extend self
12 | 
13 |     def apply!(input, filters, view:, &blk)
14 |       # Apply all filters
15 |       filters.inject(input) do |body, filter|
16 |         meth = "process_#{filter}!"
17 | 
18 |         if Filters.respond_to?(meth)
19 |           Filters.send(meth, body, view: view, &blk)
20 |         else
21 |           Flutterby.logger.warn "Unsupported filter '#{filter}'"
22 |           body
23 |         end
24 |       end
25 |     end
26 | 
27 |     def supported?(fmt)
28 |       respond_to?("process_#{fmt}!")
29 |     end
30 | 
31 |     def add(fmts, &blk)
32 |       Array(fmts).each do |fmt|
33 |         define_singleton_method("process_#{fmt}!", &blk)
34 |       end
35 |     end
36 | 
37 |     def enable_tilt(*fmts)
38 |       Array(fmts).flatten.each do |fmt|
39 |         add(fmt) do |input, view:, &blk|
40 |           tilt(fmt, input).render(view, view.locals, &blk).html_safe
41 |         end
42 |       end
43 |     end
44 | 
45 |     def tilt(format, body, options = {})
46 |       default_options = {
47 |         "erb" => { engine_class: Erubis::Auto::EscapedEruby }
48 |       }
49 | 
50 |       options = default_options.fetch(format, {}).merge(options)
51 | 
52 |       t = Tilt[format] and t.new(options) { body }
53 |     end
54 |   end
55 | end
56 | 
57 | # Add a bunch of formats that we support through Tilt
58 | Flutterby::Filters.enable_tilt("erb", "slim", "haml",
59 |   "coffee", "rdoc", "builder", "jbuilder")
60 | 
61 | Flutterby::Filters.add("rb") do |input, view:|
62 |   view.instance_eval(input)
63 | end
64 | 
65 | Flutterby::Filters.add(["md", "markdown"]) do |input, view:|
66 |   Flutterby::MarkdownFormatter.new(input).complete.to_s.html_safe
67 | end
68 | 
69 | Flutterby::Filters.add("scss") do |input, view:|
70 |   sass_options = {
71 |     syntax: :scss,
72 |     load_paths: []
73 |   }
74 | 
75 |   if view.node.fs_path
76 |     sass_options[:load_paths] << File.dirname(view.node.fs_path)
77 |   end
78 | 
79 |   Sass::Engine.new(input, sass_options).render
80 | end
81 | 


--------------------------------------------------------------------------------
/lib/flutterby/layout.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   module Layout
 3 |     extend self
 4 | 
 5 |     def collect_layouts(node, list: nil, include_tree: true)
 6 |       layouts = []
 7 |       list ||= node.layout
 8 | 
 9 |       # Collect layouts explicitly configured for node
10 |       if defined? list
11 |         Array(list).each do |sel|
12 |           # If a false is explicity specified, that's all the layouts
13 |           # we're expected to render
14 |           return layouts if sel == false
15 | 
16 |           if layout = node.find(sel)
17 |             layouts << layout
18 |           else
19 |             raise "No layout found for path expression '#{sel}'"
20 |           end
21 |         end
22 |       end
23 | 
24 |       if include_tree
25 |         # Decide on a starting node for walking the tree upwards
26 |         start = layouts.any? ? layouts.last.parent : node
27 | 
28 |         # Walk the tree up, collecting any layout files found on our way
29 |         TreeWalker.walk_up(start) do |n|
30 |           if layout = n.sibling("_layout")
31 |             layouts << layout
32 |           end
33 |         end
34 |       end
35 | 
36 |       layouts
37 |     end
38 |   end
39 | end
40 | 


--------------------------------------------------------------------------------
/lib/flutterby/livereload_server.rb:
--------------------------------------------------------------------------------
 1 | require 'em-websocket'
 2 | 
 3 | module Flutterby
 4 |   class LiveReloadServer
 5 |     attr_reader :options
 6 | 
 7 |     DEFAULT_OPTIONS = {
 8 |       host: "localhost",
 9 |       port: 35729
10 |     }
11 | 
12 |     def initialize(root, options = {})
13 |       @root = root
14 |       @options = DEFAULT_OPTIONS.merge(options)
15 |       @sockets = []
16 |       @thread = start
17 |     end
18 | 
19 |     def stop
20 |       @thread.kill
21 |     end
22 | 
23 |     def trigger_reload(paths = [])
24 |       Array(paths).each do |path|
25 |         # Find node corresponding to path
26 |         if node = @root.find_for_fs_path(path)
27 |           path = node.url
28 |         end
29 | 
30 |         data = JSON.dump(['refresh', {
31 |           path: path,
32 |           apply_js_live: true,
33 |           apply_css_live: true
34 |         }])
35 | 
36 |         @sockets.each { |ws| ws.send(data) }
37 |       end
38 |     end
39 | 
40 | 
41 |     private
42 | 
43 |     def start
44 |       logger.info "Starting LiveReload websocket server on #{options[:host]}:#{options[:port]}"
45 |       Thread.new do
46 |         EventMachine.run do
47 |           EventMachine.start_server(options[:host], options[:port], EventMachine::WebSocket::Connection, {}) do |ws|
48 |             ws.onopen do
49 |               begin
50 |                 @sockets << ws
51 |                 ws.send "!!ver:1.6"
52 |               rescue
53 |                 logger.error $!
54 |               end
55 |             end
56 | 
57 |             ws.onmessage do |msg|
58 |               logger.debug "LiveReload message: #{msg}"
59 |             end
60 | 
61 |             ws.onclose do
62 |               @sockets.delete(ws)
63 |             end
64 |           end
65 |         end
66 |       end
67 |     end
68 | 
69 |     def logger
70 |       Flutterby.logger
71 |     end
72 |   end
73 | end
74 | 


--------------------------------------------------------------------------------
/lib/flutterby/markdown_formatter.rb:
--------------------------------------------------------------------------------
 1 | require 'slodown'
 2 | 
 3 | module Flutterby
 4 |   class MarkdownFormatter < Slodown::Formatter
 5 |     def complete
 6 |       markdown.autolink.sanitize
 7 |     end
 8 | 
 9 |     def kramdown_options
10 |       {
11 |         input: "GFM",
12 |         hard_wrap: false,
13 |         syntax_highlighter: nil,
14 |         syntax_highlighter_opts: { }
15 |       }
16 |     end
17 |   end
18 | end
19 | 


--------------------------------------------------------------------------------
/lib/flutterby/node.rb:
--------------------------------------------------------------------------------
  1 | require 'flutterby/node/tree'
  2 | require 'flutterby/node/deletion'
  3 | require 'flutterby/node/reading'
  4 | require 'flutterby/node/event_handling'
  5 | require 'flutterby/node/staging'
  6 | require 'flutterby/node/rendering'
  7 | require 'flutterby/node/url'
  8 | 
  9 | module Flutterby
 10 |   class Node
 11 |     attr_accessor :name, :ext
 12 |     attr_reader :filters, :fs_path, :prefix, :slug, :timestamp
 13 | 
 14 |     def initialize(name = nil, parent: nil, fs_path: nil, source: nil)
 15 |       raise "Either name or fs_path need to be specified." unless name || fs_path
 16 | 
 17 |       clear!
 18 | 
 19 |       @original_name = name || File.basename(fs_path)
 20 |       @fs_path = fs_path ? ::File.expand_path(fs_path) : nil
 21 |       @source  = source
 22 | 
 23 |       # Register this node with its parent
 24 |       if parent
 25 |         self.parent = parent
 26 |       end
 27 | 
 28 |       load!
 29 | 
 30 |       logger.debug "Created node #{internal_path.colorize(:green)}"
 31 |     end
 32 | 
 33 |     private def clear!
 34 |       @data     = nil
 35 |       @data_proxy = nil
 36 |       @prefix   = nil
 37 |       @slug     = nil
 38 |     end
 39 | 
 40 |     prepend Tree
 41 |     prepend Deletion
 42 |     prepend Reading
 43 |     prepend EventHandling
 44 |     prepend Staging
 45 |     prepend Rendering
 46 |     prepend Url
 47 | 
 48 | 
 49 |     # Returns the node's title. If there is a `:title` key in {#data}, its
 50 |     # value will be used; otherwise, as a fallback, it will generate a
 51 |     # human-readable title from {#slug}.
 52 |     #
 53 |     def title
 54 |       data[:title] || slug.try(:titleize)
 55 |     end
 56 | 
 57 | 
 58 |     # Returns the layout(s) configured for this node. This is sourced from
 59 |     # the node's {data} attribute, so it can be set from front matter.
 60 |     #
 61 |     def layout
 62 |       data[:layout]
 63 |     end
 64 | 
 65 |     def to_s
 66 |       "<#{self.class} #{internal_path}>"
 67 |     end
 68 | 
 69 |     def full_name
 70 |       [name, ext].compact.join(".")
 71 |     end
 72 | 
 73 |     def mime_type
 74 |       (ext && MIME::Types.type_for(ext).first) || MIME::Types["text/plain"].first
 75 |     end
 76 | 
 77 |     def file?
 78 |       !folder? && should_publish?
 79 |     end
 80 | 
 81 |     def page?
 82 |       file? && ext == "html"
 83 |     end
 84 | 
 85 |     def should_publish?
 86 |       (!name.start_with?("_") && !deleted?) && (parent ? parent.should_publish? : true)
 87 |     end
 88 | 
 89 |     def logger
 90 |       Flutterby.logger
 91 |     end
 92 | 
 93 |     def copy(new_name, data = {})
 94 |       full_new_name = [new_name, ext, filters.reverse].flatten.join(".")
 95 | 
 96 |       parent.create(full_new_name, source: source, fs_path: fs_path).tap do |node|
 97 |         node.data.merge!(data)
 98 |       end
 99 |     end
100 |   end
101 | end
102 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/deletion.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   module Deletion
 3 |     def initialize(*args)
 4 |       @deleted = false
 5 |       super
 6 |     end
 7 | 
 8 |     def deleted?
 9 |       @deleted
10 |     end
11 | 
12 |     def delete!
13 |       emit(:deleted)
14 |       move_to(nil)
15 |       @deleted = true
16 |     end
17 |   end
18 | end
19 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/event_handling.rb:
--------------------------------------------------------------------------------
  1 | module Flutterby
  2 |   # Methods related to the emitting and handling of events.
  3 |   #
  4 |   module EventHandling
  5 |     def self.prepended(base)
  6 |       base.send :attr_reader, :event_handlers
  7 |     end
  8 | 
  9 |     def clear!
 10 |       super
 11 |       @event_handlers = {}
 12 |     end
 13 | 
 14 |     # Emits a new event from this node. Emitting an event will make it
 15 |     # travel up the tree, starting with this node and ending with the tree's root
 16 |     # note, invoking the corresponding handlers on each node (including the one that
 17 |     # generated the event.)
 18 |     #
 19 |     # @param [Symbol] evt The event to emit. Can be any symbol.
 20 |     # @param args Any extra arguments to be attached to the event. These will be passed back into event handlers.
 21 |     #
 22 |     # @example Basic invocation
 23 |     #   node.emit(:foo)
 24 |     #
 25 |     # @example Emitting an event with extra arguments
 26 |     #   node.emit(:foo, name: "John")
 27 |     #
 28 |     def emit(evt, **args)
 29 |       if evt.is_a?(Event)
 30 |         evt.source = self
 31 |       else
 32 |         evt = Event.new(evt, source: self, args: args)
 33 |       end
 34 | 
 35 |       logger.debug "#{self.internal_path.colorize(:green)} emitting event '#{evt.name}' with #{evt.args.inspect}"
 36 | 
 37 |       TreeWalker.walk_up(self) do |node|
 38 |         node.handle(evt)
 39 |       end
 40 |     end
 41 | 
 42 |     # @!visibility private
 43 |     def respond_to?(meth, *args)
 44 |       super || (meth =~ %r{\Ahandle_(.+)\Z} && can_handle?($1))
 45 |     end
 46 | 
 47 |     # Handle an incoming event. This will dispatch to a handle_* method if
 48 |     # it's available.
 49 |     #
 50 |     def handle(evt)
 51 |       meth = "handle_#{evt.name}"
 52 |       send(meth, evt, evt.source) if respond_to?(meth)
 53 |     end
 54 | 
 55 |     # Register an event handler.
 56 |     #
 57 |     # @param [Symbol] evts The event -- or array of events -- that should trigger this handler.
 58 |     # @param [String|Node|Proc|Regexp] selector A selector that will be
 59 |     #   matched against the node that emitted the event. If given, the
 60 |     #   handler will only be executed if the selector matches. If {selector}
 61 |     #   is a String, it matches if it equals the node's path. If it is a
 62 |     #   Regexp, it matches when it matches against the node's path.
 63 |     #   If it is a Proc, the proc will be executed (with the originating node)
 64 |     #   as its only argument), and the handler will be executed if the proc
 65 |     #   returns true.
 66 |     #   If selector is a {Node} instance, it matches if the specified node is
 67 |     #   the originating node.
 68 |     #
 69 |     def on(names, selector = nil, &blk)
 70 |       Array(names).map do |name|
 71 |         name = name.to_sym
 72 |         @event_handlers[name] ||= []
 73 |         @event_handlers[name] << { selector: selector, blk: blk }
 74 |       end
 75 |     end
 76 | 
 77 |     private
 78 | 
 79 |     def method_missing(meth, *args, &blk)
 80 |       if meth =~ %r{\Ahandle_(.+)\Z} && can_handle?($1)
 81 |         execute_event_handlers($1, *args)
 82 |       else
 83 |         super
 84 |       end
 85 |     end
 86 | 
 87 |     def execute_event_handlers(evt_name, evt, *args)
 88 |       event_handlers_for(evt_name).each do |handler|
 89 |         if evt.source.event_handler_applies?(handler)
 90 |           handler[:blk].call(evt, *args)
 91 |         end
 92 |       end
 93 |     end
 94 | 
 95 |     protected def event_handler_applies?(handler)
 96 |       selector = handler[:selector]
 97 | 
 98 |       case selector
 99 |       when nil    then true
100 |       when String then internal_path == selector
101 |       when Regexp then internal_path =~ selector
102 |       when Node   then self == selector
103 |       when Proc   then selector.call(self)
104 |       end
105 |     end
106 | 
107 |     def can_handle?(evt)
108 |       !!event_handlers_for(evt)
109 |     end
110 | 
111 |     def event_handlers_for(evt)
112 |       @event_handlers[evt.to_sym]
113 |     end
114 |   end
115 | end
116 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/reading.rb:
--------------------------------------------------------------------------------
  1 | module Flutterby
  2 |   module Reading
  3 |     # Reloads the node from the filesystem, if it's a filesystem based
  4 |     # node.
  5 |     #
  6 |     def reload!
  7 |       logger.info "Reloading #{url.colorize(:blue)}"
  8 | 
  9 |       time = Benchmark.realtime do
 10 |         load!
 11 |         stage!
 12 |         emit(:reloaded)
 13 |       end
 14 | 
 15 |       logger.info "Reloaded #{url.colorize(:blue)} in #{sprintf("%.1fms", time * 1000).colorize(:light_white)}"
 16 |     end
 17 | 
 18 |     def data
 19 |       @data_proxy ||= Dotaccess[@data]
 20 |     end
 21 | 
 22 |     # Will return the node's current source. If no source is stored with the
 23 |     # node instance, this method will load (but not memoize) the contents of
 24 |     # the file backing this node.
 25 |     #
 26 |     def source
 27 |       @source || load_file_contents
 28 |     end
 29 | 
 30 |     private
 31 | 
 32 |     # Pre-load the contents of the file backing this node and store it with
 33 |     # this node instance.
 34 |     #
 35 |     def load_source!
 36 |       @source = load_file_contents
 37 |     end
 38 | 
 39 |     def load_file_contents
 40 |       if fs_path && File.file?(fs_path)
 41 |         logger.debug "Pre-loading source for #{url.colorize(:blue)}"
 42 |         File.read(fs_path)
 43 |       end
 44 |     end
 45 | 
 46 |     def load!
 47 |       clear!
 48 |       @timestamp = Time.now
 49 | 
 50 |       # Extract name, extension, and filters from given name
 51 |       @name, @ext, @filters = split_filename(@original_name)
 52 | 
 53 |       load_from_filesystem! if @fs_path
 54 | 
 55 |       extract_data!
 56 |     end
 57 | 
 58 |     def split_filename(name)
 59 |       parts   = name.split(".")
 60 |       name    = []
 61 |       filters = []
 62 | 
 63 |       # The first part is always part of the name
 64 |       name << parts.shift
 65 | 
 66 |       # Extract filters
 67 |       while parts.any? && Filters.supported?(parts.last)
 68 |         filters << parts.pop
 69 |       end
 70 | 
 71 |       # Assign extension
 72 |       ext = parts.any? ? parts.pop : filters.pop
 73 | 
 74 |       # Make the remainder part of the name
 75 |       name += parts
 76 | 
 77 |       [name.join("."), ext, filters]
 78 |     end
 79 | 
 80 |     def load_from_filesystem!
 81 |       if @fs_path
 82 |         @timestamp = File.mtime(fs_path)
 83 | 
 84 |         if ::File.directory?(fs_path)
 85 |           Dir[::File.join(fs_path, "*")].each do |entry|
 86 |             name = ::File.basename(entry)
 87 |             Flutterby::Node.new(name, parent: self, fs_path: entry)
 88 |           end
 89 | 
 90 |         # If the file starts with frontmatter, load its entire source
 91 |         # and keep it
 92 |         elsif preload_source?
 93 |           load_source!
 94 |         end
 95 |       end
 96 |     end
 97 | 
 98 |     # Returns true if the source for this node should be preloaded.
 99 |     #
100 |     def preload_source?
101 |       mime_type.ascii? || File.size(fs_path) < 5.kilobytes
102 |     end
103 | 
104 |     def extract_data!
105 |       @data ||= {}.with_indifferent_access
106 | 
107 |       # Extract prefix and slug
108 |       if name =~ %r{\A([\d-]+)-(.+)\Z}
109 |         @prefix = $1
110 |         @slug = $2
111 |       else
112 |         @slug = name
113 |       end
114 | 
115 |       # Change this node's name to the slug. This may be made optional
116 |       # in the future.
117 |       @name = @slug
118 | 
119 |       # Extract date from prefix if possible
120 |       if prefix =~ %r{\A(\d\d\d\d\-\d\d?\-\d\d?)\Z}
121 |         @data['date'] = Date.parse($1)
122 |       end
123 | 
124 |       # Read remaining data from frontmatter. Data in frontmatter
125 |       # will always have precedence!
126 |       extract_frontmatter!
127 | 
128 |       # Do some extra processing depending on extension. This essentially
129 |       # means that your .json etc. files will be rendered at least once at
130 |       # bootup.
131 |       meth = "read_#{ext}!"
132 |       send(meth) if respond_to?(meth, true)
133 |     end
134 | 
135 |     def extract_frontmatter!
136 |       # If the file backing this node has frontmatter, preload the source.
137 |       load_source! if file_has_frontmatter?
138 | 
139 |       # If any source is available at all, let's try and parse it for
140 |       # frontmatter.
141 |       if @source
142 |         # YAML Front Matter
143 |         if @source.sub!(/\A\-\-\-\n(.+?)\n\-\-\-\n/m, "")
144 |           @data.merge! YAML.load($1)
145 |         end
146 | 
147 |         # TOML Front Matter
148 |         if @source.sub!(/\A\+\+\+\n(.+?)\n\+\+\+\n/m, "")
149 |           @data.merge! TOML.parse($1)
150 |         end
151 |       end
152 |     rescue ArgumentError => e
153 |     end
154 | 
155 |     def file_has_frontmatter?
156 |       if fs_path && File.file?(fs_path)
157 |         first_line = File.open(fs_path, &:readline)
158 |         ["---\n", "+++\n"].include? first_line
159 |       end
160 |     rescue EOFError
161 |       false
162 |     end
163 | 
164 |     def read_json!
165 |       @data.merge!(JSON.parse(render))
166 |     end
167 | 
168 |     def read_yaml!
169 |       @data.merge!(YAML.load(render))
170 |     end
171 | 
172 |     def read_yml!
173 |       read_yaml!
174 |     end
175 | 
176 |     def read_toml!
177 |       @data.merge!(TOML.parse(render))
178 |     end
179 |   end
180 | end
181 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/rendering.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   module Rendering
 3 |     # Returns true if this node can be rendered.
 4 |     #
 5 |     def can_render?
 6 |       !source.nil?
 7 |     end
 8 | 
 9 |     # Renders the node. One of the most important methods in Flutterby, which
10 |     # explains why it's wholly undocumented. Apologies, I'm working on it!
11 |     #
12 |     def render(layout: false, view: nil, extra_filters: [], locals: {}, &blk)
13 |       raise "Nodes without source can't be rendered" unless can_render?
14 | 
15 |       # If no view was specified, create a new one for this node.
16 |       view ||= View.for(self, locals: locals)
17 |       layouts = []
18 | 
19 |       if layout == true
20 |         # build standard list of layouts for rendering the full page
21 |         layouts = Layout.collect_layouts(self, include_tree: page?)
22 |       elsif layout
23 |         # build list of nodes based on specified layouts
24 |         layouts = Layout.collect_layouts(self, list: layout, include_tree: false)
25 |       end
26 | 
27 |       # Start rendering
28 |       output = ""
29 |       time = Benchmark.realtime do
30 |         # Apply filters
31 |         output = Filters.apply! source.html_safe,
32 |           filters + extra_filters,
33 |           view: view, &blk
34 | 
35 |         # Apply layouts
36 |         output = layouts.inject(output) do |acc, layout_node|
37 |           layout_node.render(layout: false,
38 |             view: view, extra_filters: [layout_node.ext]) { acc }
39 |         end
40 |       end
41 | 
42 |       # Log rendering times using different colors based on duration
43 |       color = if time > 1
44 |         :red
45 |       elsif time > 0.25
46 |         :yellow
47 |       else
48 |         :green
49 |       end
50 | 
51 |       logger.debug "Rendered #{internal_path.colorize(:magenta)} in #{sprintf("%.1fms", time * 1000).colorize(color)}"
52 | 
53 |       output
54 |     end
55 |   end
56 | end
57 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/staging.rb:
--------------------------------------------------------------------------------
 1 | module Flutterby
 2 |   module Staging
 3 |     def stage!
 4 |       # First of all, we want to make sure all initializers
 5 |       # (`_init.rb` files) are executed, starting at the top of the tree.
 6 |       #
 7 |       TreeWalker.walk_tree(self) do |node|
 8 |         if node.full_name == "_init.rb"
 9 |           node.parent.load_initializer!(node)
10 |         end
11 |       end
12 | 
13 |       # In a second pass, walk the tree to invoke any available
14 |       # setup methods.
15 |       #
16 |       TreeWalker.walk_tree(self) do |node|
17 |         node.emit(:created)
18 |       end
19 |     end
20 | 
21 |     # Extend all of this node's siblings. See {#extend_all}.
22 |     #
23 |     def extend_siblings(*mods, &blk)
24 |       extend_all(siblings, *mods, &blk)
25 |     end
26 | 
27 |     # Extend this node's parent. See {#extend_all}.
28 |     #
29 |     def extend_parent(*mods, &blk)
30 |       extend_all([parent], *mods, &blk)
31 |     end
32 | 
33 |     # Extend all of the specified `nodes` with the specified module(s). If
34 |     # a block is given, the nodes will be extended with the code found
35 |     # in the block.
36 |     #
37 |     def extend_all(nodes, *mods, &blk)
38 |       if block_given?
39 |         mods << Module.new(&blk)
40 |       end
41 | 
42 |       Array(nodes).each do |n|
43 |         n.extend(*mods)
44 |       end
45 |     end
46 | 
47 | 
48 |     protected def load_initializer!(initializer)
49 |       logger.info "Executing initializer #{initializer.internal_path}"
50 |       instance_eval(initializer.render)
51 |     end
52 |   end
53 | end
54 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/tree.rb:
--------------------------------------------------------------------------------
  1 | module Flutterby
  2 |   module Tree
  3 |     def self.prepended(base)
  4 |       base.send :attr_reader, :children, :parent
  5 |     end
  6 | 
  7 |     def clear!
  8 |       super
  9 |       @children = []
 10 |     end
 11 | 
 12 |     # Returns true if this node represents a folder, ie. it contains
 13 |     # additional children.
 14 |     #
 15 |     def folder?
 16 |       children.any?
 17 |     end
 18 | 
 19 |     # Returns the tree's root node.
 20 |     #
 21 |     def root
 22 |       parent ? parent.root : self
 23 |     end
 24 | 
 25 |     # Returns true if this node is also the tree's root node.
 26 |     #
 27 |     def root?
 28 |       root == self
 29 |     end
 30 | 
 31 |     # Returns this node's siblings (ie. other nodes within the
 32 |     # same folder node.)
 33 |     #
 34 |     def siblings
 35 |       parent && (parent.children - [self])
 36 |     end
 37 | 
 38 |     # Returns the sibling with the specified name.
 39 |     #
 40 |     def sibling(name)
 41 |       parent && parent.find(name)
 42 |     end
 43 | 
 44 |     # Returns all of this node's descendants (ie. children and
 45 |     # their children and so on.)
 46 |     #
 47 |     def descendants
 48 |       _descendants.flatten.uniq
 49 |     end
 50 | 
 51 |     private def _descendants
 52 |       [children, children.map(&:descendants)]
 53 |     end
 54 | 
 55 |     # Returns the complete tree, including this node.
 56 |     #
 57 |     def all_nodes
 58 |       [self] + descendants
 59 |     end
 60 | 
 61 |     # Returns the size of the graph starting with this
 62 |     # node.
 63 |     #
 64 |     def size
 65 |       all_nodes.length
 66 |     end
 67 | 
 68 |     # Among this node's children, find a node by its name. If the
 69 |     # name passed as an argument includes a dot, the name will match against
 70 |     # the full name of the children; otherwise, just the base name.
 71 |     #
 72 |     # Examples:
 73 |     #
 74 |     #     # returns the first child called "index"
 75 |     #     find_child("index")
 76 |     #
 77 |     #     # returns the child called "index" with extension "html"
 78 |     #     find_child("index.html")
 79 |     #
 80 |     def find_child(name, opts = {})
 81 |       name_attr = name.include?(".") ? "full_name" : "name"
 82 | 
 83 |       @children.find do |c|
 84 |         (c.should_publish? || !opts[:public_only]) &&
 85 |           (c.send(name_attr) == name)
 86 |       end
 87 |     end
 88 | 
 89 |     def emit_child(name)
 90 |       # Override this to dynamically create child nodes.
 91 |     end
 92 | 
 93 |     # Move this node to a new parent. The parent can be specified as either
 94 |     # a node object, or a path expression.
 95 |     #
 96 |     def move_to(new_parent)
 97 |       self.parent = new_parent.is_a?(String) ?
 98 |         find!(new_parent) : new_parent
 99 |     end
100 | 
101 |     def parent=(new_parent)
102 |       # Remove from previous parent
103 |       if @parent
104 |         @parent.children.delete(self)
105 |       end
106 | 
107 |       # Assign new parent (it may be nil)
108 |       @parent = new_parent
109 | 
110 |       # Notify new parent
111 |       if @parent
112 |         @parent.children << self
113 |       end
114 |     end
115 | 
116 |     # Returns all children that will compile to a HTML page.
117 |     #
118 |     def pages
119 |       children.select { |c| c.page? }
120 |     end
121 | 
122 |     # Creates a new node, using the specified arguments, as a child
123 |     # of this node.
124 |     #
125 |     def create(name, **args)
126 |       args[:parent] = self
127 |       Node.new(name.to_s, **args)
128 |     end
129 | 
130 |     # Like {find}, but raises an exception when the specified node could not
131 |     # be found.
132 |     #
133 |     def find!(path, *args)
134 |       find(path, *args) || raise("Could not find node for path expression '#{path}'")
135 |     end
136 | 
137 |     # Find a node by the specified path expression.
138 |     #
139 |     def find(path, opts = {})
140 |       path = path.to_s
141 |       return self if path.empty?
142 | 
143 |       # remove duplicate slashes
144 |       path = path.gsub(%r{/+}, "/")
145 | 
146 |       case path
147 |       # ./foo/...
148 |       when %r{^\./?} then
149 |         parent ? parent.find($', opts) : root.find($', opts)
150 | 
151 |       # /foo/...
152 |       when %r{^/} then
153 |         root.find($', opts)
154 | 
155 |       # foo/...
156 |       when %r{^([^/]+)/?} then
157 |         # Use the next path part to find a child by that name.
158 |         # If no child can't be found, try to emit a child, but
159 |         # not if the requested name starts with an underscore.
160 |         if child = find_child($1, opts) || (emit_child($1) unless $1.start_with?("_"))
161 |           # Depending on the tail of the requested find expression,
162 |           # either return the found node, or ask it to find the tail.
163 |           $'.empty? ? child : child.find($', opts)
164 |         end
165 |       end
166 |     end
167 | 
168 |     # Within this (sub-)tree, find the node that matches the file system
169 |     # path specified in `fs_path`.
170 |     #
171 |     def find_for_fs_path(fs_path)
172 |       fs_path = File.expand_path(fs_path)
173 |       TreeWalker.walk_tree(self) do |node|
174 |         return node if node.fs_path == fs_path
175 |       end
176 |     end
177 |   end
178 | end
179 | 


--------------------------------------------------------------------------------
/lib/flutterby/node/url.rb:
--------------------------------------------------------------------------------
 1 | require 'uri'
 2 | 
 3 | module Flutterby
 4 |   module Url
 5 |     # Returns the node's fully qualified URL.
 6 |     #
 7 |     def url
 8 |       Flutterby.config.prefix_uri.try(:host) ?
 9 |         URI.join(Flutterby.config.prefix_uri, path).to_s : path
10 |     end
11 | 
12 |     # Returns the node's path, taking any configured prefix into account.
13 |     #
14 |     def path
15 |       Flutterby.config.prefix_uri ?
16 |         File.join(Flutterby.config.prefix_uri.path, internal_path) : internal_path
17 |     end
18 | 
19 |     # Return's the node's "internal" path: the path of the node that can also
20 |     # be passed to {#find} to find it, not taking any configured --prefix
21 |     # into account.
22 |     #
23 |     def internal_path
24 |       raise "node has been deleted" if deleted?
25 |       File.join(parent ? parent.internal_path : "/", full_name)
26 |     end
27 |   end
28 | end
29 | 


--------------------------------------------------------------------------------
/lib/flutterby/server.rb:
--------------------------------------------------------------------------------
  1 | require 'rack'
  2 | require 'rack/livereload'
  3 | require 'listen'
  4 | require 'better_errors'
  5 | require 'flutterby/livereload_server'
  6 | 
  7 | module Flutterby
  8 |   class Server
  9 |     def initialize(root)
 10 |       @root = root
 11 |     end
 12 | 
 13 |     def run!(address: "localhost", port: 4004)
 14 |       # Spawn livereload server
 15 |       livereload = LiveReloadServer.new(@root, { host: address })
 16 | 
 17 |       # Set up listener
 18 |       listener = Listen.to(@root.fs_path) do |modified, added, removed|
 19 |         @root.reload!
 20 |         livereload.trigger_reload(modified + added + removed)
 21 |         # handle_fs_change(modified, added, removed)
 22 |       end
 23 | 
 24 |       # Set up Rack app
 25 |       BetterErrors.application_root = __dir__
 26 |       this = self
 27 |       app = Rack::Builder.app do |app|
 28 |         app.use BetterErrors::Middleware
 29 |         app.use Rack::LiveReload, no_swf: true
 30 |         app.run this
 31 |       end
 32 | 
 33 |       # Set up server
 34 |       server = Rack::Handler::WEBrick
 35 | 
 36 |       # Make sure we handle interrupts correctly
 37 |       trap('INT') do
 38 |         listener.stop
 39 |         server.shutdown
 40 |         livereload.stop
 41 |       end
 42 | 
 43 |       # Go!
 44 |       listener.start
 45 |       server.run app, Host: address, Port: port, Logger: Flutterby.logger
 46 |     end
 47 | 
 48 |     def handle_fs_change(modified, added, removed)
 49 |       modified.each do |fs_path|
 50 |         if node = @root.find_for_fs_path(fs_path)
 51 |           logger.info "Reloading node #{node}"
 52 |           node.reload!
 53 |         end
 54 |       end
 55 | 
 56 |       added.each do |fs_path|
 57 |         if parent = @root.find_for_fs_path(File.dirname(fs_path))
 58 |           logger.info "Adding node to #{parent}"
 59 |           node = parent.create(File.basename(fs_path), fs_path: fs_path)
 60 |           node.stage!
 61 |         end
 62 |       end
 63 | 
 64 |       removed.each do |fs_path|
 65 |         if node = @root.find_for_fs_path(fs_path)
 66 |           logger.info "Removing node #{node}"
 67 |           node.delete!
 68 |         end
 69 |       end
 70 |     end
 71 | 
 72 |     def call(env)
 73 |       req = Rack::Request.new(env)
 74 |       res = Rack::Response.new([], 200, {})
 75 | 
 76 |       # Look for target node in path registry
 77 |       if (node = find_node_for_path(req.path)) && node.can_render?
 78 |         res.status = 200
 79 |         render_node(res, node)
 80 |       else
 81 |         res.status = 404
 82 |         if node_404 = @root.find("/404")
 83 |           render_node(res, node_404)
 84 |         else
 85 |           res.headers["Content-Type"] = "text/html"
 86 |           res.body = [File.read(File.expand_path("../../templates/404.html", __FILE__))]
 87 |         end
 88 |       end
 89 | 
 90 |       res
 91 |     end
 92 | 
 93 |     private
 94 | 
 95 |     def find_node_for_path(path)
 96 |       if node = @root.find(path, public_only: true)
 97 |         # If the node is a folder, try and find its "index" node.
 98 |         # Otherwise, use the node directly.
 99 |         node.folder? ? node.find('index') : node
100 |       end
101 |     end
102 | 
103 |     def logger
104 |       @logger ||= Flutterby.logger
105 |     end
106 | 
107 |     def render_node(res, node)
108 |       res.headers["Content-Type"] = node.mime_type.to_s
109 |       res.body = [node.render(layout: true)]
110 |     end
111 |   end
112 | end
113 | 


--------------------------------------------------------------------------------
/lib/flutterby/tree_walker.rb:
--------------------------------------------------------------------------------
 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 |       # Build a list of nodes to run block against. Since
29 |       # tree walking will also be used to modify the tree,
30 |       # we can't relay on simple recursion and iteration here.
31 |       #
32 |       nodes = [node] + node.descendants
33 | 
34 |       # Execute block
35 |       nodes.inject(val) do |val, n|
36 |         blk.call(n, val)
37 |       end
38 |     end
39 |   end
40 | end
41 | 


--------------------------------------------------------------------------------
/lib/flutterby/version.rb:
--------------------------------------------------------------------------------
1 | module Flutterby
2 |   VERSION = "0.7.0"
3 | end
4 | 


--------------------------------------------------------------------------------
/lib/flutterby/view.rb:
--------------------------------------------------------------------------------
  1 | require 'benchmark'
  2 | require 'flutterby/layout'
  3 | 
  4 | module Flutterby
  5 |   class View
  6 |     attr_reader :node, :locals
  7 |     alias_method :page, :node
  8 | 
  9 |     # Include ERB::Util from ActiveSupport. This will provide
 10 |     # html_escape, h, and json_escape helpers.
 11 |     #
 12 |     # http://api.rubyonrails.org/classes/ERB/Util.html
 13 |     #
 14 |     include ERB::Util
 15 | 
 16 |     def initialize(node, locals: {})
 17 |       @node = node
 18 |       @locals = locals
 19 |     end
 20 | 
 21 |     def date_format(date, fmt)
 22 |       date.strftime(fmt)
 23 |     end
 24 | 
 25 |     def raw(str)
 26 |       str.html_safe
 27 |     end
 28 | 
 29 |     def render(expr, as: nil, locals: {}, **args)
 30 |       node = expr.is_a?(Node) ? expr : find(expr)
 31 | 
 32 |       # Resolve rendering "as" specific things
 33 |       if as
 34 |         locals[as] = node
 35 |         node = node.find!("./_#{as}.#{self.node.ext}")
 36 |       end
 37 | 
 38 |       node.render(**args, locals: locals.with_indifferent_access, **args)
 39 |     end
 40 | 
 41 |     def find(*args)
 42 |       node.find(*args)
 43 |     end
 44 | 
 45 |     def find!(*args)
 46 |       node.find!(*args)
 47 |     end
 48 | 
 49 |     def siblings(*args)
 50 |       node.siblings(*args)
 51 |     end
 52 | 
 53 |     def parent
 54 |       node.parent
 55 |     end
 56 | 
 57 |     def root
 58 |       node.root
 59 |     end
 60 | 
 61 |     def data
 62 |       node.data
 63 |     end
 64 | 
 65 |     def tag(name, attributes = {})
 66 |       ActiveSupport::SafeBuffer.new.tap do |output|
 67 |         attributes_str = attributes.keys.sort.map do |k|
 68 |           %{#{h k}="#{h attributes[k]}"}
 69 |         end.join(" ")
 70 | 
 71 |         opening_tag = "#{h name.downcase} #{attributes_str}".strip
 72 |         output << "<#{opening_tag}>".html_safe
 73 |         output << yield if block_given?
 74 |         output << "".html_safe
 75 |       end
 76 |     end
 77 | 
 78 |     def link_to(text, target, attrs = {})
 79 |       href = case target
 80 |       when Flutterby::Node then target.url
 81 |       else target.to_s
 82 |       end
 83 | 
 84 |       tag(:a, attrs.merge(href: href)) { text }
 85 |     end
 86 | 
 87 |     def debug(obj)
 88 |       tag(:pre, class: "debug") { h obj.to_yaml }
 89 |     end
 90 | 
 91 |     def extend_view(*mods, &blk)
 92 |       if block_given?
 93 |         mods << Module.new(&blk)
 94 |       end
 95 | 
 96 |       extend(*mods)
 97 |     end
 98 | 
 99 |     private
100 | 
101 |     def logger
102 |       @logger ||= Flutterby.logger
103 |     end
104 | 
105 |     class << self
106 |       # Factory method that returns a newly created view for the given node.
107 |       # It also makes sure all available _view.rb extensions are loaded.
108 |       #
109 |       def for(node, *args)
110 |         # create a new view instance
111 |         view = new(node, *args)
112 | 
113 |         # walk the tree up to dynamically extend the view
114 |         TreeWalker.walk_down(node) do |e|
115 |           if view_node = e.sibling("_view.rb")
116 |             case view_node.ext
117 |             when "rb" then
118 |               view.instance_eval(view_node.source)
119 |             else
120 |               raise "Unknown view extension #{view_node.full_name}"
121 |             end
122 |           end
123 |         end
124 | 
125 |         # return the finished view object
126 |         view
127 |       end
128 |     end
129 |   end
130 | end
131 | 


--------------------------------------------------------------------------------
/lib/templates/404.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     404 - Document Not Found
 6 |     
26 |   
27 |   
28 |     
29 |

404 - Document Not Found

30 |

Flutterby could not find a renderable node for this URL.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/templates/new_project/.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /.sass-cache/ 3 | -------------------------------------------------------------------------------- /lib/templates/new_project/Gemfile.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | source "https://rubygems.org" 3 | 4 | gem "rake" 5 | gem "flutterby", "~> <%= Flutterby::VERSION %>" 6 | -------------------------------------------------------------------------------- /lib/templates/new_project/README.md: -------------------------------------------------------------------------------- 1 | # New Flutterby Site 2 | 3 | Welcome to your new Flutterby site! Some notes to get you started: 4 | 5 | 1. Your site's contents can be found in the `./site/` subdirectory of your project. 6 | 2. Refer to the various "underscore" files to see how you can add layouts, helper methods and more. 7 | 2. It's very easy to build a new site with Flutterby. Feel free to delete everything from `./site/` and start from scratch! 8 | 9 | ### Important Links 10 | 11 | - https://github.com/hmans/flutterby 12 | -------------------------------------------------------------------------------- /lib/templates/new_project/Rakefile: -------------------------------------------------------------------------------- 1 | task default: [:build] 2 | 3 | desc "Build the website" 4 | task :build do 5 | system "rm -rf _build/* && bin/flutterby build" 6 | end 7 | 8 | # This is a sample "deploy" task that will upload your 9 | # statically generated website to your server via rsync. 10 | # 11 | # desc "Deploy the website" 12 | # task deploy: [:build] do 13 | # system "rsync -vr --del _build/* server:/path/to/website/" 14 | # end 15 | -------------------------------------------------------------------------------- /lib/templates/new_project/bin/flutterby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'flutterby' 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("flutterby", "flutterby") 18 | -------------------------------------------------------------------------------- /lib/templates/new_project/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rake' 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("rake", "rake") 18 | -------------------------------------------------------------------------------- /lib/templates/new_project/lib/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/flutterby/b6cfc71ff7bede348a162ecc8bd1db44821e9529/lib/templates/new_project/lib/.keep -------------------------------------------------------------------------------- /lib/templates/new_project/site/_config.yaml: -------------------------------------------------------------------------------- 1 | # This is your site's configuration file. 2 | 3 | site: 4 | title: My Flutterby Site 5 | description: > 6 | This is my new Flutterby 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? 10 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/_layout.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta charset="utf-8" 5 | title = config.site.title 6 | meta name="viewport" content="width=device-width, initial-scale=1.0" 7 | 8 | // highlight.js for syntax highlighting 9 | link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/styles/default.min.css" 10 | script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js" 11 | 12 | // Our own assets 13 | link rel="stylesheet" href="/css/styles.css" 14 | script src="/js/app.js" 15 | 16 | body 17 | .container 18 | = yield 19 | 20 | footer role="main" 21 | == config.site.description 22 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/_view.rb: -------------------------------------------------------------------------------- 1 | # Use _view.rb files like this one to add helper methos to your views. Any 2 | # helpers defined here will be available to all pages within the same 3 | # folder, AND all of its sub-folders. 4 | 5 | extend_view do 6 | # Define a `config` view helper that provides quick access to the 7 | # site configuration object's data. 8 | # 9 | def config 10 | find("/_config").data 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/about.html.md: -------------------------------------------------------------------------------- 1 | ## About this site 2 | 3 | This is a small template site [Flutterby] generated for you. You can keep using it, modify it as you wish, or get rid of everything within the `site/` directory and start from scratch. It's up to you! 4 | 5 | [Flutterby]: http://www.flutterby.run 6 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/blog/_init.rb: -------------------------------------------------------------------------------- 1 | # A _init.rb file contains Ruby code that will be executed when 2 | # your application boots up. It runs within the scope of this folder's node 3 | # and can be used to set up event handlers, modify other nodes, and more. 4 | # 5 | # In this simple example, we're simply adding some convenience methods to 6 | # all available blog posts for easier access to specific pieces of data. 7 | 8 | on(:created, ->(n) { n.page? }) do |evt, node| 9 | node.extend PostExtension 10 | end 11 | 12 | module PostExtension 13 | def date 14 | data.date 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/blog/_layout.html.slim: -------------------------------------------------------------------------------- 1 | article.post 2 | .post-meta 3 | = date_format page.date, "%B %e, %Y" 4 | 5 | h1 = page.title 6 | 7 | = yield 8 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/blog/_list.html.slim: -------------------------------------------------------------------------------- 1 | ul.post-list 2 | - for post in blog_posts 3 | li 4 | .post-meta = date_format post.date, "%B %e, %Y" 5 | = link_to post.title, post 6 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/blog/_view.rb: -------------------------------------------------------------------------------- 1 | extend_view do 2 | # Returns all blog posts contained in this directory. We assume 3 | # that a blog post is any page object that has a date set. 4 | # 5 | def blog_posts 6 | siblings 7 | .select { |p| blog_post?(p) } 8 | .sort_by(&:date) 9 | .reverse 10 | end 11 | 12 | # Checks if a specific node is a blog post. 13 | # 14 | def blog_post?(node) 15 | node.page? && !node.date.nil? 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/blog/hello-world.html.md.tt: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome to Flutterby! 3 | date: <%= Date.today.to_s %> 4 | --- 5 | 6 | 7 | **Congratulations!** You've just created a new Flutterby project through the magic of `flutterby new` -- now it's time to make it your own! 8 | 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 | 11 | | `/_config.yaml` | Your site configuration. | 12 | | `/css/styles.css.scss` | Your stylesheet. [Sass]-powered, of course! | 13 | | `/_layout.html.slim` | Your global site layout. | 14 | | `/posts/_layout.html.slim` | Your post-specific layout. | 15 | 16 | #### Recommended Reading 17 | 18 | - [Flutterby Website](http://www.flutterby.run) 19 | - [Flutterby Documentation](http://www.flutterby.run/docs/) 20 | - [Flutterby on Github](https://github.com/hmans/flutterby) 21 | 22 | 23 | 24 | 25 | [Sass]: http://sass-lang.com/ 26 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/css/styles.css.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | color: #444; 4 | font: 18px/1.5 Arial,Helvetica,sans-serif; 5 | } 6 | 7 | div.container { 8 | max-width: 800px; 9 | margin: 0 auto; 10 | padding: 30px; 11 | } 12 | 13 | footer[role="main"] { 14 | margin-top: 80px; 15 | border-top: 3px solid lightgrey; 16 | padding-top: 20px; 17 | font-size: 82.5%; 18 | color: grey; 19 | a { color: inherit } 20 | } 21 | 22 | h1 { 23 | font-family: "Arial Black"; 24 | color: #666; 25 | border-bottom: 10px solid #ddd; 26 | } 27 | 28 | a { 29 | color: rgb(74, 109, 242); 30 | } 31 | 32 | .post-meta { 33 | color: grey; 34 | } 35 | 36 | ul.post-list { 37 | list-style: none; 38 | padding-left: 0; 39 | } 40 | 41 | 42 | /* embedded code */ 43 | code { 44 | background-color: #e8e8e8; 45 | font: 85% "Office Code Pro",Menlo,Consolas,Monaco,monotype; 46 | padding: 2px 4px; 47 | border-radius: 2px; 48 | } 49 | 50 | pre>code { 51 | padding: 15px !important; 52 | } 53 | 54 | /* tables */ 55 | table { 56 | } 57 | 58 | td { 59 | vertical-align: top; 60 | } 61 | td+td { 62 | padding-left: 10px; 63 | } 64 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/index.html.slim: -------------------------------------------------------------------------------- 1 | h1 = config.site.title 2 | 3 | p Hooray, you now have a Flutterby-powered website! #{link_to "Find out more", "/about.html"}. 4 | 5 | h3 Latest Posts: 6 | = render("/blog/_list") 7 | -------------------------------------------------------------------------------- /lib/templates/new_project/site/js/app.js: -------------------------------------------------------------------------------- 1 | /* insert your JavaScript here */ 2 | 3 | hljs.initHighlightingOnLoad(); 4 | -------------------------------------------------------------------------------- /spec/data_file_spec.rb: -------------------------------------------------------------------------------- 1 | describe "JSON files" do 2 | subject { read "json_data.json" } 3 | 4 | it "imports the JSON object into #data" do 5 | expect(subject.data["name"]).to eq("Hendrik Mans") 6 | expect(subject.data["info"]["favoriteFood"]).to eq("Schnitzel") 7 | end 8 | end 9 | 10 | describe "YAML files" do 11 | subject { read "yaml_data.yaml" } 12 | 13 | it "imports the YAML into #data" do 14 | expect(subject.data["name"]).to eq("Hendrik Mans") 15 | expect(subject.data["info"]["favoriteFood"]).to eq("Schnitzel") 16 | end 17 | end 18 | 19 | describe "TOML files" do 20 | subject { node "data.toml", source: source } 21 | 22 | let :source do 23 | <<-EOF 24 | [site] 25 | title = "Site Title" 26 | url = "http://site.com" 27 | EOF 28 | end 29 | 30 | let :expected_data do 31 | { 32 | "site" => { 33 | "title" => "Site Title", 34 | "url"=>"http://site.com" 35 | } 36 | } 37 | end 38 | 39 | its(:data) { is_expected.to eq(expected_data) } 40 | end 41 | 42 | describe "data files with extra extensions" do 43 | specify "have their extra processing performed" do 44 | node = read "json_with_erb.json.erb" 45 | expect(node.data["foo"]).to eq("bar") 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/data_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Node#data" do 2 | subject do 3 | node "data.json", 4 | source: %{{"foo": {"bar": "baz"}}} 5 | end 6 | 7 | let(:data) { subject.data } 8 | 9 | it "returns the node's data hash" do 10 | expect(data).to eq({"foo" => {"bar" => "baz"}}) 11 | end 12 | 13 | it "supports the dot access syntax" do 14 | expect(data.foo.bar).to eq("baz") 15 | end 16 | 17 | it "supports indifferent access" do 18 | expect(data[:foo][:bar]).to eq("baz") 19 | end 20 | 21 | it "supports setting values" do 22 | expect { data.cow = "moo" } 23 | .to change { data[:cow] } 24 | .from(nil).to("moo") 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/date_in_filename_spec.rb: -------------------------------------------------------------------------------- 1 | describe "dates in file names" do 2 | subject { read "posts/2017-01-04-hello-world.html.md" } 3 | 4 | specify "the date is extracted from the file name" do 5 | expect(subject.data["date"]).to eq(Date.parse("2017-01-04")) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dotaccess_spec.rb: -------------------------------------------------------------------------------- 1 | describe Dotaccess do 2 | let(:hash) do 3 | { 4 | foo: { "bar" => "baz" } 5 | } 6 | end 7 | 8 | subject { Dotaccess[hash] } 9 | 10 | it "returns proxie for values that are themselves hashes" do 11 | expect(subject.foo).to be_kind_of(Dotaccess::Proxy) 12 | end 13 | 14 | it "allows for deeply nested lookups of values" do 15 | expect(subject.foo.bar).to eq("baz") 16 | end 17 | 18 | it "returns nil for missing values" do 19 | expect(subject.foe).to be_nil 20 | expect(subject.foo.baz).to be_nil 21 | end 22 | 23 | it "allows for setting values" do 24 | expect { subject.cow = "moo" } 25 | .to change { subject.cow } 26 | .from(nil).to("moo") 27 | end 28 | 29 | it "allows to compare equality with other hashes" do 30 | expect(subject.foo).to eq({ "bar" => "baz" }) 31 | end 32 | 33 | if RUBY_VERSION >= "2.3.0" 34 | it "allows for using the safe navigation operator" do 35 | expect(eval("subject&.foo&.bar")).to eq("baz") 36 | expect(eval("subject&.foe&.baz")).to be_nil 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/emitters_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Emitters" do 2 | pending 3 | end 4 | -------------------------------------------------------------------------------- /spec/event_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::Event do 2 | def event(*args) 3 | Flutterby::Event.new(*args) 4 | end 5 | 6 | it "is equal to a symbol representing its name" do 7 | expect(event(:foo, source: nil)).to eq(:foo) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/exporter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'flutterby/exporter' 2 | 3 | describe Flutterby::Exporter do 4 | include ExportHelpers 5 | 6 | before(:all) do 7 | cleanup! 8 | build! 9 | end 10 | 11 | after(:all) do 12 | cleanup! 13 | end 14 | 15 | specify "renders markdown to HTML" do 16 | expect(generated_file("markdown.html")) 17 | .to eq(%{\n

This is Markdown

\n\n

It’s great!

\n}) 18 | end 19 | 20 | specify "renders Scss to CSS" do 21 | expect(generated_file("css/styles.css")) 22 | .to eq("strong {\n color: green; }\n\nbody {\n color: red; }\n") 23 | end 24 | 25 | specify "creates subdirectories" do 26 | expect(File.directory?(generated_path("/posts"))).to be_truthy 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/filters/builder_spec.rb: -------------------------------------------------------------------------------- 1 | describe "XMLBuilder filter" do 2 | let :source do 3 | <<-EOF 4 | xml.instruct! :xml, version: "1.0" 5 | xml.test do 6 | # We can access the current node because we're in a view 7 | xml.name node.name 8 | 9 | # Normale XML attributes 10 | xml.bar "baz" 11 | end 12 | EOF 13 | end 14 | 15 | let :output do 16 | %{\n\n feed\n baz\n\n} 17 | end 18 | 19 | subject do 20 | node "feed.xml.builder", source: source 21 | end 22 | 23 | specify "renders to XML" do 24 | expect(subject.render).to eq(output) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/filters/markdown_spec.rb: -------------------------------------------------------------------------------- 1 | describe "markdown rendering" do 2 | subject { node "markdown.html.md", source: source } 3 | 4 | let :source do 5 | <<-EOF 6 | # This is Markdown 7 | 8 | It's great! Here's some Ruby code: 9 | 10 | ~~~ ruby 11 | puts "OMG" 12 | ~~~ 13 | EOF 14 | end 15 | 16 | it "renders the markdown to HTML" do 17 | expect(subject.render) 18 | .to eq %{

This is Markdown

\n\n

It’s great! Here’s some Ruby code:

\n\n
puts \"OMG\"\n
\n} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/filters/ruby_node_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Ruby nodes" do 2 | let :ruby_code do 3 | <<-EOF 4 | tag(:p) { "I'm the body!" } 5 | EOF 6 | end 7 | 8 | subject do 9 | node "ruby_node.html.rb", source: ruby_code 10 | end 11 | 12 | specify "create a new node powered by custom Ruby code" do 13 | expect(subject).to be_kind_of(Flutterby::Node) 14 | expect(subject.ext).to eq("html") 15 | expect(subject.render).to eq("

I'm the body!

") 16 | end 17 | 18 | context "with instance variables" do 19 | let :ruby_code do 20 | <<-EOF 21 | @message = "Hooray, instance variables in views!" 22 | tag(:p) { @message } 23 | EOF 24 | end 25 | 26 | its(:render) { is_expected.to eq("

Hooray, instance variables in views!

") } 27 | 28 | it "does not set the instance variable on the node" do 29 | expect(subject.instance_variable_get("@message")).to be_nil 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/filters/sass_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Sass Filter" do 2 | subject { read "css/styles.css.scss" } 3 | 4 | let :expected do 5 | "strong {\n color: green; }\n\nbody {\n color: red; }\n" 6 | end 7 | 8 | specify "converted into CSS, with working partials" do 9 | expect(subject.render).to eq(expected) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/filters/tilt_spec.rb: -------------------------------------------------------------------------------- 1 | # We're doing most template rendering through Tilt, so we can 2 | # leverage this to dynamically fall back to it for template formats 3 | # we don't support out of the box. 4 | 5 | describe "Tilt template fallback" do 6 | let :source do 7 | <<-EOF 8 | # RDoc test. +Yeah+! 9 | EOF 10 | end 11 | 12 | let :expected_body do 13 | %{\n

# RDoc test. Yeah!

\n} 14 | end 15 | 16 | subject do 17 | node "index.html.rdoc", source: source 18 | end 19 | 20 | its(:full_name) { is_expected.to eq("index.html") } 21 | its(:render) { is_expected.to eq(expected_body) } 22 | end 23 | -------------------------------------------------------------------------------- /spec/filters/unsupported_filter_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Unsupported filters" do 2 | subject { node "index.html.poop", source: source } 3 | 4 | let(:source) { "poop!" } 5 | 6 | its(:full_name) { is_expected.to eq("index.html.poop") } 7 | its(:render) { is_expected.to eq(source) } 8 | 9 | specify "does not log a warning, because filter is never executed" do 10 | expect(Flutterby.logger) 11 | .to_not receive(:warn) 12 | 13 | subject.render 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/find_spec.rb: -------------------------------------------------------------------------------- 1 | describe "#find" do 2 | # This is the node structure we'll be using for testing: 3 | # 4 | # root 5 | # +-- foo 6 | # +-- bar 7 | # +-- baz 8 | # 9 | 10 | let!(:root) { node "/" } 11 | let!(:foo) { node "foo", parent: root } 12 | let!(:bar) { node "bar", parent: root } 13 | let!(:baz) { node "baz.html", parent: bar } 14 | 15 | # add a secret folder with some equally secret data 16 | let!(:secret) { node "_secret", parent: root } 17 | let!(:data) { node "data", parent: secret } 18 | 19 | specify "normal singular expressions" do 20 | expect(root.find("foo")).to eq(foo) 21 | expect(root.find("bar")).to eq(bar) 22 | end 23 | 24 | specify "expressions starting with slash" do 25 | # Just a slash will always return root 26 | expect(root.find("/")).to eq(root) 27 | expect(baz.find("/")).to eq(root) 28 | 29 | # Expressions starting with a slash will start at root 30 | expect(baz.find("/foo")).to eq(foo) 31 | expect(baz.find("/bar/baz")).to eq(baz) 32 | end 33 | 34 | specify "find(.) returns the parent" do 35 | expect(baz.find(".")).to eq(bar) 36 | expect(baz.find("./")).to eq(bar) 37 | end 38 | 39 | specify "find(..) returns the parent's parent" do 40 | expect(baz.find("..")).to eq(root) 41 | expect(baz.find("../")).to eq(root) 42 | end 43 | 44 | specify "crazy mixed expressions" do 45 | expect(baz.find("../foo")).to eq(foo) 46 | expect(baz.find("../foo/../bar/baz")).to eq(baz) 47 | end 48 | 49 | specify "reduce duplicate slashes" do 50 | expect(baz.find("..//bar")).to eq(bar) 51 | end 52 | 53 | specify "not found" do 54 | expect(root.find("moo")).to eq(nil) 55 | end 56 | 57 | specify "faulty expressions" do 58 | expect(root.find(" haha lol zomg ")).to eq(nil) 59 | end 60 | 61 | specify "with or without extensions" do 62 | expect(bar.find("baz")).to eq(baz) 63 | expect(bar.find("baz.html")).to eq(baz) 64 | expect(bar.find("baz.txt")).to eq(nil) 65 | end 66 | 67 | describe "private vs. public nodes" do 68 | specify "by default, finds within private nodes" do 69 | expect(root.find("_secret/data")).to eq(data) 70 | end 71 | 72 | context "with public_only set to true" do 73 | specify "it does not find private nodes" do 74 | expect(root.find("_secret/data", public_only: true)).to eq(nil) 75 | end 76 | end 77 | end 78 | 79 | describe "#find!" do 80 | context "when using an invalid path" do 81 | it "raises an error" do 82 | expect { root.find!("invalid") }.to raise_error(%{Could not find node for path expression 'invalid'}) 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/frontmatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe "frontmatter" do 2 | subject do 3 | node "page.html.md", source: source 4 | end 5 | 6 | let :source do 7 | <<-EOF 8 | --- 9 | title: A file that tests Markdown. 10 | --- 11 | 12 | This is a Markdown file with frontmatter. 13 | EOF 14 | end 15 | 16 | specify "frontmatter is extracted" do 17 | expect(subject.data.title).to eq("A file that tests Markdown.") 18 | end 19 | 20 | 21 | context "with another triple-dash in the body" do 22 | let :source do 23 | <<-EOF 24 | --- 25 | title: A file that tests Markdown. 26 | --- 27 | 28 | foo: bar 29 | 30 | --- 31 | 32 | This is a Markdown file with frontmatter. 33 | EOF 34 | end 35 | 36 | specify "frontmatter is only extracted between the first two triple-dashes" do 37 | expect(subject.data.title).to eq("A file that tests Markdown.") 38 | expect(subject.data.foo).to eq(nil) 39 | expect(subject.render).to eq("\n

foo: bar

\n\n
\n\n

This is a Markdown file with frontmatter.

\n") 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/html_escaping_spec.rb: -------------------------------------------------------------------------------- 1 | describe "html escaping" do 2 | context "with ERB" do 3 | subject do 4 | node "page.html.erb", source: source 5 | end 6 | 7 | let(:source) do 8 | %{

<%= "Hi! " %>

} 9 | end 10 | 11 | its(:render) { is_expected.to include("

Hi! <g>

") } 12 | end 13 | 14 | context "with Slim" do 15 | subject do 16 | node "page.html.slim", source: source 17 | end 18 | 19 | let(:source) do 20 | %{p = "Hi! "} 21 | end 22 | 23 | its(:render) { is_expected.to include("

Hi! <g>

") } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/initializer_spec.rb: -------------------------------------------------------------------------------- 1 | describe "extending all nodes in a folder through _init.rb" do 2 | let!(:root) do 3 | node "/" 4 | end 5 | 6 | let!(:page) do 7 | root.create "page.html.erb", 8 | source: %{I'm a page! This is a <%= page.root.test %>!} 9 | end 10 | 11 | let!(:initializer) do 12 | node "_init.rb", parent: root, source: <<-EOF 13 | on :created do 14 | @test = "test" 15 | end 16 | 17 | def test 18 | @test 19 | end 20 | EOF 21 | end 22 | 23 | specify "works :)" do 24 | root.stage! 25 | expect(root.test).to eq("test") 26 | expect(page.render).to eq("I'm a page! This is a test!") 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/layout_spec.rb: -------------------------------------------------------------------------------- 1 | describe "layouts" do 2 | let!(:root) { node "/" } 3 | 4 | let!(:outer_layout) do 5 | node "_layout.html.erb", parent: root, source: <<-EOF 6 |

<%= "Outer Layout " %>

7 | <%= yield %> 8 | EOF 9 | end 10 | 11 | let!(:folder) do 12 | node "test", parent: root 13 | end 14 | 15 | let!(:page) do 16 | node "page.html", parent: folder, source: <<-EOF 17 | --- 18 | title: Page Title 19 | --- 20 |

I'm the actual page!

21 | EOF 22 | end 23 | 24 | let!(:stylesheet) do 25 | node "styles.css", parent: folder, 26 | source: %[body { color: red }] 27 | end 28 | 29 | let!(:inner_layout) do 30 | node "_layout.html.erb", parent: folder, source: <<-EOF 31 |

<%= page.title %>

32 | <%= yield %> 33 | EOF 34 | end 35 | 36 | let!(:alternative_layout) do 37 | node "_alternative_layout.html.erb", parent: folder, source: <<-EOF 38 |

Alternative Inner Layout

39 | <%= yield %> 40 | EOF 41 | end 42 | 43 | context "with the normal layout behavior" do 44 | it "walks up the tree, applying all _layout files" do 45 | expect(page.render(layout: true)) 46 | .to eq(%{

Outer Layout <g>

\n

Page Title

\n

I'm the actual page!

\n\n\n}) 47 | end 48 | end 49 | 50 | context "with all layout disabled" do 51 | before do 52 | page.data[:layout] = false 53 | end 54 | 55 | it "doesn't apply any layouts" do 56 | expect(page.render(layout: true)) 57 | .to eq(%{

I'm the actual page!

\n}) 58 | end 59 | end 60 | 61 | context "with an alternative layout specified in the node data" do 62 | before do 63 | page.data[:layout] = "./_alternative_layout" 64 | end 65 | 66 | it "applies the specified layout, then walks up the tree" do 67 | expect(page.render(layout: true)) 68 | .to eq(%{

Outer Layout <g>

\n

Alternative Inner Layout

\n

I'm the actual page!

\n\n\n}) 69 | end 70 | end 71 | 72 | context "with multiple layouts specified (you crazy person, you)" do 73 | before do 74 | page.data[:layout] = ["/_layout", "./_alternative_layout", false] 75 | end 76 | 77 | it "applies the specified layout, then walks up the tree" do 78 | expect(page.render(layout: true)) 79 | .to eq(%{

Alternative Inner Layout

\n

Outer Layout <g>

\n

I'm the actual page!

\n\n\n}) 80 | end 81 | end 82 | 83 | context "with an alternative layout specified in the node data, and a false value" do 84 | before do 85 | page.data[:layout] = ["./_alternative_layout", false] 86 | end 87 | 88 | it "applies the specified layout, then stops" do 89 | expect(page.render(layout: true)) 90 | .to eq(%{

Alternative Inner Layout

\n

I'm the actual page!

\n\n}) 91 | end 92 | end 93 | 94 | context "with an alternative layout explicitly specified" do 95 | it "applies the specified layout, then stops" do 96 | expect(page.render(layout: "./_alternative_layout")) 97 | .to eq(%{

Alternative Inner Layout

\n

I'm the actual page!

\n\n}) 98 | end 99 | end 100 | 101 | context "when rendering something that is not a HTML page" do 102 | specify "layouts are not applied automatically" do 103 | expect(stylesheet.render(layout: true)) 104 | .to eq("body { color: red }") 105 | end 106 | end 107 | 108 | context "with a missing layout specified" do 109 | before do 110 | page.data[:layout] = "./_missing_layout" 111 | end 112 | 113 | it "applies the specified layout, then walks up the tree" do 114 | expect { page.render(layout: true) } 115 | .to raise_error("No layout found for path expression './_missing_layout'") 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/names_extensions_filters_spec.rb: -------------------------------------------------------------------------------- 1 | describe "names, extensions and filters" do 2 | subject { node name } 3 | 4 | context "with a name like index.html.md.erb" do 5 | let(:name) { "index.html.md.erb" } 6 | 7 | its(:name) { is_expected.to eq("index") } 8 | its(:ext) { is_expected.to eq("html") } 9 | its(:filters) { is_expected.to eq(["erb", "md"]) } 10 | its(:full_name) { is_expected.to eq("index.html") } 11 | end 12 | 13 | context "with a name like jquery.min.js" do 14 | let(:name) { "jquery.min.js" } 15 | 16 | its(:name) { is_expected.to eq("jquery.min") } 17 | its(:ext) { is_expected.to eq("js") } 18 | its(:filters) { is_expected.to eq([]) } 19 | its(:full_name) { is_expected.to eq("jquery.min.js") } 20 | end 21 | 22 | context "with a name like static.txt" do 23 | let(:name) { "static.txt" } 24 | 25 | its(:name) { is_expected.to eq("static") } 26 | its(:ext) { is_expected.to eq("txt") } 27 | its(:filters) { is_expected.to eq([]) } 28 | its(:full_name) { is_expected.to eq("static.txt") } 29 | end 30 | 31 | context "with a name like _init.rb" do 32 | let(:name) { "_init.rb" } 33 | 34 | its(:name) { is_expected.to eq("_init") } 35 | its(:ext) { is_expected.to eq("rb") } 36 | its(:filters) { is_expected.to eq([]) } 37 | its(:full_name) { is_expected.to eq("_init.rb") } 38 | end 39 | 40 | context "with a name like foo" do 41 | let(:name) { "foo" } 42 | 43 | its(:name) { is_expected.to eq("foo") } 44 | its(:ext) { is_expected.to eq(nil) } 45 | its(:filters) { is_expected.to eq([]) } 46 | its(:full_name) { is_expected.to eq("foo") } 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/node/deletion_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::Deletion do 2 | subject { node "page.html" } 3 | 4 | describe '#delete!' do 5 | it "emits a :deleted event" do 6 | expect(subject).to receive(:emit).with(:deleted) 7 | subject.delete! 8 | end 9 | 10 | it "marks the node as deleted" do 11 | expect { subject.delete! } 12 | .to change { subject.deleted? } 13 | .from(false).to(true) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/node/event_handling_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::EventHandling do 2 | describe '#on' do 3 | let!(:root) { node "/" } 4 | let!(:foo) { root.create "foo" } 5 | let!(:bar) { root.create "bar" } 6 | 7 | it "registers an event handler" do 8 | foo.on(:created) do |evt| 9 | evt.source.data.name = "Foo" 10 | end 11 | 12 | root.stage! 13 | 14 | expect(foo.data.name).to eq "Foo" 15 | end 16 | 17 | it "registers an event handler that receives the event and the source node as arguments" do 18 | foo.on(:created) do |evt, node| 19 | expect(node).to eq(foo) 20 | expect(node).to eq(evt.source) 21 | end 22 | 23 | root.stage! 24 | end 25 | 26 | context "without a selector" do 27 | it "creates a handler that will be executed for any node" do 28 | root.on(:created) do |evt| 29 | root.data.nodes ||= [] 30 | root.data.nodes << evt.source 31 | end 32 | 33 | root.stage! 34 | 35 | expect(root.data.nodes).to eq [root, foo, bar] 36 | end 37 | end 38 | 39 | context "with a string selector" do 40 | it "creates a handler that will be executed for the node matching the path" do 41 | root.on(:created, "/foo") do |evt| 42 | root.data.nodes ||= [] 43 | root.data.nodes << evt.source 44 | end 45 | 46 | root.stage! 47 | 48 | expect(root.data.nodes).to eq [foo] 49 | end 50 | end 51 | 52 | context "with a regexp selector" do 53 | it "creates a handler that will be executed for the node matching the path by regular expression" do 54 | root.on(:created, /foo/) do |evt| 55 | root.data.nodes ||= [] 56 | root.data.nodes << evt.source 57 | end 58 | 59 | root.stage! 60 | 61 | expect(root.data.nodes).to eq [foo] 62 | end 63 | end 64 | 65 | context "with a proc selector" do 66 | it "creates a handler that will be executed for the node where proc evaluates to true" do 67 | root.on(:created, ->(n) { n.name == "foo" }) do |evt| 68 | root.data.nodes ||= [] 69 | root.data.nodes << evt.source 70 | end 71 | 72 | root.stage! 73 | 74 | expect(root.data.nodes).to eq [foo] 75 | end 76 | end 77 | end 78 | 79 | describe '#emit' do 80 | let!(:root) { node "/" } 81 | let!(:foo) { root.create "foo" } 82 | let!(:bar) { foo.create "bar" } 83 | let!(:baz) { foo.create "baz" } 84 | 85 | context "when invoked on a low-level node" do 86 | it "travels up the tree, invoking handlers on its way" do 87 | evt = Flutterby::Event.new(:test) 88 | expect(bar).to receive(:handle).with(evt) 89 | expect(foo).to receive(:handle).with(evt) 90 | expect(root).to receive(:handle).with(evt) 91 | bar.emit evt 92 | end 93 | 94 | it "does not invoke the handler on nodes that are not on the way up the tree" do 95 | expect(baz).to_not receive(:handle) 96 | bar.emit :test 97 | end 98 | end 99 | 100 | it "passes extra event arguments back into event handlers" do 101 | foo.on :foo do |evt| 102 | expect(evt.args).to eq({foo: "bar"}) 103 | end 104 | 105 | foo.emit :foo, foo: "bar" 106 | end 107 | end 108 | 109 | describe "integration with initializers" do 110 | let!(:root) { node "/" } 111 | 112 | let!(:initializer) do 113 | root.create "_init.rb", source: <<-EOF 114 | on :created do 115 | emit :set_foo 116 | end 117 | EOF 118 | end 119 | 120 | let!(:page) do 121 | root.create "page.html.erb", source: <<-EOF 122 | Hallo <%= parent.data.foo %>! 123 | EOF 124 | end 125 | 126 | specify do 127 | root.on :set_foo do 128 | root.data.foo = "foo" 129 | end 130 | 131 | root.stage! 132 | 133 | expect(page.render).to eq("Hallo foo!\n") 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/node/render_spec.rb: -------------------------------------------------------------------------------- 1 | describe "rendering" do 2 | let!(:root) { node "/" } 3 | let!(:page) { node "page.html.erb", parent: root, source: page_source } 4 | let!(:partial) { node "_partial.html.erb", parent: root, source: partial_source } 5 | 6 | subject { page } 7 | 8 | let :page_source do 9 | <<-EOF 10 |

Let's render a partial!

11 | <%= find("./_partial.html").render() %> 12 | EOF 13 | end 14 | 15 | let :partial_source do 16 | %{

I'm the partial!

} 17 | end 18 | 19 | describe "without partials" do 20 | specify do 21 | expect(partial.render).to eq("

I'm the partial!

") 22 | end 23 | end 24 | 25 | describe "rendering partials" do 26 | its(:render) { is_expected.to include("I'm the partial!") } 27 | 28 | context "when passing variables to the partial" do 29 | let :page_source do 30 | %{<%= find("./_partial.html").render(locals: {name: "John Doe"}) %>} 31 | end 32 | 33 | let :partial_source do 34 | %{Hello <%= locals[:name] %>!} 35 | end 36 | 37 | its(:render) { is_expected.to include("Hello John Doe!") } 38 | end 39 | end 40 | end 41 | 42 | describe Flutterby::Rendering do 43 | describe '#can_render?' do 44 | it "returns true if the node can be rendered" do 45 | folder = node("folder", source: nil) 46 | expect(folder.can_render?).to eq(false) 47 | end 48 | end 49 | 50 | describe '#render' do 51 | context "on a node that can't be rendered" do 52 | it "should raise an exception" do 53 | folder = node("folder", source: nil) 54 | expect{folder.render}.to raise_error("Nodes without source can't be rendered") 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/node/url_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::Node do 2 | let!(:folder) { node "folder" } 3 | subject { folder.create "page.html.md" } 4 | 5 | describe '#path' do 6 | it "returns the node's path" do 7 | expect(subject.path).to eq("/folder/page.html") 8 | end 9 | 10 | context "on a deleted node" do 11 | before { subject.delete! } 12 | 13 | it "raises an exception" do 14 | expect { subject.path }.to raise_error "node has been deleted" 15 | end 16 | end 17 | 18 | context "on a node that lives within a deleted node" do 19 | before { folder.delete! } 20 | 21 | it "raises an exception" do 22 | expect { subject.path }.to raise_error "node has been deleted" 23 | end 24 | end 25 | 26 | context "when a URL prefix is given" do 27 | it "returns the prefixed path" do 28 | Flutterby.config.prefix = "http://www.foo.com/subdir/" 29 | expect(subject.path).to eq("/subdir/folder/page.html") 30 | end 31 | end 32 | 33 | context "when a path prefix is given" do 34 | it "returns the prefixed path" do 35 | Flutterby.config.prefix = "/subdir/" 36 | expect(subject.path).to eq("/subdir/folder/page.html") 37 | end 38 | end 39 | end 40 | 41 | describe '#internal_path' do 42 | context "when no prefix is configured" do 43 | it "returns the node's path" do 44 | expect(subject.internal_path).to eq("/folder/page.html") 45 | end 46 | 47 | it "is equal to #path" do 48 | expect(subject.internal_path).to eq(subject.path) 49 | end 50 | end 51 | 52 | context "when a prefix is configured" do 53 | before { Flutterby.config.prefix = "http://www.foo.com/subdir/" } 54 | 55 | it "is not affected" do 56 | expect(subject.internal_path).to eq("/folder/page.html") 57 | end 58 | end 59 | end 60 | 61 | describe '#url' do 62 | context "when no prefix is configured" do 63 | it "returns the path" do 64 | expect(subject.url).to eq("/folder/page.html") 65 | end 66 | end 67 | 68 | context "when a URL prefix is configured" do 69 | it "returns the full URL" do 70 | Flutterby.config.prefix = "http://www.foo.com/subdir/" 71 | expect(subject.url).to eq("http://www.foo.com/subdir/folder/page.html") 72 | end 73 | end 74 | 75 | context "when a path prefix is configured" do 76 | it "returns the path instead" do 77 | Flutterby.config.prefix = "/subdir/" 78 | expect(subject.url).to eq("/subdir/folder/page.html") 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/node_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::Node do 2 | let!(:root) { node "/" } 3 | let!(:folder) { root.create("a_folder") } 4 | let!(:file) { folder.create("file.txt") } 5 | let!(:page) { folder.create("file.html.md") } 6 | 7 | describe '#page?, #file? and #folder?' do 8 | context "HTML page nodes" do 9 | subject { page } 10 | it { is_expected.to be_page } 11 | it { is_expected.to be_file } 12 | it { is_expected.to_not be_folder } 13 | end 14 | 15 | context "file nodes" do 16 | subject { file } 17 | it { is_expected.to_not be_page } 18 | it { is_expected.to be_file } 19 | it { is_expected.to_not be_folder } 20 | end 21 | 22 | context "folder nodes" do 23 | subject { folder } 24 | it { is_expected.to_not be_page } 25 | it { is_expected.to_not be_file } 26 | it { is_expected.to be_folder } 27 | end 28 | end 29 | 30 | describe '#create' do 31 | subject { folder.create("test.html") } 32 | 33 | its(:parent) { is_expected.to eq(folder) } 34 | its(:name) { is_expected.to eq('test') } 35 | its(:ext) { is_expected.to eq('html') } 36 | 37 | context 'when another parent is specified' do 38 | subject { folder.create("test.html", parent: file) } 39 | its(:parent) { is_expected.to eq(folder) } 40 | end 41 | end 42 | 43 | describe '#should_publish?' do 44 | it "returns false for nodes starting with underscores" do 45 | page = node "_secret.html" 46 | expect(page.should_publish?).to eq(false) 47 | end 48 | 49 | it "returns false for nodes within private folders" do 50 | folder = node "_secret" 51 | page = folder.create "page.html" 52 | expect(page.should_publish?).to eq(false) 53 | end 54 | end 55 | 56 | describe '#siblings' do 57 | context "when there's a parent" do 58 | subject { page } 59 | its(:siblings) { is_expected.to eq([file]) } 60 | end 61 | 62 | context "when there's no parent" do 63 | subject { root } 64 | its(:siblings) { is_expected.to eq(nil) } 65 | end 66 | end 67 | 68 | describe '#descendants' do 69 | it "returns a flat array with all of the node's descendants" do 70 | expect(root.descendants).to eq [folder, file, page] 71 | end 72 | end 73 | 74 | describe '#full_tree' do 75 | it "returns a flat array with all nodes in the tree" do 76 | expect(root.all_nodes).to eq [root, folder, file, page] 77 | end 78 | end 79 | 80 | describe '#tree_size' do 81 | it "returns the number of all nodes in the tree" do 82 | expect(root.size).to eq(4) 83 | end 84 | end 85 | 86 | describe '#move_to' do 87 | let!(:another_folder) { root.create("another_folder") } 88 | 89 | context "when specifying another node" do 90 | it "will move the node to that node" do 91 | expect { file.move_to(another_folder) } 92 | .to change { file.parent } 93 | .from(folder).to(another_folder) 94 | end 95 | end 96 | 97 | context "when specifying a path expression" do 98 | it "will move the node to the node found by the expression" do 99 | expect { file.move_to("/another_folder") } 100 | .to change { file.parent } 101 | .from(folder).to(another_folder) 102 | end 103 | 104 | context "when the path expression is invalid" do 105 | it "will raise an error" do 106 | expect { file.move_to("/INVALID") } 107 | .to raise_error %{Could not find node for path expression '/INVALID'} 108 | end 109 | end 110 | end 111 | 112 | context "when specifying another node" do 113 | it "will move the node to that node" do 114 | expect { file.move_to(nil) } 115 | .to change { file.parent } 116 | .from(folder).to(nil) 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/prefix_and_slug_spec.rb: -------------------------------------------------------------------------------- 1 | describe "prefix and slug extraction" do 2 | context "when no prefix is available" do 3 | subject { node "foo.html.erb" } 4 | 5 | its(:prefix) { is_expected.to be_nil } 6 | its(:slug) { is_expected.to eq("foo") } 7 | its(:full_name) { is_expected.to eq("foo.html") } 8 | end 9 | 10 | context "when a single prefix is available" do 11 | subject { node "123-foo.html.erb" } 12 | 13 | its(:prefix) { is_expected.to eq("123") } 14 | its(:slug) { is_expected.to eq("foo") } 15 | its(:full_name) { is_expected.to eq("foo.html") } 16 | end 17 | 18 | context "when a multipart prefix is available" do 19 | subject { node "123-45-6789-foo.html.erb" } 20 | 21 | its(:prefix) { is_expected.to eq("123-45-6789") } 22 | its(:slug) { is_expected.to eq("foo") } 23 | its(:full_name) { is_expected.to eq("foo.html") } 24 | end 25 | 26 | context "when a date prefix is available" do 27 | subject { node "2017-04-01-foo.html.erb" } 28 | 29 | its(:prefix) { is_expected.to eq("2017-04-01") } 30 | its(:slug) { is_expected.to eq("foo") } 31 | specify { expect(subject.data[:date]).to eq(Date.parse("2017-04-01")) } 32 | its(:full_name) { is_expected.to eq("foo.html") } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/site/css/_partial.scss: -------------------------------------------------------------------------------- 1 | strong { color: green } 2 | -------------------------------------------------------------------------------- /spec/site/css/styles.css.scss: -------------------------------------------------------------------------------- 1 | @import "_partial.scss"; 2 | 3 | $color: red; 4 | 5 | body { 6 | color: $color; 7 | } 8 | -------------------------------------------------------------------------------- /spec/site/json_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hendrik Mans", 3 | "info": { 4 | "favoriteFood": "Schnitzel" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/site/json_with_erb.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "<%= "bar" %>" 3 | } 4 | -------------------------------------------------------------------------------- /spec/site/markdown.html.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "A file that tests Markdown." 3 | +++ 4 | 5 | # This is Markdown 6 | 7 | It's great! 8 | -------------------------------------------------------------------------------- /spec/site/posts/2017-01-04-hello-world.html.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | 3 | I'm a blog post. 4 | -------------------------------------------------------------------------------- /spec/site/yaml_data.yaml: -------------------------------------------------------------------------------- 1 | name: Hendrik Mans 2 | info: 3 | favoriteFood: Schnitzel 4 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "flutterby" 3 | require "pry" 4 | require "rspec/its" 5 | 6 | Flutterby.logger.level = Logger::FATAL 7 | 8 | module Helpers 9 | def site_path 10 | ::File.expand_path("../site/", __FILE__) 11 | end 12 | 13 | def read(name = "/") 14 | fs_path = ::File.join(site_path, name) 15 | name = ::File.basename(name) 16 | 17 | node(name, fs_path: fs_path) 18 | end 19 | 20 | def node(*args) 21 | Flutterby::Node.new(*args) 22 | end 23 | end 24 | 25 | module ExportHelpers 26 | def export_path 27 | File::expand_path("../../tmp/flutterby_export/", __FILE__) 28 | end 29 | 30 | def cleanup! 31 | FileUtils.rm_rf(export_path) 32 | end 33 | 34 | def build! 35 | root = read 36 | root.stage! 37 | 38 | Flutterby::Exporter.new(root) 39 | .export!(into: export_path) 40 | end 41 | 42 | def generated_path(path) 43 | File.join(export_path, path) 44 | end 45 | 46 | def generated_file(path) 47 | File.read(generated_path(path)) 48 | end 49 | end 50 | 51 | RSpec.configure do |c| 52 | c.include Helpers 53 | 54 | c.before(:each) do 55 | Flutterby.reset_config! 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/title_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Node#title' do 2 | subject { node "introduction.html.md" } 3 | its(:title) { is_expected.to eq("Introduction") } 4 | 5 | context "when the node name has a prefix" do 6 | subject { node "123-introduction.html.md" } 7 | its(:title) { is_expected.to eq("Introduction") } 8 | end 9 | 10 | context "when the node slag has multiple parts" do 11 | subject { node "hello-world.html.md" } 12 | its(:title) { is_expected.to eq("Hello World") } 13 | end 14 | 15 | context "when the node sets its own title data attribute" do 16 | subject { node "introduction.html.md", source: source } 17 | 18 | let(:source) do 19 | <<-EOF 20 | --- 21 | title: "A Great Introduction" 22 | --- 23 | 24 | Hi! 25 | EOF 26 | end 27 | 28 | its(:title) { is_expected.to eq("A Great Introduction") } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/tree_walker_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::TreeWalker do 2 | pending 3 | end 4 | -------------------------------------------------------------------------------- /spec/view_spec.rb: -------------------------------------------------------------------------------- 1 | describe Flutterby::View do 2 | subject { node "page.html.erb", source: source } 3 | 4 | describe '#raw' do 5 | let(:source) { %{<%= raw "" %>} } 6 | its(:render) { is_expected.to eq("") } 7 | end 8 | 9 | describe '#html_escape' do 10 | let(:source) { %{<%= raw(html_escape "") %>} } 11 | its(:render) { is_expected.to eq('<g>') } 12 | end 13 | 14 | describe '#h' do 15 | let(:source) { %{<%= raw(h "") %>} } 16 | its(:render) { is_expected.to eq('<g>') } 17 | end 18 | end 19 | 20 | describe "tag helpers" do 21 | let(:root) { node "/" } 22 | let(:foo) { node "foo", parent: root } 23 | let(:bar) { node "bar", parent: root } 24 | let(:view) { Flutterby::View.for(foo) } 25 | 26 | describe '#tag' do 27 | it "generates HTML tags" do 28 | expect(view.tag(:div, class: "foo")).to eq(%{
}) 29 | end 30 | 31 | it "properly escapes quotes in attributes" do 32 | expect(view.tag(:div, class: "foo\"bar")).to eq(%{
}) 33 | end 34 | 35 | it "properly escapes quotes in tag names" do 36 | expect(view.tag("foo\"bar", class: "foo")).to eq(%{}) 37 | end 38 | end 39 | 40 | describe '#link_to' do 41 | it "generates links to nodes" do 42 | expect(view.link_to("Bar", bar)).to eq(%{Bar}) 43 | end 44 | 45 | it "generates links to URL strings" do 46 | expect(view.link_to("Bar", "http://bar.com")).to eq(%{Bar}) 47 | end 48 | 49 | it "can use custom HTML attributes" do 50 | expect(view.link_to("Bar", bar, class: "foo")).to eq(%{Bar}) 51 | end 52 | end 53 | end 54 | 55 | describe '#debug helper' do 56 | let(:page) { node "page.html.erb" } 57 | let(:view) { Flutterby::View.for(page) } 58 | 59 | context "when passed any object that can be serialized to YAML" do 60 | let(:object) { {bar: "baz"} } 61 | 62 | let(:expected_output) do 63 | %{
---\n:bar: baz\n
} 64 | end 65 | 66 | it "dumps the object's YAML representation into a
 tag" do
67 |       expect(view.debug(object)).to eq(expected_output)
68 |     end
69 |   end
70 | end
71 | 


--------------------------------------------------------------------------------