purity 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aa91bae52c808d7124074905c5ab0b1f69c1d46e8e7390f5e1bf0392d66c5112
4
+ data.tar.gz: 2a8794641f2a09e56f80a1d878b6f72fb21e99cc3f7fd397b958f0ae0c472bd2
5
+ SHA512:
6
+ metadata.gz: 6824c06ce5a20b9209a43ad4d6cb23593f0b015117d55761205dd839da144740cee3ecac321ebbe2fb0b25ba12187d9c7928efb83cf70eb9663410c2b6a0c86e
7
+ data.tar.gz: c7569a15cdb6e316e78b310199a362022510c2cffab07ac21d4e51d810af07d82256d9941a55587c880c9a0f0b8fbfe98ad33d47ea8b8ba53d5dab57b435d973
data/exe/purity ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "purity"
5
+
6
+ Purity::CLI.run
data/lib/purity/cli.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+
5
+ class CLI
6
+ class << self
7
+ def run(args = ARGV.dup)
8
+ include_drafts = !!args.delete("--drafts")
9
+
10
+ case args[0]
11
+ when "new", "n"
12
+ new_site(args[1])
13
+ when "serve", "s"
14
+ ENV["PURITY_ENV"] ||= "development"
15
+ port = args[1] ? args[1].to_i : 4567
16
+ Site.new.serve(port: port)
17
+ when "watch", "w"
18
+ ENV["PURITY_ENV"] ||= "development"
19
+ port = args[1] ? args[1].to_i : 4567
20
+ Site.new.watch(port: port)
21
+ when "help", "h", "-h", "--help"
22
+ help
23
+ when "version", "-v", "--version"
24
+ puts("purity #{VERSION}")
25
+ else
26
+ ENV["PURITY_ENV"] ||= "production"
27
+ Site.new.build(include_drafts: include_drafts)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def new_site(dir)
34
+ unless dir
35
+ puts("usage: purity new <directory>")
36
+ return
37
+ end
38
+ Scaffold.new(dir).run
39
+ end
40
+
41
+ def help
42
+ puts("purity #{VERSION}")
43
+ puts("")
44
+ puts("usage: purity [command] [options]")
45
+ puts("")
46
+ puts("commands:")
47
+ puts(" new <dir> create a new site")
48
+ puts(" (none) build the site")
49
+ puts(" serve, s build and serve on localhost")
50
+ puts(" watch, w build, serve, and rebuild on changes")
51
+ puts(" help, h this message")
52
+ puts(" version show version")
53
+ puts("")
54
+ puts("options:")
55
+ puts(" --drafts include pages with draft: true")
56
+ puts(" [port] port for serve/watch (default: 4567)")
57
+ puts("")
58
+ puts("template variables:")
59
+ puts(" site.title from _site.yml")
60
+ puts(" page.title from front matter")
61
+ puts(" page.url clean URL path of the current page")
62
+ puts(" page.content rendered page body (in layouts)")
63
+ puts(" data.nav from _data/nav.yml")
64
+ puts(" data.posts configured collection")
65
+ puts(" data.pages all non-standalone, non-draft pages")
66
+ puts("")
67
+ puts("front matter:")
68
+ puts(" layout: name use _name.html (default: _layout.html)")
69
+ puts(" layout: false copy file as-is, no layout")
70
+ puts(" draft: true skip unless --drafts")
71
+ puts(" date: YYYY-MM-DD date for feed generation")
72
+ puts(" permalink: /p/ custom output path")
73
+ puts("")
74
+ puts("config (_site.yml):")
75
+ puts(" url: https://... enables sitemap + feed generation")
76
+ puts(" site_name: My Site used in feed title")
77
+ puts(" clean_urls: true rewrite to clean URLs (default)")
78
+ puts(" strict_variables: true raise on undefined variables")
79
+ puts(" collections: directory-based collections")
80
+ puts(" posts:")
81
+ puts(" sort_by: date")
82
+ puts(" order: desc")
83
+ puts(" environments: per-environment overrides")
84
+ puts(" production:")
85
+ puts(" strict_variables: false")
86
+ puts("")
87
+ puts("plugins (src/_plugins/*.rb):")
88
+ puts(" hook :after_parse { |config, pages| }")
89
+ puts(" hook :before_render { |ctx, rel| }")
90
+ puts(" hook :before_layout { |content, meta, ctx| content }")
91
+ puts(" hook :after_render { |html, ctx, rel| html }")
92
+ puts(" hook :after_build { |config, dest| }")
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+ class UndefinedVariableError < StandardError; end
5
+
6
+ class Context
7
+ def initialize(data, strict: false)
8
+ @data = data || {}
9
+ @strict = strict
10
+ end
11
+
12
+ def [](key)
13
+ wrap(@data[key.to_s])
14
+ end
15
+
16
+ def []=(key, value)
17
+ @data[key.to_s] = value
18
+ end
19
+
20
+ def key?(key)
21
+ @data.key?(key.to_s)
22
+ end
23
+
24
+ def merge(other)
25
+ self.class.new(@data.merge(other.transform_keys(&:to_s)), strict: @strict)
26
+ end
27
+
28
+ def method_missing(name, *args)
29
+ key = name.to_s
30
+ if @data.key?(key)
31
+ wrap(@data[key])
32
+ elsif @strict
33
+ raise UndefinedVariableError, "undefined variable: #{key}"
34
+ else
35
+ nil
36
+ end
37
+ end
38
+
39
+ def respond_to_missing?(name, include_private = false)
40
+ true
41
+ end
42
+
43
+ def to_s
44
+ ""
45
+ end
46
+
47
+ private
48
+
49
+ def wrap(val)
50
+ case val
51
+ when Hash then self.class.new(val, strict: @strict)
52
+ when Array then val.map { |v| v.is_a?(Hash) ? self.class.new(v, strict: @strict) : v }
53
+ else val
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+ class DataStore
5
+ def initialize(dir, strict: false)
6
+ @dir = dir
7
+ @cache = {}
8
+ @strict = strict
9
+ end
10
+
11
+ def [](key)
12
+ raw = raw_get(key.to_s)
13
+ wrap(raw)
14
+ end
15
+
16
+ def []=(key, value)
17
+ @cache[key.to_s] = value
18
+ end
19
+
20
+ def key?(key)
21
+ @cache.key?(key.to_s) || !!find_file(key.to_s)
22
+ end
23
+
24
+ def empty?
25
+ !Dir.exist?(@dir) || Dir.glob(File.join(@dir, "**/*.{yml,yaml,json}")).empty?
26
+ end
27
+
28
+ def to_s
29
+ ""
30
+ end
31
+
32
+ def method_missing(name, *args)
33
+ key = name.to_s
34
+ if @cache.key?(key) || find_file(key)
35
+ self[key]
36
+ elsif @strict
37
+ raise UndefinedVariableError, "undefined data key: #{key}"
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ def respond_to_missing?(name, include_private = false)
44
+ true
45
+ end
46
+
47
+ private
48
+
49
+ def raw_get(key)
50
+ return @cache[key] if @cache.key?(key)
51
+ path = find_file(key)
52
+ return nil unless path
53
+ @cache[key] = parse(path)
54
+ end
55
+
56
+ def find_file(key)
57
+ base = File.join(@dir, key.tr(".", "/"))
58
+ %w[.yml .yaml .json].each do |ext|
59
+ path = "#{base}#{ext}"
60
+ return path if File.exist?(path)
61
+ end
62
+ nil
63
+ end
64
+
65
+ def parse(path)
66
+ content = File.read(path)
67
+ if path.end_with?(".json")
68
+ JSON.parse(content)
69
+ else
70
+ YAML.safe_load(content, permitted_classes: [Date])
71
+ end
72
+ end
73
+
74
+ def wrap(val)
75
+ case val
76
+ when Hash then Context.new(val, strict: @strict)
77
+ when Array then val.map { |v| v.is_a?(Hash) ? Context.new(v, strict: @strict) : v }
78
+ else val
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,30 @@
1
+ hook :after_build do |config, dest|
2
+ next unless config["url"]
3
+
4
+ url = config["url"].chomp("/")
5
+ posts = config["data"]["pages"]
6
+ .select { |p| p["date"] }
7
+ .sort_by { |p| p["date"].to_s }
8
+ .reverse
9
+ .first(20)
10
+
11
+ doc = REXML::Document.new
12
+ doc << REXML::XMLDecl.new("1.0", "UTF-8")
13
+ rss = doc.add_element("rss", "version" => "2.0")
14
+ channel = rss.add_element("channel")
15
+ channel.add_element("title").add_text(config["site_name"].to_s)
16
+ channel.add_element("link").add_text(url)
17
+ channel.add_element("description").add_text(config["description"].to_s)
18
+
19
+ posts.each do |p|
20
+ item = channel.add_element("item")
21
+ item.add_element("title").add_text(p["title"].to_s)
22
+ item.add_element("link").add_text("#{url}#{p["url"]}")
23
+ item.add_element("pubDate").add_text(p["date"].to_s)
24
+ end
25
+
26
+ out = +""
27
+ doc.write(out, 2)
28
+ File.write(File.join(dest, "feed.xml"), out << "\n")
29
+ puts(" feed.xml")
30
+ end
@@ -0,0 +1,18 @@
1
+ hook :after_build do |config, dest|
2
+ next unless config["url"]
3
+
4
+ url = config["url"].chomp("/")
5
+ pages = config["data"]["pages"]
6
+
7
+ doc = REXML::Document.new
8
+ doc << REXML::XMLDecl.new("1.0", "UTF-8")
9
+ urlset = doc.add_element("urlset", "xmlns" => "http://www.sitemaps.org/schemas/sitemap/0.9")
10
+ pages.each do |page|
11
+ urlset.add_element("url").add_element("loc").add_text("#{url}#{page["url"]}")
12
+ end
13
+
14
+ out = +""
15
+ doc.write(out, 2)
16
+ File.write(File.join(dest, "sitemap.xml"), out << "\n")
17
+ puts(" sitemap.xml")
18
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+ class RenderContext
5
+ def initialize(site:, context: {}, content_for_blocks: {}, strict: false)
6
+ @_site = site
7
+ @_context = context
8
+ @_content_for = content_for_blocks
9
+ @_strict = strict
10
+ @_erbout = +""
11
+ extend(site.helpers_module) if site.helpers_module
12
+ end
13
+
14
+ def content_for(name, &block)
15
+ key = name.to_s
16
+ if block
17
+ @_content_for[key] = capture(&block)
18
+ ""
19
+ else
20
+ @_content_for[key] || ""
21
+ end
22
+ end
23
+
24
+ def content_for?(name)
25
+ @_content_for.key?(name.to_s) && !@_content_for[name.to_s].empty?
26
+ end
27
+
28
+ def partial(name, **locals)
29
+ path = File.join(@_site.src, name)
30
+ return "" unless File.exist?(path)
31
+ _, body = @_site.send(:parse, path)
32
+ merged = @_context.dup
33
+ merged[:page] = (merged[:page] || {}).merge(locals.transform_keys(&:to_s))
34
+ @_site.send(:erb_render, body, context: merged, content_for_blocks: @_content_for)
35
+ end
36
+
37
+ def get_binding
38
+ binding
39
+ end
40
+
41
+ def method_missing(name, *args)
42
+ if @_strict
43
+ raise UndefinedVariableError, "undefined variable: #{name}"
44
+ else
45
+ nil
46
+ end
47
+ end
48
+
49
+ def respond_to_missing?(name, include_private = false)
50
+ true
51
+ end
52
+
53
+ private
54
+
55
+ def capture(&block)
56
+ old = @_erbout
57
+ @_erbout = +""
58
+ block.call
59
+ result = @_erbout
60
+ @_erbout = old
61
+ result
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+
5
+ class Scaffold
6
+ def initialize(dir)
7
+ @dir = File.expand_path(dir)
8
+ @src = File.join(@dir, "src")
9
+ end
10
+
11
+ def run
12
+ if Dir.exist?(@dir) && !Dir.empty?(@dir)
13
+ puts("#{@dir} already exists and is not empty")
14
+ return false
15
+ end
16
+
17
+ create_directories
18
+ create_gemfile
19
+ create_site_config
20
+ create_layout
21
+ create_index
22
+ puts("created #{@dir}")
23
+ puts(" cd #{File.basename(@dir)} && purity watch")
24
+ true
25
+ end
26
+
27
+ private
28
+
29
+ def create_directories
30
+ FileUtils.mkdir_p(File.join(@src, "_plugins"))
31
+ FileUtils.mkdir_p(File.join(@src, "_helpers"))
32
+ end
33
+
34
+ def create_gemfile
35
+ File.write(File.join(@dir, "Gemfile"), <<~RUBY)
36
+ source "https://rubygems.org"
37
+
38
+ gem "purity"
39
+ RUBY
40
+ end
41
+
42
+ def create_site_config
43
+ File.write(File.join(@src, "_site.yml"), <<~YAML)
44
+ site_name: my site
45
+ description: a site built with purity
46
+ YAML
47
+ end
48
+
49
+ def create_layout
50
+ File.write(File.join(@src, "_layout.html"), <<~'HTML')
51
+ <!doctype html>
52
+ <html lang="en">
53
+ <head>
54
+ <meta charset="utf-8">
55
+ <meta name="viewport" content="width=device-width, initial-scale=1">
56
+ <title><%= page.title %></title>
57
+ <%= content_for :head %>
58
+ </head>
59
+ <body>
60
+ <%= page.content %>
61
+ <%= content_for :scripts %>
62
+ </body>
63
+ </html>
64
+ HTML
65
+ end
66
+
67
+ def create_index
68
+ File.write(File.join(@src, "index.html"), <<~'HTML')
69
+ ---
70
+ title: home
71
+ ---
72
+ <h1><%= page.title %></h1>
73
+ <p>edit src/index.html to get started.</p>
74
+ HTML
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+
5
+ module Server
6
+ def serve(port: 4567)
7
+ build
8
+ server = make_server(port: port)
9
+ puts("serving at http://localhost:#{port}")
10
+ trap("INT") { server.shutdown }
11
+ server.start
12
+ end
13
+
14
+ def watch(port: 4567)
15
+ @livereload = true
16
+ build
17
+ @build_time = Time.now.to_i
18
+ server = make_server(port: port)
19
+ server.mount_proc("/__livereload") do |_, res|
20
+ res.content_type = "text/plain"
21
+ res.body = @build_time.to_s
22
+ end
23
+ puts("watching #{src}/ and serving at http://localhost:#{port}")
24
+ start_watcher
25
+ trap("INT") { server.shutdown }
26
+ server.start
27
+ end
28
+
29
+ private
30
+
31
+ def make_server(port:)
32
+ dest_dir = dest
33
+ server = WEBrick::HTTPServer.new(Port: port, Logger: WEBrick::Log.new("/dev/null"), AccessLog: [])
34
+ server.mount_proc("/") do |req, res|
35
+ path = req.path.chomp("/")
36
+ base = File.join(dest_dir, path)
37
+ found = [base, "#{base}.html", File.join(base, "index.html")].find { |f| File.file?(f) }
38
+ if found
39
+ res.body = File.binread(found)
40
+ res.content_type = WEBrick::HTTPUtils.mime_type(found, WEBrick::HTTPUtils::DefaultMimeTypes)
41
+ else
42
+ res.status = 404
43
+ custom_404 = File.join(dest_dir, "404.html")
44
+ if File.file?(custom_404)
45
+ res.body = File.binread(custom_404)
46
+ res.content_type = "text/html"
47
+ else
48
+ res.body = "not found"
49
+ end
50
+ end
51
+ end
52
+ server
53
+ end
54
+
55
+ def start_watcher
56
+ mtimes = {}
57
+ Dir.glob(File.join(src, "**/*")).select { |f| File.file?(f) }.each { |f| mtimes[f] = File.mtime(f) }
58
+ Thread.new do
59
+ loop do
60
+ sleep(1)
61
+ changed = false
62
+ Dir.glob(File.join(src, "**/*")).select { |f| File.file?(f) }.each do |f|
63
+ mt = File.mtime(f)
64
+ if mtimes[f] != mt
65
+ mtimes[f] = mt
66
+ changed = true
67
+ end
68
+ end
69
+ if changed
70
+ puts("rebuilding...")
71
+ build
72
+ @build_time = Time.now.to_i
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def inject_livereload(html:)
79
+ script = "<script>(function(){var t;setInterval(function(){fetch('/__livereload').then(function(r){return r.text()}).then(function(s){if(t&&s!==t)location.reload();t=s})},1000)})()</script>"
80
+ html.sub("</body>", "#{script}\n</body>")
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+
5
+ class Site
6
+ include Template
7
+ include Server
8
+
9
+ attr_reader :src, :dest, :config, :helpers_module
10
+
11
+ def initialize(src: nil, dest: nil)
12
+ @src = File.expand_path(src || "src")
13
+ @dest_override = dest
14
+ @hooks = Hash.new { |h, k| h[k] = [] }
15
+ @livereload = false
16
+ @build_time = 0
17
+ end
18
+
19
+ def hook(name, &block)
20
+ @hooks[name] << block
21
+ end
22
+
23
+ def build(include_drafts: false)
24
+ load_plugins
25
+ load_helpers
26
+ @config = load_site
27
+ @dest = File.expand_path(@dest_override || @config.delete("dest") || "build")
28
+ env = ENV.fetch("PURITY_ENV", "development")
29
+ @config["env"] = env
30
+ merge_env_config(env)
31
+ strict = @config.fetch("strict_variables", false)
32
+ @config["data"] = DataStore.new(File.join(@src, "_data"), strict: strict)
33
+
34
+ parsed = collect_pages(include_drafts: include_drafts)
35
+
36
+ all_pages = parsed.filter_map do |rel, meta, _|
37
+ next if meta["layout"].to_s == "false"
38
+ next if meta["draft"]
39
+ path = output_path(rel: rel, meta: meta)
40
+ page_url = "/#{path}".sub(/index\.html$/, "")
41
+ meta.merge("rel" => rel, "url" => page_url)
42
+ end
43
+
44
+ @config["data"]["pages"] = all_pages
45
+ build_collections(parsed: parsed)
46
+
47
+ @hooks[:after_parse].each { |fn| fn.call(@config, parsed) }
48
+
49
+ parsed.each do |rel, meta, body|
50
+ path = output_path(rel: rel, meta: meta)
51
+ page_url = "/#{path}".sub(/index\.html$/, "")
52
+
53
+ page_hash = meta.merge(
54
+ "url" => page_url,
55
+ "og_title" => meta["og_title"] || meta["title"],
56
+ "og_description" => meta["og_description"] || meta["description"]
57
+ )
58
+
59
+ ctx = { site: @config, page: page_hash, data: @config["data"] }
60
+ @hooks[:before_render].each { |fn| fn.call(ctx, rel) }
61
+ html = render_page(meta: meta, body: body, context: ctx)
62
+ html = @hooks[:after_render].reduce(html) { |h, fn| fn.call(h, ctx, rel) }
63
+ html = inject_livereload(html: html) if @livereload
64
+ write_output(html: html, rel: path)
65
+ label = meta["layout"].to_s == "false" ? " (standalone)" : ""
66
+ puts(" #{path}#{label}")
67
+ end
68
+
69
+ copied = copy_assets
70
+ @hooks[:after_build].each { |fn| fn.call(@config, @dest) }
71
+ stats = "built #{parsed.length} pages"
72
+ stats += ", copied #{copied} assets" if copied > 0
73
+ puts(stats)
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :hooks
79
+
80
+ def load_plugins
81
+ @hooks.clear
82
+ builtin = File.join(File.dirname(__FILE__), "plugins")
83
+ Dir.glob(File.join(builtin, "*.rb")).sort.each { |f| instance_eval(File.read(f), f) }
84
+ Dir.glob(File.join(@src, "_plugins", "*.rb")).sort.each { |f| instance_eval(File.read(f), f) }
85
+ end
86
+
87
+ def load_helpers
88
+ @helpers_module = Module.new
89
+ Dir.glob(File.join(@src, "_helpers", "*.rb")).sort.each do |f|
90
+ @helpers_module.module_eval(File.read(f), f)
91
+ end
92
+ end
93
+
94
+ def load_site
95
+ path = File.join(@src, "_site.yml")
96
+ config = File.exist?(path) ? YAML.safe_load(File.read(path), permitted_classes: [Date]) || {} : {}
97
+ config
98
+ end
99
+
100
+ def merge_env_config(env)
101
+ envs = @config.delete("environments") || {}
102
+ env_config = envs[env] || {}
103
+ @config.merge!(env_config)
104
+ end
105
+
106
+ def parse(path)
107
+ raw = File.read(path)
108
+ return [{}, raw] unless raw.start_with?("---\n")
109
+
110
+ parts = raw.split("---\n", 3)
111
+ [YAML.safe_load(parts[1], permitted_classes: [Date]) || {}, parts[2] || ""]
112
+ end
113
+
114
+ def collect_pages(include_drafts:)
115
+ Dir.glob(File.join(@src, "**/*.{html,md}"))
116
+ .reject { |f| File.basename(f).start_with?("_") }
117
+ .sort
118
+ .filter_map do |path|
119
+ rel = path.sub("#{@src}/", "")
120
+ meta, body = parse(path)
121
+ meta["format"] = File.extname(rel).delete_prefix(".")
122
+ rel = rel.sub(/\.md$/, ".html")
123
+ next if meta["draft"] && !include_drafts
124
+ meta["excerpt"] ||= extract_excerpt(body)
125
+ [rel, meta, body]
126
+ end
127
+ end
128
+
129
+ def extract_excerpt(body)
130
+ if body.include?("<!-- more -->")
131
+ body.split("<!-- more -->", 2).first.strip
132
+ else
133
+ first_para = body.strip.split(/\n\n/, 2).first
134
+ first_para&.strip || ""
135
+ end
136
+ end
137
+
138
+ def build_collections(parsed:)
139
+ collections_config = @config.fetch("collections", {})
140
+ return unless collections_config
141
+
142
+ collections_config.each do |name, opts|
143
+ opts ||= {}
144
+ items = parsed.filter_map do |rel, meta, _|
145
+ next unless rel.start_with?("#{name}/")
146
+ next if meta["draft"]
147
+ path = output_path(rel: rel, meta: meta)
148
+ page_url = "/#{path}".sub(/index\.html$/, "")
149
+ meta.merge("rel" => rel, "url" => page_url)
150
+ end
151
+ if opts["sort_by"]
152
+ items.sort_by! { |p| p[opts["sort_by"]].to_s }
153
+ items.reverse! if opts["order"] == "desc"
154
+ end
155
+ @config["data"][name] = items
156
+ end
157
+ end
158
+
159
+ def output_path(rel:, meta:)
160
+ if meta["permalink"]
161
+ p = meta["permalink"].sub(/^\//, "")
162
+ return "index.html" if p.empty?
163
+ return p.end_with?("/") ? "#{p}index.html" : p
164
+ end
165
+
166
+ return rel unless @config.fetch("clean_urls", true)
167
+ return rel if File.basename(rel) == "index.html"
168
+ return rel if File.basename(rel, ".html").match?(/\A\d{3}\z/)
169
+
170
+ rel.sub(/\.html$/, "/index.html")
171
+ end
172
+
173
+ def write_output(html:, rel:)
174
+ out = File.join(@dest, rel)
175
+ FileUtils.mkdir_p(File.dirname(out))
176
+ File.write(out, html)
177
+ end
178
+
179
+ def copy_assets
180
+ count = 0
181
+ Dir.glob(File.join(@src, "**/*"), File::FNM_DOTMATCH)
182
+ .select { |f| File.file?(f) }
183
+ .each do |path|
184
+ rel = path.sub("#{@src}/", "")
185
+ next if rel.end_with?(".html", ".md")
186
+ next if rel.split("/").any? { |seg| seg.start_with?("_") }
187
+
188
+ out = File.join(@dest, rel)
189
+ FileUtils.mkdir_p(File.dirname(out))
190
+ FileUtils.cp(path, out)
191
+ count += 1
192
+ end
193
+ count
194
+ end
195
+
196
+ end
197
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Purity
4
+ module Template
5
+ def render(body:, context:)
6
+ erb_render(body, context: context)
7
+ end
8
+
9
+ def render_page(meta:, body:, context:)
10
+ return body if meta["layout"].to_s == "false"
11
+
12
+ captured = {}
13
+ context[:page]["content"] = erb_render(body, context: context, content_for_blocks: captured)
14
+ context[:page]["content"] = @hooks[:before_layout].reduce(context[:page]["content"]) { |c, fn| fn.call(c, meta, context) }
15
+ apply_layout(meta: meta, context: context, content_for_blocks: captured)
16
+ end
17
+
18
+ private
19
+
20
+ def erb_render(body, context:, content_for_blocks: {})
21
+ strict = (context[:site] || {}).fetch("strict_variables", false)
22
+ ctx = RenderContext.new(site: self, context: context, content_for_blocks: content_for_blocks, strict: strict)
23
+ b = ctx.get_binding
24
+ b.local_variable_set(:site, Context.new(context[:site] || {}, strict: strict))
25
+ b.local_variable_set(:page, Context.new(context[:page] || {}, strict: strict))
26
+ b.local_variable_set(:data, context[:data]) if context[:data]
27
+ ERB.new(body, trim_mode: "-", eoutvar: "@_erbout").result(b)
28
+ end
29
+
30
+ def apply_layout(meta:, context:, content_for_blocks: {})
31
+ layout_name = meta.key?("layout") ? meta["layout"] : "layout"
32
+ lpath = File.join(src, "_#{layout_name}.html")
33
+ return context[:page]["content"] unless File.exist?(lpath)
34
+
35
+ lmeta, lbody = parse(lpath)
36
+ result = erb_render(lbody, context: context, content_for_blocks: content_for_blocks)
37
+ while lmeta["layout"]
38
+ context[:page]["content"] = result
39
+ lp = File.join(src, "_#{lmeta['layout']}.html")
40
+ break unless File.exist?(lp)
41
+ lmeta, lbody = parse(lp)
42
+ result = erb_render(lbody, context: context, content_for_blocks: content_for_blocks)
43
+ end
44
+ result
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Purity
2
+ VERSION = "1.0.0"
3
+ end
data/lib/purity.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+ require "date"
6
+ require "json"
7
+ require "webrick"
8
+ require "erb"
9
+ require "rexml/document"
10
+
11
+ require "purity/version"
12
+ require "purity/context"
13
+ require "purity/data_store"
14
+ require "purity/render_context"
15
+ require "purity/template"
16
+ require "purity/server"
17
+ require "purity/site"
18
+ require "purity/scaffold"
19
+ require "purity/cli"
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: purity
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Brody
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: webrick
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: templates, partials, conditionals, loops, layouts, blog, plugins. nothing
27
+ else.
28
+ email: josh@josh.mn
29
+ executables:
30
+ - purity
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - exe/purity
35
+ - lib/purity.rb
36
+ - lib/purity/cli.rb
37
+ - lib/purity/context.rb
38
+ - lib/purity/data_store.rb
39
+ - lib/purity/plugins/feed.rb
40
+ - lib/purity/plugins/sitemap.rb
41
+ - lib/purity/render_context.rb
42
+ - lib/purity/scaffold.rb
43
+ - lib/purity/server.rb
44
+ - lib/purity/site.rb
45
+ - lib/purity/template.rb
46
+ - lib/purity/version.rb
47
+ homepage: https://github.com/joshmn/purity
48
+ licenses:
49
+ - MIT
50
+ metadata: {}
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '2.7'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 4.0.1
66
+ specification_version: 4
67
+ summary: a simple static site generator
68
+ test_files: []