htui 0.2.0

A terminal HTML/CSS renderer with an embedded JavaScript engine — build interactive TUIs from HTML, CSS and JS
(function () {
  // Registry globals the Rust side reads and GC-roots.
  globalThis.__htuiListeners = {};
  globalThis.__htuiElements = {};
  globalThis.__htuiTimers = {};
  globalThis.__htuiFetchResolvers = {};
  globalThis.__htuiClassListCache = {};
  globalThis.__htuiDatasetCache = {};
  // Every MediaQueryList `window.matchMedia` returned — re-evaluated on
  // resize so `change` fires on flips (see `evaluate_media_changes`).
  globalThis.__htuiMQLs = [];

  // Canonical array-index property key: `'0'`, `'1'`, ... map to their
  // number; everything else (symbols, '01', '1.0', '-0', 'length') is NaN.
  // Shared by the live NodeList and DOMTokenList proxies below.
  function index(p) {
    if (typeof p !== 'string') return NaN;
    var n = Number(p);
    return String(n) === p ? n : NaN;
  }

  var PROTO_OWN = Object.getOwnPropertyNames(Object.prototype);
  function isProtoOwn(p) {
    return PROTO_OWN.indexOf(p) !== -1;
  }

  // XML Name production (minus ':'): what the HTML spec requires of a
  // dataset candidate name before it may become a `data-*` attribute.
  var NAME_REST = /^[A-Za-z_\u0080-\uFFFF][A-Za-z0-9_.\-\u0080-\uFFFF]*$/;
  function isValidName(name) {
    return NAME_REST.test(name);
  }

  globalThis.__htuiAddListener = function (nodeId, type, fn) {
    var entry = globalThis.__htuiListeners[nodeId] || (globalThis.__htuiListeners[nodeId] = {});
    var arr = entry[type] || (entry[type] = []);
    if (arr.indexOf(fn) === -1) { arr.push(fn); }
  };
  globalThis.__htuiGetListeners = function (nodeId, type) {
    var entry = globalThis.__htuiListeners[nodeId];
    return entry ? (entry[type] || []) : [];
  };
  globalThis.__htuiClearAllListeners = function (nodeId) {
    delete globalThis.__htuiListeners[nodeId];
  };

  // Single-slot registry for `on*` attribute handlers. Unlike the multicast
  // listener list above, each (node, type) holds at most one function - a
  // browser event-handler slot: installing overwrites, clearing removes.
  globalThis.__htuiAttrHandlers = {};
  globalThis.__htuiSetAttrHandler = function (nodeId, type, fn) {
    var entry = globalThis.__htuiAttrHandlers[nodeId] || (globalThis.__htuiAttrHandlers[nodeId] = {});
    entry[type] = fn;
  };
  globalThis.__htuiClearAttrHandler = function (nodeId, type) {
    var entry = globalThis.__htuiAttrHandlers[nodeId];
    if (entry) { delete entry[type]; }
  };
  globalThis.__htuiClearAllAttrHandlers = function (nodeId) {
    delete globalThis.__htuiAttrHandlers[nodeId];
  };

  // Live NodeList factory: `getLen()` / `getItem(i)` are native closures over
  // the engine arena, so every access (length / index / enumeration) reflects
  // the current DOM - a held `childNodes` reference stays live.
  globalThis.__htuiLiveList = function (getLen, getItem) {
    var target = {
      item: function (i) {
        i = Number(i) | 0;
        return (i >= 0 && i < getLen()) ? getItem(i) : null;
      },
      forEach: function (cb, self) {
        for (var i = 0; i < getLen(); i++) cb.call(self, getItem(i), i, this);
      }
    };
    return new Proxy(target, {
      get: function (t, p) {
        if (p === 'length') return getLen();
        var i = index(p);
        if (i >= 0 && i < getLen()) return getItem(i);
        if (p === Symbol.iterator) return function () {
          var i = 0;
          return { next: function () {
            return i < getLen()
              ? { value: getItem(i++), done: false }
              : { value: undefined, done: true };
          } };
        };
        return Reflect.get(t, p);
      },
      has: function (t, p) {
        if (p === 'length') return true;
        var i = index(p);
        if (i >= 0 && i < getLen()) return true;
        return Reflect.has(t, p);
      },
      ownKeys: function () {
        var keys = [];
        for (var i = 0; i < getLen(); i++) keys.push(String(i));
        keys.push('length');
        return keys;
      },
      getOwnPropertyDescriptor: function (t, p) {
        if (p === 'length') return { get: getLen, enumerable: false, configurable: true };
        var i = index(p);
        if (i >= 0 && i < getLen()) {
          return { value: getItem(i), writable: true, enumerable: true, configurable: true };
        }
        return Reflect.getOwnPropertyDescriptor(t, p);
      },
      set: function () { return false; },
      deleteProperty: function () { return false; }
    });
  };

  // Live DOMStringMap factory for `el.dataset`. `get` / `set` / `remove` /
  // `keys` are native closures over the engine arena (the element's `data-*`
  // attributes), so every access reflects the current DOM - a held `dataset`
  // reference stays live. Property names use the browser camelCase<->kebab-case
  // mapping (`dataset.fooBar` <-> `data-foo-bar`); assignment stringifies the
  globalThis.__htuiDataset = function (get, set, remove, keys) {
    function kebab(p) {
      return String(p).replace(/[A-Z]/g, function (c) { return '-' + c.toLowerCase(); });
    }
    var target = {
      toString: function () { return '[object DOMStringMap]'; }
    };
    return new Proxy(target, {
      get: function (t, p) {
        if (typeof p === 'symbol' || isProtoOwn(p)) return Reflect.get(t, p);
        return get(kebab(p));
      },
      set: function (t, p, v) {
        if (typeof p === 'symbol' || isProtoOwn(p)) return Reflect.set(t, p, v);
        var k = kebab(p);
        // The transformed candidate must be a valid name or the setter
        // throws (browser: `dataset['a b'] = 1` -> SyntaxError).
        if (!isValidName(k)) {
          throw new SyntaxError("'" + p + "' is not a valid name.");
        }
        // WebIDL `DOMString` conversion: null becomes "null", undefined
        // becomes "undefined". Assignment never removes the attribute - an
        // empty string sets an empty value; `delete dataset.key` removes.
        set(k, String(v));
        return true;
      },
      has: function (t, p) {
        if (typeof p === 'symbol' || isProtoOwn(p)) return Reflect.has(t, p);
        return get(kebab(p)) !== undefined;
      },
      deleteProperty: function (t, p) {
        if (typeof p === 'symbol') return Reflect.deleteProperty(t, p);
        if (isProtoOwn(p)) return true;
        var k = kebab(p);
        // The deleter silently ignores invalid candidate names (browser
        // parity: no throw, no removal).
        if (!isValidName(k)) return true;
        remove(k);
        return true;
      },
      ownKeys: function () {
        return keys();
      },
      getOwnPropertyDescriptor: function (t, p) {
        if (typeof p !== 'string' || isProtoOwn(p)) return undefined;
        var v = get(kebab(p));
        if (v === undefined) return undefined;
        return { value: v, writable: true, enumerable: true, configurable: true };
      }
    });
  };

  // Live DOMTokenList factory for `el.classList`. `getAttr()` / `setAttr()`
  // are native closures over the engine arena (the element's `class`
  // attribute), so every access reflects the current DOM - a held `classList`
  // reference stays live. Mirrors the browser semantics: token order and first
  // occurrences are preserved, empty / whitespace-containing tokens are
  // rejected (`SyntaxError`), and add / remove / toggle / replace only rewrite
  // the attribute when the token set actually changed.
  globalThis.__htuiClassList = function (getAttr, setAttr) {
    // Token list memoized
    var cacheRaw = null;
    var cacheTokens = [];
    function tokens() {
      var raw = getAttr();
      if (raw !== cacheRaw) {
        cacheRaw = raw;
        cacheTokens = raw ? raw.split(/\s+/).filter(Boolean) : [];
      }
      return cacheTokens;
    }
    function write(list) {
      setAttr(list.join(' '));
    }
    function check(token) {
      if (token === '') throw new SyntaxError('The token provided must not be empty.');
      if (/[ \t\n\f\r]/.test(token)) {
        throw new SyntaxError('The token provided contains HTML space characters.');
      }
    }
    function makeIter(step) {
      var it = { next: step };
      it[Symbol.iterator] = function () { return it; };
      return it;
    }
    var target = {
      item: function (i) {
        var t = tokens();
        i = Number(i) | 0;
        return (i >= 0 && i < t.length) ? t[i] : null;
      },
      contains: function (token) {
        token = String(token);
        check(token);
        return tokens().indexOf(token) !== -1;
      },
      add: function () {
        var t = tokens();
        var changed = false;
        for (var i = 0; i < arguments.length; i++) {
          var tok = String(arguments[i]);
          check(tok);
          if (t.indexOf(tok) === -1) { t.push(tok); changed = true; }
        }
        if (changed) write(t);
      },
      remove: function () {
        var t = tokens();
        var changed = false;
        for (var i = 0; i < arguments.length; i++) {
          var tok = String(arguments[i]);
          check(tok);
          var p = t.indexOf(tok);
          if (p !== -1) { t.splice(p, 1); changed = true; }
        }
        if (changed) write(t);
      },
      toggle: function (token, force) {
        token = String(token);
        check(token);
        var t = tokens();
        var p = t.indexOf(token);
        var present = p !== -1;
        if (force === undefined) {
          if (present) t.splice(p, 1); else t.push(token);
          write(t);
          return !present;
        }
        if (force) {
          if (!present) { t.push(token); write(t); }
          return true;
        }
        if (present) { t.splice(p, 1); write(t); }
        return false;
      },
      replace: function (oldToken, newToken) {
        oldToken = String(oldToken);
        newToken = String(newToken);
        check(oldToken);
        check(newToken);
        var t = tokens();
        var p = t.indexOf(oldToken);
        if (p === -1) return false;
        t.splice(p, 1);
        if (t.indexOf(newToken) === -1) t.push(newToken);
        write(t);
        return true;
      },
      forEach: function (cb, self) {
        var t = tokens();
        for (var i = 0; i < t.length; i++) cb.call(self, t[i], i, this);
      },
      toString: function () {
        return getAttr() || '';
      },
      entries: function () {
        var i = 0;
        var t = tokens();
        return makeIter(function () {
          return i < t.length ? { value: [i, t[i++]], done: false } : { value: undefined, done: true };
        });
      },
      keys: function () {
        var i = 0;
        var t = tokens();
        return makeIter(function () {
          return i < t.length ? { value: i++, done: false } : { value: undefined, done: true };
        });
      },
      values: function () {
        var i = 0;
        var t = tokens();
        return makeIter(function () {
          return i < t.length ? { value: t[i++], done: false } : { value: undefined, done: true };
        });
      }
    };
    target[Symbol.iterator] = target.values;
    // `value` reads / writes the underlying `class` attribute (configurable so
    // the proxy's `ownKeys` can omit it without violating the invariant).
    Object.defineProperty(target, 'value', {
      get: function () { return getAttr() || ''; },
      set: function (v) { setAttr(String(v)); },
      enumerable: false,
      configurable: true
    });
    return new Proxy(target, {
      get: function (t, p) {
        if (p === 'length') return tokens().length;
        var i = index(p);
        if (i >= 0) {
          var tk = tokens();
          if (i < tk.length) return tk[i];
        }
        return Reflect.get(t, p);
      },
      has: function (t, p) {
        if (p === 'length') return true;
        var i = index(p);
        return i >= 0 ? i < tokens().length : Reflect.has(t, p);
      },
      ownKeys: function () {
        var keys = [];
        var t = tokens();
        for (var i = 0; i < t.length; i++) keys.push(String(i));
        keys.push('length');
        return keys;
      },
      getOwnPropertyDescriptor: function (t, p) {
        if (p === 'length') return { get: function () { return tokens().length; }, enumerable: false, configurable: true };
        var i = index(p);
        if (i >= 0) {
          var tk = tokens();
          if (i < tk.length) {
            return { value: tk[i], writable: true, enumerable: true, configurable: true };
          }
        }
        return Reflect.getOwnPropertyDescriptor(t, p);
      },
      set: function (t, p, v) {
        // `value` is the only writable property (the `class` attribute);
        // everything else (indexed keys, methods, `length`) is read-only.
        if (p === 'value') return Reflect.set(t, 'value', v);
        return false;
      },
      deleteProperty: function () { return false; }
    });
  };
})();