gryphon_nest 4.0.1 → 4.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebc3be5da99292952a7e447920e092a23df21940c045b5b04e9c606ab41f1f90
4
- data.tar.gz: '08d4e67a95b76b98ca48ab20cd4926e2227fabeafd6f6f0dc05b2c91bef2e579'
3
+ metadata.gz: 455e829fa2827b15a7e0641049172d907615a4aa7bf5d29adefdb3440c71743e
4
+ data.tar.gz: 72a8c33e211645a385fb51c0581085159361e4c7d0dd62b1c284a7eb3e37cbc0
5
5
  SHA512:
6
- metadata.gz: 5166f66ccab09e8cb108b308b9e79681318f595d5faf8210cf783ccc714b2f0b3533a161c92eb92f94e102b6f9954ea7519c4da285a0330951340e8c61030bbb
7
- data.tar.gz: 58566f847efb6ddbc5e782e78b12b27672fcf08f44a897b68947621620b1c01a0ccb198d79f584fd3f1c41eaf68bf371c61fc65338df73933b496010c4068118
6
+ metadata.gz: b9c81a97e0bccc92c99cd1e7fd512b2e63899ab73b2de58f31a0161ad67246d27c1dc7f5fa2bc253fb4961ffa8047d79b56f26a45ca3f29b98bc5f33bdcb2c2e
7
+ data.tar.gz: 8db1246fd5023222581c26b408c98996ea935b159b1673d4b98f9cd07802c3d53109352f0f6230711260e5a4ba1f1d6b343e144e1d2e5a9e8ddddaa422fb36c1
data/bin/nest CHANGED
@@ -8,6 +8,8 @@ DEFAULT_PORT = 8000
8
8
  EXIT_FAILURE = 1
9
9
 
10
10
  options = {
11
+ compress: false,
12
+ force: false,
11
13
  port: DEFAULT_PORT,
12
14
  watch: false
13
15
  }
@@ -21,7 +23,11 @@ end
21
23
 
22
24
  begin
23
25
  parser = OptionParser.new do |opts|
24
- opts.banner = 'Usage: nest [build|serve] [options]'
26
+ opts.banner = 'Usage: nest [build|serve|clean] [options]'
27
+
28
+ opts.on('-c', '--compress', 'Create gzipped compressed versions of each file')
29
+
30
+ opts.on('-f', '--force', 'Force (re)build all files')
25
31
 
26
32
  opts.on('-p', '--port [PORT]', Integer, 'Port to run dev server on')
27
33
 
@@ -42,15 +48,22 @@ begin
42
48
 
43
49
  command = ARGV.fetch(0, 'build')
44
50
 
45
- usage_error("Unknown command #{command}", parser) unless %w[build serve].include?(command)
51
+ nest = GryphonNest::Nest.new(options[:force])
52
+ nest.compressor = GryphonNest::GzipCompressor.new if options[:compress]
46
53
 
47
- nest = GryphonNest::Nest.new
48
- nest.build
54
+ case command
55
+ when 'clean'
56
+ nest.clean
57
+ when 'build'
58
+ nest.build
59
+ when 'serve'
60
+ nest.build
49
61
 
50
- if command == 'serve'
51
62
  nest.watch if options[:watch]
52
63
 
53
64
  nest.serve(options[:port])
65
+ else
66
+ usage_error("Unknown command #{command}", parser)
54
67
  end
55
68
  rescue OptionParser::ParseError => e
56
69
  usage_error(e.message, parser)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ module GryphonNest
