darkroom 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,53 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative('../asset')
4
+ require_relative('../delegate')
4
5
 
5
6
  class Darkroom
6
- class Asset
7
- HTMLDelegate = Delegate.new(
8
- content_type: 'text/html',
9
- reference_regex: %r{
10
- <(?<tag>a|area|audio|base|embed|iframe|img|input|link|script|source|track|video)\s+[^>]*
11
- (?<attr>href|src)=#{REFERENCE_PATH.source}[^>]*>
12
- }x.freeze,
13
-
14
- validate_reference: ->(asset, match, format) do
15
- return unless format == 'displace'
16
-
17
- if match[:tag] == 'link'
18
- 'Asset type must be text/css' unless asset.content_type == 'text/css'
19
- elsif match[:tag] == 'script'
20
- 'Asset type must be text/javascript' unless asset.content_type == 'text/javascript'
21
- elsif match[:tag] == 'img'
22
- 'Asset type must be image/svg+xml' unless asset.content_type == 'image/svg+xml'
23
- else
24
- "Cannot displace <#{match[:tag]}> tags"
25
- end
26
- end,
27
-
28
- reference_content: ->(asset, match, format) do
29
- case format
30
- when 'displace'
31
- if match[:tag] == 'link' && asset.content_type == 'text/css'
7
+ class HTMLDelegate < Delegate
8
+ REFERENCE_REGEX = /
9
+ <(?<tag>a|area|audio|base|embed|iframe|img|input|link|script|source|track|video)\s+[^>]*
10
+ (?<attr>href|src)=#{Asset::REFERENCE_REGEX.source}[^>]*>
11
+ /x.freeze
12
+
13
+ content_type('text/html')
14
+
15
+ reference(REFERENCE_REGEX) do |parse_data:, match:, asset:, format:|
16
+ case format
17
+ when 'displace'
18
+ case match[:tag]
19
+ when 'link'
20
+ if asset.content_type == 'text/css'
32
21
  "<style>#{asset.content}</style>"
33
- elsif match[:tag] == 'script' && asset.content_type == 'text/javascript'
22
+ else
23
+ error('Asset content type must be text/css')
24
+ end
25
+ when 'script'
26
+ if asset.content_type == 'text/javascript'
34
27
  offset = match.begin(0)
35
28
 
36
29
  "#{match[0][0..(match.begin(:attr) - 2 - offset)]}"\
37
30
  "#{match[0][(match.end(:quoted) + match[:quote].size - offset)..(match.end(0) - offset)]}"\
38
31
  "#{asset.content}"
39
- elsif match[:tag] == 'img' && asset.content_type == 'image/svg+xml'
40
- asset.content
32
+ else
33
+ error('Asset content type must be text/javascript')
34
+ end
35
+ when 'img'
36
+ if asset.content_type == 'image/svg+xml'
37
+ asset.content(minified: false)
38
+ else
39
+ error('Asset content type must be image/svg+xml')
41
40
  end
42
- when 'utf8'
43
- quote = match[:quote] == '' ? Asset::DEFAULT_QUOTE : match[:quote]
41
+ else
42
+ error("Cannot displace <#{match[:tag]}> tags")
43
+ end
44
+ when 'utf8'
45
+ quote = match[:quote] == '' ? Asset::DEFAULT_QUOTE : match[:quote]
44
46
 
45
- content = asset.content.gsub('#', '%23')
46
- content.gsub!(quote, quote == "'" ? '"' : "'")
47
+ content = asset.content.dup
48
+ content.gsub!('#', '%23')
49
+ content.gsub!(quote, quote == '"' ? "&#34;" : '&#39;')
47
50
 
48
- content
49
- end
50
- end,
51
- )
51
+ content
52
+ end
53
+ end
52
54
  end
53
55
  end
@@ -1,21 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../asset')
4
3
  require_relative('html')
5
4
  require_relative('javascript')
6
5
 
7
6
  class Darkroom
