gryphon_nest 4.1.0 → 4.2.1

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: 0d12483d4aed715c91f61d5a9979563fca19c11816367ed746688dd574f2389a
4
- data.tar.gz: bb86f4d32b8fc8508e76df99d7da05da883ab2c8abcb8cff6ddfd0742a513d09
3
+ metadata.gz: 7dd5e55ab64ee1c053587293099a0423628f1d9075e4c73e6257fc029feefd47
4
+ data.tar.gz: b151fc9e7d09114ea9d47c1118f6b275658296f970cd0982a6145ff8c00b0c24
5
5
  SHA512:
6
- metadata.gz: 9d0fd32f0b95d06b21957abfe480bbc1e5ad49c160e98fb929c106d94f3da646dff0033c5207fb760975676c1a650b9c87c31844a8132bb73fa0f9971c92df13
7
- data.tar.gz: d110a25bd9513be044ac745cafd39600d096053d16e96968c2ace112cc6b7fa8ad675047eca2b30eaba34eead3da46d929d5dd6d89bd56191f34001287bd10b3
6
+ metadata.gz: ee54f2a45e67a122f2fa2416e2c26500a852f9e729060d7adff553a6e44b0bd897232f09aaef6224f44f102ac861918ced09aecdea0c4084604a867aa669dbb1
7
+ data.tar.gz: 2cd88219d657d3cd7d8e8d526e8d02a094ea89c684129e644c139d9dce0f2003eaa37b208d86f83086cfa67f65589bded6b71ec0ae8511bd323de1100b98cea6
data/bin/nest CHANGED
@@ -48,8 +48,7 @@ begin
48
48
 
49
49
  command = ARGV.fetch(0, 'build')
50
50
 
51
- nest = GryphonNest::Nest.new(options[:force])
52
- nest.compressor = GryphonNest::GzipCompressor.new if options[:compress]
51
+ nest = GryphonNest::Nest.new(options[:compress], options[:force])
53
52
 
54
53
  case command
55
54
  when 'clean'