6
+ class GzipCompressor
7
+ # @param file [Pathname]
8
+ # @return [Boolean]
9
+ def can_compress?(file)
10
+ file.size >= 20
11
+ end
12
+
13
+ # @param file [Pathname]
14
+ def compress(file)
15
+ compressed = "#{file}.gz"
16
+
17
+ Zlib::GzipWriter.open(compressed, Zlib::BEST_COMPRESSION) do |gz|
18
+ gz.mtime = file.mtime
19
+ gz.orig_name = file.to_s
20
+ gz.write IO.binread(file)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GryphonNest
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?
15
+ @path.exist?
16
+ end
17
+
18
+ # @return [Time]
19
+ def mtime
20
+ @path.mtime
21
+ end
22
+
23
+ # @return [String]
24
+ def content
25
+ mod_time = mtime
26
+
27
+ if @content.nil? || mod_time > @last_mtime
28
+ @content = @path.read
29
+ @last_mtime = mod_time
30
+ end
31
+
32
+ @content
33
+ end
34
+ end
35
+ end
@@ -8,8 +8,10 @@ module GryphonNest
8
8
  # Renders a Mustache template into a html file
9
9
  class MustacheProcessor
10
10
  # @param renderer [Renderers::MustacheRenderer]
11
- def initialize(renderer)
11
+ # @param layout_file [LayoutFile]
12
+ def initialize(renderer, layout_file)
12
13
  @renderer = renderer
14
+ @layout_file = layout_file
13
15
  end
14
16
 
15
17
  # @param src [Pathname]
@@ -17,8 +19,6 @@ module GryphonNest
17
19
  # @raise [Errors::YamlError]
18
20
  # @raise [Errors::ParseError]
19
21
  def process(src, dest)
20
- @layout ||= read_layout_file
21
-
22
22
  context = read_context(src)
23
23
  content = build_output(src, context)
24
24
  write_file(dest, content)
@@ -36,11 +36,21 @@ module GryphonNest
36
36
  path.join('index.html')
37
37
  end
38
38
 
39
- # @param _src [Pathname]
40
- # @param _dest [Pathname]
39
+ # @param src [Pathname]
40
+ # @param dest [Pathname]
41
41
  # @return [Boolean]
42
- def file_modified?(_src, _dest)
43
- true
42
+ def file_modified?(src, dest)
43
+ return true unless dest.exist?
44
+
45
+ mod_time = dest.mtime
46
+ return true if src.mtime > mod_time
47
+
48
+ path = context_file_name(src)
49
+ return true if path.exist? && path.mtime > mod_time
50
+
51
+ return false unless @layout_file.exist?
52
+
53
+ @layout_file.mtime > mod_time
44
54
  end
45
55
 
46
56
  private
@@ -49,25 +59,30 @@ module GryphonNest
49
59
  # @return [Hash]
50
60
  # @raise [Errors::YamlError]
51
61
  def read_context(src)
52
- path = src.sub(CONTENT_DIR, DATA_DIR).sub_ext('.yaml')
53
- YAML.safe_load_file(path, symbolize_names: true)
62
+ YAML.safe_load_file(context_file_name(src), symbolize_names: true)
54
63
  rescue IOError, Errno::ENOENT
55
64
  {}
56
65
  rescue Psych::SyntaxError => e
57
66
  raise Errors::YamlError, "Encountered error while reading context file. Reason: #{e.message}"
58
67
  end
59
68
 
69
+ # @param src [Pathname]
70
+ # @return [Pathname]
71
+ def context_file_name(src)
72
+ src.sub(CONTENT_DIR, DATA_DIR).sub_ext('.yaml')
73
+ end
74
+
60
75
  # @param file [Pathname]
61
76
  # @param context [Hash]
62
77
  # @return [String]
63
78
  # @raise [Errors::ParseError]
64
79
  def build_output(file, context)
65
80
  content =
66
- if @layout.empty?
67
- @renderer.render_file(file, context)
68
- else
81
+ if @layout_file.exist?
69
82
  context[:yield] = file.basename(TEMPLATE_EXT)
70
- @renderer.render(@layout, context)
83
+ @renderer.render(@layout_file.content, context)
84
+ else
85
+ @renderer.render_file(file, context)
71
86
  end
72
87
 
73
88
  HtmlBeautifier.beautify(content, stop_on_errors: true)
