vizbuilder 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51f92c1fd3944056565422dd247601a8b15ba9e5e173edd596689b1511469e39
4
+ data.tar.gz: '029ed027b6ca7620051c6a3003f6d4dcb646faf9cb7699af5dda3bddf56be122'
5
+ SHA512:
6
+ metadata.gz: 4f46a7be4d80cf2f2cc1b4b77d667ace49f540896397b0509aaca1149df7a0ef711b56b7d51dc26f9d390d33f69138d215759f860417c3671c261024383f4aeb
7
+ data.tar.gz: a9af4e089431d541b393a2b881d7a446d03a6597b858f0cf2c59d3d9b380a80d85b1c968c8c23006801fe74c08a25f6a7eda7372ec3dd7d8208763a7c5ac876e
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.gem
11
+ .DS*
data/.rubocop.yml ADDED
@@ -0,0 +1,53 @@
1
+ AllCops:
2
+ Include:
3
+ - '**/Rakefile'
4
+ - '*gemspec'
5
+ - '**/*.rb'
6
+
7
+ Style/GuardClause:
8
+ Enabled: false
9
+
10
+ Style/FileName:
11
+ Enabled: false
12
+
13
+ Style/LineLength:
14
+ Max: 100
15
+
16
+ Style/NumericLiterals:
17
+ Enabled: false
18
+
19
+ Style/ClassAndModuleChildren:
20
+ Enabled: false
21
+
22
+ Style/SignalException:
23
+ EnforcedStyle: only_raise
24
+
25
+ Style/ClassLength:
26
+ Enabled: false
27
+
28
+ Style/MethodLength:
29
+ Enabled: false
30
+
31
+ Style/GuardClause:
32
+ Enabled: false
33
+
34
+ Style/IfUnlessModifier:
35
+ Enabled: false
36
+
37
+ Metrics/CyclomaticComplexity:
38
+ Enabled: false
39
+
40
+ Style/Alias:
41
+ Enabled: false
42
+
43
+ Metrics/BlockLength:
44
+ Enabled: false
45
+
46
+ Metrics/AbcSize:
47
+ Enabled: false
48
+
49
+ Metrics/PerceivedComplexity:
50
+ Enabled: false
51
+
52
+ Layout/EmptyLineAfterGuardClause:
53
+ Enabled: false
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in autotune-client.gemspec
4
+ gemspec
5
+
6
+ gem 'pry'
7
+ gem 'rubocop'
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2019, Vox Media, Inc., Ryan Mark
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the {organization} nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,331 @@
1
+ A fast, simple static site builder, inspired by [Middleman](https://middlemanapp.com/). Built for news.
2
+
3
+ ## Why another static site generator?
4
+
5
+ We've been using Middleman in Vox Media newsrooms for many years to build static articles and embeddable interactives. But as we've built larger projects and projects that require frequent updates, we've found that Middleman's extensive dependencies and complexity can cause high resource use and long build times. This is not a criticism of Middleman, Middleman is great and you should use it if it does what you need.
6
+
7
+ We need something simple and fast. Something that provides only the functionality we need and uses a few simple dependencies.
8
+
9
+ This is a simple ruby library and doesn't include any setup or project creation tools. If you want a helpful setup wizard or an ecosystem of extensions and tooling, checkout Middleman or a different static site generator.
10
+
11
+ ## Quick Start
12
+
13
+ Viz Builder is a simple library that leaves it to you to load and invoke it. There is no shell command for running it or setting up a new project.
14
+
15
+ To your project's `Gemfile`, add:
16
+
17
+ ```ruby
18
+ gem 'vizbuilder'
19
+ ```
20
+
21
+ Then in a new or existing file, you will `require` it, configure it and either run as a development server or to create static html and assets ready for upload.
22
+
23
+ Here is an example file `app.rb` that will load, configure and run Viz Builder:
24
+
25
+ ```ruby
26
+ require 'vizbuilder'
27
+
28
+ app = VizBuilder.new do
29
+ set :some_global_config_thing, 'hello world'
30
+ add_page 'index.html', template: 'index.html.erb'
31
+ end
32
+
33
+ # if run with the argument `runserver` run as a development server
34
+ if ARGV[0] == 'runserver'
35
+ app.runserver
36
+ else
37
+ app.build
38
+ end
39
+ ```
40
+
41
+ And then execute your app:
42
+
43
+ ```
44
+ $ bundle exec ruby app.rb runserver
45
+ ```
46
+
47
+ ## Data
48
+
49
+ Viz Builder manages configuration, sitemap and project data in three hashes that you can access directly in templates or the configuration block.
50
+
51
+ Configuration variables added via the `set` method and can be accessed in the `config` Hash:
52
+
53
+ ```ruby
54
+ set :foo, 'bar'
55
+ config[:foo] == 'bar'
56
+ ```
57
+
58
+ Data variables can be added via the `add_data` method and can be accessed in the `data` Hash:
59
+
60
+ ```ruby
61
+ add_data :scraped, foo: 'bar'
62
+ data[:scraped] == { foo: 'bar' }
63
+ # data[:scraped][:foo]
64
+ # or
65
+ # data.dig(:scraped, :foo)
66
+ ```
67
+
68
+ Viz Builder automatically loads all JSON and YAML files located in the `data` directory. The base file name will be used as the name:
69
+
70
+ ```ruby
71
+ # assuming a file called `data/my_content.json`
72
+ data[:my_content] == JSON.parse(File.read('data/my_content.json'))
73
+ ```
74
+
75
+ Viz Builder keeps track of pages in a `sitemap` Hash. Pages are added with `add_page`:
76
+
77
+ ```ruby
78
+ add_page 'index.html', template: 'index.html.erb', foo: 'bar'
79
+ # sitemap['index.html'] == { template: 'index.html.erb', foo: 'bar' }
80
+ ```
81
+
82
+ In the context of the template for this page, the data given to `add_page` is accessible in the `page` hash.
83
+
84
+ In `index.html.erb`:
85
+ ```ERB
86
+ <% page == { template: 'index.html.erb', foo: 'bar' } %>
87
+ <%=page[:foo] # 'bar' %>
88
+ ```
89
+
90
+ ## Configuration
91
+
92
+ A new `VizBuilder` instance can be configured directly or via a block passed into the constructor:
93
+
94
+ ```ruby
95
+ app = VizBuilder.new
96
+ app.set :some_global_config_thing, 'hello world'
97
+ app.add_page 'index.html', template: 'index.html.erb'
98
+ ```
99
+
100
+ Or you can use a block:
101
+
102
+ ```ruby
103
+ app = VizBuilder.new do
104
+ set :some_global_config_thing, 'hello world'
105
+ add_page 'index.html', template: 'index.html.erb'
106
+ end
107
+ ```
108
+
109
+ There are a few config settings used by Viz Builder:
110
+
111
+ #### http_prefix
112
+
113
+ Used during building for setting canonical urls and links between pages of the site. Used by the `canonical_url` helper detailed below.
114
+
115
+ #### asset_http_prefix
116
+
117
+ Used during building for setting asset urls. Used by the `asset_path` helper detailed below.
118
+
119
+ #### layout
120
+
121
+ An optional setting that will render all pages inside the specified ERB layout file.
122
+
123
+ ## Templates
124
+
125
+ Templates are loaded by path from the root of your project directory. There are currently no special locations from which template files are loaded. If you keep your templates in a subdirectory of your project, you'll need to include the directory when specifying the file:
126
+
127
+ ```ruby
128
+ # keeping our templates in a subdirectory called 'templates'
129
+ app = VizBuilder.new do
130
+ add_page 'index.html', template: 'templates/index.html.erb'
131
+ add_page 'article.html', template: 'templates/article.html.erb'
132
+ end
133
+ ```
134
+
135
+ #### Layouts
136
+
137
+ You can have your templates render inside a `layout` file by either setting a global `:layout` or by setting the layout on a page-by-page basis.
138
+
139
+ To render all pages in the app in a layout:
140
+
141
+ ```ruby
142
+ app = VizBuilder.new do
143
+ set :layout, 'layout.html.erb'
144
+ add_page 'index.html', template: 'index.html.erb'
145
+ add_page 'article.html', template: 'article.html.erb'
146
+ end
147
+ ```
148
+
149
+ If you don't want a layout used for all pages, or if one page needs to use a different layout, you can pass `layout` to `add_page`:
150
+
151
+ ```ruby
152
+ app = VizBuilder.new do
153
+ add_page 'index.html', template: 'index.html.erb', layout: 'layout.html.erb'
154
+ add_page 'article.html', template: 'article.html.erb', layout: 'article_layout.html.erb'
155
+ add_page 'snippet.html', template: 'snippet.html.erb' # no layout is used for this one
156
+ end
157
+ ```
158
+
159
+ ## Helpers
160
+
161
+ There are a few helpers included in Viz Builder, and you can add your own as well. Helpers are methods that are usable from a template or from the configuration block.
162
+
163
+ To add helpers:
164
+
165
+ ```ruby
166
+ app = VizBuilder.new do
167
+ ...
168
+
169
+ helpers do
170
+ def do_something(args)
171
+ # do stuff
172
+ 'stuff is done'
173
+ end
174
+ end
175
+ end
176
+ ```
177
+
178
+ You can also break your helpers out into a separate file and module.
179
+
180
+ So in a new file called `helpers.rb` we add:
181
+
182
+ ```ruby
183
+ module MyHelpers
184
+ def do_something(args)
185
+ # do stuff
186
+ 'stuff is done'
187
+ end
188
+ end
189
+ ```
190
+
191
+ Then in your `app.rb` add:
192
+
193
+ ```ruby
194
+ require 'helpers.rb'
195
+
196
+ app = VizBuilder.new do
197
+ ...
198
+
199
+ helpers MyHelpers
200
+ end
201
+ ```
202
+
203
+ Helpers can be used immediately in the configuration block in addition to in the template:
204
+
205
+ ```ruby
206
+ app = VizBuilder.new do
207
+ ...
208
+
209
+ helpers do
210
+ def do_something(args)
211
+ # do stuff
212
+ 'stuff is done'
213
+ end
214
+ end
215
+
216
+ do_something :stuff
217
+ end
218
+ ```
219
+
220
+ WARNING: You should probably not use instance variables in helpers (`@myvariable`), because the instance variables are not shared between the configuration block and templates. You can use `set` and `add_data` to stash data between helper uses.
221
+
222
+ ### Built-in helpers
223
+
224
+ #### render
225
+
226
+ Render a template or partial and return the output. Takes the path to the template and a hash of local variables.
227
+
228
+ ```ruby
229
+ <%=render 'list.html.erb', list_items: items %>
230
+ ```
231
+
232
+ #### include_file
233
+
234
+ Load and return the contents of a file. Good for inlining CSS or SVG content.
235
+
236
+ ```erb
237
+ <%=include_file 'logo.svg' %>
238
+ ```
239
+
240
+ #### http_prefix
241
+
242
+ Returns the value of the config item `http_prefix`. Should represent the root path to the site.
243
+
244
+ #### asset_http_prefix
245
+
246
+ Returns the value of the config item `asset_http_prefix` or `http_prefix`. Should represent the root path to all assets for the site.
247
+
248
+ #### asset_path
249
+
250
+ Returns a working url pointing to a given asset. Any provided arguments will be added to the returned url:
251
+
252
+ ```erb
253
+ <%=asset_path :images, 'logo.png' %>
254
+ <!-- outputs {asset_http_prefix}/images/logo.png --->
255
+ <%=asset_path 'images/logo.png' %>
256
+ <!-- also outputs {asset_http_prefix}/images/logo.png --->
257
+ ```
258
+
259
+ #### canonical_url
260
+
261
+ Returns the full domain name and path to the root of the site. Any provided arguments will be added to the returned url:
262
+
263
+ ```erb
264
+ <%=canonical_url 'page1' %>
265
+ <!-- outputs {http_prefix}/page1 --->
266
+ ```
267
+
268
+ #### build?
269
+
270
+ True if Viz Builder is building out all the pages and not running as a rack app (aka web server).
271
+
272
+ #### server?
273
+
274
+ True if Viz Builder is running as a rack app (aka web server).
275
+
276
+ #### development?
277
+
278
+ True if Viz Builder is running in development mode. Usually synonymous with `server?`.
279
+
280
+ #### production?
281
+
282
+ True if Viz Builder is running in production mode. Usually synonymous with `build?`.
283
+
284
+ ## Assets
285
+
286
+ You can generate Javascript and CSS using the same template processing used for HTML files. Just add a JS or CSS file as a page in your app configuration:
287
+
288
+ ```ruby
289
+ app = VizBuilder.new do
290
+ add_page 'index.html', template: 'index.html.erb'
291
+ add_page 'app.js', template: 'app.js.erb'
292
+ add_page 'app.css', template: 'app.css.erb'
293
+ end
294
+ ```
295
+
296
+ Viz Builder will also look in the `prebuild` directory for static assets. The builder will copy these files into the `build` directory during the build process, or it will serve these files directly during development server.
297
+
298
+ Viz Builder will not transform or minify assets for you. It will not handle SCSS and it will not compile ES6+ down to ES5 Javascript. I recommend using something like [webpack](https://webpack.js.org/) to handle processing javascript and stylesheets. Whatever tool you use should be configured to save output files to `prebuild`.
299
+
300
+ If you only have a little script and style in your site, we recommend just using vanilla Javascript and CSS.
301
+
302
+ #### Asset hashing
303
+
304
+ Viz Builder automatically adds sha1 crypto hashes to the filenames of certain asset files when building out a site. Any files from the `prebuild` directory will get these hashes. You can have these crypto hashes added to any page with the `digest` option:
305
+
306
+ ```ruby
307
+ app = VizBuilder.new do
308
+ add_page 'app.js', template: 'app.js.erb', digest: true
309
+ end
310
+ ```
311
+
312
+ WARNING: You probably don't want to use the `digest` on HTML pages.
313
+
314
+ You must use the `asset_path` helper to always get a proper URL for your asset files:
315
+
316
+ ```ERB
317
+ <script src="<%=asset_path 'javascript/app.js' %>"></script>
318
+ <link ref="stylesheet" href="<%=asset_path 'stylesheets/app.css' %>" />
319
+ ```
320
+
321
+ WARNING: If you do not use the `asset_path` helper, stuff will work in development but will break when you deploy.
322
+
323
+ ## Contributing
324
+
325
+ Fork this repo, create a new branch on your fork, and make your changes there.
326
+ Open a pull request on this repo for consideration.
327
+
328
+ If its a small bugfix, feel free making the changes and opening a PR. If it's a
329
+ feature addition or a more substantial change, please open a github issue
330
+ outlining the feature or change. This is just to save you time and make sure
331
+ your efforts can get aligned with other folks' plans.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/*/test_*.rb']
8
+ end
9
+
10
+ task default: :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/do_release.sh ADDED
@@ -0,0 +1,35 @@
1
+ #!/bin/bash
2
+
3
+ if ! git diff-index --quiet HEAD --; then
4
+ echo You must commit all your changes before updating the version
5
+ exit 1
6
+ fi
7
+
8
+ old_version=$(cat VERSION)
9
+
10
+ if [ $# -ne 1 ]; then
11
+ read -p "Current version is $old_version. Enter a new version: " version
12
+ else
13
+ version=$1
14
+ fi
15
+
16
+ if [ "$old_version" = "$version" ]; then
17
+ echo Already at version $version
18
+ exit 1
19
+ fi
20
+
21
+ echo Updating version to $version
22
+
23
+ echo $version > VERSION
24
+
25
+ read -p "Do you wish to commit the new version, tag and push? [y/N] " tyn
26
+ if echo "$tyn" | grep -iq "^y"; then
27
+ git commit -am "bump to $version" && git tag v$version && git push && git push --tags
28
+
29
+ read -p "Do you wish to build and publish the release? [y/N] " pyn
30
+ if echo "$pyn" | grep -iq "^y"; then
31
+ rm *.gem
32
+ gem build vizbuilder.gemspec
33
+ gem push *.gem
34
+ fi
35
+ fi
@@ -0,0 +1 @@
1
+ require 'vizbuilder'
@@ -0,0 +1 @@
1
+ require 'vizbuilder'
data/lib/vizbuilder.rb ADDED
@@ -0,0 +1,420 @@
1
+ require 'psych'
2
+ require 'fileutils'
3
+ require 'erb'
4
+ require 'json'
5
+ require 'rack'
6
+ require 'mimemagic'
7
+ require 'active_support/all'
8
+ require 'ruby_dig'
9
+ require 'base64'
10
+ require 'digest'
11
+ require 'english'
12
+
13
+ # The VizBuilder class does all the stuff for a viz builder app
14
+ class VizBuilder
15
+ attr_reader :config
16
+ delegate :sitemap, :data, :hooks, :helper_modules, to: :config
17
+
18
+ BUILD_DIR = 'build'.freeze
19
+ PREBUILT_DIR = 'prebuild'.freeze
20
+ DATA_DIR = 'data'.freeze
21
+
22
+ def initialize(config = {}, &blk)
23
+ # Config is an object used as the context for the given block
24
+ @config = Config.new(config: config)
25
+ # Load data into @data
26
+ reload_data!
27
+ # Execute the given block as a method of this class. The block can then use
28
+ # the methods `add_page`, `add_data`, and `set`
29
+ @config.instance_exec(&blk) if block_given?
30
+ # Run any after load data hooks since they got added after the first load
31
+ # data was called.
32
+ run_hook!(:after_load_data)
33
+ # Add helpers to the TemplateContext class
34
+ TemplateContext.include(*helper_modules) unless helper_modules.empty?
35
+ end
36
+
37
+ # Generate all pages in the sitemap and save to `build/`
38
+ def build!(silent: false)
39
+ ctx = TemplateContext.new(:production, :build, @config)
40
+ index_prebuilt!
41
+ # First we build prebuilt pages that need digests calculated by build_page
42
+ digested = sitemap.select { |_path, page| page[:digest] == true }
43
+ digested.each { |path, _page| build_page(path, ctx, silent: silent) }
44
+ # Then we build all other pages
45
+ undigested = sitemap.reject { |_path, page| page[:digest] == true }
46
+ undigested.each { |path, _page| build_page(path, ctx, silent: silent) }
47
+ end
48
+
49
+ # Run this builder as a server
50
+ def runserver!(host: '127.0.0.1', port: '3456')
51
+ status = 0 # running: 0, reload: 1, exit: 2
52
+ # spawn a thread to watch the status flag and trigger a reload or exit
53
+ monitor = Thread.new do
54
+ sleep 0.1 while status.zero?
55
+ # Shutdown the server, wait for it to finish and then wait a tick
56
+ Rack::Handler::WEBrick.shutdown
57
+ sleep 0.1
58
+ # Use ps to get the command that the user executed, and use Kernel.exec
59
+ # to execute the command, replacing the current process.
60
+ # Basically restart everything.
61
+ Kernel.exec(`ps #{$PID} -o command`.split("\n").last) if status == 1
62
+ end
63
+
64
+ # trap ctrl-c and set status flag
65
+ trap('SIGINT') do
66
+ if status == 1
67
+ status = 2 # ctrl-c hit twice or more, set status to exit
68
+ elsif status.zero?
69
+ # ctrl-c hit once, notify user and set status to reload
70
+ puts "\nReloading the server, hit ctrl-c twice to exit\n"
71
+ status = 1
72
+ end
73
+ end
74
+
75
+ puts "\nStarting Dev server, hit ctrl-c once to reload, twice to exit\n"
76
+ Rack::Handler::WEBrick.run(self, Host: host, Port: port)
77
+ monitor.join # let the monitor thread finish its work
78
+ end
79
+
80
+ # Support the call method so an instance can act as a Rack app
81
+ def call(env)
82
+ # Only support GET, OPTIONS, HEAD
83
+ unless env['REQUEST_METHOD'].in?(%w[GET HEAD OPTIONS])
84
+ return [405, { 'Content-Type' => 'text/plain' }, ['METHOD NOT ALLOWED']]
85
+ end
86
+
87
+ # default response is 404 not found
88
+ status = 404
89
+ content_type = 'text/plain'
90
+ content = '404 File not found'
91
+
92
+ # Validate the requested path
93
+ path = env['PATH_INFO']
94
+ if path =~ %r{/$}
95
+ # path is for a directory
96
+ path += 'index.html'
97
+ elsif path =~ %r{/[^./]+$}
98
+ # path looks like a directory but is missing a trailing slash
99
+ path += '/index.html'
100
+ end
101
+
102
+ # remove any leading slashes
103
+ path = path[1..-1] if path =~ %r{^/}
104
+
105
+ # filename for a potential prebuilt asset
106
+ build_filename = File.join(PREBUILT_DIR, path)
107
+
108
+ # Check our sitemap then our prebuilt folder for content to serve
109
+ if sitemap[path].present?
110
+ content_type = MimeMagic.by_path(path).to_s
111
+ ctx = TemplateContext.new(:development, :server, @config)
112
+ content = build_page(path, ctx)
113
+ status = 200
114
+ elsif File.exist?(build_filename)
115
+ content_type = MimeMagic.by_path(path).to_s
116
+ content = File.read(build_filename)
117
+ status = 200
118
+ end
119
+
120
+ # Status code, headers and content for this response
121
+ [status, { 'Content-Type' => content_type }, [content]]
122
+ end
123
+
124
+ # Force a reload of data files in `data/`
125
+ def reload_data!
126
+ data.merge!(load_data)
127
+ # TODO: after load hooks only run once, not every time reload_data! is called
128
+ run_hook!(:after_load_data)
129
+ end
130
+
131
+ # Like File.extname, but gets all extensions if multiple are present
132
+ def self.fullextname(path)
133
+ fname = File.basename(path)
134
+ parts = fname.split('.')
135
+ ".#{parts[1..-1].join('.')}"
136
+ end
137
+
138
+ # Convenience methods for configuring the site
139
+ class Config
140
+ attr_accessor :data, :config, :sitemap, :hooks, :helper_modules
141
+ delegate :[], :[]=, :key?, to: :config
142
+
143
+ def initialize(config: {})
144
+ # Config is a Hash of site wide configuration variables
145
+ @config = config.with_indifferent_access
146
+ # Data contains content and data that needs displaying
147
+ @data = {}.with_indifferent_access
148
+ # Hooks is a hash of hook names and arrays of blocks registered to be
149
+ # executed when those hooks are reached
150
+ @hooks = {}.with_indifferent_access
151
+ # Sitemap is a hash representing the documents in the site that will be
152
+ # processed by Builder. No indifferent access since the keys are all
153
+ # file paths.
154
+ @sitemap = {}
155
+ # Helpers is an array of Modules that will get mixed into the template
156
+ # context and with the current Config instance
157
+ @helper_modules = []
158
+ end
159
+
160
+ # Add a page to the sitemap
161
+ def add_page(path, kwargs)
162
+ @sitemap[path] = kwargs.with_indifferent_access
163
+ self
164
+ end
165
+
166
+ # Add or replace data
167
+ def add_data(key, val)
168
+ @data[key] = val.is_a?(Hash) ? val.with_indifferent_access : val
169
+ self
170
+ end
171
+
172
+ # Set a global config option
173
+ def set(key, val)
174
+ @config[key] = val.is_a?(Hash) ? val.with_indifferent_access : val
175
+ self
176
+ end
177
+
178
+ # Add code to run after data is loaded or reloaded. Is also run after object
179
+ # is created.
180
+ def after_load_data(&blk)
181
+ hook(:after_load_data, &blk)
182
+ self
183
+ end
184
+
185
+ # Add a block to a hook name
186
+ def hook(name, &blk)
187
+ @hooks[name.to_sym] ||= []
188
+ @hooks[name.to_sym] << blk
189
+ self
190
+ end
191
+
192
+ # Add helper modules or define helpers in a block
193
+ def helpers(*mods, &blk)
194
+ new_helpers = []
195
+ # loop over the list of arguments, making sure they're all modules, and
196
+ # then add them to the list of new helpers
197
+ mods.each do |mod|
198
+ unless mods.is_a?(Module)
199
+ raise ArgumentError, 'Helpers must be defined in a module or block'
200
+ end
201
+
202
+ new_helpers << mod
203
+ end
204
+ # if block is given, turn it into a module and add it to the helpers list
205
+ new_helpers << Module.new(&blk) if blk
206
+
207
+ if new_helpers.present?
208
+ # extend the current Config instance with the helpers, making them available
209
+ # to the rest of the configuration block
210
+ extend(*new_helpers)
211
+ # add our new helpers to our array of all helpers
212
+ @helper_modules += new_helpers
213
+ end
214
+
215
+ self
216
+ end
217
+
218
+ def respond_to_missing?(sym, *)
219
+ config.key?(sym) || super
220
+ end
221
+
222
+ # Treat config options as local
223
+ def method_missing(sym)
224
+ return config[sym] if config.key?(sym)
225
+
226
+ super
227
+ end
228
+ end
229
+
230
+ # Templates are rendered using this class. Templates are effectively treated
231
+ # as methods of this class when rendered. So any methods or attributes exposed
232
+ # in this class are available inside of templates.
233
+ class TemplateContext
234
+ attr_accessor :page
235
+ delegate :data, :sitemap, :config, to: :@config_obj
236
+
237
+ def initialize(target, mode, config)
238
+ # Target is development or production
239
+ @target = target.to_sym
240
+ # Mode is build or server
241
+ @mode = mode.to_sym
242
+ # Config is our Builder config object
243
+ @config_obj = config
244
+ # Page is a hash representing the page. Is the same as whats in sitemap
245
+ # for a given page path
246
+ @page = {}
247
+ # Locals is a hash thats used to resolve missing methods, making them
248
+ # seem like local variables
249
+ @locals = {}
250
+ end
251
+
252
+ # Render any given template and return as a string. Can be used to render
253
+ # partials.
254
+ def render(template_path, locals = {})
255
+ old_locals = @locals
256
+ @locals = locals.with_indifferent_access
257
+ erb = ERB.new(File.read(template_path), trim_mode: '-')
258
+ erb.filename = File.expand_path(template_path)
259
+ ret = erb.result(binding)
260
+ @locals = old_locals
261
+ ret
262
+ end
263
+
264
+ # Load the content of a pre-rendered file and return it
265
+ def include_file(filepath)
266
+ content = File.read(filepath)
267
+ mime = MimeMagic.by_path(filepath)
268
+ if mime.text?
269
+ return content
270
+ elsif mime.image?
271
+ return Base64.strict_encode64(content)
272
+ else
273
+ raise "File '${filepath}' of type '${mime}' can't be included as text"
274
+ end
275
+ end
276
+
277
+ # HELPERS
278
+
279
+ # Get the full URL to the root of this site
280
+ def http_prefix
281
+ return '/' if server? && development?
282
+ prefix = config[:http_prefix] || '/'
283
+ prefix += '/' unless prefix =~ %r{/$}
284
+ prefix
285
+ end
286
+
287
+ # Get the full URL to the root of where assets are stored
288
+ def asset_http_prefix
289
+ return '/' if server? && development?
290
+ prefix = config[:asset_http_prefix] || http_prefix
291
+ prefix += '/' unless prefix =~ %r{/$}
292
+ prefix
293
+ end
294
+
295
+ # Generate and return the URL to any asset
296
+ def asset_path(*args)
297
+ path = args.join('/')
298
+ page = sitemap[path]
299
+ if production? && page[:digest] == true
300
+ raise "Missing digest for #{path}" if page[:digest_path].blank?
301
+ path = page[:digest_path]
302
+ end
303
+ asset_http_prefix + path
304
+ end
305
+
306
+ # Generate and return the URL to any page
307
+ def canonical_url(*args)
308
+ http_prefix + args.join('/')
309
+ end
310
+
311
+ # Are we running as a server?
312
+ def server?
313
+ @mode == :server
314
+ end
315
+
316
+ # Are we building this app out?
317
+ def build?
318
+ @mode == :build
319
+ end
320
+
321
+ # Is this in production?
322
+ def production?
323
+ @target == :production
324
+ end
325
+
326
+ # Is this in development?
327
+ def development?
328
+ @target == :development
329
+ end
330
+
331
+ # Looks for an invoked method name in locals then in config, so locals
332
+ # and config vars can be used as if they're local vars in the template
333
+ def method_missing(sym)
334
+ return @locals[sym] if @locals.key?(sym)
335
+ return config[sym] if config.key?(sym)
336
+ super
337
+ end
338
+
339
+ def respond_to_missing?(sym, *)
340
+ @locals.key?(sym) || config.key?(sym) || super
341
+ end
342
+ end
343
+
344
+ private
345
+
346
+ # Generate one page from the sitemap and save to `build/`
347
+ def build_page(path, ctx, silent: false)
348
+ ctx.page = sitemap[path]
349
+ out_fname = File.join(BUILD_DIR, path)
350
+
351
+ # Check page data for info on how to build this path
352
+ if ctx.page['template'].present?
353
+ content = ctx.render(ctx.page['template'])
354
+ elsif ctx.page['json'].present?
355
+ content = ctx.page['json'].to_json
356
+ elsif ctx.page['yaml'].present?
357
+ content = ctx.page['yaml'].to_yaml
358
+ elsif ctx.page['file'].present?
359
+ content = File.read(ctx.page['file'])
360
+ else
361
+ raise(
362
+ ArgumentError,
363
+ "Page '#{path}' missing one of required attributes: 'template', 'json', 'yaml', 'file'."
364
+ )
365
+ end
366
+
367
+ # Check if we have a layout defined, use it
368
+ layout_fname = ctx.page['layout'] || config['layout']
369
+ content = ctx.render(layout_fname) { content } if layout_fname.present?
370
+
371
+ # If page data includes a digest flag, add sha1 digest to output filename
372
+ if ctx.page['digest'] == true
373
+ ext = Builder.fullextname(path)
374
+ fname = File.basename(path, ext)
375
+ dir = File.dirname(path)
376
+ digest = Digest::SHA1.hexdigest(content)
377
+ digest_fname = "#{fname}-#{digest}#{ext}"
378
+ ctx.page['digest_path'] = "#{dir}/#{digest_fname}"
379
+ out_fname = File.join(BUILD_DIR, dir, digest_fname)
380
+ end
381
+
382
+ puts "Writing #{out_fname}..." unless silent
383
+ FileUtils.mkdir_p(File.dirname(out_fname))
384
+ File.write(out_fname, content)
385
+ content
386
+ end
387
+
388
+ # Read json and yaml files from `data/` and load them into a Hash using the
389
+ # basename of the file names.
390
+ def load_data
391
+ data = {}.with_indifferent_access
392
+
393
+ Dir.glob("#{DATA_DIR}/*.json") do |fname|
394
+ key = File.basename(fname, '.json').to_sym
395
+ puts "Loading data[:#{key}] from #{fname}..."
396
+ data[key] = JSON.parse(File.read(fname))
397
+ end
398
+
399
+ Dir.glob("#{DATA_DIR}/*.yaml") do |fname|
400
+ key = File.basename(fname, '.yaml').to_sym
401
+ puts "Loading data[:#{key}] from #{fname}..."
402
+ data[key] = Psych.parse(fname)
403
+ end
404
+
405
+ data
406
+ end
407
+
408
+ # Execute all blocks in config registered with the given hook name.
409
+ def run_hook!(name)
410
+ return unless hooks[name.to_sym]
411
+ hooks[name.to_sym].each { |blk| config.instance_exec(&blk) }
412
+ end
413
+
414
+ # Find prebuilt assets and add them to the sitemap
415
+ def index_prebuilt!
416
+ Dir.glob("#{PREBUILT_DIR}/**/[^_]*.*") do |filename|
417
+ sitemap[filename.sub("#{PREBUILT_DIR}/", '')] = { file: filename, digest: true }
418
+ end
419
+ end
420
+ end
@@ -0,0 +1,26 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'vizbuilder'
6
+ spec.version = File.read('VERSION').strip
7
+ spec.authors = ['Ryan Mark']
8
+ spec.email = ['ryan@mrk.cc']
9
+
10
+ spec.summary = 'Simple and fast static site generator'
11
+ spec.homepage = 'https://github.com/ryanmark/vizbuilder-ruby'
12
+
13
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
14
+ f.match(%r{^(test|spec|features)/})
15
+ end
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_dependency 'activesupport', '~> 5.2'
19
+ spec.add_dependency 'mimemagic', '~> 0.3'
20
+ spec.add_dependency 'rack', '~> 2.0'
21
+ spec.add_dependency 'ruby_dig', '~> 0.0.2'
22
+
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'minitest', '~> 5.11'
25
+ spec.add_development_dependency 'rake', '~> 12.3'
26
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vizbuilder
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Mark
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mimemagic
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby_dig
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.0.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.0.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.11'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '5.11'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '12.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '12.3'
111
+ description:
112
+ email:
113
+ - ryan@mrk.cc
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rubocop.yml"
120
+ - CHANGELOG.md
121
+ - Gemfile
122
+ - LICENSE
123
+ - README.md
124
+ - Rakefile
125
+ - VERSION
126
+ - do_release.sh
127
+ - lib/viz-builder.rb
128
+ - lib/viz_builder.rb
129
+ - lib/vizbuilder.rb
130
+ - vizbuilder.gemspec
131
+ homepage: https://github.com/ryanmark/vizbuilder-ruby
132
+ licenses: []
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubygems_version: 3.0.1
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Simple and fast static site generator
153
+ test_files: []