Add inline and watch helper scripts

This commit is contained in:
cpq committed 2026-04-27 09:25:29 +01:00
1 parent e21f5ab6c7
commit edd2d06627
4 files changed
+237 -3

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const args = process.argv.slice(2);
let outputFile = null;
let inputFile = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '-o' && i + 1 < args.length) {
outputFile = args[++i];
} else if (!args[i].startsWith('-')) {
inputFile = args[i];
}
}
if (!inputFile) {
console.error('Usage: node inline.js [-o OUTPUT_FILE] HTML_FILE');
process.exit(1);
}
const baseDir = path.dirname(path.resolve(inputFile));
const html = fs.readFileSync(inputFile, 'utf8');
function fetchUrl(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
client.get(url, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
}
async function inlineCss(match) {
const href = match.match(/href=["']([^"']+)["']/)[1];
let css = '';
if (href.startsWith('http')) {
css = await fetchUrl(href);
} else {
const cssPath = path.join(baseDir, href);
css = fs.readFileSync(cssPath, 'utf8');
}
return `<style>${css}</style>`;
}
async function inlineJs(match) {
const match2 = match.match(/src=["']([^"']+)["']/);
if (!match2) return match;
const src = match2[1];
let js = '';
if (src.startsWith('http')) {
js = await fetchUrl(src);
} else {
const jsPath = path.join(baseDir, src);
js = fs.readFileSync(jsPath, 'utf8');
}
return `<script>${js}</script>`;
}
async function inlineImg(match) {
const match2 = match.match(/src=["']([^"']+)["']/);
if (!match2) return match;
const src = match2[1];
const ext = path.extname(src).toLowerCase();
let mime = 'image/png';
if (ext === '.svg') mime = 'image/svg+xml';
else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg';
else if (ext === '.gif') mime = 'image/gif';
else if (ext === '.webp') mime = 'image/webp';
else if (ext === '.ico') mime = 'image/x-icon';
let data;
if (src.startsWith('http')) {
data = await fetchUrl(src);
} else {
const imgPath = path.join(baseDir, src);
data = fs.readFileSync(imgPath);
}
const base64 = Buffer.from(data).toString('base64');
if (ext === '.svg') {
const svgData = typeof data === 'string' ? data : data.toString('utf8');
return match.replace(/src="[^"]+"/, `src="data:${mime};utf8,${encodeURIComponent(svgData)}"`);
}
return match.replace(/src="[^"]+"/, `src="data:${mime};base64,${base64}"`);
}
async function inlineSvg(match) {
const match2 = match.match(/href=["']([^"']+)["']/);
if (!match2) return match;
const href = match2[1];
if (!href.toLowerCase().endsWith('.svg')) return match;
let data;
if (href.startsWith('http')) {
data = await fetchUrl(href);
} else {
const svgPath = path.join(baseDir, href);
data = fs.readFileSync(svgPath, 'utf8');
}
const base64 = Buffer.from(data).toString('base64');
return match.replace(/href="[^"]+"/, `href="data:image/svg+xml;base64,${base64}"`);
}
async function processHtml(content) {
let result = content;
const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
const imgRegex = /<img[^>]+src=["'][^"']+["'][^>]*>/g;
const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g;
const cssMatches = content.match(cssRegex) || [];
for (const match of cssMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineCss(match));
}
}
const jsMatches = content.match(jsRegex) || [];
for (const match of jsMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineJs(match));
}
}
const imgMatches = content.match(imgRegex) || [];
for (const match of imgMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineImg(match));
}
}
const svgMatches = content.match(svgRegex) || [];
for (const match of svgMatches) {
if (!match.includes('data:')) {
result = result.replace(match, await inlineSvg(match));
}
}
return result;
}
processHtml(html).then(result => {
if (outputFile) {
fs.writeFileSync(outputFile, result);
console.log(`Written to ${outputFile}`);
} else {
console.log(result);
}
}).catch(err => {
console.error('Error:', err.message);
process.exit(1);
});
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const args = process.argv.slice(2);
const watchTargets = [];
let command = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '-w' && i + 1 < args.length) {
watchTargets.push(args[++i]);
} else if (args[i] === '--') {
command = args.slice(i + 1);
break;
} else if (!args[i].startsWith('-')) {
command = args.slice(i);
break;
}
}
if (watchTargets.length === 0 || command.length === 0) {
console.error('Usage: node watch.js -w DIR_OR_FILE [-w DIR_OR_FILE] -- COMMAND');
process.exit(1);
}
let child = null;
let restartTimeout = null;
function start() {
if (child) {
child.kill();
}
console.log(`Starting: ${command.join(' ')}`);
child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: true });
child.on('exit', (code) => {
if (code !== null && code !== 0) {
console.log(`Process exited with code ${code}`);
}
});
}
function watch(target) {
const stats = fs.statSync(target);
if (stats.isFile()) {
fs.watchFile(target, (curr, prev) => {
if (curr.mtime !== prev.mtime) {
console.log(`File changed: ${target}`);
clearTimeout(restartTimeout);
restartTimeout = setTimeout(start, 100);
}
});
} else {
const dir = target;
fs.watch(dir, { recursive: true }, (eventType, filename) => {
if (filename) {
console.log(`File changed: ${filename}`);
clearTimeout(restartTimeout);
restartTimeout = setTimeout(start, 100);
}
});
}
}
start();
watchTargets.forEach(target => watch(target));
console.log(`Watching ${watchTargets.join(', ')} for changes...`);
process.on('SIGINT', () => {
console.log('\nShutting down...');
if (child) {
child.kill('SIGTERM');
}
process.exit(0);
});
+2 -2
View File
@@ -16,7 +16,7 @@ $(PROG): $(SOURCES) $(MONGOOSE_DIR)/mongoose.h
# Generate a single, self-contained dashboard.html from the UI in web_root/
dashboard.html: $(wildcard web_root/*)
monolith web_root/index.html -o $@
node $(MONGOOSE_DIR)/resources/inline.js web_root/index.html -o $@
# Create an embedded filesystem containing compressed dashboard.html
dashboard.html.c: dashboard.html
@@ -28,7 +28,7 @@ purge:
# For development: rebuild and restart if any of the sources change
watch:
watchexec -r -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -e c,h,html,css,js,svg,png -- make
node $(MONGOOSE_DIR)/resources/watch.js -w main.c -w mongoose_dashboard.c -w $(MONGOOSE_DIR)/mongoose.h -w web_root -- make
clean:
rm -rf $(PROG) *.exe *.obj *.dSYM mongoose dashboard.*
@@ -282,7 +282,7 @@
</div>
<div class="file-size">${f.size}</div>
<div class="file-actions">
<a onclick="window.open('files/${encodeURIComponent(f.name)}', '_blank')" target="_blank" class="btn btn-secondary">
<a href="files/${encodeURIComponent(f.name)}" target="_blank" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v12" />
<path d="m7 10 5 5 5-5" />