@@ -83,13 +98,6 @@ module GryphonNest
83
98
  path.dirname.mkpath
84
99
  path.write(content)
85
100
  end
86
-
87
- # @return [String]
88
- def read_layout_file
89
- File.read(LAYOUT_FILE)
90
- rescue IOError, Errno::ENOENT
91
- ''
92
- end
93
101
  end
94
102
  end
95
103
  end
@@ -12,9 +12,10 @@ module GryphonNest
12
12
  def create
13
13
  ProcessorRegistry.new do |reg|
14
14
  reg[TEMPLATE_EXT] = proc {
15
+ layout_file = LayoutFile.new(Pathname(LAYOUT_FILE))
15
16
  renderer = Renderers::MustacheRenderer.new
16
17
  renderer.template_path = CONTENT_DIR
17
- Processors::MustacheProcessor.new(renderer)
18
+ Processors::MustacheProcessor.new(renderer, layout_file)
18
19
  }
19
20
 
20
21
  sass_proc = proc { Processors::SassProcessor.new }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GryphonNest
4
- VERSION = '4.0.1'
4
+ VERSION = '4.2.0'
5
5
  end
data/lib/gryphon_nest.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'listen'
4
5
  require 'pathname'
5
6
  require 'webrick'
6
7
 
7
8
  module GryphonNest
8
9
  autoload :Errors, 'gryphon_nest/errors'
10
+ autoload :GzipCompressor, 'gryphon_nest/gzip_compressor'
11
+ autoload :LayoutFile, 'gryphon_nest/layout_file'
9
12
  autoload :Logging, 'gryphon_nest/logging'
10
13
  autoload :Processors, 'gryphon_nest/processors'
11
14
  autoload :Renderers, 'gryphon_nest/renderers'
@@ -18,43 +21,51 @@ module GryphonNest
18
21
  LAYOUT_FILE = 'layout.mustache'
19
22
 
20
23
  class Nest
21
- def initialize
24
+ # @return [GzipCompressor, nil]
25
+ attr_writer :compressor
26
+
27
+ # @param force [Boolean]
28
+ def initialize(force)
22
29
  @processors = Processors.create
23
30
  @logger = Logging.create
31
+ @force = force
32
+ @compressor = nil
33
+ @modifications = 0
24
34
  end
25
35
 
26
36
  # @raise [Errors::NotFoundError]
27
37
  def build
28
- raise Errors::NotFoundError, "Content directory doesn't exist in the current directory" unless Dir.exist?(CONTENT_DIR)
38
+ unless Dir.exist?(CONTENT_DIR)
39
+ raise Errors::NotFoundError, "Content directory doesn't exist in the current directory"
40
+ end
29
41
 
30
42
  Dir.mkdir(BUILD_DIR) unless Dir.exist?(BUILD_DIR)
31
43
 
32
- existing_files = glob("#{BUILD_DIR}/**/*")
33
- content_files = glob("#{CONTENT_DIR}/**/*")
44
+ existing_files = glob(BUILD_DIR, '[!.gz]')
45
+ content_files = glob(CONTENT_DIR)
34
46
  processed_files = content_files.collect { |src| process_file(src) }
35
- existing_files.difference(processed_files).each { |file| delete_file(file) }
47
+ files_to_delete = existing_files.difference(processed_files)
48
+ files_to_delete.each { |file| delete_file(file) }
49
+
50
+ @logger.info('No changes detected') if @modifications.zero? && files_to_delete.empty?
51
+ end
52
+
53
+ def clean
54
+ FileUtils.remove_dir(BUILD_DIR, true)
55
+ @logger.info('Removed build dir')
36
56
  end
37
57
 
38
58
  def watch
39
59
  @logger.info('Watching for content changes')
40
- Listen.to(CONTENT_DIR, relative: true) do |modified, added, removed|
41
- modified.union(added).each do |file|
42
- path = Pathname(file)
43
- process_file(path)
44
- end
45
60
 