8
- class Asset
9
- HTXDelegate = Delegate.new(
10
- content_type: JavaScriptDelegate.content_type,
11
- import_regex: HTMLDelegate.import_regex,
12
- reference_regex: HTMLDelegate.reference_regex,
13
- validate_reference: HTMLDelegate.validate_reference,
14
- reference_content: HTMLDelegate.reference_content,
15
- compile_lib: 'htx',
16
- compile: ->(path, content) { HTX.compile(path, content) },
17
- minify_lib: JavaScriptDelegate.minify_lib,
18
- minify: JavaScriptDelegate.minify,
19
- )
7
+ class HTXDelegate < HTMLDelegate
8
+ compile(lib: 'htx', delegate: JavaScriptDelegate) do |parse_data:, path:, own_content:|
9
+ module_supported = false
10
+
11
+ if defined?(HTX::VERSION)
12
+ major, minor, patch = HTX::VERSION.split('.').map(&:to_i)
13
+ module_supported = major > 0 || minor > 1 || (minor == 1 && patch >= 1)
14
+ end
15
+
16
+ if module_supported
17
+ HTX.compile(path, own_content, as_module: Darkroom.javascript_iife)
18
+ else
19
+ HTX.compile(path, own_content)
20
+ end
21
+ end
20
22
  end
21
23
  end
@@ -1,14 +1,187 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require('strscan')
3
4
  require_relative('../asset')
5
+ require_relative('../delegate')
4
6
 
5
7
  class Darkroom
