From 98b5b6e7518d44b847790aeefdac173ad66b3744 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 13:39:57 +0200 Subject: [PATCH 01/11] dmvs_html: scripts compiled by dmvs_js - the host of its DOM The page's scripts and onclick code are compiled by dmvs_js into the view's code (init, click handlers, timers, variables); script.c is now its host. Its own evaluator goes; what it did stays: a class or style change is laid out (moves, fades by transitions; other looks as variants; classes changed together - up to the compiler's flush - one look; pressed looks). A change is a CALL of a handler made when every change is laid out; one of the element a variable holds an IF for each element dmvs_js_object_domain() gives. innerText / textContent are text variables, with a number shadowing them (parseFloat(innerText)), painted on the element's first line, placed by text-align. Click handlers copy their listeners' actions (dmview's calls are 8 deep). Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 3 +- README.md | 98 +- src/layout.c | 6 + src/paint.c | 72 ++ src/private.h | 4 + src/script.c | 2645 ++++++++++++++-------------------------- tests/dmvs_html_test.c | 128 +- 7 files changed, 1161 insertions(+), 1795 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c47ad7..de55f2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,7 +74,8 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} ) dmod_link_modules(${DMOD_MODULE_NAME} - dmvsi@>=0.2 + dmvsi@>=0.4 + dmvs_js@>=0.1 ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE diff --git a/README.md b/README.md index 3e9ac91..404a68f 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ makes a dmview view of it. It has its own HTML parser, CSS cascade and layout - no browser, no dependencies - so it runs on a PC at build time and on a device. It is made for **UI pages**: the screens a designer (or an AI) draws in HTML with -Tailwind CSS - and the little JavaScript that switches between them, which -it works out at conversion and makes the view's variables and handlers. +Tailwind CSS - and their JavaScript, which +[dmvs_js](https://github.com/choco-technologies/dmvs_js) compiles into the +view's variables, handlers and timers. ## What it does @@ -70,52 +71,67 @@ CSS does; text is measured with them as the view will draw it. Icon fonts ## Scripts -A script is not run on the device: what its click handlers do is worked -out at conversion. Understood is what switches screens and toggles things: +A script isn't run on the device. The page's `")); + dmvsi_doc_t doc = convert(TEST_FILE("texts.html"), "screen", 0, 0, &status); + DMOD_TEST_EXPECT_EQ(status, 0); + if (doc == NULL) + return; + const dmvsi_node_t* root = dmvsi_root(doc); + + /* #speed: empty at first - a text of its variable, centred in its box (the scripts' characters: in its font) */ + dmvsi_var_t speed = var_named(doc, "speed_text"); + const dmvsi_node_t* st = text_of_var(root, speed); + DMOD_TEST_EXPECT_TRUE(speed != 0 && st != NULL); + if (st != NULL) + { + DMOD_TEST_EXPECT_TRUE(st->u.text.align == DMVSI_TEXT_CENTER && st->u.text.width == DMVSI_PX(100)); + } + /* #temp: its text and its number (21.5) - the click sets both */ + dmvsi_var_t temp = var_named(doc, "temp_text"); + dmvsi_var_t number = var_named(doc, "temp_number"); + DMOD_TEST_EXPECT_TRUE(text_of_var(root, temp) != NULL && var_initial(doc, number) == 21500); + dmvsi_var_info_t info; + DMOD_TEST_EXPECT_TRUE(dmvsi_var_info(doc, temp, &info) == 0 && strcmp(info.text, "21.5\xC2\xB0" "C") == 0); + const dmvsi_node_t* up = group_named(root->first, "up"); + DMOD_TEST_EXPECT_TRUE(up != NULL && up->click != 0); + if (up != NULL) + { + const dmvsi_action_t* a = NULL; + uint32_t n = flat_actions(doc, up->click, &a); + bool sets_text = false, sets_number = false; + for (uint32_t i = 0; i < n; i++) + { + sets_text = sets_text || (a[i].kind == DMVSI_ACT_SET && a[i].var == temp); + sets_number = sets_number || (a[i].kind == DMVSI_ACT_SET && a[i].var == number); + } + DMOD_TEST_EXPECT_TRUE(sets_text && sets_number); + } + /* The interval: a timer of the view */ + uint16_t ms = 0; + dmvsi_handler_t h = 0; + DMOD_TEST_EXPECT_TRUE(dmvsi_timer_at(doc, 0, &ms, &h) && ms >= 10 && ms <= 50 && h != 0); + dmvsi_free(doc); +} From 2d361c26f2e257b38a14f0bd4c8a447fc081cf6f Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 14:11:41 +0200 Subject: [PATCH 02/11] dmvs_html: a script's text - room in the first wider ancestor; undefined not set A text a script sets in a box as wide as its text (a flex item, its column as narrow) gets the room of the first wider ancestor, placed as the box is in it - centred, at its right, or from where it is: the car HMI's speed was "6" of 68. A text that is undefined is not set. Co-Authored-By: Claude Opus 5.5 --- src/paint.c | 33 +++++++++++++++++++++++++++++++++ src/script.c | 2 ++ 2 files changed, 35 insertions(+) diff --git a/src/paint.c b/src/paint.c index 72facc4..ddd9845 100644 --- a/src/paint.c +++ b/src/paint.c @@ -535,6 +535,39 @@ static void paint_text_var(painter_t* p, const node_t* block, const node_t* e, c t.x = e->box.ax - p->ox + p->dx + e->box.b[3] + e->box.p[3]; t.width = e->box.w - e->box.b[1] - e->box.b[3] - e->box.p[1] - e->box.p[3]; t.align = (st->text_align == TEXT_CENTER) ? DMVSI_TEXT_CENTER : (st->text_align == TEXT_RIGHT) ? DMVSI_TEXT_RIGHT : DMVSI_TEXT_LEFT; + /* + * A box as wide as its text (a flex item, a float): the text it gets may be + * longer - room in its parent's content box, placed as the box is in it + * (in its middle: centred; at its right: right-aligned; else from where it is) + */ + /* (its parent as narrow too - a column of such items: the first that is wider) */ + const node_t* q = e->parent; + int32_t qx = 0, qw = 0; + for (uint32_t up = 0; q != NULL && q->kind == NODE_ELEMENT && q->box.laid_out && up < 4u; q = q->parent, up++) + { + qx = q->box.ax - p->ox + p->dx + q->box.b[3] + q->box.p[3]; + qw = q->box.w - q->box.b[1] - q->box.b[3] - q->box.p[1] - q->box.p[3]; + if (qw > t.width + 2 * DMVSI_UNIT) + break; + } + if (q != NULL && q->kind == NODE_ELEMENT && q->box.laid_out && t.align == DMVSI_TEXT_LEFT) + { + int32_t mid = t.x + t.width / 2, qmid = qx + qw / 2; + if (qw > t.width) + { + if (mid - qmid <= DMVSI_UNIT && qmid - mid <= DMVSI_UNIT) + { + t.x = qx; + t.align = DMVSI_TEXT_CENTER; + } + else if ((qx + qw) - (t.x + t.width) <= DMVSI_UNIT) + { + t.x = qx; + t.align = DMVSI_TEXT_RIGHT; + } + t.width = (t.align == DMVSI_TEXT_LEFT) ? qx + qw - t.x : qw; + } + } } else t.x = block->box.ax - p->ox + p->dx + ((f != NULL) ? f->x : 0); diff --git a/src/script.c b/src/script.c index eae255d..ba5b616 100644 --- a/src/script.c +++ b/src/script.c @@ -447,6 +447,8 @@ static dynamic_t* text_of(script_t* sc, node_t* e) static void set_text(script_t* sc, node_t* e, const dmvs_js_value_t* v) { + if (v->kind == DMVS_JS_V_UNDEFINED) + return; /* What was not converted (reported): the text stays */ dynamic_t* d = text_of(sc, e); if (d == NULL || d->text == 0) return; From 58802bcebbe343293ad452cf486d689c27bc1681 Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 14:38:19 +0200 Subject: [PATCH 03/11] dmvs_html: innerHTML of text in what holds no elements - its text Co-Authored-By: Claude Opus 5.5 --- src/script.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/script.c b/src/script.c index ba5b616..be4deb8 100644 --- a/src/script.c +++ b/src/script.c @@ -661,6 +661,18 @@ static int host_set(void* ctx, dmvs_js_compiler_t js, const dmvs_js_value_t* obj set_text(sc, e, value); return 0; } + if (strcmp(name, "innerHTML") == 0) + { + /* Text (its character references decoded by the compiler) in what holds no elements: its text */ + const char* text = static_text(sc, value); + bool elements = false; + for (const node_t* k = e->first; k != NULL && !elements; k = k->next) + elements = k->kind == NODE_ELEMENT; + if (elements || (text != NULL && strchr(text, '<') != NULL)) + return -ENOTSUP; /* Elements made or replaced */ + set_text(sc, e, value); + return 0; + } if (strcmp(name, "onclick") == 0) return add_listener(sc, e, value); return -ENOTSUP; From 7391a70b427023025040c8f629a9f8a74fcf1b7f Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 14:48:59 +0200 Subject: [PATCH 04/11] dmvs_html: what scripts build as the page loads is part of the page The scripts' loading evaluated by dmvs_js (every variable known, functions run at each call): the elements they make - createElement, className, innerHTML of markup, appendChild - recorded as operations on the page's elements, applied each time it is laid out (its states too). A made element is marked data-dmvs-built="k": the compilation's k-th createElement() is it, so its listeners are its clicks; what it sets of it (className, innerHTML, appendChild, emptying its list) is as it was. The car HMI's playlist: its five songs, the current one highlighted. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 1 + Makefile | 2 +- README.md | 10 +- src/build.c | 567 +++++++++++++++++++++++++++++++++++++++++ src/dmvs_html.c | 31 +++ src/private.h | 15 ++ src/script.c | 47 ++++ tests/dmvs_html_test.c | 66 +++++ 8 files changed, 737 insertions(+), 2 deletions(-) create mode 100644 src/build.c diff --git a/CMakeLists.txt b/CMakeLists.txt index de55f2f..558f2fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} src/paint.c src/svg.c src/script.c + src/build.c ) dmod_link_modules(${DMOD_MODULE_NAME} diff --git a/Makefile b/Makefile index 519e4ca..94dfe80 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ DMOD_MODULE_VERSION=0.1 DMOD_AUTHOR_NAME=Patryk Kubiak # The list of C sources -DMOD_CSOURCES=src/dmvs_html.c src/html.c src/css.c src/tailwind.c src/style.c src/layout.c src/paint.c src/svg.c src/script.c +DMOD_CSOURCES=src/dmvs_html.c src/html.c src/css.c src/tailwind.c src/style.c src/layout.c src/paint.c src/svg.c src/script.c src/build.c # The list of C++ sources DMOD_CXXSOURCES= diff --git a/README.md b/README.md index 404a68f..ab3e939 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,16 @@ up.addEventListener('click', () => // parseFloat(innerText): the n `onclick="..."` are a click handler. `this` is the element, and `el.click()` runs its handlers. +- **What scripts build as the page loads** (`createElement`, `className`, + `innerHTML` with markup, `appendChild`; a list rendered from an array) + is part of the page. dmvs_js evaluates the loading as a browser runs + it, the elements are laid out with the page, and their listeners are + clicks (`div.onclick = () => play(i)`). A list rebuilt later stays as + it was when the page loaded. +- `innerHTML` set to text (`'22.0°'`) is the element's text. + What's reported and left out: -- elements made or replaced (`createElement`, `innerHTML`); +- elements made or replaced later, while the view runs; - `src`, `setAttribute`; - a style known only at runtime; - a style that changes how an element looks (other than its opacity); diff --git a/src/build.c b/src/build.c new file mode 100644 index 0000000..19908d2 --- /dev/null +++ b/src/build.c @@ -0,0 +1,567 @@ +#include "private.h" +#include "dmvs_js.h" +#include +#include + +/* + * What the scripts build: the elements they make as the page loads + * (createElement, className, innerHTML, appendChild - a list of a + * playlist's songs) are what the page is when it is shown. dmvs_js + * evaluates the loading as a browser runs it (every variable known, + * functions run at each call, listeners and timers not); what it does to + * the DOM is recorded here - operations on the elements of the page as it + * is parsed (dom_t), applied each time it is laid out (a state of it too). + * + * An element made is marked data-dmvs-built="k" (the k-th + * document.createElement()): the scripts' compilation finds it there. + */ + +#define MAX_MADE 64u +#define MAX_CHILDREN 32u +#define MAX_PARENTS 16u +#define MARKUP_SIZE 16384u + +#define B_DOCUMENT 1u +#define B_IGNORED 2u /* What building does not care about (style, tailwind, ...) */ +#define B_EXISTING 0x100000u /* + an element's index */ +#define B_MADE 0x200000u /* + k (1 ...) */ +#define B_MADE_CLASSES 0x300000u /* + k: its classList */ + +typedef struct +{ + char* tag; + char* classes; /* Space-separated */ + char* id; + char* text; /* textContent (escaped when written) */ + char* html; /* innerHTML (as it is) */ + char* attrs; /* ' name="value"' ... */ + uint32_t children[MAX_CHILDREN]; /* Made elements appended to it (k) */ + uint32_t child_count; + bool appended; +} made_t; + +/* An element of the page the scripts change: emptied, and what is appended to it */ +typedef struct +{ + uint32_t index; + bool clear; + char* html; /* innerHTML set to markup */ + uint32_t children[MAX_CHILDREN]; + uint32_t child_count; +} parent_t; + +typedef struct +{ + conv_t* c; + made_t made[MAX_MADE]; + uint32_t made_count; + parent_t parents[MAX_PARENTS]; + uint32_t parent_count; +} builder_t; + +static dmvs_js_value_t object(uint32_t h) +{ + dmvs_js_value_t v; + memset(&v, 0, sizeof(v)); + v.kind = (h != 0) ? DMVS_JS_V_OBJECT : DMVS_JS_V_NULL; + v.object = h; + return v; +} + +static dmvs_js_value_t string(const char* s) +{ + dmvs_js_value_t v; + memset(&v, 0, sizeof(v)); + v.kind = DMVS_JS_V_STRING; + v.text = s; + v.length = strlen(s); + return v; +} + +static node_t* by_index(node_t* n, uint32_t index, uint32_t depth) +{ + for (node_t* k = n->first; k != NULL && depth < 200U; k = k->next) + { + if (k->kind != NODE_ELEMENT) + continue; + if (k->index == index) + return k; + node_t* found = by_index(k, index, depth + 1U); + if (found != NULL) + return found; + } + return NULL; +} + +static node_t* existing(builder_t* b, uint32_t h) +{ + return (h > B_EXISTING && h < B_MADE) ? by_index(b->c->document, h - B_EXISTING, 0) : NULL; +} + +static made_t* made_of(builder_t* b, uint32_t h) +{ + return (h > B_MADE && h <= B_MADE + b->made_count) ? &b->made[h - B_MADE - 1U] : NULL; +} + +static dmvs_js_value_t element(node_t* e) +{ + return object((e != NULL) ? B_EXISTING + e->index : 0); +} + +static char* copy(builder_t* b, const char* s) +{ + return arena_strndup(&b->c->arena, s, strlen(s)); +} + +static const char* text_of(const dmvs_js_value_t* v, char* buffer, size_t size) +{ + if (v->kind == DMVS_JS_V_STRING) + return v->text; + if (v->kind == DMVS_JS_V_NUMBER) + { + Dmod_SnPrintf(buffer, size, "%d", (int)v->number); + return buffer; + } + return NULL; +} + +static parent_t* parent_of(builder_t* b, node_t* e) +{ + for (uint32_t i = 0; i < b->parent_count; i++) + { + if (b->parents[i].index == e->index) + return &b->parents[i]; + } + if (b->parent_count >= MAX_PARENTS) + return NULL; + parent_t* p = &b->parents[b->parent_count++]; + memset(p, 0, sizeof(*p)); + p->index = e->index; + return p; +} + +/* The text an element shows: its text nodes' */ +static size_t content(const node_t* n, char* out, size_t size, size_t at, uint32_t depth) +{ + for (const node_t* k = n->first; k != NULL && depth < 64U; k = k->next) + { + if (k->kind == NODE_TEXT) + for (size_t i = 0; i < k->length && at + 1U < size; i++) + out[at++] = k->text[i]; + else if (k->kind == NODE_ELEMENT) + at = content(k, out, size, at, depth + 1U); + } + out[(at < size) ? at : size - 1U] = '\0'; + return at; +} + +/* ---- The host: what building cares about ---- */ + +static bool b_global(void* ctx, dmvs_js_compiler_t c, const char* name, dmvs_js_value_t* value) +{ + (void)ctx; + (void)c; + if (strcmp(name, "document") == 0) + *value = object(B_DOCUMENT); + else if (strcmp(name, "tailwind") == 0 || strcmp(name, "window") == 0) + *value = object(B_IGNORED); + else + return false; + return true; +} + +static int b_get(void* ctx, dmvs_js_compiler_t c, const dmvs_js_value_t* obj, const char* name, dmvs_js_value_t* value) +{ + builder_t* b = ctx; + (void)c; + uint32_t h = (obj->kind == DMVS_JS_V_OBJECT) ? obj->object : 0; + node_t* e = existing(b, h); + made_t* m = made_of(b, h); + if (h == B_DOCUMENT && strcmp(name, "body") == 0) + { + node_t* body = NULL; + node_t* found[1]; + if (css_select(b->c, "body", b->c->document, found, 1) == 1) + body = found[0]; + *value = element(body); + return 0; + } + if (m != NULL && strcmp(name, "classList") == 0) + { + *value = object(B_MADE_CLASSES + (h - B_MADE)); + return 0; + } + if (e != NULL && (strcmp(name, "innerText") == 0 || strcmp(name, "textContent") == 0)) + { + char text[256]; + content(e, text, sizeof(text), 0, 0); + *value = string(copy(b, text)); + return 0; + } + if (e != NULL && strcmp(name, "id") == 0) + { + *value = string((e->id != NULL) ? e->id : ""); + return 0; + } + *value = object(B_IGNORED); /* style, classList of the page's, ...: not what is built */ + return 0; +} + +static int b_set(void* ctx, dmvs_js_compiler_t c, const dmvs_js_value_t* obj, const char* name, const dmvs_js_value_t* value) +{ + builder_t* b = ctx; + (void)c; + char number[24]; + uint32_t h = (obj->kind == DMVS_JS_V_OBJECT) ? obj->object : 0; + const char* text = text_of(value, number, sizeof(number)); + made_t* m = made_of(b, h); + node_t* e = existing(b, h); + if (text == NULL) + return 0; + if (m != NULL) + { + if (strcmp(name, "className") == 0) + m->classes = copy(b, text); + else if (strcmp(name, "id") == 0) + m->id = copy(b, text); + else if (strcmp(name, "innerHTML") == 0) + m->html = copy(b, text); + else if (strcmp(name, "textContent") == 0 || strcmp(name, "innerText") == 0) + m->text = copy(b, text); + return 0; + } + if (e != NULL && strcmp(name, "innerHTML") == 0) + { + /* Emptied (a list rendered anew), or its markup */ + parent_t* p = parent_of(b, e); + if (p != NULL) + { + p->clear = true; + p->child_count = 0; + p->html = (text[0] != '\0') ? copy(b, text) : NULL; + } + } + return 0; +} + +static int b_call(void* ctx, dmvs_js_compiler_t c, const dmvs_js_value_t* obj, const char* method, + const dmvs_js_value_t* args, uint32_t count, dmvs_js_value_t* result) +{ + builder_t* b = ctx; + char number[24]; + uint32_t h = (obj->kind == DMVS_JS_V_OBJECT) ? obj->object : 0; + const char* a0 = (count > 0) ? text_of(&args[0], number, sizeof(number)) : NULL; + memset(result, 0, sizeof(*result)); + if (h == B_DOCUMENT && strcmp(method, "getElementById") == 0 && a0 != NULL) + { + *result = element(find_id(b->c->document, a0, 0)); + return 0; + } + if ((h == B_DOCUMENT || existing(b, h) != NULL) && a0 != NULL && + (strcmp(method, "querySelector") == 0 || strcmp(method, "querySelectorAll") == 0)) + { + node_t* found[32]; + node_t* under = (h == B_DOCUMENT) ? b->c->document : existing(b, h); + uint32_t n = css_select(b->c, a0, under, found, 32); + if (method[13] != 'A') + { + *result = element((n > 0) ? found[0] : NULL); + return 0; + } + dmvs_js_value_t list[32]; + for (uint32_t i = 0; i < n; i++) + list[i] = element(found[i]); + return dmvs_js_array(c, list, n, result); + } + if (h == B_DOCUMENT && strcmp(method, "createElement") == 0 && a0 != NULL) + { + if (b->made_count >= MAX_MADE) + return 0; + made_t* m = &b->made[b->made_count++]; + memset(m, 0, sizeof(*m)); + m->tag = copy(b, a0); + *result = object(B_MADE + b->made_count); + return 0; + } + if (strcmp(method, "appendChild") == 0 && count == 1 && args[0].kind == DMVS_JS_V_OBJECT) + { + made_t* child = made_of(b, args[0].object); + made_t* m = made_of(b, h); + node_t* e = existing(b, h); + uint32_t k = args[0].object - B_MADE; + if (child == NULL || child->appended) + return 0; + if (m != NULL && m->child_count < MAX_CHILDREN) + m->children[m->child_count++] = k; + else if (e != NULL) + { + parent_t* p = parent_of(b, e); + if (p == NULL || p->child_count >= MAX_CHILDREN) + return 0; + p->children[p->child_count++] = k; + } + else + return 0; + child->appended = true; + *result = args[0]; + return 0; + } + if (h > B_MADE_CLASSES && h <= B_MADE_CLASSES + b->made_count && a0 != NULL && strcmp(method, "add") == 0) + { + made_t* m = &b->made[h - B_MADE_CLASSES - 1U]; + size_t n = (m->classes != NULL) ? strlen(m->classes) : 0; + char* joined = arena_alloc(&b->c->arena, n + strlen(a0) + 2U); + if (joined != NULL) + { + Dmod_SnPrintf(joined, n + strlen(a0) + 2U, "%s%s%s", (n > 0) ? m->classes : "", (n > 0) ? " " : "", a0); + m->classes = joined; + } + return 0; + } + if (strcmp(method, "setAttribute") == 0 && count == 2 && made_of(b, h) != NULL && a0 != NULL) + { + made_t* m = made_of(b, h); + char v[24]; + const char* value = text_of(&args[1], v, sizeof(v)); + if (value != NULL && strchr(value, '"') == NULL) + { + size_t n = (m->attrs != NULL) ? strlen(m->attrs) : 0; + size_t size = n + strlen(a0) + strlen(value) + 5U; + char* attrs = arena_alloc(&b->c->arena, size); + if (attrs != NULL) + { + Dmod_SnPrintf(attrs, size, "%s %s=\"%s\"", (n > 0) ? m->attrs : "", a0, value); + m->attrs = attrs; + } + } + return 0; + } + return 0; /* Not what is built */ +} + +static void b_report(void* ctx, uint32_t line, uint32_t column, const char* message) +{ + (void)ctx; + (void)line; + (void)column; + (void)message; /* The compilation reports it */ +} + +/* ---- The markup of what was made ---- */ + +typedef struct +{ + char* out; + size_t n; + bool full; +} markup_t; + +static void put(markup_t* m, const char* s, bool escape) +{ + for (; *s != '\0'; s++) + { + const char* e = NULL; + if (escape) + e = (*s == '<') ? "<" : (*s == '>') ? ">" : (*s == '&') ? "&" : (*s == '"') ? """ : NULL; + size_t len = (e != NULL) ? strlen(e) : 1U; + if (m->n + len + 1U >= MARKUP_SIZE) + { + m->full = true; + return; + } + memcpy(m->out + m->n, (e != NULL) ? e : s, len); + m->n += len; + } + m->out[m->n] = '\0'; +} + +static void write_made(builder_t* b, markup_t* m, uint32_t k, uint32_t depth) +{ + const made_t* e = &b->made[k - 1U]; + char mark[32]; + if (depth > 16U) + return; + put(m, "<", false); + put(m, e->tag, true); + Dmod_SnPrintf(mark, sizeof(mark), " data-dmvs-built=\"%u\"", (unsigned)k); + put(m, mark, false); + if (e->classes != NULL) + { + put(m, " class=\"", false); + put(m, e->classes, true); + put(m, "\"", false); + } + if (e->id != NULL) + { + put(m, " id=\"", false); + put(m, e->id, true); + put(m, "\"", false); + } + if (e->attrs != NULL) + put(m, e->attrs, false); + put(m, ">", false); + if (e->html != NULL) + put(m, e->html, false); + else if (e->text != NULL) + put(m, e->text, true); + for (uint32_t i = 0; i < e->child_count; i++) + write_made(b, m, e->children[i], depth + 1U); + put(m, "tag, true); + put(m, ">", false); +} + +/* ---- Building ---- */ + +static void find_scripts(conv_t* c, node_t* n, dmvs_js_compiler_t js, uint32_t depth) +{ + for (node_t* k = n->first; k != NULL && depth < 200U; k = k->next) + { + if (k->kind != NODE_ELEMENT) + continue; + if (node_is(k, "script")) + { + const char* type = node_attr(k, "type"); + bool is_js = type == NULL || type[0] == '\0' || strcmp(type, "module") == 0 || strcmp(type, "text/javascript") == 0; + if (is_js && node_attr(k, "src") == NULL && k->first != NULL && k->first->kind == NODE_TEXT) + { + dmvs_js_error_t error; + dmvs_js_ast_t ast = dmvs_js_parse(k->first->text, k->first->length, &error); + if (ast != NULL) + (void)dmvs_js_compile(js, ast); + } + continue; + } + find_scripts(c, k, js, depth + 1U); + } +} + +int script_build(conv_t* c, conv_t* into) +{ + builder_t* b = Dmod_Malloc(sizeof(*b)); + if (b == NULL) + return -ENOMEM; + memset(b, 0, sizeof(*b)); + b->c = c; + dmvs_js_host_t host; + memset(&host, 0, sizeof(host)); + host.ctx = b; + host.global = b_global; + host.get = b_get; + host.set = b_set; + host.call = b_call; + host.report = b_report; + dmvs_js_compiler_t js = dmvs_js_evaluator_new(&host); + if (js == NULL) + { + Dmod_Free(b); + return -ENOMEM; + } + find_scripts(c, c->document, js, 0); + + /* What the page's elements are given: operations of `into` */ + int status = 0; + dom_t* ops = (b->parent_count > 0) ? arena_alloc(&into->arena, b->parent_count * sizeof(dom_t)) : NULL; + char* buffer = (b->parent_count > 0) ? Dmod_Malloc(MARKUP_SIZE) : NULL; + uint32_t count = 0; + for (uint32_t i = 0; i < b->parent_count && ops != NULL && buffer != NULL; i++) + { + const parent_t* p = &b->parents[i]; + markup_t m = { buffer, 0, false }; + buffer[0] = '\0'; + if (p->html != NULL) + put(&m, p->html, false); + for (uint32_t k = 0; k < p->child_count; k++) + write_made(b, &m, p->children[k], 0); + if (m.full) + WARN(c, "script: what a script builds in #%u is too large - not converted\n", (unsigned)p->index); + if ((!p->clear && m.n == 0) || m.full) + continue; + ops[count].parent = p->index; + ops[count].clear = p->clear; + ops[count].markup = arena_strndup(&into->arena, buffer, m.n); + if (ops[count].markup == NULL) + status = -ENOMEM; + count++; + } + if (b->parent_count > 0 && (ops == NULL || buffer == NULL)) + status = -ENOMEM; + into->dom = ops; + into->dom_count = count; + Dmod_Free(buffer); + dmvs_js_compiler_free(js); + Dmod_Free(b); + return status; +} + +/* The highest index of an element under n */ +static uint32_t highest(const node_t* n, uint32_t max, uint32_t depth) +{ + for (const node_t* k = n->first; k != NULL && depth < 200U; k = k->next) + { + if (k->kind != NODE_ELEMENT) + continue; + if (k->index > max) + max = k->index; + max = highest(k, max, depth + 1U); + } + return max; +} + +static void renumber(node_t* n, node_t* parent, uint32_t* next, uint32_t depth) +{ + for (node_t* k = n->first; k != NULL && depth < 200U; k = k->next) + { + k->parent = parent; + if (k->kind == NODE_ELEMENT) + { + k->index = ++*next; + renumber(k, k, next, depth + 1U); + } + } +} + +void apply_dom(conv_t* c) +{ + uint32_t next = highest(c->document, 0, 0); + for (uint32_t i = 0; i < c->dom_count; i++) + { + const dom_t* op = &c->dom[i]; + node_t* parent = by_index(c->document, op->parent, 0); + if (parent == NULL) + continue; + if (op->clear) + { + parent->first = NULL; + parent->last = NULL; + } + node_t* frag = html_parse(c, op->markup, strlen(op->markup)); + if (frag == NULL) + continue; + renumber(frag, parent, &next, 0); /* The page's numbers, then these: the same in every state */ + for (node_t* k = frag->first; k != NULL; ) + { + node_t* following = k->next; + k->next = NULL; + k->prev = parent->last; + if (parent->last != NULL) + parent->last->next = k; + else + parent->first = k; + parent->last = k; + k->parent = parent; + k = following; + } + } +} + +node_t* built_element(conv_t* c, uint32_t k) +{ + char mark[16]; + Dmod_SnPrintf(mark, sizeof(mark), "%u", (unsigned)k); + /* By its mark: data-dmvs-built="k" */ + node_t* found[1]; + char selector[48]; + Dmod_SnPrintf(selector, sizeof(selector), "[data-dmvs-built=\"%s\"]", mark); + return (css_select(c, selector, c->document, found, 1) == 1) ? found[0] : NULL; +} diff --git a/src/dmvs_html.c b/src/dmvs_html.c index af176a2..af3b65e 100644 --- a/src/dmvs_html.c +++ b/src/dmvs_html.c @@ -608,6 +608,7 @@ int run_layout(conv_t* c) return c->arena.failed ? -ENOMEM : -ENOENT; if ((c->document = html_parse(c, text, size)) == NULL) return c->arena.failed ? -ENOMEM : -EBADMSG; + apply_dom(c); for (uint32_t i = 0; i < c->mod_count; i++) apply_mod(c, &c->mods[i]); load_sheets(c, c->document, 0); @@ -632,6 +633,36 @@ dmod_dmvsi_dif_api_declaration(1.0, dmvs_html, int, _convert, ( const char* path c->path = arena_strndup(&c->arena, path, strlen(path)); int status = (c->path != NULL) ? run_layout(c) : -ENOMEM; + if (status == 0) + { + /* What its scripts build as it loads: the page laid out with it (a conversion of its own) */ + conv_t* built = Dmod_Malloc(sizeof(*built)); + if (built == NULL) + status = -ENOMEM; + else + { + memset(built, 0, sizeof(*built)); + built->options = c->options; + built->doc = c->doc; + built->vw = c->vw; + built->vh = c->vh; + built->path = arena_strndup(&built->arena, path, strlen(path)); + status = script_build(c, built); + if (status == 0 && built->dom_count > 0) + { + built->warnings = c->warnings; + arena_free(&c->arena); + Dmod_Free(c); + c = built; + status = run_layout(c); + } + else + { + arena_free(&built->arena); + Dmod_Free(built); + } + } + } if (status == 0) status = script_compile(c); /* What its scripts do: variables, handlers */ if (status == 0) diff --git a/src/private.h b/src/private.h index a50275e..d2b6966 100644 --- a/src/private.h +++ b/src/private.h @@ -476,10 +476,20 @@ typedef struct const char* value; } mod_t; +/* What a script builds in an element of the page as it loads (build.c): emptied, then markup appended */ +typedef struct +{ + uint32_t parent; /* Its index */ + bool clear; + const char* markup; +} dom_t; + typedef struct { arena_t arena; const char* path; /* The page */ + const dom_t* dom; /* What the scripts build, applied after parsing (before mods) */ + uint32_t dom_count; const mod_t* mods; /* Applied after parsing (run_layout()) */ uint32_t mod_count; const dmvsi_options_t* options; @@ -553,6 +563,11 @@ node_t* find_id(node_t* n, const char* id, uint32_t depth); dmvsi_rect_t group_rect(const conv_t* c, const node_t* n, int32_t ox, int32_t oy); void view_origin(const conv_t* c, int32_t* ox, int32_t* oy); +/* build.c: what the scripts build as the page loads - into `into`'s dom (into->dom_count 0: nothing) */ +int script_build(conv_t* c, conv_t* into); +void apply_dom(conv_t* c); +node_t* built_element(conv_t* c, uint32_t k); /* The k-th document.createElement()'s, NULL: none */ + /* script.c */ int script_compile(conv_t* c); void script_free(conv_t* c); /* The states it laid out (after painting) */ diff --git a/src/script.c b/src/script.c index be4deb8..b324541 100644 --- a/src/script.c +++ b/src/script.c @@ -161,6 +161,7 @@ typedef struct uint32_t wide[MAX_WIDE_CHARS]; uint32_t wide_count; const char* chars; + uint32_t made; /* document.createElement() so far: the k-th is the build's */ } script_t; static void report(script_t* sc, const char* what) @@ -168,6 +169,23 @@ static void report(script_t* sc, const char* what) dmvs_js_report(sc->js, what); } +/* An element the scripts built as the page loaded (build.c): what they set of it is as it was then */ +static bool is_built(const node_t* e) +{ + return e != NULL && e->kind == NODE_ELEMENT && node_attr(e, "data-dmvs-built") != NULL; +} + +/* Holding what the scripts built: emptying it, appending to it, is what the build did */ +static bool holds_built(const node_t* e) +{ + for (const node_t* k = (e != NULL) ? e->first : NULL; k != NULL; k = k->next) + { + if (is_built(k)) + return true; + } + return false; +} + static const char* label_of(const node_t* e) { return (e->id != NULL) ? e->id : e->tag; @@ -656,6 +674,10 @@ static int host_set(void* ctx, dmvs_js_compiler_t js, const dmvs_js_value_t* obj } if (kind != H_ELEMENT || e == NULL) return -ENOTSUP; + if (is_built(e) && (strcmp(name, "className") == 0 || strcmp(name, "innerHTML") == 0 || strcmp(name, "id") == 0)) + return 0; /* As the build made it */ + if (holds_built(e) && strcmp(name, "innerHTML") == 0) + return 0; /* Emptied to be built anew: as the build left it */ if (strcmp(name, "innerText") == 0 || strcmp(name, "textContent") == 0) { set_text(sc, e, value); @@ -797,6 +819,29 @@ static int host_call(void* ctx, dmvs_js_compiler_t js, const dmvs_js_value_t* ob } if ((strcmp(method, "querySelector") == 0 || strcmp(method, "querySelectorAll") == 0) && count == 1) return select_elements(sc, under, &args[0], method[13] == 'A', result); + if (kind == H_DOCUMENT && strcmp(method, "createElement") == 0) + { + /* The k-th: the element the build made (when it was put in the page) */ + node_t* made = built_element(sc->c, ++sc->made); + if (made == NULL) + { + report(sc, "an element made that is not in the page as it loads - not converted"); + return 0; + } + *result = element_value(sc, made); + return 0; + } + if (strcmp(method, "appendChild") == 0 && count == 1) + { + node_t* child; + dmvsi_var_t held; + if (object_of(sc, &args[0], &child, &held) == H_ELEMENT && is_built(child)) + { + *result = args[0]; + return 0; /* Where the build put it */ + } + return -ENOTSUP; + } if (kind != H_ELEMENT) return -ENOTSUP; if (strcmp(method, "getAttribute") == 0 && count == 1) @@ -938,6 +983,8 @@ static conv_t* run_state(script_t* sc, const mod_t* a, const mod_t* b, int* stat s->vw = c->vw; s->vh = c->vh; s->path = c->path; + s->dom = c->dom; /* What the scripts built */ + s->dom_count = c->dom_count; st->mods[st->count][0] = *a; if (b != NULL) st->mods[st->count][1] = *b; diff --git a/tests/dmvs_html_test.c b/tests/dmvs_html_test.c index 0a0d239..10c1ada 100644 --- a/tests/dmvs_html_test.c +++ b/tests/dmvs_html_test.c @@ -597,3 +597,69 @@ DMOD_TEST_STEP(dmvs_html_converts_texts_and_timers) DMOD_TEST_EXPECT_TRUE(dmvsi_timer_at(doc, 0, &ms, &h) && ms >= 10 && ms <= 50 && h != 0); dmvsi_free(doc); } + +/* The groups clicked whose handler sets a variable */ +static uint32_t clicks_setting(dmvsi_doc_t doc, const dmvsi_node_t* n, dmvsi_var_t var) +{ + uint32_t count = 0; + for (; n != NULL; n = n->next) + { + if (n->kind != DMVSI_NODE_GROUP) + continue; + if (n->click != 0) + { + const dmvsi_action_t* a = NULL; + uint32_t k = flat_actions(doc, n->click, &a); + bool sets = false; + for (uint32_t i = 0; i < k && !sets; i++) + sets = a[i].kind == DMVSI_ACT_SET && a[i].var == var; + count += sets ? 1U : 0U; + } + count += clicks_setting(doc, n->first, var); + } + return count; +} + +DMOD_TEST_STEP(dmvs_html_converts_what_scripts_build) +{ + int status = -1; + DMOD_TEST_EXPECT_TRUE(write_file(TEST_FILE("build.html"), + "
-
" + "
")); + dmvsi_doc_t doc = convert(TEST_FILE("build.html"), "screen", 0, 0, &status); + DMOD_TEST_EXPECT_EQ(status, 0); + if (doc == NULL) + return; + const dmvsi_node_t* root = dmvsi_root(doc); + + /* The rows as the page loads: their texts, the current one's background, each clicked */ + DMOD_TEST_EXPECT_TRUE(find(root->first, DMVSI_NODE_TEXT, "One", NULL) != NULL); + DMOD_TEST_EXPECT_TRUE(find(root->first, DMVSI_NODE_TEXT, "2:10", NULL) != NULL); + DMOD_TEST_EXPECT_TRUE(find(root->first, DMVSI_NODE_TEXT, "0:07", NULL) != NULL); + dmvsi_rect_t on = { 0, DMVSI_PX(40), DMVSI_PX(200), DMVSI_PX(20) }; + const dmvsi_node_t* fill = find(root->first, DMVSI_NODE_RECT, NULL, &on); + DMOD_TEST_EXPECT_TRUE(fill != NULL && fill->u.fill.paint.color == 0xFF3366FFu); + dmvsi_var_t title = var_named(doc, "title_text"); + uint32_t clicked = clicks_setting(doc, root->first, title); + DMOD_TEST_EXPECT_EQ(clicked, 3u); + dmvsi_free(doc); +} From dad9e227c257815405ec25ac93b3f5b0a4aea3db Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 15:06:14 +0200 Subject: [PATCH 05/11] dmvs_html: className and src of known choices; an icon's glyph is its look el.className set to one of known class lists (a row rendered as `row ${active ? 'on' : ''}`): a change to each, on which it is - the row's looks follow the current song. img.src set to one of known images (playlist[i].cover): a variant of the image for each, shown on its variable. An element's look counts its text (an icon font's ::before): play / pause changed nothing before. Co-Authored-By: Claude Opus 5.5 --- src/private.h | 2 +- src/script.c | 205 ++++++++++++++++++++++++++++++++++++++++- tests/dmvs_html_test.c | 5 +- 3 files changed, 209 insertions(+), 3 deletions(-) diff --git a/src/private.h b/src/private.h index d2b6966..fefb203 100644 --- a/src/private.h +++ b/src/private.h @@ -104,7 +104,7 @@ typedef struct } box_t; /* One look of an element: the element as laid out in a state of the page, shown on its conditions */ -#define MAX_VARIANTS 4u +#define MAX_VARIANTS 8u typedef struct { diff --git a/src/script.c b/src/script.c index b324541..81ba2cf 100644 --- a/src/script.c +++ b/src/script.c @@ -135,6 +135,17 @@ typedef struct #define MAX_LOOKVARS 16u +/* An image whose src a script sets to one of known ones (dmvs_js_choices()): a variant of each, its variable which */ +#define MAX_SRCVARS 8u + +typedef struct +{ + node_t* element; + dmvsi_var_t var; + const char* picks[MAX_VARIANTS]; + uint32_t count; +} srcvar_t; + typedef struct { conv_t* c; @@ -162,6 +173,8 @@ typedef struct uint32_t wide_count; const char* chars; uint32_t made; /* document.createElement() so far: the k-th is the build's */ + srcvar_t srcvars[MAX_SRCVARS]; + uint32_t srcvar_count; } script_t; static void report(script_t* sc, const char* what) @@ -645,6 +658,182 @@ static int host_get(void* ctx, dmvs_js_compiler_t js, const dmvs_js_value_t* obj return -ENOTSUP; } +/* A class list's change from what the element has: "+a -b" (by name, as flush_pending() writes it) */ +static const char* class_diff(script_t* sc, const node_t* e, const char* list) +{ + const char* names[2 * MAX_PENDING_OPS]; + bool add[2 * MAX_PENDING_OPS]; + uint32_t n = 0; + char word[128]; + /* Added: what the list has, the element not */ + for (const char* q = list; *q != '\0' && n < MAX_PENDING_OPS; ) + { + while (*q == ' ' || *q == '\t' || *q == '\n') + q++; + size_t k = 0; + while (q[k] != '\0' && q[k] != ' ' && q[k] != '\t' && q[k] != '\n') + k++; + if (k == 0) + break; + size_t m = (k < sizeof(word) - 1U) ? k : sizeof(word) - 1U; + memcpy(word, q, m); + word[m] = '\0'; + q += k; + if (!has_class(e, word)) + { + names[n] = arena_strndup(&sc->c->arena, word, m); + add[n++] = true; + } + } + /* Removed: what the element has, the list not */ + for (uint32_t i = 0; i < e->class_count && n < 2U * MAX_PENDING_OPS; i++) + { + const char* cls = e->classes[i]; + size_t m = strlen(cls); + bool kept = false; + for (const char* q = list; *q != '\0' && !kept; ) + { + while (*q == ' ' || *q == '\t' || *q == '\n') + q++; + size_t k = 0; + while (q[k] != '\0' && q[k] != ' ' && q[k] != '\t' && q[k] != '\n') + k++; + kept = k == m && memcmp(q, cls, m) == 0; + q += k; + } + if (!kept) + { + names[n] = cls; + add[n++] = false; + } + } + char spec[512]; + size_t len = 0; + bool used[2 * MAX_PENDING_OPS] = { false }; + spec[0] = '\0'; + for (uint32_t k = 0; k < n; k++) + { + uint32_t best = n; + for (uint32_t j = 0; j < n; j++) + if (!used[j] && names[j] != NULL && (best == n || strcmp(names[j], names[best]) < 0)) + best = j; + if (best == n) + break; + used[best] = true; + len += (size_t)Dmod_SnPrintf(spec + len, sizeof(spec) - len, "%s%c%s", (len > 0) ? " " : "", add[best] ? '+' : '-', names[best]); + if (len >= sizeof(spec)) + len = sizeof(spec) - 1U; + } + return arena_strndup(&sc->c->arena, spec, len); +} + +/* el.className = one of known lists: the change to each, on which it is */ +static int set_class_name(script_t* sc, node_t* e, const dmvs_js_value_t* value) +{ + const dmvs_js_value_t* picks = NULL; + dmvsi_var_t index = 0; + uint32_t n = dmvs_js_choices(sc->js, value, &picks, &index); + const char* text = static_text(sc, value); + if (text != NULL) + { + const char* spec = class_diff(sc, e, text); + if (spec != NULL) + place(sc, e, MOD_CLASSES, spec, NULL); + return 0; + } + if (n == 0 || n > MAX_VARIANTS) + return is_built(e) ? 0 : -ENOTSUP; /* (what the build made it: as it is) */ + for (uint32_t i = 0; i < n; i++) + { + if (picks[i].kind != DMVS_JS_V_STRING) + return -ENOTSUP; + } + for (uint32_t i = 0; i < n; i++) + { + const char* spec = class_diff(sc, e, picks[i].text); + if (spec == NULL) + return -ENOMEM; + emit_op(sc, DMVSI_ACT_IF_EQ, index, 0, (int32_t)i, NULL); + place(sc, e, MOD_CLASSES, spec, NULL); + emit_op(sc, DMVSI_ACT_END, 0, 0, 0, NULL); + } + return 0; +} + +/* img.src = one of known images: a variant of each (made at the end), its variable set to which */ +static int set_src(script_t* sc, node_t* e, const dmvs_js_value_t* value) +{ + const dmvs_js_value_t* picks = NULL; + dmvsi_var_t index = 0; + uint32_t n = dmvs_js_choices(sc->js, value, &picks, &index); + if (!node_is(e, "img") || n < 2 || n > MAX_VARIANTS) + return -ENOTSUP; + srcvar_t* sv = NULL; + for (uint32_t i = 0; i < sc->srcvar_count && sv == NULL; i++) + sv = (sc->srcvars[i].element == e) ? &sc->srcvars[i] : NULL; + if (sv == NULL) + { + if (sc->srcvar_count >= MAX_SRCVARS) + return -ENOTSUP; + sv = &sc->srcvars[sc->srcvar_count]; + memset(sv, 0, sizeof(*sv)); + const char* now = node_attr(e, "src"); + int32_t initial = 0; + for (uint32_t i = 0; i < n; i++) + { + if (picks[i].kind != DMVS_JS_V_STRING) + return -ENOTSUP; + sv->picks[i] = arena_strndup(&sc->c->arena, picks[i].text, picks[i].length); + if (now != NULL && strcmp(now, picks[i].text) == 0) + initial = (int32_t)i; + } + char label[48]; + Dmod_SnPrintf(label, sizeof(label), "%s_src", label_of(e)); + if ((sv->var = dmvsi_add_var(sc->c->doc, label, initial)) == 0) + return -ENOMEM; + sv->element = e; + sv->count = n; + sc->srcvar_count++; + } + else if (sv->count != n) + return -ENOTSUP; /* Of other images elsewhere */ + emit_op(sc, DMVSI_ACT_SET, sv->var, index, 0, NULL); + return 0; +} + +/* The images' variants: the element as it is, its src another */ +static int make_src_variants(script_t* sc) +{ + for (uint32_t i = 0; i < sc->srcvar_count; i++) + { + srcvar_t* sv = &sc->srcvars[i]; + node_t* e = sv->element; + dynamic_t* d = dynamic_of(sc, e); + if (d == NULL) + return -ENOMEM; + if (d->variant_count > 0 || !e->box.placed) + { + WARN(sc->c, "script: the images of #%s as well as its looks - not converted\n", label_of(e)); + continue; + } + for (uint32_t k = 0; k < sv->count; k++) + { + node_t* look = arena_alloc(&sc->c->arena, sizeof(node_t)); + attr_t* src = arena_alloc(&sc->c->arena, sizeof(attr_t)); + if (look == NULL || src == NULL) + return -ENOMEM; + *look = *e; + look->dynamic = NULL; + src->name = (char*)"src"; + src->value = (char*)sv->picks[k]; + src->next = e->attrs; /* (before the element's own: node_attr() finds it first) */ + look->attrs = src; + d->variants[d->variant_count++] = (variant_t){ look, sv->var, (int32_t)k, -1 }; + } + } + return 0; +} + static int host_set(void* ctx, dmvs_js_compiler_t js, const dmvs_js_value_t* object, const char* name, const dmvs_js_value_t* value) { script_t* sc = ctx; @@ -674,7 +863,11 @@ static int host_set(void* ctx, dmvs_js_compiler_t js, const dmvs_js_value_t* obj } if (kind != H_ELEMENT || e == NULL) return -ENOTSUP; - if (is_built(e) && (strcmp(name, "className") == 0 || strcmp(name, "innerHTML") == 0 || strcmp(name, "id") == 0)) + if (strcmp(name, "className") == 0) + return set_class_name(sc, e, value); + if (strcmp(name, "src") == 0) + return set_src(sc, e, value); + if (is_built(e) && (strcmp(name, "innerHTML") == 0 || strcmp(name, "id") == 0)) return 0; /* As the build made it */ if (holds_built(e) && strcmp(name, "innerHTML") == 0) return 0; /* Emptied to be built anew: as the build left it */ @@ -896,7 +1089,11 @@ static uint32_t inside(const node_t* e, const node_t* n, uint32_t h, uint32_t de for (int i = 0; i < 6; i++) h = (h ^ (uint32_t)v[i]) * 16777619u; for (const frag_t* f = k->box.frags; f != NULL; f = f->next) + { h = (h ^ (uint32_t)(f->x + f->y * 7 + (int32_t)f->length)) * 16777619u; + for (size_t i = 0; f->text != NULL && i < f->length; i++) + h = (h ^ (uint8_t)f->text[i]) * 16777619u; /* Another text, another icon (::before) */ + } h = inside(e, k, h, depth + 1U); } return h; @@ -1023,7 +1220,11 @@ static uint32_t look_of(const node_t* e) for (const char* u = st->background_url; u != NULL && *u != '\0'; u++) h = mix(h, *u); for (const frag_t* f = e->box.frags; f != NULL; f = f->next) + { h = mix(h, f->x + f->y * 7 + (int32_t)f->length); + for (size_t i = 0; f->text != NULL && i < f->length; i++) + h = mix(h, (uint8_t)f->text[i]); + } return inside(e, e, h, 0); } @@ -1652,6 +1853,8 @@ int script_compile(conv_t* c) status = expand_held(sc); if (status == 0) status = lay_out_changes(sc); + if (status == 0) + status = make_src_variants(sc); if (status == 0) status = make_handlers(sc); texts(sc, c->document, 0); diff --git a/tests/dmvs_html_test.c b/tests/dmvs_html_test.c index 10c1ada..26c4a08 100644 --- a/tests/dmvs_html_test.c +++ b/tests/dmvs_html_test.c @@ -639,7 +639,7 @@ DMOD_TEST_STEP(dmvs_html_converts_what_scripts_build) " const div = document.createElement('div');\n" " div.className = `row ${i === current ? 'on' : ''}`;\n" " div.innerHTML = `${song.t} ${time(song.s)}`;\n" - " div.onclick = () => { current = i; document.getElementById('title').innerText = song.t; };\n" + " div.onclick = () => { current = i; document.getElementById('title').innerText = song.t; render(); };\n" " list.appendChild(div);\n" " });\n" "}\n" @@ -661,5 +661,8 @@ DMOD_TEST_STEP(dmvs_html_converts_what_scripts_build) dmvsi_var_t title = var_named(doc, "title_text"); uint32_t clicked = clicks_setting(doc, root->first, title); DMOD_TEST_EXPECT_EQ(clicked, 3u); + /* Rendered anew on a click: each row's class list one of two - its look's variable set */ + dmvsi_var_t look = var_named(doc, "div_look"); + DMOD_TEST_EXPECT_TRUE(look != 0 && clicks_setting(doc, root->first, look) == 3u); dmvsi_free(doc); } From 05f9fdac2f6fbc221c269c808222efbe6935dc1a Mon Sep 17 00:00:00 2001 From: Patryk Kubiak Date: Sat, 10 Oct 2026 15:48:44 +0200 Subject: [PATCH 06/11] dmvs_html: an element's other looks seen as it is A look of an element is laid out in a state of the page, where a screen hidden as the page loads (visibility) still is: revealed in the page by the change that shows it, not in the states. A row of a list in such a screen was painted unseen in its other look - it vanished when another song was chosen. A look is now seen where the element is. Co-Authored-By: Claude Opus 5.5 --- src/script.c | 38 ++++++++++++++++++++++++++++++++++++++ tests/dmvs_html_test.c | 20 ++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/script.c b/src/script.c index 81ba2cf..72b419d 100644 --- a/src/script.c +++ b/src/script.c @@ -1258,6 +1258,40 @@ static bool has_classes(const node_t* e, const char* spec) return true; } +/* + * A look of an element (laid out in a state of the page) seen as the element + * is: hidden only where the element is - a screen hidden as the page is + * (visibility), shown by a change, is revealed in the page, not in its + * states (a row of its list would be painted unseen in its other look) + */ +static void unhide(node_t* n, uint32_t depth) +{ + if (n->style != NULL) + n->style->hidden = false; + for (node_t* k = n->first; k != NULL && depth < 200U; k = k->next) + if (k->kind == NODE_ELEMENT) + unhide(k, depth + 1U); +} + +static void seen_like(node_t* look, const node_t* e, uint32_t depth) +{ + if (look == NULL || e == NULL || depth > 200U) + return; + if (look->style != NULL && e->style != NULL && !e->style->hidden) + look->style->hidden = false; + const node_t* g = e->first; + for (node_t* k = look->first; k != NULL; k = k->next) + { + if (g != NULL && g->kind == k->kind) + { + seen_like(k, g, depth + 1U); + g = g->next; + } + else if (k->kind == NODE_ELEMENT && look->style != NULL && !look->style->hidden) + unhide(k, depth + 1U); /* (none to follow: seen as its parent is) */ + } +} + /* An element a change shows that is hidden (visibility) as the page is: shown, its opacity * what hides it - so that it can fade in. Its inside hidden as it is then */ static void reveal(node_t* e, const node_t* f, uint32_t depth) @@ -1376,6 +1410,8 @@ static int make_variants(script_t* sc, node_t* e, const change_t* toggled, bool looks[n++] = (variant_t){ pressed, var, base, 1 }; if (pressed != NULL && toggled != NULL) looks[n++] = (variant_t){ pressed_toggled, var, 1 - base, 1 }; + for (uint8_t k = 1; k < n; k++) + seen_like(looks[k].node, e, 0); memcpy(d->variants, looks, n * sizeof(variant_t)); d->variant_count = n; return 0; @@ -1675,6 +1711,8 @@ static int lay_out_changes(script_t* sc) looks[n++] = (variant_t){ e->box.placed ? e : NULL, lv->var, 0, -1 }; for (uint32_t k = 0; k < lv->count; k++) looks[n++] = (variant_t){ lv->looks[k], lv->var, (int32_t)(k + 1U), -1 }; + for (uint8_t k = 1; k < n; k++) + seen_like(looks[k].node, e, 0); memcpy(e->dynamic->variants, looks, n * sizeof(variant_t)); e->dynamic->variant_count = n; } diff --git a/tests/dmvs_html_test.c b/tests/dmvs_html_test.c index 26c4a08..483833e 100644 --- a/tests/dmvs_html_test.c +++ b/tests/dmvs_html_test.c @@ -598,6 +598,19 @@ DMOD_TEST_STEP(dmvs_html_converts_texts_and_timers) dmvsi_free(doc); } +static uint32_t count_texts(const dmvsi_node_t* n, const char* text) +{ + uint32_t count = 0; + for (; n != NULL; n = n->next) + { + if (n->kind == DMVSI_NODE_TEXT && n->u.text.var == 0 && strcmp(n->u.text.text, text) == 0) + count++; + if (n->kind == DMVSI_NODE_GROUP) + count += count_texts(n->first, text); + } + return count; +} + /* The groups clicked whose handler sets a variable */ static uint32_t clicks_setting(dmvsi_doc_t doc, const dmvsi_node_t* n, dmvsi_var_t var) { @@ -627,8 +640,9 @@ DMOD_TEST_STEP(dmvs_html_converts_what_scripts_build) "
-
" - "