xstatic 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/DESIGN.rdoc +173 -0
- data/README.rdoc +88 -0
- data/Rakefile +25 -0
- data/TODO +48 -0
- data/bin/xstatic +321 -0
- data/lib/html_page.rb +193 -0
- data/xstatic.gemspec +37 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b69b5953ca4db2d64161ef0bc093227368c6bebb710efd9b5967da0d82fa8e27
|
4
|
+
data.tar.gz: 35ecaa8dad413c97c7009fa7cda3b5456e904996b564217af3e84cc804da7def
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 293657fcc5dcc1f02031c6854aeadafc253434087ae48ab1d1bf71b38a4b5f6c342abf861aea643ac507376e8408ea8b9ebef6cd232a038dc6c5a161173a2658
|
7
|
+
data.tar.gz: b3c058b68184c0caad38d6bc80486ca59a778fd187907a92b93b54499262de5749410aa99b7ad9b3a0ea0a3d46d5daa576a4c85e858cc869e26d93ada6134f6f
|
data/DESIGN.rdoc
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
= XStatic Design Notes
|
2
|
+
|
3
|
+
This is the ruby version of Staticizer. It uses a similar method as the Mote
|
4
|
+
gem to parse ERB files very quickly.
|
5
|
+
|
6
|
+
See the m4/C version in ~/work/staticizer-m4/Readme for details.
|
7
|
+
|
8
|
+
== Pros & Cons: Staticizer vs. SSI
|
9
|
+
|
10
|
+
While researching for a simpler way to build websites, I explored the plethora
|
11
|
+
of static site generators. Most are overly engineered. Others seem easy from
|
12
|
+
the user's perspective, but are complex under the hood. I also wanted something
|
13
|
+
with minimal dependencies, and preferably existing and standard tools.
|
14
|
+
|
15
|
+
So I decided to build my own using make(1) and m4(1). It worked and is super
|
16
|
+
fast. The problem is that is is difficult to understand the m4 language. Plus
|
17
|
+
it was not as flexible or extendible as something built with an interpreted
|
18
|
+
language like ruby. Awk could be a possibility but I never explored it.
|
19
|
+
|
20
|
+
Then I decided to reimplement the make/m4 system using rake/ruby. It's very
|
21
|
+
nice, easy to use with a simple and straightforward implementation. It would
|
22
|
+
also be very easy to add features, even though I haven't done it.
|
23
|
+
|
24
|
+
The one problem with both of these systems is dependency. An HTML page is
|
25
|
+
constructed from the content (markdown, html, erb), which is encapsulated in a
|
26
|
+
layout. Optionally, the content and layout may include templates. The
|
27
|
+
dependency problem occurs when a template is updated, how to update the content
|
28
|
+
and layouts that may depend on it? I know make and rake are supposed to handle
|
29
|
+
this stuff, but it requires manual intervention to assign the dependencies.
|
30
|
+
This leaves lots of room for human error.
|
31
|
+
|
32
|
+
Not satisfied, I decided to keep looking for an answer. I was reminded of
|
33
|
+
server-side includes (SSI). I had used them 20 years ago but jumped on the Ruby
|
34
|
+
on Rails bandwagon and just forgot. After revisiting SSI, I think this may
|
35
|
+
satisfy my requirements and deal with the dependency issue. They inherently
|
36
|
+
solve the dependency issue by compiling the page "parts" on the fly. For
|
37
|
+
example, if a template is updated, any page content or layout that depends on
|
38
|
+
it will simply include the new version on the next request. That's it!
|
39
|
+
|
40
|
+
But SSI is not without problems. It compiles HTML, not markdown, in which some
|
41
|
+
of my content is formatted. So a processing step would be required, which
|
42
|
+
diminishes its advantage over the make/m4 and rake/ruby systems. Therefore SSI
|
43
|
+
doesn't have a dependency problem but does require some preprocessing, which m4
|
44
|
+
and ruby do.
|
45
|
+
|
46
|
+
A notable issue with SSI is the web server configuration. It's not as friendly
|
47
|
+
or accessible as a makefile. For example, encapsulating a set of pages in a
|
48
|
+
layout would be specified in the web server routing rules (e.g., location
|
49
|
+
directive in nginx).
|
50
|
+
|
51
|
+
But the biggest problem with SSI is that, unlike the other systems, variables
|
52
|
+
are only in-scope at the point of definition or inclusion. For example, a title
|
53
|
+
in head cannot be set by a variable defined at the page-content level. The m4
|
54
|
+
processor solves this by ordering buffers. Ruby solves this with binding and
|
55
|
+
runtime evaluation. The only way I see to solve this in SSI is to parse the
|
56
|
+
include files and generate a "meta" file that would be included at the top of
|
57
|
+
the document.
|
58
|
+
|
59
|
+
SSI also lacks the programming power of a language like Ruby. This power can be
|
60
|
+
used for good. For example, auto-generating figure numbers and references
|
61
|
+
within a technical document. This could probably be solved with SSI variables,
|
62
|
+
but I think it would get ugly.
|
63
|
+
|
64
|
+
Statically generating HTML pages provides maximum portability. Any web server
|
65
|
+
can serve them. However, SSI is much less portable. The SSI directives are
|
66
|
+
somewhat similar between nginx and Apache; but other servers, like OpenBSD's
|
67
|
+
httpd, don't support them. The bigger problem is that web server configurations
|
68
|
+
are all unique.
|
69
|
+
|
70
|
+
A very powerful feature of SSI is the inclusion of a request. This provides a
|
71
|
+
static page with dynamic elements. A drawback is client-side caching. If the
|
72
|
+
page were cached then the dynamic effect would be lost. I guess this is where
|
73
|
+
client-side Javascript is the answer.
|
74
|
+
|
75
|
+
A possibly minor disadvantage of SSI over static pages is performance. The web
|
76
|
+
server is parsing—perhaps requesting virtual includes—and compiling complete
|
77
|
+
pages on the fly. However, nginx does provide some caching solutions, but this
|
78
|
+
is just more complexity.
|
79
|
+
|
80
|
+
An example SSI configuration with good presentation and content separation is
|
81
|
+
<https://www.nginx.com/resources/wiki/start/topics/examples/dynamic_ssi/>.
|
82
|
+
|
83
|
+
A cool benefit of the rake system is that it scans the page sources looking for
|
84
|
+
templates to add as dependencies, and, if a template doesn't actually exist it
|
85
|
+
complains.
|
86
|
+
|
87
|
+
*Idea*: could the include directive virtually request a markdown page, which
|
88
|
+
will get compiled on the fly? That would be kind of cool.
|
89
|
+
|
90
|
+
|
91
|
+
== Page & Template Processing
|
92
|
+
|
93
|
+
The problem with variables in XML comments (<!-- -->) and processing
|
94
|
+
instructions (<? ?>) in markdown is that kramdown will output them. Erb
|
95
|
+
processing cannot use such instructions. Alternatively, when using the Erb
|
96
|
+
processing instructions (<% %>), kramdown entity encodes the tags.
|
97
|
+
|
98
|
+
The immediate solution is to process with Erb first then kramdown. The only
|
99
|
+
problem with this is that Erb may be processing all of the markdown only for
|
100
|
+
a few variables at the top, or worse, for no reason at all. This could be
|
101
|
+
optimized later by only reading the beginning of the file where the code
|
102
|
+
block must be, then pass the rest of the file on to kramdown. A flag could be
|
103
|
+
set in the preamble that indicates if the rest of the file should be processed
|
104
|
+
by Erb or not. This could improve performance but would require more logic.
|
105
|
+
|
106
|
+
When processing markdown content, first run Erb to evaluate all processing
|
107
|
+
instructions. Erb processing instructions may evaluate to html or additional
|
108
|
+
markdown—either of which kramdown can manage. Then kramdown converts to html.
|
109
|
+
Finally, the page renders with a layout.
|
110
|
+
|
111
|
+
== Indexing & Search
|
112
|
+
|
113
|
+
At some point, need to add a search feature.
|
114
|
+
Start at <https://en.wikipedia.org/wiki/Search_engine_indexing>.
|
115
|
+
Should probably index or parse the final HTML files. It's tempting to parse the
|
116
|
+
markdown files directly because they are simple, but this would complicate the
|
117
|
+
search engine because then it would also have to be adapted to search HTML,
|
118
|
+
Erb, etc. Plus, HTML has additional semantics.
|
119
|
+
|
120
|
+
Been thinking about how to do this, but never got serious about it. While
|
121
|
+
researching email clients, I ran across [Notmuch](https://notmuchmail.org)
|
122
|
+
which is an email search utility used by Mutt and others. Notmuch uses Xapian
|
123
|
+
under the hood. The port mail/mu is a searcher that also uses Xapian.
|
124
|
+
|
125
|
+
[Xapian](https://xapian.org) is a highly adaptable toolkit which allows
|
126
|
+
developers to easily add advanced indexing and search facilities to their own
|
127
|
+
applications. It is written in C++ and has many bindings, including Lua, Ruby,
|
128
|
+
and Tcl.
|
129
|
+
|
130
|
+
== Changed Resources
|
131
|
+
|
132
|
+
What if a style sheet or script changes? How will the layouts or pages that
|
133
|
+
depend on them get updated?
|
134
|
+
|
135
|
+
Changes in CSS, Javascript, or other externally linked resources is not
|
136
|
+
particularly in XStatic's domain. This is the concern of HTML linking and HTTP
|
137
|
+
caching. XStatic is unaware of the relationship between these linked resources.
|
138
|
+
However, to serve new versions of such resources, XStatic can help overcome
|
139
|
+
long cache times.
|
140
|
+
|
141
|
+
One solution is to add or update a version parameter to the resource's URL.
|
142
|
+
That will trigger a rebuild of the page, which updates the last-modified and
|
143
|
+
etag headers sent by the HTTP server. With a proper cache configuration, the
|
144
|
+
client will request the new version of the resource.
|
145
|
+
|
146
|
+
== Path Name Conflicts Within the HTML Directory
|
147
|
+
|
148
|
+
Page names may conflict with directory or asset names or another page with the
|
149
|
+
same name but a different extension. A rendered page is stored in an
|
150
|
+
extensionless filename under the HTML site directory. For example, if there is
|
151
|
+
a directory named "book", then a page named "book.md" within the same directory
|
152
|
+
will conflict.
|
153
|
+
|
154
|
+
content/
|
155
|
+
book.md
|
156
|
+
book/
|
157
|
+
chapter1
|
158
|
+
chapter2
|
159
|
+
|
160
|
+
The page "book.md" and the directory "book/" will have the same path name under
|
161
|
+
the HTML site directory. To resolve this, move "book.md" to an index file under
|
162
|
+
the "book/" directory.
|
163
|
+
|
164
|
+
content/
|
165
|
+
book/
|
166
|
+
index.md
|
167
|
+
chapter1
|
168
|
+
chapter2
|
169
|
+
|
170
|
+
Likewise, the pages "foobar.md" and "foobar.erb" within the same directory will
|
171
|
+
conflict (i.e., they both render as the HTML file "foobar"). All markdown is
|
172
|
+
preprocessed with Erb first; using "book.md" will suffice.
|
173
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
= XStatic—Smart Static Site Generator
|
2
|
+
|
3
|
+
A smart, static site generator that automatically manages dependencies to
|
4
|
+
achieve blazing build times with minimal cognitive load. Only new and changed
|
5
|
+
files, and files upstream of a changed dependency are processed. Renders
|
6
|
+
markdown or embedded-Ruby (Erb-like) content as HTML.
|
7
|
+
|
8
|
+
Supports templates (embedded & layout), which may be included within content
|
9
|
+
sources or other templates. Document metadata may me added using a plain-text
|
10
|
+
preamble of key-value pairs. Generates a complete website that can be served by
|
11
|
+
the built-in WEBrick server.
|
12
|
+
|
13
|
+
XStatic is just a _Rake_ application with some magic sprinkled on top.
|
14
|
+
|
15
|
+
Markdown and Erb content are transformed with a custom Erb processor. This
|
16
|
+
allows you to add application logic to your markdown content, for example fetch
|
17
|
+
records from a database. All other content, such as HTML, CSS, and images, are
|
18
|
+
hard linked to the destination directory, ready to be served or copied to a
|
19
|
+
remove production server.
|
20
|
+
|
21
|
+
== Examples
|
22
|
+
|
23
|
+
Here's an example command sequence to get a project up and running.
|
24
|
+
|
25
|
+
$ mkdir myproject
|
26
|
+
$ cd myproject
|
27
|
+
|
28
|
+
$ xstatic init # set up initial project files
|
29
|
+
$ find . -type f
|
30
|
+
./content/index.md # markdown content & metadata
|
31
|
+
./content/default.css # asset to be hard linked under ./site
|
32
|
+
./templates/layouts/default.erb # layout template used to render index
|
33
|
+
|
34
|
+
$ xstatic site # build website under ./site
|
35
|
+
updated ./site/index # markdown & metadata converted to HTML
|
36
|
+
$ find ./site
|
37
|
+
./site # complete static site ready to be served
|
38
|
+
./site/index # generated HTML file
|
39
|
+
./site/default.css # hard linked asset
|
40
|
+
|
41
|
+
$ xstatic start # start WEBrick server on port 2000
|
42
|
+
$ xstatic stop # stop WEBrick server
|
43
|
+
|
44
|
+
The above sequence can be simplified to a single command:
|
45
|
+
|
46
|
+
$ xstatic init site start # and you're ready to go!
|
47
|
+
|
48
|
+
== Site Development Process
|
49
|
+
|
50
|
+
Just follow this simple 5-step process as you build your site:
|
51
|
+
|
52
|
+
1. Add assets and content to the *content* directory.
|
53
|
+
2. Factor out code and data (HTML, Erb, markdown, plain text, etc.) to the
|
54
|
+
*templates* directory.
|
55
|
+
3. Create custom layouts in the *templates/layouts* directory.
|
56
|
+
4. Update the site by running +xstatic+.
|
57
|
+
5. Reload *localhost:2000* in your browser to see your changes.
|
58
|
+
|
59
|
+
|
60
|
+
== Links
|
61
|
+
|
62
|
+
Homepage :: https://ecentryx.com/gems/xstatic
|
63
|
+
Ruby Gem :: https://rubygems.org/gems/xstatic
|
64
|
+
|
65
|
+
|
66
|
+
== History
|
67
|
+
|
68
|
+
1. 2022-02-24, v0.1.0
|
69
|
+
* First public release.
|
70
|
+
|
71
|
+
|
72
|
+
== License
|
73
|
+
|
74
|
+
({ISC License}[https://opensource.org/licenses/ISC])
|
75
|
+
|
76
|
+
Copyright (c) 2022, Clint Pachl <pachl@ecentryx.com>
|
77
|
+
|
78
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose
|
79
|
+
with or without fee is hereby granted, provided that the above copyright notice
|
80
|
+
and this permission notice appear in all copies.
|
81
|
+
|
82
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
83
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
84
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
85
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
86
|
+
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
87
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
88
|
+
THIS SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
gemspec = eval File.read(Dir['*.gemspec'].first)
|
2
|
+
|
3
|
+
require 'rubygems/package_task'
|
4
|
+
Gem::PackageTask.new(gemspec) {}
|
5
|
+
|
6
|
+
require 'rdoc/task'
|
7
|
+
RDoc::Task.new(
|
8
|
+
rdoc: 'doc',
|
9
|
+
rerdoc: 'doc:force',
|
10
|
+
clobber_rdoc: 'doc:clean'
|
11
|
+
) do |doc|
|
12
|
+
doc.rdoc_dir = 'doc'
|
13
|
+
doc.rdoc_files = gemspec.extra_rdoc_files
|
14
|
+
doc.options = gemspec.rdoc_options
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Publish RDoc HTML files'
|
18
|
+
task 'doc:pub' => 'doc' do
|
19
|
+
dir = ENV['public_gem_dir'] + gemspec.name
|
20
|
+
host = ENV['public_gem_host']
|
21
|
+
`cd ./doc && pax -w . | ssh #{host} 'cd #{dir} && rm -rf * && pax -r'`
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
Rake::TestTask.new
|
data/TODO
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# XStatic To-Do #
|
2
|
+
|
3
|
+
* Dependencies between templates is not implemented; only pages-to-templates
|
4
|
+
and layouts-to-templates dependencies are implemented. Layouts are just
|
5
|
+
templates that reside in the "layouts" directory under the main templates
|
6
|
+
directory.
|
7
|
+
|
8
|
+
For example, if a layout (a template) includes Javascript code (via a
|
9
|
+
template), and the Javascript is modified, none of the pages that depend on
|
10
|
+
that layout will be updated with the new Javascript code.
|
11
|
+
|
12
|
+
I actually implemented this on 6/1/21 using a FileList block just for
|
13
|
+
layouts only, not other non-layout templates. Could just as easily add it
|
14
|
+
for all types of templates, but will just test this first. Plus non-layout
|
15
|
+
to non-layout template dependencies are probably rare. There was no
|
16
|
+
measurable difference when adding 3 layouts; could probably just FileList
|
17
|
+
the entire TEMPLATES dir without a slow down.
|
18
|
+
|
19
|
+
* Add tests.
|
20
|
+
|
21
|
+
* Add configuration file.
|
22
|
+
|
23
|
+
|
24
|
+
## HTMLPage Class To-Do
|
25
|
+
|
26
|
+
* Because templates and layouts may be used by multiple pages, *cache* them
|
27
|
+
within thread like Mote. No need to cache the page sources because they are
|
28
|
+
not re-evaluated. Do no prematurely optimize.
|
29
|
+
|
30
|
+
* It may be useful to pass local variables into a template. For example, to set
|
31
|
+
the title of a template:
|
32
|
+
|
33
|
+
template "comment_form.html", title: "Your Feedback"
|
34
|
+
|
35
|
+
This change would require a params to #transform, like Mote.
|
36
|
+
|
37
|
+
* Add method to allow adding `<link>` tags to `<head>` (via @page?).
|
38
|
+
For example:
|
39
|
+
|
40
|
+
link_tag href: 'http://example', rel: 'stylesheet'
|
41
|
+
page.link 'http://example', rel: 'stylesheet'
|
42
|
+
link 'http://example', 'stylesheet'
|
43
|
+
|
44
|
+
See:
|
45
|
+
|
46
|
+
- https://html.spec.whatwg.org/multipage/links.html#linkTypes
|
47
|
+
- https://ogp.me/
|
48
|
+
|
data/bin/xstatic
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# XStatic is a rake-driven application.
|
4
|
+
#
|
5
|
+
# - Dependency chains are managed by rake.
|
6
|
+
# - HTML generation and Erb processing is managed by the class HTMLPage.
|
7
|
+
# - Markdown processing is manged by kramdown.
|
8
|
+
# - WEBrick is the HTTP server.
|
9
|
+
|
10
|
+
|
11
|
+
### Configuration ###
|
12
|
+
|
13
|
+
APP = File.basename($0)
|
14
|
+
ADDR = ENV['host'] || 'localhost' # web server hostname or IP address
|
15
|
+
PORT = ENV['port'] || 2000 # web server port
|
16
|
+
DST = "./site" # destination of website build: assets+pages
|
17
|
+
SRC = "./content" # sources used to generate html pages
|
18
|
+
ASSETS = "./content" # static files; e.g. scripts, styles, images
|
19
|
+
TEMPLATES = "./templates" # inline templates for pages and layouts
|
20
|
+
LAYOUTS = "./templates/layouts" # special templates used to render pages
|
21
|
+
LOG = "./LOG" # web server stdout/stderr; see :start task
|
22
|
+
Pages = [] # html page targets to be built
|
23
|
+
|
24
|
+
# default project files; see task :init
|
25
|
+
INDEX_FILE = SRC + '/index.md'
|
26
|
+
LAYOUT_FILE = LAYOUTS + '/default.erb'
|
27
|
+
STYLE_FILE = ASSETS + '/default.css'
|
28
|
+
|
29
|
+
INLINE_TEMPLATE_RE = /(?:^|<)%= ?template ['"](.*?)["']/
|
30
|
+
ACCESS_LOG_FORMAT = "[%{%F %T}t] %s %a %m %U" # see webrick/accesslog.rb
|
31
|
+
|
32
|
+
require 'html_page'
|
33
|
+
require 'rake'
|
34
|
+
Rake.application.init(APP)
|
35
|
+
TASKS = Rake.application.top_level_tasks.to_s
|
36
|
+
verbose false # silence FileUtils
|
37
|
+
|
38
|
+
|
39
|
+
### Page & Layout Targets ###
|
40
|
+
|
41
|
+
def build_html_target(source, layout_name, templates = [])
|
42
|
+
prerequisites = [source] # erb/md source for html build target
|
43
|
+
|
44
|
+
if layout_name
|
45
|
+
layout = File.join(LAYOUTS, layout_name + '.erb')
|
46
|
+
unless File.exist? layout
|
47
|
+
warn "No such file `#{layout}'.\nCheck `Layout' in #{source}."
|
48
|
+
exit 2
|
49
|
+
end
|
50
|
+
prerequisites << layout
|
51
|
+
end
|
52
|
+
|
53
|
+
templates.each {|t| prerequisites << File.join(TEMPLATES, t) }
|
54
|
+
target = source.pathmap("%{^#{SRC},#{DST}}X") # make html files extensionless
|
55
|
+
|
56
|
+
#puts "#{target}: #{templates << layout}" if DEBUG # for debugging
|
57
|
+
|
58
|
+
file(target => prerequisites) do
|
59
|
+
begin
|
60
|
+
content = HTMLPage.new(source, layout).render
|
61
|
+
rescue => e
|
62
|
+
warn "#{e.class} in source file `#{source}'."
|
63
|
+
warn e.message
|
64
|
+
raise #if DEBUG
|
65
|
+
exit 3
|
66
|
+
end
|
67
|
+
|
68
|
+
if File.write(target, content) < 1
|
69
|
+
warn "cannot write #{target}"
|
70
|
+
exit 2
|
71
|
+
else
|
72
|
+
puts "updated #{target}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
return target
|
77
|
+
end
|
78
|
+
|
79
|
+
FileList["#{SRC}/**/*.{erb,md}"].each do |source|
|
80
|
+
f = File.new(source)
|
81
|
+
layout = 'default'
|
82
|
+
|
83
|
+
10.times do # only read preamble lines to retrieve layout
|
84
|
+
line = f.gets
|
85
|
+
break unless line
|
86
|
+
name, sep, value = line.partition(':')
|
87
|
+
|
88
|
+
if sep == ':' && name.downcase == 'layout'
|
89
|
+
layout = value.strip
|
90
|
+
layout = nil if layout.empty?
|
91
|
+
break
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
f.rewind
|
96
|
+
templates = f.read.scan(INLINE_TEMPLATE_RE).flatten
|
97
|
+
f.close
|
98
|
+
|
99
|
+
Pages << build_html_target(source, layout, templates)
|
100
|
+
end
|
101
|
+
|
102
|
+
FileList["#{LAYOUTS}/**/*.erb"].each do |layout|
|
103
|
+
templates = File.read(layout).scan(INLINE_TEMPLATE_RE).flatten
|
104
|
+
if templates.size > 0
|
105
|
+
prerequisites = templates.map {|t| File.join(TEMPLATES, t)}
|
106
|
+
file(layout => prerequisites)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
### Initialization Tasks ###
|
112
|
+
|
113
|
+
desc "create project"
|
114
|
+
task :init => [INDEX_FILE, STYLE_FILE, LAYOUT_FILE] do
|
115
|
+
# Must manually build HTML target to support multi-target `init site`.
|
116
|
+
if TASKS =~ /"(pages|site)"/
|
117
|
+
index = build_html_target(INDEX_FILE, 'default')
|
118
|
+
Rake::Task[DST].execute
|
119
|
+
Rake::Task[index].execute
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
directory DST
|
124
|
+
directory SRC
|
125
|
+
directory ASSETS
|
126
|
+
directory TEMPLATES
|
127
|
+
directory LAYOUTS
|
128
|
+
|
129
|
+
file(INDEX_FILE => SRC) {|t| File.write(t.name, <<EOF) }
|
130
|
+
Title: XStatic Project
|
131
|
+
Keywords: static website generator, html site builder, markdown templates
|
132
|
+
Description: XStatic is a static website generator.
|
133
|
+
|
134
|
+
# XStatic—Static Site Generator
|
135
|
+
|
136
|
+
Web pages are written in markdown (.md) or Ruby-flavored HTML (.erb).
|
137
|
+
Learn more about the syntax of these two markup languages:
|
138
|
+
|
139
|
+
* [Markdown syntax](https://www.markdownguide.org/basic-syntax/)
|
140
|
+
* [ERB templates](https://docs.ruby-lang.org/en/master/ERB.html)
|
141
|
+
EOF
|
142
|
+
|
143
|
+
file(LAYOUT_FILE => LAYOUTS) {|t| File.write(t.name, <<EOF) }
|
144
|
+
<!DOCTYPE html>
|
145
|
+
<html lang=en-US>
|
146
|
+
<head>
|
147
|
+
<meta charset=utf-8>
|
148
|
+
<!-- STYLES, FONTS, ICONS -->
|
149
|
+
<link rel=preload href=/MY-FONT.woff2 as=font type=font/woff2 crossorigin>
|
150
|
+
<link rel=preload href=#{STYLE_FILE.delete_prefix ASSETS} as=style>
|
151
|
+
<link rel=stylesheet media=screen href=#{STYLE_FILE.delete_prefix ASSETS}>
|
152
|
+
<link rel=icon sizes=any href=/MY-ICON.svg type="image/svg+xml">
|
153
|
+
<link rel=apple-touch-icon href=/MY-ICON.png> <!-- 180x180 px PNG -->
|
154
|
+
<!-- MOBILE META -->
|
155
|
+
<meta name=viewport content="width=device-width,initial-scale=1">
|
156
|
+
<meta name=theme-color content=black>
|
157
|
+
<meta name=application-name content="MY-APP NAME ON ANDROID">
|
158
|
+
<meta name=apple-mobile-web-app-title content="MY-APP NAME ON APPLE">
|
159
|
+
<!-- DOCUMENT META -->
|
160
|
+
<meta name=author content="<%= page.author || 'MY NAME' %>">
|
161
|
+
<meta name=keywords content="<%= page.keywords %>">
|
162
|
+
<meta name=description content="<%= page.description %>">
|
163
|
+
<title><%= page.title %></title>
|
164
|
+
</head>
|
165
|
+
<body>
|
166
|
+
<header>
|
167
|
+
<nav>
|
168
|
+
<ul>
|
169
|
+
<li><a href="/">Home</a></li>
|
170
|
+
<li><a href="/link1">Link 1</a></li>
|
171
|
+
<li><a href="/link2">Link 2</a></li>
|
172
|
+
</ul>
|
173
|
+
</nav>
|
174
|
+
</header>
|
175
|
+
<main>
|
176
|
+
<%= page.content %>
|
177
|
+
</main>
|
178
|
+
<footer>
|
179
|
+
<nav>
|
180
|
+
<ul>
|
181
|
+
<li><a href="/link3">Link 3</a></li>
|
182
|
+
<li><a href="/link4">Link 4</a></li>
|
183
|
+
</ul>
|
184
|
+
</nav>
|
185
|
+
<div id=copyright>© <%= Time.now.year %> Your Name</div>
|
186
|
+
</footer>
|
187
|
+
</body>
|
188
|
+
</html>
|
189
|
+
EOF
|
190
|
+
|
191
|
+
file(STYLE_FILE => ASSETS) {|t| File.write(t.name, <<EOF) }
|
192
|
+
body {
|
193
|
+
margin: 0;
|
194
|
+
padding: 0;
|
195
|
+
}
|
196
|
+
main {
|
197
|
+
margin: 2em auto;
|
198
|
+
max-width: 70ch;
|
199
|
+
}
|
200
|
+
nav ul {
|
201
|
+
margin: 1em auto;
|
202
|
+
max-width: 70ch;
|
203
|
+
padding: 0;
|
204
|
+
}
|
205
|
+
nav li {
|
206
|
+
display: inline;
|
207
|
+
padding: 1em;
|
208
|
+
}
|
209
|
+
body > header {
|
210
|
+
border-bottom: solid 1px silver;
|
211
|
+
text-align: right;
|
212
|
+
}
|
213
|
+
body > footer {
|
214
|
+
border-top: solid 1px silver;
|
215
|
+
text-align: center;
|
216
|
+
}
|
217
|
+
#copyright {
|
218
|
+
color: gray;
|
219
|
+
font-style: italic;
|
220
|
+
}
|
221
|
+
EOF
|
222
|
+
|
223
|
+
|
224
|
+
### Content Management Tasks ###
|
225
|
+
|
226
|
+
desc "build website [assets + pages] (default)"
|
227
|
+
task :site => [:assets, :pages]
|
228
|
+
task :default => :site
|
229
|
+
|
230
|
+
desc "update assets"
|
231
|
+
task :assets => [:is_initialized, ASSETS, DST] do
|
232
|
+
ignore = '-name ".*.sw?"' # vim swap
|
233
|
+
if ASSETS == SRC # ignore source files when under same directory
|
234
|
+
ignore << ' -o -name "*.md" -o -name "*.erb"'
|
235
|
+
end
|
236
|
+
%x[cd #{ASSETS} && find . ! \\( #{ignore} \\) | cpio -pl ../#{DST}]
|
237
|
+
end
|
238
|
+
|
239
|
+
desc "update pages"
|
240
|
+
task :pages => [:is_initialized, :page_dirs, *Pages]
|
241
|
+
|
242
|
+
# make all page directories
|
243
|
+
task :page_dirs => [SRC, DST] do
|
244
|
+
%x[(cd #{SRC} && find . -type d) | (cd #{DST} && xargs mkdir -p)]
|
245
|
+
end
|
246
|
+
|
247
|
+
desc "remove website [#{DST}]"
|
248
|
+
task :clean => [:is_initialized] do
|
249
|
+
rm_rf DST
|
250
|
+
end
|
251
|
+
|
252
|
+
task :is_initialized do
|
253
|
+
[ASSETS, SRC, TEMPLATES, LAYOUTS].each do |dir|
|
254
|
+
unless Dir.exist? dir
|
255
|
+
puts "Try `#{APP} init` or `#{APP} --tasks`"
|
256
|
+
exit 1
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
### Web Server Tasks ###
|
263
|
+
|
264
|
+
desc "start web server [#{ADDR}:#{PORT}]"
|
265
|
+
task :start do
|
266
|
+
if File.exist? LOG
|
267
|
+
pid, port = File.read(LOG).scan(/pid=([0-9]+) port=([0-9]+)/)[0]
|
268
|
+
|
269
|
+
if pid
|
270
|
+
begin
|
271
|
+
Process.kill(0, pid.to_i) # check if running; see kill(1)
|
272
|
+
puts "Web server running with pid #{pid} on port #{port}."
|
273
|
+
exit 0
|
274
|
+
rescue Errno::ESRCH
|
275
|
+
puts "Web server shutdown improperly; restarting on port #{PORT}."
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
require 'webrick'
|
281
|
+
|
282
|
+
module WEBrick::HTTPUtils
|
283
|
+
# Set default content type to html for extentionless files.
|
284
|
+
def self.mime_type(filename, mime_tab)
|
285
|
+
ext = File.extname(filename)[1..]&.downcase
|
286
|
+
mime_tab[ext || 'html']
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
log_file = File.open(File.absolute_path(LOG), 'w')
|
291
|
+
log_file.sync = true
|
292
|
+
|
293
|
+
server = WEBrick::HTTPServer.new(
|
294
|
+
:AccessLog => [[log_file, ACCESS_LOG_FORMAT]],
|
295
|
+
:Logger => WEBrick::Log.new(log_file),
|
296
|
+
:DirectoryIndex => %w(index index.html),
|
297
|
+
:DocumentRoot => File.absolute_path(DST),
|
298
|
+
:BindAddress => ADDR,
|
299
|
+
:Port => PORT
|
300
|
+
)
|
301
|
+
begin
|
302
|
+
WEBrick::Daemon.start
|
303
|
+
server.start
|
304
|
+
ensure
|
305
|
+
server.shutdown
|
306
|
+
log_file.close
|
307
|
+
File.unlink(log_file.path)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
desc "stop web server"
|
312
|
+
task :stop do
|
313
|
+
if File.exists? LOG
|
314
|
+
pid = File.read(LOG)[/pid=([0-9]+)/, 1]
|
315
|
+
Process.kill('TERM', pid.to_i) if pid
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
# Run specified rake tasks.
|
321
|
+
Rake.application.top_level
|
data/lib/html_page.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'kramdown' # markdown converter
|
2
|
+
|
3
|
+
class HTMLPage
|
4
|
+
|
5
|
+
# user-defined attributes
|
6
|
+
Attributes = %i(author description keywords layout robots title)
|
7
|
+
|
8
|
+
# page attributes are available to templates
|
9
|
+
PageAttributes = Struct.new(*Attributes, :content)
|
10
|
+
attr_reader :page
|
11
|
+
|
12
|
+
TEMPLATES_DIR = './templates/'
|
13
|
+
|
14
|
+
#
|
15
|
+
# Compile an HTML page from the +content_filename+ and +layout_filename+
|
16
|
+
# sources.
|
17
|
+
#
|
18
|
+
# Content files must be markdown or embedded ruby and have an "md" or "erb"
|
19
|
+
# extension. If markdown, content is first evaluated as Erb. Layouts files
|
20
|
+
# must be "erb". The content renders within the layout template if specified.
|
21
|
+
#
|
22
|
+
# Attributes specified in the preamble of the content file will be set in
|
23
|
+
# +page+ and are available to layout and embedded templates.
|
24
|
+
#
|
25
|
+
def initialize(content_filename, layout_filename = nil)
|
26
|
+
@content_filename = content_filename
|
27
|
+
@layout_filename = layout_filename&.delete_prefix(TEMPLATES_DIR)
|
28
|
+
@page = PageAttributes.new
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Returns the rendered HTML page.
|
33
|
+
#
|
34
|
+
def render
|
35
|
+
f = File.new(@content_filename)
|
36
|
+
|
37
|
+
# parse attributes and content
|
38
|
+
while (line = f.gets)
|
39
|
+
case line
|
40
|
+
when /^(\w+):(.*)/ # begin attribute
|
41
|
+
begin
|
42
|
+
attribute = page[$1.downcase] = $2.strip
|
43
|
+
rescue NameError => e
|
44
|
+
raise e.class, "Bad page attribute `#{$1}'.\n"\
|
45
|
+
"Valid attributes: #{Attributes.to_s.tr('[:]','')}"
|
46
|
+
end
|
47
|
+
when /^\s*$/ # end attribute (blank lines between attrs)
|
48
|
+
attribute = nil
|
49
|
+
else
|
50
|
+
unless attribute # remainder is content
|
51
|
+
content = line
|
52
|
+
content << f.gets(nil) unless f.eof?
|
53
|
+
break
|
54
|
+
end
|
55
|
+
attribute << (attribute.empty? ? '':' ') << line.strip
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
f.close
|
60
|
+
content = instance_eval(transform(content)).call # process with erb
|
61
|
+
|
62
|
+
if @content_filename.end_with? '.md'
|
63
|
+
if page.title.nil? # then use H1
|
64
|
+
page.title = content[/^#\s+([^#\{\r\n]+)/, 1]&.rstrip
|
65
|
+
end
|
66
|
+
opts = { auto_ids: false }
|
67
|
+
content = Kramdown::Document.new(content, opts).to_html
|
68
|
+
end
|
69
|
+
|
70
|
+
if @layout_filename
|
71
|
+
page.content = content
|
72
|
+
template(@layout_filename) # page.content embedded in layout
|
73
|
+
else
|
74
|
+
content # without a layout
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
ERB_EVAL_OPERATORS = /
|
82
|
+
^(%)\s+(.*?)(?:\n|\Z) | # % single line of code
|
83
|
+
^(%=)(.*?)(?:\n|\Z) | # %= single line of code output as string
|
84
|
+
(<%)\s+(.*?)\s+%> | # <% inline code or code block %>
|
85
|
+
(<%=)(.*?)%> # <%= inline code or code block output as string %>
|
86
|
+
/mx
|
87
|
+
|
88
|
+
|
89
|
+
#
|
90
|
+
# Parse and translate the template string into ruby source code.
|
91
|
+
#
|
92
|
+
def transform(template)
|
93
|
+
atoms = template.split(ERB_EVAL_OPERATORS)
|
94
|
+
source = "Proc.new do __o='';"
|
95
|
+
|
96
|
+
while (atom = atoms.shift)
|
97
|
+
case atom
|
98
|
+
when "%" ,"<%" then source << "#{atoms.shift}\n"
|
99
|
+
when "%=","<%=" then source << "__o << (#{atoms.shift}).to_s\n"
|
100
|
+
else source << "__o << #{atom.dump}\n"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
source << "__o; end"
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Outputs the contents of the template +filename+. Erb files are evaluated
|
109
|
+
# in the current context. All other file types are returned verbatim.
|
110
|
+
# Can be called from content or templates.
|
111
|
+
#
|
112
|
+
# Note: layouts are templates with the following exceptions:
|
113
|
+
# 1. Layouts must be Erb files, unlike other templates which could be HTML.
|
114
|
+
# 2. Layouts must be in the "layouts" directory under +TEMPLATES_DIR+.
|
115
|
+
# 3. Layouts are a final destination; no other templates embed them.
|
116
|
+
# 4. Layouts normally call +page.content+ to get the rendered page.
|
117
|
+
#
|
118
|
+
def template(filename)
|
119
|
+
template_source = File.read(File.join(TEMPLATES_DIR, filename))
|
120
|
+
|
121
|
+
if filename.end_with? '.erb'
|
122
|
+
instance_eval(transform(template_source)).call
|
123
|
+
else
|
124
|
+
template_source
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Sets an ID cookie using pixel.
|
130
|
+
#
|
131
|
+
def memo
|
132
|
+
"<img src='/memo-#{rand(1E6)}' alt='' loading='eager'>"
|
133
|
+
end
|
134
|
+
|
135
|
+
#
|
136
|
+
# Kramdown table of contents marker.
|
137
|
+
#
|
138
|
+
def toc(auto_ids = true, levels = 2..6)
|
139
|
+
"0. TOC\n{:toc .toc}\n" \
|
140
|
+
"{::options auto_ids='#{auto_ids}' toc_levels='#{levels}' /}"
|
141
|
+
end
|
142
|
+
|
143
|
+
#
|
144
|
+
# Obfuscates +email+ address in a mailto hyperlink using Javascript. The
|
145
|
+
# optional display +text+ defaults to the encoded email address.
|
146
|
+
#
|
147
|
+
def mailto email, text = nil
|
148
|
+
encoded_proto = "0x6d,97,0151,0x6c,116,0157,072" #=> mailto: (hex/dec/oct)
|
149
|
+
encoded_email = email.codepoints.join(',')
|
150
|
+
|
151
|
+
link = "'+String.fromCodePoint(#{encoded_proto},#{encoded_email})+'"
|
152
|
+
text = "'+String.fromCodePoint(#{encoded_email})+'" unless text
|
153
|
+
|
154
|
+
%Q(<script>document.write('<a href="#{link}">#{text}</a>')</script>)
|
155
|
+
end
|
156
|
+
|
157
|
+
#
|
158
|
+
# Manages figure enumeration and references.
|
159
|
+
#
|
160
|
+
# For example, reference a defined figure:
|
161
|
+
#
|
162
|
+
# <p>This is demonstrated in <%= figure.ref :my_fig_name %>.
|
163
|
+
# ...
|
164
|
+
# <figure>
|
165
|
+
# <img>
|
166
|
+
# <figcaption><%= figure.name :my_fig_name %></figcaption>
|
167
|
+
# </figure>
|
168
|
+
#
|
169
|
+
class Figure
|
170
|
+
def initialize
|
171
|
+
@figures = Hash.new {|hsh, name| hsh[name] = hsh.size + 1 }
|
172
|
+
@names = [] # permits name/ref to be called in any order
|
173
|
+
end
|
174
|
+
|
175
|
+
def name figname
|
176
|
+
if @names.include? figname
|
177
|
+
raise "Figure name `#{figname}' already defined."
|
178
|
+
else
|
179
|
+
@names << figname
|
180
|
+
end
|
181
|
+
"<span class='figname'>Figure #{@figures[figname]}.</span>"
|
182
|
+
end
|
183
|
+
|
184
|
+
def ref figname
|
185
|
+
"<span class='figref'>Figure #{@figures[figname]}</span>"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def figure
|
190
|
+
@figure ||= Figure.new
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
data/xstatic.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
Gem::Specification.new('xstatic') do |s|
|
2
|
+
s.version = '0.1.0'
|
3
|
+
s.author = 'Clint Pachl'
|
4
|
+
s.email = 'pachl@ecentryx.com'
|
5
|
+
s.homepage = 'https://ecentryx.com/gems/xstatic'
|
6
|
+
s.license = 'ISC'
|
7
|
+
s.summary = 'Fast, dependency-aware, static site generator'
|
8
|
+
s.description = <<EOS
|
9
|
+
A smart, static site generator that automatically manages dependencies to
|
10
|
+
achieve blazing build times with minimal cognitive load. Only new and changed
|
11
|
+
files, and files upstream of a changed dependency are processed. Renders
|
12
|
+
markdown or embedded-Ruby (Erb-like) content as HTML.
|
13
|
+
|
14
|
+
Supports templates (embedded & layout), which may be included within content
|
15
|
+
sources or other templates. Document metadata may me added using a plain-text
|
16
|
+
preamble of key-value pairs. Generates a complete website that can be served by
|
17
|
+
the built-in WEBrick server.
|
18
|
+
EOS
|
19
|
+
s.executables << 'xstatic'
|
20
|
+
s.add_runtime_dependency 'kramdown', '~> 2.3', '>= 2.3.0'
|
21
|
+
s.rdoc_options = [
|
22
|
+
'--title', "XStatic Ruby Gem: #{s.summary}",
|
23
|
+
'--main', 'README.rdoc'
|
24
|
+
]
|
25
|
+
s.extra_rdoc_files = Dir[
|
26
|
+
'DESIGN*',
|
27
|
+
'README*',
|
28
|
+
'TODO*',
|
29
|
+
'lib/**/*.rb',
|
30
|
+
]
|
31
|
+
s.files = Dir[
|
32
|
+
'*.gemspec',
|
33
|
+
'Rakefile',
|
34
|
+
'bin/*',
|
35
|
+
'test/**/*'
|
36
|
+
] + s.extra_rdoc_files
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xstatic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Clint Pachl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-01-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kramdown
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.3'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.3.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.3'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.3.0
|
33
|
+
description: |
|
34
|
+
A smart, static site generator that automatically manages dependencies to
|
35
|
+
achieve blazing build times with minimal cognitive load. Only new and changed
|
36
|
+
files, and files upstream of a changed dependency are processed. Renders
|
37
|
+
markdown or embedded-Ruby (Erb-like) content as HTML.
|
38
|
+
|
39
|
+
Supports templates (embedded & layout), which may be included within content
|
40
|
+
sources or other templates. Document metadata may me added using a plain-text
|
41
|
+
preamble of key-value pairs. Generates a complete website that can be served by
|
42
|
+
the built-in WEBrick server.
|
43
|
+
email: pachl@ecentryx.com
|
44
|
+
executables:
|
45
|
+
- xstatic
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files:
|
48
|
+
- DESIGN.rdoc
|
49
|
+
- README.rdoc
|
50
|
+
- TODO
|
51
|
+
- lib/html_page.rb
|
52
|
+
files:
|
53
|
+
- DESIGN.rdoc
|
54
|
+
- README.rdoc
|
55
|
+
- Rakefile
|
56
|
+
- TODO
|
57
|
+
- bin/xstatic
|
58
|
+
- lib/html_page.rb
|
59
|
+
- xstatic.gemspec
|
60
|
+
homepage: https://ecentryx.com/gems/xstatic
|
61
|
+
licenses:
|
62
|
+
- ISC
|
63
|
+
metadata: {}
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options:
|
66
|
+
- "--title"
|
67
|
+
- 'XStatic Ruby Gem: Fast, dependency-aware, static site generator'
|
68
|
+
- "--main"
|
69
|
+
- README.rdoc
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.2.32
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Fast, dependency-aware, static site generator
|
87
|
+
test_files: []
|