gryphon 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 +7 -0
- data/LICENSE +24 -0
- data/bin/gryphon +9 -0
- data/lib/gryphon/cli.rb +87 -0
- data/lib/gryphon/compressors/brotli_compressor.rb +22 -0
- data/lib/gryphon/compressors/gzip_compressor.rb +24 -0
- data/lib/gryphon/compressors.rb +42 -0
- data/lib/gryphon/errors.rb +11 -0
- data/lib/gryphon/layout_file.rb +31 -0
- data/lib/gryphon/logging.rb +32 -0
- data/lib/gryphon/processors/asset_processor.rb +30 -0
- data/lib/gryphon/processors/mustache_processor.rb +78 -0
- data/lib/gryphon/processors/sass_processor.rb +31 -0
- data/lib/gryphon/processors.rb +32 -0
- data/lib/gryphon/renderers/mustache_renderer.rb +25 -0
- data/lib/gryphon/renderers.rb +7 -0
- data/lib/gryphon/version.rb +5 -0
- data/lib/gryphon.rb +151 -0
- metadata +163 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: aec8dfc0ffa76193cd8528f8375b6eea622aed8499d3b340945f38a1dce1bb31
|
|
4
|
+
data.tar.gz: 030fbf23c1515be0a8a207184c5f574bf8cc2d2a4574e7f92d239be6271437ac
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6b30add9bae44b30943a4bdb0ad03da9c95c09edc80b1f8881c0fb728de6cec3efd3f911094356eff0b2a3c060d4ba5c64e84709fa0d1ec7f62b61003303bf4d
|
|
7
|
+
data.tar.gz: 7083d7c2b16b3d0b19a074dc8adc954e4b7329b3b9787dc751362c8c10a3af1a7d51d06d0aac59c5ad6b91b192be31ed2bb650058145bace57e5ebdd5e2a87eb
|
data/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
data/bin/gryphon
ADDED
data/lib/gryphon/cli.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module Gryphon
|
|
6
|
+
# Command line parsing and execution class
|
|
7
|
+
class Cli
|
|
8
|
+
COMMANDS = %w[
|
|
9
|
+
build
|
|
10
|
+
clean
|
|
11
|
+
serve
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def run
|
|
15
|
+
options = {
|
|
16
|
+
compress: false,
|
|
17
|
+
force: false,
|
|
18
|
+
port: 8000,
|
|
19
|
+
watch: false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
parse!(options)
|
|
23
|
+
execute(ARGV[0], options)
|
|
24
|
+
rescue Errors::GryphonError => e
|
|
25
|
+
warn e.message
|
|
26
|
+
exit false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @param command [String]
|
|
32
|
+
# @param options [Hash]
|
|
33
|
+
# @raise [Errors::GryphonError]
|
|
34
|
+
def execute(command, options)
|
|
35
|
+
raise Errors::GryphonError, to_usage_error("Unknown command #{command}") unless COMMANDS.include?(command)
|
|
36
|
+
|
|
37
|
+
nest = Gryphon::Nest.new(
|
|
38
|
+
Processors.create,
|
|
39
|
+
options[:compress] ? Compressors.create : [],
|
|
40
|
+
options[:force]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if command == 'clean'
|
|
44
|
+
nest.clean
|
|
45
|
+
else
|
|
46
|
+
nest.build
|
|
47
|
+
|
|
48
|
+
nest.serve(options[:port], options[:watch]) if command == 'serve'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param options [Hash]
|
|
53
|
+
# @raise [Errors::GryphonError]
|
|
54
|
+
def parse!(options)
|
|
55
|
+
OptionParser.new do |opts|
|
|
56
|
+
opts.banner = 'Usage: gryphon [build|serve|clean] [options]
|
|
57
|
+
Yet another static website builder using mustache and sass'
|
|
58
|
+
|
|
59
|
+
opts.separator ''
|
|
60
|
+
|
|
61
|
+
opts.on('-c', '--compress', 'Create compressed versions of each file')
|
|
62
|
+
|
|
63
|
+
opts.on('-f', '--force', 'Force (re)build all files')
|
|
64
|
+
|
|
65
|
+
opts.on('-p', '--port [PORT]', Integer, 'Port to run dev server on')
|
|
66
|
+
|
|
67
|
+
opts.on('-w', '--watch', 'Watch for file changes when running the local server')
|
|
68
|
+
|
|
69
|
+
opts.on('-h', '--help', 'Show this message') do
|
|
70
|
+
puts opts
|
|
71
|
+
exit
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
opts.on('-v', '--version', 'Print version') do
|
|
75
|
+
puts Gryphon::VERSION
|
|
76
|
+
exit
|
|
77
|
+
end
|
|
78
|
+
end.parse!(into: options)
|
|
79
|
+
rescue OptionParser::ParseError => e
|
|
80
|
+
raise Errors::GryphonError, to_usage_error(e.message)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param msg [String]
|
|
84
|
+
# @return [String]
|
|
85
|
+
def to_usage_error(msg) = "gryphon: #{msg}\nTry 'gryphon -h' for more information"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gryphon
|
|
4
|
+
module Compressors
|
|
5
|
+
# Class for compressing files using brotli
|
|
6
|
+
class BrotliCompressor
|
|
7
|
+
# @return [String]
|
|
8
|
+
def extname = '.br'
|
|
9
|
+
|
|
10
|
+
# @param file [Pathname]
|
|
11
|
+
def compress(file)
|
|
12
|
+
compressed = "#{file}#{extname}"
|
|
13
|
+
|
|
14
|
+
File.open(compressed, 'wb') do |br|
|
|
15
|
+
writer = Brotli::Writer.new(br)
|
|
16
|
+
writer.write(File.binread(file))
|
|
17
|
+
writer.close
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zlib'
|
|
4
|
+
|
|
5
|
+
module Gryphon
|
|
6
|
+
module Compressors
|
|
7
|
+
# Class for compressing files using zlib
|
|
8
|
+
class GzipCompressor
|
|
9
|
+
# @return [String]
|
|
10
|
+
def extname = '.gz'
|
|
11
|
+
|
|
12
|
+
# @param file [Pathname]
|
|
13
|
+
def compress(file)
|
|
14
|
+
compressed = "#{file}#{extname}"
|
|
15
|
+
|
|
16
|
+
Zlib::GzipWriter.open(compressed, Zlib::BEST_COMPRESSION) do |gz|
|
|
17
|
+
gz.mtime = file.mtime
|
|
18
|
+
gz.orig_name = file.to_s
|
|
19
|
+
gz.write(File.binread(file))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gryphon
|
|
4
|
+
module Compressors
|
|
5
|
+
autoload :BrotliCompressor, 'gryphon/compressors/brotli_compressor'
|
|
6
|
+
autoload :GzipCompressor, 'gryphon/compressors/gzip_compressor'
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
COMPRESSABLE = %w[
|
|
10
|
+
.html
|
|
11
|
+
.htm
|
|
12
|
+
.xhtml
|
|
13
|
+
.txt
|
|
14
|
+
.csv
|
|
15
|
+
.css
|
|
16
|
+
.js
|
|
17
|
+
.mjs
|
|
18
|
+
.md
|
|
19
|
+
.xml
|
|
20
|
+
.svg
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# @param file [Pathname]
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def compressable?(file) = file.size >= 40 && COMPRESSABLE.include?(file.extname)
|
|
26
|
+
|
|
27
|
+
# return [Array<Object>]
|
|
28
|
+
def create
|
|
29
|
+
compressors = [GzipCompressor.new]
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
require 'brotli'
|
|
33
|
+
compressors << BrotliCompressor.new
|
|
34
|
+
rescue LoadError
|
|
35
|
+
# Do nothing
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
compressors
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gryphon
|
|
4
|
+
# Wrapper class for operations performed on the layout.yaml file
|
|
5
|
+
class LayoutFile
|
|
6
|
+
# @param path [Pathname]
|
|
7
|
+
def initialize(path)
|
|
8
|
+
@path = path
|
|
9
|
+
@content = nil
|
|
10
|
+
@last_mtime = Time.now
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [Boolean]
|
|
14
|
+
def exist? = @path.exist?
|
|
15
|
+
|
|
16
|
+
# @return [Time]
|
|
17
|
+
def mtime = @path.mtime
|
|
18
|
+
|
|
19
|
+
# @return [String]
|
|
20
|
+
def content
|
|
21
|
+
mod_time = mtime
|
|
22
|
+
|
|
23
|
+
if @content.nil? || mod_time > @last_mtime
|
|
24
|
+
@content = @path.read
|
|
25
|
+
@last_mtime = mod_time
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@content
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module Gryphon
|
|
6
|
+
# Mixin class for setting up log creation and logging
|
|
7
|
+
module Logging
|
|
8
|
+
include Kernel
|
|
9
|
+
|
|
10
|
+
# @param msg [String]
|
|
11
|
+
# @param lvl [Integer]
|
|
12
|
+
def log(msg, lvl = Logger::INFO)
|
|
13
|
+
@logger ||= create
|
|
14
|
+
@logger.add(lvl, msg)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# @return [Logger]
|
|
20
|
+
def create
|
|
21
|
+
logger = Logger.new($stdout)
|
|
22
|
+
|
|
23
|
+
# Create formatter that matches WebBricks log messages
|
|
24
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
25
|
+
date_format = datetime.strftime('%Y-%m-%d %H:%M:%S')
|
|
26
|
+
"[#{date_format}] #{severity.ljust(5)} #{msg}\n"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
logger
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Gryphon
|
|
6
|
+
module Processors
|
|
7
|
+
# Default file processor. Moves files from source to destination
|
|
8
|
+
class AssetProcessor
|
|
9
|
+
# @param src [Pathname]
|
|
10
|
+
# @param dest [Pathname]
|
|
11
|
+
def process(src, dest)
|
|
12
|
+
dest.dirname.mkpath
|
|
13
|
+
FileUtils.copy_file(src, dest)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param src [Pathname]
|
|
17
|
+
# @return [Pathname]
|
|
18
|
+
def dest_name(src) = src.sub(CONTENT_DIR, BUILD_DIR)
|
|
19
|
+
|
|
20
|
+
# @param src [Pathname]
|
|
21
|
+
# @param des [Pathname]
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def file_modified?(src, dest)
|
|
24
|
+
return true unless dest.exist?
|
|
25
|
+
|
|
26
|
+
src.mtime > dest.mtime
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'htmlbeautifier'
|
|
4
|
+
|
|
5
|
+
module Gryphon
|
|
6
|
+
module Processors
|
|
7
|
+
# Renders a Mustache template into a html file
|
|
8
|
+
class MustacheProcessor
|
|
9
|
+
# @param renderer [Renderers::MustacheRenderer]
|
|
10
|
+
# @param layout_file [LayoutFile]
|
|
11
|
+
def initialize(renderer, layout_file)
|
|
12
|
+
@renderer = renderer
|
|
13
|
+
@layout_file = layout_file
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param src [Pathname]
|
|
17
|
+
# @param dest [Pathname]
|
|
18
|
+
# @raise [Errors::ParseError]
|
|
19
|
+
def process(src, dest)
|
|
20
|
+
content = build_output(src)
|
|
21
|
+
write_file(dest, content)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param src [Pathname]
|
|
25
|
+
# @return [Pathname]
|
|
26
|
+
def dest_name(src)
|
|
27
|
+
dir = src.dirname
|
|
28
|
+
path = dir.sub(CONTENT_DIR, BUILD_DIR)
|
|
29
|
+
basename = src.basename(TEMPLATE_EXT)
|
|
30
|
+
|
|
31
|
+
path = path.join(basename) if basename.to_s != 'index'
|
|
32
|
+
|
|
33
|
+
path.join('index.html')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param src [Pathname]
|
|
37
|
+
# @param dest [Pathname]
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def file_modified?(src, dest)
|
|
40
|
+
return true unless dest.exist?
|
|
41
|
+
|
|
42
|
+
mod_time = dest.mtime
|
|
43
|
+
return true if src.mtime > mod_time
|
|
44
|
+
|
|
45
|
+
return false unless @layout_file.exist?
|
|
46
|
+
|
|
47
|
+
@layout_file.mtime > mod_time
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# @param file [Pathname]
|
|
53
|
+
# @return [String]
|
|
54
|
+
# @raise [Errors::ParseError]
|
|
55
|
+
def build_output(file)
|
|
56
|
+
content =
|
|
57
|
+
if @layout_file.exist?
|
|
58
|
+
@renderer.render(@layout_file.content, { yield: file.basename(TEMPLATE_EXT) })
|
|
59
|
+
else
|
|
60
|
+
@renderer.render_file(file)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
HtmlBeautifier.beautify(content, stop_on_errors: true)
|
|
64
|
+
rescue Mustache::Parser::SyntaxError, Psych::SyntaxError => e
|
|
65
|
+
raise Errors::ParseError, "Failed to process mustache template #{file}. Reason: #{e.message}"
|
|
66
|
+
rescue RuntimeError => e
|
|
67
|
+
raise Errors::ParseError, "Failed to beautify template output #{file}. Reason: #{e.message}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @param path [Pathname]
|
|
71
|
+
# @param content [String]
|
|
72
|
+
def write_file(path, content)
|
|
73
|
+
path.dirname.mkpath
|
|
74
|
+
path.write(content)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gryphon
|
|
4
|
+
module Processors
|
|
5
|
+
# Renders a sass file into a css file
|
|
6
|
+
class SassProcessor
|
|
7
|
+
# @param src [Pathname]
|
|
8
|
+
# @param dest [Pathname]
|
|
9
|
+
# @raise [Errors::ParseError]
|
|
10
|
+
def process(src, dest)
|
|
11
|
+
result = Sass.compile(src)
|
|
12
|
+
File.write(dest, result.css)
|
|
13
|
+
rescue Sass::CompileError => e
|
|
14
|
+
raise Errors::ParseError, "Failed to process sass style sheet #{src}. Reason: #{e.full_message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param src [Pathname]
|
|
18
|
+
# @return [Pathname]
|
|
19
|
+
def dest_name(src) = src.sub(CONTENT_DIR, BUILD_DIR).sub_ext('.css')
|
|
20
|
+
|
|
21
|
+
# @param src [Pathname]
|
|
22
|
+
# @param des [Pathname]
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
def file_modified?(src, dest)
|
|
25
|
+
return true unless dest.exist?
|
|
26
|
+
|
|
27
|
+
src.mtime > dest.mtime
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gryphon
|
|
4
|
+
module Processors
|
|
5
|
+
autoload :AssetProcessor, 'gryphon/processors/asset_processor'
|
|
6
|
+
autoload :MustacheProcessor, 'gryphon/processors/mustache_processor'
|
|
7
|
+
autoload :SassProcessor, 'gryphon/processors/sass_processor.rb'
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# @return [Array<Object>]
|
|
11
|
+
def create
|
|
12
|
+
processors = Hash.new(AssetProcessor.new)
|
|
13
|
+
|
|
14
|
+
layout_file = LayoutFile.new(Pathname(LAYOUT_FILE))
|
|
15
|
+
renderer = Renderers::MustacheRenderer.new
|
|
16
|
+
renderer.template_path = CONTENT_DIR
|
|
17
|
+
processors[TEMPLATE_EXT] = Processors::MustacheProcessor.new(renderer, layout_file)
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
require 'sass-embedded'
|
|
21
|
+
sass = Processors::SassProcessor.new
|
|
22
|
+
processors['.scss'] = sass
|
|
23
|
+
processors['.sass'] = sass
|
|
24
|
+
rescue LoadError
|
|
25
|
+
# Do nothing
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
processors
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mustache'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Gryphon
|
|
7
|
+
module Renderers
|
|
8
|
+
# Class to override default Mustache behavior
|
|
9
|
+
class MustacheRenderer < Mustache
|
|
10
|
+
# @param _name [String]
|
|
11
|
+
# @return [String]
|
|
12
|
+
# @raise [Psych::SyntaxError]
|
|
13
|
+
def partial(_name)
|
|
14
|
+
name = context[:yield]
|
|
15
|
+
path = "#{template_path}/#{name}.#{template_extension}"
|
|
16
|
+
docs = YAML.safe_load_stream(File.read(path), filename: name)
|
|
17
|
+
content = docs[1] || docs[0]
|
|
18
|
+
|
|
19
|
+
context.push(docs[0]) unless docs[1].nil?
|
|
20
|
+
|
|
21
|
+
content
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/gryphon.rb
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'listen'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'webrick'
|
|
7
|
+
|
|
8
|
+
module Gryphon
|
|
9
|
+
autoload :Cli, 'gryphon/cli'
|
|
10
|
+
autoload :Compressors, 'gryphon/compressors'
|
|
11
|
+
autoload :Errors, 'gryphon/errors'
|
|
12
|
+
autoload :LayoutFile, 'gryphon/layout_file'
|
|
13
|
+
autoload :Logging, 'gryphon/logging'
|
|
14
|
+
autoload :Processors, 'gryphon/processors'
|
|
15
|
+
autoload :Renderers, 'gryphon/renderers'
|
|
16
|
+
autoload :VERSION, 'gryphon/version'
|
|
17
|
+
|
|
18
|
+
BUILD_DIR = '_site'
|
|
19
|
+
CONTENT_DIR = 'content'
|
|
20
|
+
TEMPLATE_EXT = '.mustache'
|
|
21
|
+
LAYOUT_FILE = 'layout.mustache'
|
|
22
|
+
|
|
23
|
+
class Nest
|
|
24
|
+
include Logging
|
|
25
|
+
|
|
26
|
+
# @param processors [Array<Object>]
|
|
27
|
+
# @param compressors [Array<Object>]
|
|
28
|
+
# @param force [Boolean]
|
|
29
|
+
def initialize(processors, compressors, force)
|
|
30
|
+
@processors = processors
|
|
31
|
+
@compressors = compressors
|
|
32
|
+
@force = force
|
|
33
|
+
@modifications = 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @raise [Errors::GryphonError]
|
|
37
|
+
def build
|
|
38
|
+
unless Dir.exist?(CONTENT_DIR)
|
|
39
|
+
raise Errors::NotFoundError, "Content directory doesn't exist in the current directory"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
FileUtils.mkdir_p(BUILD_DIR)
|
|
43
|
+
existing_files = glob(BUILD_DIR, '{!.gz,!.br}')
|
|
44
|
+
content_files = glob(CONTENT_DIR)
|
|
45
|
+
processed_files = content_files.collect { |src| process_file(src) }
|
|
46
|
+
files_to_delete = existing_files.difference(processed_files)
|
|
47
|
+
files_to_delete.each { |file| delete_file(file) }
|
|
48
|
+
|
|
49
|
+
log('No changes detected') if @modifications.zero? && files_to_delete.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def clean
|
|
53
|
+
FileUtils.remove_dir(BUILD_DIR, true)
|
|
54
|
+
log('Removed build dir')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param port [Integer]
|
|
58
|
+
# @param monitor [Boolean]
|
|
59
|
+
def serve(port, monitor)
|
|
60
|
+
watch if monitor
|
|
61
|
+
|
|
62
|
+
log("Running local server on #{port}")
|
|
63
|
+
server = WEBrick::HTTPServer.new(Port: port, DocumentRoot: BUILD_DIR, AccessLog: [])
|
|
64
|
+
# Trap ctrl c so we don't get the horrible stack trace
|
|
65
|
+
trap('INT') { server.shutdown }
|
|
66
|
+
server.start
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def watch
|
|
72
|
+
log('Watching for content changes')
|
|
73
|
+
|
|
74
|
+
# Bypass modification checks, we already know the files been changed
|
|
75
|
+
@force = true
|
|
76
|
+
|
|
77
|
+
only = [/^#{CONTENT_DIR}/, /^#{LAYOUT_FILE}$/]
|
|
78
|
+
Listen.to('.', relative: true, only: only) do |modified, added, removed|
|
|
79
|
+
modified.union(added).each { |file| process_changes(file) }
|
|
80
|
+
|
|
81
|
+
removed.each { |file| process_changes(file, removal: true) }
|
|
82
|
+
end.start
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @param src [Pathname]
|
|
86
|
+
# @return [Pathname]
|
|
87
|
+
# @raise [Errors::GryphonError]
|
|
88
|
+
def process_file(src)
|
|
89
|
+
processor = @processors[src.extname]
|
|
90
|
+
dest = processor.dest_name(src)
|
|
91
|
+
|
|
92
|
+
if @force || processor.file_modified?(src, dest)
|
|
93
|
+
@modifications += 1
|
|
94
|
+
msg = File.exist?(dest) ? 'Recreating' : 'Creating'
|
|
95
|
+
log("#{msg} #{dest}")
|
|
96
|
+
processor.process(src, dest)
|
|
97
|
+
compress_file(dest)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
dest
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# @param file [Pathname]
|
|
104
|
+
def compress_file(file)
|
|
105
|
+
return if @compressors.empty?
|
|
106
|
+
|
|
107
|
+
return unless Compressors.compressable?(file)
|
|
108
|
+
|
|
109
|
+
log("Compressing #{file}")
|
|
110
|
+
@compressors.each { |compressor| compressor.compress(file) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @param src [String]
|
|
114
|
+
# @param removal [Boolean]
|
|
115
|
+
def process_changes(src, removal: false)
|
|
116
|
+
if src == LAYOUT_FILE
|
|
117
|
+
glob(CONTENT_DIR, TEMPLATE_EXT).each { |file| process_file(file) }
|
|
118
|
+
else
|
|
119
|
+
path = Pathname(src)
|
|
120
|
+
|
|
121
|
+
if removal
|
|
122
|
+
path = @processors[path.extname].dest_name(path)
|
|
123
|
+
delete_file(path)
|
|
124
|
+
else
|
|
125
|
+
process_file(path)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
rescue Errors::GryphonError => e
|
|
129
|
+
log(e.message, Logger::ERROR)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @params base [String]
|
|
133
|
+
# @params match [String]
|
|
134
|
+
# @return [Array<Pathname>]
|
|
135
|
+
def glob(base, match = '') = Pathname.glob("#{base}/**/*#{match}").reject(&:directory?)
|
|
136
|
+
|
|
137
|
+
# @param file [Pathname]
|
|
138
|
+
def delete_file(file)
|
|
139
|
+
log("Deleting #{file}")
|
|
140
|
+
file.delete
|
|
141
|
+
|
|
142
|
+
@compressors.each do |compressor|
|
|
143
|
+
compressed_file = Pathname("#{file}#{compressor.extname}")
|
|
144
|
+
next unless compressed_file.exist?
|
|
145
|
+
|
|
146
|
+
log("Deleting #{compressed_file}")
|
|
147
|
+
compressed_file.delete
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gryphon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Christopher Birmingham
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: htmlbeautifier
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.4'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.4'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: listen
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.9'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.9'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: mustache
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: psych
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.2'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.2'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: webrick
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.9'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.9'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: bundler
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '2.3'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '2.3'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '13.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '13.0'
|
|
111
|
+
description: A slightly opinionated static website generator for those who like working
|
|
112
|
+
in html and mustache
|
|
113
|
+
email:
|
|
114
|
+
- chris.birmingham@hotmail.co.uk
|
|
115
|
+
executables:
|
|
116
|
+
- gryphon
|
|
117
|
+
extensions: []
|
|
118
|
+
extra_rdoc_files: []
|
|
119
|
+
files:
|
|
120
|
+
- LICENSE
|
|
121
|
+
- bin/gryphon
|
|
122
|
+
- lib/gryphon.rb
|
|
123
|
+
- lib/gryphon/cli.rb
|
|
124
|
+
- lib/gryphon/compressors.rb
|
|
125
|
+
- lib/gryphon/compressors/brotli_compressor.rb
|
|
126
|
+
- lib/gryphon/compressors/gzip_compressor.rb
|
|
127
|
+
- lib/gryphon/errors.rb
|
|
128
|
+
- lib/gryphon/layout_file.rb
|
|
129
|
+
- lib/gryphon/logging.rb
|
|
130
|
+
- lib/gryphon/processors.rb
|
|
131
|
+
- lib/gryphon/processors/asset_processor.rb
|
|
132
|
+
- lib/gryphon/processors/mustache_processor.rb
|
|
133
|
+
- lib/gryphon/processors/sass_processor.rb
|
|
134
|
+
- lib/gryphon/renderers.rb
|
|
135
|
+
- lib/gryphon/renderers/mustache_renderer.rb
|
|
136
|
+
- lib/gryphon/version.rb
|
|
137
|
+
homepage: https://github.com/chrisBirmingham/gryphon
|
|
138
|
+
licenses:
|
|
139
|
+
- Unlicense
|
|
140
|
+
metadata:
|
|
141
|
+
homepage_uri: https://github.com/chrisBirmingham/gryphon
|
|
142
|
+
source_code_uri: https://github.com/chrisBirmingham/gryphon
|
|
143
|
+
rubygems_mfa_required: 'true'
|
|
144
|
+
post_install_message:
|
|
145
|
+
rdoc_options: []
|
|
146
|
+
require_paths:
|
|
147
|
+
- lib
|
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: 3.0.0
|
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
158
|
+
requirements: []
|
|
159
|
+
rubygems_version: 3.4.20
|
|
160
|
+
signing_key:
|
|
161
|
+
specification_version: 4
|
|
162
|
+
summary: Yet another static site generator
|
|
163
|
+
test_files: []
|