46
- removed.each do |file|
47
- path = Pathname(file)
48
- path = @processors[path.extname].dest_name(path)
49
- delete_file(path)
50
- end
51
- end.start
61
+ # Bypass modification checks, we already know the files been changed
62
+ @force = true
52
63
 
53
- Listen.to(DATA_DIR, relative: true) do |modified, added, removed|
54
- modified.union(added, removed).each do |file|
55
- path = Pathname(file)
56
- process_data_file(path)
57
- end
64
+ only = [/^#{CONTENT_DIR}/, /^#{DATA_DIR}/, /^#{LAYOUT_FILE}$/]
65
+ Listen.to('.', relative: true, only: only) do |modified, added, removed|
66
+ modified.union(added).each { |file| process_changes(file) }
67
+
68
+ removed.each { |file| process_changes(file, removal: true) }
58
69
  end.start
59
70
  end
60
71
 
@@ -75,15 +86,51 @@ module GryphonNest
75
86
  processor = @processors[src.extname]
76
87
  dest = processor.dest_name(src)
77
88
 
78
- if processor.file_modified?(src, dest)
89
+ if @force || processor.file_modified?(src, dest)
90
+ @modifications += 1
79
91
  msg = File.exist?(dest) ? 'Recreating' : 'Creating'
80
92
  @logger.info("#{msg} #{dest}")
81
93
  processor.process(src, dest)
94
+ compress_file(dest)
82
95
  end
83
96
 
84
97
  dest
85
98
  end
86
99
 
100
+ # @param file [Pathname]
101
+ def compress_file(file)
102
+ return unless @compressor.is_a?(GzipCompressor)
103
+
104
+ @logger.info("Compressing #{file}")
105
+ unless @compressor.can_compress?(file)
106
+ @logger.info("Skipping #{file}")
107
+ return
108
+ end
109
+
110
+ @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 src.start_with?(DATA_DIR)
122
+ process_data_file(path)
123
+ elsif removal
124
+ path = @processors[path.extname].dest_name(path)
125
+ delete_file(path)
126
+ else
127
+ process_file(path)
128
+ end
129
+ end
130
+ rescue StandardError => e
131
+ @logger.error(e.message)
132
+ end
133
+
87
134
  # @param src [Pathname]
88
135
  # @return [Pathname]
89
136
  def process_data_file(src)
@@ -94,16 +141,21 @@ module GryphonNest
94
141
  process_file(src)
95
142
  end
96
143
 
97
- # @params path [String]
144
+ # @params base [String]
145
+ # @params match [String]
98
146
  # @return [Array<Pathname>]
99
- def glob(path)
100
- Pathname.glob(path).reject(&:directory?)
147
+ def glob(base, match = '')
148
+ Pathname.glob("#{base}/**/*#{match}").reject(&:directory?)
101
149
  end
102
150
 
103
151
  # @param file [Pathname]
104
152
  def delete_file(file)
105
- @logger.info("Deleting #{file}")
106
- file.delete
153
+ [file, Pathname("#{file}.gz")].each do |f|
154
+ next unless f.exist?
155
+
156
+ @logger.info("Deleting #{f}")
157
+ f.delete
158
+ end
107
159
  end
108
160
  end
109
161
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gryphon_nest
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Birmingham
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-22 00:00:00.000000000 Z
11
+ date: 2025-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: htmlbeautifier
@@ -121,6 +121,8 @@ files:
121
121
  - bin/nest
122
122
  - lib/gryphon_nest.rb
123
123
  - lib/gryphon_nest/errors.rb
124
+ - lib/gryphon_nest/gzip_compressor.rb
125
+ - lib/gryphon_nest/layout_file.rb
124
126
  - lib/gryphon_nest/logging.rb
125
127
  - lib/gryphon_nest/processors.rb
126
128
  - lib/gryphon_nest/processors/asset_processor.rb