6
- class Asset
7
- JavaScriptDelegate = Delegate.new(
8
- content_type: 'text/javascript',
9
- import_regex: /^ *import +#{QUOTED_PATH.source} *;? *(\n|$)/.freeze,
10
- minify_lib: 'terser',
11
- minify: ->(content) { Terser.compile(content) },
12
- )
8
+ class JavaScriptDelegate < Delegate
9
+ IDENTIFIER_REGEX = /[_$a-zA-Z][_$a-zA-Z0-9]*/.freeze
10
+ COMMA_REGEX = /,/.freeze
11
+ QUOTED_REGEX = /
12
+ (?<quoted>
13
+ (?<quote>['"])(
14
+ (?<=[^\\])\\(\\\\)*\k<quote> |
15
+ (?!\k<quote>).
16
+ )*\k<quote>
17
+ )
18
+ /x.freeze
19
+
20
+ content_type('text/javascript')
21
+
22
+ ########################################################################################################
23
+ ## Imports ##
24
+ ########################################################################################################
25
+
26
+ IMPORT_REGEX = /
27
+ (?<=^|;|})[^\S\n]*
28
+ import\s+(
29
+ ((?<default>#{IDENTIFIER_REGEX.source})(?:\s*,\s*|(?=\s+from\s+)))?
30
+ (
31
+ \*\s+as\s+(?<module>#{IDENTIFIER_REGEX.source}) |
32
+ \{(?<named>[\s\S]+?)\}
33
+ )?
34
+ \s+from\s+
35
+ )?#{Asset::QUOTED_PATH_REGEX.source}
36
+ [^\S\n]*;?[^\S\n]*(\n|\Z)
37
+ /x.freeze
38
+
39
+ IMPORT_ITEM_REGEX = /
40
+ \s*
41
+ (?<name>#{IDENTIFIER_REGEX.source}|#{QUOTED_REGEX.source}(?=\s+as\s+))
42
+ (\s+as\s+(?<alias>#{IDENTIFIER_REGEX.source}))?
43
+ \s*
44
+ /x.freeze
45
+
46
+ import(IMPORT_REGEX) do |parse_data:, match:, asset:|
47
+ items = []
48
+ items << [match[:default], '.default'] if match[:default]
49
+ items << [match[:module], ''] if match[:module]
50
+
51
+ if match[:named]
52
+ scanner = StringScanner.new(match[:named])
53
+
54
+ while scanner.scan(IMPORT_ITEM_REGEX)
55
+ items << [
56
+ scanner[:alias] || scanner[:name],
57
+ scanner[:quote] ? "[#{scanner[:name]}]" : ".#{scanner[:name]}"
58
+ ]
59
+
60
+ break unless scanner.scan(COMMA_REGEX)
61
+ end
62
+
63
+ error('Invalid import statement') unless scanner.eos?
64
+ end
65
+
66
+ ((parse_data[:imports] ||= {})[asset.path] ||= []).concat(items) unless items.empty?
67
+
68
+ ''
69
+ end
70
+
71
+ ########################################################################################################
72
+ ## Exports ##
73
+ ########################################################################################################
74
+
75
+ EXPORT_REGEX = /
76
+ (?<=^|;|})[^\S\n]*
77
+ export\s+(
78
+ (?<default>default\s+)?
79
+ (?<keep>(let|const|var|function\*?|class)\s+(?<name>#{IDENTIFIER_REGEX.source}))
80
+ |
81
+ \{(?<named>.+?)\}
82
+ [^\S\n]*;?[^\S\n]*(\n|\Z)
83
+ )
84
+ /x.freeze
85
+
86
+ EXPORT_ITEM_REGEX = /
87
+ \s*
88
+ (?<name>#{IDENTIFIER_REGEX.source})
89
+ (\s+as\s+(?<alias>#{IDENTIFIER_REGEX.source}|#{QUOTED_REGEX.source}))?
90
+ \s*
91
+ /x.freeze
92
+
93
+ parse(:export, EXPORT_REGEX) do |parse_data:, match:|
94
+ items = (parse_data[:exports] ||= [])
95
+
96
+ if match[:default]
97
+ items << ['default', match[:name]]
98
+ elsif match[:name]
99
+ items << [match[:name], match[:name]]
100
+ else
101
+ scanner = StringScanner.new(match[:named])
102
+
103
+ while scanner.scan(EXPORT_ITEM_REGEX)
104
+ items << [scanner[:alias] || scanner[:name], scanner[:name]]
105
+
106
+ break if scanner.eos?
107
+ break unless scanner.scan(COMMA_REGEX)
108
+ end
109
+
110
+ error('Invalid export statement') unless scanner.eos?
111
+ end
112
+
113
+ match[:keep]
114
+ end
115
+
116
+ ########################################################################################################
117
+ ## Compile ##
118
+ ########################################################################################################
119
+
120
+ compile do |parse_data:, path:, own_content:|
121
+ next unless Darkroom.javascript_iife
122
+
123
+ (parse_data[:imports] || []).each do |import_path, items|
124
+ mod_suffix = nil
125
+ prefix = '{ ' if items.size != 1
126
+ suffix = ' }' if items.size != 1
127
+
128
+ begin
129
+ mod = "m#{mod_suffix}"
130
+ mod_suffix = (mod_suffix || 1) + 1
131
+ end while items.any? { |i| i.first == mod }
132
+
133
+ own_content.prepend(
134
+ "let #{items.map(&:first).join(', ')}; "\
135
+ "$import('#{import_path}', "\
136
+ "#{mod} => #{prefix}#{items.map { |(i, e)| "#{i} = #{mod}#{e}" }.join(', ')}#{suffix})\n"
137
+ )
138
+ end
139
+
140
+ own_content.prepend("['#{path}', $import => {\n\n")
141
+ own_content << <<~EOS
142
+
143
+ return Object.seal({#{
144
+ if parse_data[:exports] && !parse_data[:exports].empty?
145
+ "\n#{parse_data[:exports].map { |k, v| " #{k}: #{v},\n" }.join}"
146
+ end
147
+ }})
148
+
149
+ }],
150
+ EOS
151
+ end
152
+
153
+ ########################################################################################################
154
+ ## Finalize ##
155
+ ########################################################################################################
156
+
157
+ finalize do |parse_data:, path:, content:|
158
+ next unless Darkroom.javascript_iife
159
+
160
+ (content.frozen? ? content.dup : content).prepend(
161
+ <<~EOS
162
+ ((...bundle) => {
163
+ const modules = {}
164
+ const setters = []
165
+ const $import = (name, setter) =>
166
+ modules[name] ? setter(modules[name]) : setters.push([setter, name])
167
+
168
+ for (const [name, def] of bundle)
169
+ modules[name] = def($import)
170
+
171
+ for (const [setter, name] of setters)
172
+ setter(modules[name])
173
+ })(
174
+
175
+ EOS
176
+ ) << "\n)\n"
177
+ end
178
+
179
+ ########################################################################################################
180
+ ## Minify ##
181
+ ########################################################################################################
182
+
183
+ minify(lib: 'terser') do |parse_data:, path:, content:|
184
+ Terser.compile(content)
185
+ end
13
186
  end
14
187
  end
@@ -10,10 +10,10 @@ class Darkroom
10
10
  ##
11
11
  # Creates a new instance.
12
12
  #
13
- # * +message+ - Description of the error.
14
- # * +detail+ - Additional detail about the error.
15
- # * +source_path+ - Path of the asset that contains the error (optional).
16
- # * +source_line_num+ - Line number in the asset where the error is located (optional).
13
+ # [message] Description of the error.
14
+ # [detail] Additional detail about the error.
15
+ # [source_path] Path of the asset that contains the error.
16
+ # [source_line_num] Line number in the asset where the error is located.
17
17
  #
18
18
  def initialize(message, detail, source_path = nil, source_line_num = nil)
19
19
  super(message)
@@ -11,9 +11,9 @@ class Darkroom
11
11
  ##
12
12
  # Creates a new instance.
13
13
  #
14
- # * +path+ - Path of asset that doesn't exist.
15
- # * +source_path+ - Path of the asset that contains the error (optional).
16
- # * +source_line_num+ - Line number in the asset where the error is located (optional).
14
+ # [path] Path of asset that doesn't exist.
15
+ # [source_path] Path of the asset that contains the error.
16
+ # [source_line_num] Line number in the asset where the error is located.
17
17
  #
18
18
  def initialize(path, source_path = nil, source_line_num = nil)
19
19
  super('Asset not found', path, source_path, source_line_num)
@@ -10,9 +10,9 @@ class Darkroom
10
10
  ##
11
11
  # Creates a new instance.
12
12
  #
13
- # * +snippet+ - Snippet showing the reference.
14
- # * +source_path+ - Path of the asset that contains the error.
15
- # * +source_line_num+ - Line number in the asset where the error is located.
13
+ # [snippet] Snippet showing the reference.
14
+ # [source_path] Path of the asset that contains the error.
15
+ # [source_line_num] Line number in the asset where the error is located.
16
16
  #
17
17
  def initialize(snippet, source_path, source_line_num)
18
18
  super('Reference would result in a circular reference chain', snippet, source_path, source_line_num)
@@ -10,9 +10,9 @@ class Darkroom
10
10
  ##
11
11
  # Creates a new instance.
12
12
  #
13
- # * +path+ - Path of the asset that exists under multiple load paths.
14
- # * +first_load_path+ - Load path where the asset was first found.
15
- # * +second_load_path+ - Load path where the asset was subsequently found.
13
+ # [path] Path of the asset that exists under multiple load paths.
14
+ # [first_load_path] Load path where the asset was first found.
15
+ # [second_load_path] Load path where the asset was subsequently found.
16
16
  #
17
17
  def initialize(path, first_load_path, second_load_path)
18
18
  @path = path
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative('../asset')
4
+
3
5
  class Darkroom
4
6
  ##
5
7
  # Error class used when an asset's path contains one or more invalid characters.
@@ -10,8 +12,8 @@ class Darkroom
10
12
  ##
11
13
  # Creates a new instance.
12
14
  #
13
- # * +path+ - Path of the asset with the invalid character(s).
14
- # * +index+ - Position of the first bad character in the path.
15
+ # [path] Path of the asset with the invalid character(s).
16
+ # [index] Position of the first bad character in the path.
15
17
  #
16
18
  def initialize(path, index)
17
19
  @path = path
@@ -22,7 +24,7 @@ class Darkroom
22
24
  # Returns a string representation of the error.
23
25
  #
24
26
  def to_s
25
- "Asset path contains one or more invalid characters (#{DISALLOWED_PATH_CHARS}): #{@path}"
27
+ "Asset path contains one or more invalid characters (#{Asset::DISALLOWED_PATH_CHARS}): #{@path}"
26
28
  end
27
29
  end
28
30
  end
@@ -10,9 +10,9 @@ class Darkroom
10
10
  ##
11
11
  # Creates a new instance.
12
12
  #
13
- # * +library+ - Name of the library that's missing.
14
- # * +need+ - Reason the library is needed ('compile' or 'minify').
15
- # * +extension+ - Extension of the type of asset that needs the library.
13
+ # [library] Name of the library that's missing.
14
+ # [need] Reason the library is needed ('pre-process', 'post-process', or 'minify').
15
+ # [extension] Extension of the type of asset that needs the library.
16
16
  #
17
17
  def initialize(library, need, extension)
18
18
  @library = library
@@ -8,7 +8,7 @@ class Darkroom
8
8
  ##
9
9
  # Creates a new instance.
10
10
  #
11
- # * +errors+ - Error or array of errors.
11
+ # [errors] Error or array of errors.
12
12
  #
13
13
  def initialize(errors)
14
14
  @errors = Array(errors)
@@ -18,7 +18,11 @@ class Darkroom
18
18
  # Returns a string representation of the error.
19
19
  #
20
20
  def to_s
21
- "Errors were encountered while processing assets:\n #{@errors.map(&:to_s).join("\n ")}"
21
+ lines = @errors.map do |error|
22
+ error.kind_of?(AssetError) ? error.to_s : "#{error.inspect} at #{error.backtrace[0]}"
23
+ end
24
+
25
+ "Errors were encountered while processing assets:\n #{lines.join("\n ")}"
22
26
  end
23
27
 
24
28
  ##
@@ -10,9 +10,9 @@ class Darkroom
10
10
  ##
11
11
  # Creates a new instance.
12
12
  #
13
- # * +file+ - File with the unrecognized extension.
14
- # * +source_path+ - Path of the asset that contains the error (optional).
15
- # * +source_line_num+ - Line number in the asset where the error is located (optional).
13
+ # [file] File with the unrecognized extension.
14
+ # [source_path] Path of the asset that contains the error.
15
+ # [source_line_num] Line number in the asset where the error is located.
16
16
  #
17
17
  def initialize(file, source_path = nil, source_line_num = nil)
18
18
  super('File extension not recognized', file, source_path, source_line_num)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Darkroom
4
- VERSION = '0.0.6'
4
+ VERSION = '0.0.7'
5
5
  end
data/lib/darkroom.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require('darkroom/asset')
4
4
  require('darkroom/darkroom')
5
+ require('darkroom/delegate')
5
6
  require('darkroom/version')
6
7
 
7
8
  require('darkroom/delegates/css')
@@ -9,12 +10,12 @@ require('darkroom/delegates/html')
9
10
  require('darkroom/delegates/htx')
10
11
  require('darkroom/delegates/javascript')
11
12
 
12
- Darkroom.register('.css', Darkroom::Asset::CSSDelegate)
13
- Darkroom.register('.htm', '.html', Darkroom::Asset::HTMLDelegate)
14
- Darkroom.register('.htx', Darkroom::Asset::HTXDelegate)
13
+ Darkroom.register('.css', Darkroom::CSSDelegate)
14
+ Darkroom.register('.htm', '.html', Darkroom::HTMLDelegate)
15
+ Darkroom.register('.htx', Darkroom::HTXDelegate)
15
16
  Darkroom.register('.ico', 'image/x-icon')
16
17
  Darkroom.register('.jpg', '.jpeg', 'image/jpeg')
17
- Darkroom.register('.js', Darkroom::Asset::JavaScriptDelegate)
18
+ Darkroom.register('.js', Darkroom::JavaScriptDelegate)
18
19
  Darkroom.register('.json', 'application/json')
19
20
  Darkroom.register('.png', 'image/png')
20
21
  Darkroom.register('.svg', 'image/svg+xml')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: darkroom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Pickens
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-03 00:00:00.000000000 Z
11
+ date: 2023-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -59,6 +59,7 @@ files:
59
59
  - lib/darkroom.rb
60
60
  - lib/darkroom/asset.rb
61
61
  - lib/darkroom/darkroom.rb
62
+ - lib/darkroom/delegate.rb
62
63
  - lib/darkroom/delegates/css.rb
63
64
  - lib/darkroom/delegates/html.rb
64
65
  - lib/darkroom/delegates/htx.rb
@@ -94,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubygems_version: 3.2.32
98
+ rubygems_version: 3.4.15
98
99
  signing_key:
99
100
  specification_version: 4
100
101
  summary: A fast, lightweight, and straightforward web asset management library.