flora 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84a83e09611588abf3ede492aa734a168aa8d611eb7b33e503243df4bbe13d89
4
- data.tar.gz: 5aeb14cbae18ad40c725c8fcf88c814daf150a48491ff3594708b843cb80f200
3
+ metadata.gz: 5aad0573cb2ab398c995d0c4b52df0c85b6ddad503cf25ae3a69f9c84ee59068
4
+ data.tar.gz: ff36bfc9b1a12ff134c568abfa0abb0c37f75425dcb2bd75d475b47af3d46000
5
5
  SHA512:
6
- metadata.gz: 14084b4863fe3223d99ac2da8f3b1281ad6649f23501f150a13b73add8b451112a8f15607734b2750dd872214e40f579c51e46bf7aff2ba22f91a11e1db24c77
7
- data.tar.gz: 155da839cd37104784aea6da12760c8d342b6d25c8b4eda9f6dfb2a5d192a76f9e29c0dd4f5c8f6470ee9ba3eb8bdccd9f16189a3f20e5a9bf74ca90aa5649a2
6
+ metadata.gz: 2c333e1169af9dcaccf7d0c9492e07659db5dde8fbbf43b1107d1e8e48beb7ca75a2606f53f0c8a7ef54a812f287e5b928729215a41390c2fe69fa3f11b76e16
7
+ data.tar.gz: 8f08d99a5eccb379f715bebd480151e1a29db0ea524bd29b5c3bdef0f416934143de5103b1ed521b203f118cb18d58183d59386e391f2dd6353bc0cec7665149
data/README.md CHANGED
@@ -25,3 +25,51 @@ end
25
25
  ```
26
26
 
27
27
  Install everything with `bundle install`, and then run `bundle exec flora serve` and open up http://localhost:3000!
28
+
29
+ ## Lilac
30
+
31
+ Lilac is Flora's DSL for writing HTML. Tags are just methods named the same as the tag, and children of tags are put inside blocks.
32
+
33
+ Looking for a way to make reusable components? Write a function! For example:
34
+
35
+ ```ruby
36
+ # lib/cool_title.rb
37
+ class CoolTitle
38
+
39
+ def self.render(text)
40
+ div(class: 'title') do
41
+ h1 do
42
+ text
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ # index.rb
50
+ html do
51
+ body do
52
+ CoolTitle.render('My blog')
53
+ end
54
+ end
55
+ ```
56
+
57
+ ## The Flora way
58
+
59
+ Flora is an opinionated static site generator, and you should generally follow those opinions when making a site in Flora.
60
+
61
+ ### Static only
62
+
63
+ Flora will only ever generate static websites. If you need dynamic content, look elsewhere! (I'd suggest Rails!)
64
+
65
+ ### A Ruby-first approach
66
+
67
+ Flora prefers Ruby and you should too. Configuration and HTML-generation code is in Ruby. Not everything makes sense as Ruby (you should write content-heavy pages in Markdown, for example), but it's the default option for solving problems in a Flora project.
68
+
69
+ ### File-based routing
70
+
71
+ Flora doesn't have a routes file. Instead, requests are routed by convention based on your file's path. While this is the default, some things--like the Redirector plugin--can subvert this opinion when needed.
72
+
73
+ ### Unopinionated but pluggable
74
+
75
+ Flora is a generic static site generator and doesn't favor any particular use case. Making Flora do specific things better is achieved using plugins. Flora ships with a few plugins for common use cases like blogs and handling static files.
data/lib/flora/app.rb CHANGED
@@ -50,7 +50,7 @@ class Flora::App
50
50
  end
51
51
 
52
52
  def call(env)
53
- @flora.reload_blueprint
53
+ @flora.reload_project
54
54
 
55
55
  @app.call(env)
56
56
  end
data/lib/flora/config.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # This is the user-visible configuration class. It lives in ROOT/_config.rb.
1
2
  class Flora::Config
2
3
 
3
4
  def initialize(file, flora)
data/lib/flora/factory.rb CHANGED
@@ -1,20 +1,20 @@
1
+ # Factories assemble Projects into Websites.
1
2
  class Flora::Factory
2
3
 
3
- def initialize(blueprint, config)
4
- @blueprint = blueprint
4
+ def initialize(project, config)
5
+ @project = project
5
6
  @config = config
6
7
  end
7
8
 
8
9
 
9
- # Assemble a blueprint and put it into a directory.
10
+ # Assemble the Project into a Website, and put it into out_dir.
10
11
  def assemble(out_dir)
11
12
  out_dir.mkdir unless out_dir.exist?
12
13
 
13
- @blueprint.each_page do |page|
14
- relative_path = page.file.relative_path_from(@blueprint.dir)
15
- out_filename = out_dir.join(relative_path).sub_ext('.html')
14
+ @project.blueprints.each do |blueprint|
15
+ out_filename = out_dir.join(blueprint.page_name)
16
16
  out_filename.dirname.mkdir unless out_filename.dirname.exist?
17
- out_filename.write(page.render)
17
+ out_filename.write(blueprint.render)
18
18
  end
19
19
  end
20
20
 
@@ -1,4 +1,4 @@
1
- module Flora::Blueprint::Page::Html
1
+ module Flora::Lilac
2
2
 
3
3
  TAGS = %i[
4
4
  a abbr address area article aside audio
@@ -17,8 +17,13 @@ class Flora::Plugins::Blog::Post
17
17
  end
18
18
 
19
19
 
20
+ def date
21
+ @frontmatter['date']
22
+ end
23
+
24
+
20
25
  def method_missing(name, ...)
21
- return @frontmatter[name] if @frontmatter[name]
26
+ return @frontmatter[name.to_s] if @frontmatter[name.to_s]
22
27
 
23
28
  super(name, ...)
24
29
  end
@@ -32,7 +37,7 @@ class Flora::Plugins::Blog::Post
32
37
  return {} unless page_data.start_with?('---')
33
38
  /(?<=---\n)(?<yaml>.*)(?=---\n)/m =~ @page.read
34
39
 
35
- YAML.load(yaml)
40
+ YAML.load(yaml, permitted_classes: [Date])
36
41
  end
37
42
 
38
43
  end
@@ -1,18 +1,18 @@
1
1
  module Flora::Plugins::Blog
2
2
 
3
- module BlueprintMethods
3
+ module ProjectMethods
4
4
 
5
5
  def posts
6
- @dir.glob('posts/**').map { Post.new(it, @dir) }
6
+ @dir.glob('posts/**').map { Post.new(it, @dir) }.sort_by(&:date).reverse
7
7
  end
8
8
 
9
9
  end
10
10
 
11
11
 
12
- module PageMethods
12
+ module BlueprintMethods
13
13
 
14
14
  def posts
15
- @blueprint.posts
15
+ @project.posts
16
16
  end
17
17
 
18
18
  end
@@ -1,33 +1,19 @@
1
1
  module Flora::Plugins::StaticFiles
2
2
 
3
- class Copier
3
+ class StaticFile < Flora::Project::Blueprint
4
4
 
5
- def initialize(dir)
6
- @dir = dir
5
+ def self.recognize(file, config)
6
+ file.fnmatch?(config.static_files_dir + '/**')
7
7
  end
8
8
 
9
9
 
10
- def copy_to(out)
11
- @dir.find do |public_file|
12
- next if public_file.directory?
13
-
14
- out_filename = out.join(Pathname.new(public_file).relative_path_from(@dir))
15
- out_filename.dirname.mkdir unless out_filename.dirname.exist?
16
- out_filename.write(File.read(public_file))
17
- end
10
+ def render
11
+ @file.read
18
12
  end
19
13
 
20
- end
21
-
22
-
23
- module FactoryMethods
24
-
25
- def assemble(out_dir)
26
- super
27
14
 
28
- base = @config.static_files_dir || 'public'
29
- copier = Copier.new(@blueprint.dir.join(base))
30
- copier.copy_to(out_dir.join(base))
15
+ def page_name
16
+ @file.relative_path_from(@project.dir).to_s
31
17
  end
32
18
 
33
19
  end
@@ -1,8 +1,16 @@
1
- class Flora::Blueprint::Page::MarkdownPage < Flora::Blueprint::Page
1
+ class Flora::Project::Blueprint::Markdown < Flora::Project::Blueprint
2
+
3
+ include Flora::Project::Blueprint::Nestable
4
+
5
+
6
+ def self.recognize(file, config)
7
+ file.extname == '.md'
8
+ end
9
+
2
10
 
3
11
  private
4
12
 
5
- def render_tree
13
+ def render_lilac
6
14
  silence_warnings do
7
15
  raw_html(Kramdown::Document.new(@file.read).to_html)
8
16
  end
@@ -0,0 +1,51 @@
1
+ # A Blueprint that includes Nestable is something that can be nested into a
2
+ # Lilac-based layout.
3
+ module Flora::Project::Blueprint::Nestable
4
+
5
+ def render
6
+ # The Html DSL uses a global variable internally that needs to be reset every time.
7
+ $flora_added = []
8
+
9
+ layouts = find_layouts
10
+ tree = render_internal(layouts, layouts.size) do
11
+ render_lilac
12
+ end
13
+
14
+ Flora::Lilac.to_html(tree)
15
+ end
16
+
17
+
18
+ private
19
+
20
+ def find_layouts
21
+ layouts = []
22
+
23
+ @file.ascend do |dir|
24
+ maybe_layout = dir.join('_layout.rb')
25
+ layouts << maybe_layout if maybe_layout.exist?
26
+ end
27
+
28
+ # Ascend doesn't go to the top level directory.
29
+ # TODO: Make this code less brittle.
30
+ maybe_layout = Pathname.new('./_layout.rb')
31
+ layouts << maybe_layout if maybe_layout.exist?
32
+
33
+ layouts
34
+ end
35
+
36
+
37
+ def render_internal(layouts, i, &block)
38
+ if i <= 0
39
+ return block.call
40
+ end
41
+
42
+ cont = -> { render_internal(layouts, i-1, &block) }
43
+ eval_with_block(layouts[i-1].read, &cont)
44
+ end
45
+
46
+
47
+ def eval_with_block(str, &block)
48
+ eval(str)
49
+ end
50
+
51
+ end
@@ -0,0 +1,17 @@
1
+ class Flora::Project::Blueprint::RubyHtml < Flora::Project::Blueprint
2
+
3
+ include Flora::Project::Blueprint::Nestable
4
+
5
+
6
+ def self.recognize(file, config)
7
+ file.extname == '.rb'
8
+ end
9
+
10
+
11
+ private
12
+
13
+ def render_lilac
14
+ instance_eval(@file.read)
15
+ end
16
+
17
+ end
@@ -0,0 +1,44 @@
1
+ # A Blueprint is something can be rendered into a File. Sometimes a Blueprint
2
+ # looks very different than the File it renders into, but other times (like for
3
+ # static files), nothing changes.
4
+ class Flora::Project::Blueprint
5
+
6
+ attr_reader(:file)
7
+
8
+
9
+ # :nodoc:
10
+ def self.types
11
+ ObjectSpace.each_object(Class).select { it < self }
12
+ end
13
+
14
+
15
+ # Returns true if this Blueprint is appropriate for the file.
16
+ #
17
+ # TODO: should this be an instance method somewhere instead?
18
+ def self.recognize(file, config)
19
+ # Override in subclasses!
20
+
21
+ false
22
+ end
23
+
24
+
25
+ def initialize(file, project)
26
+ @file = file
27
+ @project = project
28
+ end
29
+
30
+
31
+ # The name of the Page this Blueprint makes. By default this is just the
32
+ # filename, but .html, but subclasses can override this.
33
+ def page_name
34
+ @file.relative_path_from(@project.dir).sub_ext('.html').to_s
35
+ end
36
+
37
+
38
+ # Render a Blueprint into a Page for a Website. This is the String contents
39
+ # of whatever the Page in the final Website looks like.
40
+ def render
41
+ # Override in subclasses!
42
+ end
43
+
44
+ end
@@ -0,0 +1,61 @@
1
+ # A Project is a template for a website made in Flora.
2
+ #
3
+ # Projects live in a directory and have:
4
+ # 1) Blueprints, files that can be transformed into another file type (like HTML).
5
+ # 2) Supporting Code, Ruby code that lives in lib/ that Blueprints can depend on.
6
+ class Flora::Project
7
+
8
+ attr_reader(:dir, :blueprints)
9
+
10
+ IGNORES = ['.git/*', 'lib/**', '**_*.rb']
11
+
12
+
13
+ def initialize(dir, config)
14
+ @dir = dir
15
+ @config = config
16
+
17
+ @blueprints = find_blueprints
18
+
19
+ @loader = Zeitwerk::Loader.new
20
+ if has_supporting_code?
21
+ @loader.push_dir(@dir.join('lib').to_s)
22
+ end
23
+ @loader.enable_reloading
24
+ @loader.setup
25
+ @loader.eager_load
26
+ end
27
+
28
+
29
+ def reload
30
+ @loader.reload
31
+ @blueprints = find_blueprints
32
+ end
33
+
34
+
35
+ private
36
+
37
+ def find_blueprints
38
+ blueprints = []
39
+
40
+ @dir.find do |file|
41
+ next if IGNORES.any? { file.fnmatch((@dir / it).to_s) }
42
+ next if file.directory?
43
+
44
+ klass = Blueprint.types.find {
45
+ it.recognize(file.relative_path_from(@dir), @config)
46
+ }
47
+
48
+ next unless klass
49
+
50
+ blueprints << klass.new(file, self)
51
+ end
52
+
53
+ blueprints
54
+ end
55
+
56
+
57
+ def has_supporting_code?
58
+ @dir.join('lib').exist?
59
+ end
60
+
61
+ end
data/lib/flora/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Flora
2
- VERSION = '0.6.2'
2
+ VERSION = '0.7.1'
3
3
  end
data/lib/flora.rb CHANGED
@@ -10,25 +10,22 @@ class Flora
10
10
  def initialize(dir)
11
11
  dir = Pathname.new(dir)
12
12
 
13
- # Inject HTML helpers into Kernel so they're available everywhere. Just
14
- # instance_eval isn't enough because they'll be missing in lib/ code.
13
+ # Inject Lilac into the Kernel so it's available everywhere. Just
14
+ # instance_eval isn't enough because it'll be missing in lib/ code.
15
15
  #
16
16
  # TODO: is there a less disruptive way to do this?
17
- Kernel.prepend(Flora::Blueprint::Page::Html)
17
+ Kernel.prepend(Flora::Lilac)
18
18
 
19
19
  # The classes that are pluggable get their own instances to avoid conflicting
20
20
  # with other instances of Flora in the same process. This is mostly for the
21
21
  # unit tests. Maybe one day we can use Ruby::Box or something here instead.
22
22
  @config_class = Class.new(Config)
23
- @blueprint_class = Class.new(Blueprint)
23
+ @project_class = Class.new(Project)
24
24
  @factory_class = Class.new(Factory)
25
25
 
26
- # Page plugins get mixed in directly to the page instances by Blueprint.
27
- @page_modules = []
28
-
29
26
  @config = @config_class.new(dir.join('_config.rb'), self)
30
- @blueprint = @blueprint_class.new(dir, @config, @page_modules)
31
- @factory = @factory_class.new(@blueprint, @config)
27
+ @project = @project_class.new(dir, @config)
28
+ @factory = @factory_class.new(@project, @config)
32
29
  end
33
30
 
34
31
 
@@ -37,18 +34,24 @@ class Flora
37
34
  end
38
35
 
39
36
 
40
- def reload_blueprint
41
- @blueprint.reload
37
+ def reload_project
38
+ @project.reload
42
39
  end
43
40
 
44
41
 
45
42
  # TODO: it would be nice if this wasn't exposed here. It's just for Config#plugin.
46
43
  def load_plugin(mod)
47
44
  @config_class.include(mod::Config) if defined?(mod::Config)
48
- @blueprint_class.include(mod::BlueprintMethods) if defined?(mod::BlueprintMethods)
45
+ @project_class.include(mod::ProjectMethods) if defined?(mod::ProjectMethods)
49
46
  @factory_class.include(mod::FactoryMethods) if defined?(mod::FactoryMethods)
50
47
 
51
- @page_modules << mod::PageMethods if defined?(mod::PageMethods)
48
+ # TODO: make this a little more resilient (like the other classes).
49
+ Flora::Project::Blueprint.include(mod::BlueprintMethods) if defined?(mod::BlueprintMethods)
52
50
  end
53
51
 
54
52
  end
53
+
54
+ # Built-in Blueprints need to be explicitly loaded or else they won't show up
55
+ # in Blueprint.types.
56
+ loader.load_file("#{__dir__}/flora/project/blueprint/ruby_html.rb")
57
+ loader.load_file("#{__dir__}/flora/project/blueprint/markdown.rb")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flora
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Vladimiroff
@@ -106,19 +106,19 @@ files:
106
106
  - exe/flora
107
107
  - lib/flora.rb
108
108
  - lib/flora/app.rb
109
- - lib/flora/blueprint.rb
110
- - lib/flora/blueprint/page.rb
111
- - lib/flora/blueprint/page/html.rb
112
- - lib/flora/blueprint/page/layout.rb
113
- - lib/flora/blueprint/page/markdown_page.rb
114
- - lib/flora/blueprint/page/ruby_page.rb
115
109
  - lib/flora/cli.rb
116
110
  - lib/flora/config.rb
117
111
  - lib/flora/factory.rb
112
+ - lib/flora/lilac.rb
118
113
  - lib/flora/plugins/blog.rb
119
114
  - lib/flora/plugins/blog/post.rb
120
115
  - lib/flora/plugins/redirector.rb
121
116
  - lib/flora/plugins/static_files.rb
117
+ - lib/flora/project.rb
118
+ - lib/flora/project/blueprint.rb
119
+ - lib/flora/project/blueprint/markdown.rb
120
+ - lib/flora/project/blueprint/nestable.rb
121
+ - lib/flora/project/blueprint/ruby_html.rb
122
122
  - lib/flora/version.rb
123
123
  - tmp/.keep
124
124
  homepage: https://code.kat5.dev/nick/flora
@@ -141,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
143
  requirements: []
144
- rubygems_version: 4.0.0.dev
144
+ rubygems_version: 4.0.3
145
145
  specification_version: 4
146
146
  summary: A static site generator.
147
147
  test_files: []
@@ -1,29 +0,0 @@
1
- class Flora::Blueprint::Page::Layout
2
-
3
- def initialize(layouts)
4
- @layouts = layouts
5
- end
6
-
7
-
8
- def render(&block)
9
- render_internal(@layouts.size - 1, &block)
10
- end
11
-
12
-
13
- private
14
-
15
- def render_internal(i, &block)
16
- if i <= 0
17
- return block.call
18
- end
19
-
20
- cont = -> { render_internal(i-1, &block) }
21
- eval_with_block(@layouts[i].read, &cont)
22
- end
23
-
24
-
25
- def eval_with_block(str, &block)
26
- eval(str)
27
- end
28
-
29
- end
@@ -1,9 +0,0 @@
1
- class Flora::Blueprint::Page::RubyPage < Flora::Blueprint::Page
2
-
3
- private
4
-
5
- def render_tree
6
- instance_eval(@file.read)
7
- end
8
-
9
- end
@@ -1,52 +0,0 @@
1
- # A page is something can be turned into an HTML file.
2
- class Flora::Blueprint::Page
3
-
4
- attr_reader(:file)
5
-
6
-
7
- def self.for(file)
8
- # TODO: make this a little more extensible so plugins can implement new page
9
- # types.
10
- case file.extname
11
- when '.rb'
12
- RubyPage
13
- when '.md'
14
- MarkdownPage
15
- end
16
- end
17
-
18
-
19
- def initialize(file, blueprint)
20
- @file = file
21
- @blueprint = blueprint
22
-
23
- @layout = find_layouts
24
- end
25
-
26
-
27
- def render
28
- # The Html DSL uses a global variable internally that needs to be reset every time.
29
- $flora_added = []
30
-
31
- tree = @layout.render do
32
- render_tree
33
- end
34
-
35
- Html.to_html(tree)
36
- end
37
-
38
-
39
- private
40
-
41
- def find_layouts
42
- layouts = []
43
-
44
- @file.ascend do |dir|
45
- maybe_layout = dir.join('_layout.rb')
46
- layouts << maybe_layout if maybe_layout.exist?
47
- end
48
-
49
- Layout.new(layouts)
50
- end
51
-
52
- end
@@ -1,64 +0,0 @@
1
- class Flora::Blueprint
2
-
3
- attr_reader(:dir)
4
-
5
- IGNORES = ['.git/*', 'lib/*', '**_layout.rb', '_config.rb']
6
-
7
-
8
- def initialize(dir, config, page_modules)
9
- @dir = dir
10
- @config = config
11
- @page_modules = page_modules
12
-
13
- @pages = load_pages
14
-
15
- @loader = Zeitwerk::Loader.new
16
- if has_supporting_code?
17
- @loader.push_dir(@dir.join('lib').to_s)
18
- end
19
- @loader.enable_reloading
20
- @loader.setup
21
- end
22
-
23
-
24
- def each_page(&block)
25
- @pages.each(&block)
26
- end
27
-
28
-
29
- def reload
30
- @loader.reload
31
- @pages = load_pages
32
- end
33
-
34
-
35
- private
36
-
37
- def load_pages
38
- pages = []
39
-
40
- @dir.find do |file|
41
- next if IGNORES.any? { file.fnmatch((@dir / it).to_s) }
42
- next if file.directory?
43
- next unless supported_file_type?(file)
44
-
45
- page = Page.for(file).new(file, self)
46
- @page_modules.each { |mod| page.extend(mod) }
47
- pages << page
48
- end
49
-
50
- pages
51
- end
52
-
53
-
54
- # TODO: this is duplicating logic from Page::for.
55
- def supported_file_type?(file)
56
- file.extname == '.rb' || file.extname == '.md'
57
- end
58
-
59
-
60
- def has_supporting_code?
61
- @dir.join('lib').exist?
62
- end
63
-
64
- end