klenod-build 0.0.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 +7 -0
- data/README.md +29 -0
- data/lib/klenod/build/asset.rb +235 -0
- data/lib/klenod/build/asset_compression.rb +58 -0
- data/lib/klenod/build/asset_generation_queue.rb +50 -0
- data/lib/klenod/build/cli/application.rb +108 -0
- data/lib/klenod/build/cli.rb +3 -0
- data/lib/klenod/build/config.rb +142 -0
- data/lib/klenod/build/context.rb +370 -0
- data/lib/klenod/build/dependency.rb +21 -0
- data/lib/klenod/build/errors.rb +175 -0
- data/lib/klenod/build/filesystem_resolver.rb +147 -0
- data/lib/klenod/build/graph/invalidator.rb +246 -0
- data/lib/klenod/build/graph.rb +864 -0
- data/lib/klenod/build/graphviz.rb +282 -0
- data/lib/klenod/build/hashing.rb +21 -0
- data/lib/klenod/build/invalidation_result.rb +55 -0
- data/lib/klenod/build/load_result.rb +7 -0
- data/lib/klenod/build/loaded_module.rb +38 -0
- data/lib/klenod/build/module_id.rb +100 -0
- data/lib/klenod/build/module_record.rb +22 -0
- data/lib/klenod/build/plugin.rb +35 -0
- data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
- data/lib/klenod/build/plugins/component_defaults.rb +12 -0
- data/lib/klenod/build/plugins/data_plugin.rb +117 -0
- data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
- data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
- data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
- data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
- data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
- data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
- data/lib/klenod/build/plugins/image_plugin.rb +401 -0
- data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
- data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
- data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
- data/lib/klenod/build/plugins/router_plugin.rb +942 -0
- data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
- data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
- data/lib/klenod/build/plugins.rb +22 -0
- data/lib/klenod/build/profiler.rb +79 -0
- data/lib/klenod/build/resolution_error_formatter.rb +42 -0
- data/lib/klenod/build/resolver.rb +95 -0
- data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
- data/lib/klenod/build/source_map/editor.rb +110 -0
- data/lib/klenod/build/source_map/map.rb +168 -0
- data/lib/klenod/build/source_map/vlq.rb +68 -0
- data/lib/klenod/build/source_map.rb +12 -0
- data/lib/klenod/build/transform_result.rb +12 -0
- data/lib/klenod/build/version.rb +7 -0
- data/lib/klenod/build/watched_pattern.rb +11 -0
- data/lib/klenod/build/watcher.rb +141 -0
- data/lib/klenod/build.rb +15 -0
- metadata +310 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "image_size"
|
|
4
|
+
require "json"
|
|
5
|
+
require "rmagick"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require_relative "../asset"
|
|
9
|
+
require_relative "../dependency"
|
|
10
|
+
require_relative "../hashing"
|
|
11
|
+
require_relative "../load_result"
|
|
12
|
+
require_relative "../plugin"
|
|
13
|
+
require_relative "../transform_result"
|
|
14
|
+
|
|
15
|
+
module Klenod
|
|
16
|
+
module Build
|
|
17
|
+
module Plugins
|
|
18
|
+
module ImagePlugin
|
|
19
|
+
def self.new(...)
|
|
20
|
+
Plugin.new(...)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Plugin < Klenod::Build::Plugin
|
|
24
|
+
EXTENSIONS = [".avif", ".gif", ".jpeg", ".jpg", ".png", ".webp"].freeze
|
|
25
|
+
IMAGE_RUNTIME_SPECIFIER = "virtual:klenod/image"
|
|
26
|
+
IMAGE_RUNTIME_MODULE_ID = ModuleId.new("#{IMAGE_RUNTIME_SPECIFIER}.rb", nil)
|
|
27
|
+
|
|
28
|
+
ImageDefaultKey = Data.define(:source_path, :source_hash, :format, :quality)
|
|
29
|
+
ImageVariantKey = Data.define(:source_path, :source_hash, :width, :format, :quality)
|
|
30
|
+
|
|
31
|
+
def initialize(widths: [], formats: nil)
|
|
32
|
+
@widths = widths
|
|
33
|
+
@formats = formats
|
|
34
|
+
@default_asset_cache = {}
|
|
35
|
+
@variant_cache = {}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolve(dependency, _context)
|
|
39
|
+
return nil unless dependency.specifier == IMAGE_RUNTIME_SPECIFIER
|
|
40
|
+
|
|
41
|
+
ResolvedDependency.new(dependency, IMAGE_RUNTIME_MODULE_ID, {virtual: true})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def load(module_id, context)
|
|
45
|
+
return image_runtime_source if module_id.scheme == :virtual && module_id == IMAGE_RUNTIME_MODULE_ID
|
|
46
|
+
return nil unless EXTENSIONS.include?(module_id.extname)
|
|
47
|
+
|
|
48
|
+
source_path = context.absolute_path(module_id)
|
|
49
|
+
source_hash = Hashing.file_hexdigest(source_path)
|
|
50
|
+
dimensions = image_dimensions(source_path)
|
|
51
|
+
image_options = image_options_for(module_id)
|
|
52
|
+
asset = default_image_asset(module_id, source_path, source_hash, dimensions, image_options, context.asset_generation_queue)
|
|
53
|
+
variant_assets = generate_variant_assets(module_id, source_path, source_hash, dimensions, image_options, context.asset_generation_queue)
|
|
54
|
+
javascript_asset = javascript_image_asset(module_id, asset, variant_assets)
|
|
55
|
+
assets = [asset, *variant_assets, javascript_asset]
|
|
56
|
+
image_runtime_dependency =
|
|
57
|
+
Dependency
|
|
58
|
+
.create(
|
|
59
|
+
specifier: IMAGE_RUNTIME_SPECIFIER,
|
|
60
|
+
importer_id: module_id,
|
|
61
|
+
kind: :image_runtime
|
|
62
|
+
)
|
|
63
|
+
.with(id: "#{module_id}:image_runtime")
|
|
64
|
+
|
|
65
|
+
transform =
|
|
66
|
+
TransformResult.new(
|
|
67
|
+
image_module_source(module_id, assets, image_runtime_dependency),
|
|
68
|
+
[image_runtime_dependency],
|
|
69
|
+
nil,
|
|
70
|
+
assets,
|
|
71
|
+
[],
|
|
72
|
+
{image_javascript_asset_path: javascript_asset.output_path}
|
|
73
|
+
)
|
|
74
|
+
LoadResult.new(
|
|
75
|
+
image_source(module_id),
|
|
76
|
+
source_hash,
|
|
77
|
+
transform
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def import_value(_resolved_dependency, record, context)
|
|
82
|
+
return nil unless EXTENSIONS.include?(record.id.extname)
|
|
83
|
+
|
|
84
|
+
context.mods.fetch(record.id).const_get(:Exports)::Default
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def runtime_import_value(_resolved_dependency, record, _context)
|
|
88
|
+
return Runtime::DefaultImport.new(:Default) if EXTENSIONS.include?(record.id.extname)
|
|
89
|
+
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
Dimensions = Data.define(:width, :height, :format)
|
|
96
|
+
|
|
97
|
+
def image_dimensions(path)
|
|
98
|
+
size = ImageSize.path(path)
|
|
99
|
+
Dimensions.new(size.width, size.height, size.format)
|
|
100
|
+
rescue ImageSize::FormatError
|
|
101
|
+
Dimensions.new(nil, nil, nil)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def default_image_asset(module_id, source_path, source_hash, dimensions, image_options, queue)
|
|
105
|
+
return static_image_asset(module_id, source_path, source_hash, dimensions) unless generated_default_image?(image_options)
|
|
106
|
+
|
|
107
|
+
format = default_image_format(module_id, image_options)
|
|
108
|
+
key = ImageDefaultKey.new(module_id.path, source_hash, format, image_options.quality)
|
|
109
|
+
@default_asset_cache[key] ||= generated_default_image_asset(module_id, source_path, source_hash, dimensions, format, image_options.quality, queue)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def generated_default_image?(image_options)
|
|
113
|
+
return true if image_options.explicit_formats && image_options.formats.any?
|
|
114
|
+
|
|
115
|
+
!image_options.quality.nil?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def default_image_format(module_id, image_options)
|
|
119
|
+
return image_options.formats.fetch(0) if image_options.explicit_formats && image_options.formats.any?
|
|
120
|
+
|
|
121
|
+
module_id.extname.delete_prefix(".")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def static_image_asset(module_id, source_path, source_hash, dimensions)
|
|
125
|
+
hash = source_hash[0, 16]
|
|
126
|
+
Asset.new(
|
|
127
|
+
module_id.path,
|
|
128
|
+
hash,
|
|
129
|
+
"/assets/#{asset_name(module_id)}.#{hash}#{module_id.extname}",
|
|
130
|
+
source_path,
|
|
131
|
+
nil,
|
|
132
|
+
content_type(module_id.extname),
|
|
133
|
+
{
|
|
134
|
+
type: :image,
|
|
135
|
+
width: dimensions.width,
|
|
136
|
+
height: dimensions.height,
|
|
137
|
+
format: dimensions.format
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def generated_default_image_asset(module_id, source_path, source_hash, dimensions, format, quality, queue)
|
|
143
|
+
format = format.downcase
|
|
144
|
+
extname = ".#{format}"
|
|
145
|
+
hash = Hashing.short("#{source_hash}:default:#{format}:#{quality}")
|
|
146
|
+
metadata = {
|
|
147
|
+
type: :image,
|
|
148
|
+
width: dimensions.width,
|
|
149
|
+
height: dimensions.height,
|
|
150
|
+
format: format.to_sym
|
|
151
|
+
}
|
|
152
|
+
metadata[:quality] = quality if quality
|
|
153
|
+
|
|
154
|
+
Asset.generated(
|
|
155
|
+
module_id.path,
|
|
156
|
+
hash,
|
|
157
|
+
"/assets/#{asset_name(module_id)}.#{hash}#{extname}",
|
|
158
|
+
source_path,
|
|
159
|
+
content_type(extname),
|
|
160
|
+
metadata,
|
|
161
|
+
writer: ->(io) { write_image_bytes(source_path, format, quality, io) },
|
|
162
|
+
queue: queue,
|
|
163
|
+
queue_kind: :cpu
|
|
164
|
+
) do
|
|
165
|
+
generate_image_bytes(source_path, format, quality:)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def generate_variant_assets(module_id, source_path, source_hash, dimensions, image_options, queue)
|
|
170
|
+
return [] if dimensions.width.nil? || dimensions.height.nil?
|
|
171
|
+
|
|
172
|
+
return [] if image_options.widths.empty?
|
|
173
|
+
|
|
174
|
+
image_options.formats.flat_map do |format|
|
|
175
|
+
image_options.widths.filter_map do |width|
|
|
176
|
+
next if width <= 0
|
|
177
|
+
|
|
178
|
+
key = ImageVariantKey.new(module_id.path, source_hash, width, format.downcase, image_options.quality)
|
|
179
|
+
@variant_cache[key] ||= variant_asset(module_id, source_path, dimensions, source_hash, width, format, image_options.quality, queue)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
ImageOptions = Data.define(:widths, :formats, :explicit_formats, :quality)
|
|
185
|
+
|
|
186
|
+
def image_runtime_source
|
|
187
|
+
<<~RUBY
|
|
188
|
+
Image =
|
|
189
|
+
Data.define(:src, :width, :height, :content_type, :variants) do
|
|
190
|
+
def srcset
|
|
191
|
+
return nil if variants.empty?
|
|
192
|
+
|
|
193
|
+
variants.map { "\#{it.src} \#{it.descriptor}" }.join(", ")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def sizes
|
|
197
|
+
display_width = variants.filter_map(&:width).max || width
|
|
198
|
+
return nil unless display_width
|
|
199
|
+
|
|
200
|
+
"(max-width: \#{display_width}px) 100vw, \#{display_width}px"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
ImageVariant = Data.define(:src, :width, :height, :content_type, :format, :descriptor, :metadata)
|
|
205
|
+
RUBY
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def image_source(module_id)
|
|
209
|
+
"# image asset: #{module_id}\n"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def image_module_source(module_id, assets, image_runtime_dependency)
|
|
213
|
+
asset = image_asset(assets)
|
|
214
|
+
variants = image_variant_assets(assets).map { |variant_asset| image_variant_source(variant_asset) }
|
|
215
|
+
|
|
216
|
+
<<~RUBY
|
|
217
|
+
ImageRuntime = __klenod_import__(#{image_runtime_dependency.id.inspect})
|
|
218
|
+
Default =
|
|
219
|
+
ImageRuntime::Image.new(
|
|
220
|
+
src: #{asset.output_path.inspect},
|
|
221
|
+
width: #{asset.metadata[:width].inspect},
|
|
222
|
+
height: #{asset.metadata[:height].inspect},
|
|
223
|
+
content_type: #{asset.content_type.inspect},
|
|
224
|
+
variants: [
|
|
225
|
+
#{variants.join(",\n ")}
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
RUBY
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def image_variant_source(asset)
|
|
233
|
+
<<~RUBY.chomp
|
|
234
|
+
ImageRuntime::ImageVariant.new(
|
|
235
|
+
src: #{asset.output_path.inspect},
|
|
236
|
+
width: #{asset.metadata[:width].inspect},
|
|
237
|
+
height: #{asset.metadata[:height].inspect},
|
|
238
|
+
content_type: #{asset.content_type.inspect},
|
|
239
|
+
format: #{asset.metadata[:format].inspect},
|
|
240
|
+
descriptor: #{asset.metadata[:descriptor].inspect},
|
|
241
|
+
metadata: #{asset.metadata.inspect}
|
|
242
|
+
)
|
|
243
|
+
RUBY
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def javascript_image_asset(module_id, asset, variant_assets)
|
|
247
|
+
code = javascript_image_module_source(asset, variant_assets)
|
|
248
|
+
hash = Hashing.short(code)
|
|
249
|
+
Asset.new(
|
|
250
|
+
module_id.path,
|
|
251
|
+
hash,
|
|
252
|
+
"#{asset.output_path}.js",
|
|
253
|
+
nil,
|
|
254
|
+
code,
|
|
255
|
+
"application/javascript",
|
|
256
|
+
{type: :image_javascript_metadata, image_metadata: true, image_asset_path: asset.output_path}
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def javascript_image_module_source(asset, variant_assets)
|
|
261
|
+
variants = variant_assets.map { |variant_asset| javascript_image_variant(variant_asset) }
|
|
262
|
+
srcset = variant_assets.empty? ? nil : variant_assets.map { "#{it.output_path} #{it.metadata[:descriptor]}" }.join(", ")
|
|
263
|
+
display_width = variant_assets.filter_map { it.metadata[:width] }.max || asset.metadata[:width]
|
|
264
|
+
sizes = display_width ? "(max-width: #{display_width}px) 100vw, #{display_width}px" : nil
|
|
265
|
+
metadata = {
|
|
266
|
+
src: asset.output_path,
|
|
267
|
+
width: asset.metadata[:width],
|
|
268
|
+
height: asset.metadata[:height],
|
|
269
|
+
contentType: asset.content_type,
|
|
270
|
+
variants: variants,
|
|
271
|
+
srcset: srcset,
|
|
272
|
+
sizes: sizes
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
"export default #{JSON.generate(metadata)};\n"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def javascript_image_variant(asset)
|
|
279
|
+
{
|
|
280
|
+
src: asset.output_path,
|
|
281
|
+
width: asset.metadata[:width],
|
|
282
|
+
height: asset.metadata[:height],
|
|
283
|
+
contentType: asset.content_type,
|
|
284
|
+
format: asset.metadata[:format],
|
|
285
|
+
descriptor: asset.metadata[:descriptor],
|
|
286
|
+
metadata: asset.metadata
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def image_asset(assets)
|
|
291
|
+
assets.find { it.metadata[:type] == :image } || assets.fetch(0)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def image_variant_assets(assets)
|
|
295
|
+
assets.select { it.metadata[:type] == :image_variant }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def image_options_for(module_id)
|
|
299
|
+
query = URI.decode_www_form(module_id.query || "").to_h
|
|
300
|
+
widths =
|
|
301
|
+
if query["width"]
|
|
302
|
+
query["width"].split(",").filter_map { |value| Integer(value, exception: false) }
|
|
303
|
+
else
|
|
304
|
+
@widths
|
|
305
|
+
end
|
|
306
|
+
explicit_formats = query.key?("format")
|
|
307
|
+
formats =
|
|
308
|
+
if explicit_formats
|
|
309
|
+
query["format"].split(",")
|
|
310
|
+
else
|
|
311
|
+
@formats || [module_id.extname.delete_prefix(".")]
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
ImageOptions.new(widths.uniq, formats.map(&:downcase).uniq, explicit_formats, image_quality(query["quality"]))
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def image_quality(value)
|
|
318
|
+
return nil if value.nil?
|
|
319
|
+
|
|
320
|
+
quality = Integer(value, exception: false)
|
|
321
|
+
return nil unless quality&.between?(1, 100)
|
|
322
|
+
|
|
323
|
+
quality
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def variant_asset(module_id, source_path, dimensions, source_hash, width, format, quality, queue)
|
|
327
|
+
format = format.downcase
|
|
328
|
+
extname = ".#{format}"
|
|
329
|
+
descriptor = "#{width}w"
|
|
330
|
+
hash = Hashing.short("#{source_hash}:#{width}:#{format}:#{quality}")
|
|
331
|
+
output_path = "/assets/#{asset_name(module_id)}.#{width}w.#{hash}#{extname}"
|
|
332
|
+
metadata = {
|
|
333
|
+
type: :image_variant,
|
|
334
|
+
width: width,
|
|
335
|
+
height: scaled_height(dimensions, width),
|
|
336
|
+
format: format.to_sym,
|
|
337
|
+
descriptor: descriptor,
|
|
338
|
+
source_width: width
|
|
339
|
+
}
|
|
340
|
+
metadata[:quality] = quality if quality
|
|
341
|
+
|
|
342
|
+
Asset.generated(
|
|
343
|
+
module_id.path,
|
|
344
|
+
hash,
|
|
345
|
+
output_path,
|
|
346
|
+
source_path,
|
|
347
|
+
content_type(extname),
|
|
348
|
+
metadata,
|
|
349
|
+
writer: ->(io) { write_variant_bytes(source_path, width, format, quality, io) },
|
|
350
|
+
queue: queue,
|
|
351
|
+
queue_kind: :cpu
|
|
352
|
+
) do
|
|
353
|
+
generate_image_bytes(source_path, format, width: width, quality:)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def generate_image_bytes(source_path, format, width: nil, quality: nil)
|
|
358
|
+
image = Magick::Image.read(source_path.to_s).first
|
|
359
|
+
output_image = width ? image.resize_to_fit(width) : image
|
|
360
|
+
output_image.to_blob do |info|
|
|
361
|
+
info.format = format.upcase
|
|
362
|
+
info.quality = quality if quality
|
|
363
|
+
end
|
|
364
|
+
ensure
|
|
365
|
+
output_image&.destroy! if output_image && output_image != image
|
|
366
|
+
image&.destroy!
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def write_variant_bytes(source_path, width, format, quality, io)
|
|
370
|
+
io.write(generate_image_bytes(source_path, format, width: width, quality:))
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def write_image_bytes(source_path, format, quality, io)
|
|
374
|
+
io.write(generate_image_bytes(source_path, format, quality:))
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def scaled_height(dimensions, width)
|
|
378
|
+
return nil if dimensions.width.nil? || dimensions.height.nil? || dimensions.width.zero?
|
|
379
|
+
|
|
380
|
+
(dimensions.height.to_f * width / dimensions.width).round
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def asset_name(module_id)
|
|
384
|
+
File.basename(module_id.path, module_id.extname).gsub(/[^A-Za-z0-9]+/, "_")
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def content_type(extname)
|
|
388
|
+
case extname
|
|
389
|
+
when ".avif" then "image/avif"
|
|
390
|
+
when ".gif" then "image/gif"
|
|
391
|
+
when ".jpeg", ".jpg" then "image/jpeg"
|
|
392
|
+
when ".png" then "image/png"
|
|
393
|
+
when ".webp" then "image/webp"
|
|
394
|
+
else "application/octet-stream"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "toml-rb"
|
|
4
|
+
|
|
5
|
+
require_relative "../plugin"
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Build
|
|
9
|
+
module Plugins
|
|
10
|
+
module IntlPlugin
|
|
11
|
+
def self.new(...)
|
|
12
|
+
Plugin.new(...)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Plugin < Klenod::Build::Plugin
|
|
16
|
+
INTL_FILE_RE = /\.intl\.(?<locale>[^\/]+)\.toml\z/
|
|
17
|
+
|
|
18
|
+
def translations_for(context, module_id)
|
|
19
|
+
base = module_id.path.delete_suffix(module_id.extname)
|
|
20
|
+
pattern = context.absolute_path(ModuleId.new("#{base}.intl.*.toml", nil)).to_s
|
|
21
|
+
|
|
22
|
+
Dir
|
|
23
|
+
.glob(pattern)
|
|
24
|
+
.sort
|
|
25
|
+
.to_h do |path|
|
|
26
|
+
locale = File.basename(path).match(INTL_FILE_RE)[:locale]
|
|
27
|
+
[locale, TomlRB.load_file(path)]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
previous_verbose = $VERBOSE
|
|
4
|
+
begin
|
|
5
|
+
$VERBOSE = nil
|
|
6
|
+
require "kramdown"
|
|
7
|
+
require "kramdown-parser-gfm"
|
|
8
|
+
ensure
|
|
9
|
+
$VERBOSE = previous_verbose
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Klenod
|
|
13
|
+
module Build
|
|
14
|
+
module Plugins
|
|
15
|
+
class MarkdownCompiler
|
|
16
|
+
ESCAPED_INTERPOLATION_SENTINEL = "\uE000klenod_escaped_interpolation\uE000"
|
|
17
|
+
|
|
18
|
+
def initialize(factory:, components_source: "{}")
|
|
19
|
+
@factory = factory
|
|
20
|
+
@components_source = components_source
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def compile(source, interpolate: false)
|
|
24
|
+
source = protect_escaped_interpolation(source) if interpolate
|
|
25
|
+
document = Kramdown::Document.new(source, input: "GFM")
|
|
26
|
+
compile_children(document.root.children, interpolate: interpolate)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def compile_children(children, parent: nil, interpolate: false)
|
|
32
|
+
expressions = compile_child_expressions(children, parent: parent, interpolate: interpolate)
|
|
33
|
+
return "nil" if expressions.empty?
|
|
34
|
+
return expressions.fetch(0) if expressions.length == 1
|
|
35
|
+
|
|
36
|
+
"[#{expressions.join(", ")}]"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def compile_child_expressions(children, parent:, interpolate:, table_section: nil)
|
|
40
|
+
children.flat_map { |child| compile_node(child, parent: parent, table_section: table_section, interpolate: interpolate) }.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compile_node(node, parent: nil, table_section: nil, interpolate: false)
|
|
44
|
+
case node.type
|
|
45
|
+
when :root
|
|
46
|
+
[compile_children(node.children, parent: node, interpolate: interpolate)]
|
|
47
|
+
when :blank
|
|
48
|
+
nil
|
|
49
|
+
when :text
|
|
50
|
+
text_expressions(node.value, interpolate: interpolate)
|
|
51
|
+
when :p
|
|
52
|
+
return compile_child_expressions(node.children, parent: node, interpolate: interpolate) if node.options[:transparent]
|
|
53
|
+
|
|
54
|
+
[factory_call(:p, node.children, attrs: node.attr, parent: node, interpolate: interpolate)]
|
|
55
|
+
when :header
|
|
56
|
+
[factory_call(:"h#{node.options.fetch(:level)}", node.children, attrs: node.attr, parent: node, interpolate: interpolate)]
|
|
57
|
+
when :em, :strong, :a, :blockquote, :ul, :ol, :li, :table
|
|
58
|
+
[factory_call(node.type, node.children, attrs: node.attr, parent: node, interpolate: interpolate)]
|
|
59
|
+
when :thead, :tbody
|
|
60
|
+
[factory_call(node.type, node.children, attrs: node.attr, parent: node, table_section: node.type, interpolate: interpolate)]
|
|
61
|
+
when :tr
|
|
62
|
+
[factory_call(node.type, node.children, attrs: node.attr, parent: node, table_section: table_section, interpolate: interpolate)]
|
|
63
|
+
when :td
|
|
64
|
+
[factory_call((table_section == :thead) ? :th : :td, node.children, attrs: node.attr, parent: node, table_section: table_section, interpolate: interpolate)]
|
|
65
|
+
when :img, :hr, :br
|
|
66
|
+
[factory_call(node.type, [], attrs: node.attr, parent: node, interpolate: interpolate)]
|
|
67
|
+
when :codespan
|
|
68
|
+
[factory_call(:code, [node], raw_text: node.value, attrs: node.attr, parent: node, interpolate: interpolate)]
|
|
69
|
+
when :codeblock
|
|
70
|
+
code = factory_call(:code, [node], raw_text: node.value, attrs: node.attr, parent: node, interpolate: interpolate)
|
|
71
|
+
[factory_call(:pre, [], child_sources: [code], parent: node, interpolate: interpolate)]
|
|
72
|
+
when :html_element
|
|
73
|
+
[factory_call(node.value.to_sym, node.children, attrs: node.attr, parent: node, interpolate: interpolate)]
|
|
74
|
+
when :raw
|
|
75
|
+
[restore_escaped_interpolation(node.value.to_s).inspect]
|
|
76
|
+
else
|
|
77
|
+
[compile_children(node.children, parent: node, interpolate: interpolate)]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def factory_call(tag, children, parent:, attrs: {}, raw_text: nil, child_sources: nil, table_section: nil, interpolate: false)
|
|
82
|
+
child_sources ||=
|
|
83
|
+
if raw_text.nil?
|
|
84
|
+
compile_child_expressions(children, parent: parent, table_section: table_section, interpolate: interpolate)
|
|
85
|
+
else
|
|
86
|
+
[restore_escaped_interpolation(raw_text).inspect]
|
|
87
|
+
end
|
|
88
|
+
tag_source = tag_source(tag)
|
|
89
|
+
parts = [tag_source, *child_sources]
|
|
90
|
+
parts << "**#{attrs_source(attrs)}" unless attrs.empty?
|
|
91
|
+
|
|
92
|
+
"#{@factory}[#{parts.join(", ")}]"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def tag_source(tag)
|
|
96
|
+
"#{@components_source}.fetch(#{tag.inspect}, #{tag.inspect})"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def attrs_source(attrs)
|
|
100
|
+
attrs.transform_keys(&:to_sym).inspect
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def text_expressions(text, interpolate:)
|
|
104
|
+
return [restore_escaped_interpolation(text).inspect] unless interpolate && text.include?("\#{")
|
|
105
|
+
|
|
106
|
+
expressions = []
|
|
107
|
+
buffer = +""
|
|
108
|
+
index = 0
|
|
109
|
+
|
|
110
|
+
while index < text.length
|
|
111
|
+
if text[index, 2] == "\#{"
|
|
112
|
+
expression, next_index = read_interpolation(text, index + 2)
|
|
113
|
+
if expression
|
|
114
|
+
expressions << restore_escaped_interpolation(buffer).inspect unless buffer.empty?
|
|
115
|
+
buffer.clear
|
|
116
|
+
expressions << "(#{expression})"
|
|
117
|
+
index = next_index
|
|
118
|
+
else
|
|
119
|
+
buffer << text[index]
|
|
120
|
+
index += 1
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
buffer << text[index]
|
|
124
|
+
index += 1
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
expressions << restore_escaped_interpolation(buffer).inspect unless buffer.empty?
|
|
129
|
+
expressions
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def read_interpolation(text, index)
|
|
133
|
+
expression = +""
|
|
134
|
+
depth = 1
|
|
135
|
+
quote = nil
|
|
136
|
+
escaped = false
|
|
137
|
+
|
|
138
|
+
while index < text.length
|
|
139
|
+
char = text[index]
|
|
140
|
+
|
|
141
|
+
if quote
|
|
142
|
+
expression << char
|
|
143
|
+
if escaped
|
|
144
|
+
escaped = false
|
|
145
|
+
elsif char == "\\"
|
|
146
|
+
escaped = true
|
|
147
|
+
elsif char == quote
|
|
148
|
+
quote = nil
|
|
149
|
+
end
|
|
150
|
+
else
|
|
151
|
+
case char
|
|
152
|
+
when "\"", "'", "`"
|
|
153
|
+
quote = char
|
|
154
|
+
when "{"
|
|
155
|
+
depth += 1
|
|
156
|
+
when "}"
|
|
157
|
+
depth -= 1
|
|
158
|
+
return [expression.strip, index + 1] if depth.zero?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
expression << char
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
index += 1
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def protect_escaped_interpolation(source)
|
|
171
|
+
source.gsub("\\\#{", ESCAPED_INTERPOLATION_SENTINEL)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def restore_escaped_interpolation(text)
|
|
175
|
+
text.gsub(ESCAPED_INTERPOLATION_SENTINEL, "\#{")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|