@@ -0,0 +1,22 @@
1
+ #frozen_string_literal: true
2
+
3
+ module GryphonNest
4
+ module Compressors
5
+ class BrotliCompressor
6
+ def extname
7
+ '.br'
8
+ end
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(IO.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 GryphonNest
6
+ module Compressors
7
+ class GzipCompressor
8
+ def extname
9
+ '.gz'
10
+ end
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 IO.binread(file)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GryphonNest
4
+ module Compressors
5
+ autoload :BrotliCompressor, 'gryphon_nest/compressors/brotli_compressor'
6
+ autoload :GzipCompressor, 'gryphon_nest/compressors/gzip_compressor'
7
+
8
+ class << self
9
+ COMPRESSABLE_FILETYPES = %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 can_compress?(file)
26
+ file.size >= 20 && COMPRESSABLE_FILETYPES.include?(file.extname)
27
+ end
28
+
29
+ # return [Array<Object>]
30
+ def create
31
+ compressors = [GzipCompressor.new]
32
+
33
+ require 'brotli'
34
+ compressors.append(BrotliCompressor.new)
35
+ compressors
36
+ rescue LoadError
37
+ compressors
38
+ end
39
+ end
40
+ end
41
+ 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.1.0'
4
+ VERSION = '4.2.1'
5
5
  end
data/lib/gryphon_nest.rb CHANGED
@@ -6,8 +6,9 @@ require 'pathname'
6
6
  require 'webrick'
7
7
 
8
8
  module GryphonNest
9
+ autoload :Compressors, 'gryphon_nest/compressors'
9
10
  autoload :Errors, 'gryphon_nest/errors'
10
- autoload :GzipCompressor, 'gryphon_nest/gzip_compressor'
11
+ autoload :LayoutFile, 'gryphon_nest/layout_file'
11
12
  autoload :Logging, 'gryphon_nest/logging'
12
13
  autoload :Processors, 'gryphon_nest/processors'
13
14
  autoload :Renderers, 'gryphon_nest/renderers'
@@ -20,27 +21,31 @@ module GryphonNest
20
21
  LAYOUT_FILE = 'layout.mustache'
21
22
 
22
23
  class Nest
23
- # @return [GzipCompressor, nil]
24
- attr_writer :compressor
25
-
24
+ # @param compress [Boolean]
26
25
  # @param force [Boolean]
27
- def initialize(force)
26
+ def initialize(compress, force)
28
27
  @processors = Processors.create
29
28
  @logger = Logging.create
30
29
  @force = force
31
- @compressor = nil
30
+ @compressors = compress ? Compressors.create : []
31
+ @modifications = 0
32
32
  end
33
33
 
34
34
  # @raise [Errors::NotFoundError]
35
35
  def build
36
- raise Errors::NotFoundError, "Content directory doesn't exist in the current directory" unless Dir.exist?(CONTENT_DIR)
36
+ unless Dir.exist?(CONTENT_DIR)
37
+ raise Errors::NotFoundError, "Content directory doesn't exist in the current directory"
38
+ end
37
39
 
38
40
  Dir.mkdir(BUILD_DIR) unless Dir.exist?(BUILD_DIR)
39
41
 
40
- existing_files = glob("#{BUILD_DIR}/**/*").reject { |file| file.to_s.end_with?('.gz') }
41
- content_files = glob("#{CONTENT_DIR}/**/*")
42
+ existing_files = glob(BUILD_DIR, '{!.gz,!.br}')
43
+ content_files = glob(CONTENT_DIR)
42
44
  processed_files = content_files.collect { |src| process_file(src) }
43
- existing_files.difference(processed_files).each { |file| delete_file(file) }
45
+ files_to_delete = existing_files.difference(processed_files)
46
+ files_to_delete.each { |file| delete_file(file) }
47
+
48
+ @logger.info('No changes detected') if @modifications.zero? && files_to_delete.empty?
44
49
  end
45
50
 
46
51
  def clean
@@ -50,24 +55,15 @@ module GryphonNest
50
55
 
51
56
  def watch
52
57
  @logger.info('Watching for content changes')
53
- Listen.to(CONTENT_DIR, relative: true) do |modified, added, removed|
54
- modified.union(added).each do |file|
55
- path = Pathname(file)
56
- process_file(path)
57
- end
58
58
 
59
- removed.each do |file|
60
- path = Pathname(file)
61
- path = @processors[path.extname].dest_name(path)
62
- delete_file(path)
63
- end
64
- end.start
59
+ # Bypass modification checks, we already know the files been changed
60
+ @force = true
65
61
 
66
- Listen.to(DATA_DIR, relative: true) do |modified, added, removed|
67
- modified.union(added, removed).each do |file|
68
- path = Pathname(file)
69
- process_data_file(path)
70
- end
62
+ only = [/^#{CONTENT_DIR}/, /^#{DATA_DIR}/, /^#{LAYOUT_FILE}$/]
63
+ Listen.to('.', relative: true, only: only) do |modified, added, removed|
64
+ modified.union(added).each { |file| process_changes(file) }
65
+
66
+ removed.each { |file| process_changes(file, removal: true) }
71
67
  end.start
72
68
  end
73
69
 
@@ -89,6 +85,7 @@ module GryphonNest
89
85
  dest = processor.dest_name(src)
90
86
 
91
87
  if @force || processor.file_modified?(src, dest)
88
+ @modifications += 1
92
89
  msg = File.exist?(dest) ? 'Recreating' : 'Creating'
93
90
  @logger.info("#{msg} #{dest}")
94
91
  processor.process(src, dest)
@@ -100,15 +97,33 @@ module GryphonNest
100
97
 
101
98
  # @param file [Pathname]
102
99
  def compress_file(file)
103
- return unless @compressor.is_a?(GzipCompressor)
100
+ return if @compressors.empty?
101
+
102
+ return unless Compressors.can_compress?(file)
104
103
 
105
104
  @logger.info("Compressing #{file}")
106
- unless @compressor.can_compress?(file)
107
- @logger.info("Skipping #{file}")
108
- return
109
- end
105
+ @compressors.each { |compressor| compressor.compress(file) }
106
+ end
110
107
 
111
- @compressor.compress(file)
108
+ # @param src [String]
109
+ # @param removal [Boolean]
110
+ def process_changes(src, removal: false)
111
+ if src == LAYOUT_FILE
112
+ glob(CONTENT_DIR, TEMPLATE_EXT).each { |file| process_file(file) }
113
+ else
114
+ path = Pathname(src)
115
+
116
+ if src.start_with?(DATA_DIR)
117
+ process_data_file(path)
118
+ elsif removal
119
+ path = @processors[path.extname].dest_name(path)
120
+ delete_file(path)
121
+ else
122
+ process_file(path)
123
+ end
124
+ end
125
+ rescue StandardError => e
126
+ @logger.error(e.message)
112
127
  end
113
128
 
114
129
  # @param src [Pathname]
@@ -121,10 +136,11 @@ module GryphonNest
121
136
  process_file(src)
122
137
  end
123
138
 
124
- # @params path [String]
139
+ # @params base [String]
140
+ # @params match [String]
125
141
  # @return [Array<Pathname>]
126
- def glob(path)
127
- Pathname.glob(path).reject(&:directory?)
142
+ def glob(base, match = '')
143
+ Pathname.glob("#{base}/**/*#{match}").reject(&:directory?)
128
144
  end
129
145
 
130
146
  # @param file [Pathname]
@@ -132,12 +148,13 @@ module GryphonNest
132
148
  @logger.info("Deleting #{file}")
133
149
  file.delete
134
150
 
135
- compressed = Pathname("#{file}.gz")
136
-
137
- return unless compressed.exist?
151
+ @compressors.each do |compressor|
152
+ compressed_file = "#{file}#{compressor.extname}"
153
+ next unless compressed_file.exist?
138
154
 
139
- @logger.info("Deleting #{compressed}")
140
- compressed.delete
155
+ @logger.info("Deleting #{compressed_file}")
156
+ compressed_file.delete
157
+ end
141
158
  end
142
159
  end
143
160
  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.1.0
4
+ version: 4.2.1
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-06-24 00:00:00.000000000 Z
11
+ date: 2025-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: htmlbeautifier
@@ -120,8 +120,11 @@ files:
120
120
  - LICENSE
121
121
  - bin/nest
122
122
  - lib/gryphon_nest.rb
123
+ - lib/gryphon_nest/compressors.rb
124
+ - lib/gryphon_nest/compressors/brotli_compressor.rb
125
+ - lib/gryphon_nest/compressors/gzip_compressor.rb
123
126
  - lib/gryphon_nest/errors.rb
124
- - lib/gryphon_nest/gzip_compressor.rb
127
+ - lib/gryphon_nest/layout_file.rb
125
128
  - lib/gryphon_nest/logging.rb
126
129
  - lib/gryphon_nest/processors.rb
127
130
  - lib/gryphon_nest/processors/asset_processor.rb
@@ -1,24 +0,0 @@
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