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.
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: []