xstatic 0.1.0
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 +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: []
|