diff --git a/runtime/el_runtime.c b/runtime/el_runtime.c
index 4c5a2d3..8f8a602 100644
--- a/runtime/el_runtime.c
+++ b/runtime/el_runtime.c
@@ -1469,22 +1469,25 @@ void http_serve(el_val_t port, el_val_t handler) {
}
int p = (int)port;
if (p <= 0 || p > 65535) { fprintf(stderr, "http_serve: invalid port %d\n", p); return; }
- int sock = socket(AF_INET, SOCK_STREAM, 0);
+ /* Dual-stack: AF_INET6 with IPV6_V6ONLY=0 accepts both IPv4 and IPv6.
+ * This makes `localhost` work in browsers that resolve it to ::1 first. */
+ int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return; }
- int yes = 1;
+ int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
- struct sockaddr_in addr;
+ setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
+ struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- addr.sin_port = htons((uint16_t)p);
+ addr.sin6_family = AF_INET6;
+ addr.sin6_addr = in6addr_any;
+ addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
- fprintf(stderr, "[http] listening on 0.0.0.0:%d\n", p);
+ fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p);
while (1) {
- struct sockaddr_in cli;
+ struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
if (cfd < 0) {
@@ -1715,22 +1718,24 @@ void http_serve_v2(el_val_t port, el_val_t handler) {
fprintf(stderr, "http_serve_v2: invalid port %d\n", p);
return;
}
- int sock = socket(AF_INET, SOCK_STREAM, 0);
+ /* Dual-stack: same as http_serve - AF_INET6 + IPV6_V6ONLY=0. */
+ int sock = socket(AF_INET6, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return; }
- int yes = 1;
+ int yes = 1; int no = 0;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
- struct sockaddr_in addr;
+ setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no));
+ struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- addr.sin_port = htons((uint16_t)p);
+ addr.sin6_family = AF_INET6;
+ addr.sin6_addr = in6addr_any;
+ addr.sin6_port = htons((uint16_t)p);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind"); close(sock); return;
}
if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; }
- fprintf(stderr, "[http v2] listening on 0.0.0.0:%d\n", p);
+ fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p);
while (1) {
- struct sockaddr_in cli;
+ struct sockaddr_in6 cli;
socklen_t clen = sizeof(cli);
int cfd = accept(sock, (struct sockaddr*)&cli, &clen);
if (cfd < 0) {
@@ -3094,6 +3099,9 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
const char* json = EL_CSTR(json_str);
const char* k = EL_CSTR(key);
const char* p = json_find_key(json, k);
+ /* Clear fs_read binary-length hint — result is a fresh null-terminated
+ * string, not the raw file bytes, so Content-Length must use strlen. */
+ _tl_fs_read_len = 0;
if (!p) return el_wrap_str(el_strdup(""));
const char* end = json_skip_value(p);
size_t n = (size_t)(end - p);
@@ -3624,6 +3632,878 @@ el_val_t ttl_cache_age(el_val_t key) {
return (el_val_t)(now_ns - set_at);
}
+/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ──────────────
+ * Phase 1.5. Calendar is pluggable: EarthCalendar (IANA zones + Gregorian +
+ * DST), MarsCalendar (sols, MTC), CycleCalendar(period), NoCycleCalendar,
+ * RelativeCalendar(epoch). Phase 1 zone wrapping folds INTO EarthCalendar;
+ * UTC and IANA zones are themselves Earth-parochial and cannot live at the
+ * lowest type layer.
+ *
+ * A Rhythm is a small AST that asks the Calendar for cycle phase, weekday,
+ * etc. Most rhythm logic is calendar-agnostic at runtime: rhythm_cycle_phase
+ * means "midpoint of cycle" whether the cycle is 24h on Earth or 30h on a
+ * station or 300y on a long-cycle world. */
+
+/* Magic headers — used by the runtime to recognize boxed temporal values
+ * arriving through el_val_t. Distinct constants so accidental misuse fails
+ * loudly rather than silently. */
+#define EL_CAL_MAGIC 0xE1CA1EDDU
+#define EL_CALTIME_MAGIC 0xE1CA1747U
+#define EL_RHYTHM_MAGIC 0xE1287A11U
+#define EL_LDATE_MAGIC 0xE1DA7E00U
+#define EL_LDT_MAGIC 0xE1DA7E1DU
+#define EL_ZONE_MAGIC 0xE12017E0U
+
+typedef enum {
+ EL_CALENDAR_EARTH = 1,
+ EL_CALENDAR_MARS = 2,
+ EL_CALENDAR_CYCLE = 3,
+ EL_CALENDAR_NO_CYCLE = 4,
+ EL_CALENDAR_RELATIVE = 5
+} el_calendar_kind_t;
+
+typedef struct {
+ uint32_t magic;
+ char* id; /* IANA name or "+HH:MM" / "-HH:MM" */
+ int fixed; /* 1 for fixed offset, 0 for IANA */
+ int64_t offset_ns; /* fixed offset in nanos (only when fixed) */
+} el_zone_t;
+
+typedef struct {
+ uint32_t magic;
+ el_calendar_kind_t kind;
+ el_zone_t* zone; /* EarthCalendar; MarsCalendar uses MTC */
+ int64_t cycle_period_ns;/* CycleCalendar; computed for Earth (86400 s) and Mars (88775.244 s) */
+ int64_t epoch_ns; /* RelativeCalendar; Unix-epoch zero otherwise */
+} el_calendar_t;
+
+typedef struct {
+ uint32_t magic;
+ int64_t instant_ns;
+ el_calendar_t* cal;
+} el_caltime_t;
+
+/* Rhythm AST. */
+typedef enum {
+ EL_RHYTHM_CYCLE_START = 1,
+ EL_RHYTHM_CYCLE_PHASE = 2,
+ EL_RHYTHM_DURATION = 3,
+ EL_RHYTHM_SESSION_START = 4,
+ EL_RHYTHM_EVENT = 5,
+ EL_RHYTHM_AND = 6,
+ EL_RHYTHM_OR = 7,
+ EL_RHYTHM_WEEKDAY = 8,
+ EL_RHYTHM_WEEKLY_AT = 9
+} el_rhythm_kind_t;
+
+typedef struct el_rhythm_s {
+ uint32_t magic;
+ el_rhythm_kind_t kind;
+ double phase; /* CYCLE_PHASE */
+ int64_t period_ns; /* DURATION */
+ int weekday; /* 1..7 Mon..Sun */
+ int hour;
+ int minute;
+ char* event_name; /* EVENT */
+ struct el_rhythm_s* a; /* AND/OR */
+ struct el_rhythm_s* b;
+} el_rhythm_t;
+
+typedef struct {
+ uint32_t magic;
+ int year;
+ int month;
+ int day;
+} el_localdate_t;
+
+typedef struct {
+ uint32_t magic;
+ el_localdate_t* date;
+ int64_t time_ns; /* nanos since midnight */
+} el_localdt_t;
+
+/* Magic-tag check helpers — peek the first 4 bytes of an el_val_t pointer
+ * and compare against the expected magic. Strings are NUL-terminated and
+ * never start with our magic byte sequence, so this is safe. */
+static int el_is_magic(el_val_t v, uint32_t want) {
+ if (v == 0) return 0;
+ /* Defensive: only follow pointers in plausible address space.
+ * On 64-bit unix processes pointers are above 0x10000. */
+ if ((uint64_t)v < 0x10000ULL) return 0;
+ uint32_t got = *(volatile uint32_t*)(uintptr_t)v;
+ return got == want;
+}
+
+/* Sol length on Mars in nanoseconds: 88775.244 seconds. */
+#define EL_MARS_SOL_NS ((int64_t)88775244000000LL)
+/* Earth solar day in nanoseconds: 86400 seconds. */
+#define EL_EARTH_DAY_NS ((int64_t)86400000000000LL)
+
+/* ── Zone construction ──────────────────────────────────────────────────────
+ * Zones intern by id string so equality comparisons are pointer-compares. */
+
+#define EL_ZONE_TABLE_CAP 64
+static el_zone_t* _el_zone_table[EL_ZONE_TABLE_CAP];
+static int _el_zone_count = 0;
+
+static el_zone_t* _el_zone_intern(const char* id, int fixed, int64_t offset_ns) {
+ for (int i = 0; i < _el_zone_count; i++) {
+ el_zone_t* z = _el_zone_table[i];
+ if (z->fixed == fixed && z->offset_ns == offset_ns &&
+ strcmp(z->id ? z->id : "", id ? id : "") == 0) {
+ return z;
+ }
+ }
+ if (_el_zone_count >= EL_ZONE_TABLE_CAP) {
+ /* Out of slots: build a non-interned zone. Equality will fail across
+ * such zones but the program still runs. */
+ el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t));
+ z->magic = EL_ZONE_MAGIC;
+ z->id = el_strdup_persist(id ? id : "");
+ z->fixed = fixed;
+ z->offset_ns = offset_ns;
+ return z;
+ }
+ el_zone_t* z = (el_zone_t*)malloc(sizeof(el_zone_t));
+ z->magic = EL_ZONE_MAGIC;
+ z->id = el_strdup_persist(id ? id : "");
+ z->fixed = fixed;
+ z->offset_ns = offset_ns;
+ _el_zone_table[_el_zone_count++] = z;
+ return z;
+}
+
+el_val_t zone(el_val_t id) {
+ const char* s = EL_CSTR(id);
+ if (!s || !*s) return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 0, 0);
+ /* Fixed-offset shortcut: "+HH:MM" or "-HH:MM". */
+ if ((s[0] == '+' || s[0] == '-') && strlen(s) >= 6 && s[3] == ':') {
+ int sign = (s[0] == '-') ? -1 : 1;
+ int hh = (s[1] - '0') * 10 + (s[2] - '0');
+ int mm = (s[4] - '0') * 10 + (s[5] - '0');
+ int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL;
+ return (el_val_t)(uintptr_t)_el_zone_intern(s, 1, off);
+ }
+ return (el_val_t)(uintptr_t)_el_zone_intern(s, 0, 0);
+}
+
+el_val_t zone_utc(void) {
+ return (el_val_t)(uintptr_t)_el_zone_intern("UTC", 1, 0);
+}
+
+el_val_t zone_local(void) {
+ /* Resolve the local zone via TZ env or system default. tzset() picks
+ * up TZ if set; otherwise the C library reads /etc/localtime. We store
+ * the zone id as "LOCAL" so subsequent equality holds; resolution is
+ * lazy at use time. */
+ return (el_val_t)(uintptr_t)_el_zone_intern("LOCAL", 0, 0);
+}
+
+el_val_t zone_offset(el_val_t hours, el_val_t minutes) {
+ int hh = (int)(int64_t)hours;
+ int mm = (int)(int64_t)minutes;
+ int sign = (hh < 0 || mm < 0) ? -1 : 1;
+ if (hh < 0) hh = -hh;
+ if (mm < 0) mm = -mm;
+ int64_t off = (int64_t)sign * ((int64_t)hh * 3600LL + (int64_t)mm * 60LL) * 1000000000LL;
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%c%02d:%02d", sign < 0 ? '-' : '+', hh, mm);
+ return (el_val_t)(uintptr_t)_el_zone_intern(buf, 1, off);
+}
+
+/* ── Calendar interning ──────────────────────────────────────────────────── */
+
+#define EL_CAL_TABLE_CAP 64
+static el_calendar_t* _el_cal_table[EL_CAL_TABLE_CAP];
+static int _el_cal_count = 0;
+
+static el_calendar_t* _el_cal_intern(el_calendar_kind_t kind, el_zone_t* z,
+ int64_t period_ns, int64_t epoch_ns) {
+ for (int i = 0; i < _el_cal_count; i++) {
+ el_calendar_t* c = _el_cal_table[i];
+ if (c->kind == kind && c->zone == z &&
+ c->cycle_period_ns == period_ns && c->epoch_ns == epoch_ns) {
+ return c;
+ }
+ }
+ el_calendar_t* c = (el_calendar_t*)malloc(sizeof(el_calendar_t));
+ c->magic = EL_CAL_MAGIC;
+ c->kind = kind;
+ c->zone = z;
+ c->cycle_period_ns = period_ns;
+ c->epoch_ns = epoch_ns;
+ if (_el_cal_count < EL_CAL_TABLE_CAP) _el_cal_table[_el_cal_count++] = c;
+ return c;
+}
+
+el_val_t earth_calendar(el_val_t z_val) {
+ el_zone_t* z = NULL;
+ if (z_val != 0 && el_is_magic(z_val, EL_ZONE_MAGIC)) {
+ z = (el_zone_t*)(uintptr_t)z_val;
+ } else {
+ z = (el_zone_t*)(uintptr_t)zone_local();
+ }
+ return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_EARTH, z, EL_EARTH_DAY_NS, 0);
+}
+
+el_val_t earth_calendar_default(void) {
+ return earth_calendar(zone_local());
+}
+
+el_val_t mars_calendar(void) {
+ el_zone_t* z = (el_zone_t*)(uintptr_t)_el_zone_intern("MTC", 1, 0);
+ return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_MARS, z, EL_MARS_SOL_NS, 0);
+}
+
+el_val_t cycle_calendar(el_val_t period_dur) {
+ int64_t period = (int64_t)period_dur;
+ if (period <= 0) period = 1;
+ return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_CYCLE, NULL, period, 0);
+}
+
+el_val_t no_cycle_calendar(void) {
+ return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_NO_CYCLE, NULL, 0, 0);
+}
+
+el_val_t relative_calendar(el_val_t epoch_inst) {
+ int64_t ep = (int64_t)epoch_inst;
+ return (el_val_t)(uintptr_t)_el_cal_intern(EL_CALENDAR_RELATIVE, NULL, 0, ep);
+}
+
+/* ── CalendarTime ───────────────────────────────────────────────────────── */
+
+static el_caltime_t* _el_caltime_alloc(int64_t inst, el_calendar_t* c) {
+ el_caltime_t* ct = (el_caltime_t*)malloc(sizeof(el_caltime_t));
+ ct->magic = EL_CALTIME_MAGIC;
+ ct->instant_ns = inst;
+ ct->cal = c;
+ return ct;
+}
+
+static el_calendar_t* _el_resolve_cal(el_val_t cal_val) {
+ if (cal_val == 0 || !el_is_magic(cal_val, EL_CAL_MAGIC)) {
+ return (el_calendar_t*)(uintptr_t)earth_calendar_default();
+ }
+ return (el_calendar_t*)(uintptr_t)cal_val;
+}
+
+el_val_t now_in(el_val_t cal_val) {
+ el_calendar_t* c = _el_resolve_cal(cal_val);
+ int64_t ns = (int64_t)el_now_instant();
+ return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c);
+}
+
+el_val_t in_calendar(el_val_t inst, el_val_t cal_val) {
+ el_calendar_t* c = _el_resolve_cal(cal_val);
+ return (el_val_t)(uintptr_t)_el_caltime_alloc((int64_t)inst, c);
+}
+
+el_val_t cal_to_instant(el_val_t ct_val) {
+ if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0;
+ el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
+ return (el_val_t)ct->instant_ns;
+}
+
+el_val_t cal_in(el_val_t ct_val, el_val_t cal_val) {
+ if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0;
+ el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
+ el_calendar_t* c = _el_resolve_cal(cal_val);
+ return (el_val_t)(uintptr_t)_el_caltime_alloc(ct->instant_ns, c);
+}
+
+el_val_t cal_cycle_phase(el_val_t ct_val) {
+ if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_from_float(0.0);
+ el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
+ el_calendar_t* c = ct->cal;
+ if (c->kind == EL_CALENDAR_NO_CYCLE) {
+ return el_from_float(0.0/0.0); /* NaN sentinel */
+ }
+ int64_t period = c->cycle_period_ns;
+ if (period <= 0) return el_from_float(0.0);
+ int64_t base = ct->instant_ns - c->epoch_ns;
+ int64_t phase_ns = base % period;
+ if (phase_ns < 0) phase_ns += period;
+ double phase = (double)phase_ns / (double)period;
+ return el_from_float(phase);
+}
+
+/* ── Earth zone resolution: TZ-based offset lookup ──────────────────────────
+ * For an EarthCalendar(zone), we want to convert an instant_ns into local
+ * y/m/d/h/m/s, including DST. Approach: setenv("TZ", id), tzset(), use
+ * localtime_r, then restore. This is not thread-safe by design — El's
+ * runtime is single-threaded for the request handler path. Cache the
+ * computed (instant -> tm) to avoid the syscall churn on repeat formats. */
+
+static void _el_apply_zone(el_zone_t* z) {
+ if (!z) { unsetenv("TZ"); tzset(); return; }
+ if (z->fixed && strcmp(z->id, "UTC") == 0) {
+ setenv("TZ", "UTC0", 1);
+ tzset();
+ return;
+ }
+ if (z->fixed) {
+ /* Fixed offset: POSIX TZ uses inverted sign (sign convention of
+ * "hours WEST of UTC" rather than east). Build the spec accordingly. */
+ char buf[32];
+ int neg_secs = (int)(-z->offset_ns / 1000000000LL);
+ int sign = neg_secs < 0 ? -1 : 1;
+ int abs_secs = neg_secs < 0 ? -neg_secs : neg_secs;
+ int hh = abs_secs / 3600;
+ int mm = (abs_secs % 3600) / 60;
+ snprintf(buf, sizeof(buf), "FIX%c%d:%02d", sign < 0 ? '-' : '+', hh, mm);
+ setenv("TZ", buf, 1);
+ tzset();
+ return;
+ }
+ if (strcmp(z->id, "LOCAL") == 0) {
+ unsetenv("TZ");
+ tzset();
+ return;
+ }
+ setenv("TZ", z->id, 1);
+ tzset();
+}
+
+static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_len, char* abbr_buf, size_t abbr_cap) {
+ el_calendar_t* c = ct->cal;
+ el_zone_t* z = c->zone;
+ _el_apply_zone(z);
+ time_t s = (time_t)(ct->instant_ns / 1000000000LL);
+ struct tm tm;
+ localtime_r(&s, &tm);
+ *tm_out = tm;
+ if (abbr_buf && abbr_cap > 0) {
+ const char* z_str = tm.tm_zone ? tm.tm_zone : "";
+ size_t n = strlen(z_str);
+ if (n >= abbr_cap) n = abbr_cap - 1;
+ memcpy(abbr_buf, z_str, n);
+ abbr_buf[n] = '\0';
+ if (abbr_len) *abbr_len = (int)n;
+ }
+ return 0;
+}
+
+/* Format an Earth CalendarTime under a Java-DateTimeFormatter-ish pattern.
+ * We support a useful core: yyyy MM dd HH mm ss z EEE MMM d h a — enough for
+ * the acceptance tests. Single quotes denote literal text. */
+static const char* _el_weekday_short[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
+static const char* _el_month_short[] = {"Jan","Feb","Mar","Apr","May","Jun",
+ "Jul","Aug","Sep","Oct","Nov","Dec"};
+
+static char* _el_format_earth(el_caltime_t* ct, const char* pattern) {
+ struct tm tm;
+ char abbr[16] = {0};
+ int abbr_len = 0;
+ _el_decompose_earth(ct, &tm, &abbr_len, abbr, sizeof(abbr));
+ size_t cap = strlen(pattern) * 4 + 64;
+ char* out = (char*)malloc(cap);
+ size_t pos = 0;
+ size_t i = 0;
+ size_t plen = strlen(pattern);
+ while (i < plen) {
+ char ch = pattern[i];
+ /* Quoted literal */
+ if (ch == '\'') {
+ i++;
+ while (i < plen && pattern[i] != '\'') {
+ if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
+ out[pos++] = pattern[i++];
+ }
+ if (i < plen) i++;
+ continue;
+ }
+ /* Count run of same letter */
+ size_t run = 1;
+ while (i + run < plen && pattern[i + run] == ch) run++;
+ char tmp[64];
+ tmp[0] = '\0';
+ if (ch == 'y') {
+ if (run >= 4) snprintf(tmp, sizeof(tmp), "%04d", tm.tm_year + 1900);
+ else snprintf(tmp, sizeof(tmp), "%02d", (tm.tm_year + 1900) % 100);
+ } else if (ch == 'M') {
+ if (run >= 3) snprintf(tmp, sizeof(tmp), "%s", _el_month_short[tm.tm_mon]);
+ else if (run == 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mon + 1);
+ else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mon + 1);
+ } else if (ch == 'd') {
+ if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_mday);
+ else snprintf(tmp, sizeof(tmp), "%d", tm.tm_mday);
+ } else if (ch == 'H') {
+ if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_hour);
+ else snprintf(tmp, sizeof(tmp), "%d", tm.tm_hour);
+ } else if (ch == 'h') {
+ int h12 = tm.tm_hour % 12; if (h12 == 0) h12 = 12;
+ if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", h12);
+ else snprintf(tmp, sizeof(tmp), "%d", h12);
+ } else if (ch == 'm') {
+ if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_min);
+ else snprintf(tmp, sizeof(tmp), "%d", tm.tm_min);
+ } else if (ch == 's') {
+ if (run >= 2) snprintf(tmp, sizeof(tmp), "%02d", tm.tm_sec);
+ else snprintf(tmp, sizeof(tmp), "%d", tm.tm_sec);
+ } else if (ch == 'a') {
+ snprintf(tmp, sizeof(tmp), "%s", tm.tm_hour < 12 ? "AM" : "PM");
+ } else if (ch == 'E') {
+ snprintf(tmp, sizeof(tmp), "%s", _el_weekday_short[tm.tm_wday]);
+ } else if (ch == 'z') {
+ snprintf(tmp, sizeof(tmp), "%s", abbr);
+ } else {
+ for (size_t k = 0; k < run; k++) {
+ if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
+ out[pos++] = ch;
+ }
+ i += run;
+ continue;
+ }
+ size_t tl = strlen(tmp);
+ if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); }
+ memcpy(out + pos, tmp, tl);
+ pos += tl;
+ i += run;
+ }
+ out[pos] = '\0';
+ char* result = el_strdup(out);
+ free(out);
+ return result;
+}
+
+/* Format a Mars CalendarTime: %sol prints the integer sol number since
+ * mission epoch (Unix epoch fallback), %phase prints cycle_phase as a
+ * 0..1 decimal. Other %-specifiers fall through. */
+static char* _el_format_mars(el_caltime_t* ct, const char* pattern) {
+ el_calendar_t* c = ct->cal;
+ int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : EL_MARS_SOL_NS;
+ int64_t base = ct->instant_ns - c->epoch_ns;
+ int64_t sol = base / period;
+ int64_t phase_ns = base % period;
+ if (phase_ns < 0) { phase_ns += period; sol -= 1; }
+ double phase = (double)phase_ns / (double)period;
+ size_t cap = strlen(pattern) * 4 + 64;
+ char* out = (char*)malloc(cap);
+ size_t pos = 0;
+ for (size_t i = 0; pattern[i]; i++) {
+ if (pattern[i] == '%' && pattern[i+1]) {
+ char tmp[64];
+ tmp[0] = '\0';
+ if (strncmp(pattern + i + 1, "sol", 3) == 0) {
+ snprintf(tmp, sizeof(tmp), "%lld", (long long)sol);
+ i += 3;
+ } else if (strncmp(pattern + i + 1, "phase", 5) == 0) {
+ snprintf(tmp, sizeof(tmp), "%.4f", phase);
+ i += 5;
+ } else if (pattern[i+1] == 'd') {
+ snprintf(tmp, sizeof(tmp), "%lld", (long long)sol);
+ i += 1;
+ } else {
+ tmp[0] = pattern[i+1]; tmp[1] = '\0';
+ i += 1;
+ }
+ size_t tl = strlen(tmp);
+ if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); }
+ memcpy(out + pos, tmp, tl);
+ pos += tl;
+ } else {
+ if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
+ out[pos++] = pattern[i];
+ }
+ }
+ out[pos] = '\0';
+ char* result = el_strdup(out);
+ free(out);
+ return result;
+}
+
+/* Format a CycleCalendar CalendarTime: %cycle and %phase. */
+static char* _el_format_cycle(el_caltime_t* ct, const char* pattern) {
+ el_calendar_t* c = ct->cal;
+ int64_t period = c->cycle_period_ns > 0 ? c->cycle_period_ns : 1;
+ int64_t base = ct->instant_ns - c->epoch_ns;
+ int64_t cycle = base / period;
+ int64_t phase_ns = base % period;
+ if (phase_ns < 0) { phase_ns += period; cycle -= 1; }
+ double phase = (double)phase_ns / (double)period;
+ size_t cap = strlen(pattern) * 4 + 64;
+ char* out = (char*)malloc(cap);
+ size_t pos = 0;
+ for (size_t i = 0; pattern[i]; i++) {
+ if (pattern[i] == '%' && pattern[i+1]) {
+ char tmp[64];
+ tmp[0] = '\0';
+ if (strncmp(pattern + i + 1, "cycle", 5) == 0) {
+ snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle);
+ i += 5;
+ } else if (strncmp(pattern + i + 1, "phase", 5) == 0) {
+ snprintf(tmp, sizeof(tmp), "%.4f", phase);
+ i += 5;
+ } else if (pattern[i+1] == 'd') {
+ snprintf(tmp, sizeof(tmp), "%lld", (long long)cycle);
+ i += 1;
+ } else if (pattern[i+1] == 'f') {
+ snprintf(tmp, sizeof(tmp), "%.2f", phase);
+ i += 1;
+ } else {
+ /* Pass through unknown specifier */
+ tmp[0] = '%'; tmp[1] = pattern[i+1]; tmp[2] = '\0';
+ i += 1;
+ }
+ size_t tl = strlen(tmp);
+ if (pos + tl + 1 >= cap) { cap = (cap + tl) * 2; out = realloc(out, cap); }
+ memcpy(out + pos, tmp, tl);
+ pos += tl;
+ } else {
+ if (pos + 1 >= cap) { cap *= 2; out = realloc(out, cap); }
+ out[pos++] = pattern[i];
+ }
+ }
+ out[pos] = '\0';
+ char* result = el_strdup(out);
+ free(out);
+ return result;
+}
+
+el_val_t cal_format(el_val_t ct_val, el_val_t pattern_val) {
+ if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return el_wrap_str(el_strdup(""));
+ el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
+ const char* pat = EL_CSTR(pattern_val);
+ if (!pat) pat = "";
+ char* result = NULL;
+ switch (ct->cal->kind) {
+ case EL_CALENDAR_EARTH: result = _el_format_earth(ct, pat); break;
+ case EL_CALENDAR_MARS: result = _el_format_mars(ct, pat); break;
+ case EL_CALENDAR_CYCLE: result = _el_format_cycle(ct, pat); break;
+ case EL_CALENDAR_RELATIVE: result = _el_format_cycle(ct, pat); break;
+ case EL_CALENDAR_NO_CYCLE: {
+ char buf[64];
+ snprintf(buf, sizeof(buf), "instant:%lld", (long long)ct->instant_ns);
+ result = el_strdup(buf);
+ break;
+ }
+ default: result = el_strdup("");
+ }
+ return el_wrap_str(result);
+}
+
+/* ── LocalDate / LocalTime / LocalDateTime ──────────────────────────────── */
+
+static int _el_days_in_month(int y, int m) {
+ static const int dim[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
+ if (m == 2) {
+ int leap = ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0);
+ return 28 + (leap ? 1 : 0);
+ }
+ if (m < 1 || m > 12) return 30;
+ return dim[m - 1];
+}
+
+el_val_t local_date(el_val_t y, el_val_t m, el_val_t d) {
+ el_localdate_t* ld = (el_localdate_t*)malloc(sizeof(el_localdate_t));
+ ld->magic = EL_LDATE_MAGIC;
+ ld->year = (int)(int64_t)y;
+ ld->month = (int)(int64_t)m;
+ ld->day = (int)(int64_t)d;
+ return (el_val_t)(uintptr_t)ld;
+}
+
+el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns) {
+ int64_t hh = (int64_t)h;
+ int64_t mm = (int64_t)m;
+ int64_t ss = (int64_t)s;
+ int64_t nn = (int64_t)ns;
+ int64_t total = hh * 3600000000000LL + mm * 60000000000LL + ss * 1000000000LL + nn;
+ return (el_val_t)total;
+}
+
+el_val_t local_datetime(el_val_t date_val, el_val_t time_val) {
+ if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0;
+ el_localdt_t* ldt = (el_localdt_t*)malloc(sizeof(el_localdt_t));
+ ldt->magic = EL_LDT_MAGIC;
+ ldt->date = (el_localdate_t*)(uintptr_t)date_val;
+ ldt->time_ns = (int64_t)time_val;
+ return (el_val_t)(uintptr_t)ldt;
+}
+
+el_val_t zoned(el_val_t date_val, el_val_t time_val, el_val_t cal_val) {
+ if (!el_is_magic(date_val, EL_LDATE_MAGIC)) return (el_val_t)0;
+ el_localdate_t* ld = (el_localdate_t*)(uintptr_t)date_val;
+ el_calendar_t* c = _el_resolve_cal(cal_val);
+ int64_t time_ns = (int64_t)time_val;
+ /* Convert (LocalDate, LocalTime, EarthCalendar) -> Instant.
+ * For non-Earth calendars we use day-anchored conversion: treat the
+ * LocalDate's (y,m,d) as a Gregorian projection, convert to seconds via
+ * mktime under the calendar's zone, then add nanos-since-midnight. */
+ if (c->kind == EL_CALENDAR_EARTH) {
+ _el_apply_zone(c->zone);
+ struct tm tm; memset(&tm, 0, sizeof(tm));
+ tm.tm_year = ld->year - 1900;
+ tm.tm_mon = ld->month - 1;
+ tm.tm_mday = ld->day;
+ tm.tm_hour = (int)(time_ns / 3600000000000LL);
+ tm.tm_min = (int)((time_ns / 60000000000LL) % 60);
+ tm.tm_sec = (int)((time_ns / 1000000000LL) % 60);
+ tm.tm_isdst = -1;
+ time_t t = mktime(&tm);
+ if (t == (time_t)-1) return (el_val_t)0;
+ int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL);
+ return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c);
+ }
+ /* Non-Earth fallback: project as if Earth UTC then attach calendar. */
+ struct tm tm; memset(&tm, 0, sizeof(tm));
+ tm.tm_year = ld->year - 1900;
+ tm.tm_mon = ld->month - 1;
+ tm.tm_mday = ld->day;
+ tm.tm_hour = (int)(time_ns / 3600000000000LL);
+ tm.tm_min = (int)((time_ns / 60000000000LL) % 60);
+ tm.tm_sec = (int)((time_ns / 1000000000LL) % 60);
+ time_t t = timegm(&tm);
+ if (t == (time_t)-1) return (el_val_t)0;
+ int64_t ns = (int64_t)t * 1000000000LL + (time_ns % 1000000000LL);
+ return (el_val_t)(uintptr_t)_el_caltime_alloc(ns, c);
+}
+
+el_val_t local_date_year(el_val_t v) {
+ if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0;
+ return (el_val_t)((el_localdate_t*)(uintptr_t)v)->year;
+}
+el_val_t local_date_month(el_val_t v) {
+ if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0;
+ return (el_val_t)((el_localdate_t*)(uintptr_t)v)->month;
+}
+el_val_t local_date_day(el_val_t v) {
+ if (!el_is_magic(v, EL_LDATE_MAGIC)) return (el_val_t)0;
+ return (el_val_t)((el_localdate_t*)(uintptr_t)v)->day;
+}
+el_val_t local_time_hour(el_val_t v) {
+ int64_t t = (int64_t)v;
+ return (el_val_t)(t / 3600000000000LL);
+}
+el_val_t local_time_minute(el_val_t v) {
+ int64_t t = (int64_t)v;
+ return (el_val_t)((t / 60000000000LL) % 60);
+}
+el_val_t local_time_second(el_val_t v) {
+ int64_t t = (int64_t)v;
+ return (el_val_t)((t / 1000000000LL) % 60);
+}
+el_val_t local_time_nanos(el_val_t v) {
+ int64_t t = (int64_t)v;
+ return (el_val_t)(t % 1000000000LL);
+}
+
+el_val_t el_local_date_add_dur(el_val_t ld_val, el_val_t dur_val) {
+ if (!el_is_magic(ld_val, EL_LDATE_MAGIC)) return ld_val;
+ el_localdate_t* ld = (el_localdate_t*)(uintptr_t)ld_val;
+ int64_t dur_ns = (int64_t)dur_val;
+ int64_t days = dur_ns / EL_EARTH_DAY_NS;
+ int y = ld->year, m = ld->month, d = ld->day;
+ /* Walk days forward/backward in canonical Gregorian. */
+ while (days > 0) {
+ int dim = _el_days_in_month(y, m);
+ if (d + days <= dim) { d += (int)days; days = 0; break; }
+ days -= (dim - d + 1);
+ d = 1;
+ m++;
+ if (m > 12) { m = 1; y++; }
+ }
+ while (days < 0) {
+ if (d + days >= 1) { d += (int)days; days = 0; break; }
+ days += d;
+ m--;
+ if (m < 1) { m = 12; y--; }
+ d = _el_days_in_month(y, m);
+ }
+ return local_date((el_val_t)y, (el_val_t)m, (el_val_t)d);
+}
+
+el_val_t el_local_time_add_dur(el_val_t lt_val, el_val_t dur_val) {
+ int64_t t = (int64_t)lt_val + (int64_t)dur_val;
+ /* Wrap mod 24h on Earth-default. CycleCalendar wrapping requires the
+ * caller to use cal_in / cal_format for the right modulus. */
+ int64_t day = EL_EARTH_DAY_NS;
+ int64_t r = t % day;
+ if (r < 0) r += day;
+ return (el_val_t)r;
+}
+
+el_val_t el_local_date_lt(el_val_t a_val, el_val_t b_val) {
+ if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0;
+ el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val;
+ el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val;
+ if (a->year != b->year) return (el_val_t)(a->year < b->year ? 1 : 0);
+ if (a->month != b->month) return (el_val_t)(a->month < b->month ? 1 : 0);
+ return (el_val_t)(a->day < b->day ? 1 : 0);
+}
+
+el_val_t el_local_date_eq(el_val_t a_val, el_val_t b_val) {
+ if (!el_is_magic(a_val, EL_LDATE_MAGIC) || !el_is_magic(b_val, EL_LDATE_MAGIC)) return (el_val_t)0;
+ el_localdate_t* a = (el_localdate_t*)(uintptr_t)a_val;
+ el_localdate_t* b = (el_localdate_t*)(uintptr_t)b_val;
+ return (el_val_t)((a->year == b->year && a->month == b->month && a->day == b->day) ? 1 : 0);
+}
+
+/* ── Rhythm ──────────────────────────────────────────────────────────────── */
+
+static el_rhythm_t* _el_rhythm_alloc(el_rhythm_kind_t k) {
+ el_rhythm_t* r = (el_rhythm_t*)calloc(1, sizeof(el_rhythm_t));
+ r->magic = EL_RHYTHM_MAGIC;
+ r->kind = k;
+ return r;
+}
+
+el_val_t rhythm_cycle_start(void) {
+ return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_CYCLE_START);
+}
+
+el_val_t rhythm_cycle_phase(el_val_t phase_val) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_CYCLE_PHASE);
+ r->phase = el_to_float(phase_val);
+ return (el_val_t)(uintptr_t)r;
+}
+
+el_val_t rhythm_duration(el_val_t d_val) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_DURATION);
+ r->period_ns = (int64_t)d_val;
+ return (el_val_t)(uintptr_t)r;
+}
+
+el_val_t rhythm_session_start(void) {
+ return (el_val_t)(uintptr_t)_el_rhythm_alloc(EL_RHYTHM_SESSION_START);
+}
+
+el_val_t rhythm_event(el_val_t name_val) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_EVENT);
+ const char* n = EL_CSTR(name_val);
+ r->event_name = el_strdup_persist(n ? n : "");
+ return (el_val_t)(uintptr_t)r;
+}
+
+el_val_t rhythm_and(el_val_t a_val, el_val_t b_val) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_AND);
+ r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL;
+ r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL;
+ return (el_val_t)(uintptr_t)r;
+}
+
+el_val_t rhythm_or(el_val_t a_val, el_val_t b_val) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_OR);
+ r->a = el_is_magic(a_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)a_val : NULL;
+ r->b = el_is_magic(b_val, EL_RHYTHM_MAGIC) ? (el_rhythm_t*)(uintptr_t)b_val : NULL;
+ return (el_val_t)(uintptr_t)r;
+}
+
+el_val_t rhythm_weekday(el_val_t day) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKDAY);
+ r->weekday = (int)(int64_t)day;
+ return (el_val_t)(uintptr_t)r;
+}
+
+el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute) {
+ el_rhythm_t* r = _el_rhythm_alloc(EL_RHYTHM_WEEKLY_AT);
+ r->weekday = (int)(int64_t)day;
+ r->hour = (int)(int64_t)hour;
+ r->minute = (int)(int64_t)minute;
+ return (el_val_t)(uintptr_t)r;
+}
+
+/* Compute the next instant on or after `after` when rhythm `r` matches,
+ * under calendar `cal`. */
+static int64_t _el_next_after(el_rhythm_t* r, int64_t after_ns, el_calendar_t* cal) {
+ if (!r) return after_ns;
+ int64_t period = cal->cycle_period_ns > 0 ? cal->cycle_period_ns : EL_EARTH_DAY_NS;
+ switch (r->kind) {
+ case EL_RHYTHM_CYCLE_START: {
+ int64_t base = after_ns - cal->epoch_ns;
+ int64_t cyc = (base / period) + 1;
+ return cal->epoch_ns + cyc * period;
+ }
+ case EL_RHYTHM_CYCLE_PHASE: {
+ int64_t base = after_ns - cal->epoch_ns;
+ int64_t cyc_ns = (int64_t)(r->phase * (double)period);
+ int64_t cur_cyc = base / period;
+ int64_t candidate = cal->epoch_ns + cur_cyc * period + cyc_ns;
+ if (candidate <= after_ns) candidate += period;
+ return candidate;
+ }
+ case EL_RHYTHM_DURATION: {
+ return after_ns + (r->period_ns > 0 ? r->period_ns : 1);
+ }
+ case EL_RHYTHM_WEEKDAY:
+ case EL_RHYTHM_WEEKLY_AT: {
+ if (cal->kind != EL_CALENDAR_EARTH) {
+ /* Non-Earth calendars: fall back to cycle math, treating
+ * weekday as a 7-cycle-per-period proxy. */
+ return after_ns + period;
+ }
+ _el_apply_zone(cal->zone);
+ time_t s = (time_t)(after_ns / 1000000000LL);
+ struct tm tm;
+ localtime_r(&s, &tm);
+ /* tm_wday: 0=Sun..6=Sat. We use 1=Mon..7=Sun. */
+ int target = r->weekday >= 1 && r->weekday <= 7 ? r->weekday : 1;
+ int target_wday = target == 7 ? 0 : target; /* 7→Sun=0, 1→Mon=1 */
+ int days_ahead = (target_wday - tm.tm_wday + 7) % 7;
+ int hour = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->hour : 0;
+ int minute = (r->kind == EL_RHYTHM_WEEKLY_AT) ? r->minute : 0;
+ struct tm cand = tm;
+ cand.tm_mday += days_ahead;
+ cand.tm_hour = hour;
+ cand.tm_min = minute;
+ cand.tm_sec = 0;
+ cand.tm_isdst = -1;
+ time_t cand_t = mktime(&cand);
+ int64_t cand_ns = (int64_t)cand_t * 1000000000LL;
+ if (cand_ns <= after_ns) {
+ cand.tm_mday += 7;
+ cand.tm_isdst = -1;
+ cand_t = mktime(&cand);
+ cand_ns = (int64_t)cand_t * 1000000000LL;
+ }
+ return cand_ns;
+ }
+ case EL_RHYTHM_AND: {
+ int64_t a = _el_next_after(r->a, after_ns, cal);
+ int64_t b = _el_next_after(r->b, after_ns, cal);
+ return a > b ? a : b;
+ }
+ case EL_RHYTHM_OR: {
+ int64_t a = _el_next_after(r->a, after_ns, cal);
+ int64_t b = _el_next_after(r->b, after_ns, cal);
+ return a < b ? a : b;
+ }
+ case EL_RHYTHM_SESSION_START:
+ case EL_RHYTHM_EVENT:
+ default:
+ return after_ns;
+ }
+}
+
+el_val_t rhythm_next_after(el_val_t r_val, el_val_t after_val, el_val_t cal_val) {
+ if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return after_val;
+ el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val;
+ el_calendar_t* c = _el_resolve_cal(cal_val);
+ int64_t out = _el_next_after(r, (int64_t)after_val, c);
+ return (el_val_t)out;
+}
+
+el_val_t rhythm_matches(el_val_t r_val, el_val_t ct_val) {
+ if (!el_is_magic(r_val, EL_RHYTHM_MAGIC)) return (el_val_t)0;
+ if (!el_is_magic(ct_val, EL_CALTIME_MAGIC)) return (el_val_t)0;
+ el_rhythm_t* r = (el_rhythm_t*)(uintptr_t)r_val;
+ el_caltime_t* ct = (el_caltime_t*)(uintptr_t)ct_val;
+ int64_t period = ct->cal->cycle_period_ns > 0 ? ct->cal->cycle_period_ns : EL_EARTH_DAY_NS;
+ int64_t base = ct->instant_ns - ct->cal->epoch_ns;
+ int64_t phase_ns = base % period;
+ if (phase_ns < 0) phase_ns += period;
+ double phase = (double)phase_ns / (double)period;
+ switch (r->kind) {
+ case EL_RHYTHM_CYCLE_START: return (el_val_t)(phase_ns == 0 ? 1 : 0);
+ case EL_RHYTHM_CYCLE_PHASE: {
+ double diff = phase - r->phase;
+ if (diff < 0) diff = -diff;
+ return (el_val_t)(diff < 0.001 ? 1 : 0);
+ }
+ default: return (el_val_t)0;
+ }
+}
+
/* ── UUID v4 ─────────────────────────────────────────────────────────────── */
static int _el_uuid_seeded = 0;
@@ -3933,6 +4813,376 @@ el_val_t str_format(el_val_t fmt, el_val_t data) {
el_val_t str_lower(el_val_t s) { return str_to_lower(s); }
el_val_t str_upper(el_val_t s) { return str_to_upper(s); }
+/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
+ *
+ * Phase 1 covers the operations every text-handling caller used to roll by
+ * hand on top of str_index_of + str_slice. The character-class predicates
+ * (is_letter / is_digit / ...) are ASCII only — Unicode-grapheme awareness,
+ * NFC/NFD normalization, and regex are Phase 2. Single-char input checks the
+ * first byte; multi-char input requires ALL bytes to match (false otherwise).
+ *
+ * Counting:
+ * str_count non-overlapping occurrences of sub in s
+ * str_count_chars codepoint count (UTF-8 leading-byte count)
+ * str_count_bytes explicit byte length (alias of str_len)
+ * str_count_lines \n-delimited line count (\r\n folded to \n)
+ * str_count_words whitespace-delimited tokens, non-empty only
+ * str_count_letters ASCII [A-Za-z]
+ * str_count_digits ASCII [0-9]
+ *
+ * Find / position:
+ * str_index_of_all all byte offsets of sub, [] if none
+ * str_last_index_of last byte offset of sub, -1 if not found
+ * str_find_chars first index of any char in any_of, -1 if none
+ *
+ * Transform:
+ * str_repeat s * n (non-negative)
+ * str_reverse codepoint-reversed (NOT grapheme-aware)
+ * str_strip_prefix s without prefix if present, else s
+ * str_strip_suffix s without suffix if present, else s
+ * str_strip_chars strip leading+trailing chars matching any in chars
+ * str_lstrip strip leading whitespace
+ * str_rstrip strip trailing whitespace
+ *
+ * Char classification (Bool):
+ * is_letter, is_digit, is_alphanumeric, is_whitespace,
+ * is_punctuation, is_uppercase, is_lowercase
+ *
+ * Splitting:
+ * str_split_lines \n-delimited (\r\n folded). Trailing empty dropped.
+ * str_split_chars alias of native_string_chars in str_ namespace
+ * str_split_n split into at most n parts (last part keeps the
+ * rest verbatim, including any further separators)
+ *
+ * Joining:
+ * str_join [String] -> String, sep between elements
+ */
+
+/* Count non-overlapping occurrences of sub in s. Empty sub returns 0. */
+el_val_t str_count(el_val_t sv, el_val_t subv) {
+ const char* s = EL_CSTR(sv);
+ const char* sub = EL_CSTR(subv);
+ if (!s || !sub || !*sub) return 0;
+ size_t lp = strlen(sub);
+ int64_t count = 0;
+ const char* p = s;
+ while ((p = strstr(p, sub)) != NULL) {
+ count++;
+ p += lp; /* non-overlapping advance */
+ }
+ return (el_val_t)count;
+}
+
+/* Codepoint count: walk bytes, count those NOT matching 10xxxxxx. */
+el_val_t str_count_chars(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return 0;
+ int64_t count = 0;
+ for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
+ if ((*p & 0xC0) != 0x80) count++;
+ }
+ return (el_val_t)count;
+}
+
+el_val_t str_count_bytes(el_val_t sv) {
+ return str_len(sv);
+}
+
+el_val_t str_count_lines(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s || !*s) return 0;
+ int64_t count = 0;
+ int has_content = 0;
+ for (const char* p = s; *p; p++) {
+ has_content = 1;
+ if (*p == '\n') {
+ count++;
+ has_content = 0; /* the \n closed the line */
+ }
+ }
+ if (has_content) count++; /* trailing line with no terminator */
+ return (el_val_t)count;
+}
+
+el_val_t str_count_words(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return 0;
+ int64_t count = 0;
+ int in_word = 0;
+ for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
+ if (isspace(*p)) {
+ in_word = 0;
+ } else if (!in_word) {
+ in_word = 1;
+ count++;
+ }
+ }
+ return (el_val_t)count;
+}
+
+el_val_t str_count_letters(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return 0;
+ int64_t count = 0;
+ for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
+ if ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z')) count++;
+ }
+ return (el_val_t)count;
+}
+
+el_val_t str_count_digits(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return 0;
+ int64_t count = 0;
+ for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
+ if (*p >= '0' && *p <= '9') count++;
+ }
+ return (el_val_t)count;
+}
+
+el_val_t str_index_of_all(el_val_t sv, el_val_t subv) {
+ const char* s = EL_CSTR(sv);
+ const char* sub = EL_CSTR(subv);
+ el_val_t lst = el_list_empty();
+ if (!s || !sub || !*sub) return lst;
+ size_t lp = strlen(sub);
+ const char* p = s;
+ const char* hit;
+ while ((hit = strstr(p, sub)) != NULL) {
+ lst = el_list_append(lst, (el_val_t)(int64_t)(hit - s));
+ p = hit + lp;
+ }
+ return lst;
+}
+
+el_val_t str_last_index_of(el_val_t sv, el_val_t subv) {
+ const char* s = EL_CSTR(sv);
+ const char* sub = EL_CSTR(subv);
+ if (!s || !sub || !*sub) return -1;
+ size_t lp = strlen(sub);
+ int64_t last = -1;
+ const char* p = s;
+ const char* hit;
+ while ((hit = strstr(p, sub)) != NULL) {
+ last = (int64_t)(hit - s);
+ p = hit + lp;
+ }
+ return (el_val_t)last;
+}
+
+el_val_t str_find_chars(el_val_t sv, el_val_t any_of_v) {
+ const char* s = EL_CSTR(sv);
+ const char* any = EL_CSTR(any_of_v);
+ if (!s || !any || !*any) return -1;
+ for (const char* p = s; *p; p++) {
+ if (strchr(any, *p)) return (el_val_t)(int64_t)(p - s);
+ }
+ return -1;
+}
+
+el_val_t str_repeat(el_val_t sv, el_val_t nv) {
+ const char* s = EL_CSTR(sv);
+ int64_t n = (int64_t)nv;
+ if (!s || n <= 0) return el_wrap_str(el_strdup(""));
+ size_t ls = strlen(s);
+ if (ls == 0) return el_wrap_str(el_strdup(""));
+ size_t total = ls * (size_t)n;
+ char* out = el_strbuf(total);
+ for (int64_t i = 0; i < n; i++) {
+ memcpy(out + i * ls, s, ls);
+ }
+ out[total] = '\0';
+ return el_wrap_str(out);
+}
+
+/* Reverse by codepoint: walk codepoints, copy each backwards into the output.
+ * NOT grapheme-aware (Phase 2). Combining marks attached to a base codepoint
+ * will detach. ASCII strings are byte-reverse equivalent. */
+el_val_t str_reverse(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return el_wrap_str(el_strdup(""));
+ size_t n = strlen(s);
+ char* out = el_strbuf(n);
+ /* Walk forward, find each codepoint's byte length, then copy from the end. */
+ size_t out_pos = n;
+ const unsigned char* p = (const unsigned char*)s;
+ while (*p) {
+ int cp_len;
+ if ((*p & 0x80) == 0x00) cp_len = 1;
+ else if ((*p & 0xE0) == 0xC0) cp_len = 2;
+ else if ((*p & 0xF0) == 0xE0) cp_len = 3;
+ else if ((*p & 0xF8) == 0xF0) cp_len = 4;
+ else cp_len = 1; /* invalid byte: passthrough */
+ out_pos -= cp_len;
+ memcpy(out + out_pos, p, cp_len);
+ p += cp_len;
+ }
+ out[n] = '\0';
+ return el_wrap_str(out);
+}
+
+el_val_t str_strip_prefix(el_val_t sv, el_val_t prefv) {
+ const char* s = EL_CSTR(sv);
+ const char* pref = EL_CSTR(prefv);
+ if (!s) return el_wrap_str(el_strdup(""));
+ if (!pref || !*pref) return el_wrap_str(el_strdup(s));
+ size_t lp = strlen(pref);
+ size_t ls = strlen(s);
+ if (lp <= ls && strncmp(s, pref, lp) == 0) {
+ char* out = el_strbuf(ls - lp);
+ memcpy(out, s + lp, ls - lp);
+ out[ls - lp] = '\0';
+ return el_wrap_str(out);
+ }
+ return el_wrap_str(el_strdup(s));
+}
+
+el_val_t str_strip_suffix(el_val_t sv, el_val_t sufv) {
+ const char* s = EL_CSTR(sv);
+ const char* suf = EL_CSTR(sufv);
+ if (!s) return el_wrap_str(el_strdup(""));
+ if (!suf || !*suf) return el_wrap_str(el_strdup(s));
+ size_t ls = strlen(s);
+ size_t lsuf = strlen(suf);
+ if (lsuf <= ls && strcmp(s + ls - lsuf, suf) == 0) {
+ char* out = el_strbuf(ls - lsuf);
+ memcpy(out, s, ls - lsuf);
+ out[ls - lsuf] = '\0';
+ return el_wrap_str(out);
+ }
+ return el_wrap_str(el_strdup(s));
+}
+
+el_val_t str_strip_chars(el_val_t sv, el_val_t charsv) {
+ const char* s = EL_CSTR(sv);
+ const char* chars = EL_CSTR(charsv);
+ if (!s) return el_wrap_str(el_strdup(""));
+ if (!chars || !*chars) return el_wrap_str(el_strdup(s));
+ const char* start = s;
+ while (*start && strchr(chars, *start)) start++;
+ size_t n = strlen(start);
+ while (n > 0 && strchr(chars, start[n - 1])) n--;
+ char* out = el_strbuf(n);
+ memcpy(out, start, n);
+ out[n] = '\0';
+ return el_wrap_str(out);
+}
+
+el_val_t str_lstrip(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return el_wrap_str(el_strdup(""));
+ while (*s && isspace((unsigned char)*s)) s++;
+ return el_wrap_str(el_strdup(s));
+}
+
+el_val_t str_rstrip(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ if (!s) return el_wrap_str(el_strdup(""));
+ size_t n = strlen(s);
+ while (n > 0 && isspace((unsigned char)s[n - 1])) n--;
+ char* out = el_strbuf(n);
+ memcpy(out, s, n);
+ out[n] = '\0';
+ return el_wrap_str(out);
+}
+
+/* Character classification.
+ * Empty input returns false. Multi-char input requires ALL bytes to match.
+ * ASCII range only; Phase 2 will widen to Unicode. */
+static int s_all_match(el_val_t sv, int (*pred)(unsigned char)) {
+ const char* s = EL_CSTR(sv);
+ if (!s || !*s) return 0;
+ for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
+ if (!pred(*p)) return 0;
+ }
+ return 1;
+}
+
+static int p_letter(unsigned char c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); }
+static int p_digit(unsigned char c) { return c >= '0' && c <= '9'; }
+static int p_alnum(unsigned char c) { return p_letter(c) || p_digit(c); }
+static int p_white(unsigned char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; }
+static int p_punct(unsigned char c) { return ispunct(c) ? 1 : 0; }
+static int p_upper(unsigned char c) { return c >= 'A' && c <= 'Z'; }
+static int p_lower(unsigned char c) { return c >= 'a' && c <= 'z'; }
+
+el_val_t is_letter(el_val_t s) { return (el_val_t)s_all_match(s, p_letter); }
+el_val_t is_digit(el_val_t s) { return (el_val_t)s_all_match(s, p_digit); }
+el_val_t is_alphanumeric(el_val_t s) { return (el_val_t)s_all_match(s, p_alnum); }
+el_val_t is_whitespace(el_val_t s) { return (el_val_t)s_all_match(s, p_white); }
+el_val_t is_punctuation(el_val_t s) { return (el_val_t)s_all_match(s, p_punct); }
+el_val_t is_uppercase(el_val_t s) { return (el_val_t)s_all_match(s, p_upper); }
+el_val_t is_lowercase(el_val_t s) { return (el_val_t)s_all_match(s, p_lower); }
+
+/* Split on \n. \r\n is folded to \n first. Trailing empty after final \n
+ * is dropped (so "a\nb\n" -> ["a", "b"], not ["a", "b", ""]). */
+el_val_t str_split_lines(el_val_t sv) {
+ const char* s = EL_CSTR(sv);
+ el_val_t lst = el_list_empty();
+ if (!s) return lst;
+ size_t n = strlen(s);
+ /* Pre-scan: build into a normalized buffer with \r\n folded. */
+ const char* line_start = s;
+ for (size_t i = 0; i <= n; i++) {
+ if (s[i] == '\n' || s[i] == '\0') {
+ size_t len = (size_t)(s + i - line_start);
+ /* Drop trailing \r if this was \r\n. */
+ if (len > 0 && line_start[len - 1] == '\r') len--;
+ /* Drop final trailing-empty-after-newline. */
+ if (s[i] == '\0' && len == 0 && i > 0 && s[i - 1] == '\n') break;
+ char* out = el_strbuf(len);
+ memcpy(out, line_start, len);
+ out[len] = '\0';
+ lst = el_list_append(lst, el_wrap_str(out));
+ if (s[i] == '\0') break;
+ line_start = s + i + 1;
+ }
+ }
+ return lst;
+}
+
+el_val_t str_split_chars(el_val_t s) {
+ return native_string_chars(s);
+}
+
+/* Split into at most n parts. The (n-1)th split point is the LAST split;
+ * after it, the remainder is appended verbatim including any further
+ * separators. n <= 0 returns an empty list. n == 1 returns [s]. */
+el_val_t str_split_n(el_val_t sv, el_val_t sepv, el_val_t nv) {
+ const char* s = EL_CSTR(sv);
+ const char* sep = EL_CSTR(sepv);
+ int64_t n = (int64_t)nv;
+ el_val_t lst = el_list_empty();
+ if (!s) return lst;
+ if (n <= 0) return lst;
+ if (n == 1 || !sep || !*sep) {
+ lst = el_list_append(lst, el_wrap_str(el_strdup(s)));
+ return lst;
+ }
+ size_t lp = strlen(sep);
+ const char* p = s;
+ int64_t parts = 0;
+ const char* hit;
+ while (parts < n - 1 && (hit = strstr(p, sep)) != NULL) {
+ size_t len = (size_t)(hit - p);
+ char* out = el_strbuf(len);
+ memcpy(out, p, len);
+ out[len] = '\0';
+ lst = el_list_append(lst, el_wrap_str(out));
+ p = hit + lp;
+ parts++;
+ }
+ /* Remainder verbatim. */
+ lst = el_list_append(lst, el_wrap_str(el_strdup(p)));
+ return lst;
+}
+
+/* Join a [String] with a separator. Empty list -> "". Single-element ->
+ * that element. Non-string elements are stringified via int_to_str. */
+el_val_t str_join(el_val_t listv, el_val_t sepv) {
+ return list_join(listv, sepv);
+}
+
/* ── List additions ──────────────────────────────────────────────────────── */
el_val_t list_push(el_val_t list, el_val_t elem) {
diff --git a/runtime/el_runtime.h b/runtime/el_runtime.h
index 6939d01..68bae8b 100644
--- a/runtime/el_runtime.h
+++ b/runtime/el_runtime.h
@@ -316,6 +316,89 @@ el_val_t ttl_cache_set(el_val_t key, el_val_t value);
el_val_t ttl_cache_get(el_val_t key, el_val_t max_age);
el_val_t ttl_cache_age(el_val_t key);
+/* ── Calendar + CalendarTime + Rhythm + LocalDate/Time/DateTime ─────────────
+ * Phase 1.5 of the time system. Calendar is pluggable: EarthCalendar (IANA
+ * zones, Gregorian, DST) is the user-facing default; MarsCalendar,
+ * CycleCalendar(period), NoCycleCalendar, RelativeCalendar handle non-Earth
+ * domains.
+ *
+ * A Calendar interprets an Instant under a particular cycle convention and
+ * produces a CalendarTime. CalendarTime carries the underlying Instant and
+ * a back-pointer to its Calendar; arithmetic and formatting consult the
+ * Calendar to convert ns since epoch into year/month/day/hour/minute/second
+ * (or sol/phase, or cycle/phase, depending on kind).
+ *
+ * Storage convention: Calendar / CalendarTime / Rhythm / LocalDate /
+ * LocalDateTime are heap-allocated structs whose pointers are cast into
+ * el_val_t. A 24-bit magic header at offset 0 lets the runtime identify
+ * the kind safely. LocalTime is small enough to live in the int64 slot
+ * directly (nanos since midnight, signed). */
+
+/* Zone — opaque IANA zone or fixed offset, used by EarthCalendar.
+ * `zone_id` is either an IANA name ("America/New_York", "UTC") or a fixed
+ * offset string ("+05:30", "-08:00"). The runtime resolves it via tzset()
+ * on first use of the owning EarthCalendar. */
+el_val_t zone(el_val_t id);
+el_val_t zone_utc(void);
+el_val_t zone_local(void);
+el_val_t zone_offset(el_val_t hours, el_val_t minutes);
+
+/* Calendar constructors. Each returns an el_val_t pointer to a heap-
+ * allocated, magic-tagged Calendar struct. Calendars are interned by
+ * (kind, zone_id, period_ns, epoch_ns) so identical constructors return
+ * the same pointer — equality is reference equality. */
+el_val_t earth_calendar(el_val_t z);
+el_val_t earth_calendar_default(void);
+el_val_t mars_calendar(void);
+el_val_t cycle_calendar(el_val_t period_dur);
+el_val_t no_cycle_calendar(void);
+el_val_t relative_calendar(el_val_t epoch_inst);
+
+/* CalendarTime constructors and methods. Returns a heap-allocated struct
+ * whose pointer fits in el_val_t. */
+el_val_t now_in(el_val_t cal);
+el_val_t in_calendar(el_val_t inst, el_val_t cal);
+el_val_t cal_format(el_val_t ct, el_val_t pattern);
+el_val_t cal_to_instant(el_val_t ct);
+el_val_t cal_cycle_phase(el_val_t ct);
+el_val_t cal_in(el_val_t ct, el_val_t cal);
+
+/* LocalDate / LocalTime / LocalDateTime — calendar-agnostic value types.
+ * LocalTime carries nanoseconds since midnight as a signed int64 directly
+ * in the el_val_t slot (no allocation). LocalDate / LocalDateTime are
+ * heap-allocated structs with magic headers. */
+el_val_t local_date(el_val_t y, el_val_t m, el_val_t d);
+el_val_t local_time(el_val_t h, el_val_t m, el_val_t s, el_val_t ns);
+el_val_t local_datetime(el_val_t date, el_val_t time);
+el_val_t zoned(el_val_t date, el_val_t time, el_val_t cal);
+
+el_val_t local_date_year(el_val_t ld);
+el_val_t local_date_month(el_val_t ld);
+el_val_t local_date_day(el_val_t ld);
+el_val_t local_time_hour(el_val_t lt);
+el_val_t local_time_minute(el_val_t lt);
+el_val_t local_time_second(el_val_t lt);
+el_val_t local_time_nanos(el_val_t lt);
+
+el_val_t el_local_date_add_dur(el_val_t ld, el_val_t dur);
+el_val_t el_local_time_add_dur(el_val_t lt, el_val_t dur);
+el_val_t el_local_date_lt(el_val_t a, el_val_t b);
+el_val_t el_local_date_eq(el_val_t a, el_val_t b);
+
+/* Rhythm — pluggable recurrence AST. Returns a heap-allocated struct
+ * pointer in el_val_t; rhythms are immutable so callers may share them. */
+el_val_t rhythm_cycle_start(void);
+el_val_t rhythm_cycle_phase(el_val_t phase);
+el_val_t rhythm_duration(el_val_t d);
+el_val_t rhythm_session_start(void);
+el_val_t rhythm_event(el_val_t name);
+el_val_t rhythm_and(el_val_t a, el_val_t b);
+el_val_t rhythm_or(el_val_t a, el_val_t b);
+el_val_t rhythm_weekday(el_val_t day);
+el_val_t rhythm_weekly_at(el_val_t day, el_val_t hour, el_val_t minute);
+el_val_t rhythm_next_after(el_val_t r, el_val_t after, el_val_t cal);
+el_val_t rhythm_matches(el_val_t r, el_val_t ct);
+
/* ── UUID ────────────────────────────────────────────────────────────────── */
el_val_t uuid_new(void);
@@ -362,6 +445,49 @@ el_val_t str_format(el_val_t fmt, el_val_t data);
el_val_t str_lower(el_val_t s);
el_val_t str_upper(el_val_t s);
+/* ── Text-processing primitives (Phase 1: byte/codepoint, ASCII char classes)
+ * Phase 2 (filed): Unicode-grapheme awareness, NFC/NFD normalization, regex.
+ * is_* predicates: empty input returns false; multi-char requires ALL bytes
+ * to match. ASCII ranges only in Phase 1. */
+
+/* Counting */
+el_val_t str_count(el_val_t s, el_val_t sub); /* non-overlapping */
+el_val_t str_count_chars(el_val_t s); /* codepoint count */
+el_val_t str_count_bytes(el_val_t s); /* alias of str_len */
+el_val_t str_count_lines(el_val_t s);
+el_val_t str_count_words(el_val_t s);
+el_val_t str_count_letters(el_val_t s); /* ASCII [A-Za-z] */
+el_val_t str_count_digits(el_val_t s); /* ASCII [0-9] */
+
+/* Find / position */
+el_val_t str_index_of_all(el_val_t s, el_val_t sub); /* [Int] of byte offsets */
+el_val_t str_last_index_of(el_val_t s, el_val_t sub);
+el_val_t str_find_chars(el_val_t s, el_val_t any_of); /* first idx of any ch */
+
+/* Transform */
+el_val_t str_repeat(el_val_t s, el_val_t n);
+el_val_t str_reverse(el_val_t s); /* by codepoint */
+el_val_t str_strip_prefix(el_val_t s, el_val_t prefix);
+el_val_t str_strip_suffix(el_val_t s, el_val_t suffix);
+el_val_t str_strip_chars(el_val_t s, el_val_t chars);
+el_val_t str_lstrip(el_val_t s);
+el_val_t str_rstrip(el_val_t s);
+
+/* Char classification (Bool) */
+el_val_t is_letter(el_val_t s);
+el_val_t is_digit(el_val_t s);
+el_val_t is_alphanumeric(el_val_t s);
+el_val_t is_whitespace(el_val_t s);
+el_val_t is_punctuation(el_val_t s);
+el_val_t is_uppercase(el_val_t s);
+el_val_t is_lowercase(el_val_t s);
+
+/* Split / join */
+el_val_t str_split_lines(el_val_t s);
+el_val_t str_split_chars(el_val_t s); /* alias of native_string_chars */
+el_val_t str_split_n(el_val_t s, el_val_t sep, el_val_t n);
+el_val_t str_join(el_val_t list, el_val_t sep); /* alias of list_join */
+
/* ── List additions ──────────────────────────────────────────────────────── */
el_val_t list_push(el_val_t list, el_val_t elem);
diff --git a/src/assets/js/7eac0621cbca.js b/src/assets/js/7eac0621cbca.js
new file mode 100644
index 0000000..3de7c54
--- /dev/null
+++ b/src/assets/js/7eac0621cbca.js
@@ -0,0 +1 @@
+(function(_0x21c579,_0x5b4e35){var _0x102698=a0_0x4681,_0x15dd70=_0x21c579();while(!![]){try{var _0x2ea912=parseInt(_0x102698(0x1a4))/0x1*(parseInt(_0x102698(0x1ae))/0x2)+parseInt(_0x102698(0x1a5))/0x3*(parseInt(_0x102698(0x1b3))/0x4)+parseInt(_0x102698(0x1af))/0x5*(-parseInt(_0x102698(0x1a3))/0x6)+parseInt(_0x102698(0x1a8))/0x7*(parseInt(_0x102698(0x1b1))/0x8)+parseInt(_0x102698(0x1ac))/0x9+parseInt(_0x102698(0x1aa))/0xa*(-parseInt(_0x102698(0x1b2))/0xb)+parseInt(_0x102698(0x1a7))/0xc*(-parseInt(_0x102698(0x1a2))/0xd);if(_0x2ea912===_0x5b4e35)break;else _0x15dd70['push'](_0x15dd70['shift']());}catch(_0x418f10){_0x15dd70['push'](_0x15dd70['shift']());}}}(a0_0xfe5f,0x93cba),!(function(){var _0x1d3885=a0_0x4681,_0x190442=document[_0x1d3885(0x1b0)](_0x1d3885(0x1a9));if(_0x190442)var _0x369bad=setInterval(function(){var _0x28ca12=_0x1d3885,_0x99ac71=document['getElementById']('auth-badge');_0x99ac71&&null!==_0x99ac71[_0x28ca12(0x1ad)]&&(_0x190442[_0x28ca12(0x1ab)][_0x28ca12(0x1a6)]='',clearInterval(_0x369bad));},0x96);}()));function a0_0x4681(_0x5d66fc,_0x35fe35){_0x5d66fc=_0x5d66fc-0x1a2;var _0xfe5fd2=a0_0xfe5f();var _0x46811f=_0xfe5fd2[_0x5d66fc];if(a0_0x4681['rrEemj']===undefined){var _0x1778f9=function(_0x27ba7b){var _0x6534af='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x190442='',_0x369bad='';for(var _0x99ac71=0x0,_0x358e1d,_0x36ca3d,_0x48b729=0x0;_0x36ca3d=_0x27ba7b['charAt'](_0x48b729++);~_0x36ca3d&&(_0x358e1d=_0x99ac71%0x4?_0x358e1d*0x40+_0x36ca3d:_0x36ca3d,_0x99ac71++%0x4)?_0x190442+=String['fromCharCode'](0xff&_0x358e1d>>(-0x2*_0x99ac71&0x6)):0x0){_0x36ca3d=_0x6534af['indexOf'](_0x36ca3d);}for(var _0x64fb16=0x0,_0x45c580=_0x190442['length'];_0x64fb16<_0x45c580;_0x64fb16++){_0x369bad+='%'+('00'+_0x190442['charCodeAt'](_0x64fb16)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x369bad);};a0_0x4681['GaaaEr']=_0x1778f9,a0_0x4681['NVuxND']={},a0_0x4681['rrEemj']=!![];}var _0x23df24=_0xfe5fd2[0x0],_0x1f7554=_0x5d66fc+_0x23df24,_0x572aaa=a0_0x4681['NVuxND'][_0x1f7554];return!_0x572aaa?(_0x46811f=a0_0x4681['GaaaEr'](_0x46811f),a0_0x4681['NVuxND'][_0x1f7554]=_0x46811f):_0x46811f=_0x572aaa,_0x46811f;}function a0_0xfe5f(){var _0x383267=['nJaXmtfoA2LKzxm','otuZmJG5t1fPqNnJ','zgLZCgXHEq','mJrRqMLIEuy','mZKYntm2ovfHDffRrG','Cgf5BwvUDc1Zzwn0Aw9U','mteWote0mejoqMTpzG','C3r5Bgu','ndiWmtyZmNz5rLbgtq','B2zMC2v0ugfYzw50','nNfdqLPpCG','nJK0mez0t0n1sG','z2v0rwXLBwvUDej5swq','oePpwhLbyG','mtfLz29cvfq','ogHSzxnxCa','ndK3mdGXyujMzxjk','ndu0mKrUANrrDa'];a0_0xfe5f=function(){return _0x383267;};return a0_0xfe5f();}
\ No newline at end of file
diff --git a/src/assets/js/manifest.json b/src/assets/js/manifest.json
index f663938..05f414f 100644
--- a/src/assets/js/manifest.json
+++ b/src/assets/js/manifest.json
@@ -10,6 +10,13 @@
"interpolated": [],
"note": "carried from prior run"
},
+ {
+ "file": "checkout.el",
+ "hash": "7eac0621cbca",
+ "asset": "/assets/js/7eac0621cbca.js",
+ "size": 2583,
+ "interpolated": []
+ },
{
"file": "checkout.el",
"hash": "db455e1671dd",
@@ -50,14 +57,6 @@
"interpolated": [],
"note": "carried from prior run"
},
- {
- "file": "gallery.el",
- "hash": "d8251f5e5aa1",
- "asset": "/assets/js/d8251f5e5aa1.js",
- "size": 12354,
- "interpolated": [],
- "note": "carried from prior run"
- },
{
"file": "main.el",
"hash": "94727a87c328",
@@ -87,7 +86,8 @@
"hash": "37b5ead0d425",
"asset": "/assets/js/37b5ead0d425.js",
"size": 23539,
- "interpolated": []
+ "interpolated": [],
+ "note": "carried from prior run"
},
{
"file": "styles.el",
diff --git a/src/checkout.el b/src/checkout.el
index d101928..de81a77 100644
--- a/src/checkout.el
+++ b/src/checkout.el
@@ -490,21 +490,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
" + (if is_free { "
-
+
" } else { "" }) + "
"
}