Refactor dashboard example
@ -1,7 +1,7 @@
|
||||
PROG ?= example
|
||||
SOURCES ?= ../../mongoose.c main.c mjson.c packed_fs.c
|
||||
SOURCES ?= ../../mongoose.c main.c packed_fs.c
|
||||
CFLAGS ?= -I../.. -DMG_ENABLE_PACKED_FS=1 $(EXTRA)
|
||||
FILES_TO_EMBED ?= $(wildcard web_root/*.js) $(wildcard web_root/*.css) $(wildcard web_root/*.html) $(wildcard web_root/images/*.png) $(wildcard images/*.jpg)
|
||||
FILES_TO_EMBED ?= $(wildcard web_root/*)
|
||||
ROOT ?= $(realpath $(CURDIR)/../../..)
|
||||
DOCKER ?= docker run --rm -e Tmp=. -e WINEDEBUG=-all -v $(ROOT):$(ROOT) -w $(CURDIR)
|
||||
VC98 ?= $(DOCKER) mdashnet/vc98 wine
|
||||
@ -13,7 +13,7 @@ all: $(PROG)
|
||||
$(PROG):
|
||||
$(CC) ../../test/pack.c -o pack
|
||||
./pack $(FILES_TO_EMBED) > packed_fs.c
|
||||
$(CC) -W -Wall -Wextra -Os -g3 $(CFLAGS) -o $(PROG) $(SOURCES)
|
||||
$(CC) -W -Wall -Wextra -O0 -g3 $(CFLAGS) -o $(PROG) $(SOURCES)
|
||||
|
||||
|
||||
mingw:
|
||||
@ -29,4 +29,4 @@ vc98:
|
||||
$(VC98) $(PROG).exe
|
||||
|
||||
clean:
|
||||
rm -rf $(PROG) *.o *.dSYM *.gcov *.gcno *.gcda *.obj *.exe *.ilk *.pdb log.txt packed_fs.c pack
|
||||
rm -rf $(PROG) *.o *.dSYM *.gcov *.gcno *.gcda *.obj *.exe *.ilk *.pdb log.txt pack
|
||||
|
@ -7,16 +7,13 @@ following features:
|
||||
- Multiple logins with different permissions (admin and user)
|
||||
- Login screen for non-authenticated connections
|
||||
- A [preact](https://preactjs.com/)-based dashboard with multiple pages
|
||||
- Internal variables
|
||||
- A form that changes those variables
|
||||
- Web UI is fully embedded into the server/firmware binary, and does not
|
||||
need a filesystem to serve it
|
||||
- View and change server settings
|
||||
- All changes are propagates to all connected clients
|
||||
- Live log stream
|
||||
- Live video stream
|
||||
- Live interactive chat that demonstrates bi-directional data exchange
|
||||
|
||||
|
||||
In essence, this example is a combination of several other examples
|
||||
pulled together (multiple-logins, dashboard, live-log, video-stream).
|
||||
|
||||
# Screenshots
|
||||
|
||||
This is a login screen that prompts for user/password
|
||||
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 10 KiB |
@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2020 Cesanta Software Limited
|
||||
// Copyright (c) 2020-2022 Cesanta Software Limited
|
||||
// All rights reserved
|
||||
|
||||
#include "mjson.h"
|
||||
#include "mongoose.h"
|
||||
|
||||
// Authenticated user.
|
||||
@ -22,25 +21,15 @@ static struct config {
|
||||
char *value2;
|
||||
} s_config = {123, NULL};
|
||||
|
||||
// Stringifies the config. A caller must free() it.
|
||||
static char *stringify_config(struct config *cfg) {
|
||||
char *s = NULL;
|
||||
mjson_printf(mjson_print_dynamic_buf, &s, "{%Q:%d,%Q:%Q}", "value1",
|
||||
cfg->value1, "value2", cfg->value2);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Update config structure. Return true if changed, false otherwise
|
||||
static bool update_config(struct mg_http_message *hm, struct config *cfg) {
|
||||
bool changed = false;
|
||||
char buf[256];
|
||||
double dv;
|
||||
if (mjson_get_number(hm->body.ptr, hm->body.len, "$.value1", &dv)) {
|
||||
cfg->value1 = dv;
|
||||
if (mg_http_get_var(&hm->body, "value1", buf, sizeof(buf)) > 0) {
|
||||
cfg->value1 = atoi(buf);
|
||||
changed = true;
|
||||
}
|
||||
if (mjson_get_string(hm->body.ptr, hm->body.len, "$.value2", buf,
|
||||
sizeof(buf)) > 0) {
|
||||
if (mg_http_get_var(&hm->body, "value2", buf, sizeof(buf)) > 0) {
|
||||
free(cfg->value2);
|
||||
cfg->value2 = strdup(buf);
|
||||
changed = true;
|
||||
@ -54,7 +43,7 @@ static struct user *getuser(struct mg_http_message *hm) {
|
||||
// In this example, user list is kept in RAM. In production, it can
|
||||
// be backed by file, database, or some other method.
|
||||
static struct user users[] = {
|
||||
{"admin", "admin", "admin_token"},
|
||||
{"admin", "pass0", "admin_token"},
|
||||
{"user1", "pass1", "user1_token"},
|
||||
{"user2", "pass2", "user2_token"},
|
||||
{NULL, NULL, NULL},
|
||||
@ -75,17 +64,18 @@ static struct user *getuser(struct mg_http_message *hm) {
|
||||
}
|
||||
|
||||
// Notify all config watchers about the config change
|
||||
static void notify_config_change(struct mg_mgr *mgr) {
|
||||
static void send_notification(struct mg_mgr *mgr, const char *name,
|
||||
const char *data) {
|
||||
struct mg_connection *c;
|
||||
char *s = stringify_config(&s_config);
|
||||
for (c = mgr->conns; c != NULL; c = c->next) {
|
||||
if (c->label[0] == 'W') mg_http_printf_chunk(c, "%s\n", s);
|
||||
if (c->label[0] == 'W')
|
||||
mg_http_printf_chunk(c, "{\"name\": \"%s\", \"data\": \"%s\"}", name,
|
||||
data == NULL ? "" : data);
|
||||
}
|
||||
free(s);
|
||||
}
|
||||
|
||||
// HTTP request handler function
|
||||
static void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||
static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||
if (ev == MG_EV_HTTP_MSG) {
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
struct user *u = getuser(hm);
|
||||
@ -95,35 +85,27 @@ static void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||
// All URIs starting with /api/ must be authenticated
|
||||
mg_printf(c, "%s", "HTTP/1.1 403 Denied\r\nContent-Length: 0\r\n\r\n");
|
||||
} else if (mg_http_match_uri(hm, "/api/config/get")) {
|
||||
char *s = stringify_config(&s_config);
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s\n",
|
||||
(int) strlen(s) + 1, s);
|
||||
free(s);
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
|
||||
mg_http_printf_chunk(c, "{\"%s\":%d,\"%s\":\"%s\"}", "value1",
|
||||
s_config.value1, "value2", s_config.value2);
|
||||
mg_http_printf_chunk(c, "");
|
||||
} else if (mg_http_match_uri(hm, "/api/config/set")) {
|
||||
// Admins only
|
||||
if (strcmp(u->name, "admin") == 0) {
|
||||
if (update_config(hm, &s_config)) notify_config_change(fn_data);
|
||||
if (update_config(hm, &s_config))
|
||||
send_notification(fn_data, "config", NULL);
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
||||
} else {
|
||||
mg_printf(c, "%s", "HTTP/1.1 403 Denied\r\nContent-Length: 0\r\n\r\n");
|
||||
}
|
||||
} else if (mg_http_match_uri(hm, "/api/config/watch")) {
|
||||
c->label[0] = 'W'; // Mark ourselves as a config watcher
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
|
||||
} else if (mg_http_match_uri(hm, "/api/video1")) {
|
||||
c->label[0] = 'S'; // Mark that connection as live streamer
|
||||
mg_printf(
|
||||
c, "%s",
|
||||
"HTTP/1.0 200 OK\r\n"
|
||||
"Cache-Control: no-cache\r\n"
|
||||
"Pragma: no-cache\r\nExpires: Thu, 01 Dec 1994 16:00:00 GMT\r\n"
|
||||
"Content-Type: multipart/x-mixed-replace; boundary=--foo\r\n\r\n");
|
||||
} else if (mg_http_match_uri(hm, "/api/log/static")) {
|
||||
struct mg_http_serve_opts opts;
|
||||
memset(&opts, 0, sizeof(opts));
|
||||
mg_http_serve_file(c, hm, "log.txt", &opts);
|
||||
} else if (mg_http_match_uri(hm, "/api/log/live")) {
|
||||
c->label[0] = 'L'; // Mark that connection as live log listener
|
||||
} else if (mg_http_match_uri(hm, "/api/message/send")) {
|
||||
char buf[256];
|
||||
if (mg_http_get_var(&hm->body, "message", buf, sizeof(buf)) > 0) {
|
||||
send_notification(fn_data, "message", buf);
|
||||
}
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
||||
} else if (mg_http_match_uri(hm, "/api/watch")) {
|
||||
c->label[0] = 'W'; // Mark ourselves as a event listener
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
|
||||
} else if (mg_http_match_uri(hm, "/api/login")) {
|
||||
mg_printf(c, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
|
||||
@ -132,71 +114,23 @@ static void cb(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
|
||||
mg_http_printf_chunk(c, "");
|
||||
} else {
|
||||
struct mg_http_serve_opts opts = {0};
|
||||
#if 1
|
||||
opts.root_dir = "/web_root";
|
||||
opts.fs = &mg_fs_packed;
|
||||
#else
|
||||
opts.root_dir = "web_root";
|
||||
#endif
|
||||
mg_http_serve_dir(c, ev_data, &opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The image stream is simulated by sending MJPEG frames specified by the
|
||||
// "files" array of file names.
|
||||
static void broadcast_mjpeg_frame(struct mg_mgr *mgr) {
|
||||
const char *files[] = {"images/1.jpg", "images/2.jpg", "images/3.jpg",
|
||||
"images/4.jpg", "images/5.jpg", "images/6.jpg"};
|
||||
size_t nfiles = sizeof(files) / sizeof(files[0]);
|
||||
static size_t i;
|
||||
const char *path = files[i++ % nfiles];
|
||||
size_t size = 0;
|
||||
char *data = mg_file_read(&mg_fs_posix, path, &size); // Read next file
|
||||
struct mg_connection *c;
|
||||
for (c = mgr->conns; c != NULL; c = c->next) {
|
||||
if (c->label[0] != 'S') continue; // Skip non-stream connections
|
||||
if (data == NULL || size == 0) continue; // Skip on file read error
|
||||
mg_printf(c,
|
||||
"--foo\r\nContent-Type: image/jpeg\r\n"
|
||||
"Content-Length: %lu\r\n\r\n",
|
||||
(unsigned long) size);
|
||||
mg_send(c, data, size);
|
||||
mg_send(c, "\r\n", 2);
|
||||
}
|
||||
free(data);
|
||||
}
|
||||
|
||||
static void mjpeg_cb(void *arg) {
|
||||
broadcast_mjpeg_frame(arg);
|
||||
}
|
||||
|
||||
static void log_message(const char *filename, const char *message) {
|
||||
FILE *fp = fopen(filename, "a");
|
||||
if (fp != NULL) {
|
||||
fprintf(fp, "%s", message);
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_message(struct mg_mgr *mgr, const char *message) {
|
||||
struct mg_connection *c;
|
||||
for (c = mgr->conns; c != NULL; c = c->next) {
|
||||
if (c->label[0] == 'L') mg_http_printf_chunk(c, "%s", message);
|
||||
}
|
||||
}
|
||||
|
||||
// Timer function - called periodically.
|
||||
// Prepare log message. Save it to a file, and broadcast.
|
||||
static void log_cb(void *arg) {
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "Time is: %lu\n", (unsigned long) time(NULL));
|
||||
log_message("log.txt", buf);
|
||||
broadcast_message(arg, buf);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
struct mg_mgr mgr;
|
||||
s_config.value2 = strdup("hello");
|
||||
|
||||
mg_mgr_init(&mgr);
|
||||
mg_http_listen(&mgr, "http://localhost:8000", cb, &mgr);
|
||||
mg_timer_add(&mgr, 500, MG_TIMER_REPEAT, mjpeg_cb, &mgr);
|
||||
mg_timer_add(&mgr, 1000, MG_TIMER_REPEAT, log_cb, &mgr);
|
||||
mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, &mgr);
|
||||
for (;;) mg_mgr_poll(&mgr, 50);
|
||||
mg_mgr_free(&mgr);
|
||||
|
||||
|
@ -1,981 +0,0 @@
|
||||
// Copyright (c) 2018-2020 Cesanta Software Limited
|
||||
// All rights reserved
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#include "mjson.h"
|
||||
|
||||
#if !defined(_MSC_VER) || _MSC_VER >= 1700
|
||||
#else
|
||||
#define va_copy(x, y) (x) = (y)
|
||||
#define snprintf _snprintf
|
||||
#define alloca _alloca
|
||||
#endif
|
||||
|
||||
static int mjson_esc(int c, int esc) {
|
||||
const char *p, *esc1 = "\b\f\n\r\t\\\"", *esc2 = "bfnrt\\\"";
|
||||
for (p = esc ? esc1 : esc2; *p != '\0'; p++) {
|
||||
if (*p == c) return esc ? esc2[p - esc1] : esc1[p - esc2];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mjson_pass_string(const char *s, int len) {
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
if (s[i] == '\\' && i + 1 < len && mjson_esc(s[i + 1], 1)) {
|
||||
i++;
|
||||
} else if (s[i] == '\0') {
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
} else if (s[i] == '"') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int mjson(const char *s, int len, mjson_cb_t cb, void *ud) {
|
||||
enum { S_VALUE, S_KEY, S_COLON, S_COMMA_OR_EOO } expecting = S_VALUE;
|
||||
unsigned char nesting[MJSON_MAX_DEPTH];
|
||||
int i, depth = 0;
|
||||
#define MJSONCALL(ev) \
|
||||
if (cb != NULL && cb(ev, s, start, i - start + 1, ud)) return i + 1;
|
||||
|
||||
// In the ascii table, the distance between `[` and `]` is 2.
|
||||
// Ditto for `{` and `}`. Hence +2 in the code below.
|
||||
#define MJSONEOO() \
|
||||
do { \
|
||||
if (c != nesting[depth - 1] + 2) return MJSON_ERROR_INVALID_INPUT; \
|
||||
depth--; \
|
||||
if (depth == 0) { \
|
||||
MJSONCALL(tok); \
|
||||
return i + 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
int start = i;
|
||||
unsigned char c = ((unsigned char *) s)[i];
|
||||
int tok = c;
|
||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') continue;
|
||||
// printf("- %c [%.*s] %d %d\n", c, i, s, depth, expecting);
|
||||
switch (expecting) {
|
||||
case S_VALUE:
|
||||
if (c == '{') {
|
||||
if (depth >= (int) sizeof(nesting)) return MJSON_ERROR_TOO_DEEP;
|
||||
nesting[depth++] = c;
|
||||
expecting = S_KEY;
|
||||
break;
|
||||
} else if (c == '[') {
|
||||
if (depth >= (int) sizeof(nesting)) return MJSON_ERROR_TOO_DEEP;
|
||||
nesting[depth++] = c;
|
||||
break;
|
||||
} else if (c == ']' && depth > 0) { // Empty array
|
||||
MJSONEOO();
|
||||
} else if (c == 't' && i + 3 < len && memcmp(&s[i], "true", 4) == 0) {
|
||||
i += 3;
|
||||
tok = MJSON_TOK_TRUE;
|
||||
} else if (c == 'n' && i + 3 < len && memcmp(&s[i], "null", 4) == 0) {
|
||||
i += 3;
|
||||
tok = MJSON_TOK_NULL;
|
||||
} else if (c == 'f' && i + 4 < len && memcmp(&s[i], "false", 5) == 0) {
|
||||
i += 4;
|
||||
tok = MJSON_TOK_FALSE;
|
||||
} else if (c == '-' || ((c >= '0' && c <= '9'))) {
|
||||
char *end = NULL;
|
||||
strtod(&s[i], &end);
|
||||
if (end != NULL) i += end - &s[i] - 1;
|
||||
tok = MJSON_TOK_NUMBER;
|
||||
} else if (c == '"') {
|
||||
int n = mjson_pass_string(&s[i + 1], len - i - 1);
|
||||
if (n < 0) return n;
|
||||
i += n + 1;
|
||||
tok = MJSON_TOK_STRING;
|
||||
} else {
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
}
|
||||
if (depth == 0) {
|
||||
MJSONCALL(tok);
|
||||
return i + 1;
|
||||
}
|
||||
expecting = S_COMMA_OR_EOO;
|
||||
break;
|
||||
|
||||
case S_KEY:
|
||||
if (c == '"') {
|
||||
int n = mjson_pass_string(&s[i + 1], len - i - 1);
|
||||
if (n < 0) return n;
|
||||
i += n + 1;
|
||||
tok = MJSON_TOK_KEY;
|
||||
expecting = S_COLON;
|
||||
} else if (c == '}') { // Empty object
|
||||
MJSONEOO();
|
||||
expecting = S_COMMA_OR_EOO;
|
||||
} else {
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
}
|
||||
break;
|
||||
|
||||
case S_COLON:
|
||||
if (c == ':') {
|
||||
expecting = S_VALUE;
|
||||
} else {
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
}
|
||||
break;
|
||||
|
||||
case S_COMMA_OR_EOO:
|
||||
if (depth <= 0) return MJSON_ERROR_INVALID_INPUT;
|
||||
if (c == ',') {
|
||||
expecting = (nesting[depth - 1] == '{') ? S_KEY : S_VALUE;
|
||||
} else if (c == ']' || c == '}') {
|
||||
MJSONEOO();
|
||||
} else {
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
MJSONCALL(tok);
|
||||
}
|
||||
return MJSON_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
struct msjon_get_data {
|
||||
const char *path; // Lookup json path
|
||||
int pos; // Current path index
|
||||
int d1; // Current depth of traversal
|
||||
int d2; // Expected depth of traversal
|
||||
int i1; // Index in an array
|
||||
int i2; // Expected index in an array
|
||||
int obj; // If the value is array/object, offset where it starts
|
||||
const char **tokptr; // Destination
|
||||
int *toklen; // Destination length
|
||||
int tok; // Returned token
|
||||
};
|
||||
|
||||
static int mjson_plen(const char *s) {
|
||||
int i = 0;
|
||||
while (s[i] != '\0' && s[i] != '.' && s[i] != '[') i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
static int mjson_get_cb(int tok, const char *s, int off, int len, void *ud) {
|
||||
struct msjon_get_data *data = (struct msjon_get_data *) ud;
|
||||
// printf("--> %2x %2d %2d %2d %2d\t'%s'\t'%.*s'\t\t'%.*s'\n", tok, data->d1,
|
||||
// data->d2, data->i1, data->i2, data->path + data->pos, off, s, len,
|
||||
// s + off);
|
||||
if (data->tok != MJSON_TOK_INVALID) return 1; // Found
|
||||
|
||||
if (tok == '{') {
|
||||
if (!data->path[data->pos] && data->d1 == data->d2) data->obj = off;
|
||||
data->d1++;
|
||||
} else if (tok == '[') {
|
||||
if (data->d1 == data->d2 && data->path[data->pos] == '[') {
|
||||
data->i1 = 0;
|
||||
data->i2 = (int) strtod(&data->path[data->pos + 1], NULL);
|
||||
if (data->i1 == data->i2) {
|
||||
data->d2++;
|
||||
data->pos += 3;
|
||||
}
|
||||
}
|
||||
if (!data->path[data->pos] && data->d1 == data->d2) data->obj = off;
|
||||
data->d1++;
|
||||
} else if (tok == ',') {
|
||||
if (data->d1 == data->d2 + 1) {
|
||||
data->i1++;
|
||||
if (data->i1 == data->i2) {
|
||||
while (data->path[data->pos] != ']') data->pos++;
|
||||
data->pos++;
|
||||
data->d2++;
|
||||
}
|
||||
}
|
||||
} else if (tok == MJSON_TOK_KEY && data->d1 == data->d2 + 1 &&
|
||||
data->path[data->pos] == '.' && s[off] == '"' &&
|
||||
s[off + len - 1] == '"' &&
|
||||
mjson_plen(&data->path[data->pos + 1]) == len - 2 &&
|
||||
!memcmp(s + off + 1, &data->path[data->pos + 1], len - 2)) {
|
||||
data->d2++;
|
||||
data->pos += len - 1;
|
||||
} else if (tok == MJSON_TOK_KEY && data->d1 == data->d2) {
|
||||
return 1; // Exhausted path, not found
|
||||
} else if (tok == '}' || tok == ']') {
|
||||
data->d1--;
|
||||
// data->d2--;
|
||||
if (!data->path[data->pos] && data->d1 == data->d2 && data->obj != -1) {
|
||||
data->tok = tok - 2;
|
||||
if (data->tokptr) *data->tokptr = s + data->obj;
|
||||
if (data->toklen) *data->toklen = off - data->obj + 1;
|
||||
return 1;
|
||||
}
|
||||
} else if (MJSON_TOK_IS_VALUE(tok)) {
|
||||
// printf("TOK --> %d\n", tok);
|
||||
if (data->d1 == data->d2 && !data->path[data->pos]) {
|
||||
data->tok = tok;
|
||||
if (data->tokptr) *data->tokptr = s + off;
|
||||
if (data->toklen) *data->toklen = len;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
enum mjson_tok mjson_find(const char *s, int len, const char *jp,
|
||||
const char **tokptr, int *toklen) {
|
||||
struct msjon_get_data data = {jp, 1, 0, 0, 0,
|
||||
0, -1, tokptr, toklen, MJSON_TOK_INVALID};
|
||||
if (jp[0] != '$') return MJSON_TOK_INVALID;
|
||||
if (mjson(s, len, mjson_get_cb, &data) < 0) return MJSON_TOK_INVALID;
|
||||
return (enum mjson_tok) data.tok;
|
||||
}
|
||||
|
||||
int mjson_get_number(const char *s, int len, const char *path, double *v) {
|
||||
const char *p;
|
||||
int tok, n;
|
||||
if ((tok = mjson_find(s, len, path, &p, &n)) == MJSON_TOK_NUMBER) {
|
||||
if (v != NULL) *v = strtod(p, NULL);
|
||||
}
|
||||
return tok == MJSON_TOK_NUMBER ? 1 : 0;
|
||||
}
|
||||
|
||||
int mjson_get_bool(const char *s, int len, const char *path, int *v) {
|
||||
int tok = mjson_find(s, len, path, NULL, NULL);
|
||||
if (tok == MJSON_TOK_TRUE && v != NULL) *v = 1;
|
||||
if (tok == MJSON_TOK_FALSE && v != NULL) *v = 0;
|
||||
return tok == MJSON_TOK_TRUE || tok == MJSON_TOK_FALSE ? 1 : 0;
|
||||
}
|
||||
|
||||
static int mjson_unescape(const char *s, int len, char *to, int n) {
|
||||
int i, j;
|
||||
for (i = 0, j = 0; i < len && j < n; i++, j++) {
|
||||
if (s[i] == '\\' && i + 1 < len) {
|
||||
int c = mjson_esc(s[i + 1], 0);
|
||||
if (c == 0) return -1;
|
||||
to[j] = c;
|
||||
i++;
|
||||
} else {
|
||||
to[j] = s[i];
|
||||
}
|
||||
}
|
||||
if (j >= n) return -1;
|
||||
if (n > 0) to[j] = '\0';
|
||||
return j;
|
||||
}
|
||||
|
||||
int mjson_get_string(const char *s, int len, const char *path, char *to,
|
||||
int n) {
|
||||
const char *p;
|
||||
int sz;
|
||||
if (mjson_find(s, len, path, &p, &sz) != MJSON_TOK_STRING) return -1;
|
||||
return mjson_unescape(p + 1, sz - 2, to, n);
|
||||
}
|
||||
|
||||
int mjson_get_hex(const char *s, int len, const char *x, char *to, int n) {
|
||||
const char *p;
|
||||
int i, j, sz;
|
||||
if (mjson_find(s, len, x, &p, &sz) != MJSON_TOK_STRING) return -1;
|
||||
for (i = j = 0; i < sz - 3 && j < n; i += 2, j++) {
|
||||
#define HEXTOI(x) (x >= '0' && x <= '9' ? x - '0' : x - 'W')
|
||||
unsigned char a = *(const unsigned char *) (p + i + 1);
|
||||
unsigned char b = *(const unsigned char *) (p + i + 2);
|
||||
((unsigned char *) to)[j] = (HEXTOI(a) << 4) | HEXTOI(b);
|
||||
}
|
||||
if (j < n) to[j] = '\0';
|
||||
return j;
|
||||
}
|
||||
|
||||
#if MJSON_ENABLE_BASE64
|
||||
static int mjson_base64rev(int c) {
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
return c - 'A';
|
||||
} else if (c >= 'a' && c <= 'z') {
|
||||
return c + 26 - 'a';
|
||||
} else if (c >= '0' && c <= '9') {
|
||||
return c + 52 - '0';
|
||||
} else if (c == '+') {
|
||||
return 62;
|
||||
} else if (c == '/') {
|
||||
return 63;
|
||||
} else {
|
||||
return 64;
|
||||
}
|
||||
}
|
||||
|
||||
int mjson_base64_dec(const char *src, int n, char *dst, int dlen) {
|
||||
const char *end = src + n;
|
||||
int len = 0;
|
||||
while (src + 3 < end && len < dlen) {
|
||||
int a = mjson_base64rev(src[0]), b = mjson_base64rev(src[1]),
|
||||
c = mjson_base64rev(src[2]), d = mjson_base64rev(src[3]);
|
||||
dst[len++] = (a << 2) | (b >> 4);
|
||||
if (src[2] != '=' && len < dlen) {
|
||||
dst[len++] = (b << 4) | (c >> 2);
|
||||
if (src[3] != '=' && len < dlen) {
|
||||
dst[len++] = (c << 6) | d;
|
||||
}
|
||||
}
|
||||
src += 4;
|
||||
}
|
||||
if (len < dlen) dst[len] = '\0';
|
||||
return len;
|
||||
}
|
||||
|
||||
int mjson_get_base64(const char *s, int len, const char *path, char *to,
|
||||
int n) {
|
||||
const char *p;
|
||||
int sz;
|
||||
if (mjson_find(s, len, path, &p, &sz) != MJSON_TOK_STRING) return 0;
|
||||
return mjson_base64_dec(p + 1, sz - 2, to, n);
|
||||
}
|
||||
#endif // MJSON_ENABLE_BASE64
|
||||
|
||||
#if MJSON_ENABLE_NEXT
|
||||
struct nextdata {
|
||||
int off, len, depth, t, vo, arrayindex;
|
||||
int *koff, *klen, *voff, *vlen, *vtype;
|
||||
};
|
||||
|
||||
static int next_cb(int tok, const char *s, int off, int len, void *ud) {
|
||||
struct nextdata *d = (struct nextdata *) ud;
|
||||
// int i;
|
||||
switch (tok) {
|
||||
case '{':
|
||||
case '[':
|
||||
if (d->depth == 0 && tok == '[') d->arrayindex = 0;
|
||||
if (d->depth == 1 && off > d->off) {
|
||||
d->vo = off;
|
||||
d->t = tok == '{' ? MJSON_TOK_OBJECT : MJSON_TOK_ARRAY;
|
||||
if (d->voff) *d->voff = off;
|
||||
if (d->vtype) *d->vtype = d->t;
|
||||
}
|
||||
d->depth++;
|
||||
break;
|
||||
case '}':
|
||||
case ']':
|
||||
d->depth--;
|
||||
if (d->depth == 1 && d->vo) {
|
||||
d->len = off + len;
|
||||
if (d->vlen) *d->vlen = d->len - d->vo;
|
||||
if (d->arrayindex >= 0) {
|
||||
if (d->koff) *d->koff = d->arrayindex; // koff holds array index
|
||||
if (d->klen) *d->klen = 0; // klen holds 0
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (d->depth == 1 && d->arrayindex >= 0) d->arrayindex++;
|
||||
break;
|
||||
case ',':
|
||||
case ':':
|
||||
break;
|
||||
case MJSON_TOK_KEY:
|
||||
if (d->depth == 1 && d->off < off) {
|
||||
if (d->koff) *d->koff = off; // And report back to the user
|
||||
if (d->klen) *d->klen = len; // If we have to
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (d->depth != 1) break;
|
||||
// If we're iterating over the array
|
||||
if (off > d->off) {
|
||||
d->len = off + len;
|
||||
if (d->vlen) *d->vlen = len; // value length
|
||||
if (d->voff) *d->voff = off; // value offset
|
||||
if (d->vtype) *d->vtype = tok; // value type
|
||||
if (d->arrayindex >= 0) {
|
||||
if (d->koff) *d->koff = d->arrayindex; // koff holds array index
|
||||
if (d->klen) *d->klen = 0; // klen holds 0
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (d->arrayindex >= 0) d->arrayindex++;
|
||||
break;
|
||||
}
|
||||
(void) s;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mjson_next(const char *s, int n, int off, int *koff, int *klen, int *voff,
|
||||
int *vlen, int *vtype) {
|
||||
struct nextdata d = {off, 0, 0, 0, 0, -1, koff, klen, voff, vlen, vtype};
|
||||
mjson(s, n, next_cb, &d);
|
||||
return d.len;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MJSON_ENABLE_PRINT
|
||||
int mjson_print_fixed_buf(const char *ptr, int len, void *fndata) {
|
||||
struct mjson_fixedbuf *fb = (struct mjson_fixedbuf *) fndata;
|
||||
int i, left = fb->size - 1 - fb->len;
|
||||
if (left < len) len = left;
|
||||
for (i = 0; i < len; i++) fb->ptr[fb->len + i] = ptr[i];
|
||||
fb->len += len;
|
||||
fb->ptr[fb->len] = '\0';
|
||||
return len;
|
||||
}
|
||||
|
||||
int mjson_print_dynamic_buf(const char *ptr, int len, void *fndata) {
|
||||
char *s, *buf = *(char **) fndata;
|
||||
int curlen = buf == NULL ? 0 : strlen(buf);
|
||||
if ((s = (char *) realloc(buf, curlen + len + 1)) == NULL) {
|
||||
return 0;
|
||||
} else {
|
||||
memcpy(s + curlen, ptr, len);
|
||||
s[curlen + len] = '\0';
|
||||
*(char **) fndata = s;
|
||||
return len;
|
||||
}
|
||||
}
|
||||
|
||||
int mjson_print_null(const char *ptr, int len, void *userdata) {
|
||||
(void) ptr;
|
||||
(void) userdata;
|
||||
return len;
|
||||
}
|
||||
|
||||
int mjson_print_file(const char *ptr, int len, void *userdata) {
|
||||
return fwrite(ptr, 1, len, (FILE *) userdata);
|
||||
}
|
||||
|
||||
int mjson_print_buf(mjson_print_fn_t fn, void *fndata, const char *buf,
|
||||
int len) {
|
||||
return fn(buf, len, fndata);
|
||||
}
|
||||
|
||||
int mjson_print_int(mjson_print_fn_t fn, void *fndata, int value,
|
||||
int is_signed) {
|
||||
char buf[20];
|
||||
int len = snprintf(buf, sizeof(buf), is_signed ? "%d" : "%u", value);
|
||||
return fn(buf, len, fndata);
|
||||
}
|
||||
|
||||
int mjson_print_long(mjson_print_fn_t fn, void *fndata, long value,
|
||||
int is_signed) {
|
||||
char buf[20];
|
||||
const char *fmt = (is_signed ? "%ld" : "%lu");
|
||||
int len = snprintf(buf, sizeof(buf), fmt, value);
|
||||
return fn(buf, len, fndata);
|
||||
}
|
||||
|
||||
int mjson_print_dbl(mjson_print_fn_t fn, void *fndata, double d,
|
||||
const char *fmt) {
|
||||
char buf[40];
|
||||
int n = snprintf(buf, sizeof(buf), fmt, d);
|
||||
return fn(buf, n, fndata);
|
||||
}
|
||||
|
||||
int mjson_print_str(mjson_print_fn_t fn, void *fndata, const char *s, int len) {
|
||||
int i, n = fn("\"", 1, fndata);
|
||||
for (i = 0; i < len; i++) {
|
||||
char c = mjson_esc(s[i], 1);
|
||||
if (c) {
|
||||
n += fn("\\", 1, fndata);
|
||||
n += fn(&c, 1, fndata);
|
||||
} else {
|
||||
n += fn(&s[i], 1, fndata);
|
||||
}
|
||||
}
|
||||
return n + fn("\"", 1, fndata);
|
||||
}
|
||||
|
||||
#if MJSON_ENABLE_BASE64
|
||||
int mjson_print_b64(mjson_print_fn_t fn, void *fndata, const unsigned char *s,
|
||||
int n) {
|
||||
const char *t =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
int i, len = fn("\"", 1, fndata);
|
||||
for (i = 0; i < n; i += 3) {
|
||||
int a = s[i], b = i + 1 < n ? s[i + 1] : 0, c = i + 2 < n ? s[i + 2] : 0;
|
||||
char buf[4] = {t[a >> 2], t[(a & 3) << 4 | (b >> 4)], '=', '='};
|
||||
if (i + 1 < n) buf[2] = t[(b & 15) << 2 | (c >> 6)];
|
||||
if (i + 2 < n) buf[3] = t[c & 63];
|
||||
len += fn(buf, sizeof(buf), fndata);
|
||||
}
|
||||
return len + fn("\"", 1, fndata);
|
||||
}
|
||||
#endif /* MJSON_ENABLE_BASE64 */
|
||||
|
||||
int mjson_vprintf(mjson_print_fn_t fn, void *fndata, const char *fmt,
|
||||
va_list xap) {
|
||||
int i = 0, n = 0;
|
||||
va_list ap;
|
||||
va_copy(ap, xap);
|
||||
while (fmt[i] != '\0') {
|
||||
if (fmt[i] == '%') {
|
||||
char fc = fmt[++i];
|
||||
int is_long = 0;
|
||||
if (fc == 'l') {
|
||||
is_long = 1;
|
||||
fc = fmt[i + 1];
|
||||
}
|
||||
if (fc == 'Q') {
|
||||
char *buf = va_arg(ap, char *);
|
||||
n += mjson_print_str(fn, fndata, buf ? buf : "", buf ? strlen(buf) : 0);
|
||||
} else if (strncmp(&fmt[i], ".*Q", 3) == 0) {
|
||||
int len = va_arg(ap, int);
|
||||
char *buf = va_arg(ap, char *);
|
||||
n += mjson_print_str(fn, fndata, buf, len);
|
||||
i += 2;
|
||||
} else if (fc == 'd' || fc == 'u') {
|
||||
int is_signed = (fc == 'd');
|
||||
if (is_long) {
|
||||
long val = va_arg(ap, long);
|
||||
n += mjson_print_long(fn, fndata, val, is_signed);
|
||||
i++;
|
||||
} else {
|
||||
int val = va_arg(ap, int);
|
||||
n += mjson_print_int(fn, fndata, val, is_signed);
|
||||
}
|
||||
} else if (fc == 'B') {
|
||||
const char *s = va_arg(ap, int) ? "true" : "false";
|
||||
n += mjson_print_buf(fn, fndata, s, strlen(s));
|
||||
} else if (fc == 's') {
|
||||
char *buf = va_arg(ap, char *);
|
||||
n += mjson_print_buf(fn, fndata, buf, strlen(buf));
|
||||
} else if (strncmp(&fmt[i], ".*s", 3) == 0) {
|
||||
int len = va_arg(ap, int);
|
||||
char *buf = va_arg(ap, char *);
|
||||
n += mjson_print_buf(fn, fndata, buf, len);
|
||||
i += 2;
|
||||
} else if (fc == 'g') {
|
||||
n += mjson_print_dbl(fn, fndata, va_arg(ap, double), "%g");
|
||||
} else if (fc == 'f') {
|
||||
n += mjson_print_dbl(fn, fndata, va_arg(ap, double), "%f");
|
||||
#if MJSON_ENABLE_BASE64
|
||||
} else if (fc == 'V') {
|
||||
int len = va_arg(ap, int);
|
||||
const char *buf = va_arg(ap, const char *);
|
||||
n += mjson_print_b64(fn, fndata, (unsigned char *) buf, len);
|
||||
#endif
|
||||
} else if (fc == 'H') {
|
||||
const char *hex = "0123456789abcdef";
|
||||
int i, len = va_arg(ap, int);
|
||||
const unsigned char *p = va_arg(ap, const unsigned char *);
|
||||
n += fn("\"", 1, fndata);
|
||||
for (i = 0; i < len; i++) {
|
||||
n += fn(&hex[(p[i] >> 4) & 15], 1, fndata);
|
||||
n += fn(&hex[p[i] & 15], 1, fndata);
|
||||
}
|
||||
n += fn("\"", 1, fndata);
|
||||
} else if (fc == 'M') {
|
||||
mjson_vprint_fn_t vfn = va_arg(ap, mjson_vprint_fn_t);
|
||||
n += vfn(fn, fndata, &ap);
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
n += mjson_print_buf(fn, fndata, &fmt[i++], 1);
|
||||
}
|
||||
}
|
||||
va_end(xap);
|
||||
va_end(ap);
|
||||
return n;
|
||||
}
|
||||
|
||||
int mjson_printf(mjson_print_fn_t fn, void *fndata, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
int len;
|
||||
va_start(ap, fmt);
|
||||
len = mjson_vprintf(fn, fndata, fmt, ap);
|
||||
va_end(ap);
|
||||
return len;
|
||||
}
|
||||
#endif /* MJSON_ENABLE_PRINT */
|
||||
|
||||
#if MJSON_IMPLEMENT_STRTOD
|
||||
static inline int is_digit(int c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
/* NOTE: strtod() implementation by Yasuhiro Matsumoto. */
|
||||
double strtod(const char *str, char **end) {
|
||||
double d = 0.0;
|
||||
int sign = 1, n = 0;
|
||||
const char *p = str, *a = str;
|
||||
|
||||
/* decimal part */
|
||||
if (*p == '-') {
|
||||
sign = -1;
|
||||
++p;
|
||||
} else if (*p == '+')
|
||||
++p;
|
||||
if (is_digit(*p)) {
|
||||
d = (double) (*p++ - '0');
|
||||
while (*p && is_digit(*p)) {
|
||||
d = d * 10.0 + (double) (*p - '0');
|
||||
++p;
|
||||
++n;
|
||||
}
|
||||
a = p;
|
||||
} else if (*p != '.')
|
||||
goto done;
|
||||
d *= sign;
|
||||
|
||||
/* fraction part */
|
||||
if (*p == '.') {
|
||||
double f = 0.0;
|
||||
double base = 0.1;
|
||||
++p;
|
||||
|
||||
if (is_digit(*p)) {
|
||||
while (*p && is_digit(*p)) {
|
||||
f += base * (*p - '0');
|
||||
base /= 10.0;
|
||||
++p;
|
||||
++n;
|
||||
}
|
||||
}
|
||||
d += f * sign;
|
||||
a = p;
|
||||
}
|
||||
|
||||
/* exponential part */
|
||||
if ((*p == 'E') || (*p == 'e')) {
|
||||
int e = 0;
|
||||
++p;
|
||||
|
||||
sign = 1;
|
||||
if (*p == '-') {
|
||||
sign = -1;
|
||||
++p;
|
||||
} else if (*p == '+')
|
||||
++p;
|
||||
|
||||
if (is_digit(*p)) {
|
||||
while (*p == '0') ++p;
|
||||
e = (int) (*p++ - '0');
|
||||
while (*p && is_digit(*p)) {
|
||||
e = e * 10 + (int) (*p - '0');
|
||||
++p;
|
||||
}
|
||||
e *= sign;
|
||||
} else if (!is_digit(*(a - 1))) {
|
||||
a = str;
|
||||
goto done;
|
||||
} else if (*p == 0)
|
||||
goto done;
|
||||
|
||||
if (d == 2.2250738585072011 && e == -308) {
|
||||
d = 0.0;
|
||||
a = p;
|
||||
goto done;
|
||||
}
|
||||
if (d == 2.2250738585072012 && e <= -308) {
|
||||
d *= 1.0e-308;
|
||||
a = p;
|
||||
goto done;
|
||||
}
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 10; i++) d *= 10;
|
||||
}
|
||||
a = p;
|
||||
} else if (p > str && !is_digit(*(p - 1))) {
|
||||
a = str;
|
||||
goto done;
|
||||
}
|
||||
|
||||
done:
|
||||
if (end) *end = (char *) a;
|
||||
return d;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MJSON_ENABLE_MERGE
|
||||
int mjson_merge(const char *s, int n, const char *s2, int n2,
|
||||
mjson_print_fn_t fn, void *userdata) {
|
||||
int koff, klen, voff, vlen, t, t2, k, off = 0, len = 0, comma = 0;
|
||||
if (n < 2) return len;
|
||||
len += fn("{", 1, userdata);
|
||||
while ((off = mjson_next(s, n, off, &koff, &klen, &voff, &vlen, &t)) != 0) {
|
||||
#if !defined(_MSC_VER) || _MSC_VER >= 1700
|
||||
char path[klen + 1];
|
||||
#else
|
||||
char *path = (char *) alloca(klen + 1);
|
||||
#endif
|
||||
const char *val;
|
||||
memcpy(path, "$.", 2);
|
||||
memcpy(path + 2, s + koff + 1, klen - 2);
|
||||
path[klen] = '\0';
|
||||
if ((t2 = mjson_find(s2, n2, path, &val, &k)) != MJSON_TOK_INVALID) {
|
||||
if (t2 == MJSON_TOK_NULL) continue; // null deletes the key
|
||||
} else {
|
||||
val = s + voff; // Key is not found in the update. Copy the old value.
|
||||
}
|
||||
if (comma) len += fn(",", 1, userdata);
|
||||
len += fn(s + koff, klen, userdata);
|
||||
len += fn(":", 1, userdata);
|
||||
if (t == MJSON_TOK_OBJECT && t2 == MJSON_TOK_OBJECT) {
|
||||
len += mjson_merge(s + voff, vlen, val, k, fn, userdata);
|
||||
} else {
|
||||
if (t2 != MJSON_TOK_INVALID) vlen = k;
|
||||
len += fn(val, vlen, userdata);
|
||||
}
|
||||
comma = 1;
|
||||
}
|
||||
// Add missing keys
|
||||
off = 0;
|
||||
while ((off = mjson_next(s2, n2, off, &koff, &klen, &voff, &vlen, &t)) != 0) {
|
||||
#if !defined(_MSC_VER) || _MSC_VER >= 1700
|
||||
char path[klen + 1];
|
||||
#else
|
||||
char *path = (char *) alloca(klen + 1);
|
||||
#endif
|
||||
const char *val;
|
||||
if (t == MJSON_TOK_NULL) continue;
|
||||
memcpy(path, "$.", 2);
|
||||
memcpy(path + 2, s2 + koff + 1, klen - 2);
|
||||
path[klen] = '\0';
|
||||
if (mjson_find(s, n, path, &val, &vlen) != MJSON_TOK_INVALID) continue;
|
||||
if (comma) len += fn(",", 1, userdata);
|
||||
len += fn(s2 + koff, klen, userdata);
|
||||
len += fn(":", 1, userdata);
|
||||
len += fn(s2 + voff, vlen, userdata);
|
||||
comma = 1;
|
||||
}
|
||||
len += fn("}", 1, userdata);
|
||||
return len;
|
||||
}
|
||||
#endif // MJSON_ENABLE_MERGE
|
||||
|
||||
#if MJSON_ENABLE_PRETTY
|
||||
struct prettydata {
|
||||
int level;
|
||||
int len;
|
||||
int prev;
|
||||
const char *pad;
|
||||
int padlen;
|
||||
mjson_print_fn_t fn;
|
||||
void *userdata;
|
||||
};
|
||||
|
||||
static int pretty_cb(int ev, const char *s, int off, int len, void *ud) {
|
||||
struct prettydata *d = (struct prettydata *) ud;
|
||||
int i;
|
||||
switch (ev) {
|
||||
case '{':
|
||||
case '[':
|
||||
d->level++;
|
||||
d->len += d->fn(s + off, len, d->userdata);
|
||||
break;
|
||||
case '}':
|
||||
case ']':
|
||||
d->level--;
|
||||
if (d->prev != '[' && d->prev != '{' && d->padlen > 0) {
|
||||
d->len += d->fn("\n", 1, d->userdata);
|
||||
for (i = 0; i < d->level; i++)
|
||||
d->len += d->fn(d->pad, d->padlen, d->userdata);
|
||||
}
|
||||
d->len += d->fn(s + off, len, d->userdata);
|
||||
break;
|
||||
case ',':
|
||||
d->len += d->fn(s + off, len, d->userdata);
|
||||
if (d->padlen > 0) {
|
||||
d->len += d->fn("\n", 1, d->userdata);
|
||||
for (i = 0; i < d->level; i++)
|
||||
d->len += d->fn(d->pad, d->padlen, d->userdata);
|
||||
}
|
||||
break;
|
||||
case ':':
|
||||
d->len += d->fn(s + off, len, d->userdata);
|
||||
if (d->padlen > 0) d->len += d->fn(" ", 1, d->userdata);
|
||||
break;
|
||||
case MJSON_TOK_KEY:
|
||||
if (d->prev == '{' && d->padlen > 0) {
|
||||
d->len += d->fn("\n", 1, d->userdata);
|
||||
for (i = 0; i < d->level; i++)
|
||||
d->len += d->fn(d->pad, d->padlen, d->userdata);
|
||||
}
|
||||
d->len += d->fn(s + off, len, d->userdata);
|
||||
break;
|
||||
default:
|
||||
if (d->prev == '[' && d->padlen > 0) {
|
||||
d->len += d->fn("\n", 1, d->userdata);
|
||||
for (i = 0; i < d->level; i++)
|
||||
d->len += d->fn(d->pad, d->padlen, d->userdata);
|
||||
}
|
||||
d->len += d->fn(s + off, len, d->userdata);
|
||||
break;
|
||||
}
|
||||
d->prev = ev;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mjson_pretty(const char *s, int n, const char *pad, mjson_print_fn_t fn,
|
||||
void *userdata) {
|
||||
struct prettydata d = {0, 0, 0, pad, strlen(pad), fn, userdata};
|
||||
if (mjson(s, n, pretty_cb, &d) < 0) return -1;
|
||||
return d.len;
|
||||
}
|
||||
#endif // MJSON_ENABLE_PRETTY
|
||||
|
||||
#if MJSON_ENABLE_RPC
|
||||
struct jsonrpc_ctx jsonrpc_default_context;
|
||||
struct jsonrpc_userdata {
|
||||
mjson_print_fn_t fn;
|
||||
void *fndata;
|
||||
};
|
||||
|
||||
int mjson_globmatch(const char *s1, int n1, const char *s2, int n2) {
|
||||
int i = 0, j = 0, ni = 0, nj = 0;
|
||||
while (i < n1 || j < n2) {
|
||||
if (i < n1 && j < n2 && (s1[i] == '?' || s2[j] == s1[i])) {
|
||||
i++, j++;
|
||||
} else if (i < n1 && (s1[i] == '*' || s1[i] == '#')) {
|
||||
ni = i, nj = j + 1, i++;
|
||||
} else if (nj > 0 && nj <= n2 && (s1[i - 1] == '#' || s2[j] != '/')) {
|
||||
i = ni, j = nj;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int jsonrpc_printer(const char *buf, int len, void *userdata) {
|
||||
struct jsonrpc_userdata *u = (struct jsonrpc_userdata *) userdata;
|
||||
return u->fn(buf, len, u->fndata);
|
||||
}
|
||||
|
||||
void jsonrpc_return_errorv(struct jsonrpc_request *r, int code,
|
||||
const char *message, const char *data_fmt,
|
||||
va_list ap) {
|
||||
if (r->id_len == 0) return;
|
||||
mjson_printf(r->fn, r->fndata,
|
||||
"{\"id\":%.*s,\"error\":{\"code\":%d,\"message\":%Q", r->id_len,
|
||||
r->id, code, message == NULL ? "" : message);
|
||||
if (data_fmt != NULL) {
|
||||
mjson_printf(r->fn, r->fndata, ",\"data\":");
|
||||
mjson_vprintf(r->fn, r->fndata, data_fmt, ap);
|
||||
}
|
||||
mjson_printf(r->fn, r->fndata, "}}\n");
|
||||
}
|
||||
|
||||
void jsonrpc_return_error(struct jsonrpc_request *r, int code,
|
||||
const char *message, const char *data_fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, data_fmt);
|
||||
jsonrpc_return_errorv(r, code, message, data_fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void jsonrpc_return_successv(struct jsonrpc_request *r, const char *result_fmt,
|
||||
va_list ap) {
|
||||
if (r->id_len == 0) return;
|
||||
mjson_printf(r->fn, r->fndata, "{\"id\":%.*s,\"result\":", r->id_len, r->id);
|
||||
if (result_fmt != NULL) {
|
||||
mjson_vprintf(r->fn, r->fndata, result_fmt, ap);
|
||||
} else {
|
||||
mjson_printf(r->fn, r->fndata, "%s", "null");
|
||||
}
|
||||
mjson_printf(r->fn, r->fndata, "}\n");
|
||||
}
|
||||
|
||||
void jsonrpc_return_success(struct jsonrpc_request *r, const char *result_fmt,
|
||||
...) {
|
||||
va_list ap;
|
||||
va_start(ap, result_fmt);
|
||||
jsonrpc_return_successv(r, result_fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void jsonrpc_ctx_process(struct jsonrpc_ctx *ctx, const char *req, int req_sz,
|
||||
mjson_print_fn_t fn, void *fndata) {
|
||||
const char *result = NULL, *error = NULL;
|
||||
int result_sz = 0, error_sz = 0;
|
||||
struct jsonrpc_method *m = NULL;
|
||||
struct jsonrpc_userdata d;
|
||||
struct jsonrpc_request r = {req, req_sz, 0, 0, 0, 0, 0,
|
||||
0, &jsonrpc_printer, NULL, NULL};
|
||||
|
||||
d.fn = fn;
|
||||
d.fndata = fndata;
|
||||
r.fndata = &d;
|
||||
|
||||
// Is is a response frame?
|
||||
mjson_find(req, req_sz, "$.result", &result, &result_sz);
|
||||
if (result == NULL) mjson_find(req, req_sz, "$.error", &error, &error_sz);
|
||||
if (result_sz > 0 || error_sz > 0) {
|
||||
if (ctx->response_cb != NULL) ctx->response_cb(req, req_sz, ctx->userdata);
|
||||
return;
|
||||
}
|
||||
|
||||
// Method must exist and must be a string
|
||||
if (mjson_find(req, req_sz, "$.method", &r.method, &r.method_len) !=
|
||||
MJSON_TOK_STRING) {
|
||||
mjson_printf(fn, fndata, "{\"error\":{\"code\":-32700,\"message\":%.*Q}}\n",
|
||||
req_sz, req);
|
||||
ctx->in_len = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// id and params are optional
|
||||
mjson_find(req, req_sz, "$.id", &r.id, &r.id_len);
|
||||
mjson_find(req, req_sz, "$.params", &r.params, &r.params_len);
|
||||
|
||||
for (m = ctx->methods; m != NULL; m = m->next) {
|
||||
if (mjson_globmatch(m->method, m->method_sz, r.method + 1,
|
||||
r.method_len - 2) > 0) {
|
||||
if (r.params == NULL) r.params = "";
|
||||
r.userdata = m->cbdata;
|
||||
m->cb(&r);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (m == NULL) {
|
||||
jsonrpc_return_error(&r, JSONRPC_ERROR_NOT_FOUND, "method not found", NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static int jsonrpc_print_methods(mjson_print_fn_t fn, void *fndata,
|
||||
va_list *ap) {
|
||||
struct jsonrpc_ctx *ctx = va_arg(*ap, struct jsonrpc_ctx *);
|
||||
struct jsonrpc_method *m;
|
||||
int len = 0;
|
||||
for (m = ctx->methods; m != NULL; m = m->next) {
|
||||
if (m != ctx->methods) len += mjson_print_buf(fn, fndata, ",", 1);
|
||||
len += mjson_print_str(fn, fndata, m->method, strlen(m->method));
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
static void rpclist(struct jsonrpc_request *r) {
|
||||
jsonrpc_return_success(r, "[%M]", jsonrpc_print_methods, r->userdata);
|
||||
}
|
||||
|
||||
void jsonrpc_ctx_init(struct jsonrpc_ctx *ctx, mjson_print_fn_t response_cb,
|
||||
void *userdata) {
|
||||
ctx->response_cb = response_cb;
|
||||
ctx->userdata = userdata;
|
||||
jsonrpc_ctx_export(ctx, MJSON_RPC_LIST_NAME, rpclist, ctx);
|
||||
}
|
||||
|
||||
void jsonrpc_ctx_process_byte(struct jsonrpc_ctx *ctx, unsigned char ch,
|
||||
mjson_print_fn_t fn, void *p) {
|
||||
if (ctx->in_len >= (int) sizeof(ctx->in)) ctx->in_len = 0; // Overflow
|
||||
if (ch == '\n') { // If new line, parse frame
|
||||
if (ctx->in_len > 1) jsonrpc_ctx_process(ctx, ctx->in, ctx->in_len, fn, p);
|
||||
ctx->in_len = 0;
|
||||
} else {
|
||||
ctx->in[ctx->in_len] = ch; // Append to the buffer
|
||||
ctx->in_len++;
|
||||
}
|
||||
}
|
||||
|
||||
void jsonrpc_init(mjson_print_fn_t response_cb, void *userdata) {
|
||||
jsonrpc_ctx_init(&jsonrpc_default_context, response_cb, userdata);
|
||||
}
|
||||
#endif // MJSON_ENABLE_RPC
|
@ -1,220 +0,0 @@
|
||||
// Copyright (c) 2018-2020 Cesanta Software Limited
|
||||
// All rights reserved
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
#ifndef MJSON_H
|
||||
#define MJSON_H
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef MJSON_ENABLE_PRINT
|
||||
#define MJSON_ENABLE_PRINT 1
|
||||
#endif
|
||||
|
||||
#ifndef MJSON_ENABLE_RPC
|
||||
#define MJSON_ENABLE_RPC 1
|
||||
#endif
|
||||
|
||||
#ifndef MJSON_ENABLE_BASE64
|
||||
#define MJSON_ENABLE_BASE64 1
|
||||
#endif
|
||||
|
||||
#ifndef MJSON_ENABLE_MERGE
|
||||
#define MJSON_ENABLE_MERGE 0
|
||||
#elif MJSON_ENABLE_MERGE
|
||||
#define MJSON_ENABLE_NEXT 1
|
||||
#endif
|
||||
|
||||
#ifndef MJSON_ENABLE_PRETTY
|
||||
#define MJSON_ENABLE_PRETTY 0
|
||||
#elif MJSON_ENABLE_PRETTY
|
||||
#define MJSON_ENABLE_NEXT 1
|
||||
#endif
|
||||
|
||||
#ifndef MJSON_ENABLE_NEXT
|
||||
#define MJSON_ENABLE_NEXT 0
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef MJSON_RPC_IN_BUF_SIZE
|
||||
#define MJSON_RPC_IN_BUF_SIZE 256
|
||||
#endif
|
||||
|
||||
#ifndef MJSON_RPC_LIST_NAME
|
||||
#define MJSON_RPC_LIST_NAME "rpc.list"
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum {
|
||||
MJSON_ERROR_INVALID_INPUT = -1,
|
||||
MJSON_ERROR_TOO_DEEP = -2,
|
||||
};
|
||||
|
||||
enum mjson_tok {
|
||||
MJSON_TOK_INVALID = 0,
|
||||
MJSON_TOK_KEY = 1,
|
||||
MJSON_TOK_STRING = 11,
|
||||
MJSON_TOK_NUMBER = 12,
|
||||
MJSON_TOK_TRUE = 13,
|
||||
MJSON_TOK_FALSE = 14,
|
||||
MJSON_TOK_NULL = 15,
|
||||
MJSON_TOK_ARRAY = 91,
|
||||
MJSON_TOK_OBJECT = 123,
|
||||
};
|
||||
#define MJSON_TOK_IS_VALUE(t) ((t) > 10 && (t) < 20)
|
||||
|
||||
typedef int (*mjson_cb_t)(int ev, const char *s, int off, int len, void *ud);
|
||||
|
||||
#ifndef MJSON_MAX_DEPTH
|
||||
#define MJSON_MAX_DEPTH 20
|
||||
#endif
|
||||
|
||||
int mjson(const char *s, int len, mjson_cb_t cb, void *ud);
|
||||
enum mjson_tok mjson_find(const char *s, int len, const char *jp,
|
||||
const char **tokptr, int *toklen);
|
||||
int mjson_get_number(const char *s, int len, const char *path, double *v);
|
||||
int mjson_get_bool(const char *s, int len, const char *path, int *v);
|
||||
int mjson_get_string(const char *s, int len, const char *path, char *to, int n);
|
||||
int mjson_get_hex(const char *s, int len, const char *path, char *to, int n);
|
||||
|
||||
#if MJSON_ENABLE_NEXT
|
||||
int mjson_next(const char *s, int n, int off, int *koff, int *klen, int *voff,
|
||||
int *vlen, int *vtype);
|
||||
#endif
|
||||
|
||||
#if MJSON_ENABLE_BASE64
|
||||
int mjson_get_base64(const char *s, int len, const char *path, char *to, int n);
|
||||
int mjson_base64_dec(const char *src, int n, char *dst, int dlen);
|
||||
#endif
|
||||
|
||||
#if MJSON_ENABLE_PRINT
|
||||
typedef int (*mjson_print_fn_t)(const char *buf, int len, void *userdata);
|
||||
typedef int (*mjson_vprint_fn_t)(mjson_print_fn_t, void *, va_list *);
|
||||
|
||||
struct mjson_fixedbuf {
|
||||
char *ptr;
|
||||
int size, len;
|
||||
};
|
||||
|
||||
int mjson_printf(mjson_print_fn_t, void *, const char *fmt, ...);
|
||||
int mjson_vprintf(mjson_print_fn_t, void *, const char *fmt, va_list ap);
|
||||
int mjson_print_str(mjson_print_fn_t, void *, const char *s, int len);
|
||||
int mjson_print_int(mjson_print_fn_t, void *, int value, int is_signed);
|
||||
int mjson_print_long(mjson_print_fn_t, void *, long value, int is_signed);
|
||||
int mjson_print_buf(mjson_print_fn_t fn, void *, const char *buf, int len);
|
||||
|
||||
int mjson_print_null(const char *ptr, int len, void *userdata);
|
||||
int mjson_print_file(const char *ptr, int len, void *userdata);
|
||||
int mjson_print_fixed_buf(const char *ptr, int len, void *userdata);
|
||||
int mjson_print_dynamic_buf(const char *ptr, int len, void *userdata);
|
||||
|
||||
#if MJSON_ENABLE_PRETTY
|
||||
int mjson_pretty(const char *, int, const char *, mjson_print_fn_t, void *);
|
||||
#endif
|
||||
|
||||
#if MJSON_ENABLE_MERGE
|
||||
int mjson_merge(const char *, int, const char *, int, mjson_print_fn_t, void *);
|
||||
#endif
|
||||
|
||||
#endif // MJSON_ENABLE_PRINT
|
||||
|
||||
#if MJSON_ENABLE_RPC
|
||||
|
||||
void jsonrpc_init(mjson_print_fn_t, void *userdata);
|
||||
int mjson_globmatch(const char *s1, int n1, const char *s2, int n2);
|
||||
|
||||
struct jsonrpc_request {
|
||||
const char *frame; // Points to the whole frame
|
||||
int frame_len; // Frame length
|
||||
const char *params; // Points to the "params" in the request frame
|
||||
int params_len; // Length of the "params"
|
||||
const char *id; // Points to the "id" in the request frame
|
||||
int id_len; // Length of the "id"
|
||||
const char *method; // Points to the "method" in the request frame
|
||||
int method_len; // Length of the "method"
|
||||
mjson_print_fn_t fn; // Printer function
|
||||
void *fndata; // Printer function data
|
||||
void *userdata; // Callback's user data as specified at export time
|
||||
};
|
||||
|
||||
struct jsonrpc_method {
|
||||
const char *method;
|
||||
int method_sz;
|
||||
void (*cb)(struct jsonrpc_request *);
|
||||
void *cbdata;
|
||||
struct jsonrpc_method *next;
|
||||
};
|
||||
|
||||
// Main RPC context, stores current request information and a list of
|
||||
// exported RPC methods.
|
||||
struct jsonrpc_ctx {
|
||||
struct jsonrpc_method *methods;
|
||||
void *userdata;
|
||||
mjson_print_fn_t response_cb;
|
||||
int in_len;
|
||||
char in[MJSON_RPC_IN_BUF_SIZE];
|
||||
};
|
||||
|
||||
// Registers function fn under the given name within the given RPC context
|
||||
#define jsonrpc_ctx_export(ctx, name, fn, ud) \
|
||||
do { \
|
||||
static struct jsonrpc_method m = {(name), sizeof(name) - 1, (fn), 0, 0}; \
|
||||
m.cbdata = (ud); \
|
||||
m.next = (ctx)->methods; \
|
||||
(ctx)->methods = &m; \
|
||||
} while (0)
|
||||
|
||||
void jsonrpc_ctx_init(struct jsonrpc_ctx *ctx, mjson_print_fn_t, void *);
|
||||
void jsonrpc_return_error(struct jsonrpc_request *r, int code,
|
||||
const char *message, const char *data_fmt, ...);
|
||||
void jsonrpc_return_success(struct jsonrpc_request *r, const char *result_fmt,
|
||||
...);
|
||||
void jsonrpc_ctx_process(struct jsonrpc_ctx *ctx, const char *req, int req_sz,
|
||||
mjson_print_fn_t fn, void *fndata);
|
||||
void jsonrpc_ctx_process_byte(struct jsonrpc_ctx *ctx, unsigned char ch,
|
||||
mjson_print_fn_t fn, void *fndata);
|
||||
|
||||
extern struct jsonrpc_ctx jsonrpc_default_context;
|
||||
|
||||
#define jsonrpc_export(name, fn, ud) \
|
||||
jsonrpc_ctx_export(&jsonrpc_default_context, (name), (fn), (ud))
|
||||
|
||||
#define jsonrpc_process(buf, len, fn, data) \
|
||||
jsonrpc_ctx_process(&jsonrpc_default_context, (buf), (len), (fn), (data))
|
||||
|
||||
#define jsonrpc_process_byte(x, fn, data) \
|
||||
jsonrpc_ctx_process_byte(&jsonrpc_default_context, (x), (fn), (data))
|
||||
|
||||
#define JSONRPC_ERROR_INVALID -32700 /* Invalid JSON was received */
|
||||
#define JSONRPC_ERROR_NOT_FOUND -32601 /* The method does not exist */
|
||||
#define JSONRPC_ERROR_BAD_PARAMS -32602 /* Invalid params passed */
|
||||
#define JSONRPC_ERROR_INTERNAL -32603 /* Internal JSON-RPC error */
|
||||
|
||||
#endif // MJSON_ENABLE_RPC
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif // MJSON_H
|
2360
examples/complete/packed_fs.c
Normal file
@ -1,312 +0,0 @@
|
||||
const {h, render, Component} = preact;
|
||||
const html = htm.bind(h);
|
||||
|
||||
class Login extends Component {
|
||||
state = {user: '', pass: ''};
|
||||
onclick = (app) => {
|
||||
const authhdr = 'Basic ' + btoa(this.state.user + ':' + this.state.pass);
|
||||
const headers = {Authorization: authhdr};
|
||||
return axios.get('/api/login', {headers})
|
||||
.then(res => app.setUser(res.data.token))
|
||||
.catch(err => alert('Login failed'));
|
||||
};
|
||||
onpassinput = (ev) => this.setState({pass: ev.target.value});
|
||||
onuserinput = (ev) => this.setState({user: ev.target.value});
|
||||
render({app}, {user, pass, signup}) {
|
||||
return html`
|
||||
<div class='mx-auto bg-light rounded border my-5' style='max-width: 480px;'>
|
||||
<div class='form p-5 rounded form-sm'>
|
||||
<h4 class="text-muted mb-4">Login </h4>
|
||||
<input type='email' placeholder='Email' class='my-2 form-control'
|
||||
oninput=${this.onuserinput} value=${user} />
|
||||
<input type="password" placeholder="Password" class="my-2 form-control"
|
||||
oninput=${this.onpassinput} value=${pass}
|
||||
onchange=${ev => this.onclick(app)} />
|
||||
<div class="mb-4">
|
||||
<button class="btn btn-info btn-block"
|
||||
disabled=${!user || !pass} onclick="${ev => this.onclick(app)}"
|
||||
> Login </button>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
Valid logins: admin:admin, user1:pass1, user2:pass2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
class Dropdown extends Component {
|
||||
state = {show: false};
|
||||
render(props, state) {
|
||||
const onclick = x => this.setState({show: x});
|
||||
const show = state.show ? 'show' : '';
|
||||
return html`
|
||||
<div class="dropdown autoexpand ${props.cls}">
|
||||
<div type="buttonx" onclick=${() => onclick(!state.show)}
|
||||
class="dropdown-toggle my-0 ${props.bcls}"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
${props.title}
|
||||
</div>
|
||||
<div onclick=${() => onclick(false)} style=${props.dstyle}
|
||||
class="dropdown-menu ${props.dcls} ${show}">
|
||||
${props.children}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
const NavLink = ({href, title, url}) => html`<a class="nav-item nav-link
|
||||
${url == href ? 'active' : ''}"
|
||||
target=${href.match(/^http/) ? '_blank' : ''}
|
||||
href="${href.match(/^http/) ? '' : '#'}${href}">${title}</a>`;
|
||||
|
||||
class Header extends Component {
|
||||
state = {expanded: false};
|
||||
ontoggle = () => this.setState({expanded: !this.state.expanded});
|
||||
render(props, state) {
|
||||
const u = props.app.state.user || {};
|
||||
return html`
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<a class="navbar-brand" href="#">
|
||||
<img src="images/logo.png" width="26" height="26" alt="" class="mr-2" />
|
||||
MyProduct
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"
|
||||
onclick=${() => this.ontoggle()} >
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse ${state.expanded ? '' : 'collapse'}"
|
||||
id="navbarNav">
|
||||
<div class="nav navbar-nav mr-auto">
|
||||
<${NavLink} href="/" title="Dashboard" url=${props.url} />
|
||||
<${NavLink} href="/logs" title="Logs" url=${props.url} />
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-inline">
|
||||
<${Dropdown} title="${u.user}" cls="mr-2"
|
||||
bcls="btn btn-sm btn-outline-light pointer" dcls="m-0 dropdown-menu-right">
|
||||
<div onclick=${() => props.app.setUser('')}
|
||||
class="dropdown-item small pointer text-center">logout</div>
|
||||
</${Dropdown}>
|
||||
<img src="images/user.png" class="rounded-circle nav-item mr-2" width="30" />
|
||||
</form>
|
||||
</nav>
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const Info = () => html`<div class="alert alert-secondary my-3">
|
||||
This dashboard shows values kept in server memory. Many clients
|
||||
can open this page. As soon as any client changes any value,
|
||||
all clients update automatically.
|
||||
The JS code that watches state changes, reconnects on network failures.
|
||||
That means if server restarts, dashboard on all connected clients
|
||||
refresh automatically.
|
||||
You can use <code>curl</code> command-line utility:
|
||||
<br/><code>curl localhost:8000/api/config/get</code>
|
||||
<br/><code>curl localhost:8000/api/config/watch</code>
|
||||
<br/><code>curl localhost:8000/api/config/set -d '{"a":123}'</code>
|
||||
<br/><span class="badge badge-danger">NOTE:</span> administrators
|
||||
can change settings values, whilst users cannot.
|
||||
</div>`;
|
||||
|
||||
class DashVideo extends Component {
|
||||
render(props, state) {
|
||||
const onclick = ev => axios.post(
|
||||
'/api/config/set', {value1: +state.value1, value2: state.value2});
|
||||
// alert(JSON.stringify(state));
|
||||
return html`<div id="form">
|
||||
<div><b>Change values</b></div>
|
||||
<table>
|
||||
<tr><td>Value 1 (number):</td><td><input value=${state.value1}
|
||||
onInput=${linkState(this, 'value1')} /></td></tr>
|
||||
<tr><td>Value 2 (string):</td><td><input value=${state.value2}
|
||||
onInput=${linkState(this, 'value2')}/></td></tr>
|
||||
<tr><td></td>
|
||||
<td><button onClick=${onclick}>Save values</button></td></tr>
|
||||
</table>
|
||||
</div>`
|
||||
}
|
||||
};
|
||||
|
||||
class DashForm extends Component {
|
||||
render(props, state) {
|
||||
const onclick = ev => axios.post(
|
||||
'/api/config/set', {value1: +state.value1, value2: state.value2});
|
||||
// alert(JSON.stringify(state));
|
||||
return html`<div id="form">
|
||||
<div><b>Change values</b></div>
|
||||
<table>
|
||||
<tr><td>Value 1 (number):</td><td><input class="form-control form-control-sm"
|
||||
value=${state.value1}
|
||||
onInput=${linkState(this, 'value1')} /></td></tr>
|
||||
<tr><td>Value 2 (string):</td><td><input class="form-control form-control-sm"
|
||||
value=${state.value2}
|
||||
onInput=${linkState(this, 'value2')}/></td></tr>
|
||||
<tr><td></td>
|
||||
<td><button class="btn btn-primary btn-block"
|
||||
onClick=${onclick}>Save values</button></td></tr>
|
||||
</table>
|
||||
</div>`
|
||||
}
|
||||
};
|
||||
|
||||
class DashSettings extends Component {
|
||||
state = {value1: null, value2: null};
|
||||
componentDidMount() {
|
||||
axios.get('/api/config/get')
|
||||
.then(r => this.setState(r.data))
|
||||
.catch(e => console.log(e));
|
||||
var self = this;
|
||||
var f = function(reader) {
|
||||
return reader.read().then(function(result) {
|
||||
var data = String.fromCharCode.apply(null, result.value);
|
||||
self.setState(JSON.parse(data));
|
||||
// console.log(JSON.parse(data));
|
||||
if (!result.done) return f(reader);
|
||||
});
|
||||
};
|
||||
fetch('/api/config/watch')
|
||||
.then(r => r.body.getReader())
|
||||
.then(f)
|
||||
.catch(e => setTimeout(x => self.componentDidMount(), 1000));
|
||||
}
|
||||
render(props, state) {
|
||||
return html
|
||||
`<div id="dashboard" style="display: flex; justify-content: flex-evenly;">
|
||||
<div style="width:50%;text-align:center;">Value 1 (number):
|
||||
<div style="font-size:140%;"><b>${state.value1}</b></div></div>
|
||||
<div style="width:50%;text-align:center;">Value 2 (string):
|
||||
<div style="font-size:140%;"><b>${state.value2}</b></div></div>
|
||||
</div>`
|
||||
}
|
||||
};
|
||||
|
||||
class AdminDashboard extends Component {
|
||||
render(props, state) {
|
||||
return html`<div class="container-fluid">
|
||||
<${Info} />
|
||||
<div class="card-deck">
|
||||
<div class="card">
|
||||
<div class="card-header">Settings</div>
|
||||
<div class="card-body"><${DashSettings} /></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Video stream</div>
|
||||
<div class="card-body"><img src="/api/video1" class="mw-100" /></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Change settings</div>
|
||||
<div class="card-body"><${DashForm} /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
class UserDashboard extends Component {
|
||||
render(props, state) {
|
||||
return html`<div class="container-fluid">
|
||||
<${Info} />
|
||||
<div class="card-deck">
|
||||
<div class="card">
|
||||
<div class="card-header">Settings</div>
|
||||
<div class="card-body"><${DashSettings} /></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Video stream</div>
|
||||
<div class="card-body"><img src="/api/video1" class="mw-100" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
class Logs extends Component {
|
||||
state = {live: '', static: ''};
|
||||
componentDidMount() {
|
||||
var self = this;
|
||||
var f = function(r) {
|
||||
return r.read().then(function(result) {
|
||||
var data = String.fromCharCode.apply(null, result.value);
|
||||
var live = self.state.live + data;
|
||||
self.setState({live}); // Append live log
|
||||
if (!result.done) f(r); // Read next chunk
|
||||
});
|
||||
};
|
||||
fetch('/api/log/static')
|
||||
.then(r => r.text())
|
||||
.then(r => {
|
||||
self.setState({static: r});
|
||||
})
|
||||
.then(r => fetch('/api/log/live'))
|
||||
.then(r => r.body.getReader())
|
||||
.then(f);
|
||||
}
|
||||
render(props, state) {
|
||||
return html`<div class="container-fluid">
|
||||
<div class="alert alert-secondary my-3">
|
||||
A div below shows file log.txt, which is produced by the server.
|
||||
The server appends a new log message to that file every second,
|
||||
and a div below also shows that, implementing a "live log" feature.
|
||||
JS code on this page first fetches /api/log/static that returns
|
||||
log.txt contents, then fetches /api/log/live that does not return,
|
||||
but feeds chunks of data as they arrive (live log).
|
||||
<br/><br/>
|
||||
You can also use <code>curl</code> command-line utility to see
|
||||
live logs:
|
||||
<br/><code>curl localhost:8000/api/log/live</code>
|
||||
</div>
|
||||
<div class="card-deck">
|
||||
<div class="card">
|
||||
<div class="card-header">Static</div>
|
||||
<div class="card-body"><pre class="log">${state.static}</pre></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">Live</div>
|
||||
<div class="card-body"><pre class="log">${state.live}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
state = {user: null, url: '/'};
|
||||
setUser(token) {
|
||||
const maxAge = token ? 86400 : 0;
|
||||
document.cookie = `access_token=${token};path=/;max-age=${maxAge}`;
|
||||
// this.setState({token});
|
||||
return axios.get('/api/login')
|
||||
.then(res => this.setState({user: res.data}))
|
||||
.catch(err => this.setState({user: {}}));
|
||||
}
|
||||
componentDidMount() {
|
||||
const getCookie = name => {
|
||||
var v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
|
||||
return v ? v[2] : '';
|
||||
};
|
||||
this.setUser(getCookie('access_token')); // Move from state1 to 2 or 3
|
||||
}
|
||||
render(props, state) {
|
||||
// We have three states:
|
||||
// 1. Beginning. We don't know if we have a valid auth token or not
|
||||
// 2. We sent an /api/login request, and failed to login
|
||||
// 3. We sent an /api/login request, and succeeded to login
|
||||
|
||||
if (!state.user) return ''; // State 1
|
||||
if (!state.user.user) return h(Login, {app: this}); // State 2
|
||||
return h( // State 3
|
||||
'div', {}, h(Header, {url: state.url, app: this}),
|
||||
h(preactRouter.Router, {
|
||||
history: History.createHashHistory(),
|
||||
onChange: ev => this.setState({url: ev.url}),
|
||||
},
|
||||
h(state.user.user == 'admin' ? AdminDashboard : UserDashboard,
|
||||
{default: true, app: this}),
|
||||
h(Logs, {path: 'logs', app: this})));
|
||||
}
|
||||
};
|
||||
|
||||
window.onload = () => render(h(App), document.body);
|
3
examples/complete/web_root/axios.min.js
vendored
7
examples/complete/web_root/bootstrap.min.css
vendored
1
examples/complete/web_root/history.min.js
vendored
1
examples/complete/web_root/htm.min.js
vendored
@ -1 +0,0 @@
|
||||
!function(){function n(n){var t=e.get(this);return t||(t=new Map,e.set(this,t)),1<(t=a(this,t.get(n)||(t.set(n,t=function(n){function t(n){1===u&&(n||(r=r.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?p.push(0,n,r):3===u&&(n||r)?(p.push(3,n,r),u=2):2===u&&"..."===r&&n?p.push(4,n,0):2===u&&r&&!n?p.push(5,0,!0,r):5<=u&&((r||!n&&5===u)&&(p.push(u,0,r,s),u=6),n&&(p.push(u,n,0,s),u=6)),r=""}for(var e,s,u=1,r="",h="",p=[0],a=0;a<n.length;a++){a&&(1===u&&t(),t(a));for(var o=0;o<n[a].length;o++)e=n[a][o],1===u?"<"===e?(t(),p=[p],u=3):r+=e:4===u?r="--"===r&&">"===e?(u=1,""):e+r[0]:h?e===h?h="":r+=e:'"'===e||"'"===e?h=e:">"===e?(t(),u=1):u&&("="===e?(u=5,s=r,r=""):"/"===e&&(u<5||">"===n[a][o+1])?(t(),3===u&&(p=p[0]),(p=(u=p)[0]).push(2,0,u),u=0):" "===e||"\t"===e||"\n"===e||"\r"===e?(t(),u=2):r+=e),3===u&&"!--"===r&&(u=4,p=p[0])}return t(),p}(n)),t),arguments,[])).length?t:t[0]}var a=function(n,t,e,s){var u;t[0]=0;for(var r=1;r<t.length;r++){var h=t[r++],p=t[r]?(t[0]|=h?1:2,e[t[r++]]):t[++r];3===h?s[0]=p:4===h?s[1]=Object.assign(s[1]||{},p):5===h?(s[1]=s[1]||{})[t[++r]]=p:6===h?s[1][t[++r]]+=p+"":h?(u=n.apply(p,a(n,p,e,["",null])),s.push(u),p[0]?t[0]|=2:(t[r-2]=0,t[r]=u)):s.push(p)}return s},e=new Map;"undefined"!=typeof module?module.exports=n:self.htm=n}();
|
Before Width: | Height: | Size: 3.2 KiB |
@ -1,19 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>example</title>
|
||||
<title>Device Dashboard</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="preact.min.js"></script>
|
||||
<script src="preact-router.min.js"></script>
|
||||
<script src="htm.min.js"></script>
|
||||
<script src="history.min.js"></script>
|
||||
<script src="linkstate.min.js"></script>
|
||||
<script src="axios.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
<script type="module" src="main.js"></script>
|
||||
</html>
|
||||
|
18
examples/complete/web_root/linkstate.min.js
vendored
@ -1,18 +0,0 @@
|
||||
function linkState(component, key, eventPath) {
|
||||
let path = key.split('.'), cache = component.__lsc || (component.__lsc = {});
|
||||
|
||||
return cache[key + eventPath] || (cache[key + eventPath] = function(e) {
|
||||
let t = e && e.target || this, state = {}, obj = state,
|
||||
v = typeof eventPath === 'string' ?
|
||||
delve(e, eventPath) :
|
||||
t.nodeName ? (t.type.match(/^che|rad/) ? t.checked : t.value) :
|
||||
e,
|
||||
i = 0;
|
||||
for (; i < path.length - 1; i++) {
|
||||
obj = obj[path[i]] ||
|
||||
(obj[path[i]] = !i && component.state[path[i]] || {});
|
||||
}
|
||||
obj[path[i]] = v;
|
||||
component.setState(state);
|
||||
});
|
||||
}
|
249
examples/complete/web_root/main.js
Normal file
@ -0,0 +1,249 @@
|
||||
'use strict';
|
||||
import {Component, h, html, render, useEffect, useState} from './preact.min.js';
|
||||
|
||||
const Nav = props => html`
|
||||
<div style="background: #333; padding: 0.5em; color: #fff;">
|
||||
<div class="container d-flex">
|
||||
<div style="flex: 1 1 auto; display: flex; align-items: center;">
|
||||
<b>Your Product</b>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; flex: 0 0 auto; ">
|
||||
<span>Logged in as:</span>
|
||||
<span style="padding: 0 0.5em;"><img src="user.png" height="22" /></span>
|
||||
<span>${props.user}</span>
|
||||
<a class="btn" onclick=${props.logout}
|
||||
style="margin-left: 1em; font-size: 0.8em; background: #8aa;">logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const Footer = props => html`
|
||||
<div style="color: silver; margin-top: 2em; padding-top: 0.5em; border-top: 1px solid #ccc; ">
|
||||
Copyright (c) Your Company
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
const Hero = props => html`
|
||||
<div style="margin-top: 1em; background: #eee; padding: 1em; border-radius: 0.5em; color: #777; ">
|
||||
<h1 style="margin: 0.2em 0;">Interactive Device Dashboard</h1>
|
||||
|
||||
This device dashboard is developed with modern and compact Preact framework,
|
||||
in order to fit on a very small devices. This is
|
||||
a <a href="https://mongoose.ws/tutorials/http-server/">hybrid server</a> which
|
||||
provides both static and dynamic content. Static files, like CSS/JS/HTML
|
||||
or images, are compiled into the server binary. Dynamic content is
|
||||
served via the REST API.
|
||||
|
||||
This dashboard shows values kept in server memory. Many clients can open
|
||||
this page. The JS code that watches state changes, reconnects on network
|
||||
failures. That means if server restarts, dashboard on all connected clients
|
||||
refresh automatically.
|
||||
|
||||
NOTE: administrators can change settings values, whilst users cannot.
|
||||
|
||||
<p>
|
||||
This UI uses the REST API implemented by the server, which you can examine
|
||||
using <code>curl</code> command-line utility:
|
||||
</p>
|
||||
|
||||
<div><code>curl localhost:8000/api/config/get</code> - get current device configuration</div>
|
||||
<div><code>curl localhost:8000/api/config/set -d 'value1=7&value2=hello'</code> - set device configuration</div>
|
||||
<div><code>curl localhost:8000/api/message/send -d 'msg=hello'</code> - send chat message</div>
|
||||
<div><code>curl localhost:8000/api/watch</code> - get notifications on config changes or chat messages</div>
|
||||
|
||||
</div>`;
|
||||
|
||||
const ShowSettings = function(props) {
|
||||
return html`
|
||||
<div style="margin: 0 0.3em;">
|
||||
<h3 style="background: #59d; color: #fff;padding: 0.4em;">Device configuration</h3>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<span class="addon">value1:</span>
|
||||
<input disabled type="text" class="smooth"
|
||||
value=${(props.config || {}).value1 || ''} />
|
||||
</div>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<span class="addon">value2:</span>
|
||||
<input disabled type="text" class="smooth"
|
||||
value=${(props.config || {}).value2 || ''} />
|
||||
</div>
|
||||
<div class="msg">
|
||||
Server's corresponding runtime configuration structure:
|
||||
<pre>
|
||||
struct config {
|
||||
int value1;
|
||||
char *value2;
|
||||
} s_config;
|
||||
</pre>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const ChangeSettings = function(props) {
|
||||
const [value1, setValue1] = useState('');
|
||||
const [value2, setValue2] = useState('');
|
||||
useEffect(() => {
|
||||
setValue1(props.config.value1);
|
||||
setValue2(props.config.value2);
|
||||
}, [props.config.value1, props.config.value2])
|
||||
const update = (name, val) => fetch('/api/config/set', {
|
||||
method: 'post',
|
||||
body: `${name}=${encodeURIComponent(val)}`
|
||||
}).catch(err => err);
|
||||
return html`
|
||||
<div style="margin: 0 0.3em;">
|
||||
<h3 style="background: #c03434; color: #fff; padding: 0.4em;">
|
||||
Change configuration</h3>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<span class="addon">value1:</span>
|
||||
<input type="text" value=${value1}
|
||||
oninput=${ev => setValue1(ev.target.value)} />
|
||||
<button class="btn" disabled=${!value1}
|
||||
onclick=${ev => update('value1', value1)}
|
||||
style="margin-left: 1em; background: #8aa;">Update</button>
|
||||
</div>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<span class="addon">value2:</span>
|
||||
<input type="text" value=${value2}
|
||||
oninput=${ev => setValue2(ev.target.value)} />
|
||||
<button class="btn" disabled=${!value2}
|
||||
onclick=${ev => update('value2', value2)}
|
||||
style="margin-left: 1em; background: #8aa;">Update</button>
|
||||
</div>
|
||||
<div class="msg">
|
||||
As soon as administrator updates configuration,
|
||||
server iterates over all connected clients and sends update
|
||||
notifications to all of them - so they update automatically.
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const Login = function(props) {
|
||||
const [user, setUser] = useState('');
|
||||
const [pass, setPass] = useState('');
|
||||
const login = ev =>
|
||||
fetch(
|
||||
'/api/login',
|
||||
{headers: {Authorization: 'Basic ' + btoa(user + ':' + pass)}})
|
||||
.then(r => r.json())
|
||||
.then(r => r && props.login(r))
|
||||
.catch(err => err);
|
||||
return html`
|
||||
<div class="rounded border" style="max-width: 480px; margin: 0 auto; margin-top: 5em; background: #eee; ">
|
||||
<div style="padding: 2em; ">
|
||||
<h1 style="color: #666;">Device Dashboard Login </h1>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<input type='text' placeholder='Name' style="width: 100%;"
|
||||
oninput=${ev => setUser(ev.target.value)} value=${user} />
|
||||
</div>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<input type="password" placeholder="Password" style="width: 100%;"
|
||||
oninput=${ev => setPass(ev.target.value)} value=${pass}
|
||||
onchange=${login} />
|
||||
</div>
|
||||
<div style="margin: 1em 0;">
|
||||
<button class="btn" style="width: 100%; background: #8aa;"
|
||||
disabled=${!user || !pass} onclick=${login}> Login </button>
|
||||
</div>
|
||||
<div style="color: #777; margin-top: 2em;">
|
||||
Valid logins: admin:pass0, user1:pass1, user2:pass2
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const Message = text => html`<div style="margin: 0.5em 0;">${text}</div>`;
|
||||
|
||||
const Chat = function(props) {
|
||||
const [message, setMessage] = useState('');
|
||||
const sendmessage = ev => fetch('/api/message/send', {
|
||||
method: 'post',
|
||||
body: `message=${encodeURIComponent(message)}`
|
||||
}).then(r => setMessage(''));
|
||||
|
||||
const messages = props.messages.map(
|
||||
text => html`<div style="margin: 0.5em 0;">${text}</div>`);
|
||||
return html`
|
||||
<div style="margin: 0 0.3em;">
|
||||
<h3 style="background: #30c040; color: #fff; padding: 0.4em;">User chat</h3>
|
||||
<div style="height: 10em; overflow: auto; padding: 0.5em; " class="border">
|
||||
${messages}
|
||||
</div>
|
||||
<div style="margin: 0.5em 0;">
|
||||
<input placeholder="type message..." style="width: 100%" value=${message}
|
||||
onchange=${sendmessage}
|
||||
oninput=${ev => setMessage(ev.target.value)} />
|
||||
</div>
|
||||
<div class="msg">
|
||||
Chat demonsrates
|
||||
real-time bidirectional data exchange between many clients and a server.
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const App = function(props) {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [user, setUser] = useState('');
|
||||
const [config, setConfig] = useState({});
|
||||
|
||||
const refresh = () =>
|
||||
fetch('/api/config/get').then(r => r.json()).then(r => setConfig(r));
|
||||
|
||||
const login = function(u) {
|
||||
document.cookie = `access_token=${u.token};path=/;max-age=3600`;
|
||||
setUser(u.user);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const logout = ev => {
|
||||
document.cookie = `access_token=;path=/;max-age=0`;
|
||||
setUser('');
|
||||
};
|
||||
|
||||
const watch = function() {
|
||||
var f = function(reader) {
|
||||
return reader.read().then(function(result) {
|
||||
var data = String.fromCharCode.apply(null, result.value);
|
||||
var notification = JSON.parse(data);
|
||||
if (notification.name == 'config') refresh();
|
||||
if (notification.name == 'message') {
|
||||
setMessages(m => m.concat([notification.data]))
|
||||
}
|
||||
// console.log(notification);
|
||||
if (!result.done) return f(reader);
|
||||
});
|
||||
};
|
||||
fetch('/api/watch')
|
||||
.then(r => r.body.getReader())
|
||||
.then(f)
|
||||
.catch(e => setTimeout(watch, 1000));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/login')
|
||||
.then(r => r.json())
|
||||
.then(r => login(r))
|
||||
.catch(err => setUser(''));
|
||||
refresh();
|
||||
watch();
|
||||
}, []);
|
||||
|
||||
if (!user) return html`<${Login} login=${login} />`;
|
||||
const admin = user == 'admin';
|
||||
const colsize = admin ? 'c4' : 'c6';
|
||||
const cs = admin ? html`<${ChangeSettings} config=${config} />` : '';
|
||||
return html`
|
||||
<${Nav} user=${user} logout=${logout} />
|
||||
<div class="container">
|
||||
<${Hero} />
|
||||
<div class="row">
|
||||
<div class="col ${colsize}"><${ShowSettings} config=${config} /></div>
|
||||
<div class="col ${colsize}">${cs}</div>
|
||||
<div class="col ${colsize}"><${Chat} messages=${messages} /></div>
|
||||
</div>
|
||||
<${Footer} />
|
||||
</div>`;
|
||||
};
|
||||
|
||||
window.onload = () => render(h(App), document.body);
|
@ -1 +0,0 @@
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("preact")):"function"==typeof define&&define.amd?define(["preact"],e):t.preactRouter=e(t.preact)}(this,function(p){function i(t,e){for(var n in e)t[n]=e[n];return t}function u(t,e,n){var r,o=/(?:\?([^#]*))?(#.*)?$/,i=t.match(o),u={};if(i&&i[1])for(var a=i[1].split("&"),p=0;p<a.length;p++){var c=a[p].split("=");u[decodeURIComponent(c[0])]=decodeURIComponent(c.slice(1).join("="))}t=y(t.replace(o,"")),e=y(e||"");for(var f=Math.max(t.length,e.length),l=0;l<f;l++)if(e[l]&&":"===e[l].charAt(0)){var s=e[l].replace(/(^:|[+*?]+$)/g,""),h=(e[l].match(/[+*?]+$/)||v)[0]||"",d=~h.indexOf("+"),g=~h.indexOf("*"),m=t[l]||"";if(!m&&!g&&(h.indexOf("?")<0||d)){r=!1;break}if(u[s]=decodeURIComponent(m),d||g){u[s]=t.slice(l).map(decodeURIComponent).join("/");break}}else if(e[l]!==t[l]){r=!1;break}return(!0===n.default||!1!==r)&&u}function e(t,e){return t.rank<e.rank?1:t.rank>e.rank?-1:t.index-e.index}function n(t,e){return t.index=e,t.rank=(n=t).props.default?0:function(t){return y(t).map(r).join("")}(n.props.path),t.props;var n}function y(t){return t.replace(/(^\/+|\/+$)/g,"").split("/")}function r(t){return":"==t.charAt(0)?1+"*+?".indexOf(t.charAt(t.length-1))||4:5}function o(){var t;return""+((t=g&&g.location?g.location:g&&g.getCurrentLocation?g.getCurrentLocation():"undefined"!=typeof location?location:b).pathname||"")+(t.search||"")}function a(t,e){return void 0===e&&(e=!1),"string"!=typeof t&&t.url&&(e=t.replace,t=t.url),function(t){for(var e=m.length;e--;)if(m[e].canRoute(t))return 1;return}(t)&&(n=t,void 0===(r=e?"replace":"push")&&(r="push"),g&&g[r]?g[r](n):"undefined"!=typeof history&&history[r+"State"]&&history[r+"State"](null,null,n)),c(t);var n,r}function c(t){for(var e=!1,n=0;n<m.length;n++)!0===m[n].routeTo(t)&&(e=!0);for(var r=C.length;r--;)C[r](t);return e}function f(t){if(t&&t.getAttribute){var e=t.getAttribute("href"),n=t.getAttribute("target");if(e&&e.match(/^\//g)&&(!n||n.match(/^_?self$/i)))return a(e)}}function l(t){if(!(t.ctrlKey||t.metaKey||t.altKey||t.shiftKey||0!==t.button))return f(t.currentTarget||t.target||this),s(t)}function s(t){return t&&(t.stopImmediatePropagation&&t.stopImmediatePropagation(),t.stopPropagation&&t.stopPropagation(),t.preventDefault()),!1}function h(t){if(!(t.ctrlKey||t.metaKey||t.altKey||t.shiftKey||0!==t.button)){var e=t.target;do{if("A"===(e.nodeName+"").toUpperCase()&&e.getAttribute("href")){if(e.hasAttribute("native"))return;if(f(e))return s(t)}}while(e=e.parentNode)}}var d,v={},g=null,m=[],C=[],b={},U=!1,t=((d=p.Component)&&(k.__proto__=d),((k.prototype=Object.create(d&&d.prototype)).constructor=k).prototype.shouldComponentUpdate=function(t){return!0!==t.static||t.url!==this.props.url||t.onChange!==this.props.onChange},k.prototype.canRoute=function(t){return 0<this.getMatchingChildren(p.toChildArray(this.props.children),t,!1).length},k.prototype.routeTo=function(t){this.setState({url:t});var e=this.canRoute(t);return this.updating||this.forceUpdate(),e},k.prototype.componentWillMount=function(){m.push(this),this.updating=!0},k.prototype.componentDidMount=function(){var e=this;g&&(this.unlisten=g.listen(function(t){e.routeTo(""+(t.pathname||"")+(t.search||""))})),this.updating=!1},k.prototype.componentWillUnmount=function(){"function"==typeof this.unlisten&&this.unlisten(),m.splice(m.indexOf(this),1)},k.prototype.componentWillUpdate=function(){this.updating=!0},k.prototype.componentDidUpdate=function(){this.updating=!1},k.prototype.getMatchingChildren=function(t,r,o){return t.filter(n).sort(e).map(function(t){var e=u(r,t.props.path,t.props);if(e){if(!1===o)return t;var n={url:r,matches:e};return i(n,e),delete n.ref,delete n.key,p.cloneElement(t,n)}}).filter(Boolean)},k.prototype.render=function(t,e){var n=t.children,r=t.onChange,o=e.url,i=this.getMatchingChildren(p.toChildArray(n),o,!0),u=i[0]||null,a=this.previousUrl;return o!==a&&(this.previousUrl=o,"function"==typeof r&&r({router:this,url:o,previous:a,active:i,current:u})),u},k);function k(t){d.call(this,t),t.history&&(g=t.history),this.state={url:t.url||o()},U||("function"==typeof addEventListener&&(g||addEventListener("popstate",function(){c(o())}),addEventListener("click",h)),U=!0)}return t.subscribers=C,t.getCurrentUrl=o,t.route=a,(t.Router=t).Route=function(t){return p.createElement(t.component,t)},t.Link=function(t){return p.createElement("a",i({onClick:l},t))},t.exec=u,t});
|
3
examples/complete/web_root/preact.min.js
vendored
@ -1,7 +1,36 @@
|
||||
html, body { margin: 0; padding: 0; height: 100%; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; font: 16px sans-serif; }
|
||||
select, input, label::before, textarea { outline: none; box-shadow:none !important; border: 1px solid #ccc !important; }
|
||||
.btn:focus,.btn:active:focus,.btn.active:focus,
|
||||
.btn.focus,.btn:active.focus,.btn.active.focus { outline: none; box-shadow:none !important; }
|
||||
.dropdown.autoexpand:hover>.dropdown-menu { display: block; }
|
||||
.pointer { cursor: pointer; }
|
||||
.log { max-height: 10em; overflow: auto; height: 100%; }
|
||||
code, pre { color: #373; font-family: monospace; font-weight: bolder; font-size: smaller; background: #ddd; padding: 0.1em 0.3em; border-radius: 0.2em; }
|
||||
textarea, input, .addon { font-size: 15px; border: 1px solid #ccc; padding: 0.5em; }
|
||||
a, a:visited, a:active { color: #55f; }
|
||||
.addon { background: #eee; }
|
||||
.btn {
|
||||
background: #ccc; border-radius: 0.3em; border: 0; color: #fff; cursor: pointer;
|
||||
display: inline-block; padding: 0.6em 2em; font-weight: bolder;
|
||||
}
|
||||
.btn[disabled] { opacity: 0.5; cursor: auto;}
|
||||
.smooth { transition: all .2s; }
|
||||
.container { margin: 0 20px; width: auto; }
|
||||
.d-flex { display: flex; }
|
||||
.d-none { display: none; }
|
||||
.border { border: 1px solid #ddd; }
|
||||
.rounded { border-radius: 0.5em; }
|
||||
.msg { background: #def; border-left: 5px solid #59d; padding: 1em; margin: 1em 0; }
|
||||
.row { margin: 1% 0; overflow: auto; }
|
||||
.col { float: left; }
|
||||
.table, .c12 { width: 100%; }
|
||||
.c11 { width: 91.66%; }
|
||||
.c10 { width: 83.33%; }
|
||||
.c9 { width: 75%; }
|
||||
.c8 { width: 66.66%; }
|
||||
.c7 { width: 58.33%; }
|
||||
.c6 { width: 50%; }
|
||||
.c5 { width: 41.66%; }
|
||||
.c4 { width: 33.33%; }
|
||||
.c3 { width: 25%; }
|
||||
.c2 { width: 16.66%; }
|
||||
.c1 { width: 8.33%; }
|
||||
|
||||
@media (min-width: 1310px) { .container { margin: auto; width: 1270px; } }
|
||||
@media (max-width: 870px) { .row .col { width: 100%; } }
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
4
src/fs.h
@ -40,3 +40,7 @@ void mg_fs_close(struct mg_fd *fd);
|
||||
char *mg_file_read(struct mg_fs *fs, const char *path, size_t *size);
|
||||
bool mg_file_write(struct mg_fs *fs, const char *path, const void *, size_t);
|
||||
bool mg_file_printf(struct mg_fs *fs, const char *path, const char *fmt, ...);
|
||||
|
||||
// API for the embedded (packed) filesystem
|
||||
const char *mg_unpack(const char *path, size_t *size, time_t *mtime);
|
||||
const char *mg_unlist(size_t no);
|
||||
|
@ -7,9 +7,6 @@ struct packed_file {
|
||||
size_t pos;
|
||||
};
|
||||
|
||||
const char *mg_unpack(const char *path, size_t *size, time_t *mtime);
|
||||
const char *mg_unlist(size_t no);
|
||||
|
||||
#if MG_ENABLE_PACKED_FS
|
||||
#else
|
||||
const char *mg_unpack(const char *path, size_t *size, time_t *mtime) {
|
||||
|
@ -92,7 +92,7 @@ int main(int argc, char *argv[]) {
|
||||
struct stat st;
|
||||
const char *name = argv[i];
|
||||
size_t n = strlen(strip_prefix);
|
||||
if (strcmp(argv[i], "-z") == 0 || strcmp(argv[i], "-s") == 0) {
|
||||
if (strcmp(argv[i], "-s") == 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|