xstatic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. checksums.yaml +7 -0
  2. data/DESIGN.rdoc +173 -0
  3. data/README.rdoc +88 -0
  4. data/Rakefile +25 -0
  5. data/TODO +48 -0
  6. data/bin/xstatic +321 -0
  7. data/lib/html_page.rb +193 -0
  8. data/xstatic.gemspec +37 -0
  9. 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: []