diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b98a58..37802d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,10 +69,11 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} src/style.c src/layout.c src/paint.c + src/script.c ) dmod_link_modules(${DMOD_MODULE_NAME} - dmvsi@>=0.1 + dmvsi@>=0.2 ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE diff --git a/Makefile b/Makefile index 4b35448..d5f558a 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 +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/script.c # The list of C++ sources DMOD_CXXSOURCES= diff --git a/README.md b/README.md index e333c55..1079b19 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,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 **still UI pages**: the screens a designer (or an AI) draws in HTML with -Tailwind CSS. Scripts are not run: a page is converted as it is when it -loads. +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. ## What it does @@ -56,6 +56,47 @@ in Tailwind's style sheet. CSS does; text is measured with them as the view will draw it. Icon fonts (Font Awesome) work through their CSS: `::before { content: "\f1eb" }`. +## 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: + +```js +const home = document.getElementById('home'); // an element +let current = null; // a variable the handlers set: the view's +function open(id) { // onclick="open('settings')": inlined + const w = document.getElementById(id); + w.classList.add('active'); // classList.add / remove / toggle + home.style.opacity = '0.3'; // style. = ... + current = w; +} +function close() { + if (current) { // if (x), (!x), (a === b), classList.contains() + current.classList.remove('active'); + home.style.opacity = '1'; + current = null; + } +} +``` + +`this` in an `onclick` is its element. Every change is laid out - the page +with that class or that style: + +- how the element **moves and fades** becomes its group's variables, set at + once or **animated by its CSS `transition`** (duration and timing + function) - the view switches its screens as the page does, and slides + them as it does; +- when it **looks different** (a switch on, a light off, play / pause, an + icon hidden, another label) it is painted in each state of its class, + shown on the class's variable; +- `:active` (and Tailwind's `active:`) is its **pressed look**, shown while + its box is pressed. + +What is reported and left out: what the page does when it loads (timers, +`Date`, text a script writes), loops, events other than clicks, a style a +script sets that changes how an element looks (but its opacity). A class +changes only the look of its element - what it moves around it stays. + ## Resources Style sheets, fonts and images are files next to the page; a URL is read @@ -74,8 +115,7 @@ the view is the viewport, or the element the options name (`-r`). ## What it does not do (yet) -- JavaScript - a page is still; what a script would show (an opened window) - is converted when the HTML says it (e.g. a class added). +- JavaScript beyond the above (see [Scripts](#scripts)). - Floats, tables (laid out as blocks), multi-line flex (`flex-wrap`), explicit grid placement (only spans), `position: sticky` (relative). - Rounded clipping: `overflow: hidden` with `border-radius` clips to the diff --git a/src/css.c b/src/css.c index e9b7640..80bc8dd 100644 --- a/src/css.c +++ b/src/css.c @@ -311,6 +311,11 @@ static bool parse_pseudo(conv_t* c, const char** p, const char* end, compound_t* out->pseudo |= PC_ROOT; else if (ieq(name, n, "empty")) out->pseudo |= PC_EMPTY; + else if (ieq(name, n, "active")) + { + out->pseudo |= PC_ACTIVE; + c->has_active = true; + } else if (args != NULL && (ieq(name, n, "not") || ieq(name, n, "is") || ieq(name, n, "where") || ieq(name, n, "matches"))) { uint32_t inner = 0; @@ -558,6 +563,8 @@ static bool compound_matches(const compound_t* k, const node_t* n) return false; if ((k->pseudo & PC_EMPTY) != 0 && n->first != NULL) return false; + if ((k->pseudo & PC_ACTIVE) != 0 && !n->active) + return false; for (uint8_t i = 0; i < k->not_count; i++) { if (compound_matches(&k->nots[i], n)) diff --git a/src/dmvs_html.c b/src/dmvs_html.c index aee09ee..3d78baa 100644 --- a/src/dmvs_html.c +++ b/src/dmvs_html.c @@ -478,6 +478,125 @@ dmod_dmvsi_dif_api_declaration(1.0, dmvs_html, bool, _probe, ( const char* path, return false; } +/* ---- A page as a script changed it ---- */ + +static node_t* element_at(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 = element_at(k, index, depth + 1U); + if (found != NULL) + return found; + } + return NULL; +} + +/* "backgroundColor" -> "background-color" */ +static void kebab(const char* name, char* out, size_t size) +{ + size_t n = 0; + for (const char* s = name; *s != '\0' && n + 2U < size; s++) + { + if (*s >= 'A' && *s <= 'Z') + { + out[n++] = '-'; + out[n++] = (char)(*s - 'A' + 'a'); + } + else + out[n++] = *s; + } + out[n] = '\0'; +} + +static void apply_mod(conv_t* c, const mod_t* m) +{ + node_t* n = element_at(c->document, m->element, 0); + if (n == NULL) + return; + if (m->kind == MOD_ACTIVE) + { + for (node_t* a = n; a != NULL && a->kind == NODE_ELEMENT; a = a->parent) + a->active = true; /* :active holds for the pressed one and its ancestors */ + return; + } + if (m->kind == MOD_STYLE) + { + /* As element.style. = value does: the style attribute, at its end */ + char property[64]; + kebab(m->name, property, sizeof(property)); + attr_t* style = NULL; + for (attr_t* a = n->attrs; a != NULL; a = a->next) + { + if (a->name != NULL && strcmp(a->name, "style") == 0) + style = a; + } + if (style == NULL) + { + style = arena_alloc(&c->arena, sizeof(*style)); + if (style == NULL) + return; + style->name = arena_strndup(&c->arena, "style", 5); + style->value = arena_strndup(&c->arena, "", 0); + style->next = n->attrs; + n->attrs = style; + } + size_t length = strlen(style->value) + strlen(property) + strlen(m->value) + 4U; + char* value = arena_alloc(&c->arena, length); + if (value != NULL) + { + Dmod_SnPrintf(value, length, "%s;%s:%s", style->value, property, m->value); + style->value = value; + } + return; + } + /* A class added or removed */ + uint32_t at = n->class_count; + for (uint32_t i = 0; i < n->class_count; i++) + { + if (strcmp(n->classes[i], m->name) == 0) + at = i; + } + if (m->kind == MOD_CLASS_REMOVE && at < n->class_count) + { + for (uint32_t i = at + 1U; i < n->class_count; i++) + n->classes[i - 1U] = n->classes[i]; + n->class_count--; + } + else if (m->kind == MOD_CLASS_ADD && at == n->class_count) + { + char** classes = arena_alloc(&c->arena, (n->class_count + 1U) * sizeof(char*)); + if (classes == NULL) + return; + if (n->class_count > 0) + memcpy(classes, n->classes, n->class_count * sizeof(char*)); + classes[n->class_count++] = (char*)m->name; + n->classes = classes; + } +} + +int run_layout(conv_t* c) +{ + size_t size = 0; + char* text = (c->path != NULL) ? read_resource(c, c->path, &size) : NULL; + if (text == NULL) + return c->arena.failed ? -ENOMEM : -ENOENT; + if ((c->document = html_parse(c, text, size)) == NULL) + return c->arena.failed ? -ENOMEM : -EBADMSG; + for (uint32_t i = 0; i < c->mod_count; i++) + apply_mod(c, &c->mods[i]); + load_sheets(c, c->document, 0); + if (c->tailwind) + css_parse(c, tailwind_preflight, strlen(tailwind_preflight), c->path, LAYER_TAILWIND); + style_compute(c, c->document); + if (!c->arena.failed) + layout_page(c, c->document); + return c->arena.failed ? -ENOMEM : 0; +} + dmod_dmvsi_dif_api_declaration(1.0, dmvs_html, int, _convert, ( const char* path, const dmvsi_options_t* options, dmvsi_doc_t doc )) { conv_t* c = Dmod_Malloc(sizeof(*c)); @@ -490,29 +609,17 @@ dmod_dmvsi_dif_api_declaration(1.0, dmvs_html, int, _convert, ( const char* path c->vh = ((options->height != 0) ? options->height : DEFAULT_HEIGHT) * U; c->path = arena_strndup(&c->arena, path, strlen(path)); - int status = 0; - size_t size = 0; - char* text = (c->path != NULL) ? read_resource(c, c->path, &size) : NULL; - if (text == NULL) - status = c->arena.failed ? -ENOMEM : -ENOENT; - if (status == 0 && (c->document = html_parse(c, text, size)) == NULL) - status = c->arena.failed ? -ENOMEM : -EBADMSG; + int status = (c->path != NULL) ? run_layout(c) : -ENOMEM; if (status == 0) - { - load_sheets(c, c->document, 0); - if (c->tailwind) - css_parse(c, tailwind_preflight, strlen(tailwind_preflight), c->path, LAYER_TAILWIND); - style_compute(c, c->document); - if (!c->arena.failed) - layout_page(c, c->document); - if (!c->arena.failed) - status = paint_page(c, c->document); - if (c->arena.failed) - status = -ENOMEM; - const char* dump_path = Dmod_GetEnv("DMVS_HTML_DUMP"); - if (status == 0 && dump_path != NULL && dump_path[0] != '\0') - dump(c, dump_path); - } + status = script_compile(c); /* What its scripts do: variables, handlers */ + if (status == 0) + status = paint_page(c, c->document); + script_free(c); + if (c->arena.failed) + status = -ENOMEM; + const char* dump_path = Dmod_GetEnv("DMVS_HTML_DUMP"); + if (status == 0 && dump_path != NULL && dump_path[0] != '\0') + dump(c, dump_path); if (c->warnings > 0) DMOD_LOG_INFO("dmvs_html: %s: %u warnings\n", path, (unsigned)c->warnings); arena_free(&c->arena); diff --git a/src/html.c b/src/html.c index 9450194..e3e8b2e 100644 --- a/src/html.c +++ b/src/html.c @@ -152,6 +152,7 @@ typedef struct conv_t* c; node_t* open[MAX_DEPTH]; uint32_t depth; + uint32_t elements; /* Numbered so far */ } parser_t; static node_t* current(parser_t* p) @@ -264,6 +265,7 @@ static const char* start_tag(parser_t* p, const char* s, const char* end) if (n == NULL) return end; n->kind = NODE_ELEMENT; + n->index = ++p->elements; n->tag = arena_strndup(&c->arena, name, (size_t)(s - name)); if (n->tag == NULL) return end; diff --git a/src/paint.c b/src/paint.c index 1a43208..dc0e48c 100644 --- a/src/paint.c +++ b/src/paint.c @@ -32,6 +32,8 @@ typedef struct uint32_t shadow_color; int32_t sigma; const style_t* canvas; /* Its background is the canvas's: not painted again */ + const node_t* plain; /* Painted as one of its looks: not as what a script changes */ + const node_t* shadowed; /* Its outer shadows painted already (outside its click) */ int status; } painter_t; @@ -96,6 +98,57 @@ static dmvsi_rect_t border_box(const painter_t* p, const node_t* n) return r; } +/* ---- What a script moves, fades, clicks ---- */ + +void view_origin(const conv_t* c, int32_t* ox, int32_t* oy) +{ + *ox = *oy = 0; + if (c->options->root != NULL) + { + const node_t* root = find_id(c->document, c->options->root, 0); + if (root != NULL) + { + *ox = root->box.ax; + *oy = root->box.ay; + } + } +} + +/* The rectangle of the group a script's element is painted in: its border box */ +dmvsi_rect_t group_rect(const conv_t* c, const node_t* n, int32_t ox, int32_t oy) +{ + (void)c; + dmvsi_rect_t r = { n->box.ax - ox, n->box.ay - oy, n->box.w, n->box.h }; + return r; +} + +/* A group for an element a script changes: bound to its variables, clicked */ +static bool begin_dynamic(painter_t* p, const node_t* n) +{ + if (n->dynamic == NULL || p->shadow || n == p->plain) + return false; + dmvsi_group_t g; + memset(&g, 0, sizeof(g)); + g.rect = group_rect(p->c, n, p->ox, p->oy); + g.opacity = 255; + g.name = n->id; + check(p, dmvsi_begin_group(p->c->doc, &g)); + for (uint8_t b = 0; b < DMVSI_BIND_COUNT; b++) + { + if (n->dynamic->bind[b] != 0) + check(p, dmvsi_bind(p->c->doc, b, n->dynamic->bind[b])); + } + if (n->dynamic->click != 0) + check(p, dmvsi_on_click(p->c->doc, n->dynamic->click)); + return true; +} + +static void end_dynamic(painter_t* p, bool begun) +{ + if (begun) + check(p, dmvsi_end_group(p->c->doc)); +} + static dmvsi_rect_t inner(const dmvsi_rect_t* r, const int32_t* b) { dmvsi_rect_t i = { r->x + b[3], r->y + b[0], max32(r->w - b[1] - b[3], 0), max32(r->h - b[0] - b[2], 0) }; @@ -103,6 +156,18 @@ static dmvsi_rect_t inner(const dmvsi_rect_t* r, const int32_t* b) } /* One radius for the box: dmview rounds all of a box's corners the same - the largest */ +static int32_t radius_of(const style_t* st, int32_t w, int32_t h); + +/* A box as large as the view (wherever it is - a window that slides in): its corners are the + * screen's when it is on it, nothing is behind them to show */ +static bool whole_view(const painter_t* p, const dmvsi_rect_t* r) +{ + uint16_t w = 0, h = 0; + if (dmvsi_view_size(p->c->doc, &w, &h) != 0) + return false; + return r->w >= DMVSI_PX(w) && r->h >= DMVSI_PX(h); +} + static int32_t radius_of(const style_t* st, int32_t w, int32_t h) { int32_t r = 0; @@ -268,14 +333,12 @@ static void fill(painter_t* p, dmvsi_rect_t r, int32_t radius, const dmvsi_paint check(p, dmvsi_add_fill(p->c->doc, &f)); } -/* Background, shadows, border of a box at r */ -static void paint_decoration(painter_t* p, const style_t* st, dmvsi_rect_t r, const int32_t* borders) +/* Outer shadows of a box at r, the last one at the bottom */ +static void paint_outer_shadows(painter_t* p, const style_t* st, dmvsi_rect_t r) { if (st->hidden || r.w <= 0 || r.h <= 0) return; int32_t radius = radius_of(st, r.w, r.h); - - /* Outer shadows, the last one at the bottom */ for (int i = (int)st->shadow_count - 1; i >= 0 && !p->shadow; i--) { const shadow_t* sh = &st->shadows[i]; @@ -295,6 +358,16 @@ static void paint_decoration(painter_t* p, const style_t* st, dmvsi_rect_t r, co if (s.shape.w > 0 && s.shape.h > 0) check(p, dmvsi_add_shadow(p->c->doc, &s)); } +} + +/* Background, shadows (the outer ones too, unless painted already), border of a box at r */ +static void paint_box(painter_t* p, const style_t* st, dmvsi_rect_t r, const int32_t* borders, bool outer) +{ + if (st->hidden || r.w <= 0 || r.h <= 0) + return; + int32_t radius = whole_view(p, &r) ? 0 : radius_of(st, r.w, r.h); + if (outer) + paint_outer_shadows(p, st, r); dmvsi_paint_t paint; memset(&paint, 0, sizeof(paint)); @@ -371,6 +444,11 @@ static void paint_decoration(painter_t* p, const style_t* st, dmvsi_rect_t r, co } } +static void paint_decoration(painter_t* p, const style_t* st, dmvsi_rect_t r, const int32_t* borders) +{ + paint_box(p, st, r, borders, p->shadowed == NULL || p->shadowed->style != st); +} + /* The lines of text (and inline boxes) a block holds */ static void paint_lines(painter_t* p, const node_t* n) { @@ -430,6 +508,7 @@ typedef struct static void paint_context(painter_t* p, node_t* n, uint32_t depth); static void paint_flow(painter_t* p, node_t* n, uint32_t depth); +static bool is_layer(const node_t* n); /* The layers of a stacking context: positioned and other layer boxes in its flow */ static void collect_layers(node_t* n, layers_t* out, uint32_t depth) @@ -452,8 +531,8 @@ static void collect_layers(node_t* n, layers_t* out, uint32_t depth) } continue; } - if (clips(k->style)) - continue; /* It holds its own */ + if (clips(k->style) || (k->dynamic != NULL && k->dynamic->variant_count > 0)) + continue; /* It holds its own (its looks paint theirs) */ collect_layers(k, out, depth + 1U); } } @@ -519,6 +598,51 @@ static void paint_inner(painter_t* p, node_t* n, uint32_t depth) check(p, dmvsi_end_group(p->c->doc)); } +static void paint_flow_element(painter_t* p, node_t* k, uint32_t depth); +static void paint_inner(painter_t* p, node_t* n, uint32_t depth); + +/* An element of looks (variants): each in a group shown on its conditions, in the element's + * group (its click, its variables); true when it has looks */ +static bool paint_looks(painter_t* p, node_t* n, uint32_t depth) +{ + if (n->dynamic == NULL || n->dynamic->variant_count == 0 || n == p->plain || p->shadow) + return false; + /* Its outer shadows beneath, outside its group: what is clicked is the element */ + if (n->box.laid_out && !clips(n->style) && displayed(n)) + paint_outer_shadows(p, n->style, border_box(p, n)); + bool dynamic = begin_dynamic(p, n); + for (uint8_t i = 0; i < n->dynamic->variant_count; i++) + { + const variant_t* v = &n->dynamic->variants[i]; + node_t* look = v->node; + if (look == NULL || look->style == NULL || look->style->display == DISPLAY_NONE || !look->box.placed) + continue; + dmvsi_group_t g; + memset(&g, 0, sizeof(g)); + g.rect = group_rect(p->c, look, p->ox, p->oy); + g.opacity = 255; + check(p, dmvsi_begin_group(p->c->doc, &g)); + if (v->var != 0) + check(p, dmvsi_show_when(p->c->doc, v->var, v->value)); + if (v->pressed >= 0) + check(p, dmvsi_show_when(p->c->doc, DMVSI_VAR_PRESSED, v->pressed)); + const node_t* saved = p->plain; + const node_t* saved_shadowed = p->shadowed; + p->plain = look; + p->shadowed = (n->box.laid_out && !clips(n->style)) ? look : NULL; + /* As a stacking context of its own: its positioned boxes are of its look */ + if (is_layer(look)) + paint_context(p, look, depth + 1U); + else + paint_inner(p, look, depth + 1U); + p->plain = saved; + p->shadowed = saved_shadowed; + check(p, dmvsi_end_group(p->c->doc)); + } + end_dynamic(p, dynamic); + return true; +} + /* The normal flow under n: each box's decoration and lines, in tree order - not the layers */ static void paint_flow(painter_t* p, node_t* n, uint32_t depth) { @@ -526,34 +650,59 @@ static void paint_flow(painter_t* p, node_t* n, uint32_t depth) return; for (node_t* k = n->first; k != NULL; k = k->next) { - if (!displayed(k) || is_layer(k)) + bool looks = k->dynamic != NULL && k->dynamic->variant_count > 0 && k->kind == NODE_ELEMENT; + if (!looks && (!displayed(k) || is_layer(k))) continue; - if (clips(k->style)) - { - paint_inner(p, k, depth + 1U); + if (looks && displayed(k) && is_layer(k)) + continue; /* Painted as a layer */ + if (paint_looks(p, k, depth + 1U)) continue; - } - if (k->box.laid_out) - { - paint_decoration(p, k->style, border_box(p, k), k->box.b); - if (node_is(k, "img")) - paint_image(p, k); - paint_lines(p, k); - } - paint_flow(p, k, depth + 1U); + paint_flow_element(p, k, depth); + } +} + +/* One element of the flow: its decoration, lines and what flows in it */ +static void paint_flow_element(painter_t* p, node_t* k, uint32_t depth) +{ + /* What a script clicks or moves: its group as large as it - its outer shadows beneath, outside */ + bool outer_done = k->dynamic != NULL && k != p->plain && !p->shadow && k->box.laid_out && !clips(k->style); + if (outer_done) + paint_outer_shadows(p, k->style, border_box(p, k)); + bool dynamic = begin_dynamic(p, k); + if (clips(k->style)) + { + paint_inner(p, k, depth + 1U); + end_dynamic(p, dynamic); + return; + } + if (k->box.laid_out) + { + paint_box(p, k->style, border_box(p, k), k->box.b, !outer_done); + if (node_is(k, "img")) + paint_image(p, k); + paint_lines(p, k); } + paint_flow(p, k, depth + 1U); + end_dynamic(p, dynamic); } static void paint_context(painter_t* p, node_t* n, uint32_t depth) { if (depth > MAX_DEPTH || !n->box.placed) return; + if (paint_looks(p, n, depth)) + return; style_t* st = n->style; - bool group = st->opacity < 255; + bool dynamic = begin_dynamic(p, n); + /* Its opacity: its variable's, when a script changes it */ + bool group = st->opacity < 255 && !(dynamic && n->dynamic->bind[DMVSI_BIND_OPACITY] != 0); if (group) { if (st->opacity == 0) + { + end_dynamic(p, dynamic); return; + } dmvsi_group_t g; memset(&g, 0, sizeof(g)); g.opacity = st->opacity; @@ -575,11 +724,12 @@ static void paint_context(painter_t* p, node_t* n, uint32_t depth) paint_inner(p, n, depth); if (group) check(p, dmvsi_end_group(p->c->doc)); + end_dynamic(p, dynamic); } /* ---- The page ---- */ -static node_t* find_id(node_t* n, const char* id, uint32_t depth) +node_t* find_id(node_t* n, const char* id, uint32_t depth) { if (depth > MAX_DEPTH) return NULL; diff --git a/src/private.h b/src/private.h index 6b39477..c79a6ce 100644 --- a/src/private.h +++ b/src/private.h @@ -103,9 +103,32 @@ typedef struct bool placed; /* Its absolute position is known */ } 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 + +typedef struct +{ + node_t* node; /* The element in that state's tree; NULL: not shown then */ + dmvsi_var_t var; /* Shown while var == value (0: always) ... */ + int32_t value; + int8_t pressed; /* ... and its box is pressed (1), is not (0); -1: either */ +} variant_t; + +/* What a script does with an element (script.c): the variables its group is bound to, its click, its looks */ +typedef struct +{ + dmvsi_var_t bind[DMVSI_BIND_COUNT]; + dmvsi_handler_t click; + variant_t variants[MAX_VARIANTS]; /* None: it looks as it is */ + uint8_t variant_count; +} dynamic_t; + struct node { uint8_t kind; + uint32_t index; /* Of the elements, in document order (as parsed) */ + dynamic_t* dynamic; /* NULL: what it is it stays */ + bool active; /* Pressed: :active holds (MOD_ACTIVE) */ uint8_t pseudo; char* tag; /* Lowercase; NULL for text */ attr_t* attrs; @@ -144,6 +167,7 @@ struct node #define PC_ONLY_CHILD 0x0004u #define PC_ROOT 0x0008u #define PC_EMPTY 0x0010u +#define PC_ACTIVE 0x0020u /* :active - a pressed element (MOD_ACTIVE) */ #define PC_NEVER 0x8000u /* :hover, :active, ... - never in a still page */ typedef struct compound compound_t; @@ -275,6 +299,14 @@ struct font_face #define MAX_SHADOWS 4u #define MAX_TRACKS 12u +#define MAX_TRANSITIONS 12u + +/* What a transition animates (of what dmview can: a box's position and opacity) */ +#define TRANSITION_NONE 0u +#define TRANSITION_ALL 1u +#define TRANSITION_POSITION 2u /* top, left, right, bottom, inset, margin, transform, translate */ +#define TRANSITION_OPACITY 3u +#define TRANSITION_OTHER 4u typedef struct { @@ -362,6 +394,13 @@ struct style uint8_t text_align, text_transform, white_space, valign; int32_t valign_by; + uint8_t transition_count; /* transition-property, -duration, -timing-function */ + uint8_t transition_props[MAX_TRANSITIONS]; /* TRANSITION_* */ + uint8_t duration_count; + uint16_t transition_ms[MAX_TRANSITIONS]; + uint8_t timing_count; + int16_t transition_easing[MAX_TRANSITIONS][4]; + shadow_t shadows[MAX_SHADOWS]; /* box-shadow, the first on top */ uint8_t shadow_count; shadow_t drops[MAX_SHADOWS]; /* filter: drop-shadow() */ @@ -407,10 +446,26 @@ struct rule_ref rule_t* rule; }; +/* What a script changes of an element, for laying the page out as it is then */ +#define MOD_CLASS_ADD 0u +#define MOD_CLASS_REMOVE 1u +#define MOD_STYLE 2u +#define MOD_ACTIVE 3u /* Pressed: it and its ancestors are :active */ + +typedef struct +{ + uint32_t element; /* Its index */ + uint8_t kind; + const char* name; /* A class, a property */ + const char* value; +} mod_t; + typedef struct { arena_t arena; const char* path; /* The page */ + const mod_t* mods; /* Applied after parsing (run_layout()) */ + uint32_t mod_count; const dmvsi_options_t* options; dmvsi_doc_t doc; node_t* document; @@ -420,11 +475,13 @@ typedef struct uint32_t order; font_face_t* faces; bool tailwind; /* The page loads Tailwind CSS (its Play CDN) */ + bool has_active; /* A rule of :active (a pressed look) */ int32_t vw, vh; /* The viewport, 1/64 px */ int status; uint32_t warnings; void* style_work; /* style.c's */ const char* font_warned; /* The font file last reported as missing */ + void* states; /* script.c's: the page laid out in other states */ } conv_t; /* Resources: a URL or a path relative to `base` made absolute (resource_url), and @@ -454,7 +511,7 @@ const rule_ref_t* css_bucket(const conv_t* c, const char* prefix, const char* na /* tailwind.c */ extern const char tailwind_preflight[]; -uint32_t tailwind_class(conv_t* c, const char* name, decl_t* decls, uint32_t max, uint32_t* rank); +uint32_t tailwind_class(conv_t* c, const char* name, decl_t* decls, uint32_t max, uint32_t* rank, bool* active); /* style.c */ void style_compute(conv_t* c, node_t* root); @@ -468,6 +525,19 @@ bool image_size(const char* path, int32_t* width, int32_t* height); /* paint.c */ int paint_page(conv_t* c, node_t* root); +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); + +/* script.c */ +int script_compile(conv_t* c); +void script_free(conv_t* c); /* The states it laid out (after painting) */ + +/* style.c: the transition of a property (TRANSITION_POSITION, _OPACITY) - false when none */ +bool style_transition(const style_t* st, uint8_t what, uint16_t* ms, int16_t* easing); + +/* dmvs_html.c: a page laid out as it is, or with what a script changed (state.c) */ +int run_layout(conv_t* c); #define WARN(c, ...) do { (c)->warnings++; DMOD_LOG_WARN("dmvs_html: " __VA_ARGS__); } while (0) diff --git a/src/script.c b/src/script.c new file mode 100644 index 0000000..8f62208 --- /dev/null +++ b/src/script.c @@ -0,0 +1,1903 @@ +#include "private.h" +#include +#include + +/* + * Scripts: what a page's JavaScript does when it is used, made the view's + * variables and handlers - not by running the script on the device, but by + * working out at conversion what each handler does. + * + * What is understood is what switches screens and toggles things: + * + * const x = document.getElementById('id'); an element + * let current = null; a variable (a global the handlers assign) + * function open(id) { ... } called from onclick="open('a')": inlined, its arguments known + * el.classList.add / remove / toggle('c') a class changes + * el.style.opacity = '0.3' a style changes + * if (current) / if (!current) / if (a === b) on a variable, at run time + * el.classList.contains('c') ... on a class + * + * Every change is laid out: the page with that class (or that style), how + * the element moved and faded is what its variables become - x, y, + * opacity of the group it is painted in - set at once, or animated by the + * element's CSS transition. A change that does more (the element's inside + * laid out anew, shown or hidden) is reported and left as it is. + * + * What is not understood (timers, Date, innerText, loops, ...) is reported + * and left out: the page stays as it is there. + */ + +#define MAX_TOKEN_TEXT 1024u +#define MAX_SCOPE 64u +#define MAX_GLOBALS 64u +#define MAX_RUNTIME 16u +#define MAX_DOMAIN 32u +#define MAX_MODS 64u +#define MAX_CLASSVARS 16u +#define MAX_ACTIONS 256u +#define MAX_INLINE 8u +#define MAX_PARSE_DEPTH 64u + +/* ---- Tokens ---- */ + +#define T_END 0u +#define T_IDENT 1u +#define T_NUMBER 2u +#define T_STRING 3u +#define T_PUNCT 4u +#define T_DYNAMIC 5u /* A template with ${...}: not a constant */ + +typedef struct +{ + uint8_t kind; + const char* s; /* T_STRING: decoded (in the arena) */ + size_t n; + int32_t num; /* x 1000 */ +} token_t; + +typedef struct +{ + conv_t* c; + const char* p; + const char* end; + token_t tok; /* The current one */ + uint32_t depth; +} lexer_t; + +static bool is_space(char ch) { return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\v'; } +static bool ident_start(char ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '$'; } +static bool ident_char(char ch) { return ident_start(ch) || (ch >= '0' && ch <= '9'); } + +static void next(lexer_t* l) +{ + const char* p = l->p; + for (;;) + { + while (p < l->end && is_space(*p)) + p++; + if (p + 1 < l->end && p[0] == '/' && p[1] == '/') + { + while (p < l->end && *p != '\n') + p++; + continue; + } + if (p + 1 < l->end && p[0] == '/' && p[1] == '*') + { + p += 2; + while (p + 1 < l->end && !(p[0] == '*' && p[1] == '/')) + p++; + p = (p + 1 < l->end) ? p + 2 : l->end; + continue; + } + break; + } + token_t* t = &l->tok; + memset(t, 0, sizeof(*t)); + if (p >= l->end) + { + t->kind = T_END; + l->p = p; + return; + } + t->s = p; + if (ident_start(*p)) + { + while (p < l->end && ident_char(*p)) + p++; + t->kind = T_IDENT; + t->n = (size_t)(p - t->s); + } + else if ((*p >= '0' && *p <= '9') || (*p == '.' && p + 1 < l->end && p[1] >= '0' && p[1] <= '9')) + { + int32_t v = 0, frac = 0, scale = 1000; + for (; p < l->end && *p >= '0' && *p <= '9'; p++) + v = (v < 1000000) ? v * 10 + (*p - '0') : v; + if (p < l->end && *p == '.') + { + for (p++; p < l->end && *p >= '0' && *p <= '9'; p++) + { + if (scale > 1) + { + scale /= 10; + frac += (*p - '0') * scale; + } + } + } + while (p < l->end && ident_char(*p)) + p++; + t->kind = T_NUMBER; + t->num = v * 1000 + frac; + t->n = (size_t)(p - t->s); + } + else if (*p == '"' || *p == '\'' || *p == '`') + { + char q = *p++; + char buffer[MAX_TOKEN_TEXT]; + size_t n = 0; + bool dynamic = false; + while (p < l->end && *p != q) + { + char ch = *p++; + if (q == '`' && ch == '$' && p < l->end && *p == '{') + dynamic = true; + if (ch == '\\' && p < l->end) + { + ch = *p++; + ch = (ch == 'n') ? '\n' : (ch == 't') ? '\t' : ch; + } + if (n + 1U < sizeof(buffer)) + buffer[n++] = ch; + } + if (p < l->end) + p++; + t->kind = dynamic ? T_DYNAMIC : T_STRING; + t->s = arena_strndup(&l->c->arena, buffer, n); + t->n = n; + } + else + { + /* Punctuators: the longest first */ + static const char three[] = "|===|!==|>>>|...|**=|"; + static const char two[] = "|==|!=|<=|>=|&&|=>|||++|--|+=|-=|*=|/=|?.|??|"; + t->kind = T_PUNCT; + t->n = 1; + for (const char* q = three; *q != '\0'; q++) + { + if (*q == '|' && q[1] != '\0' && p + 3 <= l->end && strncmp(q + 1, p, 3) == 0 && q[4] == '|') + t->n = 3; + } + for (const char* q = two; *q != '\0' && t->n == 1; q++) + { + if (*q == '|' && q[1] != '\0' && p + 2 <= l->end && strncmp(q + 1, p, 2) == 0 && q[3] == '|') + t->n = 2; + } + p += t->n; + } + l->p = p; +} + +static bool is(const lexer_t* l, const char* text) +{ + size_t n = strlen(text); + return (l->tok.kind == T_PUNCT || l->tok.kind == T_IDENT) && l->tok.n == n && strncmp(l->tok.s, text, n) == 0; +} + +static bool accept(lexer_t* l, const char* text) +{ + if (!is(l, text)) + return false; + next(l); + return true; +} + +/* ---- Syntax ---- */ + +#define J_UNKNOWN 0u /* Something not understood */ +#define J_NUMBER 1u +#define J_STRING 2u +#define J_IDENT 3u +#define J_NULL 4u +#define J_TRUE 5u +#define J_FALSE 6u +#define J_THIS 7u +#define J_MEMBER 8u /* a.name */ +#define J_CALL 9u /* a(list) */ +#define J_ASSIGN 10u /* a = b */ +#define J_NOT 11u /* !a */ +#define J_EQ 12u /* a == b, a === b */ +#define J_NE 13u +#define J_AND 14u +#define J_OR 15u +#define J_UNDEFINED 16u + +#define S_EXPR 32u +#define S_VAR 33u /* name = a */ +#define S_FUNC 34u /* name(list) { b } */ +#define S_IF 35u /* if (a) b else c */ +#define S_BLOCK 36u /* { list } */ +#define S_RETURN 37u +#define S_UNKNOWN 38u +#define S_EMPTY 39u + +typedef struct js js_t; +struct js +{ + uint8_t kind; + const char* name; /* J_IDENT, J_MEMBER, S_VAR, S_FUNC; J_STRING's text */ + size_t length; + int32_t num; + js_t* a; + js_t* b; + js_t* c; + js_t* list; /* Arguments, parameters, statements */ + js_t* next; + const char* source; /* Where it starts (for reports) */ +}; + +static js_t* node(lexer_t* l, uint8_t kind) +{ + js_t* n = arena_alloc(&l->c->arena, sizeof(*n)); + if (n != NULL) + { + n->kind = kind; + n->source = l->tok.s; + } + return n; +} + +static js_t* expression(lexer_t* l); +static js_t* statement(lexer_t* l); + +/* Past a balanced (...), [...] or {...} starting at the current token */ +static void skip_balanced(lexer_t* l) +{ + int depth = 0; + do + { + if (is(l, "(") || is(l, "[") || is(l, "{")) + depth++; + else if (is(l, ")") || is(l, "]") || is(l, "}")) + depth--; + next(l); + } while (depth > 0 && l->tok.kind != T_END); +} + +static js_t* primary(lexer_t* l) +{ + js_t* n = NULL; + if (l->tok.kind == T_NUMBER) + { + n = node(l, J_NUMBER); + if (n != NULL) + n->num = l->tok.num; + next(l); + return n; + } + if (l->tok.kind == T_STRING) + { + n = node(l, J_STRING); + if (n != NULL) + { + n->name = l->tok.s; + n->length = l->tok.n; + } + next(l); + return n; + } + if (l->tok.kind == T_IDENT) + { + static const struct { char word[10]; uint8_t kind; } words[] = { + { "null", J_NULL }, { "true", J_TRUE }, { "false", J_FALSE }, { "this", J_THIS }, { "undefined", J_UNDEFINED }, + }; + for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) + { + if (is(l, words[i].word)) + { + n = node(l, words[i].kind); + next(l); + return n; + } + } + if (is(l, "function") || is(l, "new") || is(l, "class") || is(l, "async") || is(l, "await")) + { + /* A function expression, an object made: not understood - past it */ + n = node(l, J_UNKNOWN); + next(l); + while (l->tok.kind == T_IDENT) + next(l); + if (is(l, "(")) + skip_balanced(l); + if (is(l, "{")) + skip_balanced(l); + return n; + } + n = node(l, J_IDENT); + if (n != NULL) + { + n->name = l->tok.s; + n->length = l->tok.n; + } + next(l); + if (is(l, "=>")) + { + /* x => ...: an arrow function, not understood */ + next(l); + if (is(l, "{")) + skip_balanced(l); + else + (void)expression(l); + if (n != NULL) + n->kind = J_UNKNOWN; + } + return n; + } + if (is(l, "(")) + { + /* (a) - or (a, b) => ...: an arrow function */ + const char* p = l->p; + token_t t = l->tok; + skip_balanced(l); + if (is(l, "=>")) + { + next(l); + if (is(l, "{")) + skip_balanced(l); + else + (void)expression(l); + return node(l, J_UNKNOWN); + } + l->p = p; + l->tok = t; + next(l); + n = expression(l); + (void)accept(l, ")"); + return n; + } + if (is(l, "[") || is(l, "{")) + { + n = node(l, J_UNKNOWN); + skip_balanced(l); + return n; + } + if (l->tok.kind == T_DYNAMIC) + { + n = node(l, J_UNKNOWN); + next(l); + return n; + } + n = node(l, J_UNKNOWN); + if (l->tok.kind != T_END) + next(l); + return n; +} + +static js_t* postfix(lexer_t* l) +{ + js_t* n = primary(l); + for (;;) + { + if (is(l, ".") || is(l, "?.")) + { + next(l); + js_t* m = node(l, J_MEMBER); + if (m == NULL) + return n; + m->a = n; + if (l->tok.kind == T_IDENT) + { + m->name = l->tok.s; + m->length = l->tok.n; + next(l); + } + n = m; + } + else if (is(l, "(")) + { + next(l); + js_t* call = node(l, J_CALL); + if (call == NULL) + return n; + call->a = n; + js_t** tail = &call->list; + while (!is(l, ")") && l->tok.kind != T_END) + { + js_t* arg = expression(l); + if (arg != NULL) + { + *tail = arg; + tail = &arg->next; + } + if (!accept(l, ",")) + break; + } + (void)accept(l, ")"); + n = call; + } + else if (is(l, "[")) + { + js_t* u = node(l, J_UNKNOWN); + skip_balanced(l); + n = u; + } + else if (is(l, "++") || is(l, "--")) + { + next(l); + n = node(l, J_UNKNOWN); + } + else + return n; + } +} + +static js_t* unary(lexer_t* l) +{ + if (accept(l, "!")) + { + js_t* n = node(l, J_NOT); + if (n != NULL) + n->a = unary(l); + return n; + } + if (is(l, "-") || is(l, "+") || is(l, "typeof") || is(l, "void") || is(l, "delete") || is(l, "++") || is(l, "--") || + is(l, "~")) + { + next(l); + (void)unary(l); + return node(l, J_UNKNOWN); + } + return postfix(l); +} + +/* The binary operators: those understood make a node, the others an unknown one */ +static js_t* binary(lexer_t* l, int level) +{ + static const char ops[][28] = { + "|??|||", "|&&|", "|==|===|!=|!==|", "|<|>|<=|>=|instanceof|in|", "|+|-|", "|*|/|%|**|", + }; + if (level >= (int)(sizeof(ops) / sizeof(ops[0]))) + return unary(l); + js_t* left = binary(l, level + 1); + for (;;) + { + bool found = false; + size_t n = l->tok.n; + if (l->tok.kind == T_PUNCT || l->tok.kind == T_IDENT) + { + for (const char* q = ops[level]; *q != '\0' && !found; q++) + found = *q == '|' && strncmp(q + 1, l->tok.s, n) == 0 && q[1 + n] == '|'; + } + if (!found) + return left; + uint8_t kind = J_UNKNOWN; + if (is(l, "==") || is(l, "===")) + kind = J_EQ; + else if (is(l, "!=") || is(l, "!==")) + kind = J_NE; + else if (is(l, "&&")) + kind = J_AND; + else if (is(l, "||")) + kind = J_OR; + next(l); + js_t* right = binary(l, level + 1); + js_t* b = node(l, kind); + if (b == NULL) + return left; + b->a = left; + b->b = right; + left = b; + } +} + +static js_t* expression(lexer_t* l) +{ + if (++l->depth > MAX_PARSE_DEPTH) + { + l->depth--; + skip_balanced(l); + return node(l, J_UNKNOWN); + } + js_t* left = binary(l, 0); + if (accept(l, "?")) + { + /* a ? b : c - not understood */ + (void)expression(l); + (void)accept(l, ":"); + (void)expression(l); + left = node(l, J_UNKNOWN); + } + else if (is(l, "=") || is(l, "+=") || is(l, "-=") || is(l, "*=") || is(l, "/=") || is(l, "**=")) + { + bool plain = is(l, "="); + next(l); + js_t* a = node(l, plain ? J_ASSIGN : J_UNKNOWN); + if (a != NULL) + { + a->a = left; + a->b = expression(l); + } + left = a; + } + while (accept(l, ",")) + (void)expression(l); /* a, b: only the first is kept */ + l->depth--; + return left; +} + +/* Past a statement not understood: up to its ';', or its block */ +static void skip_statement(lexer_t* l) +{ + while (l->tok.kind != T_END && !is(l, ";") && !is(l, "}")) + { + if (is(l, "{")) + { + skip_balanced(l); + if (!is(l, "else") && !is(l, "catch") && !is(l, "finally") && !is(l, "while")) + return; + continue; + } + if (is(l, "(") || is(l, "[")) + { + skip_balanced(l); + continue; + } + next(l); + } + (void)accept(l, ";"); +} + +static js_t* block(lexer_t* l) +{ + js_t* b = node(l, S_BLOCK); + if (!accept(l, "{") || b == NULL) + return b; + js_t** tail = &b->list; + while (!is(l, "}") && l->tok.kind != T_END) + { + js_t* s = statement(l); + if (s != NULL) + { + *tail = s; + tail = &s->next; + } + } + (void)accept(l, "}"); + return b; +} + +static js_t* statement(lexer_t* l) +{ + if (++l->depth > MAX_PARSE_DEPTH) + { + l->depth--; + skip_statement(l); + return node(l, S_UNKNOWN); + } + js_t* s = NULL; + if (is(l, "{")) + s = block(l); + else if (accept(l, ";")) + s = node(l, S_EMPTY); + else if (is(l, "function")) + { + s = node(l, S_FUNC); + next(l); + if (s != NULL && l->tok.kind == T_IDENT) + { + s->name = l->tok.s; + s->length = l->tok.n; + next(l); + } + if (accept(l, "(")) + { + js_t** tail = (s != NULL) ? &s->list : NULL; + while (!is(l, ")") && l->tok.kind != T_END) + { + if (l->tok.kind == T_IDENT && tail != NULL) + { + js_t* p = node(l, J_IDENT); + if (p != NULL) + { + p->name = l->tok.s; + p->length = l->tok.n; + *tail = p; + tail = &p->next; + } + } + next(l); + if (is(l, "=")) + { + next(l); + (void)expression(l); /* A default: not understood, the argument is passed */ + } + (void)accept(l, ","); + } + (void)accept(l, ")"); + } + if (s != NULL) + s->b = block(l); + else + skip_balanced(l); + } + else if (is(l, "const") || is(l, "let") || is(l, "var")) + { + next(l); + js_t** tail = &s; + do + { + js_t* v = node(l, S_VAR); + if (l->tok.kind == T_IDENT && v != NULL) + { + v->name = l->tok.s; + v->length = l->tok.n; + next(l); + if (accept(l, "=")) + v->a = expression(l); + *tail = v; + tail = &v->next; + } + else + { + /* const { a, b } = ...: not understood */ + skip_statement(l); + l->depth--; + return node(l, S_UNKNOWN); + } + } while (accept(l, ",")); + (void)accept(l, ";"); + /* Several declarators: in a block of their own */ + if (s != NULL && s->next != NULL) + { + js_t* b = node(l, S_BLOCK); + if (b != NULL) + { + b->list = s; + s = b; + } + } + } + else if (is(l, "if")) + { + s = node(l, S_IF); + next(l); + (void)accept(l, "("); + js_t* cond = expression(l); + (void)accept(l, ")"); + js_t* then = statement(l); + js_t* other = accept(l, "else") ? statement(l) : NULL; + if (s != NULL) + { + s->a = cond; + s->b = then; + s->c = other; + } + } + else if (is(l, "return")) + { + s = node(l, S_RETURN); + next(l); + if (!is(l, ";") && !is(l, "}") && s != NULL) + s->a = expression(l); + (void)accept(l, ";"); + } + else if (l->tok.kind == T_IDENT && + (is(l, "for") || is(l, "while") || is(l, "do") || is(l, "switch") || is(l, "try") || is(l, "class") || + is(l, "throw") || is(l, "import") || is(l, "export") || is(l, "async") || is(l, "break") || is(l, "continue"))) + { + s = node(l, S_UNKNOWN); + skip_statement(l); + } + else + { + s = node(l, S_EXPR); + js_t* e = expression(l); + if (s != NULL) + s->a = e; + (void)accept(l, ";"); + } + l->depth--; + return s; +} + +static js_t* parse(conv_t* c, const char* text, size_t length) +{ + lexer_t l = { c, text, text + length, { 0 }, 0 }; + js_t* first = NULL; + js_t** tail = &first; + next(&l); + while (l.tok.kind != T_END && !c->arena.failed) + { + const char* before = l.p; + js_t* s = statement(&l); + if (s != NULL) + { + *tail = s; + while (*tail != NULL) + tail = &(*tail)->next; + } + if (l.p == before && l.tok.kind != T_END) + next(&l); /* Not a step further: past it */ + } + return first; +} + +/* ---- Values ---- */ + +#define V_UNKNOWN 0u +#define V_UNDEFINED 1u +#define V_NULL 2u +#define V_BOOL 3u +#define V_NUMBER 4u +#define V_STRING 5u +#define V_ELEMENT 6u +#define V_RUNTIME 7u /* A variable holding an element (or null) at run time */ +#define V_DOCUMENT 8u +#define V_FUNCTION 9u +#define V_CLASSES 10u /* x.classList */ +#define V_STYLE 11u /* x.style */ + +typedef struct +{ + uint8_t kind; + node_t* element; /* V_ELEMENT; V_CLASSES, V_STYLE of one */ + uint32_t runtime; /* V_RUNTIME (1 ...); V_CLASSES, V_STYLE of one */ + const char* text; + int32_t num; + const js_t* function; +} val_t; + +typedef struct +{ + const char* name; + size_t length; + val_t value; +} binding_t; + +/* A global variable a handler assigns: the view's variable of the element it holds */ +typedef struct +{ + const char* name; + size_t length; + dmvsi_var_t var; + int32_t initial; /* The element's index, 0: null */ + node_t* domain[MAX_DOMAIN]; /* What it may hold */ + uint32_t domain_count; +} runtime_t; + +/* A change a script makes, and what it does to the element (when laid out) */ +typedef struct +{ + mod_t mod; + node_t* element; + bool laid_out; + bool same; /* The page is as it is: nothing changes */ + bool changed; /* It does more than move and fade the element */ + int32_t dx, dy; /* How the element moves */ + int32_t opacity; + uint16_t move_ms, fade_ms; + int16_t move_easing[4], fade_easing[4]; + node_t* look; /* changed: the element as it looks then (NULL: not shown) */ +} change_t; + +/* A class of an element a script asks about (contains, toggle): its variable */ +typedef struct +{ + node_t* element; + const char* name; + dmvsi_var_t var; +} classvar_t; + +#define PASS_DOMAINS 0u /* What the variables may hold */ +#define PASS_CHANGES 1u /* Which changes there are */ +#define PASS_EMIT 2u /* The actions */ + +typedef struct +{ + conv_t* c; + uint32_t pass; + binding_t globals[MAX_GLOBALS]; + uint32_t global_count; + binding_t scope[MAX_SCOPE]; + uint32_t scope_count; + runtime_t runtime[MAX_RUNTIME]; + uint32_t runtime_count; + change_t changes[MAX_MODS]; + uint32_t change_count; + classvar_t classvars[MAX_CLASSVARS]; + uint32_t classvar_count; + dmvsi_action_t actions[MAX_ACTIONS]; + uint32_t action_count; + node_t* self; /* `this` of a handler */ + node_t* clickables[MAX_MODS]; + uint32_t clickable_count; + uint32_t inline_depth; + bool returned; /* A return reached: the rest of the function is not run */ + uint32_t conditional; /* Inside an IF of run time */ +} script_t; + +static bool same_name(const char* a, size_t an, const char* b, size_t bn) +{ + return an == bn && strncmp(a, b, an) == 0; +} + +static bool is_name(const js_t* n, const char* name) +{ + return n != NULL && (n->kind == J_IDENT || n->kind == J_MEMBER) && same_name(n->name, n->length, name, strlen(name)); +} + +/* A report of what a script does that is not converted - once, in the last pass */ +static void report(script_t* sc, const js_t* where, const char* what) +{ + if (sc->pass != PASS_EMIT) + return; + char excerpt[48]; + size_t n = 0; + for (const char* s = (where != NULL && where->source != NULL) ? where->source : ""; *s != '\0' && *s != '\n' && n + 1U < sizeof(excerpt); s++) + excerpt[n++] = *s; + excerpt[n] = '\0'; + WARN(sc->c, "script: %s - not converted: %s\n", what, excerpt); +} + +static val_t make(uint8_t kind) +{ + val_t v; + memset(&v, 0, sizeof(v)); + v.kind = kind; + return v; +} + +static val_t* lookup(script_t* sc, const char* name, size_t n) +{ + for (uint32_t i = sc->scope_count; i > 0; i--) + { + if (same_name(sc->scope[i - 1U].name, sc->scope[i - 1U].length, name, n)) + return &sc->scope[i - 1U].value; + } + for (uint32_t i = 0; i < sc->global_count; i++) + { + if (same_name(sc->globals[i].name, sc->globals[i].length, name, n)) + return &sc->globals[i].value; + } + return NULL; +} + +/* ---- Changes ---- */ + +static change_t* change(script_t* sc, node_t* e, uint8_t kind, const char* name, const char* value) +{ + for (uint32_t i = 0; i < sc->change_count; i++) + { + change_t* ch = &sc->changes[i]; + if (ch->element == e && ch->mod.kind == kind && strcmp(ch->mod.name, name) == 0 && + ((value == NULL && ch->mod.value == NULL) || (value != NULL && ch->mod.value != NULL && strcmp(ch->mod.value, value) == 0))) + return ch; + } + if (sc->pass != PASS_CHANGES || sc->change_count >= MAX_MODS) + return NULL; + change_t* ch = &sc->changes[sc->change_count++]; + memset(ch, 0, sizeof(*ch)); + ch->element = e; + ch->mod.element = e->index; + ch->mod.kind = kind; + ch->mod.name = name; + ch->mod.value = value; + return ch; +} + +static classvar_t* classvar(script_t* sc, node_t* e, const char* name, bool make_it) +{ + for (uint32_t i = 0; i < sc->classvar_count; i++) + { + if (sc->classvars[i].element == e && strcmp(sc->classvars[i].name, name) == 0) + return &sc->classvars[i]; + } + if (!make_it || sc->pass != PASS_CHANGES || sc->classvar_count >= MAX_CLASSVARS) + return NULL; + classvar_t* cv = &sc->classvars[sc->classvar_count++]; + cv->element = e; + cv->name = name; + cv->var = 0; + return cv; +} + +static void emit(script_t* sc, uint8_t kind, dmvsi_var_t var, int32_t value) +{ + if (sc->pass != PASS_EMIT || var == 0 || sc->action_count >= MAX_ACTIONS) + return; + dmvsi_action_t* a = &sc->actions[sc->action_count++]; + memset(a, 0, sizeof(*a)); + a->kind = kind; + a->var = var; + a->value = value; +} + +static void emit_end(script_t* sc) +{ + if (sc->pass != PASS_EMIT || sc->action_count >= MAX_ACTIONS) + return; + memset(&sc->actions[sc->action_count], 0, sizeof(dmvsi_action_t)); + sc->actions[sc->action_count++].kind = DMVSI_ACT_END; +} + +/* A variable set, or animated (its transition) */ +static void emit_to(script_t* sc, dmvsi_var_t var, int32_t value, uint16_t ms, const int16_t* easing) +{ + emit(sc, (ms > 0) ? DMVSI_ACT_ANIMATE : DMVSI_ACT_SET, var, value); + if (ms > 0 && sc->pass == PASS_EMIT && sc->action_count > 0 && sc->actions[sc->action_count - 1U].var == var) + { + sc->actions[sc->action_count - 1U].duration = ms; + memcpy(sc->actions[sc->action_count - 1U].easing, easing, 4 * sizeof(int16_t)); + } +} + +/* The actions of a change of one element: its variables to where the change puts it */ +static void apply(script_t* sc, node_t* e, uint8_t kind, const char* name, const char* value) +{ + change_t* ch = change(sc, e, kind, name, value); + if (sc->pass != PASS_EMIT || ch == NULL) + return; + if (kind != MOD_STYLE) + { + classvar_t* cv = classvar(sc, e, name, false); + if (cv != NULL) + emit(sc, DMVSI_ACT_SET, cv->var, (kind == MOD_CLASS_ADD) ? 1 : 0); + } + if (e->dynamic == NULL || ch->changed) + return; + int32_t ox, oy; + view_origin(sc->c, &ox, &oy); + dmvsi_rect_t r = group_rect(sc->c, e, ox, oy); + if (e->dynamic->bind[DMVSI_BIND_X] != 0) + emit_to(sc, e->dynamic->bind[DMVSI_BIND_X], r.x + ch->dx, ch->move_ms, ch->move_easing); + if (e->dynamic->bind[DMVSI_BIND_Y] != 0) + emit_to(sc, e->dynamic->bind[DMVSI_BIND_Y], r.y + ch->dy, ch->move_ms, ch->move_easing); + if (e->dynamic->bind[DMVSI_BIND_OPACITY] != 0) + emit_to(sc, e->dynamic->bind[DMVSI_BIND_OPACITY], ch->opacity, ch->fade_ms, ch->fade_easing); +} + +/* A change of what a value is: one element, or each a variable may hold (at run time) */ +static void apply_to(script_t* sc, const val_t* target, uint8_t kind, const char* name, const char* value) +{ + if (target->element != NULL) + { + apply(sc, target->element, kind, name, value); + return; + } + runtime_t* rt = &sc->runtime[target->runtime - 1U]; + for (uint32_t i = 0; i < rt->domain_count; i++) + { + emit(sc, DMVSI_ACT_IF_EQ, rt->var, (int32_t)rt->domain[i]->index); + apply(sc, rt->domain[i], kind, name, value); + emit_end(sc); + } +} + +/* ---- Running a script, abstractly ---- */ + +static val_t eval(script_t* sc, const js_t* n); +static void run(script_t* sc, const js_t* s); + +static const char* text_of(script_t* sc, const val_t* v) +{ + char buffer[24]; + if (v->kind == V_STRING) + return v->text; + if (v->kind == V_NUMBER) + { + int32_t whole = v->num / 1000, frac = (v->num < 0 ? -v->num : v->num) % 1000; + if (frac == 0) + Dmod_SnPrintf(buffer, sizeof(buffer), "%d", (int)whole); + else + Dmod_SnPrintf(buffer, sizeof(buffer), "%d.%03d", (int)whole, (int)frac); + return arena_strndup(&sc->c->arena, buffer, strlen(buffer)); + } + return NULL; +} + +/* f(args): a function of the page inlined, with its arguments' values */ +static val_t call_function(script_t* sc, const js_t* f, const js_t* args) +{ + if (sc->inline_depth >= MAX_INLINE) + return make(V_UNKNOWN); + uint32_t saved_scope = sc->scope_count; + bool saved_returned = sc->returned; + const js_t* a = args; + for (const js_t* p = f->list; p != NULL; p = p->next) + { + val_t v = (a != NULL) ? eval(sc, a) : make(V_UNDEFINED); + if (sc->scope_count < MAX_SCOPE) + { + sc->scope[sc->scope_count].name = p->name; + sc->scope[sc->scope_count].length = p->length; + sc->scope[sc->scope_count].value = v; + sc->scope_count++; + } + a = (a != NULL) ? a->next : NULL; + } + sc->inline_depth++; + sc->returned = false; + uint32_t saved_conditional = sc->conditional; + sc->conditional = 0; + if (f->b != NULL) + run(sc, f->b); + sc->conditional = saved_conditional; + sc->inline_depth--; + sc->returned = saved_returned; + sc->scope_count = saved_scope; + return make(V_UNDEFINED); +} + +/* x.classList.add('a'), document.getElementById('a'), f(...) */ +static val_t call(script_t* sc, const js_t* n) +{ + const js_t* f = n->a; + const js_t* arg = n->list; + if (f != NULL && f->kind == J_MEMBER) + { + val_t object = eval(sc, f->a); + if (object.kind == V_DOCUMENT && (is_name(f, "getElementById") || is_name(f, "querySelector"))) + { + val_t id = (arg != NULL) ? eval(sc, arg) : make(V_UNKNOWN); + if (id.kind != V_STRING) + return make(V_UNKNOWN); + const char* name = id.text; + if (is_name(f, "querySelector")) + { + if (name[0] != '#') + return make(V_UNKNOWN); + name++; + } + node_t* e = find_id(sc->c->document, name, 0); + val_t v = make(e != NULL ? V_ELEMENT : V_NULL); + v.element = e; + return v; + } + if (object.kind == V_CLASSES && (is_name(f, "add") || is_name(f, "remove") || is_name(f, "toggle"))) + { + for (; arg != NULL; arg = arg->next) + { + val_t cls = eval(sc, arg); + if (cls.kind != V_STRING) + { + report(sc, n, "a class not known at conversion"); + continue; + } + if (!is_name(f, "toggle")) + { + apply_to(sc, &object, is_name(f, "add") ? MOD_CLASS_ADD : MOD_CLASS_REMOVE, cls.text, NULL); + continue; + } + /* toggle: its class's variable flipped, then what is is applied */ + if (object.element == NULL) + { + report(sc, n, "classList.toggle() of a variable"); + continue; + } + classvar_t* cv = classvar(sc, object.element, cls.text, true); + (void)change(sc, object.element, MOD_CLASS_ADD, cls.text, NULL); + (void)change(sc, object.element, MOD_CLASS_REMOVE, cls.text, NULL); + if (sc->pass == PASS_EMIT && cv != NULL) + { + emit(sc, DMVSI_ACT_TOGGLE, cv->var, 0); + emit(sc, DMVSI_ACT_IF_NE, cv->var, 0); + apply(sc, object.element, MOD_CLASS_ADD, cls.text, NULL); + emit_end(sc); + emit(sc, DMVSI_ACT_IF_EQ, cv->var, 0); + apply(sc, object.element, MOD_CLASS_REMOVE, cls.text, NULL); + emit_end(sc); + } + } + return make(V_UNDEFINED); + } + if (object.kind == V_CLASSES && is_name(f, "contains")) + return make(V_UNKNOWN); /* As a condition: see condition() */ + } + if (f != NULL && f->kind == J_IDENT) + { + val_t* v = lookup(sc, f->name, f->length); + if (v != NULL && v->kind == V_FUNCTION) + return call_function(sc, v->function, arg); + } + report(sc, n, "a call"); + return make(V_UNKNOWN); +} + +static val_t eval(script_t* sc, const js_t* n) +{ + if (n == NULL) + return make(V_UNDEFINED); + switch (n->kind) + { + case J_NUMBER: + { + val_t v = make(V_NUMBER); + v.num = n->num; + return v; + } + case J_STRING: + { + val_t v = make(V_STRING); + v.text = n->name; + return v; + } + case J_NULL: + return make(V_NULL); + case J_UNDEFINED: + return make(V_UNDEFINED); + case J_TRUE: + case J_FALSE: + { + val_t v = make(V_BOOL); + v.num = n->kind == J_TRUE; + return v; + } + case J_THIS: + { + val_t v = make((sc->self != NULL) ? V_ELEMENT : V_UNKNOWN); + v.element = sc->self; + return v; + } + case J_IDENT: + { + if (is_name(n, "document")) + return make(V_DOCUMENT); + val_t* v = lookup(sc, n->name, n->length); + return (v != NULL) ? *v : make(V_UNKNOWN); + } + case J_MEMBER: + { + val_t object = eval(sc, n->a); + if ((object.kind == V_ELEMENT || object.kind == V_RUNTIME) && (is_name(n, "classList") || is_name(n, "style"))) + { + object.kind = is_name(n, "style") ? V_STYLE : V_CLASSES; + return object; + } + return make(V_UNKNOWN); + } + case J_CALL: + return call(sc, n); + case J_ASSIGN: + { + const js_t* target = n->a; + val_t value = eval(sc, n->b); + if (target != NULL && target->kind == J_MEMBER) + { + val_t object = eval(sc, target->a); + if (object.kind == V_STYLE) + { + const char* text = text_of(sc, &value); + if (text == NULL) + { + report(sc, n, "a style not known at conversion"); + return make(V_UNKNOWN); + } + char* name = arena_strndup(&sc->c->arena, target->name, target->length); + if (name != NULL) + apply_to(sc, &object, MOD_STYLE, name, text); + return value; + } + report(sc, n, "an assignment"); + return make(V_UNKNOWN); + } + if (target != NULL && target->kind == J_IDENT) + { + val_t* v = lookup(sc, target->name, target->length); + if (v != NULL && v->kind == V_RUNTIME) + { + runtime_t* rt = &sc->runtime[v->runtime - 1U]; + if (value.kind == V_ELEMENT && value.element != NULL) + { + bool known = false; + for (uint32_t i = 0; i < rt->domain_count && !known; i++) + known = rt->domain[i] == value.element; + if (!known && rt->domain_count < MAX_DOMAIN) + rt->domain[rt->domain_count++] = value.element; + emit(sc, DMVSI_ACT_SET, rt->var, (int32_t)value.element->index); + } + else if (value.kind == V_NULL || value.kind == V_UNDEFINED) + emit(sc, DMVSI_ACT_SET, rt->var, 0); + else + report(sc, n, "a variable set to what is not an element"); + return value; + } + if (v != NULL && sc->conditional == 0) + { + *v = value; /* A local, at conversion */ + return value; + } + } + report(sc, n, "an assignment"); + return make(V_UNKNOWN); + } + default: + return make(V_UNKNOWN); + } +} + +/* A condition: known at conversion (*known, *truth), or a test of a variable at run time */ +typedef struct +{ + bool known; + bool truth; + dmvsi_var_t var; /* Run time: var == value (equal) or != */ + int32_t value; + bool equal; +} cond_t; + +static cond_t condition(script_t* sc, const js_t* n) +{ + cond_t c; + memset(&c, 0, sizeof(c)); + if (n == NULL) + return c; + if (n->kind == J_NOT) + { + c = condition(sc, n->a); + c.truth = !c.truth; + c.equal = !c.equal; + return c; + } + if (n->kind == J_CALL && n->a != NULL && n->a->kind == J_MEMBER && is_name(n->a, "contains")) + { + val_t object = eval(sc, n->a->a); + val_t cls = (n->list != NULL) ? eval(sc, n->list) : make(V_UNKNOWN); + if (object.kind == V_CLASSES && object.element != NULL && cls.kind == V_STRING) + { + classvar_t* cv = classvar(sc, object.element, cls.text, true); + if (cv != NULL && (cv->var != 0 || sc->pass != PASS_EMIT)) + { + c.var = cv->var; + c.value = 0; + c.equal = false; + return c; + } + } + report(sc, n, "a condition"); + c.known = true; + return c; + } + if (n->kind == J_EQ || n->kind == J_NE) + { + val_t a = eval(sc, n->a), b = eval(sc, n->b); + if (a.kind != V_RUNTIME) + { + val_t t = a; + a = b; + b = t; + } + if (a.kind == V_RUNTIME && (b.kind == V_ELEMENT || b.kind == V_NULL || b.kind == V_UNDEFINED)) + { + c.var = sc->runtime[a.runtime - 1U].var; + c.value = (b.kind == V_ELEMENT && b.element != NULL) ? (int32_t)b.element->index : 0; + c.equal = n->kind == J_EQ; + return c; + } + if (a.kind == V_STRING && b.kind == V_STRING) + { + c.known = true; + c.truth = (strcmp(a.text, b.text) == 0) == (n->kind == J_EQ); + return c; + } + report(sc, n, "a condition"); + c.known = true; + return c; + } + val_t v = eval(sc, n); + switch (v.kind) + { + case V_RUNTIME: + c.var = sc->runtime[v.runtime - 1U].var; + c.value = 0; + c.equal = false; + return c; + case V_ELEMENT: + case V_FUNCTION: + case V_DOCUMENT: + case V_CLASSES: + case V_STYLE: + c.known = true; + c.truth = true; + return c; + case V_NULL: + case V_UNDEFINED: + c.known = true; + return c; + case V_BOOL: + case V_NUMBER: + c.known = true; + c.truth = v.num != 0; + return c; + case V_STRING: + c.known = true; + c.truth = v.text[0] != '\0'; + return c; + default: + report(sc, n, "a condition"); + c.known = true; /* As false: what it guards is left out */ + return c; + } +} + +static void run_list(script_t* sc, const js_t* s) +{ + for (; s != NULL && !sc->returned; s = s->next) + run(sc, s); +} + +static void run(script_t* sc, const js_t* s) +{ + if (s == NULL || sc->c->arena.failed) + return; + switch (s->kind) + { + case S_BLOCK: + { + uint32_t saved = sc->scope_count; + run_list(sc, s->list); + sc->scope_count = saved; + return; + } + case S_EXPR: + (void)eval(sc, s->a); + return; + case S_VAR: + if (sc->scope_count < MAX_SCOPE) + { + sc->scope[sc->scope_count].name = s->name; + sc->scope[sc->scope_count].length = s->length; + sc->scope[sc->scope_count].value = eval(sc, s->a); + sc->scope_count++; + } + return; + case S_IF: + { + cond_t c = condition(sc, s->a); + if (c.known) + { + if (c.truth) + run(sc, s->b); + else if (s->c != NULL) + run(sc, s->c); + return; + } + sc->conditional++; + emit(sc, c.equal ? DMVSI_ACT_IF_EQ : DMVSI_ACT_IF_NE, c.var, c.value); + run(sc, s->b); + emit_end(sc); + if (s->c != NULL) + { + emit(sc, c.equal ? DMVSI_ACT_IF_NE : DMVSI_ACT_IF_EQ, c.var, c.value); + run(sc, s->c); + emit_end(sc); + } + sc->conditional--; + sc->returned = false; + return; + } + case S_RETURN: + if (sc->conditional > 0) + report(sc, s, "a return in a condition of run time"); + else + sc->returned = true; + return; + case S_FUNC: + case S_EMPTY: + return; + default: + report(sc, s, "a statement"); + return; + } +} + +/* ---- The page's scripts ---- */ + +typedef struct +{ + js_t* programs[16]; + uint32_t count; +} scripts_t; + +static void find_scripts(conv_t* c, node_t* n, scripts_t* out, uint32_t depth) +{ + if (depth > 200U) + return; + for (node_t* k = n->first; k != NULL; k = k->next) + { + if (k->kind != NODE_ELEMENT) + continue; + if (node_is(k, "script")) + { + const char* type = node_attr(k, "type"); + bool js = type == NULL || type[0] == '\0' || strcmp(type, "module") == 0 || strcmp(type, "text/javascript") == 0; + if (js && node_attr(k, "src") == NULL && k->first != NULL && k->first->kind == NODE_TEXT && out->count < 16U) + out->programs[out->count++] = parse(c, k->first->text, k->first->length); + continue; + } + find_scripts(c, k, out, depth + 1U); + } +} + +/* The globals: functions, and the variables of the top level (what a handler assigns runs at run time) */ +static bool assigns(const js_t* s, const char* name, size_t n) +{ + for (; s != NULL; s = s->next) + { + if (s->kind == J_ASSIGN && s->a != NULL && s->a->kind == J_IDENT && same_name(s->a->name, s->a->length, name, n)) + return true; + if (assigns(s->a, name, n) || assigns(s->b, name, n) || assigns(s->c, name, n) || assigns(s->list, name, n)) + return true; + } + return false; +} + +static void declare_globals(script_t* sc, const scripts_t* scripts) +{ + for (uint32_t p = 0; p < scripts->count; p++) + { + for (const js_t* s = scripts->programs[p]; s != NULL; s = s->next) + { + const js_t* list = (s->kind == S_BLOCK) ? s->list : s; + for (const js_t* d = list; d != NULL; d = (s->kind == S_BLOCK) ? d->next : NULL) + { + if (sc->global_count >= MAX_GLOBALS || (d->kind != S_FUNC && d->kind != S_VAR)) + { + if (s->kind != S_BLOCK) + break; + continue; + } + binding_t* g = &sc->globals[sc->global_count++]; + g->name = d->name; + g->length = d->length; + if (d->kind == S_FUNC) + { + g->value = make(V_FUNCTION); + g->value.function = d; + } + else + { + g->value = eval(sc, d->a); + bool assigned = false; + for (uint32_t q = 0; q < scripts->count && !assigned; q++) + { + for (const js_t* f = scripts->programs[q]; f != NULL && !assigned; f = f->next) + assigned = f->kind == S_FUNC && assigns(f->b, d->name, d->length); + } + if (assigned && sc->runtime_count < MAX_RUNTIME) + { + runtime_t* rt = &sc->runtime[sc->runtime_count++]; + memset(rt, 0, sizeof(*rt)); + rt->name = d->name; + rt->length = d->length; + if (g->value.kind == V_ELEMENT && g->value.element != NULL) + { + rt->initial = (int32_t)g->value.element->index; + rt->domain[rt->domain_count++] = g->value.element; + } + g->value = make(V_RUNTIME); + g->value.runtime = sc->runtime_count; + } + } + if (s->kind != S_BLOCK) + break; + } + } + } +} + +/* ---- Handlers ---- */ + +static void handlers(script_t* sc, node_t* n, uint32_t depth) +{ + if (depth > 200U) + return; + for (node_t* k = n->first; k != NULL; k = k->next) + { + if (k->kind != NODE_ELEMENT || k->pseudo != PSEUDO_NONE) + continue; + const char* onclick = node_attr(k, "onclick"); + if (onclick != NULL && k->style != NULL && k->style->display != DISPLAY_NONE) + { + js_t* program = parse(sc->c, onclick, strlen(onclick)); + if (sc->pass == PASS_CHANGES && sc->clickable_count < MAX_MODS) + sc->clickables[sc->clickable_count++] = k; + sc->self = k; + sc->action_count = 0; + sc->scope_count = 0; + sc->returned = false; + sc->conditional = 0; + run_list(sc, program); + if (sc->pass == PASS_EMIT && sc->action_count > 0) + { + dmvsi_handler_t h = dmvsi_add_handler(sc->c->doc, sc->actions, sc->action_count); + if (h != 0) + { + if (k->dynamic == NULL) + k->dynamic = arena_alloc(&sc->c->arena, sizeof(dynamic_t)); + if (k->dynamic != NULL) + k->dynamic->click = h; + } + } + } + handlers(sc, k, depth + 1U); + } +} + +/* ---- States: the page laid out with a change ---- */ + +/* A digest of an element's inside: its boxes and lines relative to it */ +static uint32_t inside(const node_t* e, const node_t* n, uint32_t h, uint32_t depth) +{ + if (depth > 200U) + return h; + for (const node_t* k = n->first; k != NULL; k = k->next) + { + if (k->kind != NODE_ELEMENT || k->style == NULL || k->style->display == DISPLAY_NONE) + continue; + /* Inline elements have no position of their own (their lines are their block's) */ + int32_t v[6] = { k->box.placed ? k->box.ax - e->box.ax : 0, k->box.placed ? k->box.ay - e->box.ay : 0, + k->box.w, k->box.h, (int32_t)k->style->color, (int32_t)k->style->background }; + 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; + h = inside(e, k, h, depth + 1U); + } + return h; +} + +static node_t* element_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 = element_index(k, index, depth + 1U); + if (found != NULL) + return found; + } + return NULL; +} + +static bool has_class(const node_t* n, const char* name) +{ + for (uint32_t i = 0; i < n->class_count; i++) + { + if (strcmp(n->classes[i], name) == 0) + return true; + } + return false; +} + +/* The states laid out, kept for painting their looks (c->states) */ +#define MAX_STATES 48u + +typedef struct +{ + conv_t* convs[MAX_STATES]; + mod_t mods[MAX_STATES][2]; + uint32_t count; +} states_t; + +void script_free(conv_t* c) +{ + states_t* st = c->states; + if (st == NULL) + return; + for (uint32_t i = 0; i < st->count; i++) + { + arena_free(&st->convs[i]->arena); + Dmod_Free(st->convs[i]); + } + Dmod_Free(st); + c->states = NULL; +} + +/* The page laid out with up to two changes, kept until it is painted */ +static conv_t* run_state(script_t* sc, const mod_t* a, const mod_t* b, int* status) +{ + conv_t* c = sc->c; + states_t* st = c->states; + if (st == NULL) + { + if ((st = Dmod_Malloc(sizeof(*st))) == NULL) + { + *status = -ENOMEM; + return NULL; + } + memset(st, 0, sizeof(*st)); + c->states = st; + } + if (st->count >= MAX_STATES) + { + WARN(c, "script: too many states of the page - the rest not converted\n"); + return NULL; + } + conv_t* s = Dmod_Malloc(sizeof(*s)); + if (s == NULL) + { + *status = -ENOMEM; + return NULL; + } + memset(s, 0, sizeof(*s)); + s->options = c->options; + s->doc = c->doc; + s->vw = c->vw; + s->vh = c->vh; + s->path = c->path; + st->mods[st->count][0] = *a; + if (b != NULL) + st->mods[st->count][1] = *b; + s->mods = st->mods[st->count]; + s->mod_count = (b != NULL) ? 2U : 1U; + st->convs[st->count++] = s; + *status = run_layout(s); + return (*status == 0) ? s : NULL; +} + +static uint32_t mix(uint32_t h, int32_t v) +{ + return (h ^ (uint32_t)v) * 16777619u; +} + +/* A digest of how an element looks, but its opacity: its own style and its inside */ +static uint32_t look_of(const node_t* e) +{ + const style_t* st = e->style; + uint32_t h = 2166136261u; + h = mix(h, (int32_t)st->color); + h = mix(h, (int32_t)st->background); + h = mix(h, e->box.w); + h = mix(h, e->box.h); + for (int i = 0; i < 4; i++) + { + h = mix(h, st->border_w[i]); + h = mix(h, (int32_t)st->border_color[i]); + h = mix(h, st->radius[i].px); + } + if (st->background_image != NULL) + { + for (uint32_t i = 0; i < st->background_image->count; i++) + h = mix(h, (int32_t)st->background_image->colors[i]); + } + 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); + return inside(e, e, h, 0); +} + +/* What a change does to its element: laid out with it, against the page as it is - a move and + * a fade (its variables), or another look (painted as it is then) */ +static int lay_out_change(script_t* sc, change_t* ch) +{ + conv_t* c = sc->c; + node_t* e = ch->element; + ch->laid_out = true; + ch->opacity = e->style->opacity; + const style_t* after = e->style; /* The style it has then: its transition */ + if ((ch->mod.kind == MOD_CLASS_ADD && has_class(e, ch->mod.name)) || + (ch->mod.kind == MOD_CLASS_REMOVE && !has_class(e, ch->mod.name))) + ch->same = true; + else + { + int status = 0; + conv_t* s = run_state(sc, &ch->mod, NULL, &status); + if (status != 0) + return status; + node_t* f = (s != NULL) ? element_index(s->document, e->index, 0) : NULL; + if (s == NULL) + ch->same = true; + else if (f == NULL || !f->box.placed || f->style == NULL || f->style->display == DISPLAY_NONE) + { + ch->changed = true; /* Hidden then */ + ch->look = NULL; + } + else + { + int32_t ox, oy, sx, sy; + view_origin(c, &ox, &oy); + view_origin(s, &sx, &sy); + ch->dx = (f->box.ax - sx) - (e->box.ax - ox); + ch->dy = (f->box.ay - sy) - (e->box.ay - oy); + ch->opacity = f->style->opacity; + if (look_of(f) != look_of(e) || !e->box.placed) + { + ch->changed = true; /* Another look */ + ch->look = f; + } + (void)style_transition(f->style, TRANSITION_POSITION, &ch->move_ms, ch->move_easing); + (void)style_transition(f->style, TRANSITION_OPACITY, &ch->fade_ms, ch->fade_easing); + after = NULL; + } + } + if (after != NULL) + { + (void)style_transition(after, TRANSITION_POSITION, &ch->move_ms, ch->move_easing); + (void)style_transition(after, TRANSITION_OPACITY, &ch->fade_ms, ch->fade_easing); + } + return 0; +} + +/* The looks of an element: one per state of its class (that a script changes) and of being pressed */ +static int make_variants(script_t* sc, node_t* e, const change_t* toggled, bool pressable) +{ + conv_t* c = sc->c; + int status = 0; + node_t* pressed = NULL; + node_t* pressed_toggled = NULL; + mod_t active; + memset(&active, 0, sizeof(active)); + active.element = e->index; + active.kind = MOD_ACTIVE; + active.name = ""; + if (pressable) + { + conv_t* s = run_state(sc, &active, NULL, &status); + node_t* f = (s != NULL) ? element_index(s->document, e->index, 0) : NULL; + if (f != NULL && f->box.placed && (look_of(f) != look_of(e) || f->style->opacity != e->style->opacity)) + pressed = f; + if (pressed != NULL && toggled != NULL) + { + s = run_state(sc, &toggled->mod, &active, &status); + pressed_toggled = (s != NULL) ? element_index(s->document, e->index, 0) : NULL; + if (pressed_toggled != NULL && !pressed_toggled->box.placed) + pressed_toggled = NULL; + } + } + if (status != 0 || (toggled == NULL && pressed == NULL)) + return status; + if (e->dynamic == NULL && (e->dynamic = arena_alloc(&c->arena, sizeof(dynamic_t))) == NULL) + return -ENOMEM; + dynamic_t* d = e->dynamic; + classvar_t* cv = (toggled != NULL) ? classvar(sc, e, toggled->mod.name, false) : NULL; + dmvsi_var_t var = (cv != NULL) ? cv->var : 0; + int32_t base = (toggled != NULL && has_class(e, toggled->mod.name)) ? 1 : 0; + variant_t looks[MAX_VARIANTS]; + uint8_t n = 0; + looks[n++] = (variant_t){ e->box.placed ? e : NULL, var, base, (int8_t)((pressed != NULL) ? 0 : -1) }; + if (toggled != NULL) + looks[n++] = (variant_t){ toggled->look, var, 1 - base, (int8_t)((pressed != NULL) ? 0 : -1) }; + if (pressed != NULL) + looks[n++] = (variant_t){ pressed, var, base, 1 }; + if (pressed != NULL && toggled != NULL) + looks[n++] = (variant_t){ pressed_toggled, var, 1 - base, 1 }; + memcpy(d->variants, looks, n * sizeof(variant_t)); + d->variant_count = n; + return 0; +} + +/* An element's variables: what its changes move and fade */ +static int bind_element(script_t* sc, node_t* e) +{ + bool moves_x = false, moves_y = false, fades = false; + for (uint32_t i = 0; i < sc->change_count; i++) + { + const change_t* ch = &sc->changes[i]; + if (ch->element != e || ch->changed) + continue; + moves_x = moves_x || ch->dx != 0; + moves_y = moves_y || ch->dy != 0; + fades = fades || ch->opacity != e->style->opacity; + } + if (!moves_x && !moves_y && !fades) + return 0; + if (e->dynamic == NULL && (e->dynamic = arena_alloc(&sc->c->arena, sizeof(dynamic_t))) == NULL) + return -ENOMEM; + char name[48]; + int32_t ox, oy; + view_origin(sc->c, &ox, &oy); + dmvsi_rect_t r = group_rect(sc->c, e, ox, oy); + const char* base = (e->id != NULL) ? e->id : e->tag; + if (moves_x) + { + Dmod_SnPrintf(name, sizeof(name), "%s_x", base); + e->dynamic->bind[DMVSI_BIND_X] = dmvsi_add_var(sc->c->doc, name, r.x); + } + if (moves_y) + { + Dmod_SnPrintf(name, sizeof(name), "%s_y", base); + e->dynamic->bind[DMVSI_BIND_Y] = dmvsi_add_var(sc->c->doc, name, r.y); + } + if (fades) + { + Dmod_SnPrintf(name, sizeof(name), "%s_alpha", base); + e->dynamic->bind[DMVSI_BIND_OPACITY] = dmvsi_add_var(sc->c->doc, name, e->style->opacity); + } + return 0; +} + +int script_compile(conv_t* c) +{ + scripts_t scripts; + memset(&scripts, 0, sizeof(scripts)); + find_scripts(c, c->document, &scripts, 0); + script_t* sc = Dmod_Malloc(sizeof(*sc)); + if (sc == NULL) + return -ENOMEM; + memset(sc, 0, sizeof(*sc)); + sc->c = c; + declare_globals(sc, &scripts); + + /* The top level: what it does when the page loads is not converted */ + for (uint32_t p = 0; p < scripts.count; p++) + { + for (const js_t* s = scripts.programs[p]; s != NULL; s = s->next) + { + if (s->kind == S_EXPR || s->kind == S_UNKNOWN || s->kind == S_IF) + { + sc->pass = PASS_EMIT; + report(sc, s, "what the page does when it loads"); + } + } + } + + int status = 0; + sc->pass = PASS_DOMAINS; + handlers(sc, c->document, 0); + sc->pass = PASS_CHANGES; + handlers(sc, c->document, 0); + for (uint32_t i = 0; i < sc->change_count && status == 0; i++) + status = lay_out_change(sc, &sc->changes[i]); + for (uint32_t i = 0; i < sc->change_count && status == 0; i++) + { + node_t* e = sc->changes[i].element; + if (e->dynamic == NULL || (e->dynamic->bind[0] == 0 && e->dynamic->bind[1] == 0 && e->dynamic->bind[2] == 0)) + status = bind_element(sc, e); + } + + /* Another look of a class: the class's variable, asked by the looks */ + sc->pass = PASS_CHANGES; + for (uint32_t i = 0; i < sc->change_count; i++) + { + change_t* ch = &sc->changes[i]; + if (ch->changed && ch->mod.kind != MOD_STYLE) + (void)classvar(sc, ch->element, ch->mod.name, true); + else if (ch->changed) + WARN(c, "script: a style that changes how #%s looks - not converted\n", (ch->element->id != NULL) ? ch->element->id : ch->element->tag); + } + + /* The variables of the script: elements as their indices; the classes asked about */ + for (uint32_t i = 0; i < sc->runtime_count && status == 0; i++) + { + runtime_t* rt = &sc->runtime[i]; + char name[48]; + size_t n = (rt->length < sizeof(name) - 1U) ? rt->length : sizeof(name) - 1U; + memcpy(name, rt->name, n); + name[n] = '\0'; + if ((rt->var = dmvsi_add_var(c->doc, name, rt->initial)) == 0) + status = -ENOMEM; + } + for (uint32_t i = 0; i < sc->classvar_count && status == 0; i++) + { + classvar_t* cv = &sc->classvars[i]; + char name[48]; + Dmod_SnPrintf(name, sizeof(name), "%s_%s", (cv->element->id != NULL) ? cv->element->id : cv->element->tag, cv->name); + if ((cv->var = dmvsi_add_var(c->doc, name, has_class(cv->element, cv->name) ? 1 : 0)) == 0) + status = -ENOMEM; + } + /* The looks: per element, its first class that changes how it looks, and being pressed */ + for (uint32_t i = 0; i < sc->change_count && status == 0; i++) + { + change_t* ch = &sc->changes[i]; + bool first = ch->changed && ch->mod.kind != MOD_STYLE; + for (uint32_t k = 0; k < i && first; k++) + first = !(sc->changes[k].element == ch->element && sc->changes[k].changed && sc->changes[k].mod.kind != MOD_STYLE); + if (first) + { + bool pressable = c->has_active && node_attr(ch->element, "onclick") != NULL; + status = make_variants(sc, ch->element, ch, pressable); + } + } + for (uint32_t i = 0; i < sc->clickable_count && status == 0; i++) + { + node_t* e = sc->clickables[i]; + if (c->has_active && (e->dynamic == NULL || e->dynamic->variant_count == 0)) + status = make_variants(sc, e, NULL, true); + } + if (status == 0) + { + sc->pass = PASS_EMIT; + handlers(sc, c->document, 0); + } + Dmod_Free(sc); + return c->arena.failed ? -ENOMEM : status; +} diff --git a/src/style.c b/src/style.c index 03cda73..b7039f8 100644 --- a/src/style.c +++ b/src/style.c @@ -1102,6 +1102,223 @@ static int split_values(const char* v, const char** parts, size_t* lengths, int return n; } + +/* ---- Transitions ---- */ + +static uint8_t transition_property(const char* s, size_t n) +{ + static const char positions[] = + "|top|left|right|bottom|inset|margin|margin-top|margin-left|margin-right|margin-bottom|transform|translate|"; + char name[32]; + if (n == 0 || n >= sizeof(name)) + return TRANSITION_OTHER; + for (size_t i = 0; i < n; i++) + name[i] = lower(s[i]); + name[n] = '\0'; + if (strcmp(name, "all") == 0) + return TRANSITION_ALL; + if (strcmp(name, "opacity") == 0) + return TRANSITION_OPACITY; + if (strcmp(name, "none") == 0) + return TRANSITION_NONE; + for (const char* p = positions; *p != '\0'; p++) + { + if (*p == '|' && strncmp(p + 1, name, n) == 0 && p[1 + n] == '|') + return TRANSITION_POSITION; + } + return TRANSITION_OTHER; +} + +/* "0.3s", "300ms" -> milliseconds; false when it is no time */ +static bool parse_time(const char** p, uint16_t* ms) +{ + const char* s = skip(*p); + int64_t v; + if (!number(&s, &v)) + return false; + if (word_is(s, "ms")) + s += 2; + else if (word_is(s, "s")) + { + s += 1; + v *= 1000; + } + else + return false; + v /= 1000; + *ms = (uint16_t)((v < 0) ? 0 : (v > 60000) ? 60000 : v); + *p = s; + return true; +} + +/* A timing function: its cubic-bezier, 1/1000; false when it is none */ +static bool parse_timing(const char** p, int16_t* curve) +{ + static const struct { char name[12]; int16_t curve[4]; } keywords[] = { + { "ease", { 250, 100, 250, 1000 } }, { "linear", { 0, 0, 1000, 1000 } }, { "ease-in", { 420, 0, 1000, 1000 } }, + { "ease-out", { 0, 0, 580, 1000 } }, { "ease-in-out", { 420, 0, 580, 1000 } }, + }; + const char* s = skip(*p); + if (word_is(s, "cubic-bezier")) + { + s = skip(s + 12); + if (*s != '(') + return false; + s++; + for (int i = 0; i < 4; i++) + { + int64_t v; + s = skip(s); + if (!number(&s, &v)) + return false; + curve[i] = (int16_t)((v < -2000) ? -2000 : (v > 2000) ? 2000 : v); + s = skip(s); + if (*s == ',') + s++; + } + s = skip(s); + if (*s == ')') + s++; + *p = s; + return true; + } + if (word_is(s, "steps") || word_is(s, "step-start") || word_is(s, "step-end")) + { + while (*s != '\0' && *s != ',' && *s != ')') + s++; + if (*s == ')') + s++; + static const int16_t linear[4] = { 0, 0, 1000, 1000 }; + memcpy(curve, linear, sizeof(linear)); + *p = s; + return true; + } + for (size_t k = 0; k < sizeof(keywords) / sizeof(keywords[0]); k++) + { + if (word_is(s, keywords[k].name)) + { + memcpy(curve, keywords[k].curve, sizeof(keywords[k].curve)); + *p = s + strlen(keywords[k].name); + return true; + } + } + return false; +} + +/* transition-property: a, b, ... */ +static void transition_properties(style_t* st, const char* v) +{ + const char* s = skip(v); + st->transition_count = 0; + while (*s != '\0' && st->transition_count < MAX_TRANSITIONS) + { + s = skip(s); + const char* start = s; + while (*s != '\0' && *s != ',' && !is_space(*s)) + s++; + uint8_t prop = transition_property(start, (size_t)(s - start)); + if (prop != TRANSITION_NONE) + st->transition_props[st->transition_count++] = prop; + while (*s != '\0' && *s != ',') + s++; + if (*s == ',') + s++; + } +} + +static void transition_durations(style_t* st, const char* v) +{ + const char* s = v; + st->duration_count = 0; + while (*s != '\0' && st->duration_count < MAX_TRANSITIONS) + { + if (!parse_time(&s, &st->transition_ms[st->duration_count])) + break; + st->duration_count++; + s = skip(s); + if (*s != ',') + break; + s++; + } +} + +static void transition_timings(style_t* st, const char* v) +{ + const char* s = v; + st->timing_count = 0; + while (*s != '\0' && st->timing_count < MAX_TRANSITIONS) + { + if (!parse_timing(&s, st->transition_easing[st->timing_count])) + break; + st->timing_count++; + s = skip(s); + if (*s != ',') + break; + s++; + } +} + +/* transition: property duration [timing] [delay], ... */ +static void transition_shorthand(style_t* st, const char* v) +{ + static const int16_t ease[4] = { 250, 100, 250, 1000 }; + const char* s = skip(v); + st->transition_count = st->duration_count = st->timing_count = 0; + if (word_is(s, "none")) + return; + while (*s != '\0' && st->transition_count < MAX_TRANSITIONS) + { + uint8_t prop = TRANSITION_ALL; + uint16_t ms = 0, delay; + int16_t curve[4]; + bool has_time = false, has_timing = false; + memcpy(curve, ease, sizeof(curve)); + while (*s != '\0' && *s != ',') + { + s = skip(s); + if (*s == '\0' || *s == ',') + break; + if (!has_time && parse_time(&s, &ms)) + has_time = true; + else if (has_time && parse_time(&s, &delay)) + continue; + else if (!has_timing && parse_timing(&s, curve)) + has_timing = true; + else + { + const char* start = s; + while (*s != '\0' && *s != ',' && !is_space(*s)) + s++; + prop = transition_property(start, (size_t)(s - start)); + } + } + uint8_t i = st->transition_count++; + st->transition_props[i] = prop; + st->transition_ms[i] = ms; + memcpy(st->transition_easing[i], curve, sizeof(curve)); + st->duration_count = st->timing_count = st->transition_count; + if (*s == ',') + s++; + } +} + +bool style_transition(const style_t* st, uint8_t what, uint16_t* ms, int16_t* easing) +{ + static const int16_t ease[4] = { 250, 100, 250, 1000 }; + for (uint8_t i = 0; i < st->transition_count; i++) + { + if (st->transition_props[i] != what && st->transition_props[i] != TRANSITION_ALL) + continue; + *ms = (st->duration_count > 0) ? st->transition_ms[i % st->duration_count] : 0; + if (st->timing_count > 0) + memcpy(easing, st->transition_easing[i % st->timing_count], 4 * sizeof(int16_t)); + else + memcpy(easing, ease, sizeof(ease)); + return *ms > 0; + } + return false; +} + /* ---- Applying a declaration ---- */ typedef struct @@ -1715,6 +1932,10 @@ static void apply(apply_t* a, const char* name, const char* v) return; } if (strcmp(name, "content") == 0) { parse_content(a, v); return; } + if (strcmp(name, "transition") == 0) { transition_shorthand(st, v); return; } + if (strcmp(name, "transition-property") == 0) { transition_properties(st, v); return; } + if (strcmp(name, "transition-duration") == 0) { transition_durations(st, v); return; } + if (strcmp(name, "transition-timing-function") == 0) { transition_timings(st, v); return; } } /* ---- Custom properties ---- */ @@ -1811,6 +2032,7 @@ typedef struct decl_t* decls; uint32_t count; uint32_t rank; + bool active; /* active:x - only while pressed */ } tw_entry_t; /* What computing styles needs besides the arena: in the conversion (c->style_work) */ @@ -1880,7 +2102,7 @@ static tw_entry_t* tailwind_entry(conv_t* c, const char* name) { decl_t decls[MAX_TW_DECLS]; e->name = name; - e->count = tailwind_class(c, name, decls, MAX_TW_DECLS, &e->rank); + e->count = tailwind_class(c, name, decls, MAX_TW_DECLS, &e->rank, &e->active); if (e->count > 0 && (e->decls = arena_alloc(&c->arena, e->count * sizeof(decl_t))) != NULL) memcpy(e->decls, decls, e->count * sizeof(decl_t)); else @@ -1912,6 +2134,8 @@ static uint32_t collect(conv_t* c, const node_t* n, uint8_t pseudo, matched_t* m for (uint32_t i = 0; i < n->class_count; i++) { tw_entry_t* e = tailwind_entry(c, n->classes[i]); + if (e != NULL && e->active && !n->active) + continue; for (uint32_t k = 0; e != NULL && k < e->count && count < MAX_MATCHED; k++) { m[count].decl = &e->decls[k]; diff --git a/src/tailwind.c b/src/tailwind.c index 5fe8a5c..a7fb60e 100644 --- a/src/tailwind.c +++ b/src/tailwind.c @@ -35,7 +35,7 @@ enum R_BORDER_STYLE, R_BORDER_COLOR, R_BORDER_COLOR_XY, R_BORDER_COLOR_SIDE, R_BG_COLOR, R_BG_IMAGE, R_GRADIENT_FROM, R_GRADIENT_VIA, R_GRADIENT_TO, R_PADDING, R_PADDING_XY, R_PADDING_SIDE, R_TEXT_ALIGN, R_VERTICAL_ALIGN, R_FONT_FAMILY, R_FONT_SIZE, R_FONT_WEIGHT, R_TEXT_TRANSFORM, R_FONT_STYLE, R_LEADING, R_TRACKING, R_TEXT_COLOR, - R_OPACITY, R_SHADOW, R_SHADOW_COLOR, R_BLUR, R_DROP_SHADOW, R_FILTER, R_ARBITRARY + R_OPACITY, R_SHADOW, R_SHADOW_COLOR, R_BLUR, R_DROP_SHADOW, R_FILTER, R_TRANSITION, R_DURATION, R_EASE, R_ARBITRARY }; static const char g_shades[11][4] = { "50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950" }; @@ -1108,6 +1108,62 @@ static bool keywords(out_t* o, const char* u, uint32_t* rank) return false; } +/* transition, transition-, duration-, ease- */ +static bool transitions(out_t* o, const char* u, uint32_t* rank) +{ + static const struct { char name[12]; char props[160]; } kinds[] = { + { "", "color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter" }, + { "all", "all" }, { "colors", "color, background-color, border-color, text-decoration-color, fill, stroke" }, + { "opacity", "opacity" }, { "shadow", "box-shadow" }, { "transform", "transform" }, { "none", "none" }, + }; + static const struct { char name[8]; char value[32]; } eases[] = { + { "linear", "linear" }, { "in", "cubic-bezier(0.4, 0, 1, 1)" }, { "out", "cubic-bezier(0, 0, 0.2, 1)" }, + { "in-out", "cubic-bezier(0.4, 0, 0.2, 1)" }, + }; + const char* v; + char value[MAX_VALUE]; + if (strcmp(u, "transition") == 0 || starts(u, "transition-", &v)) + { + const char* kind = (u[10] == '\0') ? "" : u + 11; + for (size_t i = 0; i < sizeof(kinds) / sizeof(kinds[0]); i++) + { + if (strcmp(kinds[i].name, kind) != 0) + continue; + decl(o, "transition-property", kinds[i].props); + if (strcmp(kind, "none") != 0) + { + decl(o, "transition-timing-function", "cubic-bezier(0.4, 0, 0.2, 1)"); + decl(o, "transition-duration", "150ms"); + } + *rank = R_TRANSITION; + return true; + } + return false; + } + if (starts(u, "duration-", &v) && is_number(v)) + { + Dmod_SnPrintf(value, sizeof(value), "%sms", v); + decl(o, "transition-duration", value); + *rank = R_DURATION; + return true; + } + if (starts(u, "ease-", &v)) + { + const char* e = LOOKUP(eases, v); + if (e == NULL) + return false; + decl(o, "transition-timing-function", e); + *rank = R_EASE; + return true; + } + if (starts(u, "delay-", &v)) + { + *rank = R_EASE; + return true; /* Known: nothing to animate later */ + } + return false; +} + static bool others(out_t* o, const char* u, bool negative, uint32_t* rank) { char value[MAX_VALUE]; @@ -1187,11 +1243,12 @@ static const struct int32_t min_width; } g_breakpoints[] = { { "sm", 640 }, { "md", 768 }, { "lg", 1024 }, { "xl", 1280 }, { "2xl", 1536 } }; -uint32_t tailwind_class(conv_t* c, const char* name, decl_t* decls, uint32_t max, uint32_t* rank) +uint32_t tailwind_class(conv_t* c, const char* name, decl_t* decls, uint32_t max, uint32_t* rank, bool* active) { out_t o = { c, decls, 0, max, false }; const char* u = name; uint32_t variant = 0; + *active = false; /* Variants: "md:hover:bg-x" - every one must hold */ for (;;) @@ -1221,6 +1278,12 @@ uint32_t tailwind_class(conv_t* c, const char* name, decl_t* decls, uint32_t max known = true; } } + if (n == 6 && strncmp(u, "active", 6) == 0) + { + *active = true; /* Only while it is pressed */ + c->has_active = true; + known = true; + } if (!known && !(n == 11 && strncmp(u, "motion-safe", 11) == 0)) return 0; /* hover:, focus:, dark:, group-hover:, ... */ u = colon + 1; @@ -1242,7 +1305,7 @@ uint32_t tailwind_class(conv_t* c, const char* name, decl_t* decls, uint32_t max box_sides(&o, u, 'p', "padding", false, R_PADDING, &r) || sizes(&o, u, negative, &r) || inset(&o, u, negative, &r) || texts(&o, u, &r) || backgrounds(&o, u, &r) || borders(&o, u, &r) || rounded(&o, u, &r) || flexbox(&o, u, &r) || grids(&o, u, &r) || effects(&o, u, negative, &r) || - others(&o, u, negative, &r); + transitions(&o, u, &r) || others(&o, u, negative, &r); if (!ok) return 0; *rank = variant * 1000U + r; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d4d9e71..6e9bc72 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,7 +9,7 @@ dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} ) dmod_link_modules(${DMOD_MODULE_NAME} - dmvsi@>=0.1 + dmvsi@>=0.2 ) target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE diff --git a/tests/dmvs_html_test.c b/tests/dmvs_html_test.c index ddc5eb8..e1c841d 100644 --- a/tests/dmvs_html_test.c +++ b/tests/dmvs_html_test.c @@ -223,3 +223,129 @@ DMOD_TEST_STEP(dmvs_html_reports_what_it_cannot_convert) DMOD_TEST_EXPECT_TRUE(convert(FIXTURE("dmodos.html"), "no-such-id", 0, 0, &status) == NULL); DMOD_TEST_EXPECT_EQ(status, -EINVAL); } + +/* The first group (in painting order) named `name` */ +static const dmvsi_node_t* group_named(const dmvsi_node_t* n, const char* name) +{ + for (; n != NULL; n = n->next) + { + if (n->kind != DMVSI_NODE_GROUP) + continue; + if (n->u.group.name != NULL && strcmp(n->u.group.name, name) == 0) + return n; + const dmvsi_node_t* f = group_named(n->first, name); + if (f != NULL) + return f; + } + return NULL; +} + +static int32_t var_initial(dmvsi_doc_t doc, dmvsi_var_t var) +{ + int32_t v = -1; + (void)dmvsi_var_at(doc, var - 1U, NULL, &v); + return v; +} + +DMOD_TEST_STEP(dmvs_html_converts_what_scripts_do) +{ + int status = -1; + DMOD_TEST_EXPECT_TRUE(write_file(TEST_FILE("script.html"), + "
" + "
" + "
" + "
" + "
")); + dmvsi_doc_t doc = convert(TEST_FILE("script.html"), "screen", 0, 0, &status); + DMOD_TEST_EXPECT_EQ(status, 0); + if (doc == NULL) + return; + const dmvsi_node_t* root = dmvsi_root(doc); + + /* The window: its y a variable - off the screen (100 px down) until it is shown */ + const dmvsi_node_t* win = group_named(root->first, "win"); + DMOD_TEST_EXPECT_TRUE(win != NULL && win->bind[DMVSI_BIND_Y] != 0 && win->bind[DMVSI_BIND_X] == 0); + DMOD_TEST_EXPECT_TRUE(win != NULL && var_initial(doc, win->bind[DMVSI_BIND_Y]) == DMVSI_PX(100)); + const dmvsi_node_t* home = group_named(root->first, "home"); + DMOD_TEST_EXPECT_TRUE(home != NULL && home->bind[DMVSI_BIND_OPACITY] != 0 && var_initial(doc, home->bind[DMVSI_BIND_OPACITY]) == 255); + + /* show('win'): the window up (its transition: 300 ms, ease-out), home at half, current = the window */ + const dmvsi_node_t* open = (home != NULL) ? home->first : NULL; + while (open != NULL && open->click == 0) + open = open->next; + DMOD_TEST_EXPECT_TRUE(open != NULL); + if (open != NULL && win != NULL && home != NULL) + { + const dmvsi_action_t* a = NULL; + uint32_t n = dmvsi_handler_actions(doc, open->click, &a); + DMOD_TEST_EXPECT_EQ(n, 3u); + DMOD_TEST_EXPECT_TRUE(n >= 3 && a[0].kind == DMVSI_ACT_ANIMATE && a[0].var == win->bind[DMVSI_BIND_Y] && a[0].value == 0 && + a[0].duration == 300 && a[0].easing[2] == 580); + DMOD_TEST_EXPECT_TRUE(n >= 3 && a[1].kind == DMVSI_ACT_ANIMATE && a[1].var == home->bind[DMVSI_BIND_OPACITY] && + a[1].value == 128 && a[1].duration == 200); + DMOD_TEST_EXPECT_TRUE(n >= 3 && a[2].kind == DMVSI_ACT_SET && a[2].value != 0); + } + + /* hide(): only when something is shown - IF current != 0, back, current = 0 */ + const dmvsi_node_t* close = (win != NULL) ? win->first : NULL; + while (close != NULL && close->kind == DMVSI_NODE_GROUP && close->click == 0 && close->first != NULL && close->first->kind == DMVSI_NODE_GROUP) + close = close->first; + while (close != NULL && close->click == 0) + close = close->next; + DMOD_TEST_EXPECT_TRUE(close != NULL); + if (close != NULL) + { + const dmvsi_action_t* a = NULL; + uint32_t n = dmvsi_handler_actions(doc, close->click, &a); + DMOD_TEST_EXPECT_TRUE(n >= 6 && a[0].kind == DMVSI_ACT_IF_NE && a[0].value == 0); + DMOD_TEST_EXPECT_TRUE(n >= 6 && a[1].kind == DMVSI_ACT_IF_EQ); /* current is the window */ + DMOD_TEST_EXPECT_TRUE(n >= 6 && a[2].kind == DMVSI_ACT_ANIMATE && a[2].value == DMVSI_PX(100)); + DMOD_TEST_EXPECT_TRUE(n >= 6 && a[n - 1U].kind == DMVSI_ACT_END); + } + + /* this.classList.toggle('on'): a variable of the class, the light moved by it */ + const dmvsi_node_t* light = group_named(root->first, "light"); + DMOD_TEST_EXPECT_TRUE(light != NULL && light->click != 0 && light->bind[DMVSI_BIND_X] != 0); + if (light != NULL) + { + const dmvsi_action_t* a = NULL; + uint32_t n = dmvsi_handler_actions(doc, light->click, &a); + DMOD_TEST_EXPECT_TRUE(n == 9 && a[0].kind == DMVSI_ACT_TOGGLE && a[1].kind == DMVSI_ACT_IF_NE); + DMOD_TEST_EXPECT_TRUE(n == 9 && a[3].kind == DMVSI_ACT_SET && a[3].value == DMVSI_PX(30)); /* no transition: at once */ + } + dmvsi_free(doc); +} + +DMOD_TEST_STEP(dmvs_html_switches_the_screens_of_a_page) +{ + int status = -1; + dmvsi_doc_t doc = convert(FIXTURE("dmodos.html"), "os-screen", 0, 0, &status); + DMOD_TEST_EXPECT_EQ(status, 0); + if (doc == NULL) + return; + const dmvsi_node_t* root = dmvsi_root(doc); + static const char windows[6][16] = { "app-settings", "app-system", "app-store", "app-music", "app-home", "app-weather" }; + for (uint32_t i = 0; i < 6U; i++) + { + const dmvsi_node_t* w = group_named(root->first, windows[i]); + DMOD_TEST_EXPECT_TRUE(w != NULL && w->bind[DMVSI_BIND_Y] != 0); + DMOD_TEST_EXPECT_TRUE(w != NULL && var_initial(doc, w->bind[DMVSI_BIND_Y]) == DMVSI_PX(480)); /* top: 100% */ + } + const dmvsi_node_t* home = group_named(root->first, "home-screen"); + DMOD_TEST_EXPECT_TRUE(home != NULL && home->bind[DMVSI_BIND_OPACITY] != 0); + dmvsi_free(doc); +}