/*
  PmLib: JavaScript utility library for PmWiki
  (c) 2009-2026 Petko Yotov www.pmwiki.org/petko
  Licensed GNU GPLv2 or any more recent version released by the FSF.
  
  These helper functions can be reused by recipes / extensions.
  
  They can be imported in your function, call:
  const { W, D, H, echo, Now, q, Q, ue, PHSC, ga, sa, sp, dce,
    css, min, max, minmax, ceil, floor, rnd, pf, floatval, intval,
    average, zpad, ls, jP, jS, getConf, on, off, fire, tap, pd,
    adj, posy, isVisible, isHidden, abspos,
    gcs, gbcr, preferdark, afetch, ready } = PmLib;
  
  For readability, import only those that you actually use.
  
  $Id: pmwiki-lib.js 5527 2026-07-12 13:23:32Z petko $
*/

var PmLib = (function() {
  const has = Object.prototype.hasOwnProperty;
  const unsafe = ['__proto__', 'constructor', 'prototype'];
  
  var lib = {
    __proto__: null,
    W: window,
    D: document,
    H: document.documentElement, // <html>
    loc: location,
    echo: console.log,
    get Now() { return new Date(); },
    get B() { return document.body; },
  };
  lib.echo.t = console.table;
  lib.echo.e = console.error;

  // querySelector[All]
  lib.q = function(str, par = document) { return lib._qs(str, par, ''); };
  lib.Q = function(str, par = document) { return lib._qs(str, par, 'All'); };
  lib._qs = function(s, p, all) { 
    if(s instanceof Node) [s, p] = [p, s];
    try {
      return p['querySelector'+all](s); 
    }
    catch(e) {
      echo.e({e, p, s, stack: lib.stack()});
    }
  };

  // Deprecated
  lib.qs   = function(par, str) { return lib.q(str, par); };
  lib.qsa  = function(par, str) { return lib.Q(str, par); };
  lib.dqs  = function(str) { return lib.q(str); };
  lib.dqsa = function(str) { return lib.Q(str); };

  // url-encode, htmlspecialchars
  lib.ue = function(x) { return lib.ue.core(encodeURIComponent(x)); };
  lib.ue.core = function(str) {
    var rx = new RegExp("[ <>\"{}|\\\\^`()\\[\\]'*!]", 'g');
    str = str.replace(rx, function(a){
      return '%'+a.charCodeAt(0).toString(16); });
    return str;
  };
  lib.ue.decode = function(str) {
    try {
      let san = str.replace(/%(?![0-9A-F]{2})/gi, '%25');
      return decodeURIComponent(san);
    }
    catch(er) {
      console.error(er);
      return null;
    }
  }
  lib.PHSC = function(x) { // not quotes?
    return x.replace(/[&]/g, '&amp;').replace(/[<]/g, '&lt;').replace(/[>]/g, '&gt;'); 
  };

  // get/set attributes
  lib.ga = function(el, attr, norm='') { 
    let val = el.getAttribute(attr); 
    if(typeof norm =='function') return norm(val);
    if(!val && norm) return norm;
    return val;
  };
  lib.sa = function(el, attrs) {
    let safer = lib._elemprop(attrs);
    for(let [k, v] of Object.entries(safer))
      el.setAttribute(k, v);
  };

  // set properties
  lib.own = function(o, k){ return (o!==null && o!==undefined) && has.call(o, k);};
  lib.sp = function(el, prop) {
    if(el instanceof Element) prop = lib._elemprop(prop);
    for(let i in prop) {
      if(unsafe.includes(i)) continue;
      if(i == 'style' || i == 'dataset') lib.sp(el[i], prop[i]);
      else if(i == 'attr') lib.sa(el, prop[i]);
      else if(i == 'rc') lib.adj.rc(el, ...prop[i]);
      else if(lib.own(prop, i)) el[i] = prop[i];
    }
  };
  lib._elemprop = function(prop) { // properties/attributes to elements
    let shortcuts = { 
      cn: 'className', s:'style', ds:'dataset', a: 'attr', t:'title',
      tc: 'textContent', ih: 'innerHTML', oh:'outerHTML', 
      ty:'type', v:'value', h:'href', ti:'tabIndex', ak:'accessKey',
    };
    let prop2 = {};
    for(let [i, v] of Object.entries(prop)) {
      let k = shortcuts[i] || i;
      if(k.match(/^on/i) || (k.match(/^href$/i) && v.match(/^(javascript|data):/i))) continue;
      prop2[k] = v;
    }
    return prop2;
  }
  // document.createElement
  lib.create = new Proxy(
    function(tag, prop, attr) {
      let el = document.createElement(tag);
      if(prop) lib.sp(el, prop);
      if(attr) lib.sa(el, attr);
      return el;
    }, {
    get(target, tag) {
      return function(prop, attr) { return target(tag, prop, attr); }
    }
  });
  lib.dce = lib.create;
  // set CSS, obj looks like { color: "red" }
  lib.css = lib.setStyles = function(el, obj) { lib.sp(el.style, obj); };
  lib.cl = new Proxy( // classList
    function() { // op, el, [delay], names
      let short = { a:'add', r:'remove', m:'momentary', R:'replace', t:'toggle', c: 'contains'};
      let args = [...arguments], op = args.shift(), el = args.shift(), delay = false;
      if(short[op]) op = short[op];
      if(op=='momentary') {
        op = 'add';
        delay = args.shift();
      }
      if(delay) lib.time.out(delay, function(){ el.classList.remove(...args); });
      return el.classList[op](...args);
    }, {
      get(t, p) {
        return function() { return t(p, ...arguments); };
      }
    }
  );
  lib.cloneFontMetrics = function(source, target) {
    const props = [
      'fontFamily', 'fontSize', 'fontWeight', 'lineHeight',
      'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
      'borderStyle', 'borderWidth', 'boxSizing', 'whiteSpace',
      'wordBreak', 'width', 'scrollbarWidth', 'overflowWrap'
    ];
    const style = lib.gcs(source);
    for(let prop of props) target.style[prop] = style[prop];
  }

  // Math
  lib.min = Math.min;
  lib.max = Math.max;
  lib.minmax = function(n, min, max) {
    var m = n+0;
    if(min != null) m = Math.max(m, min);
    if(max != null) m = Math.min(m, max);
    return m;
  };
  lib.abs = Math.abs;
  lib.ceil = Math.ceil;
  lib.floor = Math.floor;
  lib.rnd = function(n, precision = 0) {
    if(!precision) return Math.round(n);
    var x = Math.pow(10, precision);
    return Math.round(n*x)/x;
  };
  lib.pf       = parseFloat; // may return NaN
  lib.floatval = function(x) {var y = parseFloat(x); return isNaN(y)? 0:y; };
  lib.intval   = function(x, r=10) {var y = parseInt(x, r);   return isNaN(y)? 0:y; };
  lib.average  = function(x) {return x.reduce(function(a, b){ return a+b; }, 0) / x.length;};
  lib.zpad     = function(num, len = 2) { // zero-pad (for date/time)
    return num.toString().padStart(len, '0');
  };
  lib.requal = function(o1, o2) { // recursive equal
    if(o1 === o2) return true;
    if(!o1 || !o2 || typeof o1 !== 'object' || typeof o2 !== 'object') return false;
    const a1 = Array.isArray(o1), a2 = Array.isArray(o2);
    if(a1 && a2) {
      if(o1.length !== o2.length) return false;
      for(let i = 0; i < o1.length; i++) {
        if(!lib.requal(o1[i], o2[i])) return false;
      }
      return true;
    }
    if(a1 || a2) return false;
    const k1 = Object.keys(o1), k2 = Object.keys(o2);
    if(k1.length !== k2.length) return false;
    
    for(let i = 0; i < k1.length; i++) {
      const key = k1[i];
      if(!lib.own(o2, key)) return false;
      if(!lib.requal(o1[key], o2[key])) return false;
    }
    return true;
  };
  lib.unref = function(value) {
    if(typeof value !== 'object' || value === null) return value;
    if(Array.isArray(value)) {
      return value.map(function(item){return lib.unref(item);});
    }
    let result = Object.create(null);
    for (let k in value) {
      if(unsafe.includes(k)) continue;
      if(lib.own(value, k)) {
        result[k] = lib.unref(value[k]);
      }
    }
    return result;
  };
  lib.debounce = function(delay, fn) {
    let timeout;
    return function(...args) {
      clearTimeout(timeout);
      timeout = setTimeout(function(){ fn.apply(this, args); }, delay);
    };
  };

  // localStorage
  lib._storage = false;
  lib.getLS = function(key, dflt = null) {
    if(lib._storage===false) {
      try {
        var stored = window.localStorage.getItem('PmLib');
      }
      catch(e) {
        var stored = null;
      }
      lib._storage = stored ? lib.jP(stored, {}) : {};
    }
    let value = lib._storage[key];
    if(value === undefined) return dflt;
    return lib.unref(value);
  };
  lib._savetodisk = function() {
    try {
      var value = lib.jS(lib._storage);
      window.localStorage.setItem('PmLib', value);
      return true;
    }
    catch(e) {
      return false;
    }
  };
  lib._storeLS = lib.debounce(250, lib._savetodisk);

  lib.setLS = function(key, value) {
    if(unsafe.includes(key)) return;
    let curr = lib.getLS(key),
        unref = lib.unref(value);
    if(lib.requal(curr, unref)) return;
    lib._storage[key] = unref;
    lib._storeLS();
  };
  lib.delLS = function(key) {
    if(!lib.own(lib._storage,key)) return false;
    delete lib._storage[key];
    lib._storeLS();
    return true;
  };
  lib.ls = new Proxy(
    function(obj, dflt=null) { 
      // get: 'key', defaultValue
      if(typeof obj=='string') return lib.getLS(obj, dflt);
      // delete [key1, key2...]
      if(Array.isArray(obj)) for(let k of obj) lib.delLS(k);
      // set {key1:value1, ...} 
      else for(let k in obj) lib.setLS(k, obj[k]);
      lib._storeLS();
    }, {
    set(target, key, value) {
      return lib.setLS(key, value);
    },
    get(target, key) {
      return lib.getLS(key);
    },
    deleteProperty(target, key) {
      return lib.delLS(key);
    }
  });

  // sync other tabs
  window.addEventListener('storage', function(e){
    if(e.key == 'PmLib') lib._storage = false;
  });

  // JSON helpers
  lib.jP = function(x, dflt = null) { 
    try { 
      let y = JSON.parse(x, function(k, v){
        if(unsafe.includes(k)) return;
        return v;
      }); 
      return (y === null || typeof y === 'undefined') ? dflt : y;
    } 
    catch(e){ return dflt;}
    
  };
  lib.jS = function(obj, space = 0) { return JSON.stringify(obj, null, space); };
  lib.getConf = function(el) {
    return lib.jP(el.dataset.config, {});
  };

  // Events
  lib._handleEvent = function(ev, el, fn, opts, remove) {
    if(!el) return;
    if(typeof el == 'string') el = lib.Q(el);
    else if(el instanceof EventTarget) el = [el];
    opts = opts || false;
    let method = (remove? 'remove':'add')+'EventListener';
    let evs = ev.split(/[,\s]+/g);
    for(let e of evs) {
      if(lib._onevents[e]) e = lib._onevents[e];
      for(let elem of el) {
        elem[method](e, fn, opts);
      }
    }
  };
  lib.pd = function(e) { e.preventDefault();}
  lib.pd.sp = function(e) { lib.pd(e); e.stopPropagation(); }
  lib.pdsp = function(e, sp) { if(sp) lib.pd.sp(e); else lib.pd(e); }
  lib.aE = function(el, ev, fn, opts) {
    return lib._handleEvent(ev, el, fn, opts);
  };
  lib.rE = function (el, ev, fn, opts) {
    return lib._handleEvent(ev, el, fn, opts, true);
  };
  lib._onevents = { // onevent shortcuts
    cl: 'click', dc: 'dblclick', md: 'mousedown', mu: 'mouseup',
    mm: 'mousemove', me: 'mouseenter', ml: 'mouseleave', mw: 'wheel',
    ip: 'input', ch: 'change', sb: 'submit', fc: 'focus', iv: 'invalid',
    kd: 'keydown', ku: 'keyup', kp: 'keypress',
    ds: 'dragstart', de: 'dragenter', do: 'dragover', dr: 'drop', dl: 'dragleave',
    tg: 'toggle',
  };
  lib.on = new Proxy( // addEventListener
    lib._handleEvent, {
      get(target, e) {
        return function(q, fn, opts) { target(e, q, fn, opts); };
      }
    }
  );
  lib.tap = lib.on.click;
  lib.once = new Proxy( 
    function(ev, el, fn, opts = {}) {
      lib._handleEvent(ev, el, fn, {once:true, ...opts});
    }, {
      get(target, e) {
        return function(q, fn, opts) { target(e, q, fn, opts); };
      }
    }
  );
  lib.off = new Proxy( // removeEventListener
    function(ev, el, fn, opts) { lib._handleEvent(ev, el, fn, opts, 1) }, {
      get(target, e) {
        return function(q, fn, opts) { target(e, q, fn, opts); };
      }
    }
  );
  lib.fire = new Proxy(
    function(el, ename, options = {}) {
      if(lib._onevents[ename]) ename = lib._onevents[ename];
      let o = {bubbles:true, cancelable:true, ...options};
      let etype = o.etype? o.etype : (o.detail || ename.match(/^pm|[^a-z]/)? CustomEvent:Event);
      let ev = new etype(ename, o);
      if(!el) el = document;
      el.dispatchEvent(ev);
    }, {
      get(target, name) {
        return function(el, options = {}) { target(el, name, options); };
      }
    }
  );
  lib.dE = lib.dCE = lib.fire; // dCE, dE Deprecated
  lib.time = { // swapped args, delay in seconds
    out: function(n, fn) { return fn? setTimeout(fn, n*1000)  : clearTimeout(n); },
    int: function(n, fn) { return fn? setInterval(fn, n*1000) : clearInterval(n); },
  };
  lib.itext = function(text) {
    document.execCommand('insertText', false, text);
  }

  // DOM helpers
  lib.adj = new Proxy( // insert adjacent things
    function(el, where, what) {
      let a2 = [...arguments].slice(2);
      if(typeof el == 'string') el = lib.q(el);
      if(where == 'replaceChildren') el.replaceChildren(...a2);
      else if(where == 'replaceWith') el.replaceWith(...a2);
      else if(what instanceof Element) el.insertAdjacentElement(where, what);
      else el.insertAdjacentHTML(where, what);
    }, {
      get(target, prop) {
        let wheres = {
          bb: 'beforebegin', ab: 'afterbegin', be: 'beforeend', 
          ae: 'afterend', rw:'replaceWith', rc:'replaceChildren'
        }
        let where = wheres[prop] || 'afterbegin';
        return function(el, what) { 
          let a1 = [...arguments].slice(1);
          return target(el, where, ...a1);
        };
      }
    }
  );
  // Deprecated
  lib.adjany = lib.adj; 
  lib.adjbb = lib.adj.bb;
  lib.adjbe = lib.adj.be;
  lib.adjab = lib.adj.ab;
  lib.adjae = lib.adj.ae;


  lib.posy = function(el) {
    var top = 0;
    if(el.offsetParent) {
      do {
        top += el.offsetTop;
      } while (el = el.offsetParent);
    }
    return top;
  };

  lib.isVisible = function(el) {
    return !lib.isHidden(el);
  };
  lib.isHidden = function(el) {
    if(!el) return true;
    if(typeof el.checkVisibility == 'function') return !el.checkVisibility();
    var cs = lib.gcs(el);
    if(cs.position == 'fixed') return (cs.display === "none"); // Firefox != Chrome
    return (el.offsetParent === null);
  };

  lib.gcs = new Proxy(
    function(el, prop) {
      var s = window.getComputedStyle(el);
      if(prop) return s.getPropertyValue(prop);
      return s;
    }, {
    get(target, prop) {
      return function(el) { return target(el, prop); };
    }
  });

  lib.gbcr = function(el) { return el.getBoundingClientRect(); }

  lib.abspos = function(fixed, floating) {
    const {css, gbcr, W} = PmLib;
    css(floating, { display: 'block', 'z-index': 1000, position: 'fixed'});
    var lrect = gbcr(floating), rect = gbcr(fixed), 
    iw = W.innerWidth, ih = W.innerHeight, 
    lh = lrect.height, lw = lrect.width;
    
    var left = (rect.left < iw-lw)
      ? rect.left
      : (iw-lw-10);
    floating.style.left = left + 'px';
    
    var top = (rect.top < lh)
      ? (rect.bottom)
      : (rect.top-lh);
    floating.style.top = top + 'px';
  }

  lib.parser = function(html) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    return doc.body;
  }

  // browser prefers dark theme
  lib.wmmd = window.matchMedia
    ? window.matchMedia('(prefers-color-scheme: dark)')
    : false;
  lib.preferdark = function() {
    if(lib.wmmd && lib.wmmd.matches) return 1;
    return 0;
  };

  lib.fdata = function(data) {
    var body = new FormData;
    for(let [k, v] of Object.entries(data)) body.append(k, v);
    return body;
  };

  // Ajax / Fetch functions
  lib.afetch = new Proxy(
    function(rtype, qurl, opt = {}) {
      return new Promise(function(resolve) {
        if(opt.post && !opt.body) {
          opt.body = lib.fdata(opt.post);
          if(!opt.method) opt.method = 'POST';
          delete opt.post;
        }
        if(opt.json && !opt.body) {
          opt.body = lib.jS(opt.json);
          if(!opt.method) opt.method = 'POST';
          if(!opt.headers) opt.headers = {};
          opt.headers = { "Content-Type": 'application/json', ...opt.headers };
          delete opt.json;
        }
        
        const xhr = new XMLHttpRequest();
        
        // Binary optimization: set responseType if looking for a blob or arraybuffer
        if(rtype === 'blob' || rtype === 'arrayBuffer') {
          xhr.responseType = rtype;
        }
        
        if(opt.headers) {
          for (let [k, v] of Object.entries(opt.headers)) xhr.setRequestHeader(k, v);
        }
        
        if(opt.onprogress && xhr.upload) {
          lib.on.progress(xhr.upload, function(e){
            if(e.lengthComputable) {
              let pc = Math.round((e.loaded / e.total) * 1000)/10;
              opt.onprogress(pc);
            }
          });
        }
        
        xhr.onload = function() {
          const ok = xhr.status >= 200 && xhr.status < 300;
          const out = {
            ok,
            redirected: false, // XHR handles this transparently
            status: xhr.status,
            statusText: xhr.statusText,
            type: 'basic',
            url: xhr.responseURL || qurl,
            headers: {}
          };
          
          const rawHeaders = xhr.getAllResponseHeaders();
          if(rawHeaders) {
            let lines = rawHeaders.trim().split(/[\r\n]+/);
            for(let line of lines) {
              let parts = line.split(': ');
              let key = parts.shift().toLowerCase();
              let val = parts.join(': ');
              if(out.headers[key]) out.headers[key] += ', ' + val;
              else out.headers[key] = val;
            }
          }
          
          try {
            if(rtype === 'json') out.data = lib.jP(xhr.responseText);
            else if(rtype === 'text') out.data = xhr.responseText;
            else out.data = xhr.response;
          } catch (e) {
            out.data = null;
          }
          
          resolve(out);
        };
        
        xhr.onerror = function(error) {
          lib.echo.e('Error fetching data:', error);
          resolve({ ok: false, error });
        };
        
        xhr.open(opt.method || 'GET', qurl, true);
        xhr.send(opt.body || null);
      });
    }, {
      get(target, prop) {
        return function(url, opt) { return target(prop, url, opt); };
      }
    }
  );
  

  lib.mkDrag = function(par){
    const {on, cl, pd, fire, echo} = PmLib;
    for(let child of par.children) child.draggable = true;
    par.style.cursor = "move";
    var dragged = false;
    on.dragstart(par, function(e){
      if(![...par.children].includes(e.target)) return;
      dragged = e.target;
      cl.a(dragged, 'dragged');
    });
    on.dragend(par, function(e){
      if(![...par.children].includes(e.target)) return;
      cl.r(dragged, 'dragged');
      dragged = false;
    });
    on.dragover(par, pd);
    on.drop(par, function(e){
      var c = [...this.children], d = dragged, t = e.target;
      if(!d || d==t || d.parentNode !== t.parentNode) return;
      var b = c.indexOf(t) < c.indexOf(d) ? t : t.nextSibling;
      this.insertBefore(d, b);
      fire.pmdragend(this, {detail: {moved:d}});
    });
  };

  // call a function after loading the DOM
  lib.ready = function(fn) {
    if( document.readyState !== 'loading' ) fn();
    else window.addEventListener('DOMContentLoaded', fn);
  };
  lib.ready.done = {};
  lib.ready._init = [];
  lib.ready._queued = [];
  lib.ready.queue = function(fn) {
    if(lib.ready._queued === false) fn();
    else lib.ready._queued.push(fn);
  };

  lib.stack = function() {
    let lines = new Error().stack.split(/\n+/);
    let stack = [];
    for(let line of lines) {
      let a = line.match(/(https?:\/\/.+?|file:\/\/.+?|\/.+?):(\d+):(\d+)/);
      if(a) stack.push({file:a[1], line:parseInt(a[2]), column:parseInt(a[3])});
    }
    return stack.slice(1);
  }
  
  return lib;
})();


