jekyll-maths-latex-to-image 0.1.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
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 40fa3a5d43a4d56e7665d77049e931cdbc0342ee31995eb108e28e1cc6ec8f60
|
4
|
+
data.tar.gz: ab6bb61286ad507646a13d467780d01f528451e7aa7f171e766ced590f1694e2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 271edbc4ca16c37bfddf751215b6e078c4005751f2dddc52ebc13fa010f99a6bf53f50894679c2691f000b204d406c69be1c4519b1bd40b5a4b2e64cfa7e10e4
|
7
|
+
data.tar.gz: 0c84b3048409ad729c54a3d74cbdc8d3532fc3f82057e866aff827aa6f963dd6a8228cd2079886b32e225691c5d9a217ed2f027f50e8d3a979e29f0920934f8a
|
@@ -0,0 +1,541 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'parallel'
|
4
|
+
require 'logger'
|
5
|
+
require 'digest'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'open3'
|
8
|
+
require 'jimson'
|
9
|
+
begin
|
10
|
+
require 'mutex_m'
|
11
|
+
rescue LoadError
|
12
|
+
# For Ruby 3.4+ where mutex_m is not a default gem
|
13
|
+
gem 'mutex_m'
|
14
|
+
require 'mutex_m'
|
15
|
+
end
|
16
|
+
|
17
|
+
class NodeRendererClient
|
18
|
+
include Mutex_m # Mix in mutex capabilities
|
19
|
+
|
20
|
+
def initialize(logger)
|
21
|
+
super() # Initialize mutex
|
22
|
+
@logger = logger
|
23
|
+
@request_id = 0
|
24
|
+
start_renderer
|
25
|
+
rescue => e
|
26
|
+
@logger.error("Failed to initialize renderer: #{e.message}")
|
27
|
+
@logger.error(e.backtrace.join("\n"))
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
|
31
|
+
def start_renderer
|
32
|
+
user_dir = Dir.pwd # Get user's current working directory
|
33
|
+
node_path = File.join(user_dir, 'node_modules')
|
34
|
+
|
35
|
+
# Set environment with node_path
|
36
|
+
env = ENV.to_h.merge({
|
37
|
+
'NODE_PATH' => node_path
|
38
|
+
})
|
39
|
+
|
40
|
+
@logger.info("Node renderer NODE_PATH: #{node_path}")
|
41
|
+
@logger.info("Node renderer working directory: #{user_dir}")
|
42
|
+
|
43
|
+
# Use npx with updated command name
|
44
|
+
command = "npx jekyll-maths-tex2svg"
|
45
|
+
|
46
|
+
# Capture stderr separately for better error logging
|
47
|
+
@stdin, @stdout, @stderr, @wait_thr = Open3.popen3(env, command, chdir: user_dir)
|
48
|
+
|
49
|
+
# Rest of the start_renderer method remains the same
|
50
|
+
@stderr_thread = Thread.new do
|
51
|
+
while line = @stderr.gets
|
52
|
+
@logger.error("Node renderer stderr: #{line.strip}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Verify the renderer is working
|
57
|
+
begin
|
58
|
+
response = send_request('ping', nil)
|
59
|
+
unless response['result'] == 'pong'
|
60
|
+
raise "Renderer verification failed: #{response.inspect}"
|
61
|
+
end
|
62
|
+
@logger.info("Node renderer verified and ready")
|
63
|
+
rescue => e
|
64
|
+
@logger.error("Renderer startup failed: #{e.message}")
|
65
|
+
cleanup
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
rescue => e
|
69
|
+
@logger.error("Failed to verify renderer connection: #{e.message}")
|
70
|
+
cleanup
|
71
|
+
raise
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_svg(latex, display_mode: false, options: {})
|
75
|
+
synchronize do # Synchronize the entire request-response cycle
|
76
|
+
params = {
|
77
|
+
latex: latex.strip,
|
78
|
+
display: display_mode,
|
79
|
+
svgOptions: {
|
80
|
+
width: 'auto',
|
81
|
+
ex: 6,
|
82
|
+
em: 6,
|
83
|
+
containerWidth: display_mode ? 600 : 300
|
84
|
+
}.merge(options)
|
85
|
+
}
|
86
|
+
|
87
|
+
response = send_request('renderMath', params)
|
88
|
+
|
89
|
+
if response['error']
|
90
|
+
raise "Render error: #{response['error']['message']}"
|
91
|
+
end
|
92
|
+
|
93
|
+
if response['result'] && response['result']['svg']
|
94
|
+
Base64.decode64(response['result']['svg'])
|
95
|
+
else
|
96
|
+
raise "Invalid response format"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def cleanup
|
102
|
+
@stdin&.close
|
103
|
+
@stdout&.close
|
104
|
+
@stderr&.close
|
105
|
+
@stderr_thread&.kill
|
106
|
+
@stderr_thread&.join
|
107
|
+
Process.kill('TERM', @wait_thr.pid) if @wait_thr&.pid
|
108
|
+
@wait_thr&.join
|
109
|
+
rescue => e
|
110
|
+
@logger.error("Error during cleanup: #{e.message}")
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def send_request(method, params)
|
116
|
+
request = {
|
117
|
+
jsonrpc: '2.0',
|
118
|
+
method: method,
|
119
|
+
params: params,
|
120
|
+
id: next_request_id
|
121
|
+
}
|
122
|
+
|
123
|
+
@logger.debug("→ #{method}: #{params.inspect}")
|
124
|
+
@stdin.puts(request.to_json)
|
125
|
+
@stdin.flush
|
126
|
+
|
127
|
+
response = @stdout.gets
|
128
|
+
raise "No response from renderer" if response.nil?
|
129
|
+
|
130
|
+
parsed = JSON.parse(response)
|
131
|
+
@logger.debug("← #{parsed.inspect}")
|
132
|
+
|
133
|
+
if parsed['error']
|
134
|
+
error = parsed['error']
|
135
|
+
@logger.error("Render error (#{error['code']}): #{error['message']}")
|
136
|
+
if error['data']
|
137
|
+
@logger.error("Stack trace: #{error['data']}")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
parsed
|
142
|
+
end
|
143
|
+
|
144
|
+
def next_request_id
|
145
|
+
@request_id += 1
|
146
|
+
"req_#{@request_id}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
module Jekyll
|
151
|
+
module MathsLatexToImage
|
152
|
+
class Generator < Jekyll::Generator # Changed from MathRenderer to MathsLatexToImage
|
153
|
+
priority :highest
|
154
|
+
|
155
|
+
def initialize(config = nil)
|
156
|
+
super()
|
157
|
+
setup_logging
|
158
|
+
setup_debug_dir
|
159
|
+
@config = config
|
160
|
+
@svg_cache = {}
|
161
|
+
|
162
|
+
# Register the post_write hook during initialization
|
163
|
+
Jekyll::Hooks.register :site, :post_write do |site|
|
164
|
+
@console_logger.info("Post-write hook triggered")
|
165
|
+
begin
|
166
|
+
copy_svg_files(site) # First copy SVG files
|
167
|
+
convert_to_png(site) # Then do conversion to PNG
|
168
|
+
@console_logger.info("Post-write tasks completed successfully")
|
169
|
+
rescue => e
|
170
|
+
@console_logger.error("Post-write tasks failed: #{e.message}")
|
171
|
+
@console_logger.error(e.backtrace.join("\n"))
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
@@instance = self # Store instance for hook access
|
176
|
+
|
177
|
+
# Updated regex patterns
|
178
|
+
@display_math_pattern = /\$\$(.*?)\$\$/m
|
179
|
+
@inline_math_pattern = /(?<!\\)\$([^\$]+?)\$/ # Changed pattern
|
180
|
+
|
181
|
+
@renderer = NodeRendererClient.new(@console_logger)
|
182
|
+
rescue => e
|
183
|
+
@console_logger.error("Failed to initialize renderer: #{e.message}")
|
184
|
+
raise
|
185
|
+
end
|
186
|
+
|
187
|
+
def generate(site)
|
188
|
+
@site = site
|
189
|
+
setup_svg_dir
|
190
|
+
setup_temp_dir
|
191
|
+
start_time = Time.now
|
192
|
+
|
193
|
+
@svg_files = {} # Map of temp paths to final paths
|
194
|
+
process_documents(site)
|
195
|
+
|
196
|
+
@console_logger.info("Math rendering completed in #{Time.now - start_time.round(2)} seconds")
|
197
|
+
end
|
198
|
+
|
199
|
+
def convert_to_png(site)
|
200
|
+
build_dir = site.dest
|
201
|
+
user_dir = Dir.pwd
|
202
|
+
node_path = File.join(user_dir, 'node_modules')
|
203
|
+
|
204
|
+
@console_logger.info("Starting PNG conversion from: #{build_dir}")
|
205
|
+
@console_logger.info("PNG converter NODE_PATH: #{node_path}")
|
206
|
+
@console_logger.info("PNG converter working directory: #{user_dir}")
|
207
|
+
|
208
|
+
# Set environment with node_path
|
209
|
+
env = ENV.to_h.merge({
|
210
|
+
'NODE_PATH' => node_path
|
211
|
+
})
|
212
|
+
|
213
|
+
# Use npx with updated command name
|
214
|
+
command = "npx jekyll-maths-svg2png \"#{build_dir}\""
|
215
|
+
|
216
|
+
@console_logger.debug("Running command: #{command}")
|
217
|
+
output, status = Open3.capture2e(env, command, chdir: user_dir)
|
218
|
+
|
219
|
+
if status.success?
|
220
|
+
@console_logger.info("PNG conversion complete")
|
221
|
+
@console_logger.debug(output) if ENV['DEBUG']
|
222
|
+
else
|
223
|
+
@console_logger.error("PNG conversion failed:")
|
224
|
+
@console_logger.error(output)
|
225
|
+
raise "PNG conversion failed: #{output}"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# New method to handle post-write tasks
|
230
|
+
def post_write_tasks(site)
|
231
|
+
begin
|
232
|
+
convert_to_png(site) # Do PNG conversion first
|
233
|
+
copy_svg_files(site) # Then copy both SVG and PNG files
|
234
|
+
rescue => e
|
235
|
+
@console_logger.error("Post-write tasks failed: #{e.message}")
|
236
|
+
@console_logger.error(e.backtrace.join("\n"))
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
def setup_logging
|
243
|
+
log_dir = File.join(Dir.pwd, '_logs', 'math')
|
244
|
+
FileUtils.mkdir_p(log_dir)
|
245
|
+
|
246
|
+
@rpc_logger = Logger.new(
|
247
|
+
File.open(File.join(log_dir, "math-rpc-#{Time.now.strftime('%Y-%m-%d')}.log"), 'a')
|
248
|
+
)
|
249
|
+
@rpc_logger.formatter = proc do |_, _, _, msg|
|
250
|
+
"#{msg}\n"
|
251
|
+
end
|
252
|
+
|
253
|
+
# Minimal console logging
|
254
|
+
@console_logger = Logger.new(STDOUT)
|
255
|
+
@console_logger.formatter = proc do |severity, _, _, msg|
|
256
|
+
next unless severity == "ERROR" || ENV["DEBUG"]
|
257
|
+
"[#{severity}] #{msg}\n"
|
258
|
+
end
|
259
|
+
@console_logger.level = ENV["DEBUG"] ? Logger::DEBUG : Logger::ERROR
|
260
|
+
end
|
261
|
+
|
262
|
+
def setup_debug_dir
|
263
|
+
@debug_dir = File.join(Dir.pwd, '_debug', 'math', 'ruby')
|
264
|
+
FileUtils.mkdir_p(@debug_dir)
|
265
|
+
@console_logger.info("Debug directory: #{@debug_dir}")
|
266
|
+
end
|
267
|
+
|
268
|
+
def setup_svg_dir
|
269
|
+
# Use configurable path from _config.yml or default
|
270
|
+
base_path = @site.config.dig('texsvg_math_renderer', 'path') || '/assets/img/math'
|
271
|
+
@svg_url_path = File.join(@site.config['baseurl'].to_s, base_path)
|
272
|
+
end
|
273
|
+
|
274
|
+
def setup_temp_dir
|
275
|
+
# Create temporary directory for SVGs during generation
|
276
|
+
@temp_svg_dir = File.join(Dir.tmpdir, "jekyll-math-#{Time.now.to_i}")
|
277
|
+
FileUtils.mkdir_p(@temp_svg_dir)
|
278
|
+
@console_logger.info("Created temp SVG directory: #{@temp_svg_dir}")
|
279
|
+
end
|
280
|
+
|
281
|
+
def has_math?(content)
|
282
|
+
content =~ @display_math_pattern || content =~ @inline_math_pattern
|
283
|
+
end
|
284
|
+
|
285
|
+
def convert_math(page)
|
286
|
+
return unless has_math?(page.content)
|
287
|
+
|
288
|
+
begin
|
289
|
+
setup_page_svg_dir(page)
|
290
|
+
|
291
|
+
# Add debug logging for inline math
|
292
|
+
page.content.scan(@inline_math_pattern) do |match|
|
293
|
+
@console_logger.debug("Found inline math: #{match[0]}")
|
294
|
+
end
|
295
|
+
|
296
|
+
# Process display math first
|
297
|
+
page.content = page.content.gsub(@display_math_pattern) do |match|
|
298
|
+
math = $1.strip
|
299
|
+
next match if math.empty?
|
300
|
+
render_math(math, true)
|
301
|
+
end
|
302
|
+
|
303
|
+
# Process inline math with debug logging
|
304
|
+
page.content = page.content.gsub(@inline_math_pattern) do |match|
|
305
|
+
math = $1.strip
|
306
|
+
@console_logger.debug("Processing inline math: #{math}")
|
307
|
+
next match if math.empty?
|
308
|
+
render_math(math, false)
|
309
|
+
end
|
310
|
+
rescue => e
|
311
|
+
@console_logger.error("Error processing #{page.path}: #{e.message}")
|
312
|
+
raise e
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def setup_page_svg_dir(page)
|
317
|
+
# Get clean URL path for the page
|
318
|
+
rel_path = if page.url
|
319
|
+
# Remove leading/trailing slashes and .html
|
320
|
+
page.url.gsub(/^\/|\/$|\.html$/, '')
|
321
|
+
else
|
322
|
+
page.path.sub(/\.[^.]+$/, '').sub(/^\//, '')
|
323
|
+
end
|
324
|
+
|
325
|
+
# Create temp subdirectory for this page's SVGs
|
326
|
+
@current_temp_dir = File.join(@temp_svg_dir, rel_path)
|
327
|
+
FileUtils.mkdir_p(@current_temp_dir)
|
328
|
+
|
329
|
+
# Store the final URL path
|
330
|
+
@current_url_path = File.join(@svg_url_path, rel_path)
|
331
|
+
|
332
|
+
@console_logger.info("Page URL path: #{@current_url_path}")
|
333
|
+
@console_logger.info("Temp directory: #{@current_temp_dir}")
|
334
|
+
end
|
335
|
+
|
336
|
+
def escape_latex(latex)
|
337
|
+
# More comprehensive LaTeX escaping
|
338
|
+
latex.gsub(/\\/, '\\\\') # Escape backslashes first
|
339
|
+
.gsub(/\{/, '\\{') # Escape curly braces
|
340
|
+
.gsub(/\}/, '\\}') # Fixed closing brace
|
341
|
+
.gsub(/\$/, '\\$') # Escape dollar signs
|
342
|
+
.gsub(/&/, '\\&') # Escape ampersands
|
343
|
+
.gsub(/\#/, '\\#') # Escape hash
|
344
|
+
.gsub(/\^/, '\\^') # Escape carets
|
345
|
+
.gsub(/\_/, '\\_') # Escape underscores
|
346
|
+
.gsub(/\%/, '\\%') # Escape percent signs
|
347
|
+
.gsub(/~/, '\\~{}') # Escape tildes
|
348
|
+
end
|
349
|
+
|
350
|
+
def render_math(math, display_mode)
|
351
|
+
cache_key = "#{math}:#{display_mode}"
|
352
|
+
return @svg_cache[cache_key] if @svg_cache.key?(cache_key)
|
353
|
+
|
354
|
+
begin
|
355
|
+
cleaned_math = clean_math_input(math)
|
356
|
+
svg = @renderer.get_svg(cleaned_math, display_mode: display_mode)
|
357
|
+
svg_path = save_svg(math, svg, SecureRandom.uuid)
|
358
|
+
png_path = svg_path.sub(/\.svg$/, '.png')
|
359
|
+
|
360
|
+
# Get format preference from config
|
361
|
+
format = @site.config.dig('texsvg_math_renderer', 'format') || 'both'
|
362
|
+
|
363
|
+
# Build srcset based on format
|
364
|
+
srcset = case format
|
365
|
+
when 'svg'
|
366
|
+
"#{svg_path}"
|
367
|
+
when 'png'
|
368
|
+
"#{png_path}"
|
369
|
+
else # 'both'
|
370
|
+
"#{svg_path}, #{png_path}"
|
371
|
+
end
|
372
|
+
|
373
|
+
# Default source is PNG for better compatibility
|
374
|
+
src = format == 'svg' ? svg_path : png_path
|
375
|
+
|
376
|
+
# Build the common part of the img tag
|
377
|
+
img_class = "math-#{display_mode ? 'block' : 'inline'}"
|
378
|
+
img_tag = "<img src=\"#{src}\" "\
|
379
|
+
"srcset=\"#{srcset}\" "\
|
380
|
+
"alt=\"#{html_escape(math)}\" "\
|
381
|
+
"class=\"#{img_class}\""
|
382
|
+
|
383
|
+
# For inline math add a style override
|
384
|
+
unless display_mode
|
385
|
+
img_tag += " style=\"font-size:1.3em\""
|
386
|
+
end
|
387
|
+
img_tag += ">"
|
388
|
+
|
389
|
+
html = display_mode ?
|
390
|
+
"<div class=\"math-block\">#{img_tag}</div>" :
|
391
|
+
"<span class=\"math-inline\">#{img_tag}</span>"
|
392
|
+
|
393
|
+
html = html.html_safe if html.respond_to?(:html_safe)
|
394
|
+
@svg_cache[cache_key] = html
|
395
|
+
html
|
396
|
+
rescue => e
|
397
|
+
@console_logger.error("Math Processing Error:")
|
398
|
+
@console_logger.error(" Input: #{math.inspect}")
|
399
|
+
@console_logger.error(" Error: #{e.message}")
|
400
|
+
"<span class='math-error' title='#{html_escape(e.message)}'>#{html_escape(math)}</span>"
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def save_svg(latex, svg, id)
|
405
|
+
begin
|
406
|
+
hash = Digest::MD5.hexdigest(latex)[0..7]
|
407
|
+
sanitized_latex = latex.gsub(/[^a-zA-Z0-9]/, '_')[0..19]
|
408
|
+
filename = "math_#{sanitized_latex}-#{hash}-#{id}.svg"
|
409
|
+
|
410
|
+
# Save to temp location
|
411
|
+
temp_path = File.join(@current_temp_dir, filename)
|
412
|
+
FileUtils.mkdir_p(@current_temp_dir)
|
413
|
+
File.write(temp_path, svg)
|
414
|
+
|
415
|
+
# Calculate final path
|
416
|
+
final_url = File.join(@current_url_path, filename)
|
417
|
+
final_path = File.join(@site.dest, final_url.sub(/^\//, ''))
|
418
|
+
|
419
|
+
# Store mapping for post-write hook
|
420
|
+
@svg_files[temp_path] = final_path
|
421
|
+
|
422
|
+
# Only log errors, not successes
|
423
|
+
@rpc_logger.debug("SVG: #{filename}")
|
424
|
+
|
425
|
+
# Return URL path for HTML
|
426
|
+
final_url
|
427
|
+
rescue => e
|
428
|
+
@console_logger.error("Error saving SVG: #{e.message}")
|
429
|
+
raise
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def copy_svg_files(site)
|
434
|
+
@svg_files.each do |temp_path, final_path|
|
435
|
+
begin
|
436
|
+
FileUtils.mkdir_p(File.dirname(final_path))
|
437
|
+
FileUtils.cp(temp_path, final_path)
|
438
|
+
rescue => e
|
439
|
+
@console_logger.error("Failed to copy #{temp_path} to #{final_path}: #{e.message}")
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
FileUtils.rm_rf(@temp_svg_dir)
|
444
|
+
end
|
445
|
+
|
446
|
+
def cleanup
|
447
|
+
@renderer.cleanup
|
448
|
+
end
|
449
|
+
|
450
|
+
def clean_math_input(math)
|
451
|
+
original = math.dup
|
452
|
+
cleaned = math
|
453
|
+
# .gsub(/\\boxed\{/, '{\\boxed{') # Fix boxed command grouping
|
454
|
+
.gsub(/\s+/, ' ') # Normalize spaces
|
455
|
+
|
456
|
+
#.gsub(/[^\x00-\x7F]/, '') # Remove non-ASCII
|
457
|
+
#.strip
|
458
|
+
|
459
|
+
@console_logger.debug("Math cleaning steps:")
|
460
|
+
@console_logger.debug(" 1. Original: #{original.inspect}")
|
461
|
+
@console_logger.debug(" 2. Cleaned: #{cleaned.inspect}")
|
462
|
+
@console_logger.debug(" 3. Changed: #{original != cleaned}")
|
463
|
+
|
464
|
+
cleaned
|
465
|
+
end
|
466
|
+
|
467
|
+
def html_escape(str)
|
468
|
+
str.gsub(/[&<>"']/, {
|
469
|
+
'&' => '&',
|
470
|
+
'<' => '<',
|
471
|
+
'>' => '>',
|
472
|
+
'"' => '"',
|
473
|
+
"'" => '''
|
474
|
+
})
|
475
|
+
end
|
476
|
+
|
477
|
+
# Remove old logging methods
|
478
|
+
def log_error(message, data = {})
|
479
|
+
@console_logger.error("#{message} #{data.inspect unless data.empty()}")
|
480
|
+
end
|
481
|
+
|
482
|
+
def log_info(message, data = {})
|
483
|
+
@console_logger.info("#{message} #{data.inspect unless data.empty()}")
|
484
|
+
end
|
485
|
+
|
486
|
+
def log_debug(message, data = {})
|
487
|
+
@console_logger.debug("#{message} #{data.inspect unless data.empty()}")
|
488
|
+
end
|
489
|
+
|
490
|
+
def process_documents(site)
|
491
|
+
all_documents = site.pages + site.posts.docs
|
492
|
+
chunks = all_documents.each_slice(5).to_a
|
493
|
+
|
494
|
+
Parallel.each(chunks, in_threads: Parallel.processor_count) do |chunk|
|
495
|
+
chunk.each do |doc|
|
496
|
+
begin
|
497
|
+
convert_math(doc) if has_math?(doc.content)
|
498
|
+
rescue => e
|
499
|
+
@console_logger.error("Error processing #{doc.path}: #{e.message}")
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
def check_node_dependencies(required_modules)
|
506
|
+
missing = []
|
507
|
+
required_modules.each do |mod|
|
508
|
+
begin
|
509
|
+
require_cmd = "require.resolve('#{mod}')"
|
510
|
+
cmd = "node -e \"#{require_cmd}\""
|
511
|
+
_, status = Open3.capture2e(cmd)
|
512
|
+
missing << mod unless status.success?
|
513
|
+
rescue
|
514
|
+
missing << mod
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
unless missing.empty?
|
519
|
+
msg = "Missing required Node.js modules: #{missing.join(', ')}. "\
|
520
|
+
"Please run: npm install #{missing.join(' ')}"
|
521
|
+
raise msg
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
# Helper class for multiple IO logging
|
529
|
+
class MultiIO
|
530
|
+
def initialize(*targets)
|
531
|
+
@targets = targets
|
532
|
+
end
|
533
|
+
|
534
|
+
def write(*args)
|
535
|
+
@targets.each { |t| t.write(*args) }
|
536
|
+
end
|
537
|
+
|
538
|
+
def close
|
539
|
+
@targets.each(&:close)
|
540
|
+
end
|
541
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
const sharp = require('sharp');
|
2
|
+
const glob = require('glob');
|
3
|
+
const path = require('path');
|
4
|
+
const fs = require('fs');
|
5
|
+
|
6
|
+
async function convertSvgToPng(buildDir) {
|
7
|
+
try {
|
8
|
+
// Find all SVG files recursively
|
9
|
+
const svgFiles = glob.sync('**/*.svg', { cwd: buildDir });
|
10
|
+
|
11
|
+
for (const svgFile of svgFiles) {
|
12
|
+
const svgPath = path.join(buildDir, svgFile);
|
13
|
+
const pngPath = svgPath.replace(/\.svg$/, '.png');
|
14
|
+
|
15
|
+
try {
|
16
|
+
await sharp(svgPath)
|
17
|
+
.png()
|
18
|
+
.toFile(pngPath);
|
19
|
+
console.log(`Converted: ${svgFile} -> ${path.basename(pngPath)}`);
|
20
|
+
} catch (err) {
|
21
|
+
console.error(`Error converting ${svgFile}:`, err);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
} catch (err) {
|
25
|
+
console.error('Conversion error:', err);
|
26
|
+
process.exit(1);
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
// Get build directory from command line argument
|
31
|
+
const buildDir = process.argv[2];
|
32
|
+
if (!buildDir) {
|
33
|
+
console.error('Please provide the build directory path');
|
34
|
+
process.exit(1);
|
35
|
+
}
|
36
|
+
|
37
|
+
convertSvgToPng(buildDir);
|