mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-28 12:30:58 +00:00
29 files changed
+2635
-1478
No files matched your search
+201
-82
@@ -3,19 +3,13 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const Rpc = (function() {
|
||||
function Rpc(url, onev) {
|
||||
let ws, id = 1, oncall, q = [], reconnect = true;
|
||||
const pending = new Map();
|
||||
const realsend = s => { ws.send(s); onev && onev('send', s); };
|
||||
const fail = e => { for (const p of pending.values()) p[1](e || 'disconnected'); pending.clear(); };
|
||||
const send = x => ws && ws.readyState === 1
|
||||
? realsend(JSON.stringify(x))
|
||||
: q.push(x);
|
||||
const Watch = (function() {
|
||||
function Watch(url, onev, onmsg) {
|
||||
let ws, reconnect = true;
|
||||
const connect = () => {
|
||||
ws = new WebSocket(url);
|
||||
ws.onopen = () => { onev && onev('open'); while (q.length) realsend(JSON.stringify(q.shift())); };
|
||||
ws.onclose = () => { onev && onev('close'); fail(); reconnect && setTimeout(connect, 1000); };
|
||||
ws.onopen = () => onev && onev('open');
|
||||
ws.onclose = () => { onev && onev('close'); reconnect && setTimeout(connect, 1000); };
|
||||
ws.onmessage = e => {
|
||||
onev && onev('message', e.data);
|
||||
let m;
|
||||
@@ -25,69 +19,92 @@
|
||||
console.log('INVALID JSON:', e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m.id && pending.has(m.id)) {
|
||||
const [ok, bad] = pending.get(m.id);
|
||||
pending.delete(m.id);
|
||||
m.error ? bad(m.error) : ok(m.result);
|
||||
} else if (oncall && m.method) {
|
||||
Promise.resolve(oncall(m.method, m.params))
|
||||
.then(r => m.id && send({ id: m.id, result: r }))
|
||||
.catch(err => m.id && send({
|
||||
id: m.id,
|
||||
error: { code: -32000, message: String(err?.message || err) }
|
||||
}));
|
||||
}
|
||||
onmsg && onmsg(m);
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
return {
|
||||
close: () => { reconnect = false; ws?.close(); },
|
||||
call: (method, params) => new Promise((ok, bad) => {
|
||||
const i = id++;
|
||||
pending.set(i, [ok, bad]);
|
||||
send({ id: i, method, params });
|
||||
}),
|
||||
notify: (method, params) => send({ method, params }),
|
||||
handle: fn => (oncall = fn, this),
|
||||
get ws() { return ws; }
|
||||
};
|
||||
}
|
||||
|
||||
return Rpc;
|
||||
return Watch;
|
||||
})();
|
||||
|
||||
function rpc_connect() {
|
||||
function fetch_and_ready() {
|
||||
if (isMock) {
|
||||
setTimeout(() => userhandlers['ready']?.(), 500);
|
||||
return;
|
||||
return delay(500).then(rescan).then(mark_ready);
|
||||
}
|
||||
if (rpc) return rpc;
|
||||
//if (settings.auth && !status.authed) return null;
|
||||
rpc = Rpc('api/websocket', function (evname, args) {
|
||||
if (evname == 'open') status.online = true, rescan();
|
||||
if (evname == 'close') status.online = false, rescan();
|
||||
if (settings.debug) console.log('WS', evname, args);
|
||||
});
|
||||
rpc.handle((method, args) => {
|
||||
if (method == 'change' && args) apply_and_rescan(settings.data, args);
|
||||
if (method == 'logout') rpc_disconnect();
|
||||
if (userhandlers[method]) userhandlers[method](args);
|
||||
});
|
||||
return rpc;
|
||||
return fetch('api/get')
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
||||
.then(data => {
|
||||
for (const k in data) {
|
||||
if (typeof data[k] === 'number') {
|
||||
const prev = settings.data[k];
|
||||
const arr = prev && typeof prev === 'object' ? prev : {};
|
||||
data[k] = {
|
||||
...arr,
|
||||
size: data[k],
|
||||
start: typeof arr.start === 'number' ? arr.start : 0,
|
||||
data: Array.isArray(arr.data) ? arr.data : [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return apply_and_rescan(settings.data, data);
|
||||
})
|
||||
.then(expand_arrays)
|
||||
.then(() => {
|
||||
ws_connect();
|
||||
return mark_ready();
|
||||
})
|
||||
.catch(err => console.log('GET failed', err));
|
||||
}
|
||||
|
||||
function rpc_disconnect() {
|
||||
if (!rpc) return;
|
||||
rpc.close();
|
||||
rpc = null;
|
||||
function ws_connect() {
|
||||
if (isMock || ws) return;
|
||||
ws = Watch('api/websocket', function (evname, args) {
|
||||
if (evname == 'open') { status.online = true; rescan(); }
|
||||
if (evname == 'close') { status.online = false; rescan(); }
|
||||
if (settings.debug) console.log('WS', evname, args);
|
||||
}, function (m) {
|
||||
if (m.method == 'change' && m.params) {
|
||||
const rest = {};
|
||||
const arrays = [];
|
||||
for (const k in m.params) {
|
||||
const ma = k.match(/^(.+)\/(\d+)$/);
|
||||
if (ma) {
|
||||
const arr = settings.data[ma[1]];
|
||||
if (arr?.data) { const i = +ma[2] - (arr.start || 0); if (i >= 0 && i < arr.data.length) arr.data[i] = Object.assign(arr.data[i] || {}, m.params[k]); }
|
||||
} else if (typeof m.params[k] === 'number' && settings.data[k]?.data !== undefined) {
|
||||
const arr = settings.data[k];
|
||||
arr.size = m.params[k];
|
||||
if (is_expandable(arr)) arrays.push(expand_array(k, arr.start, arr.end));
|
||||
} else {
|
||||
rest[k] = m.params[k];
|
||||
}
|
||||
}
|
||||
apply(settings.data, rest);
|
||||
prune(settings.data, settings.edits);
|
||||
Promise.all(arrays)
|
||||
.then(rescan)
|
||||
.then(() => userhandlers['change']?.(m.params));
|
||||
}
|
||||
if (m.method == 'logout') ws_disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
function ws_disconnect() {
|
||||
if (!ws) return;
|
||||
ws.close();
|
||||
ws = null;
|
||||
status.online = false;
|
||||
}
|
||||
|
||||
|
||||
function login(params) {
|
||||
if (isMock) { rpc_connect(); return Promise.resolve({}); }
|
||||
if (isMock) return fetch_and_ready().then(() => ({}));
|
||||
const opts = {};
|
||||
if (params) {
|
||||
const username = (params?.username ?? '').trim();
|
||||
@@ -100,19 +117,18 @@
|
||||
.then(resp => {
|
||||
status.username = resp?.user || '';
|
||||
status.userlevel = resp?.level || 0;
|
||||
rpc_connect();
|
||||
return resp;
|
||||
return fetch_and_ready().then(() => resp);
|
||||
});
|
||||
};
|
||||
|
||||
function logout() {
|
||||
if (isMock) { rpc_disconnect(); return Promise.resolve({}); }
|
||||
if (isMock) { ws_disconnect(); return Promise.resolve({}); }
|
||||
return fetch('api/logout')
|
||||
.catch(() => true)
|
||||
.then(() => {
|
||||
status.username = '';
|
||||
status.userlevel = 0;
|
||||
rpc_disconnect();
|
||||
ws_disconnect();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -122,7 +138,27 @@
|
||||
const settings = {data: {}, edits: {}, debug: true};
|
||||
const userhandlers = {}; // User event handlers
|
||||
const status = { online: false, username: '', userlevel: 0, ready: false };
|
||||
let rpc = null;
|
||||
let ws = null;
|
||||
let ready_started = false;
|
||||
|
||||
function install_ready_gate() {
|
||||
document.documentElement.classList.remove('dashboard-ready');
|
||||
if (document.getElementById('dashboard-ready-gate')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'dashboard-ready-gate';
|
||||
style.textContent = `html:not(.dashboard-ready) *,html:not(.dashboard-ready) *::before,html:not(.dashboard-ready) *::after{transition:none!important;animation:none!important}`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function mark_ready() {
|
||||
if (ready_started) return Promise.resolve();
|
||||
ready_started = true;
|
||||
return next_frame().then(() => {
|
||||
status.ready = true;
|
||||
document.documentElement.classList.add('dashboard-ready');
|
||||
userhandlers['ready']?.();
|
||||
});
|
||||
}
|
||||
|
||||
const fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value);
|
||||
const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj);
|
||||
@@ -222,7 +258,11 @@
|
||||
function save(key) {
|
||||
const changes = {};
|
||||
set(changes, key, get(settings.edits, key));
|
||||
rpc.call('set', changes).then(r => apply_and_rescan(settings.data, r));
|
||||
fetch('api/set', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(changes),
|
||||
}).catch(err => console.log('SET failed', key, err));
|
||||
};
|
||||
|
||||
// Cancel the edits for keys that match pattern
|
||||
@@ -231,6 +271,14 @@
|
||||
rescan();
|
||||
};
|
||||
|
||||
// Set a checkbox's `checked` *property* from a boolean expression. The
|
||||
// `checked` content attribute is presence-based - templating its value
|
||||
// would not work, so this binds straight to the IDL property instead
|
||||
function handle_checked(el, key, context) {
|
||||
if (!has(context, key)) { console.error('EVAL', key, context); return; }
|
||||
el.checked = !!get(context, key);
|
||||
};
|
||||
|
||||
function handle_save(el, key) {
|
||||
el.disabled = !edited(key);
|
||||
if (!el.bound) el.addEventListener("click", ev => save(key)), el.bound = true;
|
||||
@@ -244,18 +292,30 @@
|
||||
function handle_repeat(el, key, context) {
|
||||
if (!has(context, key)) { console.error('EVAL', key, context); return; }
|
||||
if (!el.orig) el.orig = el.children[0].cloneNode(true);
|
||||
const v = get(context, key), v2 = JSON.stringify(v);
|
||||
if (el.v2 === v2) return;
|
||||
const frag = document.createDocumentFragment();
|
||||
const v = get(context, key);
|
||||
const it = el.dataset.iterator ?? '__obj';
|
||||
v.forEach(function (item, index) {
|
||||
const child = el.orig.cloneNode(true);
|
||||
const ctx = { ...context, [it]: item };
|
||||
process(child, ctx, true);
|
||||
frag.appendChild(child);
|
||||
});
|
||||
el.replaceChildren(frag);
|
||||
el.v2 = v2;
|
||||
const json = v.map(item => JSON.stringify(item));
|
||||
const prev = el.itemsJson || [];
|
||||
if (json.length === prev.length && el.children.length === json.length) {
|
||||
// Same length: re-template each changed row's existing DOM node in
|
||||
// place instead of replacing it. Untouched rows are left completely
|
||||
// alone, and changed rows keep their live elements too - so bound
|
||||
// properties (e.g. a checkbox's `checked`) change on an
|
||||
// already-painted node, which is what lets CSS transitions play, and
|
||||
// in-progress edits in unrelated fields of that row survive
|
||||
json.forEach((j, i) => {
|
||||
if (j !== prev[i]) process(el.children[i], { ...context, [it]: v[i] }, true);
|
||||
});
|
||||
} else {
|
||||
const frag = document.createDocumentFragment();
|
||||
v.forEach(item => {
|
||||
const node = el.orig.cloneNode(true);
|
||||
process(node, { ...context, [it]: item }, true);
|
||||
frag.appendChild(node);
|
||||
});
|
||||
el.replaceChildren(frag);
|
||||
}
|
||||
el.itemsJson = json;
|
||||
};
|
||||
|
||||
function upload(el, is_ota) {
|
||||
@@ -267,7 +327,7 @@
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(f);
|
||||
reader.onload = function () {
|
||||
const url = is_ota ? 'api/ota' : `fs/${encodeURIComponent(f.name)}`;
|
||||
const url = is_ota ? 'api/ota' : `fs/${el.dataset.upload}/${encodeURIComponent(f.name)}`;
|
||||
let begin = Date.now(), body = reader.result, ok = false;
|
||||
// console.log(2, url, body);
|
||||
fetch(url, { method: 'POST', body })
|
||||
@@ -295,7 +355,8 @@
|
||||
}
|
||||
|
||||
function process(root, context, x) {
|
||||
const all = root.querySelectorAll("*:not([data-repeat] *)");
|
||||
const selector = x ? "*" : "*:not([data-repeat] *)";
|
||||
const all = root.querySelectorAll(selector);
|
||||
const els = root.nodeType === Node.DOCUMENT_NODE ? all : [root, ...all];
|
||||
els.forEach(function (el) {
|
||||
if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
|
||||
@@ -309,14 +370,16 @@
|
||||
}
|
||||
for (const attr of el.attributes) {
|
||||
const cv = attr.value || '';
|
||||
if (attr.originalValue || cv.includes('${')) {
|
||||
if (!attr.originalValue) attr.originalValue = cv;
|
||||
const v = substituteExpressions(attr.originalValue, context);
|
||||
if (!el.originalAttrs) el.originalAttrs = {};
|
||||
if (el.originalAttrs[attr.name] || cv.includes('${')) {
|
||||
if (!el.originalAttrs[attr.name]) el.originalAttrs[attr.name] = cv;
|
||||
const v = substituteExpressions(el.originalAttrs[attr.name], context);
|
||||
if (v !== attr.value) attr.value = v;
|
||||
}
|
||||
}
|
||||
const handlers = {
|
||||
bind: handle_bind,
|
||||
checked: handle_checked,
|
||||
save: handle_save,
|
||||
cancel: handle_cancel,
|
||||
repeat: handle_repeat,
|
||||
@@ -341,21 +404,75 @@
|
||||
|
||||
function rescan() {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", realrescan, { once: true });
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
realrescan();
|
||||
resolve();
|
||||
}, { once: true });
|
||||
});
|
||||
} else {
|
||||
realrescan();
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const delay = (ms, value) => new Promise(r => setTimeout(() => r(value), ms));
|
||||
const next_frame = () => new Promise(r => requestAnimationFrame(() => r()));
|
||||
const is_expandable = arr => arr && Array.isArray(arr.data) &&
|
||||
typeof arr.start === 'number' && typeof arr.end === 'number';
|
||||
|
||||
function expand_array(name, start, end) {
|
||||
const arr = settings.data[name];
|
||||
const page = 100, all = [];
|
||||
const stop = Math.min(end, typeof arr?.size === 'number' ? arr.size - 1 : end);
|
||||
const done = () => {
|
||||
set(settings.data, `${name}.data`, all);
|
||||
set(settings.data, `${name}.start`, start);
|
||||
return rescan().then(() => all);
|
||||
};
|
||||
const next = from => {
|
||||
if (from > stop) return Promise.resolve(done());
|
||||
const to = Math.min(from + page - 1, stop);
|
||||
return fetch(`api/get/${name}/${from}/${to}`)
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
|
||||
.then(data => {
|
||||
if (!data || !data.length) return done();
|
||||
all.push(...data);
|
||||
return next(from + data.length);
|
||||
});
|
||||
};
|
||||
return next(start);
|
||||
}
|
||||
|
||||
function expand_arrays() {
|
||||
const all = [];
|
||||
for (const name in settings.data) {
|
||||
const arr = settings.data[name];
|
||||
if (is_expandable(arr)) all.push(expand_array(name, arr.start, arr.end));
|
||||
}
|
||||
return Promise.all(all);
|
||||
}
|
||||
|
||||
function call(name, args) {
|
||||
if (isMock) {
|
||||
return delay(750, true).then(val => console.log('response', name));
|
||||
} else {
|
||||
return rpc.call(name, args)
|
||||
.catch(err => console.log('CALL FAILED', name, args, err));
|
||||
return delay(750, true).then(() => console.log('response', name));
|
||||
}
|
||||
if (name === 'set') {
|
||||
return fetch('api/set', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(args),
|
||||
}).catch(err => console.log('CALL FAILED', name, args, err));
|
||||
}
|
||||
if (args && typeof args === 'object') {
|
||||
const {name: aname, start: s = 0, size: total = 100, end} = args;
|
||||
const stop = typeof end === 'number' ? end : s + total - 1;
|
||||
return expand_array(aname, s, stop).catch(err => console.log('CALL FAILED', name, args, err));
|
||||
}
|
||||
const path = typeof args === 'string' && args ? '/' + args : '';
|
||||
return fetch('api/get' + path)
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
|
||||
.catch(err => console.log('CALL FAILED', name, args, err));
|
||||
};
|
||||
|
||||
function apply(target, patch) {
|
||||
@@ -373,16 +490,18 @@
|
||||
function apply_and_rescan(target, patch) {
|
||||
apply(target, patch);
|
||||
prune(settings.data, settings.edits);
|
||||
rescan();
|
||||
return rescan();
|
||||
};
|
||||
|
||||
function on(name, fn) {
|
||||
userhandlers[name] = fn;
|
||||
if (name === 'ready' && status.ready) setTimeout(fn, 0);
|
||||
};
|
||||
|
||||
function init(conf) {
|
||||
install_ready_gate();
|
||||
apply_and_rescan(settings, conf);
|
||||
if (!settings.auth) rpc_connect();
|
||||
if (!settings.auth) fetch_and_ready();
|
||||
};
|
||||
|
||||
global.Dashboard = { init, call, on, status, login, logout };
|
||||
|
||||
@@ -62,7 +62,7 @@ $(foreach T,$(RP_TARGETS), $(foreach P,$(PROJECTS), $(eval $(call RP_DASHBOARD_R
|
||||
%.zip: %
|
||||
zip -qr $@ $<
|
||||
|
||||
upload: $(ZIPS)
|
||||
deploy: $(ZIPS)
|
||||
scp $(ZIPS) h1:/data/downloads/
|
||||
|
||||
clean:
|
||||
|
||||
+52
-35
@@ -5,7 +5,7 @@
|
||||
// inlining referenced CSS, JS, images, and icons.
|
||||
//
|
||||
// Usage:
|
||||
// node html2c.js [-o OUTPUT.c] HTML_FILE
|
||||
// node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -15,24 +15,25 @@ const zlib = require('zlib');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let outputFile = null;
|
||||
let inputFile = null;
|
||||
const inputFiles = [];
|
||||
const useMap = new Map();
|
||||
|
||||
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 === null) {
|
||||
inputFile = args[i];
|
||||
} else if (args[i] === '--use' && i + 1 < args.length) {
|
||||
const eq = args[++i].indexOf('=');
|
||||
if (eq > 0) useMap.set(args[i].slice(0, eq), args[i].slice(eq + 1));
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
inputFiles.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inputFile) {
|
||||
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE');
|
||||
if (inputFiles.length === 0) {
|
||||
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_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;
|
||||
@@ -47,12 +48,15 @@ function fetchUrl(url) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readAsset(name, encoding) {
|
||||
async function readAsset(name, encoding, baseDir) {
|
||||
const override = useMap.get(name);
|
||||
if (override != null) name = override;
|
||||
if (name.startsWith('http://') || name.startsWith('https://')) {
|
||||
const data = await fetchUrl(name);
|
||||
return encoding ? data.toString(encoding) : data;
|
||||
}
|
||||
return fs.readFileSync(path.join(baseDir, name), encoding || null);
|
||||
const fullPath = path.isAbsolute(name) ? name : path.join(baseDir, name);
|
||||
return fs.readFileSync(fullPath, encoding || null);
|
||||
}
|
||||
|
||||
function attr(match, name) {
|
||||
@@ -66,21 +70,21 @@ function replaceAttr(match, name, value) {
|
||||
return match.replace(re, `${name}="${value}"`);
|
||||
}
|
||||
|
||||
async function inlineCss(match) {
|
||||
async function inlineCss(match, baseDir) {
|
||||
const href = attr(match, 'href');
|
||||
if (!href) return match;
|
||||
const css = await readAsset(href, 'utf8');
|
||||
const css = await readAsset(href, 'utf8', baseDir);
|
||||
return `<style>${css}</style>`;
|
||||
}
|
||||
|
||||
async function inlineJs(match) {
|
||||
async function inlineJs(match, baseDir) {
|
||||
const src = attr(match, 'src');
|
||||
if (!src) return match;
|
||||
const js = await readAsset(src, 'utf8');
|
||||
const js = await readAsset(src, 'utf8', baseDir);
|
||||
return `<script>${js}</script>`;
|
||||
}
|
||||
|
||||
async function inlineImg(match) {
|
||||
async function inlineImg(match, baseDir) {
|
||||
const src = attr(match, 'src');
|
||||
if (!src) return match;
|
||||
const ext = path.extname(src).toLowerCase();
|
||||
@@ -91,7 +95,7 @@ async function inlineImg(match) {
|
||||
else if (ext === '.webp') mime = 'image/webp';
|
||||
else if (ext === '.ico') mime = 'image/x-icon';
|
||||
|
||||
const data = await readAsset(src);
|
||||
const data = await readAsset(src, null, baseDir);
|
||||
if (ext === '.svg') {
|
||||
const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
||||
return replaceAttr(
|
||||
@@ -107,10 +111,10 @@ async function inlineImg(match) {
|
||||
);
|
||||
}
|
||||
|
||||
async function inlineSvg(match) {
|
||||
async function inlineSvg(match, baseDir) {
|
||||
const href = attr(match, 'href');
|
||||
if (!href || !href.toLowerCase().endsWith('.svg')) return match;
|
||||
const data = await readAsset(href);
|
||||
const data = await readAsset(href, null, baseDir);
|
||||
return replaceAttr(
|
||||
match,
|
||||
'href',
|
||||
@@ -118,7 +122,7 @@ async function inlineSvg(match) {
|
||||
);
|
||||
}
|
||||
|
||||
async function processHtml(content) {
|
||||
async function processHtml(content, baseDir) {
|
||||
let result = content;
|
||||
const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
|
||||
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
|
||||
@@ -126,16 +130,16 @@ async function processHtml(content) {
|
||||
const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g;
|
||||
|
||||
for (const match of content.match(cssRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineCss(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineCss(match, baseDir));
|
||||
}
|
||||
for (const match of content.match(jsRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineJs(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineJs(match, baseDir));
|
||||
}
|
||||
for (const match of content.match(imgRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineImg(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineImg(match, baseDir));
|
||||
}
|
||||
for (const match of content.match(svgRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match, baseDir));
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -145,30 +149,43 @@ function cArray(data) {
|
||||
return Array.from(Buffer.from(data)).concat(0).join(',');
|
||||
}
|
||||
|
||||
function generateC(data) {
|
||||
const destination = `${path.basename(inputFile)}.gz`;
|
||||
const stat = fs.statSync(inputFile);
|
||||
const mtime = parseInt(stat.mtimeMs / 1000);
|
||||
const zipped = zlib.gzipSync(data);
|
||||
const bytes = cArray(zipped);
|
||||
function generateC(files) {
|
||||
const vars = files.map(({inputFile, data}, i) => {
|
||||
const zipped = zlib.gzipSync(data);
|
||||
return {inputFile, zipped};
|
||||
});
|
||||
|
||||
const decls = vars.map(({zipped}, i) =>
|
||||
`static const unsigned char v${i}[] = {${cArray(zipped)}};`
|
||||
).join('\n');
|
||||
|
||||
const entries = vars.map(({inputFile, zipped}, i) => {
|
||||
const destination = `${path.basename(inputFile)}.gz`;
|
||||
const mtime = parseInt(fs.statSync(inputFile).mtimeMs / 1000);
|
||||
return ` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${mtime}}, // size: ${zipped.length}`;
|
||||
}).join('\n');
|
||||
|
||||
return `// DO NOT EDIT. This file is generated using this command:
|
||||
// ${process.argv.join(' ')}
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
static const unsigned char v0[] = {${bytes}};
|
||||
${decls}
|
||||
|
||||
const struct mg_mem_file mg_packed_files[] = {
|
||||
{"/${destination}", v0, sizeof(v0) - 1, ${mtime}}, // size: ${zipped.length}
|
||||
${entries}
|
||||
{NULL, NULL, 0, 0}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
processHtml(html)
|
||||
.then(result => {
|
||||
const c = generateC(result);
|
||||
Promise.all(inputFiles.map(inputFile => {
|
||||
const baseDir = path.dirname(path.resolve(inputFile));
|
||||
const html = fs.readFileSync(inputFile, 'utf8');
|
||||
return processHtml(html, baseDir).then(data => ({inputFile, data}));
|
||||
}))
|
||||
.then(files => {
|
||||
const c = generateC(files);
|
||||
if (outputFile) {
|
||||
fs.writeFileSync(outputFile, c);
|
||||
} else {
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
// 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);
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2024 Cesanta Software Limited
|
||||
//
|
||||
// Utility that generates packed filesystem C file compatible with the
|
||||
// Mongoose Network Library, https://github.com/cesanta/mongoose
|
||||
//
|
||||
// Usage:
|
||||
// node pack.js FILE[:DESTINATION[:gzip]] ...
|
||||
|
||||
const fs = require('fs');
|
||||
const zlib = require('zlib');
|
||||
const argv = process.argv.slice(2);
|
||||
|
||||
// Convert each command-line arguments into [ DATA_ARRAY, STRUCT_INITIALIZER ]
|
||||
const entries = argv.map(function(filename, i) {
|
||||
const parts = filename.split(':');
|
||||
const stat = fs.statSync(parts[0]);
|
||||
const data = fs.readFileSync(parts[0], null);
|
||||
let bytes = Array.from(data);
|
||||
let destination = (parts[1] || parts[0]).replace(/^\.+[\/\\]*/, '');
|
||||
if (parts[2] == 'gzip') {
|
||||
bytes = Array.from(zlib.gzipSync(data));
|
||||
destination += '.gz';
|
||||
}
|
||||
|
||||
// concat(0) appends trailing 0, in order to make any file an asciz string
|
||||
return [
|
||||
`static const unsigned char v${i}[] = {${bytes.concat(0).join(',')}};`,
|
||||
` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${parseInt(stat.mtimeMs / 1000)}}`,
|
||||
];
|
||||
});
|
||||
|
||||
process.stdout.write(`// DO NOT EDIT. This file is generated using this command:
|
||||
// ${process.argv.join(' ')}
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
${entries.map(x => x[0]).join('\n\n')}
|
||||
|
||||
const struct mg_mem_file mg_packed_files[] = {
|
||||
${entries.map(x => x[1]).join(',\n')},
|
||||
{NULL, NULL, 0, 0}
|
||||
};
|
||||
`);
|
||||
+2
-2
@@ -70,8 +70,8 @@ function sign(firmware, privateKeyFile = 'private.pem') {
|
||||
? firmware.slice(0, dot) + '.signed' + firmware.slice(dot)
|
||||
: firmware + '.signed';
|
||||
|
||||
fs.writeFileSync(out, Buffer.concat([fw, raw]));
|
||||
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature)`);
|
||||
fs.writeFileSync(out, Buffer.concat([fw, raw, Buffer.from('MGSG')]));
|
||||
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature + 4 bytes magic)`);
|
||||
}
|
||||
|
||||
const [,, cmd, arg1, arg2] = process.argv;
|
||||
|
||||
Reference in new issue
Block a user