var NchanMessenger = (() => {
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, { get: all[name], enumerable: true });
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

  // node_modules/nchan-messages/js/index.js
  var index_exports = {};
  __export(index_exports, {
    AbortError: () => AbortError,
    ControllerUrlFactory: () => ControllerUrlFactory,
    IncrementalWait: () => IncrementalWait,
    MessageType: () => MessageType,
    Messenger: () => Messenger,
    Request: () => Request,
    Response: () => Response,
    TimeoutError: () => TimeoutError,
    UrlFactory: () => UrlFactory,
    WebSocketError: () => WebSocketError
  });

  // node_modules/nchan-messages/js/async/promise.js
  var TimeoutError = class extends Error {
    get name() {
      return "TimeoutError";
    }
  };
  var AbortError = class extends Error {
    get name() {
      return "AbortError";
    }
  };
  var PromiseStatus = Object.freeze({
    RESOLVED: "resolved",
    REJECTED: "rejected",
    PENDING: "pending"
  });
  var unsettledSymbol = Symbol("unsettled");
  var createPromise = (callback, { signal, timeout } = {}) => new Promise((res, rej) => {
    const tearDown = (reason) => {
      tearDownSignal.abort(reason);
      clearTimeout(timerId);
    };
    const resolve = (value) => {
      tearDown("resolved");
      res(value);
    };
    const reject = (error, reason) => {
      tearDown(reason || "rejected");
      rej(error);
    };
    const tearDownSignal = new AbortController();
    const timerId = timeout | 0 > 0 && setTimeout(() => reject(new TimeoutError(), "timeout"), timeout | 0);
    if (signal instanceof AbortSignal) {
      if (signal.aborted) {
        reject(new AbortError(signal.reason), "abort");
        return;
      } else {
        signal.addEventListener(
          "abort",
          (evt) => reject(new AbortError(evt.target.reason), "abort"),
          { signal: tearDownSignal.signal }
        );
      }
    }
    callback(resolve, reject, tearDownSignal.signal);
  });
  var PromiseManager = class extends Map {
    /**
     * The number of pending promises.
     */
    get pending() {
      return this.size;
    }
    /**
     * Creates a promise with a given signature unless one already exists
     * @param {*} signature 
     * @return {Promise}
     */
    create(signature) {
      const promiseData = this.get(signature);
      if (!promiseData) {
        const descriptor = {};
        descriptor.promise = new Promise((res, rej) => {
          descriptor.resolve = res;
          descriptor.reject = rej;
        });
        this.set(signature, descriptor);
        return descriptor.promise;
      }
      return promiseData.promise;
    }
    /**
     * Resolves and removes a promise with a matching signature
     * @param {*} signature
     * @param {*} value         The value with which the promises are resolved
     * @return {boolean}       True if a promise has been found
     */
    resolve(signature, value) {
      const promise = this.get(signature);
      if (promise) {
        this.delete(signature);
        promise.resolve(value);
        return true;
      }
      return false;
    }
    /**
     * Rejects and removes a promise with a matching signature
     * @param {*} signature
     * @param {*} error         The value with which the promises are rejected
     * @return {boolean}       True if a promise has been found
     */
    reject(signature, error) {
      const promise = this.get(signature);
      if (promise) {
        this.delete(signature);
        promise.reject(error);
      }
      return !!promise;
    }
    /**
     * Rejects all promises and clears the list
     * @param {*} error         The value with which the promises are rejected
     * @return {void}
     */
    rejectAll(error) {
      for (const p of this.values())
        p.reject(error);
      this.clear();
    }
  };

  // node_modules/nchan-messages/js/medium/urlfactory.js
  var flattenChannels = (channels, name) => {
    if (Array.isArray(channels))
      return channels.join(",");
    else if (typeof channels === "string")
      return channels;
    else if (!channels)
      return "";
    else
      throw TypeError(`${name} must be a string or an array of strings`);
  };
  var assertNonEmptyString = (value, name) => {
    if (typeof value !== "string")
      throw TypeError(`${name} must be a string`);
    else if (!value)
      throw TypeError(`${name} cannot be empty`);
  };
  var UrlFactory = class {
    constructor(baseUrl, group, channels = {}) {
      assertNonEmptyString(group, "group");
      const base = new URL(baseUrl || ".", location.href);
      if (!base.pathname.endsWith("/")) {
        base.pathname += "/";
      }
      this.generateBaseUrls(base, group, channels);
    }
    /**
     * 
     * @param {URL} base 
     * @param {string} group 
     * @param {string[]|string} param2.pub
     * @param {string[]|string} param2.sub
     */
    generateBaseUrls(base, group, { pub, sub }) {
      pub = flattenChannels(pub);
      sub = flattenChannels(sub);
      this.restBase = base.href + "rest/";
      this.pubBase = base.href + group + "/pub/";
      base.protocol = "ws" + base.protocol.substring(4);
      this.msgBase = base.href + group + `/room/pub${pub}/sub${sub}/`;
    }
    /**
     * Returns the base URL for the messenger REST service
     * 
     * @returns {string}
     */
    restEndpoint(path) {
      return this.restBase + path;
    }
    /**
     * Returns the publisher endpoint for the given channels
     * 
     * @param {string|string[]} channels 
     * @returns {string}
     */
    publisherEndpoint(channels) {
      channels = flattenChannels(channels, "channels");
      if (!channels) throw TypeError("at least one channel must be provided");
      return this.pubBase + channels;
    }
    /**
     * Returns the messenger endpoint to the given token
     * @param {string} token
     * @returns {string}
     */
    messengerEndpoint(token = "") {
      return this.msgBase + token;
    }
  };
  var ControllerUrlFactory = class extends UrlFactory {
    generateBaseUrls(base, group, { sub }) {
      sub = flattenChannels(sub);
      this.restBase = base.href + "rest/";
      this.pubBase = base.href + group + "/pub/";
      base.protocol = "ws" + base.protocol.substring(4);
      this.msgBase = base.href + group + `/controller/sub${sub}/`;
    }
  };

  // node_modules/nchan-messages/js/async/events.js
  var ChangeEvent = class extends CustomEvent {
    /**
     * Create an event
     * @param {string} type         default: 'change'
     * @param {*} param1            generic event options
     * @param {*} param1.previous   previous value; stored in detail.previous
     * @param {*} param1.current    current value; stored in detail.current
     */
    constructor(type, { previous, current, ...rest }) {
      super(type || "change", { ...rest, detail: { previous, current } });
    }
    /**
     * Returns the current value
     * @return {*}
     */
    get currentValue() {
      return this.detail.current;
    }
    /**
     * Returns the previous value
     * @return {*}
     */
    get previousValue() {
      return this.detail.previous;
    }
  };
  var eventHandlers = /* @__PURE__ */ new WeakMap();
  function defineEvents(classConstructor, ...eventNames) {
    if (!(classConstructor.prototype instanceof EventTarget))
      throw new TypeError("events can be defined only for classes derived from EventTarget");
    Object.defineProperties(classConstructor.prototype, Object.fromEntries(
      eventNames.map((name) => [
        "on" + name.toLowerCase(),
        {
          set(handler) {
            if (typeof handler !== "function" && typeof handler !== "undefined" && handler !== null) {
              return;
            }
            let handlers = eventHandlers.get(this);
            if (!handlers) {
              handlers = /* @__PURE__ */ new Map();
              eventHandlers.set(this, handlers);
            } else if (handlers.has(name)) {
              this.removeEventListener(name, handlers.get(name));
            }
            if (handler) {
              this.addEventListener(name, handler);
              handlers.set(name, handler);
            } else {
              handlers.delete(name);
            }
          },
          get() {
            const handlers = eventHandlers.get(this);
            return handlers && handlers.get(name);
          }
        }
      ])
    ));
  }

  // node_modules/nchan-messages/js/medium/protocol.js
  var MessageType = {
    MESSAGE: "message",
    PROGRESS: "progress",
    REQUEST: "request",
    RESPONSE: "response",
    CONTROL: "ctl"
  };
  var Message = class {
    /**
     * @param {*} payload - the content of the message
     * @param {MessageMetaData} meta - an object with meta data of the message
     */
    constructor(payload, meta) {
      this._meta = meta;
      this._payload = payload;
    }
    /** The source channel of the message */
    get channel() {
      return this._meta.channel;
    }
    /** The name field of the message */
    get name() {
      return this._meta.name;
    }
    /** The publish timestamp of the message */
    get timestamp() {
      return this._meta.timestamp;
    }
    /** The identifier of the message */
    get id() {
      return this._meta.id;
    }
    /** The content of the message */
    get payload() {
      return this._payload;
    }
  };
  var Request = class extends Message {
    /**
     * 
     * @param {Messenger} messenger  - the messenger object that controls the communication
     * @param {*} payload  - the content of the request
     * @param {RequestMetaData} meta - an object with meta data of the request    
     */
    constructor(payload, meta, messenger) {
      super(payload, meta);
      this._messenger = messenger;
      this._done = false;
      this.ondone = null;
    }
    /** The channel on which the requester waits for a response */
    get requester() {
      return this._meta.reply;
    }
    /** The identifier of the request */
    get requestId() {
      return this._meta.reqId;
    }
    /**
     * Sends a progress notification to the requester
     * @param {*} payload - the content of the message
     * @returns {Promise<void>}
     * @throws when a response has been already sent
     */
    async notify(payload) {
      if (this._done) {
        throw new Error("the request is already finilized");
      }
      return this._messenger.sendRaw({
        type: MessageType.PROGRESS,
        reqId: this.requestId,
        payload
      }, [this.requester]);
    }
    /**
     * Sends a response and closes the request
     * @param {*} payload - the content of the response
     * @returns {Promise<void>}
     * @throws when a response has been already sent
     */
    async respond(payload) {
      if (this._done) {
        throw new Error("the request is already finilized");
      }
      this._done = true;
      if (typeof this.ondone === "function") this.ondone();
      return this._messenger.sendRaw({
        type: MessageType.RESPONSE,
        reqId: this.requestId,
        payload
      }, [this.requester]);
    }
  };
  var Response = class extends EventTarget {
    /**
     * @param {Promise<any>} payloadPromise   a promise of the response
     */
    constructor(payloadPromise) {
      super();
      this._closed = false;
      this._payload = payloadPromise;
    }
    /**
     * Resolves to the response content once one is received.
     */
    async payload() {
      return await this._payload;
    }
    /**
     * Returns true if a reponse has been already received
     */
    get closed() {
      return this._closed;
    }
  };
  defineEvents(Response, "progress");
  var ResponseController = class {
    constructor({ signal, timeout }, cleanUpCallback) {
      this._response = new Response(
        createPromise((resolve, reject) => {
          this._resolve = resolve;
          this._reject = reject;
        }, { signal, timeout }).finally(cleanUpCallback)
      );
    }
    get response() {
      return this._response;
    }
    notify(content) {
      this.response.closed || this._response.dispatchEvent(new MessageEvent("progress", { data: content }));
    }
    close(content) {
      if (!this.response.closed) {
        this.response._closed = true;
        this._resolve(content);
      }
      ;
    }
  };

  // node_modules/nchan-messages/js/medium/nchan.js
  var parseMessageId = (msgId) => {
    const [timestamp, counters] = msgId.split(":", 2);
    const result = { timestamp: parseInt(timestamp) };
    if (counters) {
      const parts = counters.split(",");
      if (parts.length === 1) {
        result.channel = 0;
      } else {
        const channelNr = parts.findIndex((ch) => ch.charAt(0) === "[");
        if (channelNr >= 0) result.channel = channelNr;
      }
    }
    return result;
  };
  var parseWebSocketMessage = (msg) => {
    const [rawHeader, payload] = msg.split("\n\n", 2);
    const headers = Object.fromEntries(
      rawHeader.split("\n").map((line) => line.split(": ", 2))
    );
    return {
      headers,
      ...parseMessageId(headers.id),
      payload
    };
  };
  var publish = async (url, payload, { headers = {}, eventName, signal, authorization } = {}) => {
    const options = {
      method: "POST",
      body: payload,
      credentials: "include",
      headers: {
        "Content-Type": "text/json",
        // content type can be overriden
        ...headers,
        "Accept": "text/json"
        // the response must be JSON
      }
    };
    if (signal instanceof AbortSignal) options.signal = signal;
    if (authorization) options.headers["Authorization"] = authorization;
    if (eventName) options.headers["X-EventSource-Event"] = eventName;
    const response = await fetch(url, options);
    if (response.status !== 201 && response.status !== 202) {
      throw Error(`HTTP ${response.status}: access rejected`);
    }
    return await response.json();
  };

  // node_modules/nchan-messages/js/async/queue.js
  var ProcessingQueue = class {
    /**
     * Creates a processing queue
     * @param {QueueItemProcessor} action   A callback function that processes data from the queue.
     *                                      The callback must return true if the item has to be
     *                                      processed again.
     */
    constructor(action) {
      this._paused = false;
      this._processing = false;
      this._promises = new PromiseManager();
      this._items = [];
      this.action = action;
    }
    /**
     * Returns the pause status of the queue
     * @returns {boolean}
     */
    get paused() {
      return this._paused;
    }
    /**
     * Returns true if the queue is processing messages
     * @returns {boolean}
     */
    get active() {
      return this._processing;
    }
    /**
     * Pauses processing the queue
     */
    pause() {
      this._paused = true;
    }
    /**
     * Resumes processing the queue
     */
    resume() {
      this._paused = false;
      this._process();
    }
    /**
     * Returns the number of unprocessed items
     * @returns {number}
     */
    get size() {
      return this._items.length;
    }
    /**
     * Returns true if the queue has no unprocessed items.
     * @returns {boolean}
     */
    get empty() {
      return !this._items.length;
    }
    /**
     * Returns a promise that resolves when the queue has no unprocessed items.
     * @returns {Promise<void>}}
     */
    waitForEmpty() {
      return this._items.length ? this._promises.create("empty") : Promise.resolve();
    }
    /**
     * Returns a promise that resolves when the queue stops processing items.
     * @returns {Promise<void>}}
     */
    async processed() {
      this._processing && await this._promises.create("done");
    }
    /**
     * Puts data on the queue and starts processing unless already processing or paused.
     * @param {*} data 
     */
    put(data) {
      this._items.push(data);
      this._paused || this._process();
    }
    /**
     * Ensures processing messages from the internal queue.
     * 
     * This method creates a task that processes messages asynchronously,
     * allowing other tasks to be process in parallel. Nothing happens
     * if processing is already active.
     * @internal
     */
    async _process() {
      if (this._processing) return;
      this._processing = true;
      let processed = 0, unprocessed = 0;
      while (true) {
        if (processed >= this._items.length) {
          this._items.length = unprocessed;
          break;
        } else if (this._paused || !this._processing) {
          this._items.copyWithin(unprocessed, processed);
          this._items.length -= processed - unprocessed;
          break;
        } else {
          const item = this._items[processed];
          const success = await new Promise(
            (res) => res(this.action(item, this))
          ).catch(() => false);
          if (!success) this._items[unprocessed++] = item;
          processed++;
        }
      }
      this._processing = false;
      this._promises.resolve("done", unprocessed);
      if (this._items.length === 0) this._promises.resolve("empty");
    }
    /**
     * Removes all messages from the internal queue and resets the counter
     * of sent messages.
     */
    clear() {
      this._items.length = 0;
      if (!this._processing) {
        this._promises.resolve("done", 0);
        this._promises.resolve("empty");
      }
    }
  };

  // node_modules/nchan-messages/js/ext/class.js
  var defineConstants = (classConstructor, constants) => Object.defineProperties(
    classConstructor,
    Object.fromEntries(Object.entries(constants).map(
      ([name, value]) => [name, { value, enumerable: true, writable: false }]
    ))
  );

  // node_modules/nchan-messages/js/messenger.js
  var ConnectionState = {
    WAITING: 0,
    CONNECTING: 1,
    CONFIGURING: 2,
    CONNECTED: 3,
    CLOSING: 4,
    CLOSED: 5
  };
  var WebSocketError = class extends Error {
    constructor(message, code) {
      super(message);
      this.code = code;
    }
  };
  var Pinger = class {
    constructor(interval = 30) {
      this.interval = interval;
      this.connections = /* @__PURE__ */ new Set();
      this._timerId = 0;
      this._last = 0;
    }
    get last() {
      return this._last;
    }
    get active() {
      return !!this._timerId;
    }
    get message() {
      return {
        type: "ctl",
        name: "ping",
        timestamp: Date.now()
      };
    }
    start() {
      clearInterval(this._timerId);
      this.interval = Math.max(0, this.interval | 0);
      this._timerId = this.interval && setInterval(() => {
        const msg = JSON.stringify(this.message);
        for (const socket of this.connections) {
          if (socket.readyState === WebSocket.OPEN)
            socket.send(msg);
          else if (socket.readyState !== WebSocket.CONNECTING)
            this.sockets.delete(socket);
        }
      }, this.interval * 1e3);
    }
    stop() {
      clearInterval(this._timerId);
      this._timerId = 0;
    }
  };
  var IncrementalWait = class {
    constructor(options) {
      const {
        max = 3e5,
        stepFactor = 1.5,
        minStep = 1e3,
        maxStep = 3e3
      } = { ...options };
      this.currentInterval = 0;
      this.options = {
        max,
        factor: stepFactor,
        min: minStep,
        delta: maxStep - minStep
      };
      this.timer = 0;
    }
    computeNextInterval() {
      return Math.min(
        this.options.max,
        this.currentInterval * this.options.factor + this.options.min + this.options.delta * Math.random()
      );
    }
    reset() {
      this.tries = 0;
      this.currentInterval = 0;
    }
    async wait() {
      const waitPromise = new Promise((res) => this.timer = setTimeout(res, this.currentInterval));
      this.currentInterval = this.computeNextInterval();
      await waitPromise;
    }
    cancel() {
      clearInterval(this.timer);
    }
  };
  var equalSets = (list1, list2) => {
    const s1 = new Set(list1);
    const s2 = new Set(list2);
    return s1.size === s2.size && s1.isSubsetOf(s2);
  };
  var nullLogger = {
    log() {
    },
    info() {
    },
    warning() {
    },
    error() {
    }
  };
  var OwnChannels = Symbol("all channels");
  var implementsUrlFactory = (obj) => ["restEndpoint", "publisherEndpoint", "messengerEndpoint"].every(
    (prop) => typeof obj[prop] === "function"
  );
  var Messenger = class extends EventTarget {
    /**
     * @param {UrlFactory} urlFactory
     * @param {*} options 
     */
    constructor(urlFactory, options) {
      if (!implementsUrlFactory(urlFactory)) {
        throw new TypeError("the first agument must implement a UrlFactory interface");
      }
      super();
      const self = this;
      this.options = {
        waitOnClose: 500,
        autoReconnect: true,
        pingInterval: 0,
        logger: nullLogger,
        ...options
      };
      this.urlFactory = urlFactory;
      this.tokens = /* @__PURE__ */ (function(self2) {
        const collection = /* @__PURE__ */ new Set();
        let merged = null;
        return {
          add(...tokens) {
            merged = null;
            tokens.forEach((token) => collection.add(token));
          },
          clear() {
            merged = null;
            collection.clear();
          },
          delete(...tokens) {
            merged = null;
            tokens.forEach((token) => collection.delete(token));
          },
          has(token) {
            return collection.has(token);
          },
          get size() {
            return collection.size;
          },
          async merged({ signal } = {}) {
            if (!merged) {
              if (this.size < 2) {
                merged = collection.values().next().value;
              } else {
                const url = self2.urlFactory.restEndpoint("token/merge");
                const response = await fetch(url, {
                  method: "POST",
                  signal,
                  body: JSON.stringify({ tokens: [...collection] })
                }).then((data) => data.json());
                if (response.success) {
                  merged = response.data;
                } else {
                  throw Error(`Failed to generate an access token: ${response.reason}`);
                }
              }
            }
            return merged;
          }
        };
      })(this);
      this._channels = {
        sub: null,
        pub: null
      };
      this.channels = {
        get pub() {
          return self._channels.pub || [];
        },
        get sub() {
          return self._channels.sub || [];
        }
      };
      this._clientId = null;
      this._backend = null;
      this._promises = new PromiseManager();
      this._queue = new ProcessingQueue((msg) => {
        this._backend.send(msg);
        return true;
      });
      this._queue.pause();
      this.lastExitCode = 0;
      this._state = ConnectionState.CLOSED;
      this.reconnectController = this.options.autoReconnect ? new IncrementalWait() : null;
      this.pingSender = new Pinger(this.options.pingInterval | 0);
      this._subscribers = /* @__PURE__ */ new Map();
      this._subscribers.set(OwnChannels, /* @__PURE__ */ new Set());
      this._requests = /* @__PURE__ */ new Map();
      this._received = [];
      this.lastMessageTimestamp = 0;
      this.lastRequestId = 0;
    }
    get logger() {
      return this.options.logger;
    }
    get clientId() {
      return this._clientId;
    }
    get ownSubChannel() {
      return this._channels.sub[2] || this._channels.sub[0];
    }
    get state() {
      return this._state;
    }
    set state(value) {
      if (this._state === value) return;
      const previous = this._state;
      this._state = value;
      this._promises.resolve("state", value);
      this.dispatchEvent(new ChangeEvent("statechange", { previous, current: value }));
    }
    get desiredState() {
      return this._state < ConnectionState.CLOSING ? ConnectionState.CONNECTED : ConnectionState.CLOSED;
    }
    get isReady() {
      return this._state === ConnectionState.CONNECTED;
    }
    get reconnectionTime() {
      return this.reconnectController ? this.reconnectController.currentInterval : 0;
    }
    async waitForConnectionState(desiredState, options) {
      if (desiredState === this._state) return;
      await createPromise((res, rej, tearDown) => this.addEventListener(
        "statechange",
        () => this._state === desiredState && res(),
        { signal: tearDown }
      ), options);
    }
    async connected(options) {
      await Promise.race([
        this.waitForConnectionState(ConnectionState.CONFIGURING, options),
        this.waitForConnectionState(ConnectionState.CONNECTED, options)
      ]);
    }
    async opened(options) {
      await this.waitForConnectionState(ConnectionState.CONNECTED, options);
    }
    async closed(options) {
      await this.waitForConnectionState(ConnectionState.CLOSED, options);
    }
    async open() {
      if (this.state === ConnectionState.CONNECTED) return;
      const openedPromise = this._promises.create("opened");
      switch (this.state) {
        case ConnectionState.CLOSING:
          this._backend.onerror = null;
          this._backend.onclose = null;
          this._promises.reject("closed", new AbortError("Requested to reconnect"));
        // fallthrough
        case ConnectionState.WAITING:
          this.reconnectController && this.reconnectController.cancel();
        // fallthrough
        case ConnectionState.CLOSED:
          this.state = ConnectionState.CONNECTING;
          const abortController = new AbortController();
          openedPromise.then(
            () => abortController.abort(),
            () => abortController.abort()
          );
          try {
            const token = await this.tokens.merged({ signal: abortController.signal });
            const url = this.urlFactory.messengerEndpoint(token);
            this._backend = new WebSocket(url, "ws+meta.nchan");
            this._backend.onopen = this.handleSocketOpen.bind(this);
            this._backend.onclose = this.handleSocketClose.bind(this);
            this._backend.onerror = this.handleSocketError.bind(this);
            this._backend.onmessage = this.handleSocketMessage.bind(this);
          } catch (error) {
            if (!abortController.signal.aborted) {
              throw error;
            }
          }
      }
      await openedPromise;
    }
    async close(discardUnsent = false) {
      switch (this.state) {
        case ConnectionState.CONNECTING:
        // connecting - closing triggers an error, so be careful!
        case ConnectionState.CONFIGURING:
          if (!this._backend) {
            this._promises.reject("opened", new AbortError("Requested to disconnect"));
            this.state = ConnectionState.CLOSED;
            this.lastExitCode = 1e3;
          } else {
            this._backend.onerror = null;
            this._backend.onopen = null;
            this._promises.reject("opened", new AbortError("Requested to disconnect"));
            this.state = ConnectionState.CLOSING;
            this._backend.close(1e3);
            await this._promises.create("closed");
          }
          break;
        case ConnectionState.CONNECTED:
          this.state = ConnectionState.CLOSING;
          if ((this._backend.bufferedAmount || !this._queue.empty) && !discardUnsent) {
            this.logger.warning("waiting for queued data to be sent");
            setTimeout(this.close.bind(this, false), this.options.waitOnClose);
          } else {
            this.logger.info("closing connection");
            this._backend.close(1e3);
          }
        // fallthrough
        case ConnectionState.CLOSING:
          await this._promises.create("closed");
          break;
        case ConnectionState.WAITING:
          this.reconnectController && this.reconnectController.cancel();
          this._promises.reject("opened", new AbortError("Requested to disconnect"));
          this.state = ConnectionState.CLOSED;
          break;
      }
      return this.lastExitCode;
    }
    /**
     * Initiates the web socket connection and processes the message queue.
     * 
     * It updates the connection state to CONNECTED, adds the socket to
     * a ping sender if any, resolves the promise labeled by CONNECTED
     * and fires the connect event. The message queue is processed after.
     * 
     * @param {Event} evt
     */
    handleSocketOpen(evt) {
      this.logger.info(`[WebSocket]: onconnected(${this._backend.url})`);
      this.state = ConnectionState.CONFIGURING;
      this.reconnectController && this.reconnectController.reset();
      this.pingSender.connections.add(this._backend);
      this._promises.create("opened").then(
        () => this.dispatchEvent(new evt.constructor(evt.type, evt)),
        () => false
      );
      this._queue.resume();
      this.pingSender.start();
    }
    /**
     * Updates the connection state, removes the socket from a ping sender
     * if any, resolves or rejects the last promise and fires the disconnect
     * event.
     * 
     * The method is invoked in the following situations:
     *   CONNECTING:    the attempt to connect the web socket has failed
     *   CONNECTED:     the connection as been closed due to an error
     *   DISCONNECTING: the connection has been closed by calling disconnect()
     * 
     * @param {CloseEvent} evt    The event object passed to the handler of
     *                            the onclose event
     */
    async handleSocketClose(evt) {
      this.logger.info(`[WebSocket]: onclose(${this._backend.url}):`, evt.code);
      this._queue.pause();
      this.pingSender.stop();
      this.pingSender.connections.delete(this._backend);
      this.lastExitCode = evt.code;
      const previousState = this.state;
      const desiredState = this.desiredState;
      this.state = ConnectionState.CLOSED;
      switch (previousState) {
        case ConnectionState.CONNECTING:
          this._promises.reject("opened", new WebSocketError("Failed to open connection", evt.code));
          break;
        case ConnectionState.CLOSING:
          this._promises.resolve("closed", evt.code);
          break;
      }
      if (desiredState === ConnectionState.CONNECTED && this.reconnectController && ![1e3, 1001, 1005, 1010].includes(evt.code)) {
        try {
          this.dispatchEvent(new evt.constructor(evt.type, evt));
          this.state = ConnectionState.WAITING;
          await this.reconnectController.wait();
          this.open().catch(() => {
          });
        } catch (error) {
          this.logger.error("while reconnecting", error);
          this._promises.reject("open", error);
          this.state = ConnectionState.CLOSED;
        }
      } else {
        this.dispatchEvent(new evt.constructor(evt.type, evt));
      }
    }
    /**
     * A handler for the onerror event.
     * 
     * @param {ErrorEvent} evt    The event object passed to the handler
     *                            of the onerror event
     */
    handleSocketError(evt) {
      this.logger.error(evt.message, evt);
      this.dispatchEvent(new evt.constructor(evt.type, evt));
    }
    getSubscribers(channel) {
      const direct = this._subscribers.get(channel) || /* @__PURE__ */ new Set();
      const all = this._subscribers.get(OwnChannels) || /* @__PURE__ */ new Set();
      return /* @__PURE__ */ new Set([...direct, ...all]);
    }
    /**
     * A handler for the onmessage event.
     * @param {MessageEvent} evt     The event object passed to the handler
     *                               of the onmessage event.
     */
    handleSocketMessage(evt) {
      this.logger.info(`[${this.clientId}] message
`, evt.data);
      try {
        const parsed = parseWebSocketMessage(evt.data);
        parsed.payload = JSON.parse(parsed.payload);
        if (parsed.timestamp < this.lastMessageTimestamp) {
          return;
        } else if (parsed.payload.type === "ctl") {
          this.processControllMessage(parsed);
        } else if (this._channels.sub) {
          this.processMessage(parsed);
        } else {
          this._received.push(parsed);
        }
        this.lastMessageTimestamp = parsed.timestamp;
      } catch (err) {
        this.logger.error(`[${this.clientId}] message error`, err);
      }
    }
    processControllMessage(message) {
      const { name, payload } = message.payload;
      switch (name) {
        case "hello":
          this._clientId = payload.clientId;
          this._channels.pub = payload.pub || [];
          this._channels.sub = payload.sub || [];
          this.logger.debug(`[${this.clientId}] connected to group ${payload.group}.
Listens to ${this._channels.sub.join(",")}.
Publishes to ${this._channels.pub.join(",")}.`);
          if (this.state !== ConnectionState.CONNECTED) {
            this.logger.info(`[WebSocket]: onopen(${this._backend.url})`);
            this.state = ConnectionState.CONNECTED;
            this._promises.resolve("opened");
          }
          const received = [...this._received];
          this._received.length = 0;
          received.forEach(this.processMessage.bind(this));
          break;
      }
    }
    processMessage({ payload, ...message }) {
      const { type, name, reqId, reply, payload: content } = payload;
      message.channel = this._channels.sub[message.channel];
      switch (type) {
        case "message":
          const msgSubscribers = this.getSubscribers(message.channel);
          if (msgSubscribers.size) {
            Object.assign(message, { name });
            const msg = new Message(content, message);
            for (const subscriber of msgSubscribers) {
              if (typeof subscriber.handleMessage === "function")
                subscriber.handleMessage(msg);
            }
          }
          break;
        case "request":
          const reqSubscribers = this.getSubscribers(message.channel);
          if (!this._requests.has(reqId) && reqSubscribers.size) {
            Object.assign(message, { name, reqId, reply });
            const request = new Request(content, message, this);
            request.ondone = () => this._requests.delete(reqId);
            this._requests.set(reqId, request);
            for (const subscriber of reqSubscribers) {
              if (typeof subscriber.handleRequest === "function")
                subscriber.handleRequest(request);
            }
          }
          break;
        case "progress":
          const ctl = this._requests.get(reqId);
          this.logger.info(`[${this.clientId}] progress: `, ctl ? "requested" : "not requested");
          ctl && ctl.notify(content);
          break;
        case "response":
          const req = this._requests.get(reqId);
          this.logger.info(`[${this.clientId}] response: `, req ? "requested" : "not requested");
          req && req.close(content);
          break;
      }
      this.lastMessageTimestamp = message.timestamp;
    }
    subscribe(subscriber, channels) {
      if (channels) {
        for (const channel of channels) {
          this._subscribers.has(channel) || this._subscribers.set(channel, /* @__PURE__ */ new Set());
          this._subscribers.get(channel).add(subscriber);
        }
      } else {
        this._subscribers.get(OwnChannels).add(subscriber);
      }
    }
    unsubscribe(subscriber, channels) {
      if (channels) {
        channels.forEach((channel) => {
          const subscribers = this._subscribers.get(channel);
          subscribers && subscribers.delete(subscriber);
        });
      } else {
        this._subscribers.get(OwnChannels).delete(subscriber);
      }
    }
    /**
     * Sends the message when connected and puts it on an internal queue otherwise.
     * @param {string|object} msg   The message to be sent
     */
    async sendRaw(msg, channels = null) {
      msg = JSON.stringify(msg);
      if (!channels || equalSets(channels, this._channels.pub)) {
        this._queue.put(msg);
      } else {
        const url = this.urlFactory.publisherEndpoint(channels);
        const token = await this.tokens.merged();
        await publish(url, msg, { authorization: token }).catch(
          (err) => {
            this.logger.error(`Failed to publish a message on ${channels} with token ${token}`, err);
            throw err;
          }
        );
      }
    }
    async publish(name, payload, { channels = null } = {}) {
      await this.sendRaw({ type: "message", name, payload }, channels);
    }
    async request(name, payload, { channels = null, signal, timeout } = {}) {
      const reqId = `${this.clientId}:${++this.lastRequestId}`;
      const reply = this.ownSubChannel;
      if (!reply) {
        throw new Error("at least one subscription channel is required to receive a response");
      }
      const ctl = new ResponseController({
        signal,
        timeout
      }, () => this._requests.delete(reqId));
      this._requests.set(reqId, ctl);
      await this.sendRaw({ type: "request", reqId, reply, name, payload }, channels);
      return ctl.response;
    }
    async drain(options) {
      await this._queue.waitForEmpty(options);
    }
  };
  defineConstants(Messenger, ConnectionState);
  defineEvents(Messenger, "open", "close", "statechange");
  return __toCommonJS(index_exports);
})();

(() => {
  // node_modules/emoji-picker-element/database.js
  function assertNonEmptyString(str) {
    if (typeof str !== "string" || !str) {
      throw new Error("expected a non-empty string, got: " + str);
    }
  }
  function assertNumber(number) {
    if (typeof number !== "number") {
      throw new Error("expected a number, got: " + number);
    }
  }
  var DB_VERSION_CURRENT = 1;
  var DB_VERSION_INITIAL = 1;
  var STORE_EMOJI = "emoji";
  var STORE_KEYVALUE = "keyvalue";
  var STORE_FAVORITES = "favorites";
  var FIELD_TOKENS = "tokens";
  var INDEX_TOKENS = "tokens";
  var FIELD_UNICODE = "unicode";
  var INDEX_COUNT = "count";
  var FIELD_GROUP = "group";
  var FIELD_ORDER = "order";
  var INDEX_GROUP_AND_ORDER = "group-order";
  var KEY_ETAG = "eTag";
  var KEY_URL = "url";
  var KEY_PREFERRED_SKINTONE = "skinTone";
  var MODE_READONLY = "readonly";
  var MODE_READWRITE = "readwrite";
  var INDEX_SKIN_UNICODE = "skinUnicodes";
  var FIELD_SKIN_UNICODE = "skinUnicodes";
  var DEFAULT_DATA_SOURCE = "https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json";
  var DEFAULT_LOCALE = "en";
  function uniqBy(arr, func) {
    const set2 = /* @__PURE__ */ new Set();
    const res = [];
    for (const item of arr) {
      const key = func(item);
      if (!set2.has(key)) {
        set2.add(key);
        res.push(item);
      }
    }
    return res;
  }
  function uniqEmoji(emojis) {
    return uniqBy(emojis, (_) => _.unicode);
  }
  function initialMigration(db) {
    function createObjectStore(name, keyPath, indexes) {
      const store = keyPath ? db.createObjectStore(name, { keyPath }) : db.createObjectStore(name);
      if (indexes) {
        for (const [indexName, [keyPath2, multiEntry]] of Object.entries(indexes)) {
          store.createIndex(indexName, keyPath2, { multiEntry });
        }
      }
      return store;
    }
    createObjectStore(STORE_KEYVALUE);
    createObjectStore(
      STORE_EMOJI,
      /* keyPath */
      FIELD_UNICODE,
      {
        [INDEX_TOKENS]: [
          FIELD_TOKENS,
          /* multiEntry */
          true
        ],
        [INDEX_GROUP_AND_ORDER]: [[FIELD_GROUP, FIELD_ORDER]],
        [INDEX_SKIN_UNICODE]: [
          FIELD_SKIN_UNICODE,
          /* multiEntry */
          true
        ]
      }
    );
    createObjectStore(STORE_FAVORITES, void 0, {
      [INDEX_COUNT]: [""]
    });
  }
  var openIndexedDBRequests = {};
  var databaseCache = {};
  var onCloseListeners = {};
  function handleOpenOrDeleteReq(resolve, reject, req) {
    req.onerror = () => reject(req.error);
    req.onblocked = () => reject(new Error("IDB blocked"));
    req.onsuccess = () => resolve(req.result);
  }
  async function createDatabase(dbName) {
    const db = await new Promise((resolve, reject) => {
      const req = indexedDB.open(dbName, DB_VERSION_CURRENT);
      openIndexedDBRequests[dbName] = req;
      req.onupgradeneeded = (e) => {
        if (e.oldVersion < DB_VERSION_INITIAL) {
          initialMigration(req.result);
        }
      };
      handleOpenOrDeleteReq(resolve, reject, req);
    });
    db.onclose = () => closeDatabase(dbName);
    return db;
  }
  function openDatabase(dbName) {
    if (!databaseCache[dbName]) {
      databaseCache[dbName] = createDatabase(dbName);
    }
    return databaseCache[dbName];
  }
  function dbPromise(db, storeName, readOnlyOrReadWrite, cb) {
    return new Promise((resolve, reject) => {
      const txn = db.transaction(storeName, readOnlyOrReadWrite, { durability: "relaxed" });
      const store = typeof storeName === "string" ? txn.objectStore(storeName) : storeName.map((name) => txn.objectStore(name));
      let res;
      cb(store, txn, (result) => {
        res = result;
      });
      txn.oncomplete = () => resolve(res);
      txn.onerror = () => reject(txn.error);
    });
  }
  function closeDatabase(dbName) {
    const req = openIndexedDBRequests[dbName];
    const db = req && req.result;
    if (db) {
      db.close();
      const listeners = onCloseListeners[dbName];
      if (listeners) {
        for (const listener of listeners) {
          listener();
        }
      }
    }
    delete openIndexedDBRequests[dbName];
    delete databaseCache[dbName];
    delete onCloseListeners[dbName];
  }
  function deleteDatabase(dbName) {
    return new Promise((resolve, reject) => {
      closeDatabase(dbName);
      const req = indexedDB.deleteDatabase(dbName);
      handleOpenOrDeleteReq(resolve, reject, req);
    });
  }
  function addOnCloseListener(dbName, listener) {
    let listeners = onCloseListeners[dbName];
    if (!listeners) {
      listeners = onCloseListeners[dbName] = [];
    }
    listeners.push(listener);
  }
  var irregularEmoticons = /* @__PURE__ */ new Set([
    ":D",
    "XD",
    ":'D",
    "O:)",
    ":X",
    ":P",
    ";P",
    "XP",
    ":L",
    ":Z",
    ":j",
    "8D",
    "XO",
    "8)",
    ":B",
    ":O",
    ":S",
    ":'o",
    "Dx",
    "X(",
    "D:",
    ":C",
    ">0)",
    ":3",
    "</3",
    "<3",
    "\\M/",
    ":E",
    "8#"
  ]);
  function extractTokens(str) {
    return str.split(/[\s_]+/).map((word) => {
      if (!word.match(/\w/) || irregularEmoticons.has(word)) {
        return word.toLowerCase();
      }
      return word.replace(/[)(:,]/g, "").replace(/’/g, "'").toLowerCase();
    }).filter(Boolean);
  }
  var MIN_SEARCH_TEXT_LENGTH = 2;
  function normalizeTokens(str) {
    return str.filter(Boolean).map((_) => _.toLowerCase()).filter((_) => _.length >= MIN_SEARCH_TEXT_LENGTH);
  }
  function transformEmojiData(emojiData) {
    const res = emojiData.map(({ annotation, emoticon, group, order, shortcodes, skins, tags, emoji, version }) => {
      const tokens = [...new Set(
        normalizeTokens([
          ...(shortcodes || []).map(extractTokens).flat(),
          ...(tags || []).map(extractTokens).flat(),
          ...extractTokens(annotation),
          emoticon
        ])
      )].sort();
      const res2 = {
        annotation,
        group,
        order,
        tags,
        tokens,
        unicode: emoji,
        version
      };
      if (emoticon) {
        res2.emoticon = emoticon;
      }
      if (shortcodes) {
        res2.shortcodes = shortcodes;
      }
      if (skins) {
        res2.skinTones = [];
        res2.skinUnicodes = [];
        res2.skinVersions = [];
        for (const { tone, emoji: emoji2, version: version2 } of skins) {
          res2.skinTones.push(tone);
          res2.skinUnicodes.push(emoji2);
          res2.skinVersions.push(version2);
        }
      }
      return res2;
    });
    return res;
  }
  function callStore(store, method, key, cb) {
    store[method](key).onsuccess = (e) => cb && cb(e.target.result);
  }
  function getIDB(store, key, cb) {
    callStore(store, "get", key, cb);
  }
  function getAllIDB(store, key, cb) {
    callStore(store, "getAll", key, cb);
  }
  function commit(txn) {
    if (txn.commit) {
      txn.commit();
    }
  }
  function minBy(array, func) {
    let minItem = array[0];
    for (let i = 1; i < array.length; i++) {
      const item = array[i];
      if (func(minItem) > func(item)) {
        minItem = item;
      }
    }
    return minItem;
  }
  function findCommonMembers(arrays, uniqByFunc) {
    const shortestArray = minBy(arrays, (_) => _.length);
    const results = [];
    for (const item of shortestArray) {
      if (!arrays.some((array) => array.findIndex((_) => uniqByFunc(_) === uniqByFunc(item)) === -1)) {
        results.push(item);
      }
    }
    return results;
  }
  async function isEmpty(db) {
    return !await get(db, STORE_KEYVALUE, KEY_URL);
  }
  async function hasData(db, url, eTag) {
    const [oldETag, oldUrl] = await Promise.all([KEY_ETAG, KEY_URL].map((key) => get(db, STORE_KEYVALUE, key)));
    return oldETag === eTag && oldUrl === url;
  }
  async function doFullDatabaseScanForSingleResult(db, predicate) {
    const BATCH_SIZE = 50;
    return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
      let lastKey;
      const processNextBatch = () => {
        emojiStore.getAll(lastKey && IDBKeyRange.lowerBound(lastKey, true), BATCH_SIZE).onsuccess = (e) => {
          const results = e.target.result;
          for (const result of results) {
            lastKey = result.unicode;
            if (predicate(result)) {
              return cb(result);
            }
          }
          if (results.length < BATCH_SIZE) {
            return cb();
          }
          processNextBatch();
        };
      };
      processNextBatch();
    });
  }
  async function loadData(db, emojiData, url, eTag) {
    try {
      const transformedData = transformEmojiData(emojiData);
      await dbPromise(db, [STORE_EMOJI, STORE_KEYVALUE], MODE_READWRITE, ([emojiStore, metaStore], txn) => {
        let oldETag;
        let oldUrl;
        let todo = 0;
        function checkFetched() {
          if (++todo === 2) {
            onFetched();
          }
        }
        function onFetched() {
          if (oldETag === eTag && oldUrl === url) {
            return;
          }
          emojiStore.clear();
          for (const data of transformedData) {
            emojiStore.put(data);
          }
          metaStore.put(eTag, KEY_ETAG);
          metaStore.put(url, KEY_URL);
          commit(txn);
        }
        getIDB(metaStore, KEY_ETAG, (result) => {
          oldETag = result;
          checkFetched();
        });
        getIDB(metaStore, KEY_URL, (result) => {
          oldUrl = result;
          checkFetched();
        });
      });
    } finally {
    }
  }
  async function getEmojiByGroup(db, group) {
    return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
      const range = IDBKeyRange.bound([group, 0], [group + 1, 0], false, true);
      getAllIDB(emojiStore.index(INDEX_GROUP_AND_ORDER), range, cb);
    });
  }
  async function getEmojiBySearchQuery(db, query) {
    const tokens = normalizeTokens(extractTokens(query));
    if (!tokens.length) {
      return [];
    }
    return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => {
      const intermediateResults = [];
      const checkDone = () => {
        if (intermediateResults.length === tokens.length) {
          onDone();
        }
      };
      const onDone = () => {
        const results = findCommonMembers(intermediateResults, (_) => _.unicode);
        cb(results.sort((a, b) => a.order < b.order ? -1 : 1));
      };
      for (let i = 0; i < tokens.length; i++) {
        const token = tokens[i];
        const range = i === tokens.length - 1 ? IDBKeyRange.bound(token, token + "\uFFFF", false, true) : IDBKeyRange.only(token);
        getAllIDB(emojiStore.index(INDEX_TOKENS), range, (result) => {
          intermediateResults.push(result);
          checkDone();
        });
      }
    });
  }
  async function getEmojiByShortcode(db, shortcode) {
    const emojis = await getEmojiBySearchQuery(db, shortcode);
    if (!emojis.length) {
      const predicate = (_) => (_.shortcodes || []).includes(shortcode.toLowerCase());
      return await doFullDatabaseScanForSingleResult(db, predicate) || null;
    }
    return emojis.filter((_) => {
      const lowerShortcodes = (_.shortcodes || []).map((_2) => _2.toLowerCase());
      return lowerShortcodes.includes(shortcode.toLowerCase());
    })[0] || null;
  }
  async function getEmojiByUnicode(db, unicode) {
    return dbPromise(db, STORE_EMOJI, MODE_READONLY, (emojiStore, txn, cb) => getIDB(emojiStore, unicode, (result) => {
      if (result) {
        return cb(result);
      }
      getIDB(emojiStore.index(INDEX_SKIN_UNICODE), unicode, (result2) => cb(result2 || null));
    }));
  }
  function get(db, storeName, key) {
    return dbPromise(db, storeName, MODE_READONLY, (store, txn, cb) => getIDB(store, key, cb));
  }
  function set(db, storeName, key, value) {
    return dbPromise(db, storeName, MODE_READWRITE, (store, txn) => {
      store.put(value, key);
      commit(txn);
    });
  }
  function incrementFavoriteEmojiCount(db, unicode) {
    return dbPromise(db, STORE_FAVORITES, MODE_READWRITE, (store, txn) => getIDB(store, unicode, (result) => {
      store.put((result || 0) + 1, unicode);
      commit(txn);
    }));
  }
  function getTopFavoriteEmoji(db, customEmojiIndex2, limit) {
    if (limit === 0) {
      return [];
    }
    return dbPromise(db, [STORE_FAVORITES, STORE_EMOJI], MODE_READONLY, ([favoritesStore, emojiStore], txn, cb) => {
      const results = [];
      favoritesStore.index(INDEX_COUNT).openCursor(void 0, "prev").onsuccess = (e) => {
        const cursor = e.target.result;
        if (!cursor) {
          return cb(results);
        }
        function addResult(result) {
          results.push(result);
          if (results.length === limit) {
            return cb(results);
          }
          cursor.continue();
        }
        const unicodeOrName = cursor.primaryKey;
        const custom = customEmojiIndex2.byName(unicodeOrName);
        if (custom) {
          return addResult(custom);
        }
        getIDB(emojiStore, unicodeOrName, (emoji) => {
          if (emoji) {
            return addResult(emoji);
          }
          cursor.continue();
        });
      };
    });
  }
  var CODA_MARKER = "";
  function trie(arr, itemToTokens) {
    const map = /* @__PURE__ */ new Map();
    for (const item of arr) {
      const tokens = itemToTokens(item);
      for (const token of tokens) {
        let currentMap = map;
        for (let i = 0; i < token.length; i++) {
          const char = token.charAt(i);
          let nextMap = currentMap.get(char);
          if (!nextMap) {
            nextMap = /* @__PURE__ */ new Map();
            currentMap.set(char, nextMap);
          }
          currentMap = nextMap;
        }
        let valuesAtCoda = currentMap.get(CODA_MARKER);
        if (!valuesAtCoda) {
          valuesAtCoda = [];
          currentMap.set(CODA_MARKER, valuesAtCoda);
        }
        valuesAtCoda.push(item);
      }
    }
    const search = (query, exact) => {
      let currentMap = map;
      for (let i = 0; i < query.length; i++) {
        const char = query.charAt(i);
        const nextMap = currentMap.get(char);
        if (nextMap) {
          currentMap = nextMap;
        } else {
          return [];
        }
      }
      if (exact) {
        const results2 = currentMap.get(CODA_MARKER);
        return results2 || [];
      }
      const results = [];
      const queue = [currentMap];
      while (queue.length) {
        const currentMap2 = queue.shift();
        const entriesSortedByKey = [...currentMap2.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1);
        for (const [key, value] of entriesSortedByKey) {
          if (key === CODA_MARKER) {
            results.push(...value);
          } else {
            queue.push(value);
          }
        }
      }
      return results;
    };
    return search;
  }
  var requiredKeys$1 = [
    "name",
    "url"
  ];
  function assertCustomEmojis(customEmojis) {
    const isArray = customEmojis && Array.isArray(customEmojis);
    const firstItemIsFaulty = isArray && customEmojis.length && (!customEmojis[0] || requiredKeys$1.some((key) => !(key in customEmojis[0])));
    if (!isArray || firstItemIsFaulty) {
      throw new Error("Custom emojis are in the wrong format");
    }
  }
  function customEmojiIndex(customEmojis) {
    assertCustomEmojis(customEmojis);
    const sortByName = (a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
    const all = customEmojis.sort(sortByName);
    const emojiToTokens = (emoji) => {
      const set2 = /* @__PURE__ */ new Set();
      if (emoji.shortcodes) {
        for (const shortcode of emoji.shortcodes) {
          for (const token of extractTokens(shortcode)) {
            set2.add(token);
          }
        }
      }
      return set2;
    };
    const searchTrie = trie(customEmojis, emojiToTokens);
    const searchByExactMatch = (_) => searchTrie(_, true);
    const searchByPrefix = (_) => searchTrie(_, false);
    const search = (query) => {
      const tokens = extractTokens(query);
      const intermediateResults = tokens.map((token, i) => (i < tokens.length - 1 ? searchByExactMatch : searchByPrefix)(token));
      return findCommonMembers(intermediateResults, (_) => _.name).sort(sortByName);
    };
    const shortcodeToEmoji = /* @__PURE__ */ new Map();
    const nameToEmoji = /* @__PURE__ */ new Map();
    for (const customEmoji of customEmojis) {
      nameToEmoji.set(customEmoji.name.toLowerCase(), customEmoji);
      for (const shortcode of customEmoji.shortcodes || []) {
        shortcodeToEmoji.set(shortcode.toLowerCase(), customEmoji);
      }
    }
    const byShortcode = (shortcode) => shortcodeToEmoji.get(shortcode.toLowerCase());
    const byName = (name) => nameToEmoji.get(name.toLowerCase());
    return {
      all,
      search,
      byShortcode,
      byName
    };
  }
  var isFirefoxContentScript = typeof wrappedJSObject !== "undefined";
  function cleanEmoji(emoji) {
    if (!emoji) {
      return emoji;
    }
    if (isFirefoxContentScript) {
      emoji = structuredClone(emoji);
    }
    delete emoji.tokens;
    if (emoji.skinTones) {
      const len = emoji.skinTones.length;
      emoji.skins = Array(len);
      for (let i = 0; i < len; i++) {
        emoji.skins[i] = {
          tone: emoji.skinTones[i],
          unicode: emoji.skinUnicodes[i],
          version: emoji.skinVersions[i]
        };
      }
      delete emoji.skinTones;
      delete emoji.skinUnicodes;
      delete emoji.skinVersions;
    }
    return emoji;
  }
  function warnETag(eTag) {
    if (!eTag) {
      console.warn("emoji-picker-element is more efficient if the dataSource server exposes an ETag header.");
    }
  }
  var requiredKeys = [
    "annotation",
    "emoji",
    "group",
    "order",
    "version"
  ];
  function assertEmojiData(emojiData) {
    if (!emojiData || !Array.isArray(emojiData) || !emojiData[0] || typeof emojiData[0] !== "object" || requiredKeys.some((key) => !(key in emojiData[0]))) {
      throw new Error("Emoji data is in the wrong format");
    }
  }
  function assertStatus(response, dataSource) {
    if (Math.floor(response.status / 100) !== 2) {
      throw new Error("Failed to fetch: " + dataSource + ":  " + response.status);
    }
  }
  async function getETag(dataSource) {
    const response = await fetch(dataSource, { method: "HEAD" });
    assertStatus(response, dataSource);
    const eTag = response.headers.get("etag");
    warnETag(eTag);
    return eTag;
  }
  async function getETagAndData(dataSource) {
    const response = await fetch(dataSource);
    assertStatus(response, dataSource);
    const eTag = response.headers.get("etag");
    warnETag(eTag);
    const emojiData = await response.json();
    assertEmojiData(emojiData);
    return [eTag, emojiData];
  }
  function arrayBufferToBinaryString(buffer) {
    var binary = "";
    var bytes = new Uint8Array(buffer);
    var length = bytes.byteLength;
    var i = -1;
    while (++i < length) {
      binary += String.fromCharCode(bytes[i]);
    }
    return binary;
  }
  function binaryStringToArrayBuffer(binary) {
    var length = binary.length;
    var buf = new ArrayBuffer(length);
    var arr = new Uint8Array(buf);
    var i = -1;
    while (++i < length) {
      arr[i] = binary.charCodeAt(i);
    }
    return buf;
  }
  async function jsonChecksum(object) {
    const inString = JSON.stringify(object);
    let inBuffer = binaryStringToArrayBuffer(inString);
    const outBuffer = await crypto.subtle.digest("SHA-1", inBuffer);
    const outBinString = arrayBufferToBinaryString(outBuffer);
    const res = btoa(outBinString);
    return res;
  }
  async function doCheckForUpdates(db, dataSource) {
    let emojiData;
    let eTag = await getETag(dataSource);
    if (!eTag) {
      const eTagAndData = await getETagAndData(dataSource);
      eTag = eTagAndData[0];
      emojiData = eTagAndData[1];
      if (!eTag) {
        eTag = await jsonChecksum(emojiData);
      }
    }
    if (await hasData(db, dataSource, eTag)) ;
    else {
      if (!emojiData) {
        const eTagAndData = await getETagAndData(dataSource);
        emojiData = eTagAndData[1];
      }
      await loadData(db, emojiData, dataSource, eTag);
    }
  }
  async function loadDataForFirstTime(db, dataSource) {
    let [eTag, emojiData] = await getETagAndData(dataSource);
    if (!eTag) {
      eTag = await jsonChecksum(emojiData);
    }
    await loadData(db, emojiData, dataSource, eTag);
  }
  async function checkForUpdates(db, dataSource) {
    try {
      await doCheckForUpdates(db, dataSource);
    } catch (err) {
      if (err.name !== "InvalidStateError") {
        throw err;
      }
    }
  }
  var Database = class {
    constructor({ dataSource = DEFAULT_DATA_SOURCE, locale = DEFAULT_LOCALE, customEmoji = [] } = {}) {
      this.dataSource = dataSource;
      this.locale = locale;
      this._dbName = `emoji-picker-element-${this.locale}`;
      this._db = void 0;
      this._lazyUpdate = void 0;
      this._custom = customEmojiIndex(customEmoji);
      this._clear = this._clear.bind(this);
      this._ready = this._init();
    }
    async _init() {
      const db = this._db = await openDatabase(this._dbName);
      addOnCloseListener(this._dbName, this._clear);
      const dataSource = this.dataSource;
      const empty = await isEmpty(db);
      if (empty) {
        await loadDataForFirstTime(db, dataSource);
      } else {
        this._lazyUpdate = checkForUpdates(db, dataSource);
      }
    }
    async ready() {
      const checkReady = async () => {
        if (!this._ready) {
          this._ready = this._init();
        }
        return this._ready;
      };
      await checkReady();
      if (!this._db) {
        await checkReady();
      }
    }
    async getEmojiByGroup(group) {
      assertNumber(group);
      await this.ready();
      return uniqEmoji(await getEmojiByGroup(this._db, group)).map(cleanEmoji);
    }
    async getEmojiBySearchQuery(query) {
      assertNonEmptyString(query);
      await this.ready();
      const customs = this._custom.search(query);
      const natives = uniqEmoji(await getEmojiBySearchQuery(this._db, query)).map(cleanEmoji);
      return [
        ...customs,
        ...natives
      ];
    }
    async getEmojiByShortcode(shortcode) {
      assertNonEmptyString(shortcode);
      await this.ready();
      const custom = this._custom.byShortcode(shortcode);
      if (custom) {
        return custom;
      }
      return cleanEmoji(await getEmojiByShortcode(this._db, shortcode));
    }
    async getEmojiByUnicodeOrName(unicodeOrName) {
      assertNonEmptyString(unicodeOrName);
      await this.ready();
      const custom = this._custom.byName(unicodeOrName);
      if (custom) {
        return custom;
      }
      return cleanEmoji(await getEmojiByUnicode(this._db, unicodeOrName));
    }
    async getPreferredSkinTone() {
      await this.ready();
      return await get(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE) || 0;
    }
    async setPreferredSkinTone(skinTone) {
      assertNumber(skinTone);
      await this.ready();
      return set(this._db, STORE_KEYVALUE, KEY_PREFERRED_SKINTONE, skinTone);
    }
    async incrementFavoriteEmojiCount(unicodeOrName) {
      assertNonEmptyString(unicodeOrName);
      await this.ready();
      return incrementFavoriteEmojiCount(this._db, unicodeOrName);
    }
    async getTopFavoriteEmoji(limit) {
      assertNumber(limit);
      await this.ready();
      return (await getTopFavoriteEmoji(this._db, this._custom, limit)).map(cleanEmoji);
    }
    set customEmoji(customEmojis) {
      this._custom = customEmojiIndex(customEmojis);
    }
    get customEmoji() {
      return this._custom.all;
    }
    async _shutdown() {
      await this.ready();
      try {
        await this._lazyUpdate;
      } catch (err) {
      }
    }
    // clear references to IDB, e.g. during a close event
    _clear() {
      this._db = this._ready = this._lazyUpdate = void 0;
    }
    async close() {
      await this._shutdown();
      await closeDatabase(this._dbName);
    }
    async delete() {
      await this._shutdown();
      await deleteDatabase(this._dbName);
    }
  };

  // node_modules/emoji-picker-element/picker.js
  var allGroups = [
    [-1, "\u2728", "custom"],
    [0, "\u{1F600}", "smileys-emotion"],
    [1, "\u{1F44B}", "people-body"],
    [3, "\u{1F431}", "animals-nature"],
    [4, "\u{1F34E}", "food-drink"],
    [5, "\u{1F3E0}\uFE0F", "travel-places"],
    [6, "\u26BD", "activities"],
    [7, "\u{1F4DD}", "objects"],
    [8, "\u26D4\uFE0F", "symbols"],
    [9, "\u{1F3C1}", "flags"]
  ].map(([id, emoji, name]) => ({ id, emoji, name }));
  var groups = allGroups.slice(1);
  var MIN_SEARCH_TEXT_LENGTH2 = 2;
  var NUM_SKIN_TONES = 6;
  var rIC = typeof requestIdleCallback === "function" ? requestIdleCallback : setTimeout;
  function hasZwj(emoji) {
    return emoji.unicode.includes("\u200D");
  }
  var versionsAndTestEmoji = {
    "\u{1FAEA}": 17,
    // distorted face
    "\u{1FAE9}": 16,
    // face with bags under eyes
    "\u{1FAE8}": 15.1,
    // shaking head, technically from v15 but see note above
    "\u{1FAE0}": 14,
    "\u{1F972}": 13.1,
    // smiling face with tear, technically from v13 but see note above
    "\u{1F97B}": 12.1,
    // sari, technically from v12 but see note above
    "\u{1F970}": 11,
    "\u{1F929}": 5,
    "\u{1F471}\u200D\u2640\uFE0F": 4,
    "\u{1F923}": 3,
    "\u{1F441}\uFE0F\u200D\u{1F5E8}\uFE0F": 2,
    "\u{1F600}": 1,
    "\u{1F610}\uFE0F": 0.7,
    "\u{1F603}": 0.6
  };
  var TIMEOUT_BEFORE_LOADING_MESSAGE = 1e3;
  var DEFAULT_SKIN_TONE_EMOJI = "\u{1F590}\uFE0F";
  var DEFAULT_NUM_COLUMNS = 8;
  var MOST_COMMONLY_USED_EMOJI = [
    "\u{1F60A}",
    "\u{1F612}",
    "\u2764\uFE0F",
    "\u{1F44D}\uFE0F",
    "\u{1F60D}",
    "\u{1F602}",
    "\u{1F62D}",
    "\u263A\uFE0F",
    "\u{1F614}",
    "\u{1F629}",
    "\u{1F60F}",
    "\u{1F495}",
    "\u{1F64C}",
    "\u{1F618}"
  ];
  var FONT_FAMILY = '"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif';
  var DEFAULT_CATEGORY_SORTING = (a, b) => a < b ? -1 : a > b ? 1 : 0;
  var getTextFeature = (text, color) => {
    const canvas = document.createElement("canvas");
    canvas.width = canvas.height = 1;
    const ctx = canvas.getContext("2d", {
      // Improves the performance of `getImageData()`
      // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getContextAttributes#willreadfrequently
      willReadFrequently: true
    });
    ctx.textBaseline = "top";
    ctx.font = `100px ${FONT_FAMILY}`;
    ctx.fillStyle = color;
    ctx.scale(0.01, 0.01);
    ctx.fillText(text, 0, 0);
    return ctx.getImageData(0, 0, 1, 1).data;
  };
  var compareFeatures = (feature1, feature2) => {
    const feature1Str = [...feature1].join(",");
    const feature2Str = [...feature2].join(",");
    return feature1Str === feature2Str && !feature1Str.startsWith("0,0,0,");
  };
  function testColorEmojiSupported(text) {
    const feature1 = getTextFeature(text, "#000");
    const feature2 = getTextFeature(text, "#fff");
    return feature1 && feature2 && compareFeatures(feature1, feature2);
  }
  function determineEmojiSupportLevel() {
    const entries = Object.entries(versionsAndTestEmoji);
    try {
      for (const [emoji, version] of entries) {
        if (testColorEmojiSupported(emoji)) {
          return version;
        }
      }
    } catch (e) {
    } finally {
    }
    return entries[0][1];
  }
  var promise;
  var detectEmojiSupportLevel = () => {
    if (!promise) {
      promise = new Promise((resolve) => rIC(() => resolve(determineEmojiSupportLevel())));
    }
    return promise;
  };
  var supportedZwjEmojis = /* @__PURE__ */ new Map();
  var VARIATION_SELECTOR = "\uFE0F";
  var SKINTONE_MODIFIER = "\uD83C";
  var ZWJ = "\u200D";
  var LIGHT_SKIN_TONE = 127995;
  var LIGHT_SKIN_TONE_MODIFIER = 57339;
  function applySkinTone(str, skinTone) {
    if (skinTone === 0) {
      return str;
    }
    const zwjIndex = str.indexOf(ZWJ);
    if (zwjIndex !== -1) {
      return str.substring(0, zwjIndex) + String.fromCodePoint(LIGHT_SKIN_TONE + skinTone - 1) + str.substring(zwjIndex);
    }
    if (str.endsWith(VARIATION_SELECTOR)) {
      str = str.substring(0, str.length - 1);
    }
    return str + SKINTONE_MODIFIER + String.fromCodePoint(LIGHT_SKIN_TONE_MODIFIER + skinTone - 1);
  }
  function halt(event) {
    event.preventDefault();
    event.stopPropagation();
  }
  function incrementOrDecrement(decrement, val, arr) {
    val += decrement ? -1 : 1;
    if (val < 0) {
      val = arr.length - 1;
    } else if (val >= arr.length) {
      val = 0;
    }
    return val;
  }
  function uniqBy2(arr, func) {
    const set2 = /* @__PURE__ */ new Set();
    const res = [];
    for (const item of arr) {
      const key = func(item);
      if (!set2.has(key)) {
        set2.add(key);
        res.push(item);
      }
    }
    return res;
  }
  function summarizeEmojisForUI(emojis, emojiSupportLevel) {
    const toSimpleSkinsMap = (skins) => {
      const res = {};
      for (const skin of skins) {
        if (typeof skin.tone === "number" && skin.version <= emojiSupportLevel) {
          res[skin.tone] = skin.unicode;
        }
      }
      return res;
    };
    return emojis.map(({ unicode, skins, shortcodes, url, name, category, annotation }) => ({
      unicode,
      name,
      shortcodes,
      url,
      category,
      annotation,
      id: unicode || name,
      skins: skins && toSimpleSkinsMap(skins)
    }));
  }
  var rAF = requestAnimationFrame;
  var resizeObserverSupported = typeof ResizeObserver === "function";
  function resizeObserverAction(node, abortSignal, onUpdate) {
    let resizeObserver;
    if (resizeObserverSupported) {
      resizeObserver = new ResizeObserver(onUpdate);
      resizeObserver.observe(node);
    } else {
      rAF(onUpdate);
    }
    abortSignal.addEventListener("abort", () => {
      if (resizeObserver) {
        resizeObserver.disconnect();
      }
    });
  }
  function calculateTextWidth(node) {
    {
      const range = document.createRange();
      range.selectNode(node.firstChild);
      return range.getBoundingClientRect().width;
    }
  }
  var BASELINE_EMOJI = "\u{1F600}";
  var baselineEmojiWidth;
  var fallbackNode;
  function calculateTextWidthWithFallback(unicode, domNode, baselineEmojiNode) {
    const result = calculateTextWidth(domNode);
    if (!result) {
      if (!fallbackNode) {
        fallbackNode = baselineEmojiNode.cloneNode(true);
        const styles = getComputedStyle(baselineEmojiNode);
        for (const prop of ["font-family", "line-height", "width", "height", "font-size", "display", "align-items", "justify-content"]) {
          fallbackNode.style.setProperty(prop, styles.getPropertyValue(prop), "important");
        }
      }
      try {
        document.body.appendChild(fallbackNode);
        fallbackNode.firstChild.nodeValue = unicode;
        return calculateTextWidth(fallbackNode);
      } finally {
        fallbackNode.remove();
      }
    }
    return result;
  }
  function checkZwjSupport(zwjEmojisToCheck, baselineEmojiNode, emojiToDomNode) {
    let allSupported = true;
    for (const emoji of zwjEmojisToCheck) {
      const domNode = emojiToDomNode(emoji);
      if (!domNode) {
        continue;
      }
      if (typeof baselineEmojiWidth === "undefined") {
        baselineEmojiWidth = calculateTextWidthWithFallback(BASELINE_EMOJI, baselineEmojiNode, baselineEmojiNode);
      }
      const emojiWidth = calculateTextWidthWithFallback(emoji.unicode, domNode, baselineEmojiNode);
      const supported = emojiWidth / 1.8 < baselineEmojiWidth;
      supportedZwjEmojis.set(emoji.unicode, supported);
      if (!supported) {
        allSupported = false;
      }
    }
    return allSupported;
  }
  function uniq(arr) {
    return uniqBy2(arr, (_) => _);
  }
  function resetScrollTopIfPossible(element) {
    if (element) {
      element.scrollTop = 0;
    }
  }
  function getFromMap(cache, key, func) {
    let cached = cache.get(key);
    if (!cached) {
      cached = func();
      cache.set(key, cached);
    }
    return cached;
  }
  function toString(value) {
    return "" + value;
  }
  function parseTemplate(htmlString) {
    const template = document.createElement("template");
    template.innerHTML = htmlString;
    return template;
  }
  var parseCache = /* @__PURE__ */ new WeakMap();
  var domInstancesCache = /* @__PURE__ */ new WeakMap();
  var unkeyedSymbol = Symbol("un-keyed");
  var hasReplaceChildren = "replaceChildren" in Element.prototype;
  function replaceChildren(parentNode, newChildren) {
    if (hasReplaceChildren) {
      parentNode.replaceChildren(...newChildren);
    } else {
      parentNode.innerHTML = "";
      parentNode.append(...newChildren);
    }
  }
  function doChildrenNeedRerender(parentNode, newChildren) {
    let oldChild = parentNode.firstChild;
    let oldChildrenCount = 0;
    while (oldChild) {
      const newChild = newChildren[oldChildrenCount];
      if (newChild !== oldChild) {
        return true;
      }
      oldChild = oldChild.nextSibling;
      oldChildrenCount++;
    }
    return oldChildrenCount !== newChildren.length;
  }
  function patchChildren(newChildren, instanceBinding) {
    const { targetNode } = instanceBinding;
    let { targetParentNode } = instanceBinding;
    let needsRerender = false;
    if (targetParentNode) {
      needsRerender = doChildrenNeedRerender(targetParentNode, newChildren);
    } else {
      needsRerender = true;
      instanceBinding.targetNode = void 0;
      instanceBinding.targetParentNode = targetParentNode = targetNode.parentNode;
    }
    if (needsRerender) {
      replaceChildren(targetParentNode, newChildren);
    }
  }
  function patch(expressions, instanceBindings) {
    for (const instanceBinding of instanceBindings) {
      const {
        targetNode,
        currentExpression,
        binding: {
          expressionIndex,
          attributeName,
          attributeValuePre,
          attributeValuePost
        }
      } = instanceBinding;
      const expression = expressions[expressionIndex];
      if (currentExpression === expression) {
        continue;
      }
      instanceBinding.currentExpression = expression;
      if (attributeName) {
        if (expression === null) {
          targetNode.removeAttribute(attributeName);
        } else {
          const newValue = attributeValuePre + toString(expression) + attributeValuePost;
          targetNode.setAttribute(attributeName, newValue);
        }
      } else {
        let newNode;
        if (Array.isArray(expression)) {
          patchChildren(expression, instanceBinding);
        } else if (expression instanceof Element) {
          newNode = expression;
          targetNode.replaceWith(newNode);
        } else {
          targetNode.nodeValue = toString(expression);
        }
        if (newNode) {
          instanceBinding.targetNode = newNode;
        }
      }
    }
  }
  function parse(tokens) {
    let htmlString = "";
    let withinTag = false;
    let withinAttribute = false;
    let elementIndexCounter = -1;
    const elementsToBindings = /* @__PURE__ */ new Map();
    const elementIndexes = [];
    let skipTokenChars = 0;
    for (let i = 0, len = tokens.length; i < len; i++) {
      const token = tokens[i];
      htmlString += token.slice(skipTokenChars);
      if (i === len - 1) {
        break;
      }
      for (let j = 0; j < token.length; j++) {
        const char = token.charAt(j);
        switch (char) {
          case "<": {
            const nextChar = token.charAt(j + 1);
            if (nextChar === "/") {
              elementIndexes.pop();
            } else {
              withinTag = true;
              elementIndexes.push(++elementIndexCounter);
            }
            break;
          }
          case ">": {
            withinTag = false;
            withinAttribute = false;
            break;
          }
          case "=": {
            withinAttribute = true;
            break;
          }
        }
      }
      const elementIndex = elementIndexes[elementIndexes.length - 1];
      const bindings = getFromMap(elementsToBindings, elementIndex, () => []);
      let attributeName;
      let attributeValuePre;
      let attributeValuePost;
      if (withinAttribute) {
        const attributePreMatch = /(\S+)="?([^"=]*)$/.exec(token);
        attributeName = attributePreMatch[1];
        attributeValuePre = attributePreMatch[2];
        const attributePostMatch = /^([^">]*)("?)/.exec(tokens[i + 1]);
        attributeValuePost = attributePostMatch[1];
        htmlString = htmlString.slice(0, -1 * attributePreMatch[0].length);
        skipTokenChars = attributePostMatch[0].length;
      } else {
        skipTokenChars = 0;
      }
      const binding = {
        attributeName,
        attributeValuePre,
        attributeValuePost,
        expressionIndex: i
      };
      bindings.push(binding);
      if (!withinTag && !withinAttribute) {
        htmlString += " ";
      }
    }
    const template = parseTemplate(htmlString);
    return {
      template,
      elementsToBindings
    };
  }
  function applyBindings(bindings, element, instanceBindings) {
    for (let i = 0; i < bindings.length; i++) {
      const binding = bindings[i];
      const targetNode = binding.attributeName ? element : element.firstChild;
      const instanceBinding = {
        binding,
        targetNode,
        targetParentNode: void 0,
        currentExpression: void 0
      };
      instanceBindings.push(instanceBinding);
    }
  }
  function traverseAndSetupBindings(rootElement, elementsToBindings) {
    const instanceBindings = [];
    let topLevelBindings;
    if (elementsToBindings.size === 1 && (topLevelBindings = elementsToBindings.get(0))) {
      applyBindings(topLevelBindings, rootElement, instanceBindings);
    } else {
      const treeWalker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT);
      let element = rootElement;
      let elementIndex = -1;
      do {
        const bindings = elementsToBindings.get(++elementIndex);
        if (bindings) {
          applyBindings(bindings, element, instanceBindings);
        }
      } while (element = treeWalker.nextNode());
    }
    return instanceBindings;
  }
  function parseHtml(tokens) {
    const { template, elementsToBindings } = getFromMap(parseCache, tokens, () => parse(tokens));
    const dom = template.cloneNode(true).content.firstElementChild;
    const instanceBindings = traverseAndSetupBindings(dom, elementsToBindings);
    return function updateDomInstance(expressions) {
      patch(expressions, instanceBindings);
      return dom;
    };
  }
  function createFramework(state) {
    const domInstances = getFromMap(domInstancesCache, state, () => /* @__PURE__ */ new Map());
    let domInstanceCacheKey = unkeyedSymbol;
    function html(tokens, ...expressions) {
      const domInstancesForTokens = getFromMap(domInstances, tokens, () => /* @__PURE__ */ new Map());
      const updateDomInstance = getFromMap(domInstancesForTokens, domInstanceCacheKey, () => parseHtml(tokens));
      return updateDomInstance(expressions);
    }
    function map(array, callback, keyFunction) {
      return array.map((item, index) => {
        const originalCacheKey = domInstanceCacheKey;
        domInstanceCacheKey = keyFunction(item);
        try {
          return callback(item, index);
        } finally {
          domInstanceCacheKey = originalCacheKey;
        }
      });
    }
    return { map, html };
  }
  function render(container, state, helpers, events, actions, refs, abortSignal, actionContext, firstRender) {
    const { labelWithSkin, titleForEmoji, unicodeWithSkin } = helpers;
    const { html, map } = createFramework(state);
    function emojiList(emojis, searchMode, prefix) {
      return map(emojis, (emoji, i) => {
        return html`<button role="${searchMode ? "option" : "menuitem"}" aria-selected="${searchMode ? i === state.activeSearchItem : null}" aria-label="${labelWithSkin(emoji, state.currentSkinTone)}" title="${titleForEmoji(emoji)}" class="${"emoji" + (searchMode && i === state.activeSearchItem ? " active" : "") + (emoji.unicode ? "" : " custom-emoji")}" id="${`${prefix}-${emoji.id}`}" style="${emoji.unicode ? null : `--custom-emoji-background: url(${JSON.stringify(emoji.url)})`}">${emoji.unicode ? unicodeWithSkin(emoji, state.currentSkinTone) : ""}</button>`;
      }, (emoji) => `${prefix}-${emoji.id}`);
    }
    const section = () => {
      return html`<section data-ref="rootElement" class="picker" aria-label="${state.i18n.regionLabel}" style="${state.pickerStyle || ""}"><div class="pad-top"></div><div class="search-row"><div class="search-wrapper"><input id="search" class="search" type="search" role="combobox" enterkeyhint="search" placeholder="${state.i18n.searchLabel}" autocapitalize="none" autocomplete="off" spellcheck="true" aria-expanded="${!!(state.searchMode && state.currentEmojis.length)}" aria-controls="search-results" aria-describedby="search-description" aria-autocomplete="list" aria-activedescendant="${state.activeSearchItemId ? `emo-${state.activeSearchItemId}` : null}" data-ref="searchElement" data-on-input="onSearchInput" data-on-keydown="onSearchKeydown"><label class="sr-only" for="search">${state.i18n.searchLabel}</label> <span id="search-description" class="sr-only">${state.i18n.searchDescription}</span></div><div class="skintone-button-wrapper ${state.skinTonePickerExpandedAfterAnimation ? "expanded" : ""}"><button id="skintone-button" class="emoji ${state.skinTonePickerExpanded ? "hide-focus" : ""}" aria-label="${state.skinToneButtonLabel}" title="${state.skinToneButtonLabel}" aria-describedby="skintone-description" aria-haspopup="listbox" aria-expanded="${state.skinTonePickerExpanded}" aria-controls="skintone-list" data-on-click="onClickSkinToneButton">${state.skinToneButtonText || ""}</button></div><span id="skintone-description" class="sr-only">${state.i18n.skinToneDescription}</span><div data-ref="skinToneDropdown" id="skintone-list" class="skintone-list hide-focus ${state.skinTonePickerExpanded ? "" : "hidden no-animate"}" style="transform:translateY(${state.skinTonePickerExpanded ? 0 : "calc(-1 * var(--num-skintones) * var(--total-emoji-size))"})" role="listbox" aria-label="${state.i18n.skinTonesLabel}" aria-activedescendant="skintone-${state.activeSkinTone}" aria-hidden="${!state.skinTonePickerExpanded}" tabIndex="-1" data-on-focusout="onSkinToneOptionsFocusOut" data-on-click="onSkinToneOptionsClick" data-on-keydown="onSkinToneOptionsKeydown" data-on-keyup="onSkinToneOptionsKeyup">${map(state.skinTones, (skinTone, i) => {
        return html`<div id="skintone-${i}" class="emoji ${i === state.activeSkinTone ? "active" : ""}" aria-selected="${i === state.activeSkinTone}" role="option" title="${state.i18n.skinTones[i]}" aria-label="${state.i18n.skinTones[i]}">${skinTone}</div>`;
      }, (skinTone) => skinTone)}</div></div><div class="nav" role="tablist" style="grid-template-columns:repeat(${state.groups.length},1fr)" aria-label="${state.i18n.categoriesLabel}" data-on-keydown="onNavKeydown" data-on-click="onNavClick">${map(state.groups, (group) => {
        return html`<button role="tab" class="nav-button" aria-controls="tab-${group.id}" aria-label="${state.i18n.categories[group.name]}" aria-selected="${!state.searchMode && state.currentGroup.id === group.id}" title="${state.i18n.categories[group.name]}" data-group-id="${group.id}"><div class="nav-emoji emoji">${group.emoji}</div></button>`;
      }, (group) => group.id)}</div><div class="indicator-wrapper"><div class="indicator" style="transform:translateX(${/* istanbul ignore next */
      (state.isRtl ? -1 : 1) * state.currentGroupIndex * 100}%)"></div></div><div class="message ${state.message ? "" : "gone"}" role="alert" aria-live="polite">${state.message || ""}</div><div data-ref="tabpanelElement" class="tabpanel ${!state.databaseLoaded || state.message ? "gone" : ""}" role="${state.searchMode ? "region" : "tabpanel"}" aria-label="${state.searchMode ? state.i18n.searchResultsLabel : state.i18n.categories[state.currentGroup.name]}" id="${state.searchMode ? null : `tab-${state.currentGroup.id}`}" tabIndex="0" data-on-click="onEmojiClick"><div data-action="calculateEmojiGridStyle">${map(state.currentEmojisWithCategories, (emojiWithCategory, i) => {
        return html`<div><div id="menu-label-${i}" class="category ${state.currentEmojisWithCategories.length === 1 && state.currentEmojisWithCategories[0].category === "" ? "gone" : ""}" aria-hidden="true">${state.searchMode ? state.i18n.searchResultsLabel : emojiWithCategory.category ? emojiWithCategory.category : state.currentEmojisWithCategories.length > 1 ? state.i18n.categories.custom : state.i18n.categories[state.currentGroup.name]}</div><div class="emoji-menu ${i !== 0 && !state.searchMode && state.currentGroup.id === -1 ? "visibility-auto" : ""}" style="${`--num-rows: ${Math.ceil(emojiWithCategory.emojis.length / state.numColumns)}`}" data-action="updateOnIntersection" role="${state.searchMode ? "listbox" : "menu"}" aria-labelledby="menu-label-${i}" id="${state.searchMode ? "search-results" : null}">${emojiList(
          emojiWithCategory.emojis,
          state.searchMode,
          /* prefix */
          "emo"
        )}</div></div>`;
      }, (emojiWithCategory) => emojiWithCategory.category)}</div></div><div class="favorites onscreen emoji-menu ${state.message ? "gone" : ""}" role="menu" aria-label="${state.i18n.favoritesLabel}" data-on-click="onEmojiClick">${emojiList(
        state.currentFavorites,
        /* searchMode */
        false,
        /* prefix */
        "fav"
      )}</div><button data-ref="baselineEmoji" aria-hidden="true" tabindex="-1" class="abs-pos hidden emoji baseline-emoji">😀</button></section>`;
    };
    const rootDom = section();
    const forElementWithAttribute = (attributeName, callback) => {
      for (const element of container.querySelectorAll(`[${attributeName}]`)) {
        callback(element, element.getAttribute(attributeName));
      }
    };
    if (firstRender) {
      container.appendChild(rootDom);
      for (const eventName of ["click", "focusout", "input", "keydown", "keyup"]) {
        forElementWithAttribute(`data-on-${eventName}`, (element, listenerName) => {
          element.addEventListener(eventName, events[listenerName]);
        });
      }
      forElementWithAttribute("data-ref", (element, ref) => {
        refs[ref] = element;
      });
      abortSignal.addEventListener("abort", () => {
        container.removeChild(rootDom);
      });
    }
    forElementWithAttribute("data-action", (element, action) => {
      let boundActions = actionContext.get(action);
      if (!boundActions) {
        actionContext.set(action, boundActions = /* @__PURE__ */ new WeakSet());
      }
      if (!boundActions.has(element)) {
        boundActions.add(element);
        actions[action](element);
      }
    });
  }
  var qM = typeof queueMicrotask === "function" ? queueMicrotask : (callback) => Promise.resolve().then(callback);
  function createState(abortSignal) {
    let destroyed = false;
    let currentObserver;
    const propsToObservers = /* @__PURE__ */ new Map();
    const dirtyObservers = /* @__PURE__ */ new Set();
    let queued;
    const flush = () => {
      if (destroyed) {
        return;
      }
      const observersToRun = [...dirtyObservers];
      dirtyObservers.clear();
      try {
        for (const observer of observersToRun) {
          observer();
        }
      } finally {
        queued = false;
        if (dirtyObservers.size) {
          queued = true;
          qM(flush);
        }
      }
    };
    const state = new Proxy({}, {
      get(target, prop) {
        if (currentObserver) {
          let observers = propsToObservers.get(prop);
          if (!observers) {
            observers = /* @__PURE__ */ new Set();
            propsToObservers.set(prop, observers);
          }
          observers.add(currentObserver);
        }
        return target[prop];
      },
      set(target, prop, newValue) {
        if (target[prop] !== newValue) {
          target[prop] = newValue;
          const observers = propsToObservers.get(prop);
          if (observers) {
            for (const observer of observers) {
              dirtyObservers.add(observer);
            }
            if (!queued) {
              queued = true;
              qM(flush);
            }
          }
        }
        return true;
      }
    });
    const createEffect = (callback) => {
      const runnable = () => {
        const oldObserver = currentObserver;
        currentObserver = runnable;
        try {
          return callback();
        } finally {
          currentObserver = oldObserver;
        }
      };
      return runnable();
    };
    abortSignal.addEventListener("abort", () => {
      destroyed = true;
    });
    return {
      state,
      createEffect
    };
  }
  function arraysAreEqualByFunction(left, right, areEqualFunc) {
    if (left.length !== right.length) {
      return false;
    }
    for (let i = 0; i < left.length; i++) {
      if (!areEqualFunc(left[i], right[i])) {
        return false;
      }
    }
    return true;
  }
  var intersectionObserverCache = /* @__PURE__ */ new WeakMap();
  function intersectionObserverAction(node, abortSignal, listener) {
    {
      const root = node.closest(".tabpanel");
      let observer = intersectionObserverCache.get(root);
      if (!observer) {
        observer = new IntersectionObserver(listener, {
          root,
          // trigger if we are 1/2 scroll container height away so that the images load a bit quicker while scrolling
          rootMargin: "50% 0px 50% 0px",
          // trigger if any part of the emoji grid is intersecting
          threshold: 0
        });
        intersectionObserverCache.set(root, observer);
        abortSignal.addEventListener("abort", () => {
          observer.disconnect();
        });
      }
      observer.observe(node);
    }
  }
  var EMPTY_ARRAY = [];
  var { assign } = Object;
  function createRoot(shadowRoot, props) {
    const refs = {};
    const abortController = new AbortController();
    const abortSignal = abortController.signal;
    const { state, createEffect } = createState(abortSignal);
    const actionContext = /* @__PURE__ */ new Map();
    assign(state, {
      skinToneEmoji: void 0,
      i18n: void 0,
      database: void 0,
      customEmoji: void 0,
      customCategorySorting: void 0,
      emojiVersion: void 0
    });
    assign(state, props);
    assign(state, {
      initialLoad: true,
      currentEmojis: [],
      currentEmojisWithCategories: [],
      rawSearchText: "",
      searchText: "",
      searchMode: false,
      activeSearchItem: -1,
      message: void 0,
      skinTonePickerExpanded: false,
      skinTonePickerExpandedAfterAnimation: false,
      currentSkinTone: 0,
      activeSkinTone: 0,
      skinToneButtonText: void 0,
      pickerStyle: void 0,
      skinToneButtonLabel: "",
      skinTones: [],
      currentFavorites: [],
      defaultFavoriteEmojis: void 0,
      numColumns: DEFAULT_NUM_COLUMNS,
      isRtl: false,
      currentGroupIndex: 0,
      groups,
      databaseLoaded: false,
      activeSearchItemId: void 0
    });
    createEffect(() => {
      if (state.currentGroup !== state.groups[state.currentGroupIndex]) {
        state.currentGroup = state.groups[state.currentGroupIndex];
      }
    });
    const focus = (id) => {
      shadowRoot.getElementById(id).focus();
    };
    const emojiToDomNode = (emoji) => shadowRoot.getElementById(`emo-${emoji.id}`);
    const fireEvent = (name, detail) => {
      refs.rootElement.dispatchEvent(new CustomEvent(name, {
        detail,
        bubbles: true,
        composed: true
      }));
    };
    const compareEmojiArrays = (a, b) => a.id === b.id;
    const compareCurrentEmojisWithCategories = (a, b) => {
      const { category: aCategory, emojis: aEmojis } = a;
      const { category: bCategory, emojis: bEmojis } = b;
      if (aCategory !== bCategory) {
        return false;
      }
      return arraysAreEqualByFunction(aEmojis, bEmojis, compareEmojiArrays);
    };
    const updateCurrentEmojis = (newEmojis) => {
      if (!arraysAreEqualByFunction(state.currentEmojis, newEmojis, compareEmojiArrays)) {
        state.currentEmojis = newEmojis;
      }
    };
    const updateSearchMode = (newSearchMode) => {
      if (state.searchMode !== newSearchMode) {
        state.searchMode = newSearchMode;
      }
    };
    const updateCurrentEmojisWithCategories = (newEmojisWithCategories) => {
      if (!arraysAreEqualByFunction(state.currentEmojisWithCategories, newEmojisWithCategories, compareCurrentEmojisWithCategories)) {
        state.currentEmojisWithCategories = newEmojisWithCategories;
      }
    };
    const unicodeWithSkin = (emoji, currentSkinTone) => currentSkinTone && emoji.skins && emoji.skins[currentSkinTone] || emoji.unicode;
    const labelWithSkin = (emoji, currentSkinTone) => uniq([
      emoji.name || unicodeWithSkin(emoji, currentSkinTone),
      emoji.annotation,
      ...emoji.shortcodes || EMPTY_ARRAY
    ].filter(Boolean)).join(", ");
    const titleForEmoji = (emoji) => emoji.annotation || (emoji.shortcodes || EMPTY_ARRAY).join(", ");
    const helpers = {
      labelWithSkin,
      titleForEmoji,
      unicodeWithSkin
    };
    const events = {
      onClickSkinToneButton,
      onEmojiClick,
      onNavClick,
      onNavKeydown,
      onSearchKeydown,
      onSkinToneOptionsClick,
      onSkinToneOptionsFocusOut,
      onSkinToneOptionsKeydown,
      onSkinToneOptionsKeyup,
      onSearchInput
    };
    const actions = {
      calculateEmojiGridStyle,
      updateOnIntersection
    };
    let firstRender = true;
    createEffect(() => {
      render(shadowRoot, state, helpers, events, actions, refs, abortSignal, actionContext, firstRender);
      firstRender = false;
    });
    if (!state.emojiVersion) {
      detectEmojiSupportLevel().then((level) => {
        if (!level) {
          state.message = state.i18n.emojiUnsupportedMessage;
        }
      });
    }
    createEffect(() => {
      async function handleDatabaseLoading() {
        let showingLoadingMessage = false;
        const timeoutHandle = setTimeout(() => {
          showingLoadingMessage = true;
          state.message = state.i18n.loadingMessage;
        }, TIMEOUT_BEFORE_LOADING_MESSAGE);
        try {
          await state.database.ready();
          state.databaseLoaded = true;
        } catch (err) {
          console.error(err);
          state.message = state.i18n.networkErrorMessage;
        } finally {
          clearTimeout(timeoutHandle);
          if (showingLoadingMessage) {
            showingLoadingMessage = false;
            state.message = "";
          }
        }
      }
      if (state.database) {
        handleDatabaseLoading();
      }
    });
    createEffect(() => {
      state.pickerStyle = `
      --num-groups: ${state.groups.length}; 
      --indicator-opacity: ${state.searchMode ? 0 : 1}; 
      --num-skintones: ${NUM_SKIN_TONES};`;
    });
    createEffect(() => {
      if (state.customEmoji && state.database) {
        updateCustomEmoji();
      }
    });
    createEffect(() => {
      if (state.customEmoji && state.customEmoji.length) {
        if (state.groups !== allGroups) {
          state.groups = allGroups;
        }
      } else if (state.groups !== groups) {
        if (state.currentGroupIndex) {
          state.currentGroupIndex--;
        }
        state.groups = groups;
      }
    });
    createEffect(() => {
      async function updatePreferredSkinTone() {
        if (state.databaseLoaded) {
          state.currentSkinTone = await state.database.getPreferredSkinTone();
        }
      }
      updatePreferredSkinTone();
    });
    createEffect(() => {
      state.skinTones = Array(NUM_SKIN_TONES).fill().map((_, i) => applySkinTone(state.skinToneEmoji, i));
    });
    createEffect(() => {
      state.skinToneButtonText = state.skinTones[state.currentSkinTone];
    });
    createEffect(() => {
      state.skinToneButtonLabel = state.i18n.skinToneLabel.replace("{skinTone}", state.i18n.skinTones[state.currentSkinTone]);
    });
    createEffect(() => {
      async function updateDefaultFavoriteEmojis() {
        const { database } = state;
        const favs = (await Promise.all(MOST_COMMONLY_USED_EMOJI.map((unicode) => database.getEmojiByUnicodeOrName(unicode)))).filter(Boolean);
        state.defaultFavoriteEmojis = favs;
      }
      if (state.databaseLoaded) {
        updateDefaultFavoriteEmojis();
      }
    });
    function updateCustomEmoji() {
      const { customEmoji, database } = state;
      const databaseCustomEmoji = customEmoji || EMPTY_ARRAY;
      if (database.customEmoji !== databaseCustomEmoji) {
        database.customEmoji = databaseCustomEmoji;
      }
    }
    createEffect(() => {
      async function updateFavorites() {
        updateCustomEmoji();
        const { database, defaultFavoriteEmojis, numColumns } = state;
        const dbFavorites = await database.getTopFavoriteEmoji(numColumns);
        const favorites = await summarizeEmojis(uniqBy2([
          ...dbFavorites,
          ...defaultFavoriteEmojis
        ], (_) => _.unicode || _.name).slice(0, numColumns));
        state.currentFavorites = favorites;
      }
      if (state.databaseLoaded && state.defaultFavoriteEmojis) {
        updateFavorites();
      }
    });
    function calculateEmojiGridStyle(node) {
      resizeObserverAction(node, abortSignal, () => {
        {
          const style = getComputedStyle(refs.rootElement);
          const newNumColumns = parseInt(style.getPropertyValue("--num-columns"), 10);
          const newIsRtl = style.getPropertyValue("direction") === "rtl";
          state.numColumns = newNumColumns;
          state.isRtl = newIsRtl;
        }
      });
    }
    function updateOnIntersection(node) {
      intersectionObserverAction(node, abortSignal, (entries) => {
        for (const { target, isIntersecting } of entries) {
          target.classList.toggle("onscreen", isIntersecting);
        }
      });
    }
    createEffect(() => {
      async function updateEmojis() {
        const { searchText, currentGroup, databaseLoaded, customEmoji } = state;
        if (!databaseLoaded) {
          state.currentEmojis = [];
          state.searchMode = false;
        } else if (searchText.length >= MIN_SEARCH_TEXT_LENGTH2) {
          const newEmojis = await getEmojisBySearchQuery(searchText);
          if (state.searchText === searchText) {
            updateCurrentEmojis(newEmojis);
            updateSearchMode(true);
          }
        } else {
          const { id: currentGroupId } = currentGroup;
          if (currentGroupId !== -1 || customEmoji && customEmoji.length) {
            const newEmojis = await getEmojisByGroup(currentGroupId);
            if (state.currentGroup.id === currentGroupId) {
              updateCurrentEmojis(newEmojis);
              updateSearchMode(false);
            }
          }
        }
      }
      updateEmojis();
    });
    const resetScrollTopInRaf = () => {
      rAF(() => resetScrollTopIfPossible(refs.tabpanelElement));
    };
    createEffect(() => {
      const { currentEmojis, emojiVersion } = state;
      const zwjEmojisToCheck = currentEmojis.filter((emoji) => emoji.unicode).filter((emoji) => hasZwj(emoji) && !supportedZwjEmojis.has(emoji.unicode));
      if (!emojiVersion && zwjEmojisToCheck.length) {
        updateCurrentEmojis(currentEmojis);
        rAF(() => checkZwjSupportAndUpdate(zwjEmojisToCheck));
      } else {
        const newEmojis = emojiVersion ? currentEmojis : currentEmojis.filter(isZwjSupported);
        updateCurrentEmojis(newEmojis);
        resetScrollTopInRaf();
      }
    });
    function checkZwjSupportAndUpdate(zwjEmojisToCheck) {
      const allSupported = checkZwjSupport(zwjEmojisToCheck, refs.baselineEmoji, emojiToDomNode);
      if (allSupported) {
        resetScrollTopInRaf();
      } else {
        state.currentEmojis = [...state.currentEmojis];
      }
    }
    function isZwjSupported(emoji) {
      return !emoji.unicode || !hasZwj(emoji) || supportedZwjEmojis.get(emoji.unicode);
    }
    async function filterEmojisByVersion(emojis) {
      const emojiSupportLevel = state.emojiVersion || await detectEmojiSupportLevel();
      return emojis.filter(({ version }) => !version || version <= emojiSupportLevel);
    }
    async function summarizeEmojis(emojis) {
      return summarizeEmojisForUI(emojis, state.emojiVersion || await detectEmojiSupportLevel());
    }
    async function getEmojisByGroup(group) {
      const emoji = group === -1 ? state.customEmoji : await state.database.getEmojiByGroup(group);
      return summarizeEmojis(await filterEmojisByVersion(emoji));
    }
    async function getEmojisBySearchQuery(query) {
      return summarizeEmojis(await filterEmojisByVersion(await state.database.getEmojiBySearchQuery(query)));
    }
    createEffect(() => {
    });
    createEffect(() => {
      function calculateCurrentEmojisWithCategories() {
        const { searchMode, currentEmojis } = state;
        if (searchMode) {
          return [
            {
              category: "",
              emojis: currentEmojis
            }
          ];
        }
        const categoriesToEmoji = /* @__PURE__ */ new Map();
        for (const emoji of currentEmojis) {
          const category = emoji.category || "";
          let emojis = categoriesToEmoji.get(category);
          if (!emojis) {
            emojis = [];
            categoriesToEmoji.set(category, emojis);
          }
          emojis.push(emoji);
        }
        return [...categoriesToEmoji.entries()].map(([category, emojis]) => ({ category, emojis })).sort((a, b) => state.customCategorySorting(a.category, b.category));
      }
      const newEmojisWithCategories = calculateCurrentEmojisWithCategories();
      updateCurrentEmojisWithCategories(newEmojisWithCategories);
    });
    createEffect(() => {
      state.activeSearchItemId = state.activeSearchItem !== -1 && state.currentEmojis[state.activeSearchItem].id;
    });
    createEffect(() => {
      const { rawSearchText } = state;
      rIC(() => {
        state.searchText = (rawSearchText || "").trim();
        state.activeSearchItem = -1;
      });
    });
    function onSearchKeydown(event) {
      if (!state.searchMode || !state.currentEmojis.length) {
        return;
      }
      const goToNextOrPrevious = (previous) => {
        halt(event);
        state.activeSearchItem = incrementOrDecrement(previous, state.activeSearchItem, state.currentEmojis);
      };
      switch (event.key) {
        case "ArrowDown":
          return goToNextOrPrevious(false);
        case "ArrowUp":
          return goToNextOrPrevious(true);
        case "Enter":
          if (state.activeSearchItem === -1) {
            state.activeSearchItem = 0;
          } else {
            halt(event);
            return clickEmoji(state.currentEmojis[state.activeSearchItem].id);
          }
      }
    }
    function onNavClick(event) {
      const { target } = event;
      const closestTarget = target.closest(".nav-button");
      if (!closestTarget) {
        return;
      }
      const groupId = parseInt(closestTarget.dataset.groupId, 10);
      refs.searchElement.value = "";
      state.rawSearchText = "";
      state.searchText = "";
      state.activeSearchItem = -1;
      state.currentGroupIndex = state.groups.findIndex((_) => _.id === groupId);
    }
    function onNavKeydown(event) {
      const { target, key } = event;
      const doFocus = (el) => {
        if (el) {
          halt(event);
          el.focus();
        }
      };
      switch (key) {
        case "ArrowLeft":
          return doFocus(target.previousElementSibling);
        case "ArrowRight":
          return doFocus(target.nextElementSibling);
        case "Home":
          return doFocus(target.parentElement.firstElementChild);
        case "End":
          return doFocus(target.parentElement.lastElementChild);
      }
    }
    async function getDetailForClickEvent(unicodeOrName) {
      const emoji = await state.database.getEmojiByUnicodeOrName(unicodeOrName);
      const emojiSummary = [...state.currentEmojis, ...state.currentFavorites].find((_) => _.id === unicodeOrName);
      const skinTonedUnicode = emojiSummary.unicode && unicodeWithSkin(emojiSummary, state.currentSkinTone);
      await state.database.incrementFavoriteEmojiCount(unicodeOrName);
      return {
        emoji,
        skinTone: state.currentSkinTone,
        ...skinTonedUnicode && { unicode: skinTonedUnicode },
        ...emojiSummary.name && { name: emojiSummary.name }
      };
    }
    async function clickEmoji(unicodeOrName) {
      const promiseForDetail = getDetailForClickEvent(unicodeOrName);
      fireEvent("emoji-click-sync", promiseForDetail);
      fireEvent("emoji-click", await promiseForDetail);
    }
    function onEmojiClick(event) {
      const { target } = event;
      if (!target.classList.contains("emoji")) {
        return;
      }
      halt(event);
      const id = target.id.substring(4);
      clickEmoji(id);
    }
    function changeSkinTone(skinTone) {
      state.currentSkinTone = skinTone;
      state.skinTonePickerExpanded = false;
      focus("skintone-button");
      fireEvent("skin-tone-change", { skinTone });
      state.database.setPreferredSkinTone(skinTone);
    }
    function onSkinToneOptionsClick(event) {
      const { target: { id } } = event;
      const match = id && id.match(/^skintone-(\d)/);
      if (!match) {
        return;
      }
      halt(event);
      const skinTone = parseInt(match[1], 10);
      changeSkinTone(skinTone);
    }
    function onClickSkinToneButton(event) {
      state.skinTonePickerExpanded = !state.skinTonePickerExpanded;
      state.activeSkinTone = state.currentSkinTone;
      if (state.skinTonePickerExpanded) {
        halt(event);
        rAF(() => focus("skintone-list"));
      }
    }
    createEffect(() => {
      if (state.skinTonePickerExpanded) {
        refs.skinToneDropdown.addEventListener("transitionend", () => {
          state.skinTonePickerExpandedAfterAnimation = true;
        }, { once: true });
      } else {
        state.skinTonePickerExpandedAfterAnimation = false;
      }
    });
    function onSkinToneOptionsKeydown(event) {
      if (!state.skinTonePickerExpanded) {
        return;
      }
      const changeActiveSkinTone = async (nextSkinTone) => {
        halt(event);
        state.activeSkinTone = nextSkinTone;
      };
      switch (event.key) {
        case "ArrowUp":
          return changeActiveSkinTone(incrementOrDecrement(true, state.activeSkinTone, state.skinTones));
        case "ArrowDown":
          return changeActiveSkinTone(incrementOrDecrement(false, state.activeSkinTone, state.skinTones));
        case "Home":
          return changeActiveSkinTone(0);
        case "End":
          return changeActiveSkinTone(state.skinTones.length - 1);
        case "Enter":
          halt(event);
          return changeSkinTone(state.activeSkinTone);
        case "Escape":
          halt(event);
          state.skinTonePickerExpanded = false;
          return focus("skintone-button");
      }
    }
    function onSkinToneOptionsKeyup(event) {
      if (!state.skinTonePickerExpanded) {
        return;
      }
      switch (event.key) {
        case " ":
          halt(event);
          return changeSkinTone(state.activeSkinTone);
      }
    }
    async function onSkinToneOptionsFocusOut(event) {
      const { relatedTarget } = event;
      if (!relatedTarget || relatedTarget.id !== "skintone-list") {
        state.skinTonePickerExpanded = false;
      }
    }
    function onSearchInput(event) {
      state.rawSearchText = event.target.value;
    }
    return {
      $set(newState) {
        assign(state, newState);
      },
      $destroy() {
        abortController.abort();
      }
    };
  }
  var DEFAULT_DATA_SOURCE2 = "https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json";
  var DEFAULT_LOCALE2 = "en";
  var enI18n = {
    categoriesLabel: "Categories",
    emojiUnsupportedMessage: "Your browser does not support color emoji.",
    favoritesLabel: "Favorites",
    loadingMessage: "Loading\u2026",
    networkErrorMessage: "Could not load emoji.",
    regionLabel: "Emoji picker",
    searchDescription: "When search results are available, press up or down to select and enter to choose.",
    searchLabel: "Search",
    searchResultsLabel: "Search results",
    skinToneDescription: "When expanded, press up or down to select and enter to choose.",
    skinToneLabel: "Choose a skin tone (currently {skinTone})",
    skinTonesLabel: "Skin tones",
    skinTones: [
      "Default",
      "Light",
      "Medium-Light",
      "Medium",
      "Medium-Dark",
      "Dark"
    ],
    categories: {
      custom: "Custom",
      "smileys-emotion": "Smileys and emoticons",
      "people-body": "People and body",
      "animals-nature": "Animals and nature",
      "food-drink": "Food and drink",
      "travel-places": "Travel and places",
      activities: "Activities",
      objects: "Objects",
      symbols: "Symbols",
      flags: "Flags"
    }
  };
  var baseStyles = ':host{--emoji-size:1.375rem;--emoji-padding:0.5rem;--category-emoji-size:var(--emoji-size);--category-emoji-padding:var(--emoji-padding);--indicator-height:3px;--input-border-radius:0.5rem;--input-border-size:1px;--input-font-size:1rem;--input-line-height:1.5;--input-padding:0.25rem;--num-columns:8;--outline-size:2px;--border-size:1px;--border-radius:0;--skintone-border-radius:1rem;--category-font-size:1rem;display:flex;width:min-content;height:400px}:host,:host(.light){color-scheme:light;--background:#fff;--border-color:#e0e0e0;--indicator-color:#385ac1;--input-border-color:#999;--input-font-color:#111;--input-placeholder-color:#999;--outline-color:#999;--category-font-color:#111;--button-active-background:#e6e6e6;--button-hover-background:#d9d9d9}:host(.dark){color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}@media (prefers-color-scheme:dark){:host{color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}}:host([hidden]){display:none}button{margin:0;padding:0;border:0;background:0 0;box-shadow:none;-webkit-tap-highlight-color:transparent}button::-moz-focus-inner{border:0}input{padding:0;margin:0;line-height:1.15;font-family:inherit}input[type=search]{-webkit-appearance:none}:focus{outline:var(--outline-color) solid var(--outline-size);outline-offset:calc(-1*var(--outline-size))}:host([data-js-focus-visible]) :focus:not([data-focus-visible-added]){outline:0}:focus:not(:focus-visible){outline:0}.hide-focus{outline:0}*{box-sizing:border-box}.picker{contain:content;display:flex;flex-direction:column;background:var(--background);border:var(--border-size) solid var(--border-color);border-radius:var(--border-radius);width:100%;height:100%;overflow:hidden;--total-emoji-size:calc(var(--emoji-size) + (2 * var(--emoji-padding)));--total-category-emoji-size:calc(var(--category-emoji-size) + (2 * var(--category-emoji-padding)))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.hidden{opacity:0;pointer-events:none}.abs-pos{position:absolute;left:0;top:0}.gone{display:none!important}.skintone-button-wrapper,.skintone-list{background:var(--background);z-index:3}.skintone-button-wrapper.expanded{z-index:1}.skintone-list{position:absolute;inset-inline-end:0;top:0;z-index:2;overflow:visible;border-bottom:var(--border-size) solid var(--border-color);border-radius:0 0 var(--skintone-border-radius) var(--skintone-border-radius);will-change:transform;transition:transform .2s ease-in-out;transform-origin:center 0}@media (prefers-reduced-motion:reduce){.skintone-list{transition-duration:.001s}}@supports not (inset-inline-end:0){.skintone-list{right:0}}.skintone-list.no-animate{transition:none}.tabpanel{overflow-y:auto;scrollbar-gutter:stable;-webkit-overflow-scrolling:touch;will-change:transform;min-height:0;flex:1;contain:content}.emoji-menu{display:grid;grid-template-columns:repeat(var(--num-columns),var(--total-emoji-size));justify-content:space-around;align-items:flex-start;width:100%}.emoji-menu.visibility-auto{content-visibility:auto;contain-intrinsic-size:calc(var(--num-columns)*var(--total-emoji-size)) calc(var(--num-rows)*var(--total-emoji-size))}.category{padding:var(--emoji-padding);font-size:var(--category-font-size);color:var(--category-font-color)}.emoji,button.emoji{font-size:var(--emoji-size);display:flex;align-items:center;justify-content:center;border-radius:100%;height:var(--total-emoji-size);width:var(--total-emoji-size);line-height:1;overflow:hidden;font-family:var(--emoji-font-family);cursor:pointer}@media (hover:hover) and (pointer:fine){.emoji:hover,button.emoji:hover{background:var(--button-hover-background)}}.emoji.active,.emoji:active,button.emoji.active,button.emoji:active{background:var(--button-active-background)}.onscreen .custom-emoji::after{content:"";width:var(--emoji-size);height:var(--emoji-size);background-repeat:no-repeat;background-position:center center;background-size:contain;background-image:var(--custom-emoji-background)}.nav,.nav-button{align-items:center}.nav{display:grid;justify-content:space-between;contain:content}.nav-button{display:flex;justify-content:center}.nav-emoji{font-size:var(--category-emoji-size);width:var(--total-category-emoji-size);height:var(--total-category-emoji-size)}.indicator-wrapper{display:flex;border-bottom:1px solid var(--border-color)}.indicator{width:calc(100%/var(--num-groups));height:var(--indicator-height);opacity:var(--indicator-opacity);background-color:var(--indicator-color);will-change:transform,opacity;transition:opacity .1s linear,transform .25s ease-in-out}@media (prefers-reduced-motion:reduce){.indicator{will-change:opacity;transition:opacity .1s linear}}.pad-top,input.search{background:var(--background);width:100%}.pad-top{height:var(--emoji-padding);z-index:3}.search-row{display:flex;align-items:center;position:relative;padding-inline-start:var(--emoji-padding);padding-bottom:var(--emoji-padding)}.search-wrapper{flex:1;min-width:0}input.search{padding:var(--input-padding);border-radius:var(--input-border-radius);border:var(--input-border-size) solid var(--input-border-color);color:var(--input-font-color);font-size:var(--input-font-size);line-height:var(--input-line-height)}input.search::placeholder{color:var(--input-placeholder-color)}.favorites{overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:row;border-top:var(--border-size) solid var(--border-color);contain:content}.message{padding:var(--emoji-padding)}';
  var PROPS = [
    "customEmoji",
    "customCategorySorting",
    "database",
    "dataSource",
    "i18n",
    "locale",
    "skinToneEmoji",
    "emojiVersion"
  ];
  var EXTRA_STYLES = `:host{--emoji-font-family:${FONT_FAMILY}}`;
  var PickerElement = class extends HTMLElement {
    constructor(props) {
      super();
      this.attachShadow({ mode: "open" });
      const style = document.createElement("style");
      style.textContent = baseStyles + EXTRA_STYLES;
      this.shadowRoot.appendChild(style);
      this._ctx = {
        // Set defaults
        locale: DEFAULT_LOCALE2,
        dataSource: DEFAULT_DATA_SOURCE2,
        skinToneEmoji: DEFAULT_SKIN_TONE_EMOJI,
        customCategorySorting: DEFAULT_CATEGORY_SORTING,
        customEmoji: null,
        i18n: enI18n,
        emojiVersion: null,
        ...props
      };
      for (const prop of PROPS) {
        if (prop !== "database" && Object.prototype.hasOwnProperty.call(this, prop)) {
          this._ctx[prop] = this[prop];
          delete this[prop];
        }
      }
      this._dbFlush();
    }
    connectedCallback() {
      rescueElementPrototype(this);
      if (!this._cmp) {
        this._cmp = createRoot(this.shadowRoot, this._ctx);
      }
    }
    disconnectedCallback() {
      rescueElementPrototype(this);
      qM(() => {
        if (!this.isConnected && this._cmp) {
          this._cmp.$destroy();
          this._cmp = void 0;
          const { database } = this._ctx;
          database.close().catch((err) => console.error(err));
        }
      });
    }
    static get observedAttributes() {
      return ["locale", "data-source", "skin-tone-emoji", "emoji-version"];
    }
    attributeChangedCallback(attrName, oldValue, newValue) {
      this._set(
        // convert from kebab-case to camelcase
        // see https://github.com/sveltejs/svelte/issues/3852#issuecomment-665037015
        attrName.replace(/-([a-z])/g, (_, up) => up.toUpperCase()),
        // convert string attribute to float if necessary
        attrName === "emoji-version" ? parseFloat(newValue) : newValue
      );
    }
    _set(prop, newValue) {
      this._ctx[prop] = newValue;
      if (this._cmp) {
        this._cmp.$set({ [prop]: newValue });
      }
      if (["locale", "dataSource"].includes(prop)) {
        this._dbFlush();
      }
    }
    _dbCreate() {
      const { locale, dataSource, database } = this._ctx;
      if (!database || database.locale !== locale || database.dataSource !== dataSource) {
        this._set("database", new Database({ locale, dataSource }));
      }
    }
    // Update the Database in one microtask if the locale/dataSource change. We do one microtask
    // so we don't create two Databases if e.g. both the locale and the dataSource change
    _dbFlush() {
      qM(() => this._dbCreate());
    }
  };
  var definitions = {};
  for (const prop of PROPS) {
    definitions[prop] = {
      get() {
        if (prop === "database") {
          this._dbCreate();
        }
        return this._ctx[prop];
      },
      set(val) {
        if (prop === "database") {
          throw new Error("database is read-only");
        }
        this._set(prop, val);
      }
    };
  }
  Object.defineProperties(PickerElement.prototype, definitions);
  function rescueElementPrototype(element) {
    if (!(element instanceof PickerElement)) {
      Object.setPrototypeOf(element, customElements.get(element.tagName.toLowerCase()).prototype);
    }
  }
  if (!customElements.get("emoji-picker")) {
    customElements.define("emoji-picker", PickerElement);
  }
})();

(() => {
  var __create = Object.create;
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __getProtoOf = Object.getPrototypeOf;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __commonJS = (cb, mod) => function __require() {
    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
    // If the importer is in node compatibility mode or this is not an ESM
    // file that has been converted to a CommonJS file using a Babel-
    // compatible transform (i.e. "__esModule" has not been set), then set
    // "default" to the CommonJS "module.exports" for node compatibility.
    isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
    mod
  ));

  // node_modules/highlight.js/lib/core.js
  var require_core = __commonJS({
    "node_modules/highlight.js/lib/core.js"(exports, module) {
      function deepFreeze(obj) {
        if (obj instanceof Map) {
          obj.clear = obj.delete = obj.set = function() {
            throw new Error("map is read-only");
          };
        } else if (obj instanceof Set) {
          obj.add = obj.clear = obj.delete = function() {
            throw new Error("set is read-only");
          };
        }
        Object.freeze(obj);
        Object.getOwnPropertyNames(obj).forEach((name) => {
          const prop = obj[name];
          const type = typeof prop;
          if ((type === "object" || type === "function") && !Object.isFrozen(prop)) {
            deepFreeze(prop);
          }
        });
        return obj;
      }
      var Response = class {
        /**
         * @param {CompiledMode} mode
         */
        constructor(mode) {
          if (mode.data === void 0) mode.data = {};
          this.data = mode.data;
          this.isMatchIgnored = false;
        }
        ignoreMatch() {
          this.isMatchIgnored = true;
        }
      };
      function escapeHTML(value) {
        return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
      }
      function inherit$1(original, ...objects) {
        const result = /* @__PURE__ */ Object.create(null);
        for (const key in original) {
          result[key] = original[key];
        }
        objects.forEach(function(obj) {
          for (const key in obj) {
            result[key] = obj[key];
          }
        });
        return (
          /** @type {T} */
          result
        );
      }
      var SPAN_CLOSE = "</span>";
      var emitsWrappingTags = (node) => {
        return !!node.scope;
      };
      var scopeToCSSClass = (name, { prefix }) => {
        if (name.startsWith("language:")) {
          return name.replace("language:", "language-");
        }
        if (name.includes(".")) {
          const pieces = name.split(".");
          return [
            `${prefix}${pieces.shift()}`,
            ...pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)
          ].join(" ");
        }
        return `${prefix}${name}`;
      };
      var HTMLRenderer = class {
        /**
         * Creates a new HTMLRenderer
         *
         * @param {Tree} parseTree - the parse tree (must support `walk` API)
         * @param {{classPrefix: string}} options
         */
        constructor(parseTree, options) {
          this.buffer = "";
          this.classPrefix = options.classPrefix;
          parseTree.walk(this);
        }
        /**
         * Adds texts to the output stream
         *
         * @param {string} text */
        addText(text) {
          this.buffer += escapeHTML(text);
        }
        /**
         * Adds a node open to the output stream (if needed)
         *
         * @param {Node} node */
        openNode(node) {
          if (!emitsWrappingTags(node)) return;
          const className = scopeToCSSClass(
            node.scope,
            { prefix: this.classPrefix }
          );
          this.span(className);
        }
        /**
         * Adds a node close to the output stream (if needed)
         *
         * @param {Node} node */
        closeNode(node) {
          if (!emitsWrappingTags(node)) return;
          this.buffer += SPAN_CLOSE;
        }
        /**
         * returns the accumulated buffer
        */
        value() {
          return this.buffer;
        }
        // helpers
        /**
         * Builds a span element
         *
         * @param {string} className */
        span(className) {
          this.buffer += `<span class="${className}">`;
        }
      };
      var newNode = (opts = {}) => {
        const result = { children: [] };
        Object.assign(result, opts);
        return result;
      };
      var TokenTree = class _TokenTree {
        constructor() {
          this.rootNode = newNode();
          this.stack = [this.rootNode];
        }
        get top() {
          return this.stack[this.stack.length - 1];
        }
        get root() {
          return this.rootNode;
        }
        /** @param {Node} node */
        add(node) {
          this.top.children.push(node);
        }
        /** @param {string} scope */
        openNode(scope) {
          const node = newNode({ scope });
          this.add(node);
          this.stack.push(node);
        }
        closeNode() {
          if (this.stack.length > 1) {
            return this.stack.pop();
          }
          return void 0;
        }
        closeAllNodes() {
          while (this.closeNode()) ;
        }
        toJSON() {
          return JSON.stringify(this.rootNode, null, 4);
        }
        /**
         * @typedef { import("./html_renderer").Renderer } Renderer
         * @param {Renderer} builder
         */
        walk(builder) {
          return this.constructor._walk(builder, this.rootNode);
        }
        /**
         * @param {Renderer} builder
         * @param {Node} node
         */
        static _walk(builder, node) {
          if (typeof node === "string") {
            builder.addText(node);
          } else if (node.children) {
            builder.openNode(node);
            node.children.forEach((child) => this._walk(builder, child));
            builder.closeNode(node);
          }
          return builder;
        }
        /**
         * @param {Node} node
         */
        static _collapse(node) {
          if (typeof node === "string") return;
          if (!node.children) return;
          if (node.children.every((el) => typeof el === "string")) {
            node.children = [node.children.join("")];
          } else {
            node.children.forEach((child) => {
              _TokenTree._collapse(child);
            });
          }
        }
      };
      var TokenTreeEmitter = class extends TokenTree {
        /**
         * @param {*} options
         */
        constructor(options) {
          super();
          this.options = options;
        }
        /**
         * @param {string} text
         */
        addText(text) {
          if (text === "") {
            return;
          }
          this.add(text);
        }
        /** @param {string} scope */
        startScope(scope) {
          this.openNode(scope);
        }
        endScope() {
          this.closeNode();
        }
        /**
         * @param {Emitter & {root: DataNode}} emitter
         * @param {string} name
         */
        __addSublanguage(emitter, name) {
          const node = emitter.root;
          if (name) node.scope = `language:${name}`;
          this.add(node);
        }
        toHTML() {
          const renderer = new HTMLRenderer(this, this.options);
          return renderer.value();
        }
        finalize() {
          this.closeAllNodes();
          return true;
        }
      };
      function source(re) {
        if (!re) return null;
        if (typeof re === "string") return re;
        return re.source;
      }
      function lookahead(re) {
        return concat("(?=", re, ")");
      }
      function anyNumberOfTimes(re) {
        return concat("(?:", re, ")*");
      }
      function optional(re) {
        return concat("(?:", re, ")?");
      }
      function concat(...args) {
        const joined = args.map((x) => source(x)).join("");
        return joined;
      }
      function stripOptionsFromArgs(args) {
        const opts = args[args.length - 1];
        if (typeof opts === "object" && opts.constructor === Object) {
          args.splice(args.length - 1, 1);
          return opts;
        } else {
          return {};
        }
      }
      function either(...args) {
        const opts = stripOptionsFromArgs(args);
        const joined = "(" + (opts.capture ? "" : "?:") + args.map((x) => source(x)).join("|") + ")";
        return joined;
      }
      function countMatchGroups(re) {
        return new RegExp(re.toString() + "|").exec("").length - 1;
      }
      function startsWith(re, lexeme) {
        const match = re && re.exec(lexeme);
        return match && match.index === 0;
      }
      var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
      function _rewriteBackreferences(regexps, { joinWith }) {
        let numCaptures = 0;
        return regexps.map((regex) => {
          numCaptures += 1;
          const offset = numCaptures;
          let re = source(regex);
          let out = "";
          while (re.length > 0) {
            const match = BACKREF_RE.exec(re);
            if (!match) {
              out += re;
              break;
            }
            out += re.substring(0, match.index);
            re = re.substring(match.index + match[0].length);
            if (match[0][0] === "\\" && match[1]) {
              out += "\\" + String(Number(match[1]) + offset);
            } else {
              out += match[0];
              if (match[0] === "(") {
                numCaptures++;
              }
            }
          }
          return out;
        }).map((re) => `(${re})`).join(joinWith);
      }
      var MATCH_NOTHING_RE = /\b\B/;
      var IDENT_RE = "[a-zA-Z]\\w*";
      var UNDERSCORE_IDENT_RE = "[a-zA-Z_]\\w*";
      var NUMBER_RE = "\\b\\d+(\\.\\d+)?";
      var C_NUMBER_RE = "(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";
      var BINARY_NUMBER_RE = "\\b(0b[01]+)";
      var RE_STARTERS_RE = "!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";
      var SHEBANG = (opts = {}) => {
        const beginShebang = /^#![ ]*\//;
        if (opts.binary) {
          opts.begin = concat(
            beginShebang,
            /.*\b/,
            opts.binary,
            /\b.*/
          );
        }
        return inherit$1({
          scope: "meta",
          begin: beginShebang,
          end: /$/,
          relevance: 0,
          /** @type {ModeCallback} */
          "on:begin": (m, resp) => {
            if (m.index !== 0) resp.ignoreMatch();
          }
        }, opts);
      };
      var BACKSLASH_ESCAPE = {
        begin: "\\\\[\\s\\S]",
        relevance: 0
      };
      var APOS_STRING_MODE = {
        scope: "string",
        begin: "'",
        end: "'",
        illegal: "\\n",
        contains: [BACKSLASH_ESCAPE]
      };
      var QUOTE_STRING_MODE = {
        scope: "string",
        begin: '"',
        end: '"',
        illegal: "\\n",
        contains: [BACKSLASH_ESCAPE]
      };
      var PHRASAL_WORDS_MODE = {
        begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
      };
      var COMMENT = function(begin, end, modeOptions = {}) {
        const mode = inherit$1(
          {
            scope: "comment",
            begin,
            end,
            contains: []
          },
          modeOptions
        );
        mode.contains.push({
          scope: "doctag",
          // hack to avoid the space from being included. the space is necessary to
          // match here to prevent the plain text rule below from gobbling up doctags
          begin: "[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
          end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,
          excludeBegin: true,
          relevance: 0
        });
        const ENGLISH_WORD = either(
          // list of common 1 and 2 letter words in English
          "I",
          "a",
          "is",
          "so",
          "us",
          "to",
          "at",
          "if",
          "in",
          "it",
          "on",
          // note: this is not an exhaustive list of contractions, just popular ones
          /[A-Za-z]+['](d|ve|re|ll|t|s|n)/,
          // contractions - can't we'd they're let's, etc
          /[A-Za-z]+[-][a-z]+/,
          // `no-way`, etc.
          /[A-Za-z][a-z]{2,}/
          // allow capitalized words at beginning of sentences
        );
        mode.contains.push(
          {
            // TODO: how to include ", (, ) without breaking grammars that use these for
            // comment delimiters?
            // begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/
            // ---
            // this tries to find sequences of 3 english words in a row (without any
            // "programming" type syntax) this gives us a strong signal that we've
            // TRULY found a comment - vs perhaps scanning with the wrong language.
            // It's possible to find something that LOOKS like the start of the
            // comment - but then if there is no readable text - good chance it is a
            // false match and not a comment.
            //
            // for a visual example please see:
            // https://github.com/highlightjs/highlight.js/issues/2827
            begin: concat(
              /[ ]+/,
              // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */
              "(",
              ENGLISH_WORD,
              /[.]?[:]?([.][ ]|[ ])/,
              "){3}"
            )
            // look for 3 words in a row
          }
        );
        return mode;
      };
      var C_LINE_COMMENT_MODE = COMMENT("//", "$");
      var C_BLOCK_COMMENT_MODE = COMMENT("/\\*", "\\*/");
      var HASH_COMMENT_MODE = COMMENT("#", "$");
      var NUMBER_MODE = {
        scope: "number",
        begin: NUMBER_RE,
        relevance: 0
      };
      var C_NUMBER_MODE = {
        scope: "number",
        begin: C_NUMBER_RE,
        relevance: 0
      };
      var BINARY_NUMBER_MODE = {
        scope: "number",
        begin: BINARY_NUMBER_RE,
        relevance: 0
      };
      var REGEXP_MODE = {
        scope: "regexp",
        begin: /\/(?=[^/\n]*\/)/,
        end: /\/[gimuy]*/,
        contains: [
          BACKSLASH_ESCAPE,
          {
            begin: /\[/,
            end: /\]/,
            relevance: 0,
            contains: [BACKSLASH_ESCAPE]
          }
        ]
      };
      var TITLE_MODE = {
        scope: "title",
        begin: IDENT_RE,
        relevance: 0
      };
      var UNDERSCORE_TITLE_MODE = {
        scope: "title",
        begin: UNDERSCORE_IDENT_RE,
        relevance: 0
      };
      var METHOD_GUARD = {
        // excludes method names from keyword processing
        begin: "\\.\\s*" + UNDERSCORE_IDENT_RE,
        relevance: 0
      };
      var END_SAME_AS_BEGIN = function(mode) {
        return Object.assign(
          mode,
          {
            /** @type {ModeCallback} */
            "on:begin": (m, resp) => {
              resp.data._beginMatch = m[1];
            },
            /** @type {ModeCallback} */
            "on:end": (m, resp) => {
              if (resp.data._beginMatch !== m[1]) resp.ignoreMatch();
            }
          }
        );
      };
      var MODES = /* @__PURE__ */ Object.freeze({
        __proto__: null,
        APOS_STRING_MODE,
        BACKSLASH_ESCAPE,
        BINARY_NUMBER_MODE,
        BINARY_NUMBER_RE,
        COMMENT,
        C_BLOCK_COMMENT_MODE,
        C_LINE_COMMENT_MODE,
        C_NUMBER_MODE,
        C_NUMBER_RE,
        END_SAME_AS_BEGIN,
        HASH_COMMENT_MODE,
        IDENT_RE,
        MATCH_NOTHING_RE,
        METHOD_GUARD,
        NUMBER_MODE,
        NUMBER_RE,
        PHRASAL_WORDS_MODE,
        QUOTE_STRING_MODE,
        REGEXP_MODE,
        RE_STARTERS_RE,
        SHEBANG,
        TITLE_MODE,
        UNDERSCORE_IDENT_RE,
        UNDERSCORE_TITLE_MODE
      });
      function skipIfHasPrecedingDot(match, response) {
        const before = match.input[match.index - 1];
        if (before === ".") {
          response.ignoreMatch();
        }
      }
      function scopeClassName(mode, _parent) {
        if (mode.className !== void 0) {
          mode.scope = mode.className;
          delete mode.className;
        }
      }
      function beginKeywords(mode, parent) {
        if (!parent) return;
        if (!mode.beginKeywords) return;
        mode.begin = "\\b(" + mode.beginKeywords.split(" ").join("|") + ")(?!\\.)(?=\\b|\\s)";
        mode.__beforeBegin = skipIfHasPrecedingDot;
        mode.keywords = mode.keywords || mode.beginKeywords;
        delete mode.beginKeywords;
        if (mode.relevance === void 0) mode.relevance = 0;
      }
      function compileIllegal(mode, _parent) {
        if (!Array.isArray(mode.illegal)) return;
        mode.illegal = either(...mode.illegal);
      }
      function compileMatch(mode, _parent) {
        if (!mode.match) return;
        if (mode.begin || mode.end) throw new Error("begin & end are not supported with match");
        mode.begin = mode.match;
        delete mode.match;
      }
      function compileRelevance(mode, _parent) {
        if (mode.relevance === void 0) mode.relevance = 1;
      }
      var beforeMatchExt = (mode, parent) => {
        if (!mode.beforeMatch) return;
        if (mode.starts) throw new Error("beforeMatch cannot be used with starts");
        const originalMode = Object.assign({}, mode);
        Object.keys(mode).forEach((key) => {
          delete mode[key];
        });
        mode.keywords = originalMode.keywords;
        mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin));
        mode.starts = {
          relevance: 0,
          contains: [
            Object.assign(originalMode, { endsParent: true })
          ]
        };
        mode.relevance = 0;
        delete originalMode.beforeMatch;
      };
      var COMMON_KEYWORDS = [
        "of",
        "and",
        "for",
        "in",
        "not",
        "or",
        "if",
        "then",
        "parent",
        // common variable name
        "list",
        // common variable name
        "value"
        // common variable name
      ];
      var DEFAULT_KEYWORD_SCOPE = "keyword";
      function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) {
        const compiledKeywords = /* @__PURE__ */ Object.create(null);
        if (typeof rawKeywords === "string") {
          compileList(scopeName, rawKeywords.split(" "));
        } else if (Array.isArray(rawKeywords)) {
          compileList(scopeName, rawKeywords);
        } else {
          Object.keys(rawKeywords).forEach(function(scopeName2) {
            Object.assign(
              compiledKeywords,
              compileKeywords(rawKeywords[scopeName2], caseInsensitive, scopeName2)
            );
          });
        }
        return compiledKeywords;
        function compileList(scopeName2, keywordList) {
          if (caseInsensitive) {
            keywordList = keywordList.map((x) => x.toLowerCase());
          }
          keywordList.forEach(function(keyword) {
            const pair = keyword.split("|");
            compiledKeywords[pair[0]] = [scopeName2, scoreForKeyword(pair[0], pair[1])];
          });
        }
      }
      function scoreForKeyword(keyword, providedScore) {
        if (providedScore) {
          return Number(providedScore);
        }
        return commonKeyword(keyword) ? 0 : 1;
      }
      function commonKeyword(keyword) {
        return COMMON_KEYWORDS.includes(keyword.toLowerCase());
      }
      var seenDeprecations = {};
      var error = (message) => {
        console.error(message);
      };
      var warn = (message, ...args) => {
        console.log(`WARN: ${message}`, ...args);
      };
      var deprecated = (version2, message) => {
        if (seenDeprecations[`${version2}/${message}`]) return;
        console.log(`Deprecated as of ${version2}. ${message}`);
        seenDeprecations[`${version2}/${message}`] = true;
      };
      var MultiClassError = new Error();
      function remapScopeNames(mode, regexes, { key }) {
        let offset = 0;
        const scopeNames = mode[key];
        const emit = {};
        const positions = {};
        for (let i = 1; i <= regexes.length; i++) {
          positions[i + offset] = scopeNames[i];
          emit[i + offset] = true;
          offset += countMatchGroups(regexes[i - 1]);
        }
        mode[key] = positions;
        mode[key]._emit = emit;
        mode[key]._multi = true;
      }
      function beginMultiClass(mode) {
        if (!Array.isArray(mode.begin)) return;
        if (mode.skip || mode.excludeBegin || mode.returnBegin) {
          error("skip, excludeBegin, returnBegin not compatible with beginScope: {}");
          throw MultiClassError;
        }
        if (typeof mode.beginScope !== "object" || mode.beginScope === null) {
          error("beginScope must be object");
          throw MultiClassError;
        }
        remapScopeNames(mode, mode.begin, { key: "beginScope" });
        mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" });
      }
      function endMultiClass(mode) {
        if (!Array.isArray(mode.end)) return;
        if (mode.skip || mode.excludeEnd || mode.returnEnd) {
          error("skip, excludeEnd, returnEnd not compatible with endScope: {}");
          throw MultiClassError;
        }
        if (typeof mode.endScope !== "object" || mode.endScope === null) {
          error("endScope must be object");
          throw MultiClassError;
        }
        remapScopeNames(mode, mode.end, { key: "endScope" });
        mode.end = _rewriteBackreferences(mode.end, { joinWith: "" });
      }
      function scopeSugar(mode) {
        if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) {
          mode.beginScope = mode.scope;
          delete mode.scope;
        }
      }
      function MultiClass(mode) {
        scopeSugar(mode);
        if (typeof mode.beginScope === "string") {
          mode.beginScope = { _wrap: mode.beginScope };
        }
        if (typeof mode.endScope === "string") {
          mode.endScope = { _wrap: mode.endScope };
        }
        beginMultiClass(mode);
        endMultiClass(mode);
      }
      function compileLanguage(language) {
        function langRe(value, global) {
          return new RegExp(
            source(value),
            "m" + (language.case_insensitive ? "i" : "") + (language.unicodeRegex ? "u" : "") + (global ? "g" : "")
          );
        }
        class MultiRegex {
          constructor() {
            this.matchIndexes = {};
            this.regexes = [];
            this.matchAt = 1;
            this.position = 0;
          }
          // @ts-ignore
          addRule(re, opts) {
            opts.position = this.position++;
            this.matchIndexes[this.matchAt] = opts;
            this.regexes.push([opts, re]);
            this.matchAt += countMatchGroups(re) + 1;
          }
          compile() {
            if (this.regexes.length === 0) {
              this.exec = () => null;
            }
            const terminators = this.regexes.map((el) => el[1]);
            this.matcherRe = langRe(_rewriteBackreferences(terminators, { joinWith: "|" }), true);
            this.lastIndex = 0;
          }
          /** @param {string} s */
          exec(s) {
            this.matcherRe.lastIndex = this.lastIndex;
            const match = this.matcherRe.exec(s);
            if (!match) {
              return null;
            }
            const i = match.findIndex((el, i2) => i2 > 0 && el !== void 0);
            const matchData = this.matchIndexes[i];
            match.splice(0, i);
            return Object.assign(match, matchData);
          }
        }
        class ResumableMultiRegex {
          constructor() {
            this.rules = [];
            this.multiRegexes = [];
            this.count = 0;
            this.lastIndex = 0;
            this.regexIndex = 0;
          }
          // @ts-ignore
          getMatcher(index) {
            if (this.multiRegexes[index]) return this.multiRegexes[index];
            const matcher = new MultiRegex();
            this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts));
            matcher.compile();
            this.multiRegexes[index] = matcher;
            return matcher;
          }
          resumingScanAtSamePosition() {
            return this.regexIndex !== 0;
          }
          considerAll() {
            this.regexIndex = 0;
          }
          // @ts-ignore
          addRule(re, opts) {
            this.rules.push([re, opts]);
            if (opts.type === "begin") this.count++;
          }
          /** @param {string} s */
          exec(s) {
            const m = this.getMatcher(this.regexIndex);
            m.lastIndex = this.lastIndex;
            let result = m.exec(s);
            if (this.resumingScanAtSamePosition()) {
              if (result && result.index === this.lastIndex) ;
              else {
                const m2 = this.getMatcher(0);
                m2.lastIndex = this.lastIndex + 1;
                result = m2.exec(s);
              }
            }
            if (result) {
              this.regexIndex += result.position + 1;
              if (this.regexIndex === this.count) {
                this.considerAll();
              }
            }
            return result;
          }
        }
        function buildModeRegex(mode) {
          const mm = new ResumableMultiRegex();
          mode.contains.forEach((term) => mm.addRule(term.begin, { rule: term, type: "begin" }));
          if (mode.terminatorEnd) {
            mm.addRule(mode.terminatorEnd, { type: "end" });
          }
          if (mode.illegal) {
            mm.addRule(mode.illegal, { type: "illegal" });
          }
          return mm;
        }
        function compileMode(mode, parent) {
          const cmode = (
            /** @type CompiledMode */
            mode
          );
          if (mode.isCompiled) return cmode;
          [
            scopeClassName,
            // do this early so compiler extensions generally don't have to worry about
            // the distinction between match/begin
            compileMatch,
            MultiClass,
            beforeMatchExt
          ].forEach((ext) => ext(mode, parent));
          language.compilerExtensions.forEach((ext) => ext(mode, parent));
          mode.__beforeBegin = null;
          [
            beginKeywords,
            // do this later so compiler extensions that come earlier have access to the
            // raw array if they wanted to perhaps manipulate it, etc.
            compileIllegal,
            // default to 1 relevance if not specified
            compileRelevance
          ].forEach((ext) => ext(mode, parent));
          mode.isCompiled = true;
          let keywordPattern = null;
          if (typeof mode.keywords === "object" && mode.keywords.$pattern) {
            mode.keywords = Object.assign({}, mode.keywords);
            keywordPattern = mode.keywords.$pattern;
            delete mode.keywords.$pattern;
          }
          keywordPattern = keywordPattern || /\w+/;
          if (mode.keywords) {
            mode.keywords = compileKeywords(mode.keywords, language.case_insensitive);
          }
          cmode.keywordPatternRe = langRe(keywordPattern, true);
          if (parent) {
            if (!mode.begin) mode.begin = /\B|\b/;
            cmode.beginRe = langRe(cmode.begin);
            if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/;
            if (mode.end) cmode.endRe = langRe(cmode.end);
            cmode.terminatorEnd = source(cmode.end) || "";
            if (mode.endsWithParent && parent.terminatorEnd) {
              cmode.terminatorEnd += (mode.end ? "|" : "") + parent.terminatorEnd;
            }
          }
          if (mode.illegal) cmode.illegalRe = langRe(
            /** @type {RegExp | string} */
            mode.illegal
          );
          if (!mode.contains) mode.contains = [];
          mode.contains = [].concat(...mode.contains.map(function(c) {
            return expandOrCloneMode(c === "self" ? mode : c);
          }));
          mode.contains.forEach(function(c) {
            compileMode(
              /** @type Mode */
              c,
              cmode
            );
          });
          if (mode.starts) {
            compileMode(mode.starts, parent);
          }
          cmode.matcher = buildModeRegex(cmode);
          return cmode;
        }
        if (!language.compilerExtensions) language.compilerExtensions = [];
        if (language.contains && language.contains.includes("self")) {
          throw new Error("ERR: contains `self` is not supported at the top-level of a language.  See documentation.");
        }
        language.classNameAliases = inherit$1(language.classNameAliases || {});
        return compileMode(
          /** @type Mode */
          language
        );
      }
      function dependencyOnParent(mode) {
        if (!mode) return false;
        return mode.endsWithParent || dependencyOnParent(mode.starts);
      }
      function expandOrCloneMode(mode) {
        if (mode.variants && !mode.cachedVariants) {
          mode.cachedVariants = mode.variants.map(function(variant) {
            return inherit$1(mode, { variants: null }, variant);
          });
        }
        if (mode.cachedVariants) {
          return mode.cachedVariants;
        }
        if (dependencyOnParent(mode)) {
          return inherit$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null });
        }
        if (Object.isFrozen(mode)) {
          return inherit$1(mode);
        }
        return mode;
      }
      var version = "11.11.1";
      var HTMLInjectionError = class extends Error {
        constructor(reason, html) {
          super(reason);
          this.name = "HTMLInjectionError";
          this.html = html;
        }
      };
      var escape = escapeHTML;
      var inherit = inherit$1;
      var NO_MATCH = Symbol("nomatch");
      var MAX_KEYWORD_HITS = 7;
      var HLJS = function(hljs) {
        const languages = /* @__PURE__ */ Object.create(null);
        const aliases = /* @__PURE__ */ Object.create(null);
        const plugins = [];
        let SAFE_MODE = true;
        const LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?";
        const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: "Plain text", contains: [] };
        let options = {
          ignoreUnescapedHTML: false,
          throwUnescapedHTML: false,
          noHighlightRe: /^(no-?highlight)$/i,
          languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i,
          classPrefix: "hljs-",
          cssSelector: "pre code",
          languages: null,
          // beta configuration options, subject to change, welcome to discuss
          // https://github.com/highlightjs/highlight.js/issues/1086
          __emitter: TokenTreeEmitter
        };
        function shouldNotHighlight(languageName) {
          return options.noHighlightRe.test(languageName);
        }
        function blockLanguage(block) {
          let classes = block.className + " ";
          classes += block.parentNode ? block.parentNode.className : "";
          const match = options.languageDetectRe.exec(classes);
          if (match) {
            const language = getLanguage(match[1]);
            if (!language) {
              warn(LANGUAGE_NOT_FOUND.replace("{}", match[1]));
              warn("Falling back to no-highlight mode for this block.", block);
            }
            return language ? match[1] : "no-highlight";
          }
          return classes.split(/\s+/).find((_class) => shouldNotHighlight(_class) || getLanguage(_class));
        }
        function highlight2(codeOrLanguageName, optionsOrCode, ignoreIllegals) {
          let code = "";
          let languageName = "";
          if (typeof optionsOrCode === "object") {
            code = codeOrLanguageName;
            ignoreIllegals = optionsOrCode.ignoreIllegals;
            languageName = optionsOrCode.language;
          } else {
            deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated.");
            deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277");
            languageName = codeOrLanguageName;
            code = optionsOrCode;
          }
          if (ignoreIllegals === void 0) {
            ignoreIllegals = true;
          }
          const context = {
            code,
            language: languageName
          };
          fire("before:highlight", context);
          const result = context.result ? context.result : _highlight(context.language, context.code, ignoreIllegals);
          result.code = context.code;
          fire("after:highlight", result);
          return result;
        }
        function _highlight(languageName, codeToHighlight, ignoreIllegals, continuation) {
          const keywordHits = /* @__PURE__ */ Object.create(null);
          function keywordData(mode, matchText) {
            return mode.keywords[matchText];
          }
          function processKeywords() {
            if (!top.keywords) {
              emitter.addText(modeBuffer);
              return;
            }
            let lastIndex = 0;
            top.keywordPatternRe.lastIndex = 0;
            let match = top.keywordPatternRe.exec(modeBuffer);
            let buf = "";
            while (match) {
              buf += modeBuffer.substring(lastIndex, match.index);
              const word = language.case_insensitive ? match[0].toLowerCase() : match[0];
              const data = keywordData(top, word);
              if (data) {
                const [kind, keywordRelevance] = data;
                emitter.addText(buf);
                buf = "";
                keywordHits[word] = (keywordHits[word] || 0) + 1;
                if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance;
                if (kind.startsWith("_")) {
                  buf += match[0];
                } else {
                  const cssClass = language.classNameAliases[kind] || kind;
                  emitKeyword(match[0], cssClass);
                }
              } else {
                buf += match[0];
              }
              lastIndex = top.keywordPatternRe.lastIndex;
              match = top.keywordPatternRe.exec(modeBuffer);
            }
            buf += modeBuffer.substring(lastIndex);
            emitter.addText(buf);
          }
          function processSubLanguage() {
            if (modeBuffer === "") return;
            let result2 = null;
            if (typeof top.subLanguage === "string") {
              if (!languages[top.subLanguage]) {
                emitter.addText(modeBuffer);
                return;
              }
              result2 = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]);
              continuations[top.subLanguage] = /** @type {CompiledMode} */
              result2._top;
            } else {
              result2 = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null);
            }
            if (top.relevance > 0) {
              relevance += result2.relevance;
            }
            emitter.__addSublanguage(result2._emitter, result2.language);
          }
          function processBuffer() {
            if (top.subLanguage != null) {
              processSubLanguage();
            } else {
              processKeywords();
            }
            modeBuffer = "";
          }
          function emitKeyword(keyword, scope) {
            if (keyword === "") return;
            emitter.startScope(scope);
            emitter.addText(keyword);
            emitter.endScope();
          }
          function emitMultiClass(scope, match) {
            let i = 1;
            const max = match.length - 1;
            while (i <= max) {
              if (!scope._emit[i]) {
                i++;
                continue;
              }
              const klass = language.classNameAliases[scope[i]] || scope[i];
              const text = match[i];
              if (klass) {
                emitKeyword(text, klass);
              } else {
                modeBuffer = text;
                processKeywords();
                modeBuffer = "";
              }
              i++;
            }
          }
          function startNewMode(mode, match) {
            if (mode.scope && typeof mode.scope === "string") {
              emitter.openNode(language.classNameAliases[mode.scope] || mode.scope);
            }
            if (mode.beginScope) {
              if (mode.beginScope._wrap) {
                emitKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap);
                modeBuffer = "";
              } else if (mode.beginScope._multi) {
                emitMultiClass(mode.beginScope, match);
                modeBuffer = "";
              }
            }
            top = Object.create(mode, { parent: { value: top } });
            return top;
          }
          function endOfMode(mode, match, matchPlusRemainder) {
            let matched = startsWith(mode.endRe, matchPlusRemainder);
            if (matched) {
              if (mode["on:end"]) {
                const resp = new Response(mode);
                mode["on:end"](match, resp);
                if (resp.isMatchIgnored) matched = false;
              }
              if (matched) {
                while (mode.endsParent && mode.parent) {
                  mode = mode.parent;
                }
                return mode;
              }
            }
            if (mode.endsWithParent) {
              return endOfMode(mode.parent, match, matchPlusRemainder);
            }
          }
          function doIgnore(lexeme) {
            if (top.matcher.regexIndex === 0) {
              modeBuffer += lexeme[0];
              return 1;
            } else {
              resumeScanAtSamePosition = true;
              return 0;
            }
          }
          function doBeginMatch(match) {
            const lexeme = match[0];
            const newMode = match.rule;
            const resp = new Response(newMode);
            const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]];
            for (const cb of beforeCallbacks) {
              if (!cb) continue;
              cb(match, resp);
              if (resp.isMatchIgnored) return doIgnore(lexeme);
            }
            if (newMode.skip) {
              modeBuffer += lexeme;
            } else {
              if (newMode.excludeBegin) {
                modeBuffer += lexeme;
              }
              processBuffer();
              if (!newMode.returnBegin && !newMode.excludeBegin) {
                modeBuffer = lexeme;
              }
            }
            startNewMode(newMode, match);
            return newMode.returnBegin ? 0 : lexeme.length;
          }
          function doEndMatch(match) {
            const lexeme = match[0];
            const matchPlusRemainder = codeToHighlight.substring(match.index);
            const endMode = endOfMode(top, match, matchPlusRemainder);
            if (!endMode) {
              return NO_MATCH;
            }
            const origin = top;
            if (top.endScope && top.endScope._wrap) {
              processBuffer();
              emitKeyword(lexeme, top.endScope._wrap);
            } else if (top.endScope && top.endScope._multi) {
              processBuffer();
              emitMultiClass(top.endScope, match);
            } else if (origin.skip) {
              modeBuffer += lexeme;
            } else {
              if (!(origin.returnEnd || origin.excludeEnd)) {
                modeBuffer += lexeme;
              }
              processBuffer();
              if (origin.excludeEnd) {
                modeBuffer = lexeme;
              }
            }
            do {
              if (top.scope) {
                emitter.closeNode();
              }
              if (!top.skip && !top.subLanguage) {
                relevance += top.relevance;
              }
              top = top.parent;
            } while (top !== endMode.parent);
            if (endMode.starts) {
              startNewMode(endMode.starts, match);
            }
            return origin.returnEnd ? 0 : lexeme.length;
          }
          function processContinuations() {
            const list = [];
            for (let current = top; current !== language; current = current.parent) {
              if (current.scope) {
                list.unshift(current.scope);
              }
            }
            list.forEach((item) => emitter.openNode(item));
          }
          let lastMatch = {};
          function processLexeme(textBeforeMatch, match) {
            const lexeme = match && match[0];
            modeBuffer += textBeforeMatch;
            if (lexeme == null) {
              processBuffer();
              return 0;
            }
            if (lastMatch.type === "begin" && match.type === "end" && lastMatch.index === match.index && lexeme === "") {
              modeBuffer += codeToHighlight.slice(match.index, match.index + 1);
              if (!SAFE_MODE) {
                const err = new Error(`0 width match regex (${languageName})`);
                err.languageName = languageName;
                err.badRule = lastMatch.rule;
                throw err;
              }
              return 1;
            }
            lastMatch = match;
            if (match.type === "begin") {
              return doBeginMatch(match);
            } else if (match.type === "illegal" && !ignoreIllegals) {
              const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.scope || "<unnamed>") + '"');
              err.mode = top;
              throw err;
            } else if (match.type === "end") {
              const processed = doEndMatch(match);
              if (processed !== NO_MATCH) {
                return processed;
              }
            }
            if (match.type === "illegal" && lexeme === "") {
              modeBuffer += "\n";
              return 1;
            }
            if (iterations > 1e5 && iterations > match.index * 3) {
              const err = new Error("potential infinite loop, way more iterations than matches");
              throw err;
            }
            modeBuffer += lexeme;
            return lexeme.length;
          }
          const language = getLanguage(languageName);
          if (!language) {
            error(LANGUAGE_NOT_FOUND.replace("{}", languageName));
            throw new Error('Unknown language: "' + languageName + '"');
          }
          const md = compileLanguage(language);
          let result = "";
          let top = continuation || md;
          const continuations = {};
          const emitter = new options.__emitter(options);
          processContinuations();
          let modeBuffer = "";
          let relevance = 0;
          let index = 0;
          let iterations = 0;
          let resumeScanAtSamePosition = false;
          try {
            if (!language.__emitTokens) {
              top.matcher.considerAll();
              for (; ; ) {
                iterations++;
                if (resumeScanAtSamePosition) {
                  resumeScanAtSamePosition = false;
                } else {
                  top.matcher.considerAll();
                }
                top.matcher.lastIndex = index;
                const match = top.matcher.exec(codeToHighlight);
                if (!match) break;
                const beforeMatch = codeToHighlight.substring(index, match.index);
                const processedCount = processLexeme(beforeMatch, match);
                index = match.index + processedCount;
              }
              processLexeme(codeToHighlight.substring(index));
            } else {
              language.__emitTokens(codeToHighlight, emitter);
            }
            emitter.finalize();
            result = emitter.toHTML();
            return {
              language: languageName,
              value: result,
              relevance,
              illegal: false,
              _emitter: emitter,
              _top: top
            };
          } catch (err) {
            if (err.message && err.message.includes("Illegal")) {
              return {
                language: languageName,
                value: escape(codeToHighlight),
                illegal: true,
                relevance: 0,
                _illegalBy: {
                  message: err.message,
                  index,
                  context: codeToHighlight.slice(index - 100, index + 100),
                  mode: err.mode,
                  resultSoFar: result
                },
                _emitter: emitter
              };
            } else if (SAFE_MODE) {
              return {
                language: languageName,
                value: escape(codeToHighlight),
                illegal: false,
                relevance: 0,
                errorRaised: err,
                _emitter: emitter,
                _top: top
              };
            } else {
              throw err;
            }
          }
        }
        function justTextHighlightResult(code) {
          const result = {
            value: escape(code),
            illegal: false,
            relevance: 0,
            _top: PLAINTEXT_LANGUAGE,
            _emitter: new options.__emitter(options)
          };
          result._emitter.addText(code);
          return result;
        }
        function highlightAuto(code, languageSubset) {
          languageSubset = languageSubset || options.languages || Object.keys(languages);
          const plaintext = justTextHighlightResult(code);
          const results = languageSubset.filter(getLanguage).filter(autoDetection).map(
            (name) => _highlight(name, code, false)
          );
          results.unshift(plaintext);
          const sorted = results.sort((a, b) => {
            if (a.relevance !== b.relevance) return b.relevance - a.relevance;
            if (a.language && b.language) {
              if (getLanguage(a.language).supersetOf === b.language) {
                return 1;
              } else if (getLanguage(b.language).supersetOf === a.language) {
                return -1;
              }
            }
            return 0;
          });
          const [best, secondBest] = sorted;
          const result = best;
          result.secondBest = secondBest;
          return result;
        }
        function updateClassName(element, currentLang, resultLang) {
          const language = currentLang && aliases[currentLang] || resultLang;
          element.classList.add("hljs");
          element.classList.add(`language-${language}`);
        }
        function highlightElement(element) {
          let node = null;
          const language = blockLanguage(element);
          if (shouldNotHighlight(language)) return;
          fire(
            "before:highlightElement",
            { el: element, language }
          );
          if (element.dataset.highlighted) {
            console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.", element);
            return;
          }
          if (element.children.length > 0) {
            if (!options.ignoreUnescapedHTML) {
              console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk.");
              console.warn("https://github.com/highlightjs/highlight.js/wiki/security");
              console.warn("The element with unescaped HTML:");
              console.warn(element);
            }
            if (options.throwUnescapedHTML) {
              const err = new HTMLInjectionError(
                "One of your code blocks includes unescaped HTML.",
                element.innerHTML
              );
              throw err;
            }
          }
          node = element;
          const text = node.textContent;
          const result = language ? highlight2(text, { language, ignoreIllegals: true }) : highlightAuto(text);
          element.innerHTML = result.value;
          element.dataset.highlighted = "yes";
          updateClassName(element, language, result.language);
          element.result = {
            language: result.language,
            // TODO: remove with version 11.0
            re: result.relevance,
            relevance: result.relevance
          };
          if (result.secondBest) {
            element.secondBest = {
              language: result.secondBest.language,
              relevance: result.secondBest.relevance
            };
          }
          fire("after:highlightElement", { el: element, result, text });
        }
        function configure(userOptions) {
          options = inherit(options, userOptions);
        }
        const initHighlighting = () => {
          highlightAll();
          deprecated("10.6.0", "initHighlighting() deprecated.  Use highlightAll() now.");
        };
        function initHighlightingOnLoad() {
          highlightAll();
          deprecated("10.6.0", "initHighlightingOnLoad() deprecated.  Use highlightAll() now.");
        }
        let wantsHighlight = false;
        function highlightAll() {
          function boot() {
            highlightAll();
          }
          if (document.readyState === "loading") {
            if (!wantsHighlight) {
              window.addEventListener("DOMContentLoaded", boot, false);
            }
            wantsHighlight = true;
            return;
          }
          const blocks = document.querySelectorAll(options.cssSelector);
          blocks.forEach(highlightElement);
        }
        function registerLanguage(languageName, languageDefinition) {
          let lang = null;
          try {
            lang = languageDefinition(hljs);
          } catch (error$1) {
            error("Language definition for '{}' could not be registered.".replace("{}", languageName));
            if (!SAFE_MODE) {
              throw error$1;
            } else {
              error(error$1);
            }
            lang = PLAINTEXT_LANGUAGE;
          }
          if (!lang.name) lang.name = languageName;
          languages[languageName] = lang;
          lang.rawDefinition = languageDefinition.bind(null, hljs);
          if (lang.aliases) {
            registerAliases(lang.aliases, { languageName });
          }
        }
        function unregisterLanguage(languageName) {
          delete languages[languageName];
          for (const alias of Object.keys(aliases)) {
            if (aliases[alias] === languageName) {
              delete aliases[alias];
            }
          }
        }
        function listLanguages() {
          return Object.keys(languages);
        }
        function getLanguage(name) {
          name = (name || "").toLowerCase();
          return languages[name] || languages[aliases[name]];
        }
        function registerAliases(aliasList, { languageName }) {
          if (typeof aliasList === "string") {
            aliasList = [aliasList];
          }
          aliasList.forEach((alias) => {
            aliases[alias.toLowerCase()] = languageName;
          });
        }
        function autoDetection(name) {
          const lang = getLanguage(name);
          return lang && !lang.disableAutodetect;
        }
        function upgradePluginAPI(plugin) {
          if (plugin["before:highlightBlock"] && !plugin["before:highlightElement"]) {
            plugin["before:highlightElement"] = (data) => {
              plugin["before:highlightBlock"](
                Object.assign({ block: data.el }, data)
              );
            };
          }
          if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) {
            plugin["after:highlightElement"] = (data) => {
              plugin["after:highlightBlock"](
                Object.assign({ block: data.el }, data)
              );
            };
          }
        }
        function addPlugin(plugin) {
          upgradePluginAPI(plugin);
          plugins.push(plugin);
        }
        function removePlugin(plugin) {
          const index = plugins.indexOf(plugin);
          if (index !== -1) {
            plugins.splice(index, 1);
          }
        }
        function fire(event, args) {
          const cb = event;
          plugins.forEach(function(plugin) {
            if (plugin[cb]) {
              plugin[cb](args);
            }
          });
        }
        function deprecateHighlightBlock(el) {
          deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0");
          deprecated("10.7.0", "Please use highlightElement now.");
          return highlightElement(el);
        }
        Object.assign(hljs, {
          highlight: highlight2,
          highlightAuto,
          highlightAll,
          highlightElement,
          // TODO: Remove with v12 API
          highlightBlock: deprecateHighlightBlock,
          configure,
          initHighlighting,
          initHighlightingOnLoad,
          registerLanguage,
          unregisterLanguage,
          listLanguages,
          getLanguage,
          registerAliases,
          autoDetection,
          inherit,
          addPlugin,
          removePlugin
        });
        hljs.debugMode = function() {
          SAFE_MODE = false;
        };
        hljs.safeMode = function() {
          SAFE_MODE = true;
        };
        hljs.versionString = version;
        hljs.regex = {
          concat,
          lookahead,
          either,
          optional,
          anyNumberOfTimes
        };
        for (const key in MODES) {
          if (typeof MODES[key] === "object") {
            deepFreeze(MODES[key]);
          }
        }
        Object.assign(hljs, MODES);
        return hljs;
      };
      var highlight = HLJS({});
      highlight.newInstance = () => HLJS({});
      module.exports = highlight;
      highlight.HighlightJS = highlight;
      highlight.default = highlight;
    }
  });

  // node_modules/highlight.js/lib/languages/xml.js
  var require_xml = __commonJS({
    "node_modules/highlight.js/lib/languages/xml.js"(exports, module) {
      function xml(hljs) {
        const regex = hljs.regex;
        const TAG_NAME_RE = regex.concat(/[\p{L}_]/u, regex.optional(/[\p{L}0-9_.-]*:/u), /[\p{L}0-9_.-]*/u);
        const XML_IDENT_RE = /[\p{L}0-9._:-]+/u;
        const XML_ENTITIES = {
          className: "symbol",
          begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/
        };
        const XML_META_KEYWORDS = {
          begin: /\s/,
          contains: [
            {
              className: "keyword",
              begin: /#?[a-z_][a-z1-9_-]+/,
              illegal: /\n/
            }
          ]
        };
        const XML_META_PAR_KEYWORDS = hljs.inherit(XML_META_KEYWORDS, {
          begin: /\(/,
          end: /\)/
        });
        const APOS_META_STRING_MODE = hljs.inherit(hljs.APOS_STRING_MODE, { className: "string" });
        const QUOTE_META_STRING_MODE = hljs.inherit(hljs.QUOTE_STRING_MODE, { className: "string" });
        const TAG_INTERNALS = {
          endsWithParent: true,
          illegal: /</,
          relevance: 0,
          contains: [
            {
              className: "attr",
              begin: XML_IDENT_RE,
              relevance: 0
            },
            {
              begin: /=\s*/,
              relevance: 0,
              contains: [
                {
                  className: "string",
                  endsParent: true,
                  variants: [
                    {
                      begin: /"/,
                      end: /"/,
                      contains: [XML_ENTITIES]
                    },
                    {
                      begin: /'/,
                      end: /'/,
                      contains: [XML_ENTITIES]
                    },
                    { begin: /[^\s"'=<>`]+/ }
                  ]
                }
              ]
            }
          ]
        };
        return {
          name: "HTML, XML",
          aliases: [
            "html",
            "xhtml",
            "rss",
            "atom",
            "xjb",
            "xsd",
            "xsl",
            "plist",
            "wsf",
            "svg"
          ],
          case_insensitive: true,
          unicodeRegex: true,
          contains: [
            {
              className: "meta",
              begin: /<![a-z]/,
              end: />/,
              relevance: 10,
              contains: [
                XML_META_KEYWORDS,
                QUOTE_META_STRING_MODE,
                APOS_META_STRING_MODE,
                XML_META_PAR_KEYWORDS,
                {
                  begin: /\[/,
                  end: /\]/,
                  contains: [
                    {
                      className: "meta",
                      begin: /<![a-z]/,
                      end: />/,
                      contains: [
                        XML_META_KEYWORDS,
                        XML_META_PAR_KEYWORDS,
                        QUOTE_META_STRING_MODE,
                        APOS_META_STRING_MODE
                      ]
                    }
                  ]
                }
              ]
            },
            hljs.COMMENT(
              /<!--/,
              /-->/,
              { relevance: 10 }
            ),
            {
              begin: /<!\[CDATA\[/,
              end: /\]\]>/,
              relevance: 10
            },
            XML_ENTITIES,
            // xml processing instructions
            {
              className: "meta",
              end: /\?>/,
              variants: [
                {
                  begin: /<\?xml/,
                  relevance: 10,
                  contains: [
                    QUOTE_META_STRING_MODE
                  ]
                },
                {
                  begin: /<\?[a-z][a-z0-9]+/
                }
              ]
            },
            {
              className: "tag",
              /*
              The lookahead pattern (?=...) ensures that 'begin' only matches
              '<style' as a single word, followed by a whitespace or an
              ending bracket.
              */
              begin: /<style(?=\s|>)/,
              end: />/,
              keywords: { name: "style" },
              contains: [TAG_INTERNALS],
              starts: {
                end: /<\/style>/,
                returnEnd: true,
                subLanguage: [
                  "css",
                  "xml"
                ]
              }
            },
            {
              className: "tag",
              // See the comment in the <style tag about the lookahead pattern
              begin: /<script(?=\s|>)/,
              end: />/,
              keywords: { name: "script" },
              contains: [TAG_INTERNALS],
              starts: {
                end: /<\/script>/,
                returnEnd: true,
                subLanguage: [
                  "javascript",
                  "handlebars",
                  "xml"
                ]
              }
            },
            // we need this for now for jSX
            {
              className: "tag",
              begin: /<>|<\/>/
            },
            // open tag
            {
              className: "tag",
              begin: regex.concat(
                /</,
                regex.lookahead(regex.concat(
                  TAG_NAME_RE,
                  // <tag/>
                  // <tag>
                  // <tag ...
                  regex.either(/\/>/, />/, /\s/)
                ))
              ),
              end: /\/?>/,
              contains: [
                {
                  className: "name",
                  begin: TAG_NAME_RE,
                  relevance: 0,
                  starts: TAG_INTERNALS
                }
              ]
            },
            // close tag
            {
              className: "tag",
              begin: regex.concat(
                /<\//,
                regex.lookahead(regex.concat(
                  TAG_NAME_RE,
                  />/
                ))
              ),
              contains: [
                {
                  className: "name",
                  begin: TAG_NAME_RE,
                  relevance: 0
                },
                {
                  begin: />/,
                  relevance: 0,
                  endsParent: true
                }
              ]
            }
          ]
        };
      }
      module.exports = xml;
    }
  });

  // node_modules/highlight.js/lib/languages/bash.js
  var require_bash = __commonJS({
    "node_modules/highlight.js/lib/languages/bash.js"(exports, module) {
      function bash(hljs) {
        const regex = hljs.regex;
        const VAR = {};
        const BRACED_VAR = {
          begin: /\$\{/,
          end: /\}/,
          contains: [
            "self",
            {
              begin: /:-/,
              contains: [VAR]
            }
            // default values
          ]
        };
        Object.assign(VAR, {
          className: "variable",
          variants: [
            { begin: regex.concat(
              /\$[\w\d#@][\w\d_]*/,
              // negative look-ahead tries to avoid matching patterns that are not
              // Perl at all like $ident$, @ident@, etc.
              `(?![\\w\\d])(?![$])`
            ) },
            BRACED_VAR
          ]
        });
        const SUBST = {
          className: "subst",
          begin: /\$\(/,
          end: /\)/,
          contains: [hljs.BACKSLASH_ESCAPE]
        };
        const COMMENT = hljs.inherit(
          hljs.COMMENT(),
          {
            match: [
              /(^|\s)/,
              /#.*$/
            ],
            scope: {
              2: "comment"
            }
          }
        );
        const HERE_DOC = {
          begin: /<<-?\s*(?=\w+)/,
          starts: { contains: [
            hljs.END_SAME_AS_BEGIN({
              begin: /(\w+)/,
              end: /(\w+)/,
              className: "string"
            })
          ] }
        };
        const QUOTE_STRING = {
          className: "string",
          begin: /"/,
          end: /"/,
          contains: [
            hljs.BACKSLASH_ESCAPE,
            VAR,
            SUBST
          ]
        };
        SUBST.contains.push(QUOTE_STRING);
        const ESCAPED_QUOTE = {
          match: /\\"/
        };
        const APOS_STRING = {
          className: "string",
          begin: /'/,
          end: /'/
        };
        const ESCAPED_APOS = {
          match: /\\'/
        };
        const ARITHMETIC = {
          begin: /\$?\(\(/,
          end: /\)\)/,
          contains: [
            {
              begin: /\d+#[0-9a-f]+/,
              className: "number"
            },
            hljs.NUMBER_MODE,
            VAR
          ]
        };
        const SH_LIKE_SHELLS = [
          "fish",
          "bash",
          "zsh",
          "sh",
          "csh",
          "ksh",
          "tcsh",
          "dash",
          "scsh"
        ];
        const KNOWN_SHEBANG = hljs.SHEBANG({
          binary: `(${SH_LIKE_SHELLS.join("|")})`,
          relevance: 10
        });
        const FUNCTION = {
          className: "function",
          begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/,
          returnBegin: true,
          contains: [hljs.inherit(hljs.TITLE_MODE, { begin: /\w[\w\d_]*/ })],
          relevance: 0
        };
        const KEYWORDS = [
          "if",
          "then",
          "else",
          "elif",
          "fi",
          "time",
          "for",
          "while",
          "until",
          "in",
          "do",
          "done",
          "case",
          "esac",
          "coproc",
          "function",
          "select"
        ];
        const LITERALS = [
          "true",
          "false"
        ];
        const PATH_MODE = { match: /(\/[a-z._-]+)+/ };
        const SHELL_BUILT_INS = [
          "break",
          "cd",
          "continue",
          "eval",
          "exec",
          "exit",
          "export",
          "getopts",
          "hash",
          "pwd",
          "readonly",
          "return",
          "shift",
          "test",
          "times",
          "trap",
          "umask",
          "unset"
        ];
        const BASH_BUILT_INS = [
          "alias",
          "bind",
          "builtin",
          "caller",
          "command",
          "declare",
          "echo",
          "enable",
          "help",
          "let",
          "local",
          "logout",
          "mapfile",
          "printf",
          "read",
          "readarray",
          "source",
          "sudo",
          "type",
          "typeset",
          "ulimit",
          "unalias"
        ];
        const ZSH_BUILT_INS = [
          "autoload",
          "bg",
          "bindkey",
          "bye",
          "cap",
          "chdir",
          "clone",
          "comparguments",
          "compcall",
          "compctl",
          "compdescribe",
          "compfiles",
          "compgroups",
          "compquote",
          "comptags",
          "comptry",
          "compvalues",
          "dirs",
          "disable",
          "disown",
          "echotc",
          "echoti",
          "emulate",
          "fc",
          "fg",
          "float",
          "functions",
          "getcap",
          "getln",
          "history",
          "integer",
          "jobs",
          "kill",
          "limit",
          "log",
          "noglob",
          "popd",
          "print",
          "pushd",
          "pushln",
          "rehash",
          "sched",
          "setcap",
          "setopt",
          "stat",
          "suspend",
          "ttyctl",
          "unfunction",
          "unhash",
          "unlimit",
          "unsetopt",
          "vared",
          "wait",
          "whence",
          "where",
          "which",
          "zcompile",
          "zformat",
          "zftp",
          "zle",
          "zmodload",
          "zparseopts",
          "zprof",
          "zpty",
          "zregexparse",
          "zsocket",
          "zstyle",
          "ztcp"
        ];
        const GNU_CORE_UTILS = [
          "chcon",
          "chgrp",
          "chown",
          "chmod",
          "cp",
          "dd",
          "df",
          "dir",
          "dircolors",
          "ln",
          "ls",
          "mkdir",
          "mkfifo",
          "mknod",
          "mktemp",
          "mv",
          "realpath",
          "rm",
          "rmdir",
          "shred",
          "sync",
          "touch",
          "truncate",
          "vdir",
          "b2sum",
          "base32",
          "base64",
          "cat",
          "cksum",
          "comm",
          "csplit",
          "cut",
          "expand",
          "fmt",
          "fold",
          "head",
          "join",
          "md5sum",
          "nl",
          "numfmt",
          "od",
          "paste",
          "ptx",
          "pr",
          "sha1sum",
          "sha224sum",
          "sha256sum",
          "sha384sum",
          "sha512sum",
          "shuf",
          "sort",
          "split",
          "sum",
          "tac",
          "tail",
          "tr",
          "tsort",
          "unexpand",
          "uniq",
          "wc",
          "arch",
          "basename",
          "chroot",
          "date",
          "dirname",
          "du",
          "echo",
          "env",
          "expr",
          "factor",
          // "false", // keyword literal already
          "groups",
          "hostid",
          "id",
          "link",
          "logname",
          "nice",
          "nohup",
          "nproc",
          "pathchk",
          "pinky",
          "printenv",
          "printf",
          "pwd",
          "readlink",
          "runcon",
          "seq",
          "sleep",
          "stat",
          "stdbuf",
          "stty",
          "tee",
          "test",
          "timeout",
          // "true", // keyword literal already
          "tty",
          "uname",
          "unlink",
          "uptime",
          "users",
          "who",
          "whoami",
          "yes"
        ];
        return {
          name: "Bash",
          aliases: [
            "sh",
            "zsh"
          ],
          keywords: {
            $pattern: /\b[a-z][a-z0-9._-]+\b/,
            keyword: KEYWORDS,
            literal: LITERALS,
            built_in: [
              ...SHELL_BUILT_INS,
              ...BASH_BUILT_INS,
              // Shell modifiers
              "set",
              "shopt",
              ...ZSH_BUILT_INS,
              ...GNU_CORE_UTILS
            ]
          },
          contains: [
            KNOWN_SHEBANG,
            // to catch known shells and boost relevancy
            hljs.SHEBANG(),
            // to catch unknown shells but still highlight the shebang
            FUNCTION,
            ARITHMETIC,
            COMMENT,
            HERE_DOC,
            PATH_MODE,
            QUOTE_STRING,
            ESCAPED_QUOTE,
            APOS_STRING,
            ESCAPED_APOS,
            VAR
          ]
        };
      }
      module.exports = bash;
    }
  });

  // node_modules/highlight.js/lib/languages/c.js
  var require_c = __commonJS({
    "node_modules/highlight.js/lib/languages/c.js"(exports, module) {
      function c(hljs) {
        const regex = hljs.regex;
        const C_LINE_COMMENT_MODE = hljs.COMMENT("//", "$", { contains: [{ begin: /\\\n/ }] });
        const DECLTYPE_AUTO_RE = "decltype\\(auto\\)";
        const NAMESPACE_RE = "[a-zA-Z_]\\w*::";
        const TEMPLATE_ARGUMENT_RE = "<[^<>]+>";
        const FUNCTION_TYPE_RE = "(" + DECLTYPE_AUTO_RE + "|" + regex.optional(NAMESPACE_RE) + "[a-zA-Z_]\\w*" + regex.optional(TEMPLATE_ARGUMENT_RE) + ")";
        const TYPES = {
          className: "type",
          variants: [
            { begin: "\\b[a-z\\d_]*_t\\b" },
            { match: /\batomic_[a-z]{3,6}\b/ }
          ]
        };
        const CHARACTER_ESCAPES = "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)";
        const STRINGS = {
          className: "string",
          variants: [
            {
              begin: '(u8?|U|L)?"',
              end: '"',
              illegal: "\\n",
              contains: [hljs.BACKSLASH_ESCAPE]
            },
            {
              begin: "(u8?|U|L)?'(" + CHARACTER_ESCAPES + "|.)",
              end: "'",
              illegal: "."
            },
            hljs.END_SAME_AS_BEGIN({
              begin: /(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,
              end: /\)([^()\\ ]{0,16})"/
            })
          ]
        };
        const NUMBERS = {
          className: "number",
          variants: [
            { match: /\b(0b[01']+)/ },
            { match: /(-?)\b([\d']+(\.[\d']*)?|\.[\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)/ },
            { match: /(-?)\b(0[xX][a-fA-F0-9]+(?:'[a-fA-F0-9]+)*(?:\.[a-fA-F0-9]*(?:'[a-fA-F0-9]*)*)?(?:[pP][-+]?[0-9]+)?(l|L)?(u|U)?)/ },
            { match: /(-?)\b\d+(?:'\d+)*(?:\.\d*(?:'\d*)*)?(?:[eE][-+]?\d+)?/ }
          ],
          relevance: 0
        };
        const PREPROCESSOR = {
          className: "meta",
          begin: /#\s*[a-z]+\b/,
          end: /$/,
          keywords: { keyword: "if else elif endif define undef warning error line pragma _Pragma ifdef ifndef elifdef elifndef include" },
          contains: [
            {
              begin: /\\\n/,
              relevance: 0
            },
            hljs.inherit(STRINGS, { className: "string" }),
            {
              className: "string",
              begin: /<.*?>/
            },
            C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE
          ]
        };
        const TITLE_MODE = {
          className: "title",
          begin: regex.optional(NAMESPACE_RE) + hljs.IDENT_RE,
          relevance: 0
        };
        const FUNCTION_TITLE = regex.optional(NAMESPACE_RE) + hljs.IDENT_RE + "\\s*\\(";
        const C_KEYWORDS = [
          "asm",
          "auto",
          "break",
          "case",
          "continue",
          "default",
          "do",
          "else",
          "enum",
          "extern",
          "for",
          "fortran",
          "goto",
          "if",
          "inline",
          "register",
          "restrict",
          "return",
          "sizeof",
          "typeof",
          "typeof_unqual",
          "struct",
          "switch",
          "typedef",
          "union",
          "volatile",
          "while",
          "_Alignas",
          "_Alignof",
          "_Atomic",
          "_Generic",
          "_Noreturn",
          "_Static_assert",
          "_Thread_local",
          // aliases
          "alignas",
          "alignof",
          "noreturn",
          "static_assert",
          "thread_local",
          // not a C keyword but is, for all intents and purposes, treated exactly like one.
          "_Pragma"
        ];
        const C_TYPES = [
          "float",
          "double",
          "signed",
          "unsigned",
          "int",
          "short",
          "long",
          "char",
          "void",
          "_Bool",
          "_BitInt",
          "_Complex",
          "_Imaginary",
          "_Decimal32",
          "_Decimal64",
          "_Decimal96",
          "_Decimal128",
          "_Decimal64x",
          "_Decimal128x",
          "_Float16",
          "_Float32",
          "_Float64",
          "_Float128",
          "_Float32x",
          "_Float64x",
          "_Float128x",
          // modifiers
          "const",
          "static",
          "constexpr",
          // aliases
          "complex",
          "bool",
          "imaginary"
        ];
        const KEYWORDS = {
          keyword: C_KEYWORDS,
          type: C_TYPES,
          literal: "true false NULL",
          // TODO: apply hinting work similar to what was done in cpp.js
          built_in: "std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr"
        };
        const EXPRESSION_CONTAINS = [
          PREPROCESSOR,
          TYPES,
          C_LINE_COMMENT_MODE,
          hljs.C_BLOCK_COMMENT_MODE,
          NUMBERS,
          STRINGS
        ];
        const EXPRESSION_CONTEXT = {
          // This mode covers expression context where we can't expect a function
          // definition and shouldn't highlight anything that looks like one:
          // `return some()`, `else if()`, `(x*sum(1, 2))`
          variants: [
            {
              begin: /=/,
              end: /;/
            },
            {
              begin: /\(/,
              end: /\)/
            },
            {
              beginKeywords: "new throw return else",
              end: /;/
            }
          ],
          keywords: KEYWORDS,
          contains: EXPRESSION_CONTAINS.concat([
            {
              begin: /\(/,
              end: /\)/,
              keywords: KEYWORDS,
              contains: EXPRESSION_CONTAINS.concat(["self"]),
              relevance: 0
            }
          ]),
          relevance: 0
        };
        const FUNCTION_DECLARATION = {
          begin: "(" + FUNCTION_TYPE_RE + "[\\*&\\s]+)+" + FUNCTION_TITLE,
          returnBegin: true,
          end: /[{;=]/,
          excludeEnd: true,
          keywords: KEYWORDS,
          illegal: /[^\w\s\*&:<>.]/,
          contains: [
            {
              // to prevent it from being confused as the function title
              begin: DECLTYPE_AUTO_RE,
              keywords: KEYWORDS,
              relevance: 0
            },
            {
              begin: FUNCTION_TITLE,
              returnBegin: true,
              contains: [hljs.inherit(TITLE_MODE, { className: "title.function" })],
              relevance: 0
            },
            // allow for multiple declarations, e.g.:
            // extern void f(int), g(char);
            {
              relevance: 0,
              match: /,/
            },
            {
              className: "params",
              begin: /\(/,
              end: /\)/,
              keywords: KEYWORDS,
              relevance: 0,
              contains: [
                C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE,
                STRINGS,
                NUMBERS,
                TYPES,
                // Count matching parentheses.
                {
                  begin: /\(/,
                  end: /\)/,
                  keywords: KEYWORDS,
                  relevance: 0,
                  contains: [
                    "self",
                    C_LINE_COMMENT_MODE,
                    hljs.C_BLOCK_COMMENT_MODE,
                    STRINGS,
                    NUMBERS,
                    TYPES
                  ]
                }
              ]
            },
            TYPES,
            C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            PREPROCESSOR
          ]
        };
        return {
          name: "C",
          aliases: ["h"],
          keywords: KEYWORDS,
          // Until differentiations are added between `c` and `cpp`, `c` will
          // not be auto-detected to avoid auto-detect conflicts between C and C++
          disableAutodetect: true,
          illegal: "</",
          contains: [].concat(
            EXPRESSION_CONTEXT,
            FUNCTION_DECLARATION,
            EXPRESSION_CONTAINS,
            [
              PREPROCESSOR,
              {
                begin: hljs.IDENT_RE + "::",
                keywords: KEYWORDS
              },
              {
                className: "class",
                beginKeywords: "enum class struct union",
                end: /[{;:<>=]/,
                contains: [
                  { beginKeywords: "final class struct" },
                  hljs.TITLE_MODE
                ]
              }
            ]
          ),
          exports: {
            preprocessor: PREPROCESSOR,
            strings: STRINGS,
            keywords: KEYWORDS
          }
        };
      }
      module.exports = c;
    }
  });

  // node_modules/highlight.js/lib/languages/cpp.js
  var require_cpp = __commonJS({
    "node_modules/highlight.js/lib/languages/cpp.js"(exports, module) {
      function cpp(hljs) {
        const regex = hljs.regex;
        const C_LINE_COMMENT_MODE = hljs.COMMENT("//", "$", { contains: [{ begin: /\\\n/ }] });
        const DECLTYPE_AUTO_RE = "decltype\\(auto\\)";
        const NAMESPACE_RE = "[a-zA-Z_]\\w*::";
        const TEMPLATE_ARGUMENT_RE = "<[^<>]+>";
        const FUNCTION_TYPE_RE = "(?!struct)(" + DECLTYPE_AUTO_RE + "|" + regex.optional(NAMESPACE_RE) + "[a-zA-Z_]\\w*" + regex.optional(TEMPLATE_ARGUMENT_RE) + ")";
        const CPP_PRIMITIVE_TYPES = {
          className: "type",
          begin: "\\b[a-z\\d_]*_t\\b"
        };
        const CHARACTER_ESCAPES = "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)";
        const STRINGS = {
          className: "string",
          variants: [
            {
              begin: '(u8?|U|L)?"',
              end: '"',
              illegal: "\\n",
              contains: [hljs.BACKSLASH_ESCAPE]
            },
            {
              begin: "(u8?|U|L)?'(" + CHARACTER_ESCAPES + "|.)",
              end: "'",
              illegal: "."
            },
            hljs.END_SAME_AS_BEGIN({
              begin: /(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,
              end: /\)([^()\\ ]{0,16})"/
            })
          ]
        };
        const NUMBERS = {
          className: "number",
          variants: [
            // Floating-point literal.
            {
              begin: "[+-]?(?:(?:[0-9](?:'?[0-9])*\\.(?:[0-9](?:'?[0-9])*)?|\\.[0-9](?:'?[0-9])*)(?:[Ee][+-]?[0-9](?:'?[0-9])*)?|[0-9](?:'?[0-9])*[Ee][+-]?[0-9](?:'?[0-9])*|0[Xx](?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*(?:\\.(?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)?)?|\\.[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)[Pp][+-]?[0-9](?:'?[0-9])*)(?:[Ff](?:16|32|64|128)?|(BF|bf)16|[Ll]|)"
            },
            // Integer literal.
            {
              begin: "[+-]?\\b(?:0[Bb][01](?:'?[01])*|0[Xx][0-9A-Fa-f](?:'?[0-9A-Fa-f])*|0(?:'?[0-7])*|[1-9](?:'?[0-9])*)(?:[Uu](?:LL?|ll?)|[Uu][Zz]?|(?:LL?|ll?)[Uu]?|[Zz][Uu]|)"
              // Note: there are user-defined literal suffixes too, but perhaps having the custom suffix not part of the
              // literal highlight actually makes it stand out more.
            }
          ],
          relevance: 0
        };
        const PREPROCESSOR = {
          className: "meta",
          begin: /#\s*[a-z]+\b/,
          end: /$/,
          keywords: { keyword: "if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" },
          contains: [
            {
              begin: /\\\n/,
              relevance: 0
            },
            hljs.inherit(STRINGS, { className: "string" }),
            {
              className: "string",
              begin: /<.*?>/
            },
            C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE
          ]
        };
        const TITLE_MODE = {
          className: "title",
          begin: regex.optional(NAMESPACE_RE) + hljs.IDENT_RE,
          relevance: 0
        };
        const FUNCTION_TITLE = regex.optional(NAMESPACE_RE) + hljs.IDENT_RE + "\\s*\\(";
        const RESERVED_KEYWORDS = [
          "alignas",
          "alignof",
          "and",
          "and_eq",
          "asm",
          "atomic_cancel",
          "atomic_commit",
          "atomic_noexcept",
          "auto",
          "bitand",
          "bitor",
          "break",
          "case",
          "catch",
          "class",
          "co_await",
          "co_return",
          "co_yield",
          "compl",
          "concept",
          "const_cast|10",
          "consteval",
          "constexpr",
          "constinit",
          "continue",
          "decltype",
          "default",
          "delete",
          "do",
          "dynamic_cast|10",
          "else",
          "enum",
          "explicit",
          "export",
          "extern",
          "false",
          "final",
          "for",
          "friend",
          "goto",
          "if",
          "import",
          "inline",
          "module",
          "mutable",
          "namespace",
          "new",
          "noexcept",
          "not",
          "not_eq",
          "nullptr",
          "operator",
          "or",
          "or_eq",
          "override",
          "private",
          "protected",
          "public",
          "reflexpr",
          "register",
          "reinterpret_cast|10",
          "requires",
          "return",
          "sizeof",
          "static_assert",
          "static_cast|10",
          "struct",
          "switch",
          "synchronized",
          "template",
          "this",
          "thread_local",
          "throw",
          "transaction_safe",
          "transaction_safe_dynamic",
          "true",
          "try",
          "typedef",
          "typeid",
          "typename",
          "union",
          "using",
          "virtual",
          "volatile",
          "while",
          "xor",
          "xor_eq"
        ];
        const RESERVED_TYPES = [
          "bool",
          "char",
          "char16_t",
          "char32_t",
          "char8_t",
          "double",
          "float",
          "int",
          "long",
          "short",
          "void",
          "wchar_t",
          "unsigned",
          "signed",
          "const",
          "static"
        ];
        const TYPE_HINTS = [
          "any",
          "auto_ptr",
          "barrier",
          "binary_semaphore",
          "bitset",
          "complex",
          "condition_variable",
          "condition_variable_any",
          "counting_semaphore",
          "deque",
          "false_type",
          "flat_map",
          "flat_set",
          "future",
          "imaginary",
          "initializer_list",
          "istringstream",
          "jthread",
          "latch",
          "lock_guard",
          "multimap",
          "multiset",
          "mutex",
          "optional",
          "ostringstream",
          "packaged_task",
          "pair",
          "promise",
          "priority_queue",
          "queue",
          "recursive_mutex",
          "recursive_timed_mutex",
          "scoped_lock",
          "set",
          "shared_future",
          "shared_lock",
          "shared_mutex",
          "shared_timed_mutex",
          "shared_ptr",
          "stack",
          "string_view",
          "stringstream",
          "timed_mutex",
          "thread",
          "true_type",
          "tuple",
          "unique_lock",
          "unique_ptr",
          "unordered_map",
          "unordered_multimap",
          "unordered_multiset",
          "unordered_set",
          "variant",
          "vector",
          "weak_ptr",
          "wstring",
          "wstring_view"
        ];
        const FUNCTION_HINTS = [
          "abort",
          "abs",
          "acos",
          "apply",
          "as_const",
          "asin",
          "atan",
          "atan2",
          "calloc",
          "ceil",
          "cerr",
          "cin",
          "clog",
          "cos",
          "cosh",
          "cout",
          "declval",
          "endl",
          "exchange",
          "exit",
          "exp",
          "fabs",
          "floor",
          "fmod",
          "forward",
          "fprintf",
          "fputs",
          "free",
          "frexp",
          "fscanf",
          "future",
          "invoke",
          "isalnum",
          "isalpha",
          "iscntrl",
          "isdigit",
          "isgraph",
          "islower",
          "isprint",
          "ispunct",
          "isspace",
          "isupper",
          "isxdigit",
          "labs",
          "launder",
          "ldexp",
          "log",
          "log10",
          "make_pair",
          "make_shared",
          "make_shared_for_overwrite",
          "make_tuple",
          "make_unique",
          "malloc",
          "memchr",
          "memcmp",
          "memcpy",
          "memset",
          "modf",
          "move",
          "pow",
          "printf",
          "putchar",
          "puts",
          "realloc",
          "scanf",
          "sin",
          "sinh",
          "snprintf",
          "sprintf",
          "sqrt",
          "sscanf",
          "std",
          "stderr",
          "stdin",
          "stdout",
          "strcat",
          "strchr",
          "strcmp",
          "strcpy",
          "strcspn",
          "strlen",
          "strncat",
          "strncmp",
          "strncpy",
          "strpbrk",
          "strrchr",
          "strspn",
          "strstr",
          "swap",
          "tan",
          "tanh",
          "terminate",
          "to_underlying",
          "tolower",
          "toupper",
          "vfprintf",
          "visit",
          "vprintf",
          "vsprintf"
        ];
        const LITERALS = [
          "NULL",
          "false",
          "nullopt",
          "nullptr",
          "true"
        ];
        const BUILT_IN = ["_Pragma"];
        const CPP_KEYWORDS = {
          type: RESERVED_TYPES,
          keyword: RESERVED_KEYWORDS,
          literal: LITERALS,
          built_in: BUILT_IN,
          _type_hints: TYPE_HINTS
        };
        const FUNCTION_DISPATCH = {
          className: "function.dispatch",
          relevance: 0,
          keywords: {
            // Only for relevance, not highlighting.
            _hint: FUNCTION_HINTS
          },
          begin: regex.concat(
            /\b/,
            /(?!decltype)/,
            /(?!if)/,
            /(?!for)/,
            /(?!switch)/,
            /(?!while)/,
            hljs.IDENT_RE,
            regex.lookahead(/(<[^<>]+>|)\s*\(/)
          )
        };
        const EXPRESSION_CONTAINS = [
          FUNCTION_DISPATCH,
          PREPROCESSOR,
          CPP_PRIMITIVE_TYPES,
          C_LINE_COMMENT_MODE,
          hljs.C_BLOCK_COMMENT_MODE,
          NUMBERS,
          STRINGS
        ];
        const EXPRESSION_CONTEXT = {
          // This mode covers expression context where we can't expect a function
          // definition and shouldn't highlight anything that looks like one:
          // `return some()`, `else if()`, `(x*sum(1, 2))`
          variants: [
            {
              begin: /=/,
              end: /;/
            },
            {
              begin: /\(/,
              end: /\)/
            },
            {
              beginKeywords: "new throw return else",
              end: /;/
            }
          ],
          keywords: CPP_KEYWORDS,
          contains: EXPRESSION_CONTAINS.concat([
            {
              begin: /\(/,
              end: /\)/,
              keywords: CPP_KEYWORDS,
              contains: EXPRESSION_CONTAINS.concat(["self"]),
              relevance: 0
            }
          ]),
          relevance: 0
        };
        const FUNCTION_DECLARATION = {
          className: "function",
          begin: "(" + FUNCTION_TYPE_RE + "[\\*&\\s]+)+" + FUNCTION_TITLE,
          returnBegin: true,
          end: /[{;=]/,
          excludeEnd: true,
          keywords: CPP_KEYWORDS,
          illegal: /[^\w\s\*&:<>.]/,
          contains: [
            {
              // to prevent it from being confused as the function title
              begin: DECLTYPE_AUTO_RE,
              keywords: CPP_KEYWORDS,
              relevance: 0
            },
            {
              begin: FUNCTION_TITLE,
              returnBegin: true,
              contains: [TITLE_MODE],
              relevance: 0
            },
            // needed because we do not have look-behind on the below rule
            // to prevent it from grabbing the final : in a :: pair
            {
              begin: /::/,
              relevance: 0
            },
            // initializers
            {
              begin: /:/,
              endsWithParent: true,
              contains: [
                STRINGS,
                NUMBERS
              ]
            },
            // allow for multiple declarations, e.g.:
            // extern void f(int), g(char);
            {
              relevance: 0,
              match: /,/
            },
            {
              className: "params",
              begin: /\(/,
              end: /\)/,
              keywords: CPP_KEYWORDS,
              relevance: 0,
              contains: [
                C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE,
                STRINGS,
                NUMBERS,
                CPP_PRIMITIVE_TYPES,
                // Count matching parentheses.
                {
                  begin: /\(/,
                  end: /\)/,
                  keywords: CPP_KEYWORDS,
                  relevance: 0,
                  contains: [
                    "self",
                    C_LINE_COMMENT_MODE,
                    hljs.C_BLOCK_COMMENT_MODE,
                    STRINGS,
                    NUMBERS,
                    CPP_PRIMITIVE_TYPES
                  ]
                }
              ]
            },
            CPP_PRIMITIVE_TYPES,
            C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            PREPROCESSOR
          ]
        };
        return {
          name: "C++",
          aliases: [
            "cc",
            "c++",
            "h++",
            "hpp",
            "hh",
            "hxx",
            "cxx"
          ],
          keywords: CPP_KEYWORDS,
          illegal: "</",
          classNameAliases: { "function.dispatch": "built_in" },
          contains: [].concat(
            EXPRESSION_CONTEXT,
            FUNCTION_DECLARATION,
            FUNCTION_DISPATCH,
            EXPRESSION_CONTAINS,
            [
              PREPROCESSOR,
              {
                // containers: ie, `vector <int> rooms (9);`
                begin: "\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array|tuple|optional|variant|function|flat_map|flat_set)\\s*<(?!<)",
                end: ">",
                keywords: CPP_KEYWORDS,
                contains: [
                  "self",
                  CPP_PRIMITIVE_TYPES
                ]
              },
              {
                begin: hljs.IDENT_RE + "::",
                keywords: CPP_KEYWORDS
              },
              {
                match: [
                  // extra complexity to deal with `enum class` and `enum struct`
                  /\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,
                  /\s+/,
                  /\w+/
                ],
                className: {
                  1: "keyword",
                  3: "title.class"
                }
              }
            ]
          )
        };
      }
      module.exports = cpp;
    }
  });

  // node_modules/highlight.js/lib/languages/csharp.js
  var require_csharp = __commonJS({
    "node_modules/highlight.js/lib/languages/csharp.js"(exports, module) {
      function csharp(hljs) {
        const BUILT_IN_KEYWORDS = [
          "bool",
          "byte",
          "char",
          "decimal",
          "delegate",
          "double",
          "dynamic",
          "enum",
          "float",
          "int",
          "long",
          "nint",
          "nuint",
          "object",
          "sbyte",
          "short",
          "string",
          "ulong",
          "uint",
          "ushort"
        ];
        const FUNCTION_MODIFIERS = [
          "public",
          "private",
          "protected",
          "static",
          "internal",
          "protected",
          "abstract",
          "async",
          "extern",
          "override",
          "unsafe",
          "virtual",
          "new",
          "sealed",
          "partial"
        ];
        const LITERAL_KEYWORDS = [
          "default",
          "false",
          "null",
          "true"
        ];
        const NORMAL_KEYWORDS = [
          "abstract",
          "as",
          "base",
          "break",
          "case",
          "catch",
          "class",
          "const",
          "continue",
          "do",
          "else",
          "event",
          "explicit",
          "extern",
          "finally",
          "fixed",
          "for",
          "foreach",
          "goto",
          "if",
          "implicit",
          "in",
          "interface",
          "internal",
          "is",
          "lock",
          "namespace",
          "new",
          "operator",
          "out",
          "override",
          "params",
          "private",
          "protected",
          "public",
          "readonly",
          "record",
          "ref",
          "return",
          "scoped",
          "sealed",
          "sizeof",
          "stackalloc",
          "static",
          "struct",
          "switch",
          "this",
          "throw",
          "try",
          "typeof",
          "unchecked",
          "unsafe",
          "using",
          "virtual",
          "void",
          "volatile",
          "while"
        ];
        const CONTEXTUAL_KEYWORDS = [
          "add",
          "alias",
          "and",
          "ascending",
          "args",
          "async",
          "await",
          "by",
          "descending",
          "dynamic",
          "equals",
          "file",
          "from",
          "get",
          "global",
          "group",
          "init",
          "into",
          "join",
          "let",
          "nameof",
          "not",
          "notnull",
          "on",
          "or",
          "orderby",
          "partial",
          "record",
          "remove",
          "required",
          "scoped",
          "select",
          "set",
          "unmanaged",
          "value|0",
          "var",
          "when",
          "where",
          "with",
          "yield"
        ];
        const KEYWORDS = {
          keyword: NORMAL_KEYWORDS.concat(CONTEXTUAL_KEYWORDS),
          built_in: BUILT_IN_KEYWORDS,
          literal: LITERAL_KEYWORDS
        };
        const TITLE_MODE = hljs.inherit(hljs.TITLE_MODE, { begin: "[a-zA-Z](\\.?\\w)*" });
        const NUMBERS = {
          className: "number",
          variants: [
            { begin: "\\b(0b[01']+)" },
            { begin: "(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)" },
            { begin: "(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" }
          ],
          relevance: 0
        };
        const RAW_STRING = {
          className: "string",
          begin: /"""("*)(?!")(.|\n)*?"""\1/,
          relevance: 1
        };
        const VERBATIM_STRING = {
          className: "string",
          begin: '@"',
          end: '"',
          contains: [{ begin: '""' }]
        };
        const VERBATIM_STRING_NO_LF = hljs.inherit(VERBATIM_STRING, { illegal: /\n/ });
        const SUBST = {
          className: "subst",
          begin: /\{/,
          end: /\}/,
          keywords: KEYWORDS
        };
        const SUBST_NO_LF = hljs.inherit(SUBST, { illegal: /\n/ });
        const INTERPOLATED_STRING = {
          className: "string",
          begin: /\$"/,
          end: '"',
          illegal: /\n/,
          contains: [
            { begin: /\{\{/ },
            { begin: /\}\}/ },
            hljs.BACKSLASH_ESCAPE,
            SUBST_NO_LF
          ]
        };
        const INTERPOLATED_VERBATIM_STRING = {
          className: "string",
          begin: /\$@"/,
          end: '"',
          contains: [
            { begin: /\{\{/ },
            { begin: /\}\}/ },
            { begin: '""' },
            SUBST
          ]
        };
        const INTERPOLATED_VERBATIM_STRING_NO_LF = hljs.inherit(INTERPOLATED_VERBATIM_STRING, {
          illegal: /\n/,
          contains: [
            { begin: /\{\{/ },
            { begin: /\}\}/ },
            { begin: '""' },
            SUBST_NO_LF
          ]
        });
        SUBST.contains = [
          INTERPOLATED_VERBATIM_STRING,
          INTERPOLATED_STRING,
          VERBATIM_STRING,
          hljs.APOS_STRING_MODE,
          hljs.QUOTE_STRING_MODE,
          NUMBERS,
          hljs.C_BLOCK_COMMENT_MODE
        ];
        SUBST_NO_LF.contains = [
          INTERPOLATED_VERBATIM_STRING_NO_LF,
          INTERPOLATED_STRING,
          VERBATIM_STRING_NO_LF,
          hljs.APOS_STRING_MODE,
          hljs.QUOTE_STRING_MODE,
          NUMBERS,
          hljs.inherit(hljs.C_BLOCK_COMMENT_MODE, { illegal: /\n/ })
        ];
        const STRING = { variants: [
          RAW_STRING,
          INTERPOLATED_VERBATIM_STRING,
          INTERPOLATED_STRING,
          VERBATIM_STRING,
          hljs.APOS_STRING_MODE,
          hljs.QUOTE_STRING_MODE
        ] };
        const GENERIC_MODIFIER = {
          begin: "<",
          end: ">",
          contains: [
            { beginKeywords: "in out" },
            TITLE_MODE
          ]
        };
        const TYPE_IDENT_RE = hljs.IDENT_RE + "(<" + hljs.IDENT_RE + "(\\s*,\\s*" + hljs.IDENT_RE + ")*>)?(\\[\\])?";
        const AT_IDENTIFIER = {
          // prevents expressions like `@class` from incorrect flagging
          // `class` as a keyword
          begin: "@" + hljs.IDENT_RE,
          relevance: 0
        };
        return {
          name: "C#",
          aliases: [
            "cs",
            "c#"
          ],
          keywords: KEYWORDS,
          illegal: /::/,
          contains: [
            hljs.COMMENT(
              "///",
              "$",
              {
                returnBegin: true,
                contains: [
                  {
                    className: "doctag",
                    variants: [
                      {
                        begin: "///",
                        relevance: 0
                      },
                      { begin: "<!--|-->" },
                      {
                        begin: "</?",
                        end: ">"
                      }
                    ]
                  }
                ]
              }
            ),
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            {
              className: "meta",
              begin: "#",
              end: "$",
              keywords: { keyword: "if else elif endif define undef warning error line region endregion pragma checksum" }
            },
            STRING,
            NUMBERS,
            {
              beginKeywords: "class interface",
              relevance: 0,
              end: /[{;=]/,
              illegal: /[^\s:,]/,
              contains: [
                { beginKeywords: "where class" },
                TITLE_MODE,
                GENERIC_MODIFIER,
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            {
              beginKeywords: "namespace",
              relevance: 0,
              end: /[{;=]/,
              illegal: /[^\s:]/,
              contains: [
                TITLE_MODE,
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            {
              beginKeywords: "record",
              relevance: 0,
              end: /[{;=]/,
              illegal: /[^\s:]/,
              contains: [
                TITLE_MODE,
                GENERIC_MODIFIER,
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            {
              // [Attributes("")]
              className: "meta",
              begin: "^\\s*\\[(?=[\\w])",
              excludeBegin: true,
              end: "\\]",
              excludeEnd: true,
              contains: [
                {
                  className: "string",
                  begin: /"/,
                  end: /"/
                }
              ]
            },
            {
              // Expression keywords prevent 'keyword Name(...)' from being
              // recognized as a function definition
              beginKeywords: "new return throw await else",
              relevance: 0
            },
            {
              className: "function",
              begin: "(" + TYPE_IDENT_RE + "\\s+)+" + hljs.IDENT_RE + "\\s*(<[^=]+>\\s*)?\\(",
              returnBegin: true,
              end: /\s*[{;=]/,
              excludeEnd: true,
              keywords: KEYWORDS,
              contains: [
                // prevents these from being highlighted `title`
                {
                  beginKeywords: FUNCTION_MODIFIERS.join(" "),
                  relevance: 0
                },
                {
                  begin: hljs.IDENT_RE + "\\s*(<[^=]+>\\s*)?\\(",
                  returnBegin: true,
                  contains: [
                    hljs.TITLE_MODE,
                    GENERIC_MODIFIER
                  ],
                  relevance: 0
                },
                { match: /\(\)/ },
                {
                  className: "params",
                  begin: /\(/,
                  end: /\)/,
                  excludeBegin: true,
                  excludeEnd: true,
                  keywords: KEYWORDS,
                  relevance: 0,
                  contains: [
                    STRING,
                    NUMBERS,
                    hljs.C_BLOCK_COMMENT_MODE
                  ]
                },
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            AT_IDENTIFIER
          ]
        };
      }
      module.exports = csharp;
    }
  });

  // node_modules/highlight.js/lib/languages/css.js
  var require_css = __commonJS({
    "node_modules/highlight.js/lib/languages/css.js"(exports, module) {
      var MODES = (hljs) => {
        return {
          IMPORTANT: {
            scope: "meta",
            begin: "!important"
          },
          BLOCK_COMMENT: hljs.C_BLOCK_COMMENT_MODE,
          HEXCOLOR: {
            scope: "number",
            begin: /#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/
          },
          FUNCTION_DISPATCH: {
            className: "built_in",
            begin: /[\w-]+(?=\()/
          },
          ATTRIBUTE_SELECTOR_MODE: {
            scope: "selector-attr",
            begin: /\[/,
            end: /\]/,
            illegal: "$",
            contains: [
              hljs.APOS_STRING_MODE,
              hljs.QUOTE_STRING_MODE
            ]
          },
          CSS_NUMBER_MODE: {
            scope: "number",
            begin: hljs.NUMBER_RE + "(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
            relevance: 0
          },
          CSS_VARIABLE: {
            className: "attr",
            begin: /--[A-Za-z_][A-Za-z0-9_-]*/
          }
        };
      };
      var HTML_TAGS = [
        "a",
        "abbr",
        "address",
        "article",
        "aside",
        "audio",
        "b",
        "blockquote",
        "body",
        "button",
        "canvas",
        "caption",
        "cite",
        "code",
        "dd",
        "del",
        "details",
        "dfn",
        "div",
        "dl",
        "dt",
        "em",
        "fieldset",
        "figcaption",
        "figure",
        "footer",
        "form",
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "h6",
        "header",
        "hgroup",
        "html",
        "i",
        "iframe",
        "img",
        "input",
        "ins",
        "kbd",
        "label",
        "legend",
        "li",
        "main",
        "mark",
        "menu",
        "nav",
        "object",
        "ol",
        "optgroup",
        "option",
        "p",
        "picture",
        "q",
        "quote",
        "samp",
        "section",
        "select",
        "source",
        "span",
        "strong",
        "summary",
        "sup",
        "table",
        "tbody",
        "td",
        "textarea",
        "tfoot",
        "th",
        "thead",
        "time",
        "tr",
        "ul",
        "var",
        "video"
      ];
      var SVG_TAGS = [
        "defs",
        "g",
        "marker",
        "mask",
        "pattern",
        "svg",
        "switch",
        "symbol",
        "feBlend",
        "feColorMatrix",
        "feComponentTransfer",
        "feComposite",
        "feConvolveMatrix",
        "feDiffuseLighting",
        "feDisplacementMap",
        "feFlood",
        "feGaussianBlur",
        "feImage",
        "feMerge",
        "feMorphology",
        "feOffset",
        "feSpecularLighting",
        "feTile",
        "feTurbulence",
        "linearGradient",
        "radialGradient",
        "stop",
        "circle",
        "ellipse",
        "image",
        "line",
        "path",
        "polygon",
        "polyline",
        "rect",
        "text",
        "use",
        "textPath",
        "tspan",
        "foreignObject",
        "clipPath"
      ];
      var TAGS = [
        ...HTML_TAGS,
        ...SVG_TAGS
      ];
      var MEDIA_FEATURES = [
        "any-hover",
        "any-pointer",
        "aspect-ratio",
        "color",
        "color-gamut",
        "color-index",
        "device-aspect-ratio",
        "device-height",
        "device-width",
        "display-mode",
        "forced-colors",
        "grid",
        "height",
        "hover",
        "inverted-colors",
        "monochrome",
        "orientation",
        "overflow-block",
        "overflow-inline",
        "pointer",
        "prefers-color-scheme",
        "prefers-contrast",
        "prefers-reduced-motion",
        "prefers-reduced-transparency",
        "resolution",
        "scan",
        "scripting",
        "update",
        "width",
        // TODO: find a better solution?
        "min-width",
        "max-width",
        "min-height",
        "max-height"
      ].sort().reverse();
      var PSEUDO_CLASSES = [
        "active",
        "any-link",
        "blank",
        "checked",
        "current",
        "default",
        "defined",
        "dir",
        // dir()
        "disabled",
        "drop",
        "empty",
        "enabled",
        "first",
        "first-child",
        "first-of-type",
        "fullscreen",
        "future",
        "focus",
        "focus-visible",
        "focus-within",
        "has",
        // has()
        "host",
        // host or host()
        "host-context",
        // host-context()
        "hover",
        "indeterminate",
        "in-range",
        "invalid",
        "is",
        // is()
        "lang",
        // lang()
        "last-child",
        "last-of-type",
        "left",
        "link",
        "local-link",
        "not",
        // not()
        "nth-child",
        // nth-child()
        "nth-col",
        // nth-col()
        "nth-last-child",
        // nth-last-child()
        "nth-last-col",
        // nth-last-col()
        "nth-last-of-type",
        //nth-last-of-type()
        "nth-of-type",
        //nth-of-type()
        "only-child",
        "only-of-type",
        "optional",
        "out-of-range",
        "past",
        "placeholder-shown",
        "read-only",
        "read-write",
        "required",
        "right",
        "root",
        "scope",
        "target",
        "target-within",
        "user-invalid",
        "valid",
        "visited",
        "where"
        // where()
      ].sort().reverse();
      var PSEUDO_ELEMENTS = [
        "after",
        "backdrop",
        "before",
        "cue",
        "cue-region",
        "first-letter",
        "first-line",
        "grammar-error",
        "marker",
        "part",
        "placeholder",
        "selection",
        "slotted",
        "spelling-error"
      ].sort().reverse();
      var ATTRIBUTES = [
        "accent-color",
        "align-content",
        "align-items",
        "align-self",
        "alignment-baseline",
        "all",
        "anchor-name",
        "animation",
        "animation-composition",
        "animation-delay",
        "animation-direction",
        "animation-duration",
        "animation-fill-mode",
        "animation-iteration-count",
        "animation-name",
        "animation-play-state",
        "animation-range",
        "animation-range-end",
        "animation-range-start",
        "animation-timeline",
        "animation-timing-function",
        "appearance",
        "aspect-ratio",
        "backdrop-filter",
        "backface-visibility",
        "background",
        "background-attachment",
        "background-blend-mode",
        "background-clip",
        "background-color",
        "background-image",
        "background-origin",
        "background-position",
        "background-position-x",
        "background-position-y",
        "background-repeat",
        "background-size",
        "baseline-shift",
        "block-size",
        "border",
        "border-block",
        "border-block-color",
        "border-block-end",
        "border-block-end-color",
        "border-block-end-style",
        "border-block-end-width",
        "border-block-start",
        "border-block-start-color",
        "border-block-start-style",
        "border-block-start-width",
        "border-block-style",
        "border-block-width",
        "border-bottom",
        "border-bottom-color",
        "border-bottom-left-radius",
        "border-bottom-right-radius",
        "border-bottom-style",
        "border-bottom-width",
        "border-collapse",
        "border-color",
        "border-end-end-radius",
        "border-end-start-radius",
        "border-image",
        "border-image-outset",
        "border-image-repeat",
        "border-image-slice",
        "border-image-source",
        "border-image-width",
        "border-inline",
        "border-inline-color",
        "border-inline-end",
        "border-inline-end-color",
        "border-inline-end-style",
        "border-inline-end-width",
        "border-inline-start",
        "border-inline-start-color",
        "border-inline-start-style",
        "border-inline-start-width",
        "border-inline-style",
        "border-inline-width",
        "border-left",
        "border-left-color",
        "border-left-style",
        "border-left-width",
        "border-radius",
        "border-right",
        "border-right-color",
        "border-right-style",
        "border-right-width",
        "border-spacing",
        "border-start-end-radius",
        "border-start-start-radius",
        "border-style",
        "border-top",
        "border-top-color",
        "border-top-left-radius",
        "border-top-right-radius",
        "border-top-style",
        "border-top-width",
        "border-width",
        "bottom",
        "box-align",
        "box-decoration-break",
        "box-direction",
        "box-flex",
        "box-flex-group",
        "box-lines",
        "box-ordinal-group",
        "box-orient",
        "box-pack",
        "box-shadow",
        "box-sizing",
        "break-after",
        "break-before",
        "break-inside",
        "caption-side",
        "caret-color",
        "clear",
        "clip",
        "clip-path",
        "clip-rule",
        "color",
        "color-interpolation",
        "color-interpolation-filters",
        "color-profile",
        "color-rendering",
        "color-scheme",
        "column-count",
        "column-fill",
        "column-gap",
        "column-rule",
        "column-rule-color",
        "column-rule-style",
        "column-rule-width",
        "column-span",
        "column-width",
        "columns",
        "contain",
        "contain-intrinsic-block-size",
        "contain-intrinsic-height",
        "contain-intrinsic-inline-size",
        "contain-intrinsic-size",
        "contain-intrinsic-width",
        "container",
        "container-name",
        "container-type",
        "content",
        "content-visibility",
        "counter-increment",
        "counter-reset",
        "counter-set",
        "cue",
        "cue-after",
        "cue-before",
        "cursor",
        "cx",
        "cy",
        "direction",
        "display",
        "dominant-baseline",
        "empty-cells",
        "enable-background",
        "field-sizing",
        "fill",
        "fill-opacity",
        "fill-rule",
        "filter",
        "flex",
        "flex-basis",
        "flex-direction",
        "flex-flow",
        "flex-grow",
        "flex-shrink",
        "flex-wrap",
        "float",
        "flood-color",
        "flood-opacity",
        "flow",
        "font",
        "font-display",
        "font-family",
        "font-feature-settings",
        "font-kerning",
        "font-language-override",
        "font-optical-sizing",
        "font-palette",
        "font-size",
        "font-size-adjust",
        "font-smooth",
        "font-smoothing",
        "font-stretch",
        "font-style",
        "font-synthesis",
        "font-synthesis-position",
        "font-synthesis-small-caps",
        "font-synthesis-style",
        "font-synthesis-weight",
        "font-variant",
        "font-variant-alternates",
        "font-variant-caps",
        "font-variant-east-asian",
        "font-variant-emoji",
        "font-variant-ligatures",
        "font-variant-numeric",
        "font-variant-position",
        "font-variation-settings",
        "font-weight",
        "forced-color-adjust",
        "gap",
        "glyph-orientation-horizontal",
        "glyph-orientation-vertical",
        "grid",
        "grid-area",
        "grid-auto-columns",
        "grid-auto-flow",
        "grid-auto-rows",
        "grid-column",
        "grid-column-end",
        "grid-column-start",
        "grid-gap",
        "grid-row",
        "grid-row-end",
        "grid-row-start",
        "grid-template",
        "grid-template-areas",
        "grid-template-columns",
        "grid-template-rows",
        "hanging-punctuation",
        "height",
        "hyphenate-character",
        "hyphenate-limit-chars",
        "hyphens",
        "icon",
        "image-orientation",
        "image-rendering",
        "image-resolution",
        "ime-mode",
        "initial-letter",
        "initial-letter-align",
        "inline-size",
        "inset",
        "inset-area",
        "inset-block",
        "inset-block-end",
        "inset-block-start",
        "inset-inline",
        "inset-inline-end",
        "inset-inline-start",
        "isolation",
        "justify-content",
        "justify-items",
        "justify-self",
        "kerning",
        "left",
        "letter-spacing",
        "lighting-color",
        "line-break",
        "line-height",
        "line-height-step",
        "list-style",
        "list-style-image",
        "list-style-position",
        "list-style-type",
        "margin",
        "margin-block",
        "margin-block-end",
        "margin-block-start",
        "margin-bottom",
        "margin-inline",
        "margin-inline-end",
        "margin-inline-start",
        "margin-left",
        "margin-right",
        "margin-top",
        "margin-trim",
        "marker",
        "marker-end",
        "marker-mid",
        "marker-start",
        "marks",
        "mask",
        "mask-border",
        "mask-border-mode",
        "mask-border-outset",
        "mask-border-repeat",
        "mask-border-slice",
        "mask-border-source",
        "mask-border-width",
        "mask-clip",
        "mask-composite",
        "mask-image",
        "mask-mode",
        "mask-origin",
        "mask-position",
        "mask-repeat",
        "mask-size",
        "mask-type",
        "masonry-auto-flow",
        "math-depth",
        "math-shift",
        "math-style",
        "max-block-size",
        "max-height",
        "max-inline-size",
        "max-width",
        "min-block-size",
        "min-height",
        "min-inline-size",
        "min-width",
        "mix-blend-mode",
        "nav-down",
        "nav-index",
        "nav-left",
        "nav-right",
        "nav-up",
        "none",
        "normal",
        "object-fit",
        "object-position",
        "offset",
        "offset-anchor",
        "offset-distance",
        "offset-path",
        "offset-position",
        "offset-rotate",
        "opacity",
        "order",
        "orphans",
        "outline",
        "outline-color",
        "outline-offset",
        "outline-style",
        "outline-width",
        "overflow",
        "overflow-anchor",
        "overflow-block",
        "overflow-clip-margin",
        "overflow-inline",
        "overflow-wrap",
        "overflow-x",
        "overflow-y",
        "overlay",
        "overscroll-behavior",
        "overscroll-behavior-block",
        "overscroll-behavior-inline",
        "overscroll-behavior-x",
        "overscroll-behavior-y",
        "padding",
        "padding-block",
        "padding-block-end",
        "padding-block-start",
        "padding-bottom",
        "padding-inline",
        "padding-inline-end",
        "padding-inline-start",
        "padding-left",
        "padding-right",
        "padding-top",
        "page",
        "page-break-after",
        "page-break-before",
        "page-break-inside",
        "paint-order",
        "pause",
        "pause-after",
        "pause-before",
        "perspective",
        "perspective-origin",
        "place-content",
        "place-items",
        "place-self",
        "pointer-events",
        "position",
        "position-anchor",
        "position-visibility",
        "print-color-adjust",
        "quotes",
        "r",
        "resize",
        "rest",
        "rest-after",
        "rest-before",
        "right",
        "rotate",
        "row-gap",
        "ruby-align",
        "ruby-position",
        "scale",
        "scroll-behavior",
        "scroll-margin",
        "scroll-margin-block",
        "scroll-margin-block-end",
        "scroll-margin-block-start",
        "scroll-margin-bottom",
        "scroll-margin-inline",
        "scroll-margin-inline-end",
        "scroll-margin-inline-start",
        "scroll-margin-left",
        "scroll-margin-right",
        "scroll-margin-top",
        "scroll-padding",
        "scroll-padding-block",
        "scroll-padding-block-end",
        "scroll-padding-block-start",
        "scroll-padding-bottom",
        "scroll-padding-inline",
        "scroll-padding-inline-end",
        "scroll-padding-inline-start",
        "scroll-padding-left",
        "scroll-padding-right",
        "scroll-padding-top",
        "scroll-snap-align",
        "scroll-snap-stop",
        "scroll-snap-type",
        "scroll-timeline",
        "scroll-timeline-axis",
        "scroll-timeline-name",
        "scrollbar-color",
        "scrollbar-gutter",
        "scrollbar-width",
        "shape-image-threshold",
        "shape-margin",
        "shape-outside",
        "shape-rendering",
        "speak",
        "speak-as",
        "src",
        // @font-face
        "stop-color",
        "stop-opacity",
        "stroke",
        "stroke-dasharray",
        "stroke-dashoffset",
        "stroke-linecap",
        "stroke-linejoin",
        "stroke-miterlimit",
        "stroke-opacity",
        "stroke-width",
        "tab-size",
        "table-layout",
        "text-align",
        "text-align-all",
        "text-align-last",
        "text-anchor",
        "text-combine-upright",
        "text-decoration",
        "text-decoration-color",
        "text-decoration-line",
        "text-decoration-skip",
        "text-decoration-skip-ink",
        "text-decoration-style",
        "text-decoration-thickness",
        "text-emphasis",
        "text-emphasis-color",
        "text-emphasis-position",
        "text-emphasis-style",
        "text-indent",
        "text-justify",
        "text-orientation",
        "text-overflow",
        "text-rendering",
        "text-shadow",
        "text-size-adjust",
        "text-transform",
        "text-underline-offset",
        "text-underline-position",
        "text-wrap",
        "text-wrap-mode",
        "text-wrap-style",
        "timeline-scope",
        "top",
        "touch-action",
        "transform",
        "transform-box",
        "transform-origin",
        "transform-style",
        "transition",
        "transition-behavior",
        "transition-delay",
        "transition-duration",
        "transition-property",
        "transition-timing-function",
        "translate",
        "unicode-bidi",
        "user-modify",
        "user-select",
        "vector-effect",
        "vertical-align",
        "view-timeline",
        "view-timeline-axis",
        "view-timeline-inset",
        "view-timeline-name",
        "view-transition-name",
        "visibility",
        "voice-balance",
        "voice-duration",
        "voice-family",
        "voice-pitch",
        "voice-range",
        "voice-rate",
        "voice-stress",
        "voice-volume",
        "white-space",
        "white-space-collapse",
        "widows",
        "width",
        "will-change",
        "word-break",
        "word-spacing",
        "word-wrap",
        "writing-mode",
        "x",
        "y",
        "z-index",
        "zoom"
      ].sort().reverse();
      function css(hljs) {
        const regex = hljs.regex;
        const modes = MODES(hljs);
        const VENDOR_PREFIX = { begin: /-(webkit|moz|ms|o)-(?=[a-z])/ };
        const AT_MODIFIERS = "and or not only";
        const AT_PROPERTY_RE = /@-?\w[\w]*(-\w+)*/;
        const IDENT_RE = "[a-zA-Z-][a-zA-Z0-9_-]*";
        const STRINGS = [
          hljs.APOS_STRING_MODE,
          hljs.QUOTE_STRING_MODE
        ];
        return {
          name: "CSS",
          case_insensitive: true,
          illegal: /[=|'\$]/,
          keywords: { keyframePosition: "from to" },
          classNameAliases: {
            // for visual continuity with `tag {}` and because we
            // don't have a great class for this?
            keyframePosition: "selector-tag"
          },
          contains: [
            modes.BLOCK_COMMENT,
            VENDOR_PREFIX,
            // to recognize keyframe 40% etc which are outside the scope of our
            // attribute value mode
            modes.CSS_NUMBER_MODE,
            {
              className: "selector-id",
              begin: /#[A-Za-z0-9_-]+/,
              relevance: 0
            },
            {
              className: "selector-class",
              begin: "\\." + IDENT_RE,
              relevance: 0
            },
            modes.ATTRIBUTE_SELECTOR_MODE,
            {
              className: "selector-pseudo",
              variants: [
                { begin: ":(" + PSEUDO_CLASSES.join("|") + ")" },
                { begin: ":(:)?(" + PSEUDO_ELEMENTS.join("|") + ")" }
              ]
            },
            // we may actually need this (12/2020)
            // { // pseudo-selector params
            //   begin: /\(/,
            //   end: /\)/,
            //   contains: [ hljs.CSS_NUMBER_MODE ]
            // },
            modes.CSS_VARIABLE,
            {
              className: "attribute",
              begin: "\\b(" + ATTRIBUTES.join("|") + ")\\b"
            },
            // attribute values
            {
              begin: /:/,
              end: /[;}{]/,
              contains: [
                modes.BLOCK_COMMENT,
                modes.HEXCOLOR,
                modes.IMPORTANT,
                modes.CSS_NUMBER_MODE,
                ...STRINGS,
                // needed to highlight these as strings and to avoid issues with
                // illegal characters that might be inside urls that would tigger the
                // languages illegal stack
                {
                  begin: /(url|data-uri)\(/,
                  end: /\)/,
                  relevance: 0,
                  // from keywords
                  keywords: { built_in: "url data-uri" },
                  contains: [
                    ...STRINGS,
                    {
                      className: "string",
                      // any character other than `)` as in `url()` will be the start
                      // of a string, which ends with `)` (from the parent mode)
                      begin: /[^)]/,
                      endsWithParent: true,
                      excludeEnd: true
                    }
                  ]
                },
                modes.FUNCTION_DISPATCH
              ]
            },
            {
              begin: regex.lookahead(/@/),
              end: "[{;]",
              relevance: 0,
              illegal: /:/,
              // break on Less variables @var: ...
              contains: [
                {
                  className: "keyword",
                  begin: AT_PROPERTY_RE
                },
                {
                  begin: /\s/,
                  endsWithParent: true,
                  excludeEnd: true,
                  relevance: 0,
                  keywords: {
                    $pattern: /[a-z-]+/,
                    keyword: AT_MODIFIERS,
                    attribute: MEDIA_FEATURES.join(" ")
                  },
                  contains: [
                    {
                      begin: /[a-z-]+(?=:)/,
                      className: "attribute"
                    },
                    ...STRINGS,
                    modes.CSS_NUMBER_MODE
                  ]
                }
              ]
            },
            {
              className: "selector-tag",
              begin: "\\b(" + TAGS.join("|") + ")\\b"
            }
          ]
        };
      }
      module.exports = css;
    }
  });

  // node_modules/highlight.js/lib/languages/markdown.js
  var require_markdown = __commonJS({
    "node_modules/highlight.js/lib/languages/markdown.js"(exports, module) {
      function markdown(hljs) {
        const regex = hljs.regex;
        const INLINE_HTML = {
          begin: /<\/?[A-Za-z_]/,
          end: ">",
          subLanguage: "xml",
          relevance: 0
        };
        const HORIZONTAL_RULE = {
          begin: "^[-\\*]{3,}",
          end: "$"
        };
        const CODE = {
          className: "code",
          variants: [
            // TODO: fix to allow these to work with sublanguage also
            { begin: "(`{3,})[^`](.|\\n)*?\\1`*[ ]*" },
            { begin: "(~{3,})[^~](.|\\n)*?\\1~*[ ]*" },
            // needed to allow markdown as a sublanguage to work
            {
              begin: "```",
              end: "```+[ ]*$"
            },
            {
              begin: "~~~",
              end: "~~~+[ ]*$"
            },
            { begin: "`.+?`" },
            {
              begin: "(?=^( {4}|\\t))",
              // use contains to gobble up multiple lines to allow the block to be whatever size
              // but only have a single open/close tag vs one per line
              contains: [
                {
                  begin: "^( {4}|\\t)",
                  end: "(\\n)$"
                }
              ],
              relevance: 0
            }
          ]
        };
        const LIST = {
          className: "bullet",
          begin: "^[ 	]*([*+-]|(\\d+\\.))(?=\\s+)",
          end: "\\s+",
          excludeEnd: true
        };
        const LINK_REFERENCE = {
          begin: /^\[[^\n]+\]:/,
          returnBegin: true,
          contains: [
            {
              className: "symbol",
              begin: /\[/,
              end: /\]/,
              excludeBegin: true,
              excludeEnd: true
            },
            {
              className: "link",
              begin: /:\s*/,
              end: /$/,
              excludeBegin: true
            }
          ]
        };
        const URL_SCHEME = /[A-Za-z][A-Za-z0-9+.-]*/;
        const LINK = {
          variants: [
            // too much like nested array access in so many languages
            // to have any real relevance
            {
              begin: /\[.+?\]\[.*?\]/,
              relevance: 0
            },
            // popular internet URLs
            {
              begin: /\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/,
              relevance: 2
            },
            {
              begin: regex.concat(/\[.+?\]\(/, URL_SCHEME, /:\/\/.*?\)/),
              relevance: 2
            },
            // relative urls
            {
              begin: /\[.+?\]\([./?&#].*?\)/,
              relevance: 1
            },
            // whatever else, lower relevance (might not be a link at all)
            {
              begin: /\[.*?\]\(.*?\)/,
              relevance: 0
            }
          ],
          returnBegin: true,
          contains: [
            {
              // empty strings for alt or link text
              match: /\[(?=\])/
            },
            {
              className: "string",
              relevance: 0,
              begin: "\\[",
              end: "\\]",
              excludeBegin: true,
              returnEnd: true
            },
            {
              className: "link",
              relevance: 0,
              begin: "\\]\\(",
              end: "\\)",
              excludeBegin: true,
              excludeEnd: true
            },
            {
              className: "symbol",
              relevance: 0,
              begin: "\\]\\[",
              end: "\\]",
              excludeBegin: true,
              excludeEnd: true
            }
          ]
        };
        const BOLD = {
          className: "strong",
          contains: [],
          // defined later
          variants: [
            {
              begin: /_{2}(?!\s)/,
              end: /_{2}/
            },
            {
              begin: /\*{2}(?!\s)/,
              end: /\*{2}/
            }
          ]
        };
        const ITALIC = {
          className: "emphasis",
          contains: [],
          // defined later
          variants: [
            {
              begin: /\*(?![*\s])/,
              end: /\*/
            },
            {
              begin: /_(?![_\s])/,
              end: /_/,
              relevance: 0
            }
          ]
        };
        const BOLD_WITHOUT_ITALIC = hljs.inherit(BOLD, { contains: [] });
        const ITALIC_WITHOUT_BOLD = hljs.inherit(ITALIC, { contains: [] });
        BOLD.contains.push(ITALIC_WITHOUT_BOLD);
        ITALIC.contains.push(BOLD_WITHOUT_ITALIC);
        let CONTAINABLE = [
          INLINE_HTML,
          LINK
        ];
        [
          BOLD,
          ITALIC,
          BOLD_WITHOUT_ITALIC,
          ITALIC_WITHOUT_BOLD
        ].forEach((m) => {
          m.contains = m.contains.concat(CONTAINABLE);
        });
        CONTAINABLE = CONTAINABLE.concat(BOLD, ITALIC);
        const HEADER = {
          className: "section",
          variants: [
            {
              begin: "^#{1,6}",
              end: "$",
              contains: CONTAINABLE
            },
            {
              begin: "(?=^.+?\\n[=-]{2,}$)",
              contains: [
                { begin: "^[=-]*$" },
                {
                  begin: "^",
                  end: "\\n",
                  contains: CONTAINABLE
                }
              ]
            }
          ]
        };
        const BLOCKQUOTE = {
          className: "quote",
          begin: "^>\\s+",
          contains: CONTAINABLE,
          end: "$"
        };
        const ENTITY = {
          //https://spec.commonmark.org/0.31.2/#entity-references
          scope: "literal",
          match: /&([a-zA-Z0-9]+|#[0-9]{1,7}|#[Xx][0-9a-fA-F]{1,6});/
        };
        return {
          name: "Markdown",
          aliases: [
            "md",
            "mkdown",
            "mkd"
          ],
          contains: [
            HEADER,
            INLINE_HTML,
            LIST,
            BOLD,
            ITALIC,
            BLOCKQUOTE,
            CODE,
            HORIZONTAL_RULE,
            LINK,
            LINK_REFERENCE,
            ENTITY
          ]
        };
      }
      module.exports = markdown;
    }
  });

  // node_modules/highlight.js/lib/languages/diff.js
  var require_diff = __commonJS({
    "node_modules/highlight.js/lib/languages/diff.js"(exports, module) {
      function diff(hljs) {
        const regex = hljs.regex;
        return {
          name: "Diff",
          aliases: ["patch"],
          contains: [
            {
              className: "meta",
              relevance: 10,
              match: regex.either(
                /^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,
                /^\*\*\* +\d+,\d+ +\*\*\*\*$/,
                /^--- +\d+,\d+ +----$/
              )
            },
            {
              className: "comment",
              variants: [
                {
                  begin: regex.either(
                    /Index: /,
                    /^index/,
                    /={3,}/,
                    /^-{3}/,
                    /^\*{3} /,
                    /^\+{3}/,
                    /^diff --git/
                  ),
                  end: /$/
                },
                { match: /^\*{15}$/ }
              ]
            },
            {
              className: "addition",
              begin: /^\+/,
              end: /$/
            },
            {
              className: "deletion",
              begin: /^-/,
              end: /$/
            },
            {
              className: "addition",
              begin: /^!/,
              end: /$/
            }
          ]
        };
      }
      module.exports = diff;
    }
  });

  // node_modules/highlight.js/lib/languages/ruby.js
  var require_ruby = __commonJS({
    "node_modules/highlight.js/lib/languages/ruby.js"(exports, module) {
      function ruby(hljs) {
        const regex = hljs.regex;
        const RUBY_METHOD_RE = "([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)";
        const CLASS_NAME_RE = regex.either(
          /\b([A-Z]+[a-z0-9]+)+/,
          // ends in caps
          /\b([A-Z]+[a-z0-9]+)+[A-Z]+/
        );
        const CLASS_NAME_WITH_NAMESPACE_RE = regex.concat(CLASS_NAME_RE, /(::\w+)*/);
        const PSEUDO_KWS = [
          "include",
          "extend",
          "prepend",
          "public",
          "private",
          "protected",
          "raise",
          "throw"
        ];
        const RUBY_KEYWORDS = {
          "variable.constant": [
            "__FILE__",
            "__LINE__",
            "__ENCODING__"
          ],
          "variable.language": [
            "self",
            "super"
          ],
          keyword: [
            "alias",
            "and",
            "begin",
            "BEGIN",
            "break",
            "case",
            "class",
            "defined",
            "do",
            "else",
            "elsif",
            "end",
            "END",
            "ensure",
            "for",
            "if",
            "in",
            "module",
            "next",
            "not",
            "or",
            "redo",
            "require",
            "rescue",
            "retry",
            "return",
            "then",
            "undef",
            "unless",
            "until",
            "when",
            "while",
            "yield",
            ...PSEUDO_KWS
          ],
          built_in: [
            "proc",
            "lambda",
            "attr_accessor",
            "attr_reader",
            "attr_writer",
            "define_method",
            "private_constant",
            "module_function"
          ],
          literal: [
            "true",
            "false",
            "nil"
          ]
        };
        const YARDOCTAG = {
          className: "doctag",
          begin: "@[A-Za-z]+"
        };
        const IRB_OBJECT = {
          begin: "#<",
          end: ">"
        };
        const COMMENT_MODES = [
          hljs.COMMENT(
            "#",
            "$",
            { contains: [YARDOCTAG] }
          ),
          hljs.COMMENT(
            "^=begin",
            "^=end",
            {
              contains: [YARDOCTAG],
              relevance: 10
            }
          ),
          hljs.COMMENT("^__END__", hljs.MATCH_NOTHING_RE)
        ];
        const SUBST = {
          className: "subst",
          begin: /#\{/,
          end: /\}/,
          keywords: RUBY_KEYWORDS
        };
        const STRING = {
          className: "string",
          contains: [
            hljs.BACKSLASH_ESCAPE,
            SUBST
          ],
          variants: [
            {
              begin: /'/,
              end: /'/
            },
            {
              begin: /"/,
              end: /"/
            },
            {
              begin: /`/,
              end: /`/
            },
            {
              begin: /%[qQwWx]?\(/,
              end: /\)/
            },
            {
              begin: /%[qQwWx]?\[/,
              end: /\]/
            },
            {
              begin: /%[qQwWx]?\{/,
              end: /\}/
            },
            {
              begin: /%[qQwWx]?</,
              end: />/
            },
            {
              begin: /%[qQwWx]?\//,
              end: /\//
            },
            {
              begin: /%[qQwWx]?%/,
              end: /%/
            },
            {
              begin: /%[qQwWx]?-/,
              end: /-/
            },
            {
              begin: /%[qQwWx]?\|/,
              end: /\|/
            },
            // in the following expressions, \B in the beginning suppresses recognition of ?-sequences
            // where ? is the last character of a preceding identifier, as in: `func?4`
            { begin: /\B\?(\\\d{1,3})/ },
            { begin: /\B\?(\\x[A-Fa-f0-9]{1,2})/ },
            { begin: /\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/ },
            { begin: /\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/ },
            { begin: /\B\?\\(c|C-)[\x20-\x7e]/ },
            { begin: /\B\?\\?\S/ },
            // heredocs
            {
              // this guard makes sure that we have an entire heredoc and not a false
              // positive (auto-detect, etc.)
              begin: regex.concat(
                /<<[-~]?'?/,
                regex.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)
              ),
              contains: [
                hljs.END_SAME_AS_BEGIN({
                  begin: /(\w+)/,
                  end: /(\w+)/,
                  contains: [
                    hljs.BACKSLASH_ESCAPE,
                    SUBST
                  ]
                })
              ]
            }
          ]
        };
        const decimal = "[1-9](_?[0-9])*|0";
        const digits = "[0-9](_?[0-9])*";
        const NUMBER = {
          className: "number",
          relevance: 0,
          variants: [
            // decimal integer/float, optionally exponential or rational, optionally imaginary
            { begin: `\\b(${decimal})(\\.(${digits}))?([eE][+-]?(${digits})|r)?i?\\b` },
            // explicit decimal/binary/octal/hexadecimal integer,
            // optionally rational and/or imaginary
            { begin: "\\b0[dD][0-9](_?[0-9])*r?i?\\b" },
            { begin: "\\b0[bB][0-1](_?[0-1])*r?i?\\b" },
            { begin: "\\b0[oO][0-7](_?[0-7])*r?i?\\b" },
            { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b" },
            // 0-prefixed implicit octal integer, optionally rational and/or imaginary
            { begin: "\\b0(_?[0-7])+r?i?\\b" }
          ]
        };
        const PARAMS = {
          variants: [
            {
              match: /\(\)/
            },
            {
              className: "params",
              begin: /\(/,
              end: /(?=\))/,
              excludeBegin: true,
              endsParent: true,
              keywords: RUBY_KEYWORDS
            }
          ]
        };
        const INCLUDE_EXTEND = {
          match: [
            /(include|extend)\s+/,
            CLASS_NAME_WITH_NAMESPACE_RE
          ],
          scope: {
            2: "title.class"
          },
          keywords: RUBY_KEYWORDS
        };
        const CLASS_DEFINITION = {
          variants: [
            {
              match: [
                /class\s+/,
                CLASS_NAME_WITH_NAMESPACE_RE,
                /\s+<\s+/,
                CLASS_NAME_WITH_NAMESPACE_RE
              ]
            },
            {
              match: [
                /\b(class|module)\s+/,
                CLASS_NAME_WITH_NAMESPACE_RE
              ]
            }
          ],
          scope: {
            2: "title.class",
            4: "title.class.inherited"
          },
          keywords: RUBY_KEYWORDS
        };
        const UPPER_CASE_CONSTANT = {
          relevance: 0,
          match: /\b[A-Z][A-Z_0-9]+\b/,
          className: "variable.constant"
        };
        const METHOD_DEFINITION = {
          match: [
            /def/,
            /\s+/,
            RUBY_METHOD_RE
          ],
          scope: {
            1: "keyword",
            3: "title.function"
          },
          contains: [
            PARAMS
          ]
        };
        const OBJECT_CREATION = {
          relevance: 0,
          match: [
            CLASS_NAME_WITH_NAMESPACE_RE,
            /\.new[. (]/
          ],
          scope: {
            1: "title.class"
          }
        };
        const CLASS_REFERENCE = {
          relevance: 0,
          match: CLASS_NAME_RE,
          scope: "title.class"
        };
        const RUBY_DEFAULT_CONTAINS = [
          STRING,
          CLASS_DEFINITION,
          INCLUDE_EXTEND,
          OBJECT_CREATION,
          UPPER_CASE_CONSTANT,
          CLASS_REFERENCE,
          METHOD_DEFINITION,
          {
            // swallow namespace qualifiers before symbols
            begin: hljs.IDENT_RE + "::"
          },
          {
            className: "symbol",
            begin: hljs.UNDERSCORE_IDENT_RE + "(!|\\?)?:",
            relevance: 0
          },
          {
            className: "symbol",
            begin: ":(?!\\s)",
            contains: [
              STRING,
              { begin: RUBY_METHOD_RE }
            ],
            relevance: 0
          },
          NUMBER,
          {
            // negative-look forward attempts to prevent false matches like:
            // @ident@ or $ident$ that might indicate this is not ruby at all
            className: "variable",
            begin: `(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])`
          },
          {
            className: "params",
            begin: /\|(?!=)/,
            end: /\|/,
            excludeBegin: true,
            excludeEnd: true,
            relevance: 0,
            // this could be a lot of things (in other languages) other than params
            keywords: RUBY_KEYWORDS
          },
          {
            // regexp container
            begin: "(" + hljs.RE_STARTERS_RE + "|unless)\\s*",
            keywords: "unless",
            contains: [
              {
                className: "regexp",
                contains: [
                  hljs.BACKSLASH_ESCAPE,
                  SUBST
                ],
                illegal: /\n/,
                variants: [
                  {
                    begin: "/",
                    end: "/[a-z]*"
                  },
                  {
                    begin: /%r\{/,
                    end: /\}[a-z]*/
                  },
                  {
                    begin: "%r\\(",
                    end: "\\)[a-z]*"
                  },
                  {
                    begin: "%r!",
                    end: "![a-z]*"
                  },
                  {
                    begin: "%r\\[",
                    end: "\\][a-z]*"
                  }
                ]
              }
            ].concat(IRB_OBJECT, COMMENT_MODES),
            relevance: 0
          }
        ].concat(IRB_OBJECT, COMMENT_MODES);
        SUBST.contains = RUBY_DEFAULT_CONTAINS;
        PARAMS.contains = RUBY_DEFAULT_CONTAINS;
        const SIMPLE_PROMPT = "[>?]>";
        const DEFAULT_PROMPT = "[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]";
        const RVM_PROMPT = "(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>";
        const IRB_DEFAULT = [
          {
            begin: /^\s*=>/,
            starts: {
              end: "$",
              contains: RUBY_DEFAULT_CONTAINS
            }
          },
          {
            className: "meta.prompt",
            begin: "^(" + SIMPLE_PROMPT + "|" + DEFAULT_PROMPT + "|" + RVM_PROMPT + ")(?=[ ])",
            starts: {
              end: "$",
              keywords: RUBY_KEYWORDS,
              contains: RUBY_DEFAULT_CONTAINS
            }
          }
        ];
        COMMENT_MODES.unshift(IRB_OBJECT);
        return {
          name: "Ruby",
          aliases: [
            "rb",
            "gemspec",
            "podspec",
            "thor",
            "irb"
          ],
          keywords: RUBY_KEYWORDS,
          illegal: /\/\*/,
          contains: [hljs.SHEBANG({ binary: "ruby" })].concat(IRB_DEFAULT).concat(COMMENT_MODES).concat(RUBY_DEFAULT_CONTAINS)
        };
      }
      module.exports = ruby;
    }
  });

  // node_modules/highlight.js/lib/languages/go.js
  var require_go = __commonJS({
    "node_modules/highlight.js/lib/languages/go.js"(exports, module) {
      function go(hljs) {
        const LITERALS = [
          "true",
          "false",
          "iota",
          "nil"
        ];
        const BUILT_INS = [
          "append",
          "cap",
          "close",
          "complex",
          "copy",
          "imag",
          "len",
          "make",
          "new",
          "panic",
          "print",
          "println",
          "real",
          "recover",
          "delete"
        ];
        const TYPES = [
          "bool",
          "byte",
          "complex64",
          "complex128",
          "error",
          "float32",
          "float64",
          "int8",
          "int16",
          "int32",
          "int64",
          "string",
          "uint8",
          "uint16",
          "uint32",
          "uint64",
          "int",
          "uint",
          "uintptr",
          "rune"
        ];
        const KWS = [
          "break",
          "case",
          "chan",
          "const",
          "continue",
          "default",
          "defer",
          "else",
          "fallthrough",
          "for",
          "func",
          "go",
          "goto",
          "if",
          "import",
          "interface",
          "map",
          "package",
          "range",
          "return",
          "select",
          "struct",
          "switch",
          "type",
          "var"
        ];
        const KEYWORDS = {
          keyword: KWS,
          type: TYPES,
          literal: LITERALS,
          built_in: BUILT_INS
        };
        return {
          name: "Go",
          aliases: ["golang"],
          keywords: KEYWORDS,
          illegal: "</",
          contains: [
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            {
              className: "string",
              variants: [
                hljs.QUOTE_STRING_MODE,
                hljs.APOS_STRING_MODE,
                {
                  begin: "`",
                  end: "`"
                }
              ]
            },
            {
              className: "number",
              variants: [
                {
                  match: /-?\b0[xX]\.[a-fA-F0-9](_?[a-fA-F0-9])*[pP][+-]?\d(_?\d)*i?/,
                  // hex without a present digit before . (making a digit afterwards required)
                  relevance: 0
                },
                {
                  match: /-?\b0[xX](_?[a-fA-F0-9])+((\.([a-fA-F0-9](_?[a-fA-F0-9])*)?)?[pP][+-]?\d(_?\d)*)?i?/,
                  // hex with a present digit before . (making a digit afterwards optional)
                  relevance: 0
                },
                {
                  match: /-?\b0[oO](_?[0-7])*i?/,
                  // leading 0o octal
                  relevance: 0
                },
                {
                  match: /-?\.\d(_?\d)*([eE][+-]?\d(_?\d)*)?i?/,
                  // decimal without a present digit before . (making a digit afterwards required)
                  relevance: 0
                },
                {
                  match: /-?\b\d(_?\d)*(\.(\d(_?\d)*)?)?([eE][+-]?\d(_?\d)*)?i?/,
                  // decimal with a present digit before . (making a digit afterwards optional)
                  relevance: 0
                }
              ]
            },
            {
              begin: /:=/
              // relevance booster
            },
            {
              className: "function",
              beginKeywords: "func",
              end: "\\s*(\\{|$)",
              excludeEnd: true,
              contains: [
                hljs.TITLE_MODE,
                {
                  className: "params",
                  begin: /\(/,
                  end: /\)/,
                  endsParent: true,
                  keywords: KEYWORDS,
                  illegal: /["']/
                }
              ]
            }
          ]
        };
      }
      module.exports = go;
    }
  });

  // node_modules/highlight.js/lib/languages/graphql.js
  var require_graphql = __commonJS({
    "node_modules/highlight.js/lib/languages/graphql.js"(exports, module) {
      function graphql(hljs) {
        const regex = hljs.regex;
        const GQL_NAME = /[_A-Za-z][_0-9A-Za-z]*/;
        return {
          name: "GraphQL",
          aliases: ["gql"],
          case_insensitive: true,
          disableAutodetect: false,
          keywords: {
            keyword: [
              "query",
              "mutation",
              "subscription",
              "type",
              "input",
              "schema",
              "directive",
              "interface",
              "union",
              "scalar",
              "fragment",
              "enum",
              "on"
            ],
            literal: [
              "true",
              "false",
              "null"
            ]
          },
          contains: [
            hljs.HASH_COMMENT_MODE,
            hljs.QUOTE_STRING_MODE,
            hljs.NUMBER_MODE,
            {
              scope: "punctuation",
              match: /[.]{3}/,
              relevance: 0
            },
            {
              scope: "punctuation",
              begin: /[\!\(\)\:\=\[\]\{\|\}]{1}/,
              relevance: 0
            },
            {
              scope: "variable",
              begin: /\$/,
              end: /\W/,
              excludeEnd: true,
              relevance: 0
            },
            {
              scope: "meta",
              match: /@\w+/,
              excludeEnd: true
            },
            {
              scope: "symbol",
              begin: regex.concat(GQL_NAME, regex.lookahead(/\s*:/)),
              relevance: 0
            }
          ],
          illegal: [
            /[;<']/,
            /BEGIN/
          ]
        };
      }
      module.exports = graphql;
    }
  });

  // node_modules/highlight.js/lib/languages/ini.js
  var require_ini = __commonJS({
    "node_modules/highlight.js/lib/languages/ini.js"(exports, module) {
      function ini(hljs) {
        const regex = hljs.regex;
        const NUMBERS = {
          className: "number",
          relevance: 0,
          variants: [
            { begin: /([+-]+)?[\d]+_[\d_]+/ },
            { begin: hljs.NUMBER_RE }
          ]
        };
        const COMMENTS = hljs.COMMENT();
        COMMENTS.variants = [
          {
            begin: /;/,
            end: /$/
          },
          {
            begin: /#/,
            end: /$/
          }
        ];
        const VARIABLES = {
          className: "variable",
          variants: [
            { begin: /\$[\w\d"][\w\d_]*/ },
            { begin: /\$\{(.*?)\}/ }
          ]
        };
        const LITERALS = {
          className: "literal",
          begin: /\bon|off|true|false|yes|no\b/
        };
        const STRINGS = {
          className: "string",
          contains: [hljs.BACKSLASH_ESCAPE],
          variants: [
            {
              begin: "'''",
              end: "'''",
              relevance: 10
            },
            {
              begin: '"""',
              end: '"""',
              relevance: 10
            },
            {
              begin: '"',
              end: '"'
            },
            {
              begin: "'",
              end: "'"
            }
          ]
        };
        const ARRAY = {
          begin: /\[/,
          end: /\]/,
          contains: [
            COMMENTS,
            LITERALS,
            VARIABLES,
            STRINGS,
            NUMBERS,
            "self"
          ],
          relevance: 0
        };
        const BARE_KEY = /[A-Za-z0-9_-]+/;
        const QUOTED_KEY_DOUBLE_QUOTE = /"(\\"|[^"])*"/;
        const QUOTED_KEY_SINGLE_QUOTE = /'[^']*'/;
        const ANY_KEY = regex.either(
          BARE_KEY,
          QUOTED_KEY_DOUBLE_QUOTE,
          QUOTED_KEY_SINGLE_QUOTE
        );
        const DOTTED_KEY = regex.concat(
          ANY_KEY,
          "(\\s*\\.\\s*",
          ANY_KEY,
          ")*",
          regex.lookahead(/\s*=\s*[^#\s]/)
        );
        return {
          name: "TOML, also INI",
          aliases: ["toml"],
          case_insensitive: true,
          illegal: /\S/,
          contains: [
            COMMENTS,
            {
              className: "section",
              begin: /\[+/,
              end: /\]+/
            },
            {
              begin: DOTTED_KEY,
              className: "attr",
              starts: {
                end: /$/,
                contains: [
                  COMMENTS,
                  ARRAY,
                  LITERALS,
                  VARIABLES,
                  STRINGS,
                  NUMBERS
                ]
              }
            }
          ]
        };
      }
      module.exports = ini;
    }
  });

  // node_modules/highlight.js/lib/languages/java.js
  var require_java = __commonJS({
    "node_modules/highlight.js/lib/languages/java.js"(exports, module) {
      var decimalDigits = "[0-9](_*[0-9])*";
      var frac = `\\.(${decimalDigits})`;
      var hexDigits = "[0-9a-fA-F](_*[0-9a-fA-F])*";
      var NUMERIC = {
        className: "number",
        variants: [
          // DecimalFloatingPointLiteral
          // including ExponentPart
          { begin: `(\\b(${decimalDigits})((${frac})|\\.)?|(${frac}))[eE][+-]?(${decimalDigits})[fFdD]?\\b` },
          // excluding ExponentPart
          { begin: `\\b(${decimalDigits})((${frac})[fFdD]?\\b|\\.([fFdD]\\b)?)` },
          { begin: `(${frac})[fFdD]?\\b` },
          { begin: `\\b(${decimalDigits})[fFdD]\\b` },
          // HexadecimalFloatingPointLiteral
          { begin: `\\b0[xX]((${hexDigits})\\.?|(${hexDigits})?\\.(${hexDigits}))[pP][+-]?(${decimalDigits})[fFdD]?\\b` },
          // DecimalIntegerLiteral
          { begin: "\\b(0|[1-9](_*[0-9])*)[lL]?\\b" },
          // HexIntegerLiteral
          { begin: `\\b0[xX](${hexDigits})[lL]?\\b` },
          // OctalIntegerLiteral
          { begin: "\\b0(_*[0-7])*[lL]?\\b" },
          // BinaryIntegerLiteral
          { begin: "\\b0[bB][01](_*[01])*[lL]?\\b" }
        ],
        relevance: 0
      };
      function recurRegex(re, substitution, depth) {
        if (depth === -1) return "";
        return re.replace(substitution, (_) => {
          return recurRegex(re, substitution, depth - 1);
        });
      }
      function java(hljs) {
        const regex = hljs.regex;
        const JAVA_IDENT_RE = "[\xC0-\u02B8a-zA-Z_$][\xC0-\u02B8a-zA-Z_$0-9]*";
        const GENERIC_IDENT_RE = JAVA_IDENT_RE + recurRegex("(?:<" + JAVA_IDENT_RE + "~~~(?:\\s*,\\s*" + JAVA_IDENT_RE + "~~~)*>)?", /~~~/g, 2);
        const MAIN_KEYWORDS = [
          "synchronized",
          "abstract",
          "private",
          "var",
          "static",
          "if",
          "const ",
          "for",
          "while",
          "strictfp",
          "finally",
          "protected",
          "import",
          "native",
          "final",
          "void",
          "enum",
          "else",
          "break",
          "transient",
          "catch",
          "instanceof",
          "volatile",
          "case",
          "assert",
          "package",
          "default",
          "public",
          "try",
          "switch",
          "continue",
          "throws",
          "protected",
          "public",
          "private",
          "module",
          "requires",
          "exports",
          "do",
          "sealed",
          "yield",
          "permits",
          "goto",
          "when"
        ];
        const BUILT_INS = [
          "super",
          "this"
        ];
        const LITERALS = [
          "false",
          "true",
          "null"
        ];
        const TYPES = [
          "char",
          "boolean",
          "long",
          "float",
          "int",
          "byte",
          "short",
          "double"
        ];
        const KEYWORDS = {
          keyword: MAIN_KEYWORDS,
          literal: LITERALS,
          type: TYPES,
          built_in: BUILT_INS
        };
        const ANNOTATION = {
          className: "meta",
          begin: "@" + JAVA_IDENT_RE,
          contains: [
            {
              begin: /\(/,
              end: /\)/,
              contains: ["self"]
              // allow nested () inside our annotation
            }
          ]
        };
        const PARAMS = {
          className: "params",
          begin: /\(/,
          end: /\)/,
          keywords: KEYWORDS,
          relevance: 0,
          contains: [hljs.C_BLOCK_COMMENT_MODE],
          endsParent: true
        };
        return {
          name: "Java",
          aliases: ["jsp"],
          keywords: KEYWORDS,
          illegal: /<\/|#/,
          contains: [
            hljs.COMMENT(
              "/\\*\\*",
              "\\*/",
              {
                relevance: 0,
                contains: [
                  {
                    // eat up @'s in emails to prevent them to be recognized as doctags
                    begin: /\w+@/,
                    relevance: 0
                  },
                  {
                    className: "doctag",
                    begin: "@[A-Za-z]+"
                  }
                ]
              }
            ),
            // relevance boost
            {
              begin: /import java\.[a-z]+\./,
              keywords: "import",
              relevance: 2
            },
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            {
              begin: /"""/,
              end: /"""/,
              className: "string",
              contains: [hljs.BACKSLASH_ESCAPE]
            },
            hljs.APOS_STRING_MODE,
            hljs.QUOTE_STRING_MODE,
            {
              match: [
                /\b(?:class|interface|enum|extends|implements|new)/,
                /\s+/,
                JAVA_IDENT_RE
              ],
              className: {
                1: "keyword",
                3: "title.class"
              }
            },
            {
              // Exceptions for hyphenated keywords
              match: /non-sealed/,
              scope: "keyword"
            },
            {
              begin: [
                regex.concat(/(?!else)/, JAVA_IDENT_RE),
                /\s+/,
                JAVA_IDENT_RE,
                /\s+/,
                /=(?!=)/
              ],
              className: {
                1: "type",
                3: "variable",
                5: "operator"
              }
            },
            {
              begin: [
                /record/,
                /\s+/,
                JAVA_IDENT_RE
              ],
              className: {
                1: "keyword",
                3: "title.class"
              },
              contains: [
                PARAMS,
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            {
              // Expression keywords prevent 'keyword Name(...)' from being
              // recognized as a function definition
              beginKeywords: "new throw return else",
              relevance: 0
            },
            {
              begin: [
                "(?:" + GENERIC_IDENT_RE + "\\s+)",
                hljs.UNDERSCORE_IDENT_RE,
                /\s*(?=\()/
              ],
              className: { 2: "title.function" },
              keywords: KEYWORDS,
              contains: [
                {
                  className: "params",
                  begin: /\(/,
                  end: /\)/,
                  keywords: KEYWORDS,
                  relevance: 0,
                  contains: [
                    ANNOTATION,
                    hljs.APOS_STRING_MODE,
                    hljs.QUOTE_STRING_MODE,
                    NUMERIC,
                    hljs.C_BLOCK_COMMENT_MODE
                  ]
                },
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            NUMERIC,
            ANNOTATION
          ]
        };
      }
      module.exports = java;
    }
  });

  // node_modules/highlight.js/lib/languages/javascript.js
  var require_javascript = __commonJS({
    "node_modules/highlight.js/lib/languages/javascript.js"(exports, module) {
      var IDENT_RE = "[A-Za-z$_][0-9A-Za-z$_]*";
      var KEYWORDS = [
        "as",
        // for exports
        "in",
        "of",
        "if",
        "for",
        "while",
        "finally",
        "var",
        "new",
        "function",
        "do",
        "return",
        "void",
        "else",
        "break",
        "catch",
        "instanceof",
        "with",
        "throw",
        "case",
        "default",
        "try",
        "switch",
        "continue",
        "typeof",
        "delete",
        "let",
        "yield",
        "const",
        "class",
        // JS handles these with a special rule
        // "get",
        // "set",
        "debugger",
        "async",
        "await",
        "static",
        "import",
        "from",
        "export",
        "extends",
        // It's reached stage 3, which is "recommended for implementation":
        "using"
      ];
      var LITERALS = [
        "true",
        "false",
        "null",
        "undefined",
        "NaN",
        "Infinity"
      ];
      var TYPES = [
        // Fundamental objects
        "Object",
        "Function",
        "Boolean",
        "Symbol",
        // numbers and dates
        "Math",
        "Date",
        "Number",
        "BigInt",
        // text
        "String",
        "RegExp",
        // Indexed collections
        "Array",
        "Float32Array",
        "Float64Array",
        "Int8Array",
        "Uint8Array",
        "Uint8ClampedArray",
        "Int16Array",
        "Int32Array",
        "Uint16Array",
        "Uint32Array",
        "BigInt64Array",
        "BigUint64Array",
        // Keyed collections
        "Set",
        "Map",
        "WeakSet",
        "WeakMap",
        // Structured data
        "ArrayBuffer",
        "SharedArrayBuffer",
        "Atomics",
        "DataView",
        "JSON",
        // Control abstraction objects
        "Promise",
        "Generator",
        "GeneratorFunction",
        "AsyncFunction",
        // Reflection
        "Reflect",
        "Proxy",
        // Internationalization
        "Intl",
        // WebAssembly
        "WebAssembly"
      ];
      var ERROR_TYPES = [
        "Error",
        "EvalError",
        "InternalError",
        "RangeError",
        "ReferenceError",
        "SyntaxError",
        "TypeError",
        "URIError"
      ];
      var BUILT_IN_GLOBALS = [
        "setInterval",
        "setTimeout",
        "clearInterval",
        "clearTimeout",
        "require",
        "exports",
        "eval",
        "isFinite",
        "isNaN",
        "parseFloat",
        "parseInt",
        "decodeURI",
        "decodeURIComponent",
        "encodeURI",
        "encodeURIComponent",
        "escape",
        "unescape"
      ];
      var BUILT_IN_VARIABLES = [
        "arguments",
        "this",
        "super",
        "console",
        "window",
        "document",
        "localStorage",
        "sessionStorage",
        "module",
        "global"
        // Node.js
      ];
      var BUILT_INS = [].concat(
        BUILT_IN_GLOBALS,
        TYPES,
        ERROR_TYPES
      );
      function javascript(hljs) {
        const regex = hljs.regex;
        const hasClosingTag = (match, { after }) => {
          const tag = "</" + match[0].slice(1);
          const pos = match.input.indexOf(tag, after);
          return pos !== -1;
        };
        const IDENT_RE$1 = IDENT_RE;
        const FRAGMENT = {
          begin: "<>",
          end: "</>"
        };
        const XML_SELF_CLOSING = /<[A-Za-z0-9\\._:-]+\s*\/>/;
        const XML_TAG = {
          begin: /<[A-Za-z0-9\\._:-]+/,
          end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
          /**
           * @param {RegExpMatchArray} match
           * @param {CallbackResponse} response
           */
          isTrulyOpeningTag: (match, response) => {
            const afterMatchIndex = match[0].length + match.index;
            const nextChar = match.input[afterMatchIndex];
            if (
              // HTML should not include another raw `<` inside a tag
              // nested type?
              // `<Array<Array<number>>`, etc.
              nextChar === "<" || // the , gives away that this is not HTML
              // `<T, A extends keyof T, V>`
              nextChar === ","
            ) {
              response.ignoreMatch();
              return;
            }
            if (nextChar === ">") {
              if (!hasClosingTag(match, { after: afterMatchIndex })) {
                response.ignoreMatch();
              }
            }
            let m;
            const afterMatch = match.input.substring(afterMatchIndex);
            if (m = afterMatch.match(/^\s*=/)) {
              response.ignoreMatch();
              return;
            }
            if (m = afterMatch.match(/^\s+extends\s+/)) {
              if (m.index === 0) {
                response.ignoreMatch();
                return;
              }
            }
          }
        };
        const KEYWORDS$1 = {
          $pattern: IDENT_RE,
          keyword: KEYWORDS,
          literal: LITERALS,
          built_in: BUILT_INS,
          "variable.language": BUILT_IN_VARIABLES
        };
        const decimalDigits = "[0-9](_?[0-9])*";
        const frac = `\\.(${decimalDigits})`;
        const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;
        const NUMBER = {
          className: "number",
          variants: [
            // DecimalLiteral
            { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))[eE][+-]?(${decimalDigits})\\b` },
            { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
            // DecimalBigIntegerLiteral
            { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` },
            // NonDecimalIntegerLiteral
            { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" },
            { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" },
            { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" },
            // LegacyOctalIntegerLiteral (does not include underscore separators)
            // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
            { begin: "\\b0[0-7]+n?\\b" }
          ],
          relevance: 0
        };
        const SUBST = {
          className: "subst",
          begin: "\\$\\{",
          end: "\\}",
          keywords: KEYWORDS$1,
          contains: []
          // defined later
        };
        const HTML_TEMPLATE = {
          begin: ".?html`",
          end: "",
          starts: {
            end: "`",
            returnEnd: false,
            contains: [
              hljs.BACKSLASH_ESCAPE,
              SUBST
            ],
            subLanguage: "xml"
          }
        };
        const CSS_TEMPLATE = {
          begin: ".?css`",
          end: "",
          starts: {
            end: "`",
            returnEnd: false,
            contains: [
              hljs.BACKSLASH_ESCAPE,
              SUBST
            ],
            subLanguage: "css"
          }
        };
        const GRAPHQL_TEMPLATE = {
          begin: ".?gql`",
          end: "",
          starts: {
            end: "`",
            returnEnd: false,
            contains: [
              hljs.BACKSLASH_ESCAPE,
              SUBST
            ],
            subLanguage: "graphql"
          }
        };
        const TEMPLATE_STRING = {
          className: "string",
          begin: "`",
          end: "`",
          contains: [
            hljs.BACKSLASH_ESCAPE,
            SUBST
          ]
        };
        const JSDOC_COMMENT = hljs.COMMENT(
          /\/\*\*(?!\/)/,
          "\\*/",
          {
            relevance: 0,
            contains: [
              {
                begin: "(?=@[A-Za-z]+)",
                relevance: 0,
                contains: [
                  {
                    className: "doctag",
                    begin: "@[A-Za-z]+"
                  },
                  {
                    className: "type",
                    begin: "\\{",
                    end: "\\}",
                    excludeEnd: true,
                    excludeBegin: true,
                    relevance: 0
                  },
                  {
                    className: "variable",
                    begin: IDENT_RE$1 + "(?=\\s*(-)|$)",
                    endsParent: true,
                    relevance: 0
                  },
                  // eat spaces (not newlines) so we can find
                  // types or variables
                  {
                    begin: /(?=[^\n])\s/,
                    relevance: 0
                  }
                ]
              }
            ]
          }
        );
        const COMMENT = {
          className: "comment",
          variants: [
            JSDOC_COMMENT,
            hljs.C_BLOCK_COMMENT_MODE,
            hljs.C_LINE_COMMENT_MODE
          ]
        };
        const SUBST_INTERNALS = [
          hljs.APOS_STRING_MODE,
          hljs.QUOTE_STRING_MODE,
          HTML_TEMPLATE,
          CSS_TEMPLATE,
          GRAPHQL_TEMPLATE,
          TEMPLATE_STRING,
          // Skip numbers when they are part of a variable name
          { match: /\$\d+/ },
          NUMBER
          // This is intentional:
          // See https://github.com/highlightjs/highlight.js/issues/3288
          // hljs.REGEXP_MODE
        ];
        SUBST.contains = SUBST_INTERNALS.concat({
          // we need to pair up {} inside our subst to prevent
          // it from ending too early by matching another }
          begin: /\{/,
          end: /\}/,
          keywords: KEYWORDS$1,
          contains: [
            "self"
          ].concat(SUBST_INTERNALS)
        });
        const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);
        const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([
          // eat recursive parens in sub expressions
          {
            begin: /(\s*)\(/,
            end: /\)/,
            keywords: KEYWORDS$1,
            contains: ["self"].concat(SUBST_AND_COMMENTS)
          }
        ]);
        const PARAMS = {
          className: "params",
          // convert this to negative lookbehind in v12
          begin: /(\s*)\(/,
          // to match the parms with
          end: /\)/,
          excludeBegin: true,
          excludeEnd: true,
          keywords: KEYWORDS$1,
          contains: PARAMS_CONTAINS
        };
        const CLASS_OR_EXTENDS = {
          variants: [
            // class Car extends vehicle
            {
              match: [
                /class/,
                /\s+/,
                IDENT_RE$1,
                /\s+/,
                /extends/,
                /\s+/,
                regex.concat(IDENT_RE$1, "(", regex.concat(/\./, IDENT_RE$1), ")*")
              ],
              scope: {
                1: "keyword",
                3: "title.class",
                5: "keyword",
                7: "title.class.inherited"
              }
            },
            // class Car
            {
              match: [
                /class/,
                /\s+/,
                IDENT_RE$1
              ],
              scope: {
                1: "keyword",
                3: "title.class"
              }
            }
          ]
        };
        const CLASS_REFERENCE = {
          relevance: 0,
          match: regex.either(
            // Hard coded exceptions
            /\bJSON/,
            // Float32Array, OutT
            /\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,
            // CSSFactory, CSSFactoryT
            /\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,
            // FPs, FPsT
            /\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/
            // P
            // single letters are not highlighted
            // BLAH
            // this will be flagged as a UPPER_CASE_CONSTANT instead
          ),
          className: "title.class",
          keywords: {
            _: [
              // se we still get relevance credit for JS library classes
              ...TYPES,
              ...ERROR_TYPES
            ]
          }
        };
        const USE_STRICT = {
          label: "use_strict",
          className: "meta",
          relevance: 10,
          begin: /^\s*['"]use (strict|asm)['"]/
        };
        const FUNCTION_DEFINITION = {
          variants: [
            {
              match: [
                /function/,
                /\s+/,
                IDENT_RE$1,
                /(?=\s*\()/
              ]
            },
            // anonymous function
            {
              match: [
                /function/,
                /\s*(?=\()/
              ]
            }
          ],
          className: {
            1: "keyword",
            3: "title.function"
          },
          label: "func.def",
          contains: [PARAMS],
          illegal: /%/
        };
        const UPPER_CASE_CONSTANT = {
          relevance: 0,
          match: /\b[A-Z][A-Z_0-9]+\b/,
          className: "variable.constant"
        };
        function noneOf(list) {
          return regex.concat("(?!", list.join("|"), ")");
        }
        const FUNCTION_CALL = {
          match: regex.concat(
            /\b/,
            noneOf([
              ...BUILT_IN_GLOBALS,
              "super",
              "import"
            ].map((x) => `${x}\\s*\\(`)),
            IDENT_RE$1,
            regex.lookahead(/\s*\(/)
          ),
          className: "title.function",
          relevance: 0
        };
        const PROPERTY_ACCESS = {
          begin: regex.concat(/\./, regex.lookahead(
            regex.concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/)
          )),
          end: IDENT_RE$1,
          excludeBegin: true,
          keywords: "prototype",
          className: "property",
          relevance: 0
        };
        const GETTER_OR_SETTER = {
          match: [
            /get|set/,
            /\s+/,
            IDENT_RE$1,
            /(?=\()/
          ],
          className: {
            1: "keyword",
            3: "title.function"
          },
          contains: [
            {
              // eat to avoid empty params
              begin: /\(\)/
            },
            PARAMS
          ]
        };
        const FUNC_LEAD_IN_RE = "(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|" + hljs.UNDERSCORE_IDENT_RE + ")\\s*=>";
        const FUNCTION_VARIABLE = {
          match: [
            /const|var|let/,
            /\s+/,
            IDENT_RE$1,
            /\s*/,
            /=\s*/,
            /(async\s*)?/,
            // async is optional
            regex.lookahead(FUNC_LEAD_IN_RE)
          ],
          keywords: "async",
          className: {
            1: "keyword",
            3: "title.function"
          },
          contains: [
            PARAMS
          ]
        };
        return {
          name: "JavaScript",
          aliases: ["js", "jsx", "mjs", "cjs"],
          keywords: KEYWORDS$1,
          // this will be extended by TypeScript
          exports: { PARAMS_CONTAINS, CLASS_REFERENCE },
          illegal: /#(?![$_A-z])/,
          contains: [
            hljs.SHEBANG({
              label: "shebang",
              binary: "node",
              relevance: 5
            }),
            USE_STRICT,
            hljs.APOS_STRING_MODE,
            hljs.QUOTE_STRING_MODE,
            HTML_TEMPLATE,
            CSS_TEMPLATE,
            GRAPHQL_TEMPLATE,
            TEMPLATE_STRING,
            COMMENT,
            // Skip numbers when they are part of a variable name
            { match: /\$\d+/ },
            NUMBER,
            CLASS_REFERENCE,
            {
              scope: "attr",
              match: IDENT_RE$1 + regex.lookahead(":"),
              relevance: 0
            },
            FUNCTION_VARIABLE,
            {
              // "value" container
              begin: "(" + hljs.RE_STARTERS_RE + "|\\b(case|return|throw)\\b)\\s*",
              keywords: "return throw case",
              relevance: 0,
              contains: [
                COMMENT,
                hljs.REGEXP_MODE,
                {
                  className: "function",
                  // we have to count the parens to make sure we actually have the
                  // correct bounding ( ) before the =>.  There could be any number of
                  // sub-expressions inside also surrounded by parens.
                  begin: FUNC_LEAD_IN_RE,
                  returnBegin: true,
                  end: "\\s*=>",
                  contains: [
                    {
                      className: "params",
                      variants: [
                        {
                          begin: hljs.UNDERSCORE_IDENT_RE,
                          relevance: 0
                        },
                        {
                          className: null,
                          begin: /\(\s*\)/,
                          skip: true
                        },
                        {
                          begin: /(\s*)\(/,
                          end: /\)/,
                          excludeBegin: true,
                          excludeEnd: true,
                          keywords: KEYWORDS$1,
                          contains: PARAMS_CONTAINS
                        }
                      ]
                    }
                  ]
                },
                {
                  // could be a comma delimited list of params to a function call
                  begin: /,/,
                  relevance: 0
                },
                {
                  match: /\s+/,
                  relevance: 0
                },
                {
                  // JSX
                  variants: [
                    { begin: FRAGMENT.begin, end: FRAGMENT.end },
                    { match: XML_SELF_CLOSING },
                    {
                      begin: XML_TAG.begin,
                      // we carefully check the opening tag to see if it truly
                      // is a tag and not a false positive
                      "on:begin": XML_TAG.isTrulyOpeningTag,
                      end: XML_TAG.end
                    }
                  ],
                  subLanguage: "xml",
                  contains: [
                    {
                      begin: XML_TAG.begin,
                      end: XML_TAG.end,
                      skip: true,
                      contains: ["self"]
                    }
                  ]
                }
              ]
            },
            FUNCTION_DEFINITION,
            {
              // prevent this from getting swallowed up by function
              // since they appear "function like"
              beginKeywords: "while if switch catch for"
            },
            {
              // we have to count the parens to make sure we actually have the correct
              // bounding ( ).  There could be any number of sub-expressions inside
              // also surrounded by parens.
              begin: "\\b(?!function)" + hljs.UNDERSCORE_IDENT_RE + "\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
              // end parens
              returnBegin: true,
              label: "func.def",
              contains: [
                PARAMS,
                hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1, className: "title.function" })
              ]
            },
            // catch ... so it won't trigger the property rule below
            {
              match: /\.\.\./,
              relevance: 0
            },
            PROPERTY_ACCESS,
            // hack: prevents detection of keywords in some circumstances
            // .keyword()
            // $keyword = x
            {
              match: "\\$" + IDENT_RE$1,
              relevance: 0
            },
            {
              match: [/\bconstructor(?=\s*\()/],
              className: { 1: "title.function" },
              contains: [PARAMS]
            },
            FUNCTION_CALL,
            UPPER_CASE_CONSTANT,
            CLASS_OR_EXTENDS,
            GETTER_OR_SETTER,
            {
              match: /\$[(.]/
              // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`
            }
          ]
        };
      }
      module.exports = javascript;
    }
  });

  // node_modules/highlight.js/lib/languages/json.js
  var require_json = __commonJS({
    "node_modules/highlight.js/lib/languages/json.js"(exports, module) {
      function json(hljs) {
        const ATTRIBUTE = {
          className: "attr",
          begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/,
          relevance: 1.01
        };
        const PUNCTUATION = {
          match: /[{}[\],:]/,
          className: "punctuation",
          relevance: 0
        };
        const LITERALS = [
          "true",
          "false",
          "null"
        ];
        const LITERALS_MODE = {
          scope: "literal",
          beginKeywords: LITERALS.join(" ")
        };
        return {
          name: "JSON",
          aliases: ["jsonc"],
          keywords: {
            literal: LITERALS
          },
          contains: [
            ATTRIBUTE,
            PUNCTUATION,
            hljs.QUOTE_STRING_MODE,
            LITERALS_MODE,
            hljs.C_NUMBER_MODE,
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE
          ],
          illegal: "\\S"
        };
      }
      module.exports = json;
    }
  });

  // node_modules/highlight.js/lib/languages/kotlin.js
  var require_kotlin = __commonJS({
    "node_modules/highlight.js/lib/languages/kotlin.js"(exports, module) {
      var decimalDigits = "[0-9](_*[0-9])*";
      var frac = `\\.(${decimalDigits})`;
      var hexDigits = "[0-9a-fA-F](_*[0-9a-fA-F])*";
      var NUMERIC = {
        className: "number",
        variants: [
          // DecimalFloatingPointLiteral
          // including ExponentPart
          { begin: `(\\b(${decimalDigits})((${frac})|\\.)?|(${frac}))[eE][+-]?(${decimalDigits})[fFdD]?\\b` },
          // excluding ExponentPart
          { begin: `\\b(${decimalDigits})((${frac})[fFdD]?\\b|\\.([fFdD]\\b)?)` },
          { begin: `(${frac})[fFdD]?\\b` },
          { begin: `\\b(${decimalDigits})[fFdD]\\b` },
          // HexadecimalFloatingPointLiteral
          { begin: `\\b0[xX]((${hexDigits})\\.?|(${hexDigits})?\\.(${hexDigits}))[pP][+-]?(${decimalDigits})[fFdD]?\\b` },
          // DecimalIntegerLiteral
          { begin: "\\b(0|[1-9](_*[0-9])*)[lL]?\\b" },
          // HexIntegerLiteral
          { begin: `\\b0[xX](${hexDigits})[lL]?\\b` },
          // OctalIntegerLiteral
          { begin: "\\b0(_*[0-7])*[lL]?\\b" },
          // BinaryIntegerLiteral
          { begin: "\\b0[bB][01](_*[01])*[lL]?\\b" }
        ],
        relevance: 0
      };
      function kotlin(hljs) {
        const KEYWORDS = {
          keyword: "abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual",
          built_in: "Byte Short Char Int Long Boolean Float Double Void Unit Nothing",
          literal: "true false null"
        };
        const KEYWORDS_WITH_LABEL = {
          className: "keyword",
          begin: /\b(break|continue|return|this)\b/,
          starts: { contains: [
            {
              className: "symbol",
              begin: /@\w+/
            }
          ] }
        };
        const LABEL = {
          className: "symbol",
          begin: hljs.UNDERSCORE_IDENT_RE + "@"
        };
        const SUBST = {
          className: "subst",
          begin: /\$\{/,
          end: /\}/,
          contains: [hljs.C_NUMBER_MODE]
        };
        const VARIABLE = {
          className: "variable",
          begin: "\\$" + hljs.UNDERSCORE_IDENT_RE
        };
        const STRING = {
          className: "string",
          variants: [
            {
              begin: '"""',
              end: '"""(?=[^"])',
              contains: [
                VARIABLE,
                SUBST
              ]
            },
            // Can't use built-in modes easily, as we want to use STRING in the meta
            // context as 'meta-string' and there's no syntax to remove explicitly set
            // classNames in built-in modes.
            {
              begin: "'",
              end: "'",
              illegal: /\n/,
              contains: [hljs.BACKSLASH_ESCAPE]
            },
            {
              begin: '"',
              end: '"',
              illegal: /\n/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                VARIABLE,
                SUBST
              ]
            }
          ]
        };
        SUBST.contains.push(STRING);
        const ANNOTATION_USE_SITE = {
          className: "meta",
          begin: "@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*" + hljs.UNDERSCORE_IDENT_RE + ")?"
        };
        const ANNOTATION = {
          className: "meta",
          begin: "@" + hljs.UNDERSCORE_IDENT_RE,
          contains: [
            {
              begin: /\(/,
              end: /\)/,
              contains: [
                hljs.inherit(STRING, { className: "string" }),
                "self"
              ]
            }
          ]
        };
        const KOTLIN_NUMBER_MODE = NUMERIC;
        const KOTLIN_NESTED_COMMENT = hljs.COMMENT(
          "/\\*",
          "\\*/",
          { contains: [hljs.C_BLOCK_COMMENT_MODE] }
        );
        const KOTLIN_PAREN_TYPE = { variants: [
          {
            className: "type",
            begin: hljs.UNDERSCORE_IDENT_RE
          },
          {
            begin: /\(/,
            end: /\)/,
            contains: []
            // defined later
          }
        ] };
        const KOTLIN_PAREN_TYPE2 = KOTLIN_PAREN_TYPE;
        KOTLIN_PAREN_TYPE2.variants[1].contains = [KOTLIN_PAREN_TYPE];
        KOTLIN_PAREN_TYPE.variants[1].contains = [KOTLIN_PAREN_TYPE2];
        return {
          name: "Kotlin",
          aliases: [
            "kt",
            "kts"
          ],
          keywords: KEYWORDS,
          contains: [
            hljs.COMMENT(
              "/\\*\\*",
              "\\*/",
              {
                relevance: 0,
                contains: [
                  {
                    className: "doctag",
                    begin: "@[A-Za-z]+"
                  }
                ]
              }
            ),
            hljs.C_LINE_COMMENT_MODE,
            KOTLIN_NESTED_COMMENT,
            KEYWORDS_WITH_LABEL,
            LABEL,
            ANNOTATION_USE_SITE,
            ANNOTATION,
            {
              className: "function",
              beginKeywords: "fun",
              end: "[(]|$",
              returnBegin: true,
              excludeEnd: true,
              keywords: KEYWORDS,
              relevance: 5,
              contains: [
                {
                  begin: hljs.UNDERSCORE_IDENT_RE + "\\s*\\(",
                  returnBegin: true,
                  relevance: 0,
                  contains: [hljs.UNDERSCORE_TITLE_MODE]
                },
                {
                  className: "type",
                  begin: /</,
                  end: />/,
                  keywords: "reified",
                  relevance: 0
                },
                {
                  className: "params",
                  begin: /\(/,
                  end: /\)/,
                  endsParent: true,
                  keywords: KEYWORDS,
                  relevance: 0,
                  contains: [
                    {
                      begin: /:/,
                      end: /[=,\/]/,
                      endsWithParent: true,
                      contains: [
                        KOTLIN_PAREN_TYPE,
                        hljs.C_LINE_COMMENT_MODE,
                        KOTLIN_NESTED_COMMENT
                      ],
                      relevance: 0
                    },
                    hljs.C_LINE_COMMENT_MODE,
                    KOTLIN_NESTED_COMMENT,
                    ANNOTATION_USE_SITE,
                    ANNOTATION,
                    STRING,
                    hljs.C_NUMBER_MODE
                  ]
                },
                KOTLIN_NESTED_COMMENT
              ]
            },
            {
              begin: [
                /class|interface|trait/,
                /\s+/,
                hljs.UNDERSCORE_IDENT_RE
              ],
              beginScope: {
                3: "title.class"
              },
              keywords: "class interface trait",
              end: /[:\{(]|$/,
              excludeEnd: true,
              illegal: "extends implements",
              contains: [
                { beginKeywords: "public protected internal private constructor" },
                hljs.UNDERSCORE_TITLE_MODE,
                {
                  className: "type",
                  begin: /</,
                  end: />/,
                  excludeBegin: true,
                  excludeEnd: true,
                  relevance: 0
                },
                {
                  className: "type",
                  begin: /[,:]\s*/,
                  end: /[<\(,){\s]|$/,
                  excludeBegin: true,
                  returnEnd: true
                },
                ANNOTATION_USE_SITE,
                ANNOTATION
              ]
            },
            STRING,
            {
              className: "meta",
              begin: "^#!/usr/bin/env",
              end: "$",
              illegal: "\n"
            },
            KOTLIN_NUMBER_MODE
          ]
        };
      }
      module.exports = kotlin;
    }
  });

  // node_modules/highlight.js/lib/languages/less.js
  var require_less = __commonJS({
    "node_modules/highlight.js/lib/languages/less.js"(exports, module) {
      var MODES = (hljs) => {
        return {
          IMPORTANT: {
            scope: "meta",
            begin: "!important"
          },
          BLOCK_COMMENT: hljs.C_BLOCK_COMMENT_MODE,
          HEXCOLOR: {
            scope: "number",
            begin: /#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/
          },
          FUNCTION_DISPATCH: {
            className: "built_in",
            begin: /[\w-]+(?=\()/
          },
          ATTRIBUTE_SELECTOR_MODE: {
            scope: "selector-attr",
            begin: /\[/,
            end: /\]/,
            illegal: "$",
            contains: [
              hljs.APOS_STRING_MODE,
              hljs.QUOTE_STRING_MODE
            ]
          },
          CSS_NUMBER_MODE: {
            scope: "number",
            begin: hljs.NUMBER_RE + "(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
            relevance: 0
          },
          CSS_VARIABLE: {
            className: "attr",
            begin: /--[A-Za-z_][A-Za-z0-9_-]*/
          }
        };
      };
      var HTML_TAGS = [
        "a",
        "abbr",
        "address",
        "article",
        "aside",
        "audio",
        "b",
        "blockquote",
        "body",
        "button",
        "canvas",
        "caption",
        "cite",
        "code",
        "dd",
        "del",
        "details",
        "dfn",
        "div",
        "dl",
        "dt",
        "em",
        "fieldset",
        "figcaption",
        "figure",
        "footer",
        "form",
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "h6",
        "header",
        "hgroup",
        "html",
        "i",
        "iframe",
        "img",
        "input",
        "ins",
        "kbd",
        "label",
        "legend",
        "li",
        "main",
        "mark",
        "menu",
        "nav",
        "object",
        "ol",
        "optgroup",
        "option",
        "p",
        "picture",
        "q",
        "quote",
        "samp",
        "section",
        "select",
        "source",
        "span",
        "strong",
        "summary",
        "sup",
        "table",
        "tbody",
        "td",
        "textarea",
        "tfoot",
        "th",
        "thead",
        "time",
        "tr",
        "ul",
        "var",
        "video"
      ];
      var SVG_TAGS = [
        "defs",
        "g",
        "marker",
        "mask",
        "pattern",
        "svg",
        "switch",
        "symbol",
        "feBlend",
        "feColorMatrix",
        "feComponentTransfer",
        "feComposite",
        "feConvolveMatrix",
        "feDiffuseLighting",
        "feDisplacementMap",
        "feFlood",
        "feGaussianBlur",
        "feImage",
        "feMerge",
        "feMorphology",
        "feOffset",
        "feSpecularLighting",
        "feTile",
        "feTurbulence",
        "linearGradient",
        "radialGradient",
        "stop",
        "circle",
        "ellipse",
        "image",
        "line",
        "path",
        "polygon",
        "polyline",
        "rect",
        "text",
        "use",
        "textPath",
        "tspan",
        "foreignObject",
        "clipPath"
      ];
      var TAGS = [
        ...HTML_TAGS,
        ...SVG_TAGS
      ];
      var MEDIA_FEATURES = [
        "any-hover",
        "any-pointer",
        "aspect-ratio",
        "color",
        "color-gamut",
        "color-index",
        "device-aspect-ratio",
        "device-height",
        "device-width",
        "display-mode",
        "forced-colors",
        "grid",
        "height",
        "hover",
        "inverted-colors",
        "monochrome",
        "orientation",
        "overflow-block",
        "overflow-inline",
        "pointer",
        "prefers-color-scheme",
        "prefers-contrast",
        "prefers-reduced-motion",
        "prefers-reduced-transparency",
        "resolution",
        "scan",
        "scripting",
        "update",
        "width",
        // TODO: find a better solution?
        "min-width",
        "max-width",
        "min-height",
        "max-height"
      ].sort().reverse();
      var PSEUDO_CLASSES = [
        "active",
        "any-link",
        "blank",
        "checked",
        "current",
        "default",
        "defined",
        "dir",
        // dir()
        "disabled",
        "drop",
        "empty",
        "enabled",
        "first",
        "first-child",
        "first-of-type",
        "fullscreen",
        "future",
        "focus",
        "focus-visible",
        "focus-within",
        "has",
        // has()
        "host",
        // host or host()
        "host-context",
        // host-context()
        "hover",
        "indeterminate",
        "in-range",
        "invalid",
        "is",
        // is()
        "lang",
        // lang()
        "last-child",
        "last-of-type",
        "left",
        "link",
        "local-link",
        "not",
        // not()
        "nth-child",
        // nth-child()
        "nth-col",
        // nth-col()
        "nth-last-child",
        // nth-last-child()
        "nth-last-col",
        // nth-last-col()
        "nth-last-of-type",
        //nth-last-of-type()
        "nth-of-type",
        //nth-of-type()
        "only-child",
        "only-of-type",
        "optional",
        "out-of-range",
        "past",
        "placeholder-shown",
        "read-only",
        "read-write",
        "required",
        "right",
        "root",
        "scope",
        "target",
        "target-within",
        "user-invalid",
        "valid",
        "visited",
        "where"
        // where()
      ].sort().reverse();
      var PSEUDO_ELEMENTS = [
        "after",
        "backdrop",
        "before",
        "cue",
        "cue-region",
        "first-letter",
        "first-line",
        "grammar-error",
        "marker",
        "part",
        "placeholder",
        "selection",
        "slotted",
        "spelling-error"
      ].sort().reverse();
      var ATTRIBUTES = [
        "accent-color",
        "align-content",
        "align-items",
        "align-self",
        "alignment-baseline",
        "all",
        "anchor-name",
        "animation",
        "animation-composition",
        "animation-delay",
        "animation-direction",
        "animation-duration",
        "animation-fill-mode",
        "animation-iteration-count",
        "animation-name",
        "animation-play-state",
        "animation-range",
        "animation-range-end",
        "animation-range-start",
        "animation-timeline",
        "animation-timing-function",
        "appearance",
        "aspect-ratio",
        "backdrop-filter",
        "backface-visibility",
        "background",
        "background-attachment",
        "background-blend-mode",
        "background-clip",
        "background-color",
        "background-image",
        "background-origin",
        "background-position",
        "background-position-x",
        "background-position-y",
        "background-repeat",
        "background-size",
        "baseline-shift",
        "block-size",
        "border",
        "border-block",
        "border-block-color",
        "border-block-end",
        "border-block-end-color",
        "border-block-end-style",
        "border-block-end-width",
        "border-block-start",
        "border-block-start-color",
        "border-block-start-style",
        "border-block-start-width",
        "border-block-style",
        "border-block-width",
        "border-bottom",
        "border-bottom-color",
        "border-bottom-left-radius",
        "border-bottom-right-radius",
        "border-bottom-style",
        "border-bottom-width",
        "border-collapse",
        "border-color",
        "border-end-end-radius",
        "border-end-start-radius",
        "border-image",
        "border-image-outset",
        "border-image-repeat",
        "border-image-slice",
        "border-image-source",
        "border-image-width",
        "border-inline",
        "border-inline-color",
        "border-inline-end",
        "border-inline-end-color",
        "border-inline-end-style",
        "border-inline-end-width",
        "border-inline-start",
        "border-inline-start-color",
        "border-inline-start-style",
        "border-inline-start-width",
        "border-inline-style",
        "border-inline-width",
        "border-left",
        "border-left-color",
        "border-left-style",
        "border-left-width",
        "border-radius",
        "border-right",
        "border-right-color",
        "border-right-style",
        "border-right-width",
        "border-spacing",
        "border-start-end-radius",
        "border-start-start-radius",
        "border-style",
        "border-top",
        "border-top-color",
        "border-top-left-radius",
        "border-top-right-radius",
        "border-top-style",
        "border-top-width",
        "border-width",
        "bottom",
        "box-align",
        "box-decoration-break",
        "box-direction",
        "box-flex",
        "box-flex-group",
        "box-lines",
        "box-ordinal-group",
        "box-orient",
        "box-pack",
        "box-shadow",
        "box-sizing",
        "break-after",
        "break-before",
        "break-inside",
        "caption-side",
        "caret-color",
        "clear",
        "clip",
        "clip-path",
        "clip-rule",
        "color",
        "color-interpolation",
        "color-interpolation-filters",
        "color-profile",
        "color-rendering",
        "color-scheme",
        "column-count",
        "column-fill",
        "column-gap",
        "column-rule",
        "column-rule-color",
        "column-rule-style",
        "column-rule-width",
        "column-span",
        "column-width",
        "columns",
        "contain",
        "contain-intrinsic-block-size",
        "contain-intrinsic-height",
        "contain-intrinsic-inline-size",
        "contain-intrinsic-size",
        "contain-intrinsic-width",
        "container",
        "container-name",
        "container-type",
        "content",
        "content-visibility",
        "counter-increment",
        "counter-reset",
        "counter-set",
        "cue",
        "cue-after",
        "cue-before",
        "cursor",
        "cx",
        "cy",
        "direction",
        "display",
        "dominant-baseline",
        "empty-cells",
        "enable-background",
        "field-sizing",
        "fill",
        "fill-opacity",
        "fill-rule",
        "filter",
        "flex",
        "flex-basis",
        "flex-direction",
        "flex-flow",
        "flex-grow",
        "flex-shrink",
        "flex-wrap",
        "float",
        "flood-color",
        "flood-opacity",
        "flow",
        "font",
        "font-display",
        "font-family",
        "font-feature-settings",
        "font-kerning",
        "font-language-override",
        "font-optical-sizing",
        "font-palette",
        "font-size",
        "font-size-adjust",
        "font-smooth",
        "font-smoothing",
        "font-stretch",
        "font-style",
        "font-synthesis",
        "font-synthesis-position",
        "font-synthesis-small-caps",
        "font-synthesis-style",
        "font-synthesis-weight",
        "font-variant",
        "font-variant-alternates",
        "font-variant-caps",
        "font-variant-east-asian",
        "font-variant-emoji",
        "font-variant-ligatures",
        "font-variant-numeric",
        "font-variant-position",
        "font-variation-settings",
        "font-weight",
        "forced-color-adjust",
        "gap",
        "glyph-orientation-horizontal",
        "glyph-orientation-vertical",
        "grid",
        "grid-area",
        "grid-auto-columns",
        "grid-auto-flow",
        "grid-auto-rows",
        "grid-column",
        "grid-column-end",
        "grid-column-start",
        "grid-gap",
        "grid-row",
        "grid-row-end",
        "grid-row-start",
        "grid-template",
        "grid-template-areas",
        "grid-template-columns",
        "grid-template-rows",
        "hanging-punctuation",
        "height",
        "hyphenate-character",
        "hyphenate-limit-chars",
        "hyphens",
        "icon",
        "image-orientation",
        "image-rendering",
        "image-resolution",
        "ime-mode",
        "initial-letter",
        "initial-letter-align",
        "inline-size",
        "inset",
        "inset-area",
        "inset-block",
        "inset-block-end",
        "inset-block-start",
        "inset-inline",
        "inset-inline-end",
        "inset-inline-start",
        "isolation",
        "justify-content",
        "justify-items",
        "justify-self",
        "kerning",
        "left",
        "letter-spacing",
        "lighting-color",
        "line-break",
        "line-height",
        "line-height-step",
        "list-style",
        "list-style-image",
        "list-style-position",
        "list-style-type",
        "margin",
        "margin-block",
        "margin-block-end",
        "margin-block-start",
        "margin-bottom",
        "margin-inline",
        "margin-inline-end",
        "margin-inline-start",
        "margin-left",
        "margin-right",
        "margin-top",
        "margin-trim",
        "marker",
        "marker-end",
        "marker-mid",
        "marker-start",
        "marks",
        "mask",
        "mask-border",
        "mask-border-mode",
        "mask-border-outset",
        "mask-border-repeat",
        "mask-border-slice",
        "mask-border-source",
        "mask-border-width",
        "mask-clip",
        "mask-composite",
        "mask-image",
        "mask-mode",
        "mask-origin",
        "mask-position",
        "mask-repeat",
        "mask-size",
        "mask-type",
        "masonry-auto-flow",
        "math-depth",
        "math-shift",
        "math-style",
        "max-block-size",
        "max-height",
        "max-inline-size",
        "max-width",
        "min-block-size",
        "min-height",
        "min-inline-size",
        "min-width",
        "mix-blend-mode",
        "nav-down",
        "nav-index",
        "nav-left",
        "nav-right",
        "nav-up",
        "none",
        "normal",
        "object-fit",
        "object-position",
        "offset",
        "offset-anchor",
        "offset-distance",
        "offset-path",
        "offset-position",
        "offset-rotate",
        "opacity",
        "order",
        "orphans",
        "outline",
        "outline-color",
        "outline-offset",
        "outline-style",
        "outline-width",
        "overflow",
        "overflow-anchor",
        "overflow-block",
        "overflow-clip-margin",
        "overflow-inline",
        "overflow-wrap",
        "overflow-x",
        "overflow-y",
        "overlay",
        "overscroll-behavior",
        "overscroll-behavior-block",
        "overscroll-behavior-inline",
        "overscroll-behavior-x",
        "overscroll-behavior-y",
        "padding",
        "padding-block",
        "padding-block-end",
        "padding-block-start",
        "padding-bottom",
        "padding-inline",
        "padding-inline-end",
        "padding-inline-start",
        "padding-left",
        "padding-right",
        "padding-top",
        "page",
        "page-break-after",
        "page-break-before",
        "page-break-inside",
        "paint-order",
        "pause",
        "pause-after",
        "pause-before",
        "perspective",
        "perspective-origin",
        "place-content",
        "place-items",
        "place-self",
        "pointer-events",
        "position",
        "position-anchor",
        "position-visibility",
        "print-color-adjust",
        "quotes",
        "r",
        "resize",
        "rest",
        "rest-after",
        "rest-before",
        "right",
        "rotate",
        "row-gap",
        "ruby-align",
        "ruby-position",
        "scale",
        "scroll-behavior",
        "scroll-margin",
        "scroll-margin-block",
        "scroll-margin-block-end",
        "scroll-margin-block-start",
        "scroll-margin-bottom",
        "scroll-margin-inline",
        "scroll-margin-inline-end",
        "scroll-margin-inline-start",
        "scroll-margin-left",
        "scroll-margin-right",
        "scroll-margin-top",
        "scroll-padding",
        "scroll-padding-block",
        "scroll-padding-block-end",
        "scroll-padding-block-start",
        "scroll-padding-bottom",
        "scroll-padding-inline",
        "scroll-padding-inline-end",
        "scroll-padding-inline-start",
        "scroll-padding-left",
        "scroll-padding-right",
        "scroll-padding-top",
        "scroll-snap-align",
        "scroll-snap-stop",
        "scroll-snap-type",
        "scroll-timeline",
        "scroll-timeline-axis",
        "scroll-timeline-name",
        "scrollbar-color",
        "scrollbar-gutter",
        "scrollbar-width",
        "shape-image-threshold",
        "shape-margin",
        "shape-outside",
        "shape-rendering",
        "speak",
        "speak-as",
        "src",
        // @font-face
        "stop-color",
        "stop-opacity",
        "stroke",
        "stroke-dasharray",
        "stroke-dashoffset",
        "stroke-linecap",
        "stroke-linejoin",
        "stroke-miterlimit",
        "stroke-opacity",
        "stroke-width",
        "tab-size",
        "table-layout",
        "text-align",
        "text-align-all",
        "text-align-last",
        "text-anchor",
        "text-combine-upright",
        "text-decoration",
        "text-decoration-color",
        "text-decoration-line",
        "text-decoration-skip",
        "text-decoration-skip-ink",
        "text-decoration-style",
        "text-decoration-thickness",
        "text-emphasis",
        "text-emphasis-color",
        "text-emphasis-position",
        "text-emphasis-style",
        "text-indent",
        "text-justify",
        "text-orientation",
        "text-overflow",
        "text-rendering",
        "text-shadow",
        "text-size-adjust",
        "text-transform",
        "text-underline-offset",
        "text-underline-position",
        "text-wrap",
        "text-wrap-mode",
        "text-wrap-style",
        "timeline-scope",
        "top",
        "touch-action",
        "transform",
        "transform-box",
        "transform-origin",
        "transform-style",
        "transition",
        "transition-behavior",
        "transition-delay",
        "transition-duration",
        "transition-property",
        "transition-timing-function",
        "translate",
        "unicode-bidi",
        "user-modify",
        "user-select",
        "vector-effect",
        "vertical-align",
        "view-timeline",
        "view-timeline-axis",
        "view-timeline-inset",
        "view-timeline-name",
        "view-transition-name",
        "visibility",
        "voice-balance",
        "voice-duration",
        "voice-family",
        "voice-pitch",
        "voice-range",
        "voice-rate",
        "voice-stress",
        "voice-volume",
        "white-space",
        "white-space-collapse",
        "widows",
        "width",
        "will-change",
        "word-break",
        "word-spacing",
        "word-wrap",
        "writing-mode",
        "x",
        "y",
        "z-index",
        "zoom"
      ].sort().reverse();
      var PSEUDO_SELECTORS = PSEUDO_CLASSES.concat(PSEUDO_ELEMENTS).sort().reverse();
      function less(hljs) {
        const modes = MODES(hljs);
        const PSEUDO_SELECTORS$1 = PSEUDO_SELECTORS;
        const AT_MODIFIERS = "and or not only";
        const IDENT_RE = "[\\w-]+";
        const INTERP_IDENT_RE = "(" + IDENT_RE + "|@\\{" + IDENT_RE + "\\})";
        const RULES = [];
        const VALUE_MODES = [];
        const STRING_MODE = function(c) {
          return {
            // Less strings are not multiline (also include '~' for more consistent coloring of "escaped" strings)
            className: "string",
            begin: "~?" + c + ".*?" + c
          };
        };
        const IDENT_MODE = function(name, begin, relevance) {
          return {
            className: name,
            begin,
            relevance
          };
        };
        const AT_KEYWORDS = {
          $pattern: /[a-z-]+/,
          keyword: AT_MODIFIERS,
          attribute: MEDIA_FEATURES.join(" ")
        };
        const PARENS_MODE = {
          // used only to properly balance nested parens inside mixin call, def. arg list
          begin: "\\(",
          end: "\\)",
          contains: VALUE_MODES,
          keywords: AT_KEYWORDS,
          relevance: 0
        };
        VALUE_MODES.push(
          hljs.C_LINE_COMMENT_MODE,
          hljs.C_BLOCK_COMMENT_MODE,
          STRING_MODE("'"),
          STRING_MODE('"'),
          modes.CSS_NUMBER_MODE,
          // fixme: it does not include dot for numbers like .5em :(
          {
            begin: "(url|data-uri)\\(",
            starts: {
              className: "string",
              end: "[\\)\\n]",
              excludeEnd: true
            }
          },
          modes.HEXCOLOR,
          PARENS_MODE,
          IDENT_MODE("variable", "@@?" + IDENT_RE, 10),
          IDENT_MODE("variable", "@\\{" + IDENT_RE + "\\}"),
          IDENT_MODE("built_in", "~?`[^`]*?`"),
          // inline javascript (or whatever host language) *multiline* string
          {
            // @media features (it’s here to not duplicate things in AT_RULE_MODE with extra PARENS_MODE overriding):
            className: "attribute",
            begin: IDENT_RE + "\\s*:",
            end: ":",
            returnBegin: true,
            excludeEnd: true
          },
          modes.IMPORTANT,
          { beginKeywords: "and not" },
          modes.FUNCTION_DISPATCH
        );
        const VALUE_WITH_RULESETS = VALUE_MODES.concat({
          begin: /\{/,
          end: /\}/,
          contains: RULES
        });
        const MIXIN_GUARD_MODE = {
          beginKeywords: "when",
          endsWithParent: true,
          contains: [{ beginKeywords: "and not" }].concat(VALUE_MODES)
          // using this form to override VALUE’s 'function' match
        };
        const RULE_MODE = {
          begin: INTERP_IDENT_RE + "\\s*:",
          returnBegin: true,
          end: /[;}]/,
          relevance: 0,
          contains: [
            { begin: /-(webkit|moz|ms|o)-/ },
            modes.CSS_VARIABLE,
            {
              className: "attribute",
              begin: "\\b(" + ATTRIBUTES.join("|") + ")\\b",
              end: /(?=:)/,
              starts: {
                endsWithParent: true,
                illegal: "[<=$]",
                relevance: 0,
                contains: VALUE_MODES
              }
            }
          ]
        };
        const AT_RULE_MODE = {
          className: "keyword",
          begin: "@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",
          starts: {
            end: "[;{}]",
            keywords: AT_KEYWORDS,
            returnEnd: true,
            contains: VALUE_MODES,
            relevance: 0
          }
        };
        const VAR_RULE_MODE = {
          className: "variable",
          variants: [
            // using more strict pattern for higher relevance to increase chances of Less detection.
            // this is *the only* Less specific statement used in most of the sources, so...
            // (we’ll still often loose to the css-parser unless there's '//' comment,
            // simply because 1 variable just can't beat 99 properties :)
            {
              begin: "@" + IDENT_RE + "\\s*:",
              relevance: 15
            },
            { begin: "@" + IDENT_RE }
          ],
          starts: {
            end: "[;}]",
            returnEnd: true,
            contains: VALUE_WITH_RULESETS
          }
        };
        const SELECTOR_MODE = {
          // first parse unambiguous selectors (i.e. those not starting with tag)
          // then fall into the scary lookahead-discriminator variant.
          // this mode also handles mixin definitions and calls
          variants: [
            {
              begin: "[\\.#:&\\[>]",
              end: "[;{}]"
              // mixin calls end with ';'
            },
            {
              begin: INTERP_IDENT_RE,
              end: /\{/
            }
          ],
          returnBegin: true,
          returnEnd: true,
          illegal: `[<='$"]`,
          relevance: 0,
          contains: [
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            MIXIN_GUARD_MODE,
            IDENT_MODE("keyword", "all\\b"),
            IDENT_MODE("variable", "@\\{" + IDENT_RE + "\\}"),
            // otherwise it’s identified as tag
            {
              begin: "\\b(" + TAGS.join("|") + ")\\b",
              className: "selector-tag"
            },
            modes.CSS_NUMBER_MODE,
            IDENT_MODE("selector-tag", INTERP_IDENT_RE, 0),
            IDENT_MODE("selector-id", "#" + INTERP_IDENT_RE),
            IDENT_MODE("selector-class", "\\." + INTERP_IDENT_RE, 0),
            IDENT_MODE("selector-tag", "&", 0),
            modes.ATTRIBUTE_SELECTOR_MODE,
            {
              className: "selector-pseudo",
              begin: ":(" + PSEUDO_CLASSES.join("|") + ")"
            },
            {
              className: "selector-pseudo",
              begin: ":(:)?(" + PSEUDO_ELEMENTS.join("|") + ")"
            },
            {
              begin: /\(/,
              end: /\)/,
              relevance: 0,
              contains: VALUE_WITH_RULESETS
            },
            // argument list of parametric mixins
            { begin: "!important" },
            // eat !important after mixin call or it will be colored as tag
            modes.FUNCTION_DISPATCH
          ]
        };
        const PSEUDO_SELECTOR_MODE = {
          begin: IDENT_RE + `:(:)?(${PSEUDO_SELECTORS$1.join("|")})`,
          returnBegin: true,
          contains: [SELECTOR_MODE]
        };
        RULES.push(
          hljs.C_LINE_COMMENT_MODE,
          hljs.C_BLOCK_COMMENT_MODE,
          AT_RULE_MODE,
          VAR_RULE_MODE,
          PSEUDO_SELECTOR_MODE,
          RULE_MODE,
          SELECTOR_MODE,
          MIXIN_GUARD_MODE,
          modes.FUNCTION_DISPATCH
        );
        return {
          name: "Less",
          case_insensitive: true,
          illegal: `[=>'/<($"]`,
          contains: RULES
        };
      }
      module.exports = less;
    }
  });

  // node_modules/highlight.js/lib/languages/lua.js
  var require_lua = __commonJS({
    "node_modules/highlight.js/lib/languages/lua.js"(exports, module) {
      function lua(hljs) {
        const OPENING_LONG_BRACKET = "\\[=*\\[";
        const CLOSING_LONG_BRACKET = "\\]=*\\]";
        const LONG_BRACKETS = {
          begin: OPENING_LONG_BRACKET,
          end: CLOSING_LONG_BRACKET,
          contains: ["self"]
        };
        const COMMENTS = [
          hljs.COMMENT("--(?!" + OPENING_LONG_BRACKET + ")", "$"),
          hljs.COMMENT(
            "--" + OPENING_LONG_BRACKET,
            CLOSING_LONG_BRACKET,
            {
              contains: [LONG_BRACKETS],
              relevance: 10
            }
          )
        ];
        return {
          name: "Lua",
          aliases: ["pluto"],
          keywords: {
            $pattern: hljs.UNDERSCORE_IDENT_RE,
            literal: "true false nil",
            keyword: "and break do else elseif end for goto if in local not or repeat return then until while",
            built_in: (
              // Metatags and globals:
              "_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"
            )
          },
          contains: COMMENTS.concat([
            {
              className: "function",
              beginKeywords: "function",
              end: "\\)",
              contains: [
                hljs.inherit(hljs.TITLE_MODE, { begin: "([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*" }),
                {
                  className: "params",
                  begin: "\\(",
                  endsWithParent: true,
                  contains: COMMENTS
                }
              ].concat(COMMENTS)
            },
            hljs.C_NUMBER_MODE,
            hljs.APOS_STRING_MODE,
            hljs.QUOTE_STRING_MODE,
            {
              className: "string",
              begin: OPENING_LONG_BRACKET,
              end: CLOSING_LONG_BRACKET,
              contains: [LONG_BRACKETS],
              relevance: 5
            }
          ])
        };
      }
      module.exports = lua;
    }
  });

  // node_modules/highlight.js/lib/languages/makefile.js
  var require_makefile = __commonJS({
    "node_modules/highlight.js/lib/languages/makefile.js"(exports, module) {
      function makefile(hljs) {
        const VARIABLE = {
          className: "variable",
          variants: [
            {
              begin: "\\$\\(" + hljs.UNDERSCORE_IDENT_RE + "\\)",
              contains: [hljs.BACKSLASH_ESCAPE]
            },
            { begin: /\$[@%<?\^\+\*]/ }
          ]
        };
        const QUOTE_STRING = {
          className: "string",
          begin: /"/,
          end: /"/,
          contains: [
            hljs.BACKSLASH_ESCAPE,
            VARIABLE
          ]
        };
        const FUNC = {
          className: "variable",
          begin: /\$\([\w-]+\s/,
          end: /\)/,
          keywords: { built_in: "subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value" },
          contains: [
            VARIABLE,
            QUOTE_STRING
            // Added QUOTE_STRING as they can be a part of functions
          ]
        };
        const ASSIGNMENT = { begin: "^" + hljs.UNDERSCORE_IDENT_RE + "\\s*(?=[:+?]?=)" };
        const META = {
          className: "meta",
          begin: /^\.PHONY:/,
          end: /$/,
          keywords: {
            $pattern: /[\.\w]+/,
            keyword: ".PHONY"
          }
        };
        const TARGET = {
          className: "section",
          begin: /^[^\s]+:/,
          end: /$/,
          contains: [VARIABLE]
        };
        return {
          name: "Makefile",
          aliases: [
            "mk",
            "mak",
            "make"
          ],
          keywords: {
            $pattern: /[\w-]+/,
            keyword: "define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath"
          },
          contains: [
            hljs.HASH_COMMENT_MODE,
            VARIABLE,
            QUOTE_STRING,
            FUNC,
            ASSIGNMENT,
            META,
            TARGET
          ]
        };
      }
      module.exports = makefile;
    }
  });

  // node_modules/highlight.js/lib/languages/perl.js
  var require_perl = __commonJS({
    "node_modules/highlight.js/lib/languages/perl.js"(exports, module) {
      function perl(hljs) {
        const regex = hljs.regex;
        const KEYWORDS = [
          "abs",
          "accept",
          "alarm",
          "and",
          "atan2",
          "bind",
          "binmode",
          "bless",
          "break",
          "caller",
          "chdir",
          "chmod",
          "chomp",
          "chop",
          "chown",
          "chr",
          "chroot",
          "class",
          "close",
          "closedir",
          "connect",
          "continue",
          "cos",
          "crypt",
          "dbmclose",
          "dbmopen",
          "defined",
          "delete",
          "die",
          "do",
          "dump",
          "each",
          "else",
          "elsif",
          "endgrent",
          "endhostent",
          "endnetent",
          "endprotoent",
          "endpwent",
          "endservent",
          "eof",
          "eval",
          "exec",
          "exists",
          "exit",
          "exp",
          "fcntl",
          "field",
          "fileno",
          "flock",
          "for",
          "foreach",
          "fork",
          "format",
          "formline",
          "getc",
          "getgrent",
          "getgrgid",
          "getgrnam",
          "gethostbyaddr",
          "gethostbyname",
          "gethostent",
          "getlogin",
          "getnetbyaddr",
          "getnetbyname",
          "getnetent",
          "getpeername",
          "getpgrp",
          "getpriority",
          "getprotobyname",
          "getprotobynumber",
          "getprotoent",
          "getpwent",
          "getpwnam",
          "getpwuid",
          "getservbyname",
          "getservbyport",
          "getservent",
          "getsockname",
          "getsockopt",
          "given",
          "glob",
          "gmtime",
          "goto",
          "grep",
          "gt",
          "hex",
          "if",
          "index",
          "int",
          "ioctl",
          "join",
          "keys",
          "kill",
          "last",
          "lc",
          "lcfirst",
          "length",
          "link",
          "listen",
          "local",
          "localtime",
          "log",
          "lstat",
          "lt",
          "ma",
          "map",
          "method",
          "mkdir",
          "msgctl",
          "msgget",
          "msgrcv",
          "msgsnd",
          "my",
          "ne",
          "next",
          "no",
          "not",
          "oct",
          "open",
          "opendir",
          "or",
          "ord",
          "our",
          "pack",
          "package",
          "pipe",
          "pop",
          "pos",
          "print",
          "printf",
          "prototype",
          "push",
          "q|0",
          "qq",
          "quotemeta",
          "qw",
          "qx",
          "rand",
          "read",
          "readdir",
          "readline",
          "readlink",
          "readpipe",
          "recv",
          "redo",
          "ref",
          "rename",
          "require",
          "reset",
          "return",
          "reverse",
          "rewinddir",
          "rindex",
          "rmdir",
          "say",
          "scalar",
          "seek",
          "seekdir",
          "select",
          "semctl",
          "semget",
          "semop",
          "send",
          "setgrent",
          "sethostent",
          "setnetent",
          "setpgrp",
          "setpriority",
          "setprotoent",
          "setpwent",
          "setservent",
          "setsockopt",
          "shift",
          "shmctl",
          "shmget",
          "shmread",
          "shmwrite",
          "shutdown",
          "sin",
          "sleep",
          "socket",
          "socketpair",
          "sort",
          "splice",
          "split",
          "sprintf",
          "sqrt",
          "srand",
          "stat",
          "state",
          "study",
          "sub",
          "substr",
          "symlink",
          "syscall",
          "sysopen",
          "sysread",
          "sysseek",
          "system",
          "syswrite",
          "tell",
          "telldir",
          "tie",
          "tied",
          "time",
          "times",
          "tr",
          "truncate",
          "uc",
          "ucfirst",
          "umask",
          "undef",
          "unless",
          "unlink",
          "unpack",
          "unshift",
          "untie",
          "until",
          "use",
          "utime",
          "values",
          "vec",
          "wait",
          "waitpid",
          "wantarray",
          "warn",
          "when",
          "while",
          "write",
          "x|0",
          "xor",
          "y|0"
        ];
        const REGEX_MODIFIERS = /[dualxmsipngr]{0,12}/;
        const PERL_KEYWORDS = {
          $pattern: /[\w.]+/,
          keyword: KEYWORDS.join(" ")
        };
        const SUBST = {
          className: "subst",
          begin: "[$@]\\{",
          end: "\\}",
          keywords: PERL_KEYWORDS
        };
        const METHOD = {
          begin: /->\{/,
          end: /\}/
          // contains defined later
        };
        const ATTR = {
          scope: "attr",
          match: /\s+:\s*\w+(\s*\(.*?\))?/
        };
        const VAR = {
          scope: "variable",
          variants: [
            { begin: /\$\d/ },
            {
              begin: regex.concat(
                /[$%@](?!")(\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,
                // negative look-ahead tries to avoid matching patterns that are not
                // Perl at all like $ident$, @ident@, etc.
                `(?![A-Za-z])(?![@$%])`
              )
            },
            {
              // Only $= is a special Perl variable and one can't declare @= or %=.
              begin: /[$%@](?!")[^\s\w{=]|\$=/,
              relevance: 0
            }
          ],
          contains: [ATTR]
        };
        const NUMBER = {
          className: "number",
          variants: [
            // decimal numbers:
            // include the case where a number starts with a dot (eg. .9), and
            // the leading 0? avoids mixing the first and second match on 0.x cases
            { match: /0?\.[0-9][0-9_]+\b/ },
            // include the special versioned number (eg. v5.38)
            { match: /\bv?(0|[1-9][0-9_]*(\.[0-9_]+)?|[1-9][0-9_]*)\b/ },
            // non-decimal numbers:
            { match: /\b0[0-7][0-7_]*\b/ },
            { match: /\b0x[0-9a-fA-F][0-9a-fA-F_]*\b/ },
            { match: /\b0b[0-1][0-1_]*\b/ }
          ],
          relevance: 0
        };
        const STRING_CONTAINS = [
          hljs.BACKSLASH_ESCAPE,
          SUBST,
          VAR
        ];
        const REGEX_DELIMS = [
          /!/,
          /\//,
          /\|/,
          /\?/,
          /'/,
          /"/,
          // valid but infrequent and weird
          /#/
          // valid but infrequent and weird
        ];
        const PAIRED_DOUBLE_RE = (prefix, open, close = "\\1") => {
          const middle = close === "\\1" ? close : regex.concat(close, open);
          return regex.concat(
            regex.concat("(?:", prefix, ")"),
            open,
            /(?:\\.|[^\\\/])*?/,
            middle,
            /(?:\\.|[^\\\/])*?/,
            close,
            REGEX_MODIFIERS
          );
        };
        const PAIRED_RE = (prefix, open, close) => {
          return regex.concat(
            regex.concat("(?:", prefix, ")"),
            open,
            /(?:\\.|[^\\\/])*?/,
            close,
            REGEX_MODIFIERS
          );
        };
        const PERL_DEFAULT_CONTAINS = [
          VAR,
          hljs.HASH_COMMENT_MODE,
          hljs.COMMENT(
            /^=\w/,
            /=cut/,
            { endsWithParent: true }
          ),
          METHOD,
          {
            className: "string",
            contains: STRING_CONTAINS,
            variants: [
              {
                begin: "q[qwxr]?\\s*\\(",
                end: "\\)",
                relevance: 5
              },
              {
                begin: "q[qwxr]?\\s*\\[",
                end: "\\]",
                relevance: 5
              },
              {
                begin: "q[qwxr]?\\s*\\{",
                end: "\\}",
                relevance: 5
              },
              {
                begin: "q[qwxr]?\\s*\\|",
                end: "\\|",
                relevance: 5
              },
              {
                begin: "q[qwxr]?\\s*<",
                end: ">",
                relevance: 5
              },
              {
                begin: "qw\\s+q",
                end: "q",
                relevance: 5
              },
              {
                begin: "'",
                end: "'",
                contains: [hljs.BACKSLASH_ESCAPE]
              },
              {
                begin: '"',
                end: '"'
              },
              {
                begin: "`",
                end: "`",
                contains: [hljs.BACKSLASH_ESCAPE]
              },
              {
                begin: /\{\w+\}/,
                relevance: 0
              },
              {
                begin: "-?\\w+\\s*=>",
                relevance: 0
              }
            ]
          },
          NUMBER,
          {
            // regexp container
            begin: "(\\/\\/|" + hljs.RE_STARTERS_RE + "|\\b(split|return|print|reverse|grep)\\b)\\s*",
            keywords: "split return print reverse grep",
            relevance: 0,
            contains: [
              hljs.HASH_COMMENT_MODE,
              {
                className: "regexp",
                variants: [
                  // allow matching common delimiters
                  { begin: PAIRED_DOUBLE_RE("s|tr|y", regex.either(...REGEX_DELIMS, { capture: true })) },
                  // and then paired delmis
                  { begin: PAIRED_DOUBLE_RE("s|tr|y", "\\(", "\\)") },
                  { begin: PAIRED_DOUBLE_RE("s|tr|y", "\\[", "\\]") },
                  { begin: PAIRED_DOUBLE_RE("s|tr|y", "\\{", "\\}") }
                ],
                relevance: 2
              },
              {
                className: "regexp",
                variants: [
                  {
                    // could be a comment in many languages so do not count
                    // as relevant
                    begin: /(m|qr)\/\//,
                    relevance: 0
                  },
                  // prefix is optional with /regex/
                  { begin: PAIRED_RE("(?:m|qr)?", /\//, /\//) },
                  // allow matching common delimiters
                  { begin: PAIRED_RE("m|qr", regex.either(...REGEX_DELIMS, { capture: true }), /\1/) },
                  // allow common paired delmins
                  { begin: PAIRED_RE("m|qr", /\(/, /\)/) },
                  { begin: PAIRED_RE("m|qr", /\[/, /\]/) },
                  { begin: PAIRED_RE("m|qr", /\{/, /\}/) }
                ]
              }
            ]
          },
          {
            className: "function",
            beginKeywords: "sub method",
            end: "(\\s*\\(.*?\\))?[;{]",
            excludeEnd: true,
            relevance: 5,
            contains: [hljs.TITLE_MODE, ATTR]
          },
          {
            className: "class",
            beginKeywords: "class",
            end: "[;{]",
            excludeEnd: true,
            relevance: 5,
            contains: [hljs.TITLE_MODE, ATTR, NUMBER]
          },
          {
            begin: "-\\w\\b",
            relevance: 0
          },
          {
            begin: "^__DATA__$",
            end: "^__END__$",
            subLanguage: "mojolicious",
            contains: [
              {
                begin: "^@@.*",
                end: "$",
                className: "comment"
              }
            ]
          }
        ];
        SUBST.contains = PERL_DEFAULT_CONTAINS;
        METHOD.contains = PERL_DEFAULT_CONTAINS;
        return {
          name: "Perl",
          aliases: [
            "pl",
            "pm"
          ],
          keywords: PERL_KEYWORDS,
          contains: PERL_DEFAULT_CONTAINS
        };
      }
      module.exports = perl;
    }
  });

  // node_modules/highlight.js/lib/languages/objectivec.js
  var require_objectivec = __commonJS({
    "node_modules/highlight.js/lib/languages/objectivec.js"(exports, module) {
      function objectivec(hljs) {
        const API_CLASS = {
          className: "built_in",
          begin: "\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"
        };
        const IDENTIFIER_RE = /[a-zA-Z@][a-zA-Z0-9_]*/;
        const TYPES = [
          "int",
          "float",
          "char",
          "unsigned",
          "signed",
          "short",
          "long",
          "double",
          "wchar_t",
          "unichar",
          "void",
          "bool",
          "BOOL",
          "id|0",
          "_Bool"
        ];
        const KWS = [
          "while",
          "export",
          "sizeof",
          "typedef",
          "const",
          "struct",
          "for",
          "union",
          "volatile",
          "static",
          "mutable",
          "if",
          "do",
          "return",
          "goto",
          "enum",
          "else",
          "break",
          "extern",
          "asm",
          "case",
          "default",
          "register",
          "explicit",
          "typename",
          "switch",
          "continue",
          "inline",
          "readonly",
          "assign",
          "readwrite",
          "self",
          "@synchronized",
          "id",
          "typeof",
          "nonatomic",
          "IBOutlet",
          "IBAction",
          "strong",
          "weak",
          "copy",
          "in",
          "out",
          "inout",
          "bycopy",
          "byref",
          "oneway",
          "__strong",
          "__weak",
          "__block",
          "__autoreleasing",
          "@private",
          "@protected",
          "@public",
          "@try",
          "@property",
          "@end",
          "@throw",
          "@catch",
          "@finally",
          "@autoreleasepool",
          "@synthesize",
          "@dynamic",
          "@selector",
          "@optional",
          "@required",
          "@encode",
          "@package",
          "@import",
          "@defs",
          "@compatibility_alias",
          "__bridge",
          "__bridge_transfer",
          "__bridge_retained",
          "__bridge_retain",
          "__covariant",
          "__contravariant",
          "__kindof",
          "_Nonnull",
          "_Nullable",
          "_Null_unspecified",
          "__FUNCTION__",
          "__PRETTY_FUNCTION__",
          "__attribute__",
          "getter",
          "setter",
          "retain",
          "unsafe_unretained",
          "nonnull",
          "nullable",
          "null_unspecified",
          "null_resettable",
          "class",
          "instancetype",
          "NS_DESIGNATED_INITIALIZER",
          "NS_UNAVAILABLE",
          "NS_REQUIRES_SUPER",
          "NS_RETURNS_INNER_POINTER",
          "NS_INLINE",
          "NS_AVAILABLE",
          "NS_DEPRECATED",
          "NS_ENUM",
          "NS_OPTIONS",
          "NS_SWIFT_UNAVAILABLE",
          "NS_ASSUME_NONNULL_BEGIN",
          "NS_ASSUME_NONNULL_END",
          "NS_REFINED_FOR_SWIFT",
          "NS_SWIFT_NAME",
          "NS_SWIFT_NOTHROW",
          "NS_DURING",
          "NS_HANDLER",
          "NS_ENDHANDLER",
          "NS_VALUERETURN",
          "NS_VOIDRETURN"
        ];
        const LITERALS = [
          "false",
          "true",
          "FALSE",
          "TRUE",
          "nil",
          "YES",
          "NO",
          "NULL"
        ];
        const BUILT_INS = [
          "dispatch_once_t",
          "dispatch_queue_t",
          "dispatch_sync",
          "dispatch_async",
          "dispatch_once"
        ];
        const KEYWORDS = {
          "variable.language": [
            "this",
            "super"
          ],
          $pattern: IDENTIFIER_RE,
          keyword: KWS,
          literal: LITERALS,
          built_in: BUILT_INS,
          type: TYPES
        };
        const CLASS_KEYWORDS = {
          $pattern: IDENTIFIER_RE,
          keyword: [
            "@interface",
            "@class",
            "@protocol",
            "@implementation"
          ]
        };
        return {
          name: "Objective-C",
          aliases: [
            "mm",
            "objc",
            "obj-c",
            "obj-c++",
            "objective-c++"
          ],
          keywords: KEYWORDS,
          illegal: "</",
          contains: [
            API_CLASS,
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            hljs.C_NUMBER_MODE,
            hljs.QUOTE_STRING_MODE,
            hljs.APOS_STRING_MODE,
            {
              className: "string",
              variants: [
                {
                  begin: '@"',
                  end: '"',
                  illegal: "\\n",
                  contains: [hljs.BACKSLASH_ESCAPE]
                }
              ]
            },
            {
              className: "meta",
              begin: /#\s*[a-z]+\b/,
              end: /$/,
              keywords: { keyword: "if else elif endif define undef warning error line pragma ifdef ifndef include" },
              contains: [
                {
                  begin: /\\\n/,
                  relevance: 0
                },
                hljs.inherit(hljs.QUOTE_STRING_MODE, { className: "string" }),
                {
                  className: "string",
                  begin: /<.*?>/,
                  end: /$/,
                  illegal: "\\n"
                },
                hljs.C_LINE_COMMENT_MODE,
                hljs.C_BLOCK_COMMENT_MODE
              ]
            },
            {
              className: "class",
              begin: "(" + CLASS_KEYWORDS.keyword.join("|") + ")\\b",
              end: /(\{|$)/,
              excludeEnd: true,
              keywords: CLASS_KEYWORDS,
              contains: [hljs.UNDERSCORE_TITLE_MODE]
            },
            {
              begin: "\\." + hljs.UNDERSCORE_IDENT_RE,
              relevance: 0
            }
          ]
        };
      }
      module.exports = objectivec;
    }
  });

  // node_modules/highlight.js/lib/languages/php.js
  var require_php = __commonJS({
    "node_modules/highlight.js/lib/languages/php.js"(exports, module) {
      function php(hljs) {
        const regex = hljs.regex;
        const NOT_PERL_ETC = /(?![A-Za-z0-9])(?![$])/;
        const IDENT_RE = regex.concat(
          /[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,
          NOT_PERL_ETC
        );
        const PASCAL_CASE_CLASS_NAME_RE = regex.concat(
          /(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,
          NOT_PERL_ETC
        );
        const UPCASE_NAME_RE = regex.concat(
          /[A-Z]+/,
          NOT_PERL_ETC
        );
        const VARIABLE = {
          scope: "variable",
          match: "\\$+" + IDENT_RE
        };
        const PREPROCESSOR = {
          scope: "meta",
          variants: [
            { begin: /<\?php/, relevance: 10 },
            // boost for obvious PHP
            { begin: /<\?=/ },
            // less relevant per PSR-1 which says not to use short-tags
            { begin: /<\?/, relevance: 0.1 },
            { begin: /\?>/ }
            // end php tag
          ]
        };
        const SUBST = {
          scope: "subst",
          variants: [
            { begin: /\$\w+/ },
            {
              begin: /\{\$/,
              end: /\}/
            }
          ]
        };
        const SINGLE_QUOTED = hljs.inherit(hljs.APOS_STRING_MODE, { illegal: null });
        const DOUBLE_QUOTED = hljs.inherit(hljs.QUOTE_STRING_MODE, {
          illegal: null,
          contains: hljs.QUOTE_STRING_MODE.contains.concat(SUBST)
        });
        const HEREDOC = {
          begin: /<<<[ \t]*(?:(\w+)|"(\w+)")\n/,
          end: /[ \t]*(\w+)\b/,
          contains: hljs.QUOTE_STRING_MODE.contains.concat(SUBST),
          "on:begin": (m, resp) => {
            resp.data._beginMatch = m[1] || m[2];
          },
          "on:end": (m, resp) => {
            if (resp.data._beginMatch !== m[1]) resp.ignoreMatch();
          }
        };
        const NOWDOC = hljs.END_SAME_AS_BEGIN({
          begin: /<<<[ \t]*'(\w+)'\n/,
          end: /[ \t]*(\w+)\b/
        });
        const WHITESPACE = "[ 	\n]";
        const STRING = {
          scope: "string",
          variants: [
            DOUBLE_QUOTED,
            SINGLE_QUOTED,
            HEREDOC,
            NOWDOC
          ]
        };
        const NUMBER = {
          scope: "number",
          variants: [
            { begin: `\\b0[bB][01]+(?:_[01]+)*\\b` },
            // Binary w/ underscore support
            { begin: `\\b0[oO][0-7]+(?:_[0-7]+)*\\b` },
            // Octals w/ underscore support
            { begin: `\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b` },
            // Hex w/ underscore support
            // Decimals w/ underscore support, with optional fragments and scientific exponent (e) suffix.
            { begin: `(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?` }
          ],
          relevance: 0
        };
        const LITERALS = [
          "false",
          "null",
          "true"
        ];
        const KWS = [
          // Magic constants:
          // <https://www.php.net/manual/en/language.constants.predefined.php>
          "__CLASS__",
          "__DIR__",
          "__FILE__",
          "__FUNCTION__",
          "__COMPILER_HALT_OFFSET__",
          "__LINE__",
          "__METHOD__",
          "__NAMESPACE__",
          "__TRAIT__",
          // Function that look like language construct or language construct that look like function:
          // List of keywords that may not require parenthesis
          "die",
          "echo",
          "exit",
          "include",
          "include_once",
          "print",
          "require",
          "require_once",
          // These are not language construct (function) but operate on the currently-executing function and can access the current symbol table
          // 'compact extract func_get_arg func_get_args func_num_args get_called_class get_parent_class ' +
          // Other keywords:
          // <https://www.php.net/manual/en/reserved.php>
          // <https://www.php.net/manual/en/language.types.type-juggling.php>
          "array",
          "abstract",
          "and",
          "as",
          "binary",
          "bool",
          "boolean",
          "break",
          "callable",
          "case",
          "catch",
          "class",
          "clone",
          "const",
          "continue",
          "declare",
          "default",
          "do",
          "double",
          "else",
          "elseif",
          "empty",
          "enddeclare",
          "endfor",
          "endforeach",
          "endif",
          "endswitch",
          "endwhile",
          "enum",
          "eval",
          "extends",
          "final",
          "finally",
          "float",
          "for",
          "foreach",
          "from",
          "global",
          "goto",
          "if",
          "implements",
          "instanceof",
          "insteadof",
          "int",
          "integer",
          "interface",
          "isset",
          "iterable",
          "list",
          "match|0",
          "mixed",
          "new",
          "never",
          "object",
          "or",
          "private",
          "protected",
          "public",
          "readonly",
          "real",
          "return",
          "string",
          "switch",
          "throw",
          "trait",
          "try",
          "unset",
          "use",
          "var",
          "void",
          "while",
          "xor",
          "yield"
        ];
        const BUILT_INS = [
          // Standard PHP library:
          // <https://www.php.net/manual/en/book.spl.php>
          "Error|0",
          "AppendIterator",
          "ArgumentCountError",
          "ArithmeticError",
          "ArrayIterator",
          "ArrayObject",
          "AssertionError",
          "BadFunctionCallException",
          "BadMethodCallException",
          "CachingIterator",
          "CallbackFilterIterator",
          "CompileError",
          "Countable",
          "DirectoryIterator",
          "DivisionByZeroError",
          "DomainException",
          "EmptyIterator",
          "ErrorException",
          "Exception",
          "FilesystemIterator",
          "FilterIterator",
          "GlobIterator",
          "InfiniteIterator",
          "InvalidArgumentException",
          "IteratorIterator",
          "LengthException",
          "LimitIterator",
          "LogicException",
          "MultipleIterator",
          "NoRewindIterator",
          "OutOfBoundsException",
          "OutOfRangeException",
          "OuterIterator",
          "OverflowException",
          "ParentIterator",
          "ParseError",
          "RangeException",
          "RecursiveArrayIterator",
          "RecursiveCachingIterator",
          "RecursiveCallbackFilterIterator",
          "RecursiveDirectoryIterator",
          "RecursiveFilterIterator",
          "RecursiveIterator",
          "RecursiveIteratorIterator",
          "RecursiveRegexIterator",
          "RecursiveTreeIterator",
          "RegexIterator",
          "RuntimeException",
          "SeekableIterator",
          "SplDoublyLinkedList",
          "SplFileInfo",
          "SplFileObject",
          "SplFixedArray",
          "SplHeap",
          "SplMaxHeap",
          "SplMinHeap",
          "SplObjectStorage",
          "SplObserver",
          "SplPriorityQueue",
          "SplQueue",
          "SplStack",
          "SplSubject",
          "SplTempFileObject",
          "TypeError",
          "UnderflowException",
          "UnexpectedValueException",
          "UnhandledMatchError",
          // Reserved interfaces:
          // <https://www.php.net/manual/en/reserved.interfaces.php>
          "ArrayAccess",
          "BackedEnum",
          "Closure",
          "Fiber",
          "Generator",
          "Iterator",
          "IteratorAggregate",
          "Serializable",
          "Stringable",
          "Throwable",
          "Traversable",
          "UnitEnum",
          "WeakReference",
          "WeakMap",
          // Reserved classes:
          // <https://www.php.net/manual/en/reserved.classes.php>
          "Directory",
          "__PHP_Incomplete_Class",
          "parent",
          "php_user_filter",
          "self",
          "static",
          "stdClass"
        ];
        const dualCase = (items) => {
          const result = [];
          items.forEach((item) => {
            result.push(item);
            if (item.toLowerCase() === item) {
              result.push(item.toUpperCase());
            } else {
              result.push(item.toLowerCase());
            }
          });
          return result;
        };
        const KEYWORDS = {
          keyword: KWS,
          literal: dualCase(LITERALS),
          built_in: BUILT_INS
        };
        const normalizeKeywords = (items) => {
          return items.map((item) => {
            return item.replace(/\|\d+$/, "");
          });
        };
        const CONSTRUCTOR_CALL = { variants: [
          {
            match: [
              /new/,
              regex.concat(WHITESPACE, "+"),
              // to prevent built ins from being confused as the class constructor call
              regex.concat("(?!", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"),
              PASCAL_CASE_CLASS_NAME_RE
            ],
            scope: {
              1: "keyword",
              4: "title.class"
            }
          }
        ] };
        const CONSTANT_REFERENCE = regex.concat(IDENT_RE, "\\b(?!\\()");
        const LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON = { variants: [
          {
            match: [
              regex.concat(
                /::/,
                regex.lookahead(/(?!class\b)/)
              ),
              CONSTANT_REFERENCE
            ],
            scope: { 2: "variable.constant" }
          },
          {
            match: [
              /::/,
              /class/
            ],
            scope: { 2: "variable.language" }
          },
          {
            match: [
              PASCAL_CASE_CLASS_NAME_RE,
              regex.concat(
                /::/,
                regex.lookahead(/(?!class\b)/)
              ),
              CONSTANT_REFERENCE
            ],
            scope: {
              1: "title.class",
              3: "variable.constant"
            }
          },
          {
            match: [
              PASCAL_CASE_CLASS_NAME_RE,
              regex.concat(
                "::",
                regex.lookahead(/(?!class\b)/)
              )
            ],
            scope: { 1: "title.class" }
          },
          {
            match: [
              PASCAL_CASE_CLASS_NAME_RE,
              /::/,
              /class/
            ],
            scope: {
              1: "title.class",
              3: "variable.language"
            }
          }
        ] };
        const NAMED_ARGUMENT = {
          scope: "attr",
          match: regex.concat(IDENT_RE, regex.lookahead(":"), regex.lookahead(/(?!::)/))
        };
        const PARAMS_MODE = {
          relevance: 0,
          begin: /\(/,
          end: /\)/,
          keywords: KEYWORDS,
          contains: [
            NAMED_ARGUMENT,
            VARIABLE,
            LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
            hljs.C_BLOCK_COMMENT_MODE,
            STRING,
            NUMBER,
            CONSTRUCTOR_CALL
          ]
        };
        const FUNCTION_INVOKE = {
          relevance: 0,
          match: [
            /\b/,
            // to prevent keywords from being confused as the function title
            regex.concat("(?!fn\\b|function\\b|", normalizeKeywords(KWS).join("\\b|"), "|", normalizeKeywords(BUILT_INS).join("\\b|"), "\\b)"),
            IDENT_RE,
            regex.concat(WHITESPACE, "*"),
            regex.lookahead(/(?=\()/)
          ],
          scope: { 3: "title.function.invoke" },
          contains: [PARAMS_MODE]
        };
        PARAMS_MODE.contains.push(FUNCTION_INVOKE);
        const ATTRIBUTE_CONTAINS = [
          NAMED_ARGUMENT,
          LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
          hljs.C_BLOCK_COMMENT_MODE,
          STRING,
          NUMBER,
          CONSTRUCTOR_CALL
        ];
        const ATTRIBUTES = {
          begin: regex.concat(
            /#\[\s*\\?/,
            regex.either(
              PASCAL_CASE_CLASS_NAME_RE,
              UPCASE_NAME_RE
            )
          ),
          beginScope: "meta",
          end: /]/,
          endScope: "meta",
          keywords: {
            literal: LITERALS,
            keyword: [
              "new",
              "array"
            ]
          },
          contains: [
            {
              begin: /\[/,
              end: /]/,
              keywords: {
                literal: LITERALS,
                keyword: [
                  "new",
                  "array"
                ]
              },
              contains: [
                "self",
                ...ATTRIBUTE_CONTAINS
              ]
            },
            ...ATTRIBUTE_CONTAINS,
            {
              scope: "meta",
              variants: [
                { match: PASCAL_CASE_CLASS_NAME_RE },
                { match: UPCASE_NAME_RE }
              ]
            }
          ]
        };
        return {
          case_insensitive: false,
          keywords: KEYWORDS,
          contains: [
            ATTRIBUTES,
            hljs.HASH_COMMENT_MODE,
            hljs.COMMENT("//", "$"),
            hljs.COMMENT(
              "/\\*",
              "\\*/",
              { contains: [
                {
                  scope: "doctag",
                  match: "@[A-Za-z]+"
                }
              ] }
            ),
            {
              match: /__halt_compiler\(\);/,
              keywords: "__halt_compiler",
              starts: {
                scope: "comment",
                end: hljs.MATCH_NOTHING_RE,
                contains: [
                  {
                    match: /\?>/,
                    scope: "meta",
                    endsParent: true
                  }
                ]
              }
            },
            PREPROCESSOR,
            {
              scope: "variable.language",
              match: /\$this\b/
            },
            VARIABLE,
            FUNCTION_INVOKE,
            LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
            {
              match: [
                /const/,
                /\s/,
                IDENT_RE
              ],
              scope: {
                1: "keyword",
                3: "variable.constant"
              }
            },
            CONSTRUCTOR_CALL,
            {
              scope: "function",
              relevance: 0,
              beginKeywords: "fn function",
              end: /[;{]/,
              excludeEnd: true,
              illegal: "[$%\\[]",
              contains: [
                { beginKeywords: "use" },
                hljs.UNDERSCORE_TITLE_MODE,
                {
                  begin: "=>",
                  // No markup, just a relevance booster
                  endsParent: true
                },
                {
                  scope: "params",
                  begin: "\\(",
                  end: "\\)",
                  excludeBegin: true,
                  excludeEnd: true,
                  keywords: KEYWORDS,
                  contains: [
                    "self",
                    ATTRIBUTES,
                    VARIABLE,
                    LEFT_AND_RIGHT_SIDE_OF_DOUBLE_COLON,
                    hljs.C_BLOCK_COMMENT_MODE,
                    STRING,
                    NUMBER
                  ]
                }
              ]
            },
            {
              scope: "class",
              variants: [
                {
                  beginKeywords: "enum",
                  illegal: /[($"]/
                },
                {
                  beginKeywords: "class interface trait",
                  illegal: /[:($"]/
                }
              ],
              relevance: 0,
              end: /\{/,
              excludeEnd: true,
              contains: [
                { beginKeywords: "extends implements" },
                hljs.UNDERSCORE_TITLE_MODE
              ]
            },
            // both use and namespace still use "old style" rules (vs multi-match)
            // because the namespace name can include `\` and we still want each
            // element to be treated as its own *individual* title
            {
              beginKeywords: "namespace",
              relevance: 0,
              end: ";",
              illegal: /[.']/,
              contains: [hljs.inherit(hljs.UNDERSCORE_TITLE_MODE, { scope: "title.class" })]
            },
            {
              beginKeywords: "use",
              relevance: 0,
              end: ";",
              contains: [
                // TODO: title.function vs title.class
                {
                  match: /\b(as|const|function)\b/,
                  scope: "keyword"
                },
                // TODO: could be title.class or title.function
                hljs.UNDERSCORE_TITLE_MODE
              ]
            },
            STRING,
            NUMBER
          ]
        };
      }
      module.exports = php;
    }
  });

  // node_modules/highlight.js/lib/languages/php-template.js
  var require_php_template = __commonJS({
    "node_modules/highlight.js/lib/languages/php-template.js"(exports, module) {
      function phpTemplate(hljs) {
        return {
          name: "PHP template",
          subLanguage: "xml",
          contains: [
            {
              begin: /<\?(php|=)?/,
              end: /\?>/,
              subLanguage: "php",
              contains: [
                // We don't want the php closing tag ?> to close the PHP block when
                // inside any of the following blocks:
                {
                  begin: "/\\*",
                  end: "\\*/",
                  skip: true
                },
                {
                  begin: 'b"',
                  end: '"',
                  skip: true
                },
                {
                  begin: "b'",
                  end: "'",
                  skip: true
                },
                hljs.inherit(hljs.APOS_STRING_MODE, {
                  illegal: null,
                  className: null,
                  contains: null,
                  skip: true
                }),
                hljs.inherit(hljs.QUOTE_STRING_MODE, {
                  illegal: null,
                  className: null,
                  contains: null,
                  skip: true
                })
              ]
            }
          ]
        };
      }
      module.exports = phpTemplate;
    }
  });

  // node_modules/highlight.js/lib/languages/plaintext.js
  var require_plaintext = __commonJS({
    "node_modules/highlight.js/lib/languages/plaintext.js"(exports, module) {
      function plaintext(hljs) {
        return {
          name: "Plain text",
          aliases: [
            "text",
            "txt"
          ],
          disableAutodetect: true
        };
      }
      module.exports = plaintext;
    }
  });

  // node_modules/highlight.js/lib/languages/python.js
  var require_python = __commonJS({
    "node_modules/highlight.js/lib/languages/python.js"(exports, module) {
      function python(hljs) {
        const regex = hljs.regex;
        const IDENT_RE = /[\p{XID_Start}_]\p{XID_Continue}*/u;
        const RESERVED_WORDS = [
          "and",
          "as",
          "assert",
          "async",
          "await",
          "break",
          "case",
          "class",
          "continue",
          "def",
          "del",
          "elif",
          "else",
          "except",
          "finally",
          "for",
          "from",
          "global",
          "if",
          "import",
          "in",
          "is",
          "lambda",
          "match",
          "nonlocal|10",
          "not",
          "or",
          "pass",
          "raise",
          "return",
          "try",
          "while",
          "with",
          "yield"
        ];
        const BUILT_INS = [
          "__import__",
          "abs",
          "all",
          "any",
          "ascii",
          "bin",
          "bool",
          "breakpoint",
          "bytearray",
          "bytes",
          "callable",
          "chr",
          "classmethod",
          "compile",
          "complex",
          "delattr",
          "dict",
          "dir",
          "divmod",
          "enumerate",
          "eval",
          "exec",
          "filter",
          "float",
          "format",
          "frozenset",
          "getattr",
          "globals",
          "hasattr",
          "hash",
          "help",
          "hex",
          "id",
          "input",
          "int",
          "isinstance",
          "issubclass",
          "iter",
          "len",
          "list",
          "locals",
          "map",
          "max",
          "memoryview",
          "min",
          "next",
          "object",
          "oct",
          "open",
          "ord",
          "pow",
          "print",
          "property",
          "range",
          "repr",
          "reversed",
          "round",
          "set",
          "setattr",
          "slice",
          "sorted",
          "staticmethod",
          "str",
          "sum",
          "super",
          "tuple",
          "type",
          "vars",
          "zip"
        ];
        const LITERALS = [
          "__debug__",
          "Ellipsis",
          "False",
          "None",
          "NotImplemented",
          "True"
        ];
        const TYPES = [
          "Any",
          "Callable",
          "Coroutine",
          "Dict",
          "List",
          "Literal",
          "Generic",
          "Optional",
          "Sequence",
          "Set",
          "Tuple",
          "Type",
          "Union"
        ];
        const KEYWORDS = {
          $pattern: /[A-Za-z]\w+|__\w+__/,
          keyword: RESERVED_WORDS,
          built_in: BUILT_INS,
          literal: LITERALS,
          type: TYPES
        };
        const PROMPT = {
          className: "meta",
          begin: /^(>>>|\.\.\.) /
        };
        const SUBST = {
          className: "subst",
          begin: /\{/,
          end: /\}/,
          keywords: KEYWORDS,
          illegal: /#/
        };
        const LITERAL_BRACKET = {
          begin: /\{\{/,
          relevance: 0
        };
        const STRING = {
          className: "string",
          contains: [hljs.BACKSLASH_ESCAPE],
          variants: [
            {
              begin: /([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,
              end: /'''/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                PROMPT
              ],
              relevance: 10
            },
            {
              begin: /([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,
              end: /"""/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                PROMPT
              ],
              relevance: 10
            },
            {
              begin: /([fF][rR]|[rR][fF]|[fF])'''/,
              end: /'''/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                PROMPT,
                LITERAL_BRACKET,
                SUBST
              ]
            },
            {
              begin: /([fF][rR]|[rR][fF]|[fF])"""/,
              end: /"""/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                PROMPT,
                LITERAL_BRACKET,
                SUBST
              ]
            },
            {
              begin: /([uU]|[rR])'/,
              end: /'/,
              relevance: 10
            },
            {
              begin: /([uU]|[rR])"/,
              end: /"/,
              relevance: 10
            },
            {
              begin: /([bB]|[bB][rR]|[rR][bB])'/,
              end: /'/
            },
            {
              begin: /([bB]|[bB][rR]|[rR][bB])"/,
              end: /"/
            },
            {
              begin: /([fF][rR]|[rR][fF]|[fF])'/,
              end: /'/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                LITERAL_BRACKET,
                SUBST
              ]
            },
            {
              begin: /([fF][rR]|[rR][fF]|[fF])"/,
              end: /"/,
              contains: [
                hljs.BACKSLASH_ESCAPE,
                LITERAL_BRACKET,
                SUBST
              ]
            },
            hljs.APOS_STRING_MODE,
            hljs.QUOTE_STRING_MODE
          ]
        };
        const digitpart = "[0-9](_?[0-9])*";
        const pointfloat = `(\\b(${digitpart}))?\\.(${digitpart})|\\b(${digitpart})\\.`;
        const lookahead = `\\b|${RESERVED_WORDS.join("|")}`;
        const NUMBER = {
          className: "number",
          relevance: 0,
          variants: [
            // exponentfloat, pointfloat
            // https://docs.python.org/3.9/reference/lexical_analysis.html#floating-point-literals
            // optionally imaginary
            // https://docs.python.org/3.9/reference/lexical_analysis.html#imaginary-literals
            // Note: no leading \b because floats can start with a decimal point
            // and we don't want to mishandle e.g. `fn(.5)`,
            // no trailing \b for pointfloat because it can end with a decimal point
            // and we don't want to mishandle e.g. `0..hex()`; this should be safe
            // because both MUST contain a decimal point and so cannot be confused with
            // the interior part of an identifier
            {
              begin: `(\\b(${digitpart})|(${pointfloat}))[eE][+-]?(${digitpart})[jJ]?(?=${lookahead})`
            },
            {
              begin: `(${pointfloat})[jJ]?`
            },
            // decinteger, bininteger, octinteger, hexinteger
            // https://docs.python.org/3.9/reference/lexical_analysis.html#integer-literals
            // optionally "long" in Python 2
            // https://docs.python.org/2.7/reference/lexical_analysis.html#integer-and-long-integer-literals
            // decinteger is optionally imaginary
            // https://docs.python.org/3.9/reference/lexical_analysis.html#imaginary-literals
            {
              begin: `\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${lookahead})`
            },
            {
              begin: `\\b0[bB](_?[01])+[lL]?(?=${lookahead})`
            },
            {
              begin: `\\b0[oO](_?[0-7])+[lL]?(?=${lookahead})`
            },
            {
              begin: `\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${lookahead})`
            },
            // imagnumber (digitpart-based)
            // https://docs.python.org/3.9/reference/lexical_analysis.html#imaginary-literals
            {
              begin: `\\b(${digitpart})[jJ](?=${lookahead})`
            }
          ]
        };
        const COMMENT_TYPE = {
          className: "comment",
          begin: regex.lookahead(/# type:/),
          end: /$/,
          keywords: KEYWORDS,
          contains: [
            {
              // prevent keywords from coloring `type`
              begin: /# type:/
            },
            // comment within a datatype comment includes no keywords
            {
              begin: /#/,
              end: /\b\B/,
              endsWithParent: true
            }
          ]
        };
        const PARAMS = {
          className: "params",
          variants: [
            // Exclude params in functions without params
            {
              className: "",
              begin: /\(\s*\)/,
              skip: true
            },
            {
              begin: /\(/,
              end: /\)/,
              excludeBegin: true,
              excludeEnd: true,
              keywords: KEYWORDS,
              contains: [
                "self",
                PROMPT,
                NUMBER,
                STRING,
                hljs.HASH_COMMENT_MODE
              ]
            }
          ]
        };
        SUBST.contains = [
          STRING,
          NUMBER,
          PROMPT
        ];
        return {
          name: "Python",
          aliases: [
            "py",
            "gyp",
            "ipython"
          ],
          unicodeRegex: true,
          keywords: KEYWORDS,
          illegal: /(<\/|\?)|=>/,
          contains: [
            PROMPT,
            NUMBER,
            {
              // very common convention
              scope: "variable.language",
              match: /\bself\b/
            },
            {
              // eat "if" prior to string so that it won't accidentally be
              // labeled as an f-string
              beginKeywords: "if",
              relevance: 0
            },
            { match: /\bor\b/, scope: "keyword" },
            STRING,
            COMMENT_TYPE,
            hljs.HASH_COMMENT_MODE,
            {
              match: [
                /\bdef/,
                /\s+/,
                IDENT_RE
              ],
              scope: {
                1: "keyword",
                3: "title.function"
              },
              contains: [PARAMS]
            },
            {
              variants: [
                {
                  match: [
                    /\bclass/,
                    /\s+/,
                    IDENT_RE,
                    /\s*/,
                    /\(\s*/,
                    IDENT_RE,
                    /\s*\)/
                  ]
                },
                {
                  match: [
                    /\bclass/,
                    /\s+/,
                    IDENT_RE
                  ]
                }
              ],
              scope: {
                1: "keyword",
                3: "title.class",
                6: "title.class.inherited"
              }
            },
            {
              className: "meta",
              begin: /^[\t ]*@/,
              end: /(?=#)|$/,
              contains: [
                NUMBER,
                PARAMS,
                STRING
              ]
            }
          ]
        };
      }
      module.exports = python;
    }
  });

  // node_modules/highlight.js/lib/languages/python-repl.js
  var require_python_repl = __commonJS({
    "node_modules/highlight.js/lib/languages/python-repl.js"(exports, module) {
      function pythonRepl(hljs) {
        return {
          aliases: ["pycon"],
          contains: [
            {
              className: "meta.prompt",
              starts: {
                // a space separates the REPL prefix from the actual code
                // this is purely for cleaner HTML output
                end: / |$/,
                starts: {
                  end: "$",
                  subLanguage: "python"
                }
              },
              variants: [
                { begin: /^>>>(?=[ ]|$)/ },
                { begin: /^\.\.\.(?=[ ]|$)/ }
              ]
            }
          ]
        };
      }
      module.exports = pythonRepl;
    }
  });

  // node_modules/highlight.js/lib/languages/r.js
  var require_r = __commonJS({
    "node_modules/highlight.js/lib/languages/r.js"(exports, module) {
      function r(hljs) {
        const regex = hljs.regex;
        const IDENT_RE = /(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/;
        const NUMBER_TYPES_RE = regex.either(
          // Special case: only hexadecimal binary powers can contain fractions
          /0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,
          // Hexadecimal numbers without fraction and optional binary power
          /0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,
          // Decimal numbers
          /(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/
        );
        const OPERATORS_RE = /[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/;
        const PUNCTUATION_RE = regex.either(
          /[()]/,
          /[{}]/,
          /\[\[/,
          /[[\]]/,
          /\\/,
          /,/
        );
        return {
          name: "R",
          keywords: {
            $pattern: IDENT_RE,
            keyword: "function if in break next repeat else for while",
            literal: "NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10",
            built_in: (
              // Builtin constants
              "LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm"
            )
          },
          contains: [
            // Roxygen comments
            hljs.COMMENT(
              /#'/,
              /$/,
              { contains: [
                {
                  // Handle `@examples` separately to cause all subsequent code
                  // until the next `@`-tag on its own line to be kept as-is,
                  // preventing highlighting. This code is example R code, so nested
                  // doctags shouldn’t be treated as such. See
                  // `test/markup/r/roxygen.txt` for an example.
                  scope: "doctag",
                  match: /@examples/,
                  starts: {
                    end: regex.lookahead(regex.either(
                      // end if another doc comment
                      /\n^#'\s*(?=@[a-zA-Z]+)/,
                      // or a line with no comment
                      /\n^(?!#')/
                    )),
                    endsParent: true
                  }
                },
                {
                  // Handle `@param` to highlight the parameter name following
                  // after.
                  scope: "doctag",
                  begin: "@param",
                  end: /$/,
                  contains: [
                    {
                      scope: "variable",
                      variants: [
                        { match: IDENT_RE },
                        { match: /`(?:\\.|[^`\\])+`/ }
                      ],
                      endsParent: true
                    }
                  ]
                },
                {
                  scope: "doctag",
                  match: /@[a-zA-Z]+/
                },
                {
                  scope: "keyword",
                  match: /\\[a-zA-Z]+/
                }
              ] }
            ),
            hljs.HASH_COMMENT_MODE,
            {
              scope: "string",
              contains: [hljs.BACKSLASH_ESCAPE],
              variants: [
                hljs.END_SAME_AS_BEGIN({
                  begin: /[rR]"(-*)\(/,
                  end: /\)(-*)"/
                }),
                hljs.END_SAME_AS_BEGIN({
                  begin: /[rR]"(-*)\{/,
                  end: /\}(-*)"/
                }),
                hljs.END_SAME_AS_BEGIN({
                  begin: /[rR]"(-*)\[/,
                  end: /\](-*)"/
                }),
                hljs.END_SAME_AS_BEGIN({
                  begin: /[rR]'(-*)\(/,
                  end: /\)(-*)'/
                }),
                hljs.END_SAME_AS_BEGIN({
                  begin: /[rR]'(-*)\{/,
                  end: /\}(-*)'/
                }),
                hljs.END_SAME_AS_BEGIN({
                  begin: /[rR]'(-*)\[/,
                  end: /\](-*)'/
                }),
                {
                  begin: '"',
                  end: '"',
                  relevance: 0
                },
                {
                  begin: "'",
                  end: "'",
                  relevance: 0
                }
              ]
            },
            // Matching numbers immediately following punctuation and operators is
            // tricky since we need to look at the character ahead of a number to
            // ensure the number is not part of an identifier, and we cannot use
            // negative look-behind assertions. So instead we explicitly handle all
            // possible combinations of (operator|punctuation), number.
            // TODO: replace with negative look-behind when available
            // { begin: /(?<![a-zA-Z0-9._])0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/ },
            // { begin: /(?<![a-zA-Z0-9._])0[xX][0-9a-fA-F]+([pP][+-]?\d+)?[Li]?/ },
            // { begin: /(?<![a-zA-Z0-9._])(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[Li]?/ }
            {
              relevance: 0,
              variants: [
                {
                  scope: {
                    1: "operator",
                    2: "number"
                  },
                  match: [
                    OPERATORS_RE,
                    NUMBER_TYPES_RE
                  ]
                },
                {
                  scope: {
                    1: "operator",
                    2: "number"
                  },
                  match: [
                    /%[^%]*%/,
                    NUMBER_TYPES_RE
                  ]
                },
                {
                  scope: {
                    1: "punctuation",
                    2: "number"
                  },
                  match: [
                    PUNCTUATION_RE,
                    NUMBER_TYPES_RE
                  ]
                },
                {
                  scope: { 2: "number" },
                  match: [
                    /[^a-zA-Z0-9._]|^/,
                    // not part of an identifier, or start of document
                    NUMBER_TYPES_RE
                  ]
                }
              ]
            },
            // Operators/punctuation when they're not directly followed by numbers
            {
              // Relevance boost for the most common assignment form.
              scope: { 3: "operator" },
              match: [
                IDENT_RE,
                /\s+/,
                /<-/,
                /\s+/
              ]
            },
            {
              scope: "operator",
              relevance: 0,
              variants: [
                { match: OPERATORS_RE },
                { match: /%[^%]*%/ }
              ]
            },
            {
              scope: "punctuation",
              relevance: 0,
              match: PUNCTUATION_RE
            },
            {
              // Escaped identifier
              begin: "`",
              end: "`",
              contains: [{ begin: /\\./ }]
            }
          ]
        };
      }
      module.exports = r;
    }
  });

  // node_modules/highlight.js/lib/languages/rust.js
  var require_rust = __commonJS({
    "node_modules/highlight.js/lib/languages/rust.js"(exports, module) {
      function rust(hljs) {
        const regex = hljs.regex;
        const RAW_IDENTIFIER = /(r#)?/;
        const UNDERSCORE_IDENT_RE = regex.concat(RAW_IDENTIFIER, hljs.UNDERSCORE_IDENT_RE);
        const IDENT_RE = regex.concat(RAW_IDENTIFIER, hljs.IDENT_RE);
        const FUNCTION_INVOKE = {
          className: "title.function.invoke",
          relevance: 0,
          begin: regex.concat(
            /\b/,
            /(?!let|for|while|if|else|match\b)/,
            IDENT_RE,
            regex.lookahead(/\s*\(/)
          )
        };
        const NUMBER_SUFFIX = "([ui](8|16|32|64|128|size)|f(32|64))?";
        const KEYWORDS = [
          "abstract",
          "as",
          "async",
          "await",
          "become",
          "box",
          "break",
          "const",
          "continue",
          "crate",
          "do",
          "dyn",
          "else",
          "enum",
          "extern",
          "false",
          "final",
          "fn",
          "for",
          "if",
          "impl",
          "in",
          "let",
          "loop",
          "macro",
          "match",
          "mod",
          "move",
          "mut",
          "override",
          "priv",
          "pub",
          "ref",
          "return",
          "self",
          "Self",
          "static",
          "struct",
          "super",
          "trait",
          "true",
          "try",
          "type",
          "typeof",
          "union",
          "unsafe",
          "unsized",
          "use",
          "virtual",
          "where",
          "while",
          "yield"
        ];
        const LITERALS = [
          "true",
          "false",
          "Some",
          "None",
          "Ok",
          "Err"
        ];
        const BUILTINS = [
          // functions
          "drop ",
          // traits
          "Copy",
          "Send",
          "Sized",
          "Sync",
          "Drop",
          "Fn",
          "FnMut",
          "FnOnce",
          "ToOwned",
          "Clone",
          "Debug",
          "PartialEq",
          "PartialOrd",
          "Eq",
          "Ord",
          "AsRef",
          "AsMut",
          "Into",
          "From",
          "Default",
          "Iterator",
          "Extend",
          "IntoIterator",
          "DoubleEndedIterator",
          "ExactSizeIterator",
          "SliceConcatExt",
          "ToString",
          // macros
          "assert!",
          "assert_eq!",
          "bitflags!",
          "bytes!",
          "cfg!",
          "col!",
          "concat!",
          "concat_idents!",
          "debug_assert!",
          "debug_assert_eq!",
          "env!",
          "eprintln!",
          "panic!",
          "file!",
          "format!",
          "format_args!",
          "include_bytes!",
          "include_str!",
          "line!",
          "local_data_key!",
          "module_path!",
          "option_env!",
          "print!",
          "println!",
          "select!",
          "stringify!",
          "try!",
          "unimplemented!",
          "unreachable!",
          "vec!",
          "write!",
          "writeln!",
          "macro_rules!",
          "assert_ne!",
          "debug_assert_ne!"
        ];
        const TYPES = [
          "i8",
          "i16",
          "i32",
          "i64",
          "i128",
          "isize",
          "u8",
          "u16",
          "u32",
          "u64",
          "u128",
          "usize",
          "f32",
          "f64",
          "str",
          "char",
          "bool",
          "Box",
          "Option",
          "Result",
          "String",
          "Vec"
        ];
        return {
          name: "Rust",
          aliases: ["rs"],
          keywords: {
            $pattern: hljs.IDENT_RE + "!?",
            type: TYPES,
            keyword: KEYWORDS,
            literal: LITERALS,
            built_in: BUILTINS
          },
          illegal: "</",
          contains: [
            hljs.C_LINE_COMMENT_MODE,
            hljs.COMMENT("/\\*", "\\*/", { contains: ["self"] }),
            hljs.inherit(hljs.QUOTE_STRING_MODE, {
              begin: /b?"/,
              illegal: null
            }),
            {
              className: "symbol",
              // negative lookahead to avoid matching `'`
              begin: /'[a-zA-Z_][a-zA-Z0-9_]*(?!')/
            },
            {
              scope: "string",
              variants: [
                { begin: /b?r(#*)"(.|\n)*?"\1(?!#)/ },
                {
                  begin: /b?'/,
                  end: /'/,
                  contains: [
                    {
                      scope: "char.escape",
                      match: /\\('|\w|x\w{2}|u\w{4}|U\w{8})/
                    }
                  ]
                }
              ]
            },
            {
              className: "number",
              variants: [
                { begin: "\\b0b([01_]+)" + NUMBER_SUFFIX },
                { begin: "\\b0o([0-7_]+)" + NUMBER_SUFFIX },
                { begin: "\\b0x([A-Fa-f0-9_]+)" + NUMBER_SUFFIX },
                { begin: "\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)" + NUMBER_SUFFIX }
              ],
              relevance: 0
            },
            {
              begin: [
                /fn/,
                /\s+/,
                UNDERSCORE_IDENT_RE
              ],
              className: {
                1: "keyword",
                3: "title.function"
              }
            },
            {
              className: "meta",
              begin: "#!?\\[",
              end: "\\]",
              contains: [
                {
                  className: "string",
                  begin: /"/,
                  end: /"/,
                  contains: [
                    hljs.BACKSLASH_ESCAPE
                  ]
                }
              ]
            },
            {
              begin: [
                /let/,
                /\s+/,
                /(?:mut\s+)?/,
                UNDERSCORE_IDENT_RE
              ],
              className: {
                1: "keyword",
                3: "keyword",
                4: "variable"
              }
            },
            // must come before impl/for rule later
            {
              begin: [
                /for/,
                /\s+/,
                UNDERSCORE_IDENT_RE,
                /\s+/,
                /in/
              ],
              className: {
                1: "keyword",
                3: "variable",
                5: "keyword"
              }
            },
            {
              begin: [
                /type/,
                /\s+/,
                UNDERSCORE_IDENT_RE
              ],
              className: {
                1: "keyword",
                3: "title.class"
              }
            },
            {
              begin: [
                /(?:trait|enum|struct|union|impl|for)/,
                /\s+/,
                UNDERSCORE_IDENT_RE
              ],
              className: {
                1: "keyword",
                3: "title.class"
              }
            },
            {
              begin: hljs.IDENT_RE + "::",
              keywords: {
                keyword: "Self",
                built_in: BUILTINS,
                type: TYPES
              }
            },
            {
              className: "punctuation",
              begin: "->"
            },
            FUNCTION_INVOKE
          ]
        };
      }
      module.exports = rust;
    }
  });

  // node_modules/highlight.js/lib/languages/scss.js
  var require_scss = __commonJS({
    "node_modules/highlight.js/lib/languages/scss.js"(exports, module) {
      var MODES = (hljs) => {
        return {
          IMPORTANT: {
            scope: "meta",
            begin: "!important"
          },
          BLOCK_COMMENT: hljs.C_BLOCK_COMMENT_MODE,
          HEXCOLOR: {
            scope: "number",
            begin: /#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/
          },
          FUNCTION_DISPATCH: {
            className: "built_in",
            begin: /[\w-]+(?=\()/
          },
          ATTRIBUTE_SELECTOR_MODE: {
            scope: "selector-attr",
            begin: /\[/,
            end: /\]/,
            illegal: "$",
            contains: [
              hljs.APOS_STRING_MODE,
              hljs.QUOTE_STRING_MODE
            ]
          },
          CSS_NUMBER_MODE: {
            scope: "number",
            begin: hljs.NUMBER_RE + "(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
            relevance: 0
          },
          CSS_VARIABLE: {
            className: "attr",
            begin: /--[A-Za-z_][A-Za-z0-9_-]*/
          }
        };
      };
      var HTML_TAGS = [
        "a",
        "abbr",
        "address",
        "article",
        "aside",
        "audio",
        "b",
        "blockquote",
        "body",
        "button",
        "canvas",
        "caption",
        "cite",
        "code",
        "dd",
        "del",
        "details",
        "dfn",
        "div",
        "dl",
        "dt",
        "em",
        "fieldset",
        "figcaption",
        "figure",
        "footer",
        "form",
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "h6",
        "header",
        "hgroup",
        "html",
        "i",
        "iframe",
        "img",
        "input",
        "ins",
        "kbd",
        "label",
        "legend",
        "li",
        "main",
        "mark",
        "menu",
        "nav",
        "object",
        "ol",
        "optgroup",
        "option",
        "p",
        "picture",
        "q",
        "quote",
        "samp",
        "section",
        "select",
        "source",
        "span",
        "strong",
        "summary",
        "sup",
        "table",
        "tbody",
        "td",
        "textarea",
        "tfoot",
        "th",
        "thead",
        "time",
        "tr",
        "ul",
        "var",
        "video"
      ];
      var SVG_TAGS = [
        "defs",
        "g",
        "marker",
        "mask",
        "pattern",
        "svg",
        "switch",
        "symbol",
        "feBlend",
        "feColorMatrix",
        "feComponentTransfer",
        "feComposite",
        "feConvolveMatrix",
        "feDiffuseLighting",
        "feDisplacementMap",
        "feFlood",
        "feGaussianBlur",
        "feImage",
        "feMerge",
        "feMorphology",
        "feOffset",
        "feSpecularLighting",
        "feTile",
        "feTurbulence",
        "linearGradient",
        "radialGradient",
        "stop",
        "circle",
        "ellipse",
        "image",
        "line",
        "path",
        "polygon",
        "polyline",
        "rect",
        "text",
        "use",
        "textPath",
        "tspan",
        "foreignObject",
        "clipPath"
      ];
      var TAGS = [
        ...HTML_TAGS,
        ...SVG_TAGS
      ];
      var MEDIA_FEATURES = [
        "any-hover",
        "any-pointer",
        "aspect-ratio",
        "color",
        "color-gamut",
        "color-index",
        "device-aspect-ratio",
        "device-height",
        "device-width",
        "display-mode",
        "forced-colors",
        "grid",
        "height",
        "hover",
        "inverted-colors",
        "monochrome",
        "orientation",
        "overflow-block",
        "overflow-inline",
        "pointer",
        "prefers-color-scheme",
        "prefers-contrast",
        "prefers-reduced-motion",
        "prefers-reduced-transparency",
        "resolution",
        "scan",
        "scripting",
        "update",
        "width",
        // TODO: find a better solution?
        "min-width",
        "max-width",
        "min-height",
        "max-height"
      ].sort().reverse();
      var PSEUDO_CLASSES = [
        "active",
        "any-link",
        "blank",
        "checked",
        "current",
        "default",
        "defined",
        "dir",
        // dir()
        "disabled",
        "drop",
        "empty",
        "enabled",
        "first",
        "first-child",
        "first-of-type",
        "fullscreen",
        "future",
        "focus",
        "focus-visible",
        "focus-within",
        "has",
        // has()
        "host",
        // host or host()
        "host-context",
        // host-context()
        "hover",
        "indeterminate",
        "in-range",
        "invalid",
        "is",
        // is()
        "lang",
        // lang()
        "last-child",
        "last-of-type",
        "left",
        "link",
        "local-link",
        "not",
        // not()
        "nth-child",
        // nth-child()
        "nth-col",
        // nth-col()
        "nth-last-child",
        // nth-last-child()
        "nth-last-col",
        // nth-last-col()
        "nth-last-of-type",
        //nth-last-of-type()
        "nth-of-type",
        //nth-of-type()
        "only-child",
        "only-of-type",
        "optional",
        "out-of-range",
        "past",
        "placeholder-shown",
        "read-only",
        "read-write",
        "required",
        "right",
        "root",
        "scope",
        "target",
        "target-within",
        "user-invalid",
        "valid",
        "visited",
        "where"
        // where()
      ].sort().reverse();
      var PSEUDO_ELEMENTS = [
        "after",
        "backdrop",
        "before",
        "cue",
        "cue-region",
        "first-letter",
        "first-line",
        "grammar-error",
        "marker",
        "part",
        "placeholder",
        "selection",
        "slotted",
        "spelling-error"
      ].sort().reverse();
      var ATTRIBUTES = [
        "accent-color",
        "align-content",
        "align-items",
        "align-self",
        "alignment-baseline",
        "all",
        "anchor-name",
        "animation",
        "animation-composition",
        "animation-delay",
        "animation-direction",
        "animation-duration",
        "animation-fill-mode",
        "animation-iteration-count",
        "animation-name",
        "animation-play-state",
        "animation-range",
        "animation-range-end",
        "animation-range-start",
        "animation-timeline",
        "animation-timing-function",
        "appearance",
        "aspect-ratio",
        "backdrop-filter",
        "backface-visibility",
        "background",
        "background-attachment",
        "background-blend-mode",
        "background-clip",
        "background-color",
        "background-image",
        "background-origin",
        "background-position",
        "background-position-x",
        "background-position-y",
        "background-repeat",
        "background-size",
        "baseline-shift",
        "block-size",
        "border",
        "border-block",
        "border-block-color",
        "border-block-end",
        "border-block-end-color",
        "border-block-end-style",
        "border-block-end-width",
        "border-block-start",
        "border-block-start-color",
        "border-block-start-style",
        "border-block-start-width",
        "border-block-style",
        "border-block-width",
        "border-bottom",
        "border-bottom-color",
        "border-bottom-left-radius",
        "border-bottom-right-radius",
        "border-bottom-style",
        "border-bottom-width",
        "border-collapse",
        "border-color",
        "border-end-end-radius",
        "border-end-start-radius",
        "border-image",
        "border-image-outset",
        "border-image-repeat",
        "border-image-slice",
        "border-image-source",
        "border-image-width",
        "border-inline",
        "border-inline-color",
        "border-inline-end",
        "border-inline-end-color",
        "border-inline-end-style",
        "border-inline-end-width",
        "border-inline-start",
        "border-inline-start-color",
        "border-inline-start-style",
        "border-inline-start-width",
        "border-inline-style",
        "border-inline-width",
        "border-left",
        "border-left-color",
        "border-left-style",
        "border-left-width",
        "border-radius",
        "border-right",
        "border-right-color",
        "border-right-style",
        "border-right-width",
        "border-spacing",
        "border-start-end-radius",
        "border-start-start-radius",
        "border-style",
        "border-top",
        "border-top-color",
        "border-top-left-radius",
        "border-top-right-radius",
        "border-top-style",
        "border-top-width",
        "border-width",
        "bottom",
        "box-align",
        "box-decoration-break",
        "box-direction",
        "box-flex",
        "box-flex-group",
        "box-lines",
        "box-ordinal-group",
        "box-orient",
        "box-pack",
        "box-shadow",
        "box-sizing",
        "break-after",
        "break-before",
        "break-inside",
        "caption-side",
        "caret-color",
        "clear",
        "clip",
        "clip-path",
        "clip-rule",
        "color",
        "color-interpolation",
        "color-interpolation-filters",
        "color-profile",
        "color-rendering",
        "color-scheme",
        "column-count",
        "column-fill",
        "column-gap",
        "column-rule",
        "column-rule-color",
        "column-rule-style",
        "column-rule-width",
        "column-span",
        "column-width",
        "columns",
        "contain",
        "contain-intrinsic-block-size",
        "contain-intrinsic-height",
        "contain-intrinsic-inline-size",
        "contain-intrinsic-size",
        "contain-intrinsic-width",
        "container",
        "container-name",
        "container-type",
        "content",
        "content-visibility",
        "counter-increment",
        "counter-reset",
        "counter-set",
        "cue",
        "cue-after",
        "cue-before",
        "cursor",
        "cx",
        "cy",
        "direction",
        "display",
        "dominant-baseline",
        "empty-cells",
        "enable-background",
        "field-sizing",
        "fill",
        "fill-opacity",
        "fill-rule",
        "filter",
        "flex",
        "flex-basis",
        "flex-direction",
        "flex-flow",
        "flex-grow",
        "flex-shrink",
        "flex-wrap",
        "float",
        "flood-color",
        "flood-opacity",
        "flow",
        "font",
        "font-display",
        "font-family",
        "font-feature-settings",
        "font-kerning",
        "font-language-override",
        "font-optical-sizing",
        "font-palette",
        "font-size",
        "font-size-adjust",
        "font-smooth",
        "font-smoothing",
        "font-stretch",
        "font-style",
        "font-synthesis",
        "font-synthesis-position",
        "font-synthesis-small-caps",
        "font-synthesis-style",
        "font-synthesis-weight",
        "font-variant",
        "font-variant-alternates",
        "font-variant-caps",
        "font-variant-east-asian",
        "font-variant-emoji",
        "font-variant-ligatures",
        "font-variant-numeric",
        "font-variant-position",
        "font-variation-settings",
        "font-weight",
        "forced-color-adjust",
        "gap",
        "glyph-orientation-horizontal",
        "glyph-orientation-vertical",
        "grid",
        "grid-area",
        "grid-auto-columns",
        "grid-auto-flow",
        "grid-auto-rows",
        "grid-column",
        "grid-column-end",
        "grid-column-start",
        "grid-gap",
        "grid-row",
        "grid-row-end",
        "grid-row-start",
        "grid-template",
        "grid-template-areas",
        "grid-template-columns",
        "grid-template-rows",
        "hanging-punctuation",
        "height",
        "hyphenate-character",
        "hyphenate-limit-chars",
        "hyphens",
        "icon",
        "image-orientation",
        "image-rendering",
        "image-resolution",
        "ime-mode",
        "initial-letter",
        "initial-letter-align",
        "inline-size",
        "inset",
        "inset-area",
        "inset-block",
        "inset-block-end",
        "inset-block-start",
        "inset-inline",
        "inset-inline-end",
        "inset-inline-start",
        "isolation",
        "justify-content",
        "justify-items",
        "justify-self",
        "kerning",
        "left",
        "letter-spacing",
        "lighting-color",
        "line-break",
        "line-height",
        "line-height-step",
        "list-style",
        "list-style-image",
        "list-style-position",
        "list-style-type",
        "margin",
        "margin-block",
        "margin-block-end",
        "margin-block-start",
        "margin-bottom",
        "margin-inline",
        "margin-inline-end",
        "margin-inline-start",
        "margin-left",
        "margin-right",
        "margin-top",
        "margin-trim",
        "marker",
        "marker-end",
        "marker-mid",
        "marker-start",
        "marks",
        "mask",
        "mask-border",
        "mask-border-mode",
        "mask-border-outset",
        "mask-border-repeat",
        "mask-border-slice",
        "mask-border-source",
        "mask-border-width",
        "mask-clip",
        "mask-composite",
        "mask-image",
        "mask-mode",
        "mask-origin",
        "mask-position",
        "mask-repeat",
        "mask-size",
        "mask-type",
        "masonry-auto-flow",
        "math-depth",
        "math-shift",
        "math-style",
        "max-block-size",
        "max-height",
        "max-inline-size",
        "max-width",
        "min-block-size",
        "min-height",
        "min-inline-size",
        "min-width",
        "mix-blend-mode",
        "nav-down",
        "nav-index",
        "nav-left",
        "nav-right",
        "nav-up",
        "none",
        "normal",
        "object-fit",
        "object-position",
        "offset",
        "offset-anchor",
        "offset-distance",
        "offset-path",
        "offset-position",
        "offset-rotate",
        "opacity",
        "order",
        "orphans",
        "outline",
        "outline-color",
        "outline-offset",
        "outline-style",
        "outline-width",
        "overflow",
        "overflow-anchor",
        "overflow-block",
        "overflow-clip-margin",
        "overflow-inline",
        "overflow-wrap",
        "overflow-x",
        "overflow-y",
        "overlay",
        "overscroll-behavior",
        "overscroll-behavior-block",
        "overscroll-behavior-inline",
        "overscroll-behavior-x",
        "overscroll-behavior-y",
        "padding",
        "padding-block",
        "padding-block-end",
        "padding-block-start",
        "padding-bottom",
        "padding-inline",
        "padding-inline-end",
        "padding-inline-start",
        "padding-left",
        "padding-right",
        "padding-top",
        "page",
        "page-break-after",
        "page-break-before",
        "page-break-inside",
        "paint-order",
        "pause",
        "pause-after",
        "pause-before",
        "perspective",
        "perspective-origin",
        "place-content",
        "place-items",
        "place-self",
        "pointer-events",
        "position",
        "position-anchor",
        "position-visibility",
        "print-color-adjust",
        "quotes",
        "r",
        "resize",
        "rest",
        "rest-after",
        "rest-before",
        "right",
        "rotate",
        "row-gap",
        "ruby-align",
        "ruby-position",
        "scale",
        "scroll-behavior",
        "scroll-margin",
        "scroll-margin-block",
        "scroll-margin-block-end",
        "scroll-margin-block-start",
        "scroll-margin-bottom",
        "scroll-margin-inline",
        "scroll-margin-inline-end",
        "scroll-margin-inline-start",
        "scroll-margin-left",
        "scroll-margin-right",
        "scroll-margin-top",
        "scroll-padding",
        "scroll-padding-block",
        "scroll-padding-block-end",
        "scroll-padding-block-start",
        "scroll-padding-bottom",
        "scroll-padding-inline",
        "scroll-padding-inline-end",
        "scroll-padding-inline-start",
        "scroll-padding-left",
        "scroll-padding-right",
        "scroll-padding-top",
        "scroll-snap-align",
        "scroll-snap-stop",
        "scroll-snap-type",
        "scroll-timeline",
        "scroll-timeline-axis",
        "scroll-timeline-name",
        "scrollbar-color",
        "scrollbar-gutter",
        "scrollbar-width",
        "shape-image-threshold",
        "shape-margin",
        "shape-outside",
        "shape-rendering",
        "speak",
        "speak-as",
        "src",
        // @font-face
        "stop-color",
        "stop-opacity",
        "stroke",
        "stroke-dasharray",
        "stroke-dashoffset",
        "stroke-linecap",
        "stroke-linejoin",
        "stroke-miterlimit",
        "stroke-opacity",
        "stroke-width",
        "tab-size",
        "table-layout",
        "text-align",
        "text-align-all",
        "text-align-last",
        "text-anchor",
        "text-combine-upright",
        "text-decoration",
        "text-decoration-color",
        "text-decoration-line",
        "text-decoration-skip",
        "text-decoration-skip-ink",
        "text-decoration-style",
        "text-decoration-thickness",
        "text-emphasis",
        "text-emphasis-color",
        "text-emphasis-position",
        "text-emphasis-style",
        "text-indent",
        "text-justify",
        "text-orientation",
        "text-overflow",
        "text-rendering",
        "text-shadow",
        "text-size-adjust",
        "text-transform",
        "text-underline-offset",
        "text-underline-position",
        "text-wrap",
        "text-wrap-mode",
        "text-wrap-style",
        "timeline-scope",
        "top",
        "touch-action",
        "transform",
        "transform-box",
        "transform-origin",
        "transform-style",
        "transition",
        "transition-behavior",
        "transition-delay",
        "transition-duration",
        "transition-property",
        "transition-timing-function",
        "translate",
        "unicode-bidi",
        "user-modify",
        "user-select",
        "vector-effect",
        "vertical-align",
        "view-timeline",
        "view-timeline-axis",
        "view-timeline-inset",
        "view-timeline-name",
        "view-transition-name",
        "visibility",
        "voice-balance",
        "voice-duration",
        "voice-family",
        "voice-pitch",
        "voice-range",
        "voice-rate",
        "voice-stress",
        "voice-volume",
        "white-space",
        "white-space-collapse",
        "widows",
        "width",
        "will-change",
        "word-break",
        "word-spacing",
        "word-wrap",
        "writing-mode",
        "x",
        "y",
        "z-index",
        "zoom"
      ].sort().reverse();
      function scss(hljs) {
        const modes = MODES(hljs);
        const PSEUDO_ELEMENTS$1 = PSEUDO_ELEMENTS;
        const PSEUDO_CLASSES$1 = PSEUDO_CLASSES;
        const AT_IDENTIFIER = "@[a-z-]+";
        const AT_MODIFIERS = "and or not only";
        const IDENT_RE = "[a-zA-Z-][a-zA-Z0-9_-]*";
        const VARIABLE = {
          className: "variable",
          begin: "(\\$" + IDENT_RE + ")\\b",
          relevance: 0
        };
        return {
          name: "SCSS",
          case_insensitive: true,
          illegal: "[=/|']",
          contains: [
            hljs.C_LINE_COMMENT_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            // to recognize keyframe 40% etc which are outside the scope of our
            // attribute value mode
            modes.CSS_NUMBER_MODE,
            {
              className: "selector-id",
              begin: "#[A-Za-z0-9_-]+",
              relevance: 0
            },
            {
              className: "selector-class",
              begin: "\\.[A-Za-z0-9_-]+",
              relevance: 0
            },
            modes.ATTRIBUTE_SELECTOR_MODE,
            {
              className: "selector-tag",
              begin: "\\b(" + TAGS.join("|") + ")\\b",
              // was there, before, but why?
              relevance: 0
            },
            {
              className: "selector-pseudo",
              begin: ":(" + PSEUDO_CLASSES$1.join("|") + ")"
            },
            {
              className: "selector-pseudo",
              begin: ":(:)?(" + PSEUDO_ELEMENTS$1.join("|") + ")"
            },
            VARIABLE,
            {
              // pseudo-selector params
              begin: /\(/,
              end: /\)/,
              contains: [modes.CSS_NUMBER_MODE]
            },
            modes.CSS_VARIABLE,
            {
              className: "attribute",
              begin: "\\b(" + ATTRIBUTES.join("|") + ")\\b"
            },
            { begin: "\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" },
            {
              begin: /:/,
              end: /[;}{]/,
              relevance: 0,
              contains: [
                modes.BLOCK_COMMENT,
                VARIABLE,
                modes.HEXCOLOR,
                modes.CSS_NUMBER_MODE,
                hljs.QUOTE_STRING_MODE,
                hljs.APOS_STRING_MODE,
                modes.IMPORTANT,
                modes.FUNCTION_DISPATCH
              ]
            },
            // matching these here allows us to treat them more like regular CSS
            // rules so everything between the {} gets regular rule highlighting,
            // which is what we want for page and font-face
            {
              begin: "@(page|font-face)",
              keywords: {
                $pattern: AT_IDENTIFIER,
                keyword: "@page @font-face"
              }
            },
            {
              begin: "@",
              end: "[{;]",
              returnBegin: true,
              keywords: {
                $pattern: /[a-z-]+/,
                keyword: AT_MODIFIERS,
                attribute: MEDIA_FEATURES.join(" ")
              },
              contains: [
                {
                  begin: AT_IDENTIFIER,
                  className: "keyword"
                },
                {
                  begin: /[a-z-]+(?=:)/,
                  className: "attribute"
                },
                VARIABLE,
                hljs.QUOTE_STRING_MODE,
                hljs.APOS_STRING_MODE,
                modes.HEXCOLOR,
                modes.CSS_NUMBER_MODE
              ]
            },
            modes.FUNCTION_DISPATCH
          ]
        };
      }
      module.exports = scss;
    }
  });

  // node_modules/highlight.js/lib/languages/shell.js
  var require_shell = __commonJS({
    "node_modules/highlight.js/lib/languages/shell.js"(exports, module) {
      function shell(hljs) {
        return {
          name: "Shell Session",
          aliases: [
            "console",
            "shellsession"
          ],
          contains: [
            {
              className: "meta.prompt",
              // We cannot add \s (spaces) in the regular expression otherwise it will be too broad and produce unexpected result.
              // For instance, in the following example, it would match "echo /path/to/home >" as a prompt:
              // echo /path/to/home > t.exe
              begin: /^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,
              starts: {
                end: /[^\\](?=\s*$)/,
                subLanguage: "bash"
              }
            }
          ]
        };
      }
      module.exports = shell;
    }
  });

  // node_modules/highlight.js/lib/languages/sql.js
  var require_sql = __commonJS({
    "node_modules/highlight.js/lib/languages/sql.js"(exports, module) {
      function sql(hljs) {
        const regex = hljs.regex;
        const COMMENT_MODE = hljs.COMMENT("--", "$");
        const STRING = {
          scope: "string",
          variants: [
            {
              begin: /'/,
              end: /'/,
              contains: [{ match: /''/ }]
            }
          ]
        };
        const QUOTED_IDENTIFIER = {
          begin: /"/,
          end: /"/,
          contains: [{ match: /""/ }]
        };
        const LITERALS = [
          "true",
          "false",
          // Not sure it's correct to call NULL literal, and clauses like IS [NOT] NULL look strange that way.
          // "null",
          "unknown"
        ];
        const MULTI_WORD_TYPES = [
          "double precision",
          "large object",
          "with timezone",
          "without timezone"
        ];
        const TYPES = [
          "bigint",
          "binary",
          "blob",
          "boolean",
          "char",
          "character",
          "clob",
          "date",
          "dec",
          "decfloat",
          "decimal",
          "float",
          "int",
          "integer",
          "interval",
          "nchar",
          "nclob",
          "national",
          "numeric",
          "real",
          "row",
          "smallint",
          "time",
          "timestamp",
          "varchar",
          "varying",
          // modifier (character varying)
          "varbinary"
        ];
        const NON_RESERVED_WORDS = [
          "add",
          "asc",
          "collation",
          "desc",
          "final",
          "first",
          "last",
          "view"
        ];
        const RESERVED_WORDS = [
          "abs",
          "acos",
          "all",
          "allocate",
          "alter",
          "and",
          "any",
          "are",
          "array",
          "array_agg",
          "array_max_cardinality",
          "as",
          "asensitive",
          "asin",
          "asymmetric",
          "at",
          "atan",
          "atomic",
          "authorization",
          "avg",
          "begin",
          "begin_frame",
          "begin_partition",
          "between",
          "bigint",
          "binary",
          "blob",
          "boolean",
          "both",
          "by",
          "call",
          "called",
          "cardinality",
          "cascaded",
          "case",
          "cast",
          "ceil",
          "ceiling",
          "char",
          "char_length",
          "character",
          "character_length",
          "check",
          "classifier",
          "clob",
          "close",
          "coalesce",
          "collate",
          "collect",
          "column",
          "commit",
          "condition",
          "connect",
          "constraint",
          "contains",
          "convert",
          "copy",
          "corr",
          "corresponding",
          "cos",
          "cosh",
          "count",
          "covar_pop",
          "covar_samp",
          "create",
          "cross",
          "cube",
          "cume_dist",
          "current",
          "current_catalog",
          "current_date",
          "current_default_transform_group",
          "current_path",
          "current_role",
          "current_row",
          "current_schema",
          "current_time",
          "current_timestamp",
          "current_path",
          "current_role",
          "current_transform_group_for_type",
          "current_user",
          "cursor",
          "cycle",
          "date",
          "day",
          "deallocate",
          "dec",
          "decimal",
          "decfloat",
          "declare",
          "default",
          "define",
          "delete",
          "dense_rank",
          "deref",
          "describe",
          "deterministic",
          "disconnect",
          "distinct",
          "double",
          "drop",
          "dynamic",
          "each",
          "element",
          "else",
          "empty",
          "end",
          "end_frame",
          "end_partition",
          "end-exec",
          "equals",
          "escape",
          "every",
          "except",
          "exec",
          "execute",
          "exists",
          "exp",
          "external",
          "extract",
          "false",
          "fetch",
          "filter",
          "first_value",
          "float",
          "floor",
          "for",
          "foreign",
          "frame_row",
          "free",
          "from",
          "full",
          "function",
          "fusion",
          "get",
          "global",
          "grant",
          "group",
          "grouping",
          "groups",
          "having",
          "hold",
          "hour",
          "identity",
          "in",
          "indicator",
          "initial",
          "inner",
          "inout",
          "insensitive",
          "insert",
          "int",
          "integer",
          "intersect",
          "intersection",
          "interval",
          "into",
          "is",
          "join",
          "json_array",
          "json_arrayagg",
          "json_exists",
          "json_object",
          "json_objectagg",
          "json_query",
          "json_table",
          "json_table_primitive",
          "json_value",
          "lag",
          "language",
          "large",
          "last_value",
          "lateral",
          "lead",
          "leading",
          "left",
          "like",
          "like_regex",
          "listagg",
          "ln",
          "local",
          "localtime",
          "localtimestamp",
          "log",
          "log10",
          "lower",
          "match",
          "match_number",
          "match_recognize",
          "matches",
          "max",
          "member",
          "merge",
          "method",
          "min",
          "minute",
          "mod",
          "modifies",
          "module",
          "month",
          "multiset",
          "national",
          "natural",
          "nchar",
          "nclob",
          "new",
          "no",
          "none",
          "normalize",
          "not",
          "nth_value",
          "ntile",
          "null",
          "nullif",
          "numeric",
          "octet_length",
          "occurrences_regex",
          "of",
          "offset",
          "old",
          "omit",
          "on",
          "one",
          "only",
          "open",
          "or",
          "order",
          "out",
          "outer",
          "over",
          "overlaps",
          "overlay",
          "parameter",
          "partition",
          "pattern",
          "per",
          "percent",
          "percent_rank",
          "percentile_cont",
          "percentile_disc",
          "period",
          "portion",
          "position",
          "position_regex",
          "power",
          "precedes",
          "precision",
          "prepare",
          "primary",
          "procedure",
          "ptf",
          "range",
          "rank",
          "reads",
          "real",
          "recursive",
          "ref",
          "references",
          "referencing",
          "regr_avgx",
          "regr_avgy",
          "regr_count",
          "regr_intercept",
          "regr_r2",
          "regr_slope",
          "regr_sxx",
          "regr_sxy",
          "regr_syy",
          "release",
          "result",
          "return",
          "returns",
          "revoke",
          "right",
          "rollback",
          "rollup",
          "row",
          "row_number",
          "rows",
          "running",
          "savepoint",
          "scope",
          "scroll",
          "search",
          "second",
          "seek",
          "select",
          "sensitive",
          "session_user",
          "set",
          "show",
          "similar",
          "sin",
          "sinh",
          "skip",
          "smallint",
          "some",
          "specific",
          "specifictype",
          "sql",
          "sqlexception",
          "sqlstate",
          "sqlwarning",
          "sqrt",
          "start",
          "static",
          "stddev_pop",
          "stddev_samp",
          "submultiset",
          "subset",
          "substring",
          "substring_regex",
          "succeeds",
          "sum",
          "symmetric",
          "system",
          "system_time",
          "system_user",
          "table",
          "tablesample",
          "tan",
          "tanh",
          "then",
          "time",
          "timestamp",
          "timezone_hour",
          "timezone_minute",
          "to",
          "trailing",
          "translate",
          "translate_regex",
          "translation",
          "treat",
          "trigger",
          "trim",
          "trim_array",
          "true",
          "truncate",
          "uescape",
          "union",
          "unique",
          "unknown",
          "unnest",
          "update",
          "upper",
          "user",
          "using",
          "value",
          "values",
          "value_of",
          "var_pop",
          "var_samp",
          "varbinary",
          "varchar",
          "varying",
          "versioning",
          "when",
          "whenever",
          "where",
          "width_bucket",
          "window",
          "with",
          "within",
          "without",
          "year"
        ];
        const RESERVED_FUNCTIONS = [
          "abs",
          "acos",
          "array_agg",
          "asin",
          "atan",
          "avg",
          "cast",
          "ceil",
          "ceiling",
          "coalesce",
          "corr",
          "cos",
          "cosh",
          "count",
          "covar_pop",
          "covar_samp",
          "cume_dist",
          "dense_rank",
          "deref",
          "element",
          "exp",
          "extract",
          "first_value",
          "floor",
          "json_array",
          "json_arrayagg",
          "json_exists",
          "json_object",
          "json_objectagg",
          "json_query",
          "json_table",
          "json_table_primitive",
          "json_value",
          "lag",
          "last_value",
          "lead",
          "listagg",
          "ln",
          "log",
          "log10",
          "lower",
          "max",
          "min",
          "mod",
          "nth_value",
          "ntile",
          "nullif",
          "percent_rank",
          "percentile_cont",
          "percentile_disc",
          "position",
          "position_regex",
          "power",
          "rank",
          "regr_avgx",
          "regr_avgy",
          "regr_count",
          "regr_intercept",
          "regr_r2",
          "regr_slope",
          "regr_sxx",
          "regr_sxy",
          "regr_syy",
          "row_number",
          "sin",
          "sinh",
          "sqrt",
          "stddev_pop",
          "stddev_samp",
          "substring",
          "substring_regex",
          "sum",
          "tan",
          "tanh",
          "translate",
          "translate_regex",
          "treat",
          "trim",
          "trim_array",
          "unnest",
          "upper",
          "value_of",
          "var_pop",
          "var_samp",
          "width_bucket"
        ];
        const POSSIBLE_WITHOUT_PARENS = [
          "current_catalog",
          "current_date",
          "current_default_transform_group",
          "current_path",
          "current_role",
          "current_schema",
          "current_transform_group_for_type",
          "current_user",
          "session_user",
          "system_time",
          "system_user",
          "current_time",
          "localtime",
          "current_timestamp",
          "localtimestamp"
        ];
        const COMBOS = [
          "create table",
          "insert into",
          "primary key",
          "foreign key",
          "not null",
          "alter table",
          "add constraint",
          "grouping sets",
          "on overflow",
          "character set",
          "respect nulls",
          "ignore nulls",
          "nulls first",
          "nulls last",
          "depth first",
          "breadth first"
        ];
        const FUNCTIONS = RESERVED_FUNCTIONS;
        const KEYWORDS = [
          ...RESERVED_WORDS,
          ...NON_RESERVED_WORDS
        ].filter((keyword) => {
          return !RESERVED_FUNCTIONS.includes(keyword);
        });
        const VARIABLE = {
          scope: "variable",
          match: /@[a-z0-9][a-z0-9_]*/
        };
        const OPERATOR = {
          scope: "operator",
          match: /[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,
          relevance: 0
        };
        const FUNCTION_CALL = {
          match: regex.concat(/\b/, regex.either(...FUNCTIONS), /\s*\(/),
          relevance: 0,
          keywords: { built_in: FUNCTIONS }
        };
        function kws_to_regex(list) {
          return regex.concat(
            /\b/,
            regex.either(...list.map((kw) => {
              return kw.replace(/\s+/, "\\s+");
            })),
            /\b/
          );
        }
        const MULTI_WORD_KEYWORDS = {
          scope: "keyword",
          match: kws_to_regex(COMBOS),
          relevance: 0
        };
        function reduceRelevancy(list, {
          exceptions,
          when
        } = {}) {
          const qualifyFn = when;
          exceptions = exceptions || [];
          return list.map((item) => {
            if (item.match(/\|\d+$/) || exceptions.includes(item)) {
              return item;
            } else if (qualifyFn(item)) {
              return `${item}|0`;
            } else {
              return item;
            }
          });
        }
        return {
          name: "SQL",
          case_insensitive: true,
          // does not include {} or HTML tags `</`
          illegal: /[{}]|<\//,
          keywords: {
            $pattern: /\b[\w\.]+/,
            keyword: reduceRelevancy(KEYWORDS, { when: (x) => x.length < 3 }),
            literal: LITERALS,
            type: TYPES,
            built_in: POSSIBLE_WITHOUT_PARENS
          },
          contains: [
            {
              scope: "type",
              match: kws_to_regex(MULTI_WORD_TYPES)
            },
            MULTI_WORD_KEYWORDS,
            FUNCTION_CALL,
            VARIABLE,
            STRING,
            QUOTED_IDENTIFIER,
            hljs.C_NUMBER_MODE,
            hljs.C_BLOCK_COMMENT_MODE,
            COMMENT_MODE,
            OPERATOR
          ]
        };
      }
      module.exports = sql;
    }
  });

  // node_modules/highlight.js/lib/languages/swift.js
  var require_swift = __commonJS({
    "node_modules/highlight.js/lib/languages/swift.js"(exports, module) {
      function source(re) {
        if (!re) return null;
        if (typeof re === "string") return re;
        return re.source;
      }
      function lookahead(re) {
        return concat("(?=", re, ")");
      }
      function concat(...args) {
        const joined = args.map((x) => source(x)).join("");
        return joined;
      }
      function stripOptionsFromArgs(args) {
        const opts = args[args.length - 1];
        if (typeof opts === "object" && opts.constructor === Object) {
          args.splice(args.length - 1, 1);
          return opts;
        } else {
          return {};
        }
      }
      function either(...args) {
        const opts = stripOptionsFromArgs(args);
        const joined = "(" + (opts.capture ? "" : "?:") + args.map((x) => source(x)).join("|") + ")";
        return joined;
      }
      var keywordWrapper = (keyword) => concat(
        /\b/,
        keyword,
        /\w$/.test(keyword) ? /\b/ : /\B/
      );
      var dotKeywords = [
        "Protocol",
        // contextual
        "Type"
        // contextual
      ].map(keywordWrapper);
      var optionalDotKeywords = [
        "init",
        "self"
      ].map(keywordWrapper);
      var keywordTypes = [
        "Any",
        "Self"
      ];
      var keywords = [
        // strings below will be fed into the regular `keywords` engine while regex
        // will result in additional modes being created to scan for those keywords to
        // avoid conflicts with other rules
        "actor",
        "any",
        // contextual
        "associatedtype",
        "async",
        "await",
        /as\?/,
        // operator
        /as!/,
        // operator
        "as",
        // operator
        "borrowing",
        // contextual
        "break",
        "case",
        "catch",
        "class",
        "consume",
        // contextual
        "consuming",
        // contextual
        "continue",
        "convenience",
        // contextual
        "copy",
        // contextual
        "default",
        "defer",
        "deinit",
        "didSet",
        // contextual
        "distributed",
        "do",
        "dynamic",
        // contextual
        "each",
        "else",
        "enum",
        "extension",
        "fallthrough",
        /fileprivate\(set\)/,
        "fileprivate",
        "final",
        // contextual
        "for",
        "func",
        "get",
        // contextual
        "guard",
        "if",
        "import",
        "indirect",
        // contextual
        "infix",
        // contextual
        /init\?/,
        /init!/,
        "inout",
        /internal\(set\)/,
        "internal",
        "in",
        "is",
        // operator
        "isolated",
        // contextual
        "nonisolated",
        // contextual
        "lazy",
        // contextual
        "let",
        "macro",
        "mutating",
        // contextual
        "nonmutating",
        // contextual
        /open\(set\)/,
        // contextual
        "open",
        // contextual
        "operator",
        "optional",
        // contextual
        "override",
        // contextual
        "package",
        "postfix",
        // contextual
        "precedencegroup",
        "prefix",
        // contextual
        /private\(set\)/,
        "private",
        "protocol",
        /public\(set\)/,
        "public",
        "repeat",
        "required",
        // contextual
        "rethrows",
        "return",
        "set",
        // contextual
        "some",
        // contextual
        "static",
        "struct",
        "subscript",
        "super",
        "switch",
        "throws",
        "throw",
        /try\?/,
        // operator
        /try!/,
        // operator
        "try",
        // operator
        "typealias",
        /unowned\(safe\)/,
        // contextual
        /unowned\(unsafe\)/,
        // contextual
        "unowned",
        // contextual
        "var",
        "weak",
        // contextual
        "where",
        "while",
        "willSet"
        // contextual
      ];
      var literals = [
        "false",
        "nil",
        "true"
      ];
      var precedencegroupKeywords = [
        "assignment",
        "associativity",
        "higherThan",
        "left",
        "lowerThan",
        "none",
        "right"
      ];
      var numberSignKeywords = [
        "#colorLiteral",
        "#column",
        "#dsohandle",
        "#else",
        "#elseif",
        "#endif",
        "#error",
        "#file",
        "#fileID",
        "#fileLiteral",
        "#filePath",
        "#function",
        "#if",
        "#imageLiteral",
        "#keyPath",
        "#line",
        "#selector",
        "#sourceLocation",
        "#warning"
      ];
      var builtIns = [
        "abs",
        "all",
        "any",
        "assert",
        "assertionFailure",
        "debugPrint",
        "dump",
        "fatalError",
        "getVaList",
        "isKnownUniquelyReferenced",
        "max",
        "min",
        "numericCast",
        "pointwiseMax",
        "pointwiseMin",
        "precondition",
        "preconditionFailure",
        "print",
        "readLine",
        "repeatElement",
        "sequence",
        "stride",
        "swap",
        "swift_unboxFromSwiftValueWithType",
        "transcode",
        "type",
        "unsafeBitCast",
        "unsafeDowncast",
        "withExtendedLifetime",
        "withUnsafeMutablePointer",
        "withUnsafePointer",
        "withVaList",
        "withoutActuallyEscaping",
        "zip"
      ];
      var operatorHead = either(
        /[/=\-+!*%<>&|^~?]/,
        /[\u00A1-\u00A7]/,
        /[\u00A9\u00AB]/,
        /[\u00AC\u00AE]/,
        /[\u00B0\u00B1]/,
        /[\u00B6\u00BB\u00BF\u00D7\u00F7]/,
        /[\u2016-\u2017]/,
        /[\u2020-\u2027]/,
        /[\u2030-\u203E]/,
        /[\u2041-\u2053]/,
        /[\u2055-\u205E]/,
        /[\u2190-\u23FF]/,
        /[\u2500-\u2775]/,
        /[\u2794-\u2BFF]/,
        /[\u2E00-\u2E7F]/,
        /[\u3001-\u3003]/,
        /[\u3008-\u3020]/,
        /[\u3030]/
      );
      var operatorCharacter = either(
        operatorHead,
        /[\u0300-\u036F]/,
        /[\u1DC0-\u1DFF]/,
        /[\u20D0-\u20FF]/,
        /[\uFE00-\uFE0F]/,
        /[\uFE20-\uFE2F]/
        // TODO: The following characters are also allowed, but the regex isn't supported yet.
        // /[\u{E0100}-\u{E01EF}]/u
      );
      var operator = concat(operatorHead, operatorCharacter, "*");
      var identifierHead = either(
        /[a-zA-Z_]/,
        /[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,
        /[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,
        /[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,
        /[\u1E00-\u1FFF]/,
        /[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,
        /[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,
        /[\u2C00-\u2DFF\u2E80-\u2FFF]/,
        /[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,
        /[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,
        /[\uFE47-\uFEFE\uFF00-\uFFFD]/
        // Should be /[\uFE47-\uFFFD]/, but we have to exclude FEFF.
        // The following characters are also allowed, but the regexes aren't supported yet.
        // /[\u{10000}-\u{1FFFD}\u{20000-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}]/u,
        // /[\u{50000}-\u{5FFFD}\u{60000-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}]/u,
        // /[\u{90000}-\u{9FFFD}\u{A0000-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}]/u,
        // /[\u{D0000}-\u{DFFFD}\u{E0000-\u{EFFFD}]/u
      );
      var identifierCharacter = either(
        identifierHead,
        /\d/,
        /[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/
      );
      var identifier = concat(identifierHead, identifierCharacter, "*");
      var typeIdentifier = concat(/[A-Z]/, identifierCharacter, "*");
      var keywordAttributes = [
        "attached",
        "autoclosure",
        concat(/convention\(/, either("swift", "block", "c"), /\)/),
        "discardableResult",
        "dynamicCallable",
        "dynamicMemberLookup",
        "escaping",
        "freestanding",
        "frozen",
        "GKInspectable",
        "IBAction",
        "IBDesignable",
        "IBInspectable",
        "IBOutlet",
        "IBSegueAction",
        "inlinable",
        "main",
        "nonobjc",
        "NSApplicationMain",
        "NSCopying",
        "NSManaged",
        concat(/objc\(/, identifier, /\)/),
        "objc",
        "objcMembers",
        "propertyWrapper",
        "requires_stored_property_inits",
        "resultBuilder",
        "Sendable",
        "testable",
        "UIApplicationMain",
        "unchecked",
        "unknown",
        "usableFromInline",
        "warn_unqualified_access"
      ];
      var availabilityKeywords = [
        "iOS",
        "iOSApplicationExtension",
        "macOS",
        "macOSApplicationExtension",
        "macCatalyst",
        "macCatalystApplicationExtension",
        "watchOS",
        "watchOSApplicationExtension",
        "tvOS",
        "tvOSApplicationExtension",
        "swift"
      ];
      function swift(hljs) {
        const WHITESPACE = {
          match: /\s+/,
          relevance: 0
        };
        const BLOCK_COMMENT = hljs.COMMENT(
          "/\\*",
          "\\*/",
          { contains: ["self"] }
        );
        const COMMENTS = [
          hljs.C_LINE_COMMENT_MODE,
          BLOCK_COMMENT
        ];
        const DOT_KEYWORD = {
          match: [
            /\./,
            either(...dotKeywords, ...optionalDotKeywords)
          ],
          className: { 2: "keyword" }
        };
        const KEYWORD_GUARD = {
          // Consume .keyword to prevent highlighting properties and methods as keywords.
          match: concat(/\./, either(...keywords)),
          relevance: 0
        };
        const PLAIN_KEYWORDS = keywords.filter((kw) => typeof kw === "string").concat(["_|0"]);
        const REGEX_KEYWORDS = keywords.filter((kw) => typeof kw !== "string").concat(keywordTypes).map(keywordWrapper);
        const KEYWORD = { variants: [
          {
            className: "keyword",
            match: either(...REGEX_KEYWORDS, ...optionalDotKeywords)
          }
        ] };
        const KEYWORDS = {
          $pattern: either(
            /\b\w+/,
            // regular keywords
            /#\w+/
            // number keywords
          ),
          keyword: PLAIN_KEYWORDS.concat(numberSignKeywords),
          literal: literals
        };
        const KEYWORD_MODES = [
          DOT_KEYWORD,
          KEYWORD_GUARD,
          KEYWORD
        ];
        const BUILT_IN_GUARD = {
          // Consume .built_in to prevent highlighting properties and methods.
          match: concat(/\./, either(...builtIns)),
          relevance: 0
        };
        const BUILT_IN = {
          className: "built_in",
          match: concat(/\b/, either(...builtIns), /(?=\()/)
        };
        const BUILT_INS = [
          BUILT_IN_GUARD,
          BUILT_IN
        ];
        const OPERATOR_GUARD = {
          // Prevent -> from being highlighting as an operator.
          match: /->/,
          relevance: 0
        };
        const OPERATOR = {
          className: "operator",
          relevance: 0,
          variants: [
            { match: operator },
            {
              // dot-operator: only operators that start with a dot are allowed to use dots as
              // characters (..., ...<, .*, etc). So there rule here is: a dot followed by one or more
              // characters that may also include dots.
              match: `\\.(\\.|${operatorCharacter})+`
            }
          ]
        };
        const OPERATORS = [
          OPERATOR_GUARD,
          OPERATOR
        ];
        const decimalDigits = "([0-9]_*)+";
        const hexDigits = "([0-9a-fA-F]_*)+";
        const NUMBER = {
          className: "number",
          relevance: 0,
          variants: [
            // decimal floating-point-literal (subsumes decimal-literal)
            { match: `\\b(${decimalDigits})(\\.(${decimalDigits}))?([eE][+-]?(${decimalDigits}))?\\b` },
            // hexadecimal floating-point-literal (subsumes hexadecimal-literal)
            { match: `\\b0x(${hexDigits})(\\.(${hexDigits}))?([pP][+-]?(${decimalDigits}))?\\b` },
            // octal-literal
            { match: /\b0o([0-7]_*)+\b/ },
            // binary-literal
            { match: /\b0b([01]_*)+\b/ }
          ]
        };
        const ESCAPED_CHARACTER = (rawDelimiter = "") => ({
          className: "subst",
          variants: [
            { match: concat(/\\/, rawDelimiter, /[0\\tnr"']/) },
            { match: concat(/\\/, rawDelimiter, /u\{[0-9a-fA-F]{1,8}\}/) }
          ]
        });
        const ESCAPED_NEWLINE = (rawDelimiter = "") => ({
          className: "subst",
          match: concat(/\\/, rawDelimiter, /[\t ]*(?:[\r\n]|\r\n)/)
        });
        const INTERPOLATION = (rawDelimiter = "") => ({
          className: "subst",
          label: "interpol",
          begin: concat(/\\/, rawDelimiter, /\(/),
          end: /\)/
        });
        const MULTILINE_STRING = (rawDelimiter = "") => ({
          begin: concat(rawDelimiter, /"""/),
          end: concat(/"""/, rawDelimiter),
          contains: [
            ESCAPED_CHARACTER(rawDelimiter),
            ESCAPED_NEWLINE(rawDelimiter),
            INTERPOLATION(rawDelimiter)
          ]
        });
        const SINGLE_LINE_STRING = (rawDelimiter = "") => ({
          begin: concat(rawDelimiter, /"/),
          end: concat(/"/, rawDelimiter),
          contains: [
            ESCAPED_CHARACTER(rawDelimiter),
            INTERPOLATION(rawDelimiter)
          ]
        });
        const STRING = {
          className: "string",
          variants: [
            MULTILINE_STRING(),
            MULTILINE_STRING("#"),
            MULTILINE_STRING("##"),
            MULTILINE_STRING("###"),
            SINGLE_LINE_STRING(),
            SINGLE_LINE_STRING("#"),
            SINGLE_LINE_STRING("##"),
            SINGLE_LINE_STRING("###")
          ]
        };
        const REGEXP_CONTENTS = [
          hljs.BACKSLASH_ESCAPE,
          {
            begin: /\[/,
            end: /\]/,
            relevance: 0,
            contains: [hljs.BACKSLASH_ESCAPE]
          }
        ];
        const BARE_REGEXP_LITERAL = {
          begin: /\/[^\s](?=[^/\n]*\/)/,
          end: /\//,
          contains: REGEXP_CONTENTS
        };
        const EXTENDED_REGEXP_LITERAL = (rawDelimiter) => {
          const begin = concat(rawDelimiter, /\//);
          const end = concat(/\//, rawDelimiter);
          return {
            begin,
            end,
            contains: [
              ...REGEXP_CONTENTS,
              {
                scope: "comment",
                begin: `#(?!.*${end})`,
                end: /$/
              }
            ]
          };
        };
        const REGEXP = {
          scope: "regexp",
          variants: [
            EXTENDED_REGEXP_LITERAL("###"),
            EXTENDED_REGEXP_LITERAL("##"),
            EXTENDED_REGEXP_LITERAL("#"),
            BARE_REGEXP_LITERAL
          ]
        };
        const QUOTED_IDENTIFIER = { match: concat(/`/, identifier, /`/) };
        const IMPLICIT_PARAMETER = {
          className: "variable",
          match: /\$\d+/
        };
        const PROPERTY_WRAPPER_PROJECTION = {
          className: "variable",
          match: `\\$${identifierCharacter}+`
        };
        const IDENTIFIERS = [
          QUOTED_IDENTIFIER,
          IMPLICIT_PARAMETER,
          PROPERTY_WRAPPER_PROJECTION
        ];
        const AVAILABLE_ATTRIBUTE = {
          match: /(@|#(un)?)available/,
          scope: "keyword",
          starts: { contains: [
            {
              begin: /\(/,
              end: /\)/,
              keywords: availabilityKeywords,
              contains: [
                ...OPERATORS,
                NUMBER,
                STRING
              ]
            }
          ] }
        };
        const KEYWORD_ATTRIBUTE = {
          scope: "keyword",
          match: concat(/@/, either(...keywordAttributes), lookahead(either(/\(/, /\s+/)))
        };
        const USER_DEFINED_ATTRIBUTE = {
          scope: "meta",
          match: concat(/@/, identifier)
        };
        const ATTRIBUTES = [
          AVAILABLE_ATTRIBUTE,
          KEYWORD_ATTRIBUTE,
          USER_DEFINED_ATTRIBUTE
        ];
        const TYPE = {
          match: lookahead(/\b[A-Z]/),
          relevance: 0,
          contains: [
            {
              // Common Apple frameworks, for relevance boost
              className: "type",
              match: concat(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/, identifierCharacter, "+")
            },
            {
              // Type identifier
              className: "type",
              match: typeIdentifier,
              relevance: 0
            },
            {
              // Optional type
              match: /[?!]+/,
              relevance: 0
            },
            {
              // Variadic parameter
              match: /\.\.\./,
              relevance: 0
            },
            {
              // Protocol composition
              match: concat(/\s+&\s+/, lookahead(typeIdentifier)),
              relevance: 0
            }
          ]
        };
        const GENERIC_ARGUMENTS = {
          begin: /</,
          end: />/,
          keywords: KEYWORDS,
          contains: [
            ...COMMENTS,
            ...KEYWORD_MODES,
            ...ATTRIBUTES,
            OPERATOR_GUARD,
            TYPE
          ]
        };
        TYPE.contains.push(GENERIC_ARGUMENTS);
        const TUPLE_ELEMENT_NAME = {
          match: concat(identifier, /\s*:/),
          keywords: "_|0",
          relevance: 0
        };
        const TUPLE = {
          begin: /\(/,
          end: /\)/,
          relevance: 0,
          keywords: KEYWORDS,
          contains: [
            "self",
            TUPLE_ELEMENT_NAME,
            ...COMMENTS,
            REGEXP,
            ...KEYWORD_MODES,
            ...BUILT_INS,
            ...OPERATORS,
            NUMBER,
            STRING,
            ...IDENTIFIERS,
            ...ATTRIBUTES,
            TYPE
          ]
        };
        const GENERIC_PARAMETERS = {
          begin: /</,
          end: />/,
          keywords: "repeat each",
          contains: [
            ...COMMENTS,
            TYPE
          ]
        };
        const FUNCTION_PARAMETER_NAME = {
          begin: either(
            lookahead(concat(identifier, /\s*:/)),
            lookahead(concat(identifier, /\s+/, identifier, /\s*:/))
          ),
          end: /:/,
          relevance: 0,
          contains: [
            {
              className: "keyword",
              match: /\b_\b/
            },
            {
              className: "params",
              match: identifier
            }
          ]
        };
        const FUNCTION_PARAMETERS = {
          begin: /\(/,
          end: /\)/,
          keywords: KEYWORDS,
          contains: [
            FUNCTION_PARAMETER_NAME,
            ...COMMENTS,
            ...KEYWORD_MODES,
            ...OPERATORS,
            NUMBER,
            STRING,
            ...ATTRIBUTES,
            TYPE,
            TUPLE
          ],
          endsParent: true,
          illegal: /["']/
        };
        const FUNCTION_OR_MACRO = {
          match: [
            /(func|macro)/,
            /\s+/,
            either(QUOTED_IDENTIFIER.match, identifier, operator)
          ],
          className: {
            1: "keyword",
            3: "title.function"
          },
          contains: [
            GENERIC_PARAMETERS,
            FUNCTION_PARAMETERS,
            WHITESPACE
          ],
          illegal: [
            /\[/,
            /%/
          ]
        };
        const INIT_SUBSCRIPT = {
          match: [
            /\b(?:subscript|init[?!]?)/,
            /\s*(?=[<(])/
          ],
          className: { 1: "keyword" },
          contains: [
            GENERIC_PARAMETERS,
            FUNCTION_PARAMETERS,
            WHITESPACE
          ],
          illegal: /\[|%/
        };
        const OPERATOR_DECLARATION = {
          match: [
            /operator/,
            /\s+/,
            operator
          ],
          className: {
            1: "keyword",
            3: "title"
          }
        };
        const PRECEDENCEGROUP = {
          begin: [
            /precedencegroup/,
            /\s+/,
            typeIdentifier
          ],
          className: {
            1: "keyword",
            3: "title"
          },
          contains: [TYPE],
          keywords: [
            ...precedencegroupKeywords,
            ...literals
          ],
          end: /}/
        };
        const CLASS_FUNC_DECLARATION = {
          match: [
            /class\b/,
            /\s+/,
            /func\b/,
            /\s+/,
            /\b[A-Za-z_][A-Za-z0-9_]*\b/
          ],
          scope: {
            1: "keyword",
            3: "keyword",
            5: "title.function"
          }
        };
        const CLASS_VAR_DECLARATION = {
          match: [
            /class\b/,
            /\s+/,
            /var\b/
          ],
          scope: {
            1: "keyword",
            3: "keyword"
          }
        };
        const TYPE_DECLARATION = {
          begin: [
            /(struct|protocol|class|extension|enum|actor)/,
            /\s+/,
            identifier,
            /\s*/
          ],
          beginScope: {
            1: "keyword",
            3: "title.class"
          },
          keywords: KEYWORDS,
          contains: [
            GENERIC_PARAMETERS,
            ...KEYWORD_MODES,
            {
              begin: /:/,
              end: /\{/,
              keywords: KEYWORDS,
              contains: [
                {
                  scope: "title.class.inherited",
                  match: typeIdentifier
                },
                ...KEYWORD_MODES
              ],
              relevance: 0
            }
          ]
        };
        for (const variant of STRING.variants) {
          const interpolation = variant.contains.find((mode) => mode.label === "interpol");
          interpolation.keywords = KEYWORDS;
          const submodes = [
            ...KEYWORD_MODES,
            ...BUILT_INS,
            ...OPERATORS,
            NUMBER,
            STRING,
            ...IDENTIFIERS
          ];
          interpolation.contains = [
            ...submodes,
            {
              begin: /\(/,
              end: /\)/,
              contains: [
                "self",
                ...submodes
              ]
            }
          ];
        }
        return {
          name: "Swift",
          keywords: KEYWORDS,
          contains: [
            ...COMMENTS,
            FUNCTION_OR_MACRO,
            INIT_SUBSCRIPT,
            CLASS_FUNC_DECLARATION,
            CLASS_VAR_DECLARATION,
            TYPE_DECLARATION,
            OPERATOR_DECLARATION,
            PRECEDENCEGROUP,
            {
              beginKeywords: "import",
              end: /$/,
              contains: [...COMMENTS],
              relevance: 0
            },
            REGEXP,
            ...KEYWORD_MODES,
            ...BUILT_INS,
            ...OPERATORS,
            NUMBER,
            STRING,
            ...IDENTIFIERS,
            ...ATTRIBUTES,
            TYPE,
            TUPLE
          ]
        };
      }
      module.exports = swift;
    }
  });

  // node_modules/highlight.js/lib/languages/yaml.js
  var require_yaml = __commonJS({
    "node_modules/highlight.js/lib/languages/yaml.js"(exports, module) {
      function yaml(hljs) {
        const LITERALS = "true false yes no null";
        const URI_CHARACTERS = "[\\w#;/?:@&=+$,.~*'()[\\]]+";
        const KEY = {
          className: "attr",
          variants: [
            // added brackets support and special char support
            { begin: /[\w*@][\w*@ :()\./-]*:(?=[ \t]|$)/ },
            {
              // double quoted keys - with brackets and special char support
              begin: /"[\w*@][\w*@ :()\./-]*":(?=[ \t]|$)/
            },
            {
              // single quoted keys - with brackets and special char support
              begin: /'[\w*@][\w*@ :()\./-]*':(?=[ \t]|$)/
            }
          ]
        };
        const TEMPLATE_VARIABLES = {
          className: "template-variable",
          variants: [
            {
              // jinja templates Ansible
              begin: /\{\{/,
              end: /\}\}/
            },
            {
              // Ruby i18n
              begin: /%\{/,
              end: /\}/
            }
          ]
        };
        const SINGLE_QUOTE_STRING = {
          className: "string",
          relevance: 0,
          begin: /'/,
          end: /'/,
          contains: [
            {
              match: /''/,
              scope: "char.escape",
              relevance: 0
            }
          ]
        };
        const STRING = {
          className: "string",
          relevance: 0,
          variants: [
            {
              begin: /"/,
              end: /"/
            },
            { begin: /\S+/ }
          ],
          contains: [
            hljs.BACKSLASH_ESCAPE,
            TEMPLATE_VARIABLES
          ]
        };
        const CONTAINER_STRING = hljs.inherit(STRING, { variants: [
          {
            begin: /'/,
            end: /'/,
            contains: [
              {
                begin: /''/,
                relevance: 0
              }
            ]
          },
          {
            begin: /"/,
            end: /"/
          },
          { begin: /[^\s,{}[\]]+/ }
        ] });
        const DATE_RE = "[0-9]{4}(-[0-9][0-9]){0,2}";
        const TIME_RE = "([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?";
        const FRACTION_RE = "(\\.[0-9]*)?";
        const ZONE_RE = "([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?";
        const TIMESTAMP = {
          className: "number",
          begin: "\\b" + DATE_RE + TIME_RE + FRACTION_RE + ZONE_RE + "\\b"
        };
        const VALUE_CONTAINER = {
          end: ",",
          endsWithParent: true,
          excludeEnd: true,
          keywords: LITERALS,
          relevance: 0
        };
        const OBJECT = {
          begin: /\{/,
          end: /\}/,
          contains: [VALUE_CONTAINER],
          illegal: "\\n",
          relevance: 0
        };
        const ARRAY = {
          begin: "\\[",
          end: "\\]",
          contains: [VALUE_CONTAINER],
          illegal: "\\n",
          relevance: 0
        };
        const MODES = [
          KEY,
          {
            className: "meta",
            begin: "^---\\s*$",
            relevance: 10
          },
          {
            // multi line string
            // Blocks start with a | or > followed by a newline
            //
            // Indentation of subsequent lines must be the same to
            // be considered part of the block
            className: "string",
            begin: "[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"
          },
          {
            // Ruby/Rails erb
            begin: "<%[%=-]?",
            end: "[%-]?%>",
            subLanguage: "ruby",
            excludeBegin: true,
            excludeEnd: true,
            relevance: 0
          },
          {
            // named tags
            className: "type",
            begin: "!\\w+!" + URI_CHARACTERS
          },
          // https://yaml.org/spec/1.2/spec.html#id2784064
          {
            // verbatim tags
            className: "type",
            begin: "!<" + URI_CHARACTERS + ">"
          },
          {
            // primary tags
            className: "type",
            begin: "!" + URI_CHARACTERS
          },
          {
            // secondary tags
            className: "type",
            begin: "!!" + URI_CHARACTERS
          },
          {
            // fragment id &ref
            className: "meta",
            begin: "&" + hljs.UNDERSCORE_IDENT_RE + "$"
          },
          {
            // fragment reference *ref
            className: "meta",
            begin: "\\*" + hljs.UNDERSCORE_IDENT_RE + "$"
          },
          {
            // array listing
            className: "bullet",
            // TODO: remove |$ hack when we have proper look-ahead support
            begin: "-(?=[ ]|$)",
            relevance: 0
          },
          hljs.HASH_COMMENT_MODE,
          {
            beginKeywords: LITERALS,
            keywords: { literal: LITERALS }
          },
          TIMESTAMP,
          // numbers are any valid C-style number that
          // sit isolated from other words
          {
            className: "number",
            begin: hljs.C_NUMBER_RE + "\\b",
            relevance: 0
          },
          OBJECT,
          ARRAY,
          SINGLE_QUOTE_STRING,
          STRING
        ];
        const VALUE_MODES = [...MODES];
        VALUE_MODES.pop();
        VALUE_MODES.push(CONTAINER_STRING);
        VALUE_CONTAINER.contains = VALUE_MODES;
        return {
          name: "YAML",
          case_insensitive: true,
          aliases: ["yml"],
          contains: MODES
        };
      }
      module.exports = yaml;
    }
  });

  // node_modules/highlight.js/lib/languages/typescript.js
  var require_typescript = __commonJS({
    "node_modules/highlight.js/lib/languages/typescript.js"(exports, module) {
      var IDENT_RE = "[A-Za-z$_][0-9A-Za-z$_]*";
      var KEYWORDS = [
        "as",
        // for exports
        "in",
        "of",
        "if",
        "for",
        "while",
        "finally",
        "var",
        "new",
        "function",
        "do",
        "return",
        "void",
        "else",
        "break",
        "catch",
        "instanceof",
        "with",
        "throw",
        "case",
        "default",
        "try",
        "switch",
        "continue",
        "typeof",
        "delete",
        "let",
        "yield",
        "const",
        "class",
        // JS handles these with a special rule
        // "get",
        // "set",
        "debugger",
        "async",
        "await",
        "static",
        "import",
        "from",
        "export",
        "extends",
        // It's reached stage 3, which is "recommended for implementation":
        "using"
      ];
      var LITERALS = [
        "true",
        "false",
        "null",
        "undefined",
        "NaN",
        "Infinity"
      ];
      var TYPES = [
        // Fundamental objects
        "Object",
        "Function",
        "Boolean",
        "Symbol",
        // numbers and dates
        "Math",
        "Date",
        "Number",
        "BigInt",
        // text
        "String",
        "RegExp",
        // Indexed collections
        "Array",
        "Float32Array",
        "Float64Array",
        "Int8Array",
        "Uint8Array",
        "Uint8ClampedArray",
        "Int16Array",
        "Int32Array",
        "Uint16Array",
        "Uint32Array",
        "BigInt64Array",
        "BigUint64Array",
        // Keyed collections
        "Set",
        "Map",
        "WeakSet",
        "WeakMap",
        // Structured data
        "ArrayBuffer",
        "SharedArrayBuffer",
        "Atomics",
        "DataView",
        "JSON",
        // Control abstraction objects
        "Promise",
        "Generator",
        "GeneratorFunction",
        "AsyncFunction",
        // Reflection
        "Reflect",
        "Proxy",
        // Internationalization
        "Intl",
        // WebAssembly
        "WebAssembly"
      ];
      var ERROR_TYPES = [
        "Error",
        "EvalError",
        "InternalError",
        "RangeError",
        "ReferenceError",
        "SyntaxError",
        "TypeError",
        "URIError"
      ];
      var BUILT_IN_GLOBALS = [
        "setInterval",
        "setTimeout",
        "clearInterval",
        "clearTimeout",
        "require",
        "exports",
        "eval",
        "isFinite",
        "isNaN",
        "parseFloat",
        "parseInt",
        "decodeURI",
        "decodeURIComponent",
        "encodeURI",
        "encodeURIComponent",
        "escape",
        "unescape"
      ];
      var BUILT_IN_VARIABLES = [
        "arguments",
        "this",
        "super",
        "console",
        "window",
        "document",
        "localStorage",
        "sessionStorage",
        "module",
        "global"
        // Node.js
      ];
      var BUILT_INS = [].concat(
        BUILT_IN_GLOBALS,
        TYPES,
        ERROR_TYPES
      );
      function javascript(hljs) {
        const regex = hljs.regex;
        const hasClosingTag = (match, { after }) => {
          const tag = "</" + match[0].slice(1);
          const pos = match.input.indexOf(tag, after);
          return pos !== -1;
        };
        const IDENT_RE$1 = IDENT_RE;
        const FRAGMENT = {
          begin: "<>",
          end: "</>"
        };
        const XML_SELF_CLOSING = /<[A-Za-z0-9\\._:-]+\s*\/>/;
        const XML_TAG = {
          begin: /<[A-Za-z0-9\\._:-]+/,
          end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
          /**
           * @param {RegExpMatchArray} match
           * @param {CallbackResponse} response
           */
          isTrulyOpeningTag: (match, response) => {
            const afterMatchIndex = match[0].length + match.index;
            const nextChar = match.input[afterMatchIndex];
            if (
              // HTML should not include another raw `<` inside a tag
              // nested type?
              // `<Array<Array<number>>`, etc.
              nextChar === "<" || // the , gives away that this is not HTML
              // `<T, A extends keyof T, V>`
              nextChar === ","
            ) {
              response.ignoreMatch();
              return;
            }
            if (nextChar === ">") {
              if (!hasClosingTag(match, { after: afterMatchIndex })) {
                response.ignoreMatch();
              }
            }
            let m;
            const afterMatch = match.input.substring(afterMatchIndex);
            if (m = afterMatch.match(/^\s*=/)) {
              response.ignoreMatch();
              return;
            }
            if (m = afterMatch.match(/^\s+extends\s+/)) {
              if (m.index === 0) {
                response.ignoreMatch();
                return;
              }
            }
          }
        };
        const KEYWORDS$1 = {
          $pattern: IDENT_RE,
          keyword: KEYWORDS,
          literal: LITERALS,
          built_in: BUILT_INS,
          "variable.language": BUILT_IN_VARIABLES
        };
        const decimalDigits = "[0-9](_?[0-9])*";
        const frac = `\\.(${decimalDigits})`;
        const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;
        const NUMBER = {
          className: "number",
          variants: [
            // DecimalLiteral
            { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))[eE][+-]?(${decimalDigits})\\b` },
            { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
            // DecimalBigIntegerLiteral
            { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` },
            // NonDecimalIntegerLiteral
            { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" },
            { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" },
            { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" },
            // LegacyOctalIntegerLiteral (does not include underscore separators)
            // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
            { begin: "\\b0[0-7]+n?\\b" }
          ],
          relevance: 0
        };
        const SUBST = {
          className: "subst",
          begin: "\\$\\{",
          end: "\\}",
          keywords: KEYWORDS$1,
          contains: []
          // defined later
        };
        const HTML_TEMPLATE = {
          begin: ".?html`",
          end: "",
          starts: {
            end: "`",
            returnEnd: false,
            contains: [
              hljs.BACKSLASH_ESCAPE,
              SUBST
            ],
            subLanguage: "xml"
          }
        };
        const CSS_TEMPLATE = {
          begin: ".?css`",
          end: "",
          starts: {
            end: "`",
            returnEnd: false,
            contains: [
              hljs.BACKSLASH_ESCAPE,
              SUBST
            ],
            subLanguage: "css"
          }
        };
        const GRAPHQL_TEMPLATE = {
          begin: ".?gql`",
          end: "",
          starts: {
            end: "`",
            returnEnd: false,
            contains: [
              hljs.BACKSLASH_ESCAPE,
              SUBST
            ],
            subLanguage: "graphql"
          }
        };
        const TEMPLATE_STRING = {
          className: "string",
          begin: "`",
          end: "`",
          contains: [
            hljs.BACKSLASH_ESCAPE,
            SUBST
          ]
        };
        const JSDOC_COMMENT = hljs.COMMENT(
          /\/\*\*(?!\/)/,
          "\\*/",
          {
            relevance: 0,
            contains: [
              {
                begin: "(?=@[A-Za-z]+)",
                relevance: 0,
                contains: [
                  {
                    className: "doctag",
                    begin: "@[A-Za-z]+"
                  },
                  {
                    className: "type",
                    begin: "\\{",
                    end: "\\}",
                    excludeEnd: true,
                    excludeBegin: true,
                    relevance: 0
                  },
                  {
                    className: "variable",
                    begin: IDENT_RE$1 + "(?=\\s*(-)|$)",
                    endsParent: true,
                    relevance: 0
                  },
                  // eat spaces (not newlines) so we can find
                  // types or variables
                  {
                    begin: /(?=[^\n])\s/,
                    relevance: 0
                  }
                ]
              }
            ]
          }
        );
        const COMMENT = {
          className: "comment",
          variants: [
            JSDOC_COMMENT,
            hljs.C_BLOCK_COMMENT_MODE,
            hljs.C_LINE_COMMENT_MODE
          ]
        };
        const SUBST_INTERNALS = [
          hljs.APOS_STRING_MODE,
          hljs.QUOTE_STRING_MODE,
          HTML_TEMPLATE,
          CSS_TEMPLATE,
          GRAPHQL_TEMPLATE,
          TEMPLATE_STRING,
          // Skip numbers when they are part of a variable name
          { match: /\$\d+/ },
          NUMBER
          // This is intentional:
          // See https://github.com/highlightjs/highlight.js/issues/3288
          // hljs.REGEXP_MODE
        ];
        SUBST.contains = SUBST_INTERNALS.concat({
          // we need to pair up {} inside our subst to prevent
          // it from ending too early by matching another }
          begin: /\{/,
          end: /\}/,
          keywords: KEYWORDS$1,
          contains: [
            "self"
          ].concat(SUBST_INTERNALS)
        });
        const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);
        const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([
          // eat recursive parens in sub expressions
          {
            begin: /(\s*)\(/,
            end: /\)/,
            keywords: KEYWORDS$1,
            contains: ["self"].concat(SUBST_AND_COMMENTS)
          }
        ]);
        const PARAMS = {
          className: "params",
          // convert this to negative lookbehind in v12
          begin: /(\s*)\(/,
          // to match the parms with
          end: /\)/,
          excludeBegin: true,
          excludeEnd: true,
          keywords: KEYWORDS$1,
          contains: PARAMS_CONTAINS
        };
        const CLASS_OR_EXTENDS = {
          variants: [
            // class Car extends vehicle
            {
              match: [
                /class/,
                /\s+/,
                IDENT_RE$1,
                /\s+/,
                /extends/,
                /\s+/,
                regex.concat(IDENT_RE$1, "(", regex.concat(/\./, IDENT_RE$1), ")*")
              ],
              scope: {
                1: "keyword",
                3: "title.class",
                5: "keyword",
                7: "title.class.inherited"
              }
            },
            // class Car
            {
              match: [
                /class/,
                /\s+/,
                IDENT_RE$1
              ],
              scope: {
                1: "keyword",
                3: "title.class"
              }
            }
          ]
        };
        const CLASS_REFERENCE = {
          relevance: 0,
          match: regex.either(
            // Hard coded exceptions
            /\bJSON/,
            // Float32Array, OutT
            /\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,
            // CSSFactory, CSSFactoryT
            /\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,
            // FPs, FPsT
            /\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/
            // P
            // single letters are not highlighted
            // BLAH
            // this will be flagged as a UPPER_CASE_CONSTANT instead
          ),
          className: "title.class",
          keywords: {
            _: [
              // se we still get relevance credit for JS library classes
              ...TYPES,
              ...ERROR_TYPES
            ]
          }
        };
        const USE_STRICT = {
          label: "use_strict",
          className: "meta",
          relevance: 10,
          begin: /^\s*['"]use (strict|asm)['"]/
        };
        const FUNCTION_DEFINITION = {
          variants: [
            {
              match: [
                /function/,
                /\s+/,
                IDENT_RE$1,
                /(?=\s*\()/
              ]
            },
            // anonymous function
            {
              match: [
                /function/,
                /\s*(?=\()/
              ]
            }
          ],
          className: {
            1: "keyword",
            3: "title.function"
          },
          label: "func.def",
          contains: [PARAMS],
          illegal: /%/
        };
        const UPPER_CASE_CONSTANT = {
          relevance: 0,
          match: /\b[A-Z][A-Z_0-9]+\b/,
          className: "variable.constant"
        };
        function noneOf(list) {
          return regex.concat("(?!", list.join("|"), ")");
        }
        const FUNCTION_CALL = {
          match: regex.concat(
            /\b/,
            noneOf([
              ...BUILT_IN_GLOBALS,
              "super",
              "import"
            ].map((x) => `${x}\\s*\\(`)),
            IDENT_RE$1,
            regex.lookahead(/\s*\(/)
          ),
          className: "title.function",
          relevance: 0
        };
        const PROPERTY_ACCESS = {
          begin: regex.concat(/\./, regex.lookahead(
            regex.concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/)
          )),
          end: IDENT_RE$1,
          excludeBegin: true,
          keywords: "prototype",
          className: "property",
          relevance: 0
        };
        const GETTER_OR_SETTER = {
          match: [
            /get|set/,
            /\s+/,
            IDENT_RE$1,
            /(?=\()/
          ],
          className: {
            1: "keyword",
            3: "title.function"
          },
          contains: [
            {
              // eat to avoid empty params
              begin: /\(\)/
            },
            PARAMS
          ]
        };
        const FUNC_LEAD_IN_RE = "(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|" + hljs.UNDERSCORE_IDENT_RE + ")\\s*=>";
        const FUNCTION_VARIABLE = {
          match: [
            /const|var|let/,
            /\s+/,
            IDENT_RE$1,
            /\s*/,
            /=\s*/,
            /(async\s*)?/,
            // async is optional
            regex.lookahead(FUNC_LEAD_IN_RE)
          ],
          keywords: "async",
          className: {
            1: "keyword",
            3: "title.function"
          },
          contains: [
            PARAMS
          ]
        };
        return {
          name: "JavaScript",
          aliases: ["js", "jsx", "mjs", "cjs"],
          keywords: KEYWORDS$1,
          // this will be extended by TypeScript
          exports: { PARAMS_CONTAINS, CLASS_REFERENCE },
          illegal: /#(?![$_A-z])/,
          contains: [
            hljs.SHEBANG({
              label: "shebang",
              binary: "node",
              relevance: 5
            }),
            USE_STRICT,
            hljs.APOS_STRING_MODE,
            hljs.QUOTE_STRING_MODE,
            HTML_TEMPLATE,
            CSS_TEMPLATE,
            GRAPHQL_TEMPLATE,
            TEMPLATE_STRING,
            COMMENT,
            // Skip numbers when they are part of a variable name
            { match: /\$\d+/ },
            NUMBER,
            CLASS_REFERENCE,
            {
              scope: "attr",
              match: IDENT_RE$1 + regex.lookahead(":"),
              relevance: 0
            },
            FUNCTION_VARIABLE,
            {
              // "value" container
              begin: "(" + hljs.RE_STARTERS_RE + "|\\b(case|return|throw)\\b)\\s*",
              keywords: "return throw case",
              relevance: 0,
              contains: [
                COMMENT,
                hljs.REGEXP_MODE,
                {
                  className: "function",
                  // we have to count the parens to make sure we actually have the
                  // correct bounding ( ) before the =>.  There could be any number of
                  // sub-expressions inside also surrounded by parens.
                  begin: FUNC_LEAD_IN_RE,
                  returnBegin: true,
                  end: "\\s*=>",
                  contains: [
                    {
                      className: "params",
                      variants: [
                        {
                          begin: hljs.UNDERSCORE_IDENT_RE,
                          relevance: 0
                        },
                        {
                          className: null,
                          begin: /\(\s*\)/,
                          skip: true
                        },
                        {
                          begin: /(\s*)\(/,
                          end: /\)/,
                          excludeBegin: true,
                          excludeEnd: true,
                          keywords: KEYWORDS$1,
                          contains: PARAMS_CONTAINS
                        }
                      ]
                    }
                  ]
                },
                {
                  // could be a comma delimited list of params to a function call
                  begin: /,/,
                  relevance: 0
                },
                {
                  match: /\s+/,
                  relevance: 0
                },
                {
                  // JSX
                  variants: [
                    { begin: FRAGMENT.begin, end: FRAGMENT.end },
                    { match: XML_SELF_CLOSING },
                    {
                      begin: XML_TAG.begin,
                      // we carefully check the opening tag to see if it truly
                      // is a tag and not a false positive
                      "on:begin": XML_TAG.isTrulyOpeningTag,
                      end: XML_TAG.end
                    }
                  ],
                  subLanguage: "xml",
                  contains: [
                    {
                      begin: XML_TAG.begin,
                      end: XML_TAG.end,
                      skip: true,
                      contains: ["self"]
                    }
                  ]
                }
              ]
            },
            FUNCTION_DEFINITION,
            {
              // prevent this from getting swallowed up by function
              // since they appear "function like"
              beginKeywords: "while if switch catch for"
            },
            {
              // we have to count the parens to make sure we actually have the correct
              // bounding ( ).  There could be any number of sub-expressions inside
              // also surrounded by parens.
              begin: "\\b(?!function)" + hljs.UNDERSCORE_IDENT_RE + "\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
              // end parens
              returnBegin: true,
              label: "func.def",
              contains: [
                PARAMS,
                hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1, className: "title.function" })
              ]
            },
            // catch ... so it won't trigger the property rule below
            {
              match: /\.\.\./,
              relevance: 0
            },
            PROPERTY_ACCESS,
            // hack: prevents detection of keywords in some circumstances
            // .keyword()
            // $keyword = x
            {
              match: "\\$" + IDENT_RE$1,
              relevance: 0
            },
            {
              match: [/\bconstructor(?=\s*\()/],
              className: { 1: "title.function" },
              contains: [PARAMS]
            },
            FUNCTION_CALL,
            UPPER_CASE_CONSTANT,
            CLASS_OR_EXTENDS,
            GETTER_OR_SETTER,
            {
              match: /\$[(.]/
              // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`
            }
          ]
        };
      }
      function typescript(hljs) {
        const regex = hljs.regex;
        const tsLanguage = javascript(hljs);
        const IDENT_RE$1 = IDENT_RE;
        const TYPES2 = [
          "any",
          "void",
          "number",
          "boolean",
          "string",
          "object",
          "never",
          "symbol",
          "bigint",
          "unknown"
        ];
        const NAMESPACE = {
          begin: [
            /namespace/,
            /\s+/,
            hljs.IDENT_RE
          ],
          beginScope: {
            1: "keyword",
            3: "title.class"
          }
        };
        const INTERFACE = {
          beginKeywords: "interface",
          end: /\{/,
          excludeEnd: true,
          keywords: {
            keyword: "interface extends",
            built_in: TYPES2
          },
          contains: [tsLanguage.exports.CLASS_REFERENCE]
        };
        const USE_STRICT = {
          className: "meta",
          relevance: 10,
          begin: /^\s*['"]use strict['"]/
        };
        const TS_SPECIFIC_KEYWORDS = [
          "type",
          // "namespace",
          "interface",
          "public",
          "private",
          "protected",
          "implements",
          "declare",
          "abstract",
          "readonly",
          "enum",
          "override",
          "satisfies"
        ];
        const KEYWORDS$1 = {
          $pattern: IDENT_RE,
          keyword: KEYWORDS.concat(TS_SPECIFIC_KEYWORDS),
          literal: LITERALS,
          built_in: BUILT_INS.concat(TYPES2),
          "variable.language": BUILT_IN_VARIABLES
        };
        const DECORATOR = {
          className: "meta",
          begin: "@" + IDENT_RE$1
        };
        const swapMode = (mode, label, replacement) => {
          const indx = mode.contains.findIndex((m) => m.label === label);
          if (indx === -1) {
            throw new Error("can not find mode to replace");
          }
          mode.contains.splice(indx, 1, replacement);
        };
        Object.assign(tsLanguage.keywords, KEYWORDS$1);
        tsLanguage.exports.PARAMS_CONTAINS.push(DECORATOR);
        const ATTRIBUTE_HIGHLIGHT = tsLanguage.contains.find((c) => c.scope === "attr");
        const OPTIONAL_KEY_OR_ARGUMENT = Object.assign(
          {},
          ATTRIBUTE_HIGHLIGHT,
          { match: regex.concat(IDENT_RE$1, regex.lookahead(/\s*\?:/)) }
        );
        tsLanguage.exports.PARAMS_CONTAINS.push([
          tsLanguage.exports.CLASS_REFERENCE,
          // class reference for highlighting the params types
          ATTRIBUTE_HIGHLIGHT,
          // highlight the params key
          OPTIONAL_KEY_OR_ARGUMENT
          // Added for optional property assignment highlighting
        ]);
        tsLanguage.contains = tsLanguage.contains.concat([
          DECORATOR,
          NAMESPACE,
          INTERFACE,
          OPTIONAL_KEY_OR_ARGUMENT
          // Added for optional property assignment highlighting
        ]);
        swapMode(tsLanguage, "shebang", hljs.SHEBANG());
        swapMode(tsLanguage, "use_strict", USE_STRICT);
        const functionDeclaration = tsLanguage.contains.find((m) => m.label === "func.def");
        functionDeclaration.relevance = 0;
        Object.assign(tsLanguage, {
          name: "TypeScript",
          aliases: [
            "ts",
            "tsx",
            "mts",
            "cts"
          ]
        });
        return tsLanguage;
      }
      module.exports = typescript;
    }
  });

  // node_modules/highlight.js/lib/languages/vbnet.js
  var require_vbnet = __commonJS({
    "node_modules/highlight.js/lib/languages/vbnet.js"(exports, module) {
      function vbnet(hljs) {
        const regex = hljs.regex;
        const CHARACTER = {
          className: "string",
          begin: /"(""|[^/n])"C\b/
        };
        const STRING = {
          className: "string",
          begin: /"/,
          end: /"/,
          illegal: /\n/,
          contains: [
            {
              // double quote escape
              begin: /""/
            }
          ]
        };
        const MM_DD_YYYY = /\d{1,2}\/\d{1,2}\/\d{4}/;
        const YYYY_MM_DD = /\d{4}-\d{1,2}-\d{1,2}/;
        const TIME_12H = /(\d|1[012])(:\d+){0,2} *(AM|PM)/;
        const TIME_24H = /\d{1,2}(:\d{1,2}){1,2}/;
        const DATE = {
          className: "literal",
          variants: [
            {
              // #YYYY-MM-DD# (ISO-Date) or #M/D/YYYY# (US-Date)
              begin: regex.concat(/# */, regex.either(YYYY_MM_DD, MM_DD_YYYY), / *#/)
            },
            {
              // #H:mm[:ss]# (24h Time)
              begin: regex.concat(/# */, TIME_24H, / *#/)
            },
            {
              // #h[:mm[:ss]] A# (12h Time)
              begin: regex.concat(/# */, TIME_12H, / *#/)
            },
            {
              // date plus time
              begin: regex.concat(
                /# */,
                regex.either(YYYY_MM_DD, MM_DD_YYYY),
                / +/,
                regex.either(TIME_12H, TIME_24H),
                / *#/
              )
            }
          ]
        };
        const NUMBER = {
          className: "number",
          relevance: 0,
          variants: [
            {
              // Float
              begin: /\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/
            },
            {
              // Integer (base 10)
              begin: /\b\d[\d_]*((U?[SIL])|[%&])?/
            },
            {
              // Integer (base 16)
              begin: /&H[\dA-F_]+((U?[SIL])|[%&])?/
            },
            {
              // Integer (base 8)
              begin: /&O[0-7_]+((U?[SIL])|[%&])?/
            },
            {
              // Integer (base 2)
              begin: /&B[01_]+((U?[SIL])|[%&])?/
            }
          ]
        };
        const LABEL = {
          className: "label",
          begin: /^\w+:/
        };
        const DOC_COMMENT = hljs.COMMENT(/'''/, /$/, { contains: [
          {
            className: "doctag",
            begin: /<\/?/,
            end: />/
          }
        ] });
        const COMMENT = hljs.COMMENT(null, /$/, { variants: [
          { begin: /'/ },
          {
            // TODO: Use multi-class for leading spaces
            begin: /([\t ]|^)REM(?=\s)/
          }
        ] });
        const DIRECTIVES = {
          className: "meta",
          // TODO: Use multi-class for indentation once available
          begin: /[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/,
          end: /$/,
          keywords: { keyword: "const disable else elseif enable end externalsource if region then" },
          contains: [COMMENT]
        };
        return {
          name: "Visual Basic .NET",
          aliases: ["vb"],
          case_insensitive: true,
          classNameAliases: { label: "symbol" },
          keywords: {
            keyword: "addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield",
            built_in: (
              // Operators https://docs.microsoft.com/dotnet/visual-basic/language-reference/operators
              "addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort"
            ),
            type: (
              // Data types https://docs.microsoft.com/dotnet/visual-basic/language-reference/data-types
              "boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort"
            ),
            literal: "true false nothing"
          },
          illegal: "//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",
          contains: [
            CHARACTER,
            STRING,
            DATE,
            NUMBER,
            LABEL,
            DOC_COMMENT,
            COMMENT,
            DIRECTIVES
          ]
        };
      }
      module.exports = vbnet;
    }
  });

  // node_modules/highlight.js/lib/languages/wasm.js
  var require_wasm = __commonJS({
    "node_modules/highlight.js/lib/languages/wasm.js"(exports, module) {
      function wasm(hljs) {
        hljs.regex;
        const BLOCK_COMMENT = hljs.COMMENT(/\(;/, /;\)/);
        BLOCK_COMMENT.contains.push("self");
        const LINE_COMMENT = hljs.COMMENT(/;;/, /$/);
        const KWS = [
          "anyfunc",
          "block",
          "br",
          "br_if",
          "br_table",
          "call",
          "call_indirect",
          "data",
          "drop",
          "elem",
          "else",
          "end",
          "export",
          "func",
          "global.get",
          "global.set",
          "local.get",
          "local.set",
          "local.tee",
          "get_global",
          "get_local",
          "global",
          "if",
          "import",
          "local",
          "loop",
          "memory",
          "memory.grow",
          "memory.size",
          "module",
          "mut",
          "nop",
          "offset",
          "param",
          "result",
          "return",
          "select",
          "set_global",
          "set_local",
          "start",
          "table",
          "tee_local",
          "then",
          "type",
          "unreachable"
        ];
        const FUNCTION_REFERENCE = {
          begin: [
            /(?:func|call|call_indirect)/,
            /\s+/,
            /\$[^\s)]+/
          ],
          className: {
            1: "keyword",
            3: "title.function"
          }
        };
        const ARGUMENT = {
          className: "variable",
          begin: /\$[\w_]+/
        };
        const PARENS = {
          match: /(\((?!;)|\))+/,
          className: "punctuation",
          relevance: 0
        };
        const NUMBER = {
          className: "number",
          relevance: 0,
          // borrowed from Prism, TODO: split out into variants
          match: /[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/
        };
        const TYPE = {
          // look-ahead prevents us from gobbling up opcodes
          match: /(i32|i64|f32|f64)(?!\.)/,
          className: "type"
        };
        const MATH_OPERATIONS = {
          className: "keyword",
          // borrowed from Prism, TODO: split out into variants
          match: /\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/
        };
        const OFFSET_ALIGN = {
          match: [
            /(?:offset|align)/,
            /\s*/,
            /=/
          ],
          className: {
            1: "keyword",
            3: "operator"
          }
        };
        return {
          name: "WebAssembly",
          keywords: {
            $pattern: /[\w.]+/,
            keyword: KWS
          },
          contains: [
            LINE_COMMENT,
            BLOCK_COMMENT,
            OFFSET_ALIGN,
            ARGUMENT,
            PARENS,
            FUNCTION_REFERENCE,
            hljs.QUOTE_STRING_MODE,
            TYPE,
            MATH_OPERATIONS,
            NUMBER
          ]
        };
      }
      module.exports = wasm;
    }
  });

  // node_modules/highlight.js/lib/common.js
  var require_common = __commonJS({
    "node_modules/highlight.js/lib/common.js"(exports, module) {
      var hljs = require_core();
      hljs.registerLanguage("xml", require_xml());
      hljs.registerLanguage("bash", require_bash());
      hljs.registerLanguage("c", require_c());
      hljs.registerLanguage("cpp", require_cpp());
      hljs.registerLanguage("csharp", require_csharp());
      hljs.registerLanguage("css", require_css());
      hljs.registerLanguage("markdown", require_markdown());
      hljs.registerLanguage("diff", require_diff());
      hljs.registerLanguage("ruby", require_ruby());
      hljs.registerLanguage("go", require_go());
      hljs.registerLanguage("graphql", require_graphql());
      hljs.registerLanguage("ini", require_ini());
      hljs.registerLanguage("java", require_java());
      hljs.registerLanguage("javascript", require_javascript());
      hljs.registerLanguage("json", require_json());
      hljs.registerLanguage("kotlin", require_kotlin());
      hljs.registerLanguage("less", require_less());
      hljs.registerLanguage("lua", require_lua());
      hljs.registerLanguage("makefile", require_makefile());
      hljs.registerLanguage("perl", require_perl());
      hljs.registerLanguage("objectivec", require_objectivec());
      hljs.registerLanguage("php", require_php());
      hljs.registerLanguage("php-template", require_php_template());
      hljs.registerLanguage("plaintext", require_plaintext());
      hljs.registerLanguage("python", require_python());
      hljs.registerLanguage("python-repl", require_python_repl());
      hljs.registerLanguage("r", require_r());
      hljs.registerLanguage("rust", require_rust());
      hljs.registerLanguage("scss", require_scss());
      hljs.registerLanguage("shell", require_shell());
      hljs.registerLanguage("sql", require_sql());
      hljs.registerLanguage("swift", require_swift());
      hljs.registerLanguage("yaml", require_yaml());
      hljs.registerLanguage("typescript", require_typescript());
      hljs.registerLanguage("vbnet", require_vbnet());
      hljs.registerLanguage("wasm", require_wasm());
      hljs.HighlightJS = hljs;
      hljs.default = hljs;
      module.exports = hljs;
    }
  });

  // node_modules/highlight.js/es/common.js
  var import_common = __toESM(require_common(), 1);
  var common_default = import_common.default;

  // javascript/build/hljs-entry.js
  window.hljs = common_default;
})();

/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global EventEmitter */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     *
     * @type {{makePayload: Function, onMixin: Function}}
     *
     */
    n.EventEmitter = {
        makePayload: function (target, data, additionalArgs) {
            return [$.extend({},
                typeof additionalArgs === "object" ? additionalArgs : null,
                {
                    target: target,
                    data: data
                }
            )];
        },
        onMixin: function (event, func) {
            this.eventEmitter.addListener(event, func);
        }
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';

    /**
     * Form Group represents a `<input>/<select>` element including the label and help block.
     *
     * It is not meant to be used directly. Use the specialized objects instead.
     *
     * The parameter `elementSelector` seems to be redundant, but `$enclosedElement` can be any element enclosed in
     * the form group, thus we need a way to identify the actual form element(s).
     *
     * @param $enclosedElement {jQuery} a jQuery object contained in the Form Group. It used to find the enclosing
     * HTML element having the `.form-group` class assigned.
     *
     * @param elementSelector {string} jQuery selector selecting the form element. It is used to find all elements
     * contained in the form group identified by $enclosedElement.
     *
     * @constructor
     * @name QfqNS.Element.FormGroup
     */
    n.FormGroup = function ($enclosedElement, elementSelector) {
        elementSelector = elementSelector || 'input:not([type="hidden"])';
        if (!$enclosedElement || $enclosedElement.length === 0) {
            throw new Error("No enclosed element");
        }

        this.$formGroup = this.$findFormGroup($enclosedElement);
        this.$element = this.$formGroup.find(elementSelector);
        this.$label = this.$formGroup.find('.control-label');
        this.$helpBlock = this.$formGroup.find(".help-block");
    };

    /**
     * Test if the Form Group is of the given type
     *
     * @param {string} type type name
     * @returns {boolean} true if the Form Group is of the given type. False otherwise
     * @protected
     */
    n.FormGroup.prototype.isType = function (type) {
        var lowerCaseType = type.toLowerCase();
        var isOfType = false;
        this.$element.each(function () {
            if (this.hasAttribute('type')) {
                if (this.getAttribute('type') === lowerCaseType) {
                    isOfType = true;
                    return true;
                } else {
                    isOfType = false;
                    return false;
                }
            } else {
                // <select> is not an attribute value, obviously, so check for nodename
                if (this.nodeName.toLowerCase() === lowerCaseType) {
                    isOfType = true;
                    return true;
                } else if (lowerCaseType === 'text') {
                    isOfType = true;
                    return true;
                } else {
                    isOfType = false;
                    return false;
                }
            }
        });

        return isOfType;
    };

    /**
     *
     * @param $enclosedElement
     * @returns {*}
     *
     * @private
     */
    n.FormGroup.prototype.$findFormGroup = function ($enclosedElement) {
        var idArray = $enclosedElement.attr('id').split("-");
        var searchString = "#";
        for(var i = 0; i < 8 && i < idArray.length; i++) {
            // Handle form-group for chat input element
            if (idArray[i] === 'chat') {
                continue;
            }
            searchString += idArray[i] + "-";
        }

        var $formGroup = $(searchString + 'i');

        if (!$formGroup || $formGroup.length === 0) {
            $formGroup = $(searchString.replace(/-0-?$/, '-') + 'i')
            if (!$formGroup) {
                console.log("Unable to find Form Group for", $enclosedElement);
                console.log("trying with: " + '#' + $enclosedElement.attr('id') + '-i');
                throw new Error("Unable to find Form Group for " + $enclosedElement.attr('id') + '-i');
            }
        }

        if ($formGroup.length > 1) {
            $formGroup = $(searchString + 'i');
            console.log("Enclosed Element Id: " + $enclosedElement.attr('id'));
            if ($formGroup.length !== 1) {
                throw new Error("enclosed element yields ambiguous form group");
            }
        }
        return $formGroup;
    };

    /**
     * @public
     * @returns {boolean}
     */
    n.FormGroup.prototype.hasLabel = function () {
        return this.$label.length > 0;
    };

    /**
     * @public
     * @returns {boolean}
     */
    n.FormGroup.prototype.hasHelpBlock = function () {
        return this.$helpBlock.length > 0;
    };

    /**
     * @deprecated
     *
     * Read-only is mapped onto setEnabled(). We do not distinguish between those two.
     *
     * @param readonly
     * @public
     */
    n.FormGroup.prototype.setReadOnly = function (readonly) {
        this.setEnabled(!readonly);
    };

    /**
     * @public
     * @param enabled
     */
    n.FormGroup.prototype.setEnabled = function (enabled) {
        this.$element.prop('disabled', !enabled);

        if (enabled) {
            //this.$formGroup.removeClass("text-muted");
            //this.$label.removeClass("disabled");
            this.$element.parents("div.radio").removeClass("disabled");
        } else {
            //this.$formGroup.addClass("text-muted");
            //this.$label.addClass("disabled");
            this.$element.parents("div.radio").addClass("disabled");
        }
    };

    /**
     * @public
     * @param hidden
     */
    n.FormGroup.prototype.setHidden = function (hidden) {
        if (hidden) {
            this.$formGroup.addClass("hidden");
        } else {
            this.$formGroup.removeClass("hidden");
        }
    };

    /**
     * @public
     * @param required
     */
    n.FormGroup.prototype.setRequired = function (required) {
        if(this.$element.is('div')) {
            if (!!this.$element.find('input')) {
                console.log("children found");
                this.$element.find('input').prop('required', required);
            }
        } else {
            this.$element.prop('required', required);
        }
    };

    /**
     * @public
     * @param isError
     */
    n.FormGroup.prototype.setError = function (isError) {
        if (isError) {
            this.$formGroup.addClass("has-error has-danger");
        } else {
            this.$formGroup.removeClass("has-error has-danger");
        }
    };

    n.FormGroup.prototype.setHelp = function (help) {
        if (!this.hasHelpBlock()) {
            return;
        }

        this.$helpBlock.text(help);
    };

    n.FormGroup.prototype.clearHelp = function () {
        if (!this.hasHelpBlock()) {
            return;
        }

        this.$helpBlock.empty();
    };

})(QfqNS.Element);
/**
 * QFQ AI assist: 'improve / generate / translate' actions on text inputs,
 * textareas and TinyMCE editors (FormElement parameter 'aiAssist').
 *
 * Plain inputs/textareas get a small button group rendered next to the element
 * (install()). TinyMCE gets one 'AI' dropdown registered in Helper/tinyMCE.js
 * which calls tinyMceAction().
 *
 * Server side counterpart: Classes/Api/aiHandler.php. The action is streamed over SSE
 * (_ai_stream=1) so a running request can be cancelled for real (the Stop indicator, or
 * closing the tab, aborts the GPU); the result is applied to the field/editor on completion.
 * All security relevant settings (allowed actions, prompt, context SQL, model, offered
 * translate targets) live behind the SIP created at form render time - the client only sends
 * sip, action, target language (validated server side) and the user input.
 */

/* global $ */
/* global fetch, AbortController, TextDecoder */

var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.AiAssist = {};

    n.AiAssist.modeConfig = {
        improve: {
            menuLabel: 'Improve',
            tooltip: 'Improve spelling, grammar and style',
            needsInstruction: false
        },
        generate: {
            menuLabel: 'Generate',
            tooltip: 'Generate text from an instruction',
            needsInstruction: true
        },
        translate: {
            menuLabel: 'Translate',
            tooltip: 'Translate text',
            needsInstruction: false
        }
    };

    /**
     * Short display label for a target language ('German' -> 'DE'). Falls back
     * to the name itself for languages without a known short form.
     */
    n.AiAssist.languageLabel = function (language) {
        var known = {
            english: 'EN',
            german: 'DE',
            french: 'FR',
            italian: 'IT',
            spanish: 'ES'
        };
        return known[String(language).toLowerCase()] || language;
    };

    /**
     * Bind the items of the server rendered 'AI' dropdowns (extra button area,
     * see HelperFormElement::prepareExtraButton). One delegated handler covers
     * everything, including dynamically added elements (templateGroup, dynamic
     * update). TinyMCE elements get their actions via the editor toolbar instead
     * (tinyMCE.js).
     */
    n.AiAssist.install = function () {
        if (n.AiAssist.menuHandlerInstalled) {
            return;
        }
        n.AiAssist.menuHandlerInstalled = true;

        $(document).on('click', '.qfq-ai-dropdown a[data-ai-mode]', function (event) {
            event.preventDefault();
            var $item = $(this);
            var element = document.getElementById(String($item.data('ai-target')));
            if (!element) {
                return;
            }
            var $button = $item.closest('.qfq-ai-dropdown').find('button');
            n.AiAssist.elementAction($(element), String($item.data('ai-mode')),
                String($item.data('ai-language') || ''), $button);
        });

        // The button wrappers live in low stacking contexts (.textarea-button-wrapper
        // has z-index 1, Bootstrap gives following .form-control z-index 2) - an open
        // menu would render below the next form elements. Lift the wrappers while the
        // menu is open.
        $(document).on('shown.bs.dropdown', '.qfq-ai-dropdown', function () {
            $(this).closest('.textarea-button-wrapper, .input-group-btn')
                .add($(this).closest('.textarea-extraButton-wrapper, .input-group'))
                .addClass('qfq-ai-open');
        });
        $(document).on('hidden.bs.dropdown', '.qfq-ai-dropdown', function () {
            $(this).closest('.textarea-button-wrapper, .input-group-btn')
                .add($(this).closest('.textarea-extraButton-wrapper, .input-group'))
                .removeClass('qfq-ai-open');
        });
    };

    /**
     * Dropdown item handler for plain input/textarea elements.
     */
    n.AiAssist.elementAction = function ($element, mode, targetLanguage, $button) {
        // collectInput is async: 'generate' opens an instruction bubble below the
        // AI button (openInstructionBubble); other modes resolve immediately.
        n.AiAssist.collectInput(mode, $element.val(), $button).then(function (input) {
            if (input === null) {
                return;
            }

            // 'generate' passes the current field value as reference material the
            // instruction can work with (e.g. keywords to turn into prose).
            var reference = (mode === 'generate') ? String($element.val()).trim() : '';

            // Busy state: spinner on the button, field locked, and a reasoning box (timer + Stop)
            // shown below the field. The request streams, so Stop cancels for real
            var ctrl = new AbortController();
            // Anchor the box below the whole field block so adding it does not disturb the AI /
            // extra buttons: an inline input wraps field + buttons in .input-group; a textarea
            // wraps them in .textarea-extraButton-wrapper (the buttons sit absolutely inside it).
            // Insert after that container; fall back to the element if neither wrapper is present.
            var $fieldWrap = $element.closest('.textarea-extraButton-wrapper, .input-group');
            var thinking = (String($element.data('ai-thinking')) === '1');
            var $busy = n.AiAssist.showBusy($fieldWrap.length ? $fieldWrap : $element, ctrl,
                thinking ? 'Thinking…' : 'Generating…', thinking);
            var buttonHtml = $button.html();
            $button.prop('disabled', true)
                .html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span>');
            $element.prop('readonly', true).addClass('qfq-ai-busy');

            // Reveal streamed reasoning word by word into the busy box (same smooth effect as the
            // chat thinking), instead of appending each chunk verbatim.
            var reasonReveal = n.AiChat.makeReveal(function (visible) {
                n.AiChat.revealReasoningBody($busy, visible);
            });
            n.AiAssist.request($element.data('ai-url'), $element.data('ai-sip'), mode, input, targetLanguage, reference, 'text', ctrl.signal,
                function (text) {
                    reasonReveal.push(text);
                },
                function (count) {
                    n.AiChat.setReasoningTokens($busy, count);
                })
                .then(function (content) {
                    $element.val(content);
                    // Notify the QFQ form handling (dirty state, save button).
                    $element.trigger('change');
                })
                .catch(function (error) {
                    if (!(error && error.name === 'AbortError')) {
                        n.AiAssist.showError(error);
                    }
                })
                .then(function () {
                    reasonReveal.stop();
                    $busy.remove();
                    $button.prop('disabled', false).html(buttonHtml);
                    $element.prop('readonly', false).removeClass('qfq-ai-busy');
                });
        });
    };

    /**
     * TinyMCE handler (called from the 'AI' toolbar dropdown). Works on the
     * selection if present, else on the whole editor content.
     */
    n.AiAssist.tinyMceAction = function (editor, url, sip, mode, targetLanguage) {
        var selection = editor.selection.getContent({format: 'html'});
        var useSelection = selection !== '';
        var current = useSelection ? selection : editor.getContent();

        // Anchor the instruction bubble (generate) to the editor container, near
        // its top where the AI toolbar button sits ('top' placement).
        var $anchor = $(editor.getContainer());
        n.AiAssist.collectInput(mode, current, $anchor, 'top').then(function (input) {
            if (input === null) {
                return;
            }

            // 'generate' passes the selection resp. editor content as reference
            // material the instruction can work with. The answer replaces exactly
            // that scope (selection or whole content).
            var reference = (mode === 'generate') ? current : '';

            var ctrl = new AbortController();
            var thinking = (String($(editor.getElement()).data('ai-thinking')) === '1');
            var $busy = n.AiAssist.showBusy($anchor, ctrl, thinking ? 'Thinking…' : 'Generating…', thinking);
            editor.setProgressState(true);

            var reasonReveal = n.AiChat.makeReveal(function (visible) {
                n.AiChat.revealReasoningBody($busy, visible);
            });
            n.AiAssist.request(url, sip, mode, input, targetLanguage, reference, 'html', ctrl.signal,
                function (text) {
                    reasonReveal.push(text);
                },
                function (count) {
                    n.AiChat.setReasoningTokens($busy, count);
                })
                .then(function (content) {
                    if (useSelection) {
                        editor.selection.setContent(content);
                    } else {
                        editor.setContent(content);
                    }
                    editor.undoManager.add();
                    // Notify the QFQ form handling (dirty state, save button).
                    editor.fire('change');
                })
                .catch(function (error) {
                    if (!(error && error.name === 'AbortError')) {
                        n.AiAssist.showError(error);
                    }
                })
                .then(function () {
                    reasonReveal.stop();
                    $busy.remove();
                    editor.setProgressState(false);
                });
        });
    };

    /**
     * Determine the text sent to the server. 'generate' asks for an instruction
     * via an animated bubble below the AI button (openInstructionBubble); the other
     * modes require existing text. Returns a Promise resolving with the text, or
     * null to abort.
     */
    n.AiAssist.collectInput = function (mode, currentValue, $anchor, placement) {
        if (n.AiAssist.modeConfig[mode].needsInstruction) {
            return n.AiAssist.openInstructionBubble($anchor, placement);
        }

        if (String(currentValue).trim() === '') {
            n.AiAssist.showError('Please enter a text first.');
            return Promise.resolve(null);
        }
        return Promise.resolve(currentValue);
    };

    /**
     * Animated instruction bubble shown below the AI button (replaces the native
     * window.prompt). Resolves with the entered instruction, or null if the user
     * cancels (Esc, Cancel button, click outside). Only one bubble exists at a time.
     *
     * @param {jQuery} $anchor  element the bubble is positioned against (the AI
     *                 button, or the TinyMCE container with placement 'top').
     * @param {string} [placement]  'top' positions inside the anchor's top area
     *                 (large anchors); default positions directly below the anchor.
     * @returns {Promise<?string>}
     */
    n.AiAssist.openInstructionBubble = function ($anchor, placement) {
        return new Promise(function (resolve) {
            n.AiAssist.closeBubble();

            var $bubble = $(
                '<div class="qfq-ai-bubble" role="dialog" aria-label="Generate text">' +
                    '<span class="qfq-ai-bubble-arrow"></span>' +
                    '<label class="qfq-ai-bubble-label">Describe the text to generate</label>' +
                    '<textarea class="qfq-ai-bubble-input form-control" rows="3" ' +
                        'placeholder="e.g. a friendly two-sentence welcome message"></textarea>' +
                    '<div class="qfq-ai-bubble-actions">' +
                        '<button type="button" class="btn btn-default btn-sm qfq-ai-bubble-cancel">Cancel</button>' +
                        '<button type="button" class="btn btn-info btn-sm qfq-ai-bubble-submit">Generate</button>' +
                    '</div>' +
                '</div>'
            );
            if (placement === 'top') {
                $bubble.addClass('qfq-ai-bubble-noarrow');
            }

            $('body').append($bubble);
            n.AiAssist.$bubble = $bubble;
            n.AiAssist.positionBubble($bubble, $anchor, placement);

            // Animate in on the next frame so the CSS transition actually runs.
            window.requestAnimationFrame(function () {
                $bubble.addClass('qfq-ai-bubble-open');
            });

            var $input = $bubble.find('.qfq-ai-bubble-input');
            $input.trigger('focus');

            var settled = false;
            function finish(value) {
                if (settled) {
                    return;
                }
                settled = true;
                $(document).off('.qfqAiBubble');
                $(window).off('.qfqAiBubble');
                $bubble.removeClass('qfq-ai-bubble-open');
                window.setTimeout(function () {
                    $bubble.remove();
                    if (n.AiAssist.$bubble === $bubble) {
                        n.AiAssist.$bubble = null;
                    }
                }, 150);
                resolve(value);
            }

            function submit() {
                var text = String($input.val()).trim();
                if (text === '') {
                    $input.trigger('focus');
                    return;
                }
                finish(text);
            }

            $bubble.find('.qfq-ai-bubble-submit').on('click', submit);
            $bubble.find('.qfq-ai-bubble-cancel').on('click', function () {
                finish(null);
            });

            // Enter submits, Shift+Enter inserts a newline, Esc cancels.
            $input.on('keydown', function (event) {
                if (event.key === 'Enter' && !event.shiftKey) {
                    event.preventDefault();
                    submit();
                } else if (event.key === 'Escape') {
                    event.preventDefault();
                    finish(null);
                }
            });

            // Click outside closes (cancel). Bind after the current click finished
            // so the dropdown item click that opened us does not close it instantly.
            window.setTimeout(function () {
                $(document).on('mousedown.qfqAiBubble', function (event) {
                    if (!$bubble[0].contains(event.target)) {
                        finish(null);
                    }
                });
            }, 0);

            // Keep the bubble glued to the anchor while scrolling/resizing.
            $(window).on('scroll.qfqAiBubble resize.qfqAiBubble', function () {
                n.AiAssist.positionBubble($bubble, $anchor, placement);
            });
        });
    };

    /**
     * Position the (body-appended) bubble against the anchor. Default: directly
     * below the anchor, right edges aligned (matches the dropdown-menu-right AI
     * button). 'top': inside the anchor's top-left (for the TinyMCE container).
     */
    n.AiAssist.positionBubble = function ($bubble, $anchor, placement) {
        var anchor = ($anchor && $anchor.length) ? $anchor[0] : null;
        if (!anchor) {
            return;
        }
        var rect = anchor.getBoundingClientRect();
        var width = $bubble.outerWidth();
        var top, left;

        if (placement === 'top') {
            top = rect.top + window.pageYOffset + 44;   // ~ below the editor toolbar
            left = rect.left + window.pageXOffset + 8;
        } else {
            top = rect.bottom + window.pageYOffset + 6; // 6px gap below the button
            left = rect.right + window.pageXOffset - width;
        }

        // Keep the bubble within the viewport horizontally.
        var minLeft = window.pageXOffset + 8;
        var maxLeft = window.pageXOffset + document.documentElement.clientWidth - width - 8;
        if (left > maxLeft) {
            left = maxLeft;
        }
        if (left < minLeft) {
            left = minLeft;
        }

        $bubble.css({top: top + 'px', left: left + 'px'});

        // Point the arrow at the anchor's horizontal centre (default placement).
        if (placement !== 'top') {
            var arrowLeft = rect.left + window.pageXOffset + (rect.width / 2) - left - 6;
            arrowLeft = Math.max(10, Math.min(width - 22, arrowLeft));
            $bubble.find('.qfq-ai-bubble-arrow').css('left', arrowLeft + 'px');
        }
    };

    /**
     * Remove any open instruction bubble immediately (no animation).
     */
    n.AiAssist.closeBubble = function () {
        if (n.AiAssist.$bubble) {
            $(document).off('.qfqAiBubble');
            $(window).off('.qfqAiBubble');
            n.AiAssist.$bubble.remove();
            n.AiAssist.$bubble = null;
        }
    };

    /**
     * Stream the AI action from Api/aiHandler.php over SSE (_ai_stream=1), so it can be
     * cancelled for real via the AbortSignal or by closing the tab. Resolves with the final
     * generated text on the 'done' frame; rejects on an 'error' frame, an abort, or a transport
     * error. 'reasoning' deltas (thinking models) -> onReasoning(text); 'tokens' (cumulative
     * usage) -> onTokens(count). Both callbacks are optional.
     */
    n.AiAssist.request = function (url, sip, mode, input, targetLanguage, reference, format, signal, onReasoning, onTokens) {
        var data = {sip: sip, _ai_mode: mode, _ai_input: input, _ai_stream: '1'};
        if (targetLanguage) {
            data._ai_target_language = targetLanguage;
        }
        if (reference) {
            data._ai_reference = reference;
        }
        if (format) {
            data._ai_format = format;
        }
        return fetch(url, {
            method: 'POST',
            credentials: 'same-origin',
            headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
            body: $.param(data),
            signal: signal
        }).then(function (res) {
            if (!res.ok || !res.body) {
                throw new Error('AI request failed (HTTP ' + res.status + ').');
            }
            var reader = res.body.getReader();
            var decoder = new TextDecoder();
            var buf = '', answer = '', errorMsg = null, gotDone = false, genTokens = 0;
            function pump() {
                return reader.read().then(function (r) {
                    if (r.done) {
                        return;
                    }
                    buf += decoder.decode(r.value, {stream: true});
                    var idx = buf.indexOf('\n\n');
                    while (idx >= 0) {
                        var parsed = n.AiChat.parseSseFrame(buf.slice(0, idx));
                        buf = buf.slice(idx + 2);
                        if (parsed) {
                            if (parsed.event === 'content') {
                                answer += (parsed.data.text || '');
                                genTokens++;   // live estimate; the exact 'tokens' event overrides
                                if (onTokens) {
                                    onTokens(genTokens);
                                }
                            } else if (parsed.event === 'reasoning') {
                                if (onReasoning) {
                                    onReasoning(parsed.data.text || '');
                                }
                                genTokens++;
                                if (onTokens) {
                                    onTokens(genTokens);
                                }
                            } else if (parsed.event === 'tokens') {
                                if (onTokens) {
                                    onTokens(parseInt(parsed.data.text, 10) || 0);
                                }
                            } else if (parsed.event === 'done') {
                                gotDone = true;
                                if (typeof parsed.data.content === 'string') {
                                    answer = parsed.data.content;
                                }
                            } else if (parsed.event === 'error') {
                                errorMsg = parsed.data.message || 'AI request failed.';
                            }
                        }
                        idx = buf.indexOf('\n\n');
                    }
                    return pump();
                });
            }
            return pump().then(function () {
                if (errorMsg !== null) {
                    throw errorMsg;
                }
                if (!gotDone) {
                    throw 'AI request ended unexpectedly.';
                }
                return answer;
            });
        });
    };

    /**
     * Show the running indicator below the aiAssist element: the same collapsed reasoning box as
     * the chat (AiChat.buildReasoning) with header "Thinking…", an elapsed timer and a Stop button
     * (far right inside the header) that aborts the request for real. Starts collapsed; the
     * caller expands it and streams reasoning into its body. $anchor is the element / editor
     * container the box is inserted after. Returns the box (jQuery) - remove it when done.
     */
    n.AiAssist.showBusy = function ($anchor, ctrl, label, collapsible) {
        var $box = n.AiChat.buildReasoning(function () {
            ctrl.abort();
        }, label, collapsible).addClass('collapsed qfq-ai-assist-busy');
        $anchor.after($box);
        return $box;
    };

    n.AiAssist.showError = function (error) {
        var message = (error && error.message) ? error.message : String(error);
        if (n.Alert) {
            new n.Alert({message: message, type: 'warning', timeout: 8000}).show();
        } else {
            window.alert(message);
        }
    };

})(QfqNS);

/**
 * QFQ RAG chatbot widget (native FormElement type 'aiChat').
 *
 * The server renders the host element with class 'qfq-ai-chat' + data-ai-sip /
 * data-ai-url (the SIP carries source/project/acl/prompt). This script turns it into a
 * chat box: each question is POSTed to Api/aiHandler.php with mode 'chat' and _ai_stream=1,
 * and the answer is streamed live (reasoning + answer) via Server-Sent Events. The chat always
 * streams so a running request can always be cancelled for real (Stop); it therefore needs a
 * browser with fetch + streaming. The answer + cited sources are rendered. Per-user access
 * control happens server side (the login is taken from the session, never sent by the client).
 *
 * The model output and source content are untrusted (anti-XSS): the answer is
 * rendered through a tiny Markdown subset built from DOM nodes (text() + created
 * elements, never innerHTML); user input, reasoning and source text use .text() directly.
 */

/* global $ */
/* global Promise */
/* global fetch, AbortController, TextDecoder */

var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.AiChat = {};

    // Generation-coupled reveal: every REVEAL_TICK_MS the visible text advances by REVEAL_FRACTION
    // of the not-yet-shown buffer (rounded to whole words). This keeps the word-by-word reveal in
    // step with however fast the model streams
    var REVEAL_TICK_MS = 120;
    var REVEAL_FRACTION = 0.12;

    function isSpace(ch) {
        return ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r' || ch === '\f';
    }
    function nextWordEnd(text, from) {
        var i = from;
        while (i < text.length && isSpace(text.charAt(i))) {   // leading whitespace / newlines
            i++;
        }
        while (i < text.length && !isSpace(text.charAt(i))) {  // the word itself
            i++;
        }
        return i;
    }

    /**
     * Smooth word-by-word reveal of a growing text buffer (turns chunky SSE arrival into a steady
     * stream). push(text) appends received text; it is revealed a fraction at a time (REVEAL_*).
     * render(visible) paints what's revealed so far; flush() shows the rest at once; onCaughtUp(cb)
     * fires once the visible text reaches the buffer end. Shared by the chat (answer + thinking) and
     * aiAssist (thinking). Returns {push, flush, stop, text, onCaughtUp}.
     */
    n.AiChat.makeReveal = function (render, onIdle) {
        var buffer = '', shown = 0, timer = null, paintPending = false, caughtUpCb = null;
        function paint() {
            paintPending = false;
            render(buffer.slice(0, shown));
        }
        function schedule() {           // coalesce a burst of ticks into one repaint per frame
            if (!paintPending) {
                paintPending = true;
                window.requestAnimationFrame(paint);
            }
        }
        function stop() {
            if (timer !== null) {
                window.clearInterval(timer);
                timer = null;
            }
        }
        function tick() {
            if (shown >= buffer.length) {
                stop();                  // caught up with everything received so far
                if (caughtUpCb) {
                    var cb = caughtUpCb;
                    caughtUpCb = null;
                    cb();
                } else if (onIdle) {
                    onIdle();            // caught up, nothing finalizing -> idle (awaiting next chunk)
                }
                return;
            }
            // Advance whole words covering a fraction of the remaining buffer: bigger steps when
            // far behind a fast stream, ~1 word when nearly caught up.
            var target = shown + Math.max(1, Math.round((buffer.length - shown) * REVEAL_FRACTION));
            while (shown < target && shown < buffer.length) {
                shown = nextWordEnd(buffer, shown);
            }
            schedule();
        }
        function start() {
            if (timer === null) {
                timer = window.setInterval(tick, REVEAL_TICK_MS);
            }
        }
        return {
            push: function (text) { buffer += (text || ''); start(); },
            flush: function () {          // reveal the rest at once (idempotent)
                if (shown >= buffer.length && timer === null) {
                    return;
                }
                shown = buffer.length;
                stop();
                schedule();
            },
            stop: stop,
            text: function () { return buffer; },
            onCaughtUp: function (cb) {
                if (shown >= buffer.length) { cb(); } else { caughtUpCb = cb; start(); }
            }
        };
    };

    /** Paint plain text into a reasoning box body, keeping its own scroll pinned to the bottom if it
     *  was already there. Shared by the chat thinking box and the aiAssist busy box. */
    n.AiChat.revealReasoningBody = function ($box, visible) {
        var el = $box.find('.qfq-ai-chat-reasoning-body')[0];
        if (!el) {
            return;
        }
        var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 8;
        el.textContent = visible;
        if (atBottom) {
            el.scrollTop = el.scrollHeight;
        }
    };

    /** Install the chat widget on all '.qfq-ai-chat' host elements (idempotent). */
    n.AiChat.install = function () {
        $('.qfq-ai-chat[data-ai-sip]:not(.ai-chat-installed)').each(function () {
            n.AiChat.build($(this).addClass('ai-chat-installed'));
        });
    };

    n.AiChat.build = function ($host) {
        var url = $host.data('ai-url');
        var sip = $host.data('ai-sip');
        var placeholder = $host.data('ai-chat-placeholder') || 'Ask a question…';
        // The chat ALWAYS streams (so a running request can always be cancelled for real).
        // This needs fetch + ReadableStream + AbortController; without them the chat is not run.
        var canStream = !!(window.fetch && window.ReadableStream && window.AbortController);

        // Conversation transcript for multi-turn follow-ups (sent as _ai_history).
        var history = [];
        // Sources cited so far (with dates), resent so follow-ups can rank the exact set.
        var citedSources = [];
        // Active AbortController while a streaming request is in flight (null otherwise).
        var aborter = null;
        // Per-request 'thinking' toggle (lightbulb), shown only when aiThinking is available.
        var thinkingAvailable = String($host.data('ai-thinking')) === '1';
        var thinkingOn = false, $think = null;

        var $widget = $('<div/>', {'class': 'qfq-ai-chat-widget'});
        var $log = $('<div/>', {'class': 'qfq-ai-chat-log'});
        var $input = $('<textarea/>', {'class': 'form-control qfq-ai-chat-textarea', rows: 1, placeholder: placeholder});
        // Grow the textarea with its content up to the CSS max-height (then it scrolls); no resize handle.
        function autoGrow() {
            $input.css('height', 'auto');
            $input.css('height', $input[0].scrollHeight + 'px');
        }
        $input.on('input', autoGrow);
        var $send = $('<button/>', {type: 'button', 'class': 'btn btn-primary', title: 'Send'}).html('<i class="far fa-paper-plane"></i>');
        var $sendGroup = $('<span/>', {'class': 'input-group-btn'});
        if (thinkingAvailable) {
            // Thinking on/off for this request: default off (btn-default), on = blue (btn-info).
            $think = $('<button/>', {type: 'button', 'class': 'btn btn-default qfq-ai-chat-think', title: 'Thinking'}).html('<i class="far fa-lightbulb"></i>');
            $think.on('click', function () {
                thinkingOn = !thinkingOn;
                $think.toggleClass('btn-info', thinkingOn).toggleClass('btn-default', !thinkingOn);
            });
            $sendGroup.append($think);   // glued left of the Send button
        }
        $sendGroup.append($send);
        var $bar = $('<div/>', {'class': 'input-group qfq-ai-chat-input'})
            .append($input)
            .append($sendGroup);
        $widget.append($log).append($bar);
        // Carry any extra classes the server put on the host (e.g. 'qfq-ai-chat-dock' via the
        // aiChatClass FE parameter) onto the widget, since the host is replaced below.
        var extraClasses = ($host.attr('class') || '').split(/\s+/).filter(function (c) {
            return c && c !== 'qfq-ai-chat' && c !== 'ai-chat-installed';
        });
        if (extraClasses.length) {
            $widget.addClass(extraClasses.join(' '));
        }
        // Dock variant: start collapsed (only the input row shows) with an 'AI' toggle at the far
        // left that opens the chat upward (the widget is anchored at the bottom of the viewport).
        var isDock = extraClasses.indexOf('qfq-ai-chat-dock') !== -1;
        if (isDock) {
            $widget.addClass('qfq-ai-chat-collapsed');
            var $toggle = $('<button/>', {type: 'button', 'class': 'btn btn-info qfq-ai-chat-toggle', title: 'Open / close chat'}).text('AI');
            $bar.prepend($('<span/>', {'class': 'input-group-btn'}).append($toggle));
            $toggle.on('click', function () {
                $widget.toggleClass('qfq-ai-chat-collapsed');
                if (!$widget.hasClass('qfq-ai-chat-collapsed')) {
                    $input.trigger('focus');
                }
            });
        }
        // The host field only carries the chat config and is not meant to be edited —
        // replace it with the widget so nothing overlaps and no empty field box remains.
        $host.replaceWith($widget);

        // Toggle the Send button into a Stop button (and back) while streaming.
        function setStop(on) {
            if (on) {
                $send.html('<i class="far fa-window-close"></i>').attr('title', 'Stop').removeClass('btn-primary').addClass('btn-danger qfq-ai-chat-stop').prop('disabled', false);
            } else {
                $send.html('<i class="far fa-paper-plane"></i>').attr('title', 'Send').removeClass('btn-danger qfq-ai-chat-stop').addClass('btn-primary').prop('disabled', false);
            }
        }
        function lockInput() {
            $input.val('').prop('disabled', true);
            $input.css('height', 'auto');   // shrink back to a single row after sending
        }
        function unlockInput() {
            $input.prop('disabled', false).trigger('focus');
        }
        function rememberTurn(q, answer, sources) {
            history.push({role: 'user', content: q}, {role: 'assistant', content: answer});
            if (history.length > 16) {
                history = history.slice(-16);
            }
            n.AiChat.mergeSources(citedSources, sources);
            if (citedSources.length > 40) {
                citedSources = citedSources.slice(-40);
            }
        }
        function requestBody(q) {
            return {
                sip: sip,
                _ai_mode: 'chat',
                _ai_input: q,
                _ai_history: JSON.stringify(history.slice(-8)),
                _ai_sources: JSON.stringify(citedSources.slice(-40)),
                _ai_thinking: thinkingOn ? '1' : '0'
            };
        }

        // The only chat path: SSE from aiHandler.php (_ai_stream=1). Reasoning + answer render
        // live; the Stop button aborts the fetch -> connection closes -> upstream generation is
        // aborted (REAL cancel). There is deliberately no non-streaming fallback for chat,
        // because a blocking request can only be cancelled cosmetically (the GPU keeps running).
        function askStream(q, $pending) {
            aborter = new AbortController();
            setStop(true);
            var $bubble = null, $reason = null, firstToken = false, genTokens = 0;
            // 'finalizing' is set on 'done' so the final .then() lets the answer reveal drain to the
            // end instead of cutting it off (see makeReveal / finalizeWhenCaughtUp below).
            var finalizing = false;
            // Capture the thinking toggle at send time: the lightbulb can be toggled mid-stream, but
            // the box must match what was requested (reasoning only streams when it was on at send).
            var thinking = thinkingOn;

            function dropPending() {
                if (!firstToken) {
                    firstToken = true;
                    $pending.remove();
                }
            }
            function logAtBottom() {
                // Within ~40px of the bottom counts as "following the newest".
                return $log[0].scrollHeight - $log[0].scrollTop - $log[0].clientHeight < 40;
            }
            // Two smooth reveals via the shared n.AiChat.makeReveal: the answer (rendered as Markdown
            // into the streaming bubble) and the reasoning/thinking stream (plain text into the box).
            var answerReveal = n.AiChat.makeReveal(function (visible) {
                if (!$bubble || !$bubble.closest('html').length) {
                    return;   // bubble already swapped for the final rendered answer (done/error)
                }
                var follow = logAtBottom();
                $bubble.empty();
                n.AiChat.renderMarkdown($bubble, visible);
                if (follow) {
                    $log.scrollTop($log[0].scrollHeight);
                }
            }, function () {
                // Caught up but the stream is still open: show "typing" dots after the last word
                // until the next chunk arrives (bridges the small inter-chunk gap).
                if ($bubble && $bubble.closest('html').length) {
                    $bubble.addClass('qfq-ai-chat-waiting');
                    if (logAtBottom()) {
                        $log.scrollTop($log[0].scrollHeight);
                    }
                }
            });
            var reasonReveal = n.AiChat.makeReveal(function (visible) {
                if (!$reason) {
                    return;
                }
                var logFollow = logAtBottom();
                n.AiChat.revealReasoningBody($reason, visible);   // box keeps its own internal scroll
                if (logFollow) {
                    $log.scrollTop($log[0].scrollHeight);   // and the whole log follows the newest
                }
            });
            function doFinalize(data) {
                answerReveal.stop();
                if ($bubble) {
                    $bubble.remove();   // swap the live answer for the cleaned, server-finalized one (SOURCES stripped, + sources + MathJax)
                    $bubble = null;
                }
                n.AiChat.collapseReasoning($reason);
                n.AiChat.appendMessage($log, 'bot', data.content, data.sources);
                rememberTurn(q, data.content || answerReveal.text(), data.sources);
            }
            function finalizeWhenCaughtUp(data) {
                finalizing = true;
                reasonReveal.flush();   // thinking is over -> reveal any remaining reasoning at once
                answerReveal.onCaughtUp(function () { doFinalize(data); });
            }
            function onEvent(name, data) {
                if (name === 'reasoning') {
                    dropPending();
                    if (!$reason) {
                        $reason = n.AiChat.appendReasoning($log);
                    }
                    reasonReveal.push(data.text || '');   // smooth word-by-word reveal into the thinking box
                    genTokens++;   // live estimate (1 delta ≈ 1 token); the exact 'tokens' event overrides
                    n.AiChat.setReasoningTokens($reason, genTokens);
                } else if (name === 'content') {
                    dropPending();
                    if (!$reason) {
                        // Thinking off has no reasoning stream, but tokens + timer still matter: show
                        // them as a plain non-expandable 'Generating…' row above the answer. (On: the
                        // box already exists, created by the 'reasoning' branch.)
                        $reason = thinking ? n.AiChat.appendReasoning($log) : n.AiChat.appendGenerating($log);
                    } else {
                        reasonReveal.flush();   // first answer token -> reveal any remaining thinking at once
                    }
                    if (!$bubble) {
                        $bubble = n.AiChat.appendStreamingBubble($log);
                    }
                    $bubble.removeClass('qfq-ai-chat-waiting');   // new chunk -> hide the waiting dots
                    answerReveal.push(data.text || '');   // smooth reveal, paced to the stream
                    genTokens++;   // keep counting through the answer
                    n.AiChat.setReasoningTokens($reason, genTokens);
                } else if (name === 'tokens') {
                    n.AiChat.setReasoningTokens($reason, parseInt(data.text, 10) || 0);
                } else if (name === 'done') {
                    dropPending();
                    finalizeWhenCaughtUp(data);   // let the reveal drain the buffer, then swap in the final bubble
                } else if (name === 'error') {
                    dropPending();
                    answerReveal.stop();
                    reasonReveal.stop();
                    if ($bubble) {
                        $bubble.remove();
                    }
                    n.AiChat.appendMessage($log, 'bot', data.message || 'Chat request failed.', null, true);
                }
            }

            var params = requestBody(q);
            params._ai_stream = '1';
            fetch(url, {
                method: 'POST',
                credentials: 'same-origin',   // send the session cookie (SIP lookup needs the session)
                headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
                body: $.param(params),
                signal: aborter.signal
            }).then(function (res) {
                if (!res.ok || !res.body) {
                    throw new Error('Chat request failed (HTTP ' + res.status + ').');
                }
                var reader = res.body.getReader();
                var decoder = new TextDecoder();
                var buf = '';
                function pump() {
                    return reader.read().then(function (r) {
                        if (r.done) {
                            return;
                        }
                        buf += decoder.decode(r.value, {stream: true});
                        var idx = buf.indexOf('\n\n');
                        while (idx >= 0) {
                            var parsed = n.AiChat.parseSseFrame(buf.slice(0, idx));
                            buf = buf.slice(idx + 2);
                            if (parsed) {
                                onEvent(parsed.event, parsed.data);
                            }
                            idx = buf.indexOf('\n\n');
                        }
                        return pump();
                    });
                }
                return pump();
            }).catch(function (err) {
                dropPending();
                answerReveal.stop();
                reasonReveal.stop();
                if ($bubble) {
                    $bubble.remove();
                }
                n.AiChat.collapseReasoning($reason);
                if (err && err.name === 'AbortError') {
                    n.AiChat.appendMessage($log, 'bot', 'Cancelled.', null, true);
                } else {
                    n.AiChat.appendMessage($log, 'bot', (err && err.message) ? err.message : String(err), null, true);
                }
            }).then(function () {
                if (!finalizing) {
                    answerReveal.stop();   // no graceful drain pending (error / abort) -> stop now
                    reasonReveal.stop();
                }
                aborter = null;
                setStop(false);
                unlockInput();
            });
        }

        function ask() {
            var q = $.trim($input.val());
            if (q === '') {
                return;
            }
            // The chat always streams so a running request can always be cancelled for real.
            // Without streaming support there is no cancellable path, so the chat is not run.
            if (!canStream) {
                n.AiChat.appendMessage($log, 'bot', 'This browser is too old for the AI chat (needs fetch + streaming).', null, true);
                return;
            }
            $widget.removeClass('qfq-ai-chat-collapsed');   // open the dock so the answer is visible
            n.AiChat.appendMessage($log, 'user', q);
            lockInput();
            askStream(q, n.AiChat.appendTyping($log));
        }

        // While a stream is in flight the Send button acts as Stop.
        $send.on('click', function () {
            if (aborter) {
                aborter.abort();
            } else {
                ask();
            }
        });
        $input.on('keydown', function (e) {
            // Enter submits; Shift+Enter inserts a newline (textarea default).
            if (e.which === 13 && !e.shiftKey) {
                e.preventDefault();
                if (!aborter) {
                    ask();
                }
            }
        });
    };

    /** Append a message row. text() only (no HTML) for untrusted model/source content. */
    n.AiChat.appendMessage = function ($log, who, text, sources, isError) {
        var cls = 'qfq-ai-chat-msg qfq-ai-chat-' + who + (isError ? ' qfq-ai-chat-error' : '');
        var $m = $('<div/>', {'class': cls});
        // Bot answers may contain Markdown -> render the safe subset. User input and
        // error strings are shown verbatim via .text() (no Markdown interpretation).
        if (who === 'bot' && !isError) {
            n.AiChat.renderMarkdown($m, text);
        } else {
            $m.text(text);
        }
        // Semi-transparent copy-to-clipboard button on bot answers and user prompts (not errors).
        if (!isError) {
            n.AiChat.addCopyButton($m, text);
        }

        if (sources && sources.length) {
            var $src = $('<div/>', {'class': 'qfq-ai-chat-sources'});
            $src.append(document.createTextNode('Sources: '));
            sources.forEach(function (s, i) {
                if (i) {
                    $src.append(document.createTextNode(', '));
                }
                var date = s.updated_on ? ' (' + s.updated_on.substring(0, 10) + ')' : '';
                var label = '#' + s.external_id + date + (s.title ? ' ' + s.title : '');
                if (s.url) {
                    $('<a/>', {href: s.url, target: '_blank', rel: 'noopener'}).text(label).appendTo($src);
                } else {
                    $src.append(document.createTextNode(label));
                }
            });
            $m.append($src);
        }

        $log.append($m);
        // If the optional MathJax library is loaded, typeset LaTeX math ($...$, \(...\)) in the
        // finalized bot answer.
        if (who === 'bot' && !isError) {
            n.AiChat.typeset($m);
        }
        $log.scrollTop($log[0].scrollHeight);
        return $m;
    };

    /** Add a semi-transparent copy-to-clipboard button to the top-right of a bot answer. Copies the
     *  raw answer text (Markdown source) passed in - NOT the DOM, so the sources line and the button
     *  itself are never part of what gets copied. */
    n.AiChat.addCopyButton = function ($m, text) {
        var payload = (text === null || text === undefined) ? '' : String(text);
        var $btn = $('<button/>', {
            type: 'button',
            'class': 'qfq-ai-chat-copy',
            title: 'Copy',
            'aria-label': 'Copy'
        }).html('<i class="fas fa-clipboard"></i>');
        $btn.on('click', function (e) {
            e.stopPropagation();
            n.AiChat.copyText(payload, $btn);
        });
        $m.append($btn);
    };

    /** Copy `text` to the clipboard (async Clipboard API, with a legacy execCommand fallback for
     *  insecure contexts / old browsers) and flash a short check/cross confirmation on $btn. */
    n.AiChat.copyText = function (text, $btn) {
        function flash(ok) {
            var $i = $btn.find('i');
            var prev = $i.attr('class');
            $i.attr('class', ok ? 'fas fa-check' : 'fas fa-times');
            $btn.toggleClass('qfq-ai-chat-copied', ok);
            window.setTimeout(function () {
                $i.attr('class', prev);
                $btn.removeClass('qfq-ai-chat-copied');
            }, 1200);
        }
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(text).then(function () {
                flash(true);
            }, function () {
                flash(n.AiChat.legacyCopy(text));
            });
        } else {
            flash(n.AiChat.legacyCopy(text));
        }
    };

    /** Legacy clipboard fallback: copy via a hidden, off-screen textarea + execCommand('copy').
     *  Returns true on success. */
    n.AiChat.legacyCopy = function (text) {
        var ta = document.createElement('textarea');
        ta.value = text;
        ta.setAttribute('readonly', '');
        ta.style.position = 'absolute';
        ta.style.left = '-9999px';
        document.body.appendChild(ta);
        ta.select();
        var ok = false;
        try {
            ok = document.execCommand('copy');
        } catch (e) {
            ok = false;
        }
        document.body.removeChild(ta);
        return ok;
    };

    /**
     * Typeset LaTeX math ($...$, \(...\)) inside $el via MathJax - but only if the MathJax
     * library is actually loaded. MathJax is OPTIONAL in QFQ
     */
    n.AiChat.typeset = function ($el) {
        var mj = window.MathJax;
        if (!$el || !$el.length || !mj || typeof mj.typesetPromise !== 'function') {
            return;
        }
        mj.typesetPromise([$el[0]]).catch(function () {});
    };

    /** Merge new sources into the running list, dedup by external_id (latest wins). */
    n.AiChat.mergeSources = function (list, sources) {
        if (!sources || !sources.length) {
            return;
        }
        sources.forEach(function (s) {
            var i, found = false;
            for (i = 0; i < list.length; i++) {
                if (list[i].external_id === s.external_id) {
                    list[i] = s;
                    found = true;
                    break;
                }
            }
            if (!found) {
                list.push(s);
            }
        });
    };

    /** Tick an elapsed-seconds counter ("12s" then "1:05") into $timer once per second until it
     *  leaves the DOM. Returns the interval id so a caller can stop it explicitly for a box that
     *  stays in the DOM (e.g. the collapsed reasoning box keeps its final time). */
    n.AiChat.startElapsed = function ($timer) {
        var start = Date.now();
        var id = window.setInterval(function () {
            if (!$timer.closest('html').length) {
                window.clearInterval(id);
                return;
            }
            var s = Math.floor((Date.now() - start) / 1000);
            $timer.text(s < 60 ? s + 's' : Math.floor(s / 60) + ':' + ('0' + (s % 60)).slice(-2));
        }, 1000);
        return id;
    };

    /** Append an animated "typing" indicator (three dots bouncing one after another) with an
     *  elapsed timer to its right. Returns the row so it can be removed when the answer arrives. */
    n.AiChat.appendTyping = function ($log) {
        var $m = $('<div/>', {'class': 'qfq-ai-chat-msg qfq-ai-chat-bot qfq-ai-chat-typing'});
        var $dots = $('<span/>', {'class': 'qfq-ai-chat-dots'});
        var i;
        for (i = 0; i < 3; i++) {
            $dots.append($('<span/>', {'class': 'qfq-ai-chat-dot'}));
        }
        $m.append($dots);
        var $timer = $('<span/>', {'class': 'qfq-ai-chat-timer', text: '0s'});
        $m.append($timer);
        n.AiChat.startElapsed($timer);   // self-stops once the row is removed (answer or error)
        $log.append($m);
        $log.scrollTop($log[0].scrollHeight);
        return $m;
    };

    /** Parse one SSE frame ("event: x\ndata: {...}") into {event, data}, or null. */
    n.AiChat.parseSseFrame = function (frame) {
        var lines = frame.split('\n');
        var ev = 'message', dataStr = '';
        var i, ln;
        for (i = 0; i < lines.length; i++) {
            ln = lines[i];
            if (ln.charAt(0) === ':') {
                continue;   // SSE comment line
            }
            if (ln.indexOf('event:') === 0) {
                ev = $.trim(ln.slice(6));
            } else if (ln.indexOf('data:') === 0) {
                dataStr += (dataStr ? '\n' : '') + ln.slice(5).replace(/^ /, '');
            }
        }
        if (dataStr === '') {
            return null;
        }
        try {
            return {event: ev, data: JSON.parse(dataStr)};
        } catch (e) {
            return null;
        }
    };

    /** Build a collapsible reasoning box: a header with a label, an elapsed timer (pushed right)
     *  and an optional Stop button, plus a body for the streamed reasoning text. Reused by the
     *  chat (no Stop - the Send button stops) and by aiAssist (Stop aborts the request). The
     *  timer ticks until collapseReasoning() or removal. Returns the wrapper. */
    n.AiChat.buildReasoning = function (onStop, label, collapsible) {
        var $wrap = $('<div/>', {'class': 'qfq-ai-chat-reasoning'});
        var $head = $('<div/>', {'class': 'qfq-ai-chat-reasoning-head'});
        $head.append($('<span/>', {'class': 'qfq-ai-chat-reasoning-label', text: label || 'Thinking…'}));
        $head.append($('<span/>', {'class': 'qfq-ai-chat-reasoning-tokens', title: 'Tokens'}));
        $head.append($('<span/>', {'class': 'qfq-ai-chat-reasoning-timer', text: '0s'}));
        if (typeof onStop === 'function') {
            $('<button/>', {type: 'button', 'class': 'qfq-ai-chat-reasoning-stop', text: 'Stop', title: 'Stop'})
                .on('click', function (e) {
                    e.stopPropagation();   // don't toggle collapse
                    onStop();
                }).appendTo($head);
        }
        if (collapsible === false) {
            // Nothing to expand (e.g. thinking off) -> header not clickable; Stop still works.
            $wrap.addClass('qfq-ai-no-toggle');
        } else {
            $head.on('click', function () {
                $wrap.toggleClass('collapsed');   // click the header to collapse/expand
            });
        }
        $wrap.append($head).append($('<div/>', {'class': 'qfq-ai-chat-reasoning-body'}));
        $wrap.data('qfqTimerId', n.AiChat.startElapsed($wrap.find('.qfq-ai-chat-reasoning-timer')));
        return $wrap;
    };

    /** Collapsible "Thinking…" panel for the model's live reasoning stream (chat); on 'done' it
     *  collapses and the label becomes "Reasoning". Returns the wrapper. Scrolling is left to the
     *  caller (askStream follows the newest while the user is at the bottom). */
    n.AiChat.appendReasoning = function ($log) {
        var $wrap = n.AiChat.buildReasoning(null).data('qfqDoneLabel', 'Reasoning');
        $log.append($wrap);
        return $wrap;
    };

    /** Plain status row for the thinking-off case: "Generating…" + token count + timer, but NOT
     *  expandable (no triangle, not clickable) and with no reasoning body. */
    n.AiChat.appendGenerating = function ($log) {
        var $wrap = n.AiChat.buildReasoning(null, 'Generating…', false)
            .addClass('collapsed').data('qfqDoneLabel', 'Generated');
        $log.append($wrap);
        return $wrap;
    };

    /** Format a token count for display: "123" or "1.5k" (>= 1000). */
    n.AiChat.formatTokens = function (count) {
        return count < 1000 ? String(count) : (count / 1000).toFixed(1) + 'k';
    };

    /** Show the (accumulating) token count in parentheses, left of the timer, in a reasoning box. */
    n.AiChat.setReasoningTokens = function ($wrap, count) {
        if ($wrap && $wrap.length && count > 0) {
            $wrap.find('.qfq-ai-chat-reasoning-tokens').text('(' + n.AiChat.formatTokens(count) + ')');
        }
    };

    /** Append reasoning text (plain, text() only) and follow the newest - but only while the user
     *  is already at the bottom of the panel. If they scrolled up to re-read we leave their
     *  position alone, and resume following once they scroll back down. */
    n.AiChat.appendReasoningText = function ($wrap, text) {
        var el = $wrap.find('.qfq-ai-chat-reasoning-body')[0];
        var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 8;   // tolerance for sub-pixels
        el.appendChild(document.createTextNode(text));
        if (atBottom) {
            el.scrollTop = el.scrollHeight;
        }
    };

    /** Finalize the status box once the answer is final: freeze its timer and switch the label to its
     *  done text - "Reasoning" for the thinking box, "Generated" for the thinking-off box (stored as
     *  qfqDoneLabel). The thinking box also collapses (hides its body); the non-expandable Generating
     *  box is already collapsed. No-op if there was none. */
    n.AiChat.collapseReasoning = function ($wrap) {
        if ($wrap && $wrap.length) {
            window.clearInterval($wrap.data('qfqTimerId'));
            $wrap.addClass('collapsed');
            $wrap.find('.qfq-ai-chat-reasoning-label').text($wrap.data('qfqDoneLabel') || 'Reasoning');
        }
    };

    /** Bot bubble holding the live answer (rendered Markdown) while it streams in; replaced on
     *  'done' by the cleaned, server-finalized message (+ sources + MathJax). */
    n.AiChat.appendStreamingBubble = function ($log) {
        var $m = $('<div/>', {'class': 'qfq-ai-chat-msg qfq-ai-chat-bot qfq-ai-chat-streaming'});
        $log.append($m);
        return $m;   // scrolling handled by the caller (gated once the thinking box has opened)
    };

    /**
     * Render a tiny, safe Markdown subset into $container: bold, inline code, bullet
     * lists ('-', '*', '+') and paragraphs. DOM only - never innerHTML, so untrusted
     * model output cannot inject markup.
     */
    n.AiChat.renderMarkdown = function ($container, text) {
        var raw = (text === null || text === undefined) ? '' : String(text);
        var lines = raw.replace(/\r\n?/g, '\n').split('\n');
        var bulletRe = /^\s*[-*+]\s+(.*)$/;
        var i = 0;
        while (i < lines.length) {
            if (bulletRe.test(lines[i])) {
                var $ul = $('<ul/>', {'class': 'qfq-ai-chat-md-list'});
                while (i < lines.length) {
                    var m = lines[i].match(bulletRe);
                    if (!m) {
                        break;
                    }
                    n.AiChat.renderInline($('<li/>').appendTo($ul), m[1]);
                    i++;
                }
                $container.append($ul);
                continue;
            }
            if ($.trim(lines[i]) === '') {
                i++;
                continue;
            }
            n.AiChat.renderInline($('<div/>', {'class': 'qfq-ai-chat-md-p'}).appendTo($container), lines[i]);
            i++;
        }
        // Never leave the bubble empty (e.g. answer was only blank lines).
        if ($container.children().length === 0) {
            $container.text(raw);
        }
    };

    /** Inline render: **bold** and `code`; everything else as plain text nodes. */
    n.AiChat.renderInline = function ($el, text) {
        var re = /\*\*([^*]+)\*\*|`([^`]+)`/g;
        var last = 0;
        var match = re.exec(text);
        while (match !== null) {
            if (match.index > last) {
                $el.append(document.createTextNode(text.slice(last, match.index)));
            }
            if (match[1] !== undefined) {
                $el.append($('<strong/>').text(match[1]));
            } else {
                $el.append($('<code/>').text(match[2]));
            }
            last = re.lastIndex;
            match = re.exec(text);
        }
        if (last < text.length) {
            $el.append(document.createTextNode(text.slice(last)));
        }
    };

    // Self-install on DOM ready so the chat widget also works outside a form - e.g. rendered in a
    // Report via the _input special column
    $(function () {
        n.AiChat.install();
    });

})(QfqNS);

/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend AlertManager.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Display a message.
     *
     * A typical call sequence might look like:
     *
     *     var alert = new QfqNS.Alert({
     *       message: "Text being displayed",
     *       type: "info"
     *     });
     *     alert.show();
     *
     * Messages may have different background colors (severity levels), controlled by the `type` property. Possible
     * values are
     *
     *  * `"success"`
     *  * `"info"`
     *  * `"warning"`
     *  * `"error"`, `"danger"`
     *
     * The values are translated into Bootstrap `alert-*` classes internally.
     *
     * If no buttons are configured, a click anywhere on the alert will close it.
     *
     * Buttons are configured by passing an array of objects in the `buttons` property. The properties of the object
     * are as follows
     *
     *     {
     *       label: <button label>,
     *       focus: true | false,
     *       eventName: <eventname>
     *     }
     *
     * You can connect to the button events by using
     *
     *     var alert = new QfqNS.Alert({
     *        message: "Text being displayed",
     *        type: "info",
     *        buttons: [
     *             { label: "OK", eventName: "ok" }
     *        ]
     *     });
     *     alert.on('alert.ok', function(...) { ... });
     *
     * Events are named according to `alert.<eventname>`.
     *
     * If the property `modal` is set to `true`, a kind-of modal alert will be displayed, preventing clicks
     * anywhere but the alert.
     *
     * For compatibility reasons, the old constructor signature is still supported but deprecated
     *
     *      var alert = new QfqNS.Alert(message, type, buttons)
     *
     * @param {object} options option object has following properties
     * @param {string} options.message message to be displayed
     * @param {string} [options.type] type of message, can be `"info"`, `"warning"`, or `"error"`. Default is `"info"`.
     * @param {number} [options.timeout] timeout in milliseconds. If timeout is less than or equal to 0, the alert
     * won't timeout and stay open until dismissed by the user. Default `n.Alert.constants.NO_TIMEOUT`.
     * @param {boolean} [options.modal] whether or not alert is modal, i.e. prevent clicks anywhere but the dialog.
     * Default is `false`.
     * @param {object[]} options.buttons what buttons to display on alert. If empty array is provided, no buttons are
     * displayed and a click anywhere in the alert will dismiss it.
     * @param {string} options.buttons.label label of the button
     * @param {string} options.buttons.eventName name of the event when button is clicked.
     * @param {boolean} [options.buttons.focus] whether or not button has focus by default. Default is `false`.
     *
     * @constructor
     */
    n.Alert = function (options) {
        // Emulate old behavior of method signature
        //  function(message, messageType, buttons)
        if (typeof options === "string") {
            this.message = arguments[0];
            this.messageType = arguments[1] || "info";
            this.buttons = arguments[2] || [];
            this.modal = false;
            // this.timeout < 1 means forever
            this.timeout = n.Alert.constants.NO_TIMEOUT;
        } else {
            // new style
            this.message = options.message || "MESSAGE";
            this.messageType = options.type || "info";
            this.messageTitle = options.title || false;
            this.errorCode = options.code || false;
            this.buttons = options.buttons || [];
            this.modal = options.modal || false;
            this.timeout = options.timeout || n.Alert.constants.NO_TIMEOUT;
        }

        this.$alertDiv = null;
        this.$modalDiv = null;
        this.shown = false;

        this.fadeInDuration = 400;
        this.fadeOutDuration = 400;
        this.timerId = null;
        this.parent = {};
        this.identifier = false;

        this.eventEmitter = new EventEmitter();

        if (this.message.indexOf('qfq-debug-detail') >= 0) {
            this.buttons.push({ label: 'Debug info', eventName: 'show-debug' });
        setTimeout(function() {
            this.on('alert.show-debug', function() {
                $('.qfq-debug-detail').toggleClass('qfq-alert-hidden');
            });
        }.bind(this), 100);
        }
    };

    n.Alert.prototype.on = n.EventEmitter.onMixin;
    n.Alert.constants = {
        alertContainerId: "alert-interactive",
        alertContainerSelector: "#qfqAlertContainer",
        jQueryAlertRemoveEventName: "qfqalert.remove:",
        NO_TIMEOUT: 0
    };

    /**
     *
     * @private
     */
    n.Alert.prototype.makeAlertContainerSingleton = function () {
        if (!n.QfqPage.alertManager) {
            n.QfqPage.alertManager = new n.AlertManager({});
        }

        return n.QfqPage.alertManager;
    };

    n.Alert.prototype.setIdentifier = function (i) {
        this.identifier = i;
    };

    /**
     *
     * @returns {number|jQuery}
     * @private
     */
    n.Alert.prototype.countAlertsInAlertContainer = function () {
        return $(n.Alert.constants.alertContainerSelector + " > div").length;
    };

    /**
     * @private
     */
    n.Alert.prototype.removeAlertContainer = function () {
        if (this.modal) {
            this.shown = false;
            this.parent.removeModalAlert();
        }
    };

    /**
     * @private
     */
    n.Alert.prototype.getAlertClassBasedOnMessageType = function () {
        switch (this.messageType) {
            case "warning":
                return "border-warning";
            case "danger":
            case "error":
                return "border-error";
            case "info":
                return "border-info";
            case "success":
                return "border-success";
            /* jshint -W086 */
            default:
                n.Log.warning("Message type '" + this.messageType + "' unknown. Use default type.");
            /* jshint +W086 */
        }
    };

    /**
     * @private
     */
    n.Alert.prototype.getButtons = function () {
        var $buttons = null;
        var $container = $("<p>").addClass("buttons");
        var numberOfButtons = this.buttons.length;
        var index;
        var buttonConfiguration;

        for (index = 0; index < numberOfButtons; index++) {
            buttonConfiguration = this.buttons[index];

            if (!$buttons) {
                if (numberOfButtons > 1) {
                    $buttons = $("<div>").addClass("btn-group");
                } else {
                    $buttons = $container;
                }
            }

            var focus = buttonConfiguration.focus ? buttonConfiguration.focus : false;

            $buttons.append($("<button>").append(buttonConfiguration.label)
                .attr('type', 'button')
                .addClass("btn btn-default" + (focus ? " wants-focus" : ""))
                .click(buttonConfiguration, this.buttonHandler.bind(this)));
        }

        if (numberOfButtons > 1) {
            $container.append($buttons);
            $buttons = $container;
        }

        return $buttons;
    };

    /**
     * @public
     */
    n.Alert.prototype.show = function () {
        $(".removeMe").remove();
        var alertContainer;
        if (this.shown) {
            // We only allow showing once
            return;
        }

        this.parent = this.makeAlertContainerSingleton();
        this.parent.addAlert(this);

        if (this.modal) {
            this.$modalDiv = $("<div>", {
                class: "removeMe"
            });
            this.parent.createBlockScreen(this);
        }

        if (this.messageTitle) {
            this.$titleWrap = $("<p>")
                .addClass("title")
                .append(this.messageTitle);
        }

        this.$messageWrap = $("<p>")
            .addClass("body")
            .append(this.message);

        this.$alertDiv = $("<div>")
            .hide()
            .addClass(this.getAlertClassBasedOnMessageType())
            .attr("role", "alert")
            .append(this.$messageWrap);

        if (this.$titleWrap) {
            this.$alertDiv.prepend(this.$titleWrap);
        }

        if (this.modal) {
            this.$alertDiv.addClass("alert-interactive");
            this.$alertDiv.css('z-index', 1000);
        } else {
            this.$alertDiv.addClass("alert-side");
        }
        this.$alertDiv.addClass("removeMe");

        var buttons = this.getButtons();
        if (buttons) {
            // Buttons will take care of removing the message
            this.$alertDiv.append(buttons);
        } else {
            // Click on the message anywhere will remove the message
            this.$alertDiv.click(this.removeAlert.bind(this));
            // Allows to remove all alerts that do not require user interaction, programmatically. Yes, we could send
            // the "click" event, but we want to communicate our intention clearly.
            this.$alertDiv.on(n.Alert.constants.jQueryAlertRemoveEventName, this.removeAlert.bind(this));
        }

        this.parent.$container.append(this.$alertDiv);


        //this.$alertDiv.slideDown(this.fadeInDuration, this.afterFadeIn.bind(this));
        if (!this.modal) {
            this.$alertDiv.animate({width:'show'}, this.fadeInDuration, this.afterFadeIn.bind(this));
        } else {
            this.$alertDiv.fadeIn(this.fadeInDuration);
        }

        this.$alertDiv.find(".wants-focus").focus();
        this.shown = true;
    };

    /**
     * @private
     */
    n.Alert.prototype.afterFadeIn = function () {
        if (this.timeout > 0) {
            this.timerId = window.setTimeout(this.removeAlert.bind(this), this.timeout);
        }
    };

    /**
     *
     *
     * @private
     */
    n.Alert.prototype.removeAlert = function () {

        // In case we have an armed timer (or expired timer, for that matter), disarm it.
        if (this.timerId) {
            window.clearTimeout(this.timerId);
            this.timerId = null;
        }

        var that = this;
        if (!this.modal) {
            this.$alertDiv.animate({width:'hide'}, this.fadeOutDuration, function () {
                that.$alertDiv.remove();
                that.$alertDiv = null;
                that.shown = false;
                that.parent.removeOutdatedAlerts();
            });
        } else {
            this.$alertDiv.fadeOut(this.fadeOutDuration, function(){
                that.$alertDiv.remove();
                that.$alertDiv = null;
                that.shown = false;
                that.removeAlertContainer();
            });
        }
        this.parent.removeAlert(this.identifier);
    };

    /**
     *
     * @param handler
     *
     * @private
     */
    n.Alert.prototype.buttonHandler = function (event) {
        if(event.data.eventName !== "show-debug") this.removeAlert();
        this.eventEmitter.emitEvent('alert.' + event.data.eventName, n.EventEmitter.makePayload(this, null));
    };

    n.Alert.prototype.isShown = function () {
        return this.shown;
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Manages multiple Alerts and provides an Alert Container.
     *
     * Is usually initialized with the first Alert that is created. (see n.Alert)
     *
     * @param {object} options For later
     */

    n.AlertManager = function(options) {
        this.screenBlocked = false;
        this.alerts = [];
        this.$container = $("<div>");
        this.blockingAlert = {};
        this.eventEmitter = new EventEmitter();
        this.regularCheck = {};
        this.parent = $(".container") || $(".container-fluid");
        if(this.parent.length === 0) {
            this.parent = $(".container-fluid");
        }

        $("body").append(this.$container);
        console.log("Created Alert Container");
    };

    n.AlertManager.prototype.on = n.EventEmitter.onMixin;

    /**
     * Add an Alert to the Alert Manager
     * @param alert
     */
    n.AlertManager.prototype.addAlert = function(alert) {
        this.alerts.push(alert);
        alert.setIdentifier(this.alerts.length);
        console.log(this.alerts);
    };

    n.AlertManager.prototype.removeAlert = function(identifier) {
        for(var i=0; this.alerts.length > i; i++) {
            if (this.alerts[i].identifier === identifier) {
                this.alerts.splice(i, 1);
            }
        }
    };

    /**
     * Removes the last Alert in the Array. Can be used to safely delete all alerts in a loop.
     * Returns false when the AlertManager has no more Alerts.
     * @returns Boolean
     */
    n.AlertManager.prototype.removeLastAlert = function() {
        if (this.alert.length > 0) {
            var alert = this.alerts.pop();
            alert.removeAlert();
            return true;
        } else {
            return false;
        }
    };

    /**
     * Savely removes outdated Alerts with isShown = false
     */
    n.AlertManager.prototype.removeOutdatedAlerts = function() {
        for(var i = 0; this.alerts.length > i; i++) {
            if(!this.alerts[i].isShown) {
                this.alerts[i].removeAlert();
                this.alerts.splice(i, 1);
            }
        }
    };

    /**
     * Returns the number of Alerts currently active
     * @returns {number}
     */
    n.AlertManager.prototype.count = function() {
        return this.alerts.length;
    };

    /**
     * Creates a semi-transparent black screen behind the alert.
     * Used to block other user input by modal alerts
     * @param alert
     */
    n.AlertManager.prototype.createBlockScreen = function(alert) {
        if (!this.screenBlocked) {
            var $blockScreen = $("<div>")
                .addClass("blockscreenQfq")
                .appendTo(this.$container);
            $blockScreen.css({
                'width': '100%',
                'height': Math.max($(document).height(), $(window).height()) + "px"
            });
            this.parent.addClass("blur");

            var that = this;
            this.screenBlocked = true;
            this.blockingAlert = alert;
            this.regularCheck = setInterval(function () {
                that.checkAlert();
            }, 500);
        }
    };

    /**
     * Is used by the interval this.regularcheck to guarantee
     * that the screen block is removed.
     */
    n.AlertManager.prototype.checkAlert = function() {
        if (!this.blockingAlert.isShown) {
            this.removeModalAlert();
        }
    };

    /**
     * Remove modal alerts
     */
    n.AlertManager.prototype.removeModalAlert = function() {
        if (this.screenBlocked) {
            $(".blockscreenQfq").remove();
            this.parent.removeClass("blur");
            clearInterval(this.regularCheck);
            this.screenBlocked = false;
        }
        this.removeOutdatedAlerts();
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global console */
/* global EventEmitter */

/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};


(function (n) {
    'use strict';
    /**
     * Tab Constructor.
     *
     * Programmatically access Bootstrap nav-tabs.
     *
     * @param {string} tabId HTML id of the element having `nav` and `nav-tabs` classes
     * @constructor
     *
     * @name QfqNS.BSTabs
     */
    n.BSTabs = function (tabId) {
        this.tabId = tabId;
        this._tabContainerLinkSelector = '#' + this.tabId + ' a[data-toggle="tab"]';
        this._tabActiveSelector = '#' + this.tabId + ' .active a[data-toggle="tab"]';
        this.tabs = {};
        this.currentTab = this.getActiveTabFromDOM();
        this.eventEmitter = new EventEmitter();
        this.currentFormName = $('#' + this.tabId + ' .active a[data-toggle="tab"]')[0].hash.slice(1).split("_")[0];
        this.currentRecordId = $('#' + this.tabId + ' a[data-toggle="tab"]')[0].id.split("-")[2];
        this.currentActiveLastPill = document.getElementById(this.tabId).getAttribute('data-active-last-pill');

        // Fill this.tabs
        this.fillTabInformation();

        // Enable update of current tab field
        this.installTabHandlers();
    };

    n.BSTabs.prototype.on = n.EventEmitter.onMixin;

    /**
     * Get active tab from DOM.
     *
     * Used upon object creation to fill the currentTab. It gets the ID of the currently shown tab. It does it, by
     * targeting the element in the navigator having the `active` class set.
     *
     * @private
     */
    n.BSTabs.prototype.getActiveTabFromDOM = function () {
        var activeTabAnchors = $(this._tabActiveSelector);
        if (activeTabAnchors.length < 1) {
            // This could be due to the DOM not fully loaded. If that's really the case, then the active tab
            // attribute should be set by the show.bs.tab handler
            return null;
        }

        return activeTabAnchors[0].hash.slice(1);
    };

    /**
     * Fill tabs object.
     *
     * Fill the tabs object using the tab HTML id as attribute name
     *
     * @private
     */
    n.BSTabs.prototype.fillTabInformation = function () {
        var tabLinks = $(this._tabContainerLinkSelector);
        if ($(tabLinks).length === 0) {
            throw new Error("Unable to find a BootStrap container matching: " + this._tabContainerLinkSelector);
        }

        var that = this;
        tabLinks.each(function (index, element) {
                if (element.hash !== "") {
                    var tabId = element.hash.slice(1);

                    that.tabs[tabId] = {
                        index: index,
                        element: element
                    };
                }
            }
        );
    };

    /**
     * @private
     */
    n.BSTabs.prototype.installTabHandlers = function () {
        $(this._tabContainerLinkSelector)
            .on('show.bs.tab', this.tabShowHandler.bind(this));

    };

    /**
     * Tab Show handler.
     *
     * Sets this.currentTab to the clicked tab and calls all registered tab click handlers.
     *
     * @private
     * @param event
     */
    n.BSTabs.prototype.tabShowHandler = function (event) {
        n.Log.debug('Enter: BSTabs.tabShowHandler()');
        this.currentTab = event.target.hash.slice(1);
        n.Log.debug("BSTabs.tabShowHandler(): invoke user handler(s)");
        this.eventEmitter.emitEvent('bootstrap.tab.shown', n.EventEmitter.makePayload(this, null));
        this.removeDot(this.currentTab);
        n.Log.debug('Exit: BSTabs.tabShowHandler()');
    };

    

    /**
     * Get all tab IDs.
     *
     * @returns {Array}
     *
     * @public
     */
    n.BSTabs.prototype.getTabIds = function () {
        var tabIds = [];
        for (var tabId in this.tabs) {
            if (this.tabs.hasOwnProperty(tabId)) {
                tabIds.push(tabId);
            }
        }
        return tabIds;
    };

    /**
     *
     * @returns {Array}
     *
     * @public
     */
    n.BSTabs.prototype.getTabAnchors = function () {
        var tabLinks = [];
        for (var tabId in this.tabs) {
            if (this.tabs.hasOwnProperty(tabId)) {
                tabLinks.push(this.tabs[tabId].element);
            }
        }

        return tabLinks;
    };

    /**
     * Activate a given tab.
     *
     * @param {string} tabId Id of the tab to activate
     *
     */
    n.BSTabs.prototype.activateTab = function (tabId) {
        if (!this.tabs[tabId]) {
            console.error("Unable to find tab with id: " + tabId);
            return false;
        }

        $(this.tabs[tabId].element).tab('show');
        this.removeDot(tabId);
        return true;
    };

    n.BSTabs.prototype.getCurrentTab = function () {
        return this.currentTab;
    };

    n.BSTabs.prototype.getTabName = function (tabId) {
        if (!this.tabs[tabId]) {
            console.error("Unable to find tab with id: " + tabId);
            return null;
        }

        return $(this.tabs[tabId].element).text().trim();
    };

    n.BSTabs.prototype.setTabName = function (tabId, text) {
        if(!this.tabs[tabId]) {
            console.error("Unable to find tab with id: " + tabId);
        }
        var $tab = $(this.tabs[tabId].element);
        $tab.text(text);
    };

    n.BSTabs.prototype.addDot = function(tabId) {
        var $tab = $(this.tabs[tabId].element);
        $tab.find(".qfq-dot").remove();
        var $coolBadge = $("<span>", {
            class: 'qfq-dot'
        });
        $tab.append($coolBadge);
    };

    n.BSTabs.prototype.removeDot = function(tabId) {
        $(this.tabs[tabId].element).find(".qfq-dot").remove();
    };

    n.BSTabs.prototype.getActiveTab = function () {
        return this.currentTab;
    };

    n.BSTabs.prototype.getContainingTabIdForFormControl = function (formControlName) {
        var $formControl = $("[name='" + formControlName + "']");
        if ($formControl.length === 0) {
            n.Log.debug("BSTabs.getContainingTabForFormControl(): unable to find form control with name '" + formControlName + "'");
            return null;
        }

        var i;
        var iterator;
        for (i = 0; i < $formControl.length; i++) {  // workaroud: checkbox renders two input elements with the same name so we loop through all input elements with that name. See issue #11752
            iterator = $formControl[i];
            while (iterator !== null) {
                if (iterator.hasAttribute('role') &&
                    iterator.getAttribute('role') === 'tabpanel') {
                    return iterator.id || null;
                }
                iterator = iterator.parentElement;
            }
        }

        return null;
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */


/* global $ */

var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.CharacterCount = {};

    /**
     * Initialize character count.
     *
     * Character count keeps track of characters in `<input>` and `<textarea>` elements (tracked elements). By default,
     * all elements having the `qfq-character-count` class are initialized. When non-`<input>/<textarea>` elements are
     * encountered during initialization, the behavior is undefined.
     *
     * Each element eligible for character count must provide a `data-character-count-id` attribute holding the element
     * id of the element receiving the character count. The receiving element's text is replaced by the current number
     * of characters of the tracked element. The number of characters in a tracked element is updated in the receiving
     * element upon a `change` or `keyup` event.
     *
     * If the `maxlength` attribute is present on the tracked element, the receiving element will display
     *
     *     N/<maxlength>
     *
     * where `N` is the current number of characters of the tracked element. If `maxlength` is not present, the
     * receiving element will display
     *
     *     N/∞
     *
     * where `N` is the current number of characters of the tracked element.
     *
     * @param selector {string} optional selector. Defaults to `.qfq-character-count`.
     */
    n.CharacterCount.initialize = function (selector) {
        selector = selector || ".qfq-character-count";
        $(selector).each(function () {
            var characterCountTarget, $targetElement;

            var $element = $(this);

            characterCountTarget = "#" + n.CharacterCount.getCharacterCountTargetId($element);

            $element.data('character-count-display', $(characterCountTarget));

            n.CharacterCount.updateCountForElement($element);

            $element.on('change keyup', function (evt) {
                n.CharacterCount.updateCountForElement($(evt.delegateTarget));
            });

        });
    };

    n.CharacterCount.updateCountForElement = function ($targetElement) {
        var maxLength = $targetElement.attr('maxlength') || "∞";
        var currentLength = $targetElement.val().length;
        $targetElement.data('character-count-display').text(currentLength + "/" + maxLength);
    };

    n.CharacterCount.getCharacterCountTargetId = function ($element) {
        return $element.data('character-count-id');
    };

})(QfqNS);


/* @author Benjamin Baer <benjamin.baer@math.uzh.ch> */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */


var QfqNS = QfqNS || {};

(function (n) {

    n.Clipboard = function (data) {
        this.text = '';
        this.events = [];
        if(data.text) {
            this.copyTextToClipboardAsync(data.text);
            return;
        }
        if(data.uri) {
            this.getDataFromUri(data.uri);
            return;
        }
        this.buildError("Called Clipboard without any Data to copy");
        console.error("Clipboard has to be called with an url or text to copy");
    };

    /**
     * @private
     * Has to be a bit hacky, since copy to clipboard only works with user confirmation from an API.
     * We fake this user confirmation by listening for the mouseup event after the button press.
     * As a fallback we also listen to a click event if the API took longer than the original click.
     * @param uri
     */
    n.Clipboard.prototype.getDataFromUri = function(uri) {
        var that = this;
        $.getJSON(uri, function(data) {
            if (data.text) {
                that.text = data.text;
                $(document).click(function() {that.copyTextToClipboardAsync(that.text); $(this).off();});
                $(document).mouseup(function() {that.copyTextToClipboardAsync(that.text); $(this).off();});
            } else {
                console.error("JSON Response didn't include a variable called 'text'");
                that.buildError("Didn't receive any Data to copy");
            }
        });
    };

    n.Clipboard.prototype.copyTextToClipboard = function(text) {
        var textArea = document.createElement("textarea");
       /* var focusedElement = $(":focus"); */
        textArea.value = text;
        document.body.appendChild(textArea);
       /* textArea.focus(); */
        textArea.select();

        try {
            var successful = document.execCommand('copy');
            var msg = successful ? 'successful' : 'unsuccessful';
            console.log('Fallback: Copying text command was ' + msg);
        } catch (err) {
            this.buildError("Couldn't copy text to clipboard: " + err);
        }

        document.body.removeChild(textArea);
       /* focusedElement.focus(); */
    };

    /**
     * Tries to copy text to the clipboard using asynchronous browser API.
     * If it doesn't exist, calls copyTextToClipboard (synchronous) instead.
     * @private String text
     */
    n.Clipboard.prototype.copyTextToClipboardAsync = function(text) {
        if (!navigator.clipboard) {
            this.copyTextToClipboard(text);
            return;
        }

        var that = this;
        navigator.clipboard.writeText(text).then(function() {
            console.log('Async: Copying to clipboard was successful!');
        }, function(err) {
            that.buildError("Could not copy text: " + err);
        });
    };

    n.Clipboard.prototype.buildError = function(message) {
        var alert = new n.Alert(
            {
                type: "error",
                message: message,
                modal: true,
                buttons: [{label: "Ok", eventName: 'close'}]
            }
        );
        alert.show();
    };

})(QfqNS);

/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */
/* @depend Comment.js */
/* @depend CommentController */
/* @depend SyntaxHighlighter */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Displays Code in a stylized fashion and allows
     * to write and display comments for each line of code.
     *
     * @param form Reference to the parent qfq Element
     * @param data Object containing the to be displayed data
     * @param $container Reference to the HTML Element that displays
     *                   the code correction
     * @param $target Reference to the HTML Element where the output (comment JSON)
     *                should be stored.
     */
    n.CodeCorrection = function () {
        this.page = {};
        this.data = {};
        this.eventEmitter = new EventEmitter();
        this.$parent = {};
        this.$target = {};
        this.$rows = [];
        this.annotations = [];
        this.users = [];
        this.currentUser = {};
        this.language = "";
        this.readOnly = false;
        this.syntaxHighlight = {};
    };

    /**
     * Initializes the Code Correction object, fetches data from URL or API if needed
     */
    n.CodeCorrection.prototype.initialize = function($container, page) {
        this.$parent = $container;
        this.$target = $("#" + $container.data("target"));
        this.data = {
            url: this.$parent.data("file"),
            text: this.$parent.data("text")

        };
        this.page = page;
        this.language = this.$parent.data("highlight") || "typo3conf/ext/qfq/Resources/Public/Json/javascript.json";
        this.readOnly = this.$parent.data("view-only") || false;
        this.currentUser = $container.data("uid");
        var that = this;
        if (this.readOnly) {
            if (this.$parent.data("annotations")) {
                var jsonAnnotations = this.$parent.data("annotations");
                this.annotations = jsonAnnotations.annotations;
                this.users = jsonAnnotations.users;
            } else {
                this._importFromTarget();
            }
        } else {
            this._importFromTarget();
        }

        if (this.data.url) {
            // Get data of a file and write it to data.text
            $.get(this.data.url, function(response) {
                that.data.text = response;
                that._prepareBuild();
            });
        } else if (this.data.text) {
            this._prepareBuild();
        } else {
            console.error("[CodeCorrection] No Code to correct passed to the object.");
        }
    };

    n.CodeCorrection.prototype._importFromTarget = function() {
        if (this.$target.val()) {
            var jImport = $.parseJSON(this.$target.val());
            if (jImport.annotations) {
                this.annotations = jImport.annotations;
                console.log("[CodeCorrection] Imported Annotations: " + this.annotations.length);
            }
            if (jImport.users) {
                this.users = jImport.users;
                console.log("[CodeCorrection] Imported Users: " + this.users.length);
            }
        }
    };

    n.CodeCorrection.prototype._prepareBuild = function() {
        var that = this;
        this.syntaxHighlight = new n.SyntaxHighlighter();
        this.syntaxHighlight.importInstructions(this.language, function() {
            that._buildEditor();
        });
    };

    /**
     * Breaks up the String by line and returns it as an Array
     * @param text Unix formatted text of the Code File
     * @returns {Array} Array with the Code broken up by Line
     */
    n.CodeCorrection.prototype.createLineByLineArray = function(text) {
        var textArray = [];
        if (typeof text === 'string' || text instanceof String) {
            textArray = text.split("\n");
        }
        return textArray;
    };

    /**
     * Builds the Code Correction HTML Element that should be displayed to the user.
     * @private
     */
    n.CodeCorrection.prototype._buildEditor = function() {
        var that = this;
        var $title = $('<div/>', {
            class: 'qfqCodeCorrectionTitle'
        });
        $title.appendTo(this.$parent);
        var container = $('<div/>', {
            class: 'codeCorrectionWrap'
        });
        var lineCount = 1;
        var textArray = this.createLineByLineArray(this.data.text);
        textArray.forEach(function(line) {
            that.$rows[lineCount] = that._buildLine(lineCount, line);
            that.$rows[lineCount].appendTo(container);
            lineCount++;
        });
        container.appendTo(this.$parent);
        this._buildAnnotations();

    };

    /**
     * Checks which codelines have annotations and initializes a CommentController
     * for each of them.
     * @private
     */
    n.CodeCorrection.prototype._buildAnnotations = function() {
        for (var i = 0; i < this.annotations.length; i++) {
            var annotation = this.annotations[i];
            var $hook = this.$rows[annotation.lineNumber];
            var commentController = this._buildCommentContainer($hook);
            commentController.importComments(annotation.comments, this.users);
            $hook.append(this._getCommentMarker(annotation.comments.length));
            this._setListeners(commentController);
            this._setCommentController(annotation.lineNumber, commentController);
        }
    };

    n.CodeCorrection.prototype._getCommentMarker = function(numberOfComments) {
        var container = $('<span/>', {
            class: "badge qfq-comment-marker",
            text: numberOfComments + ' '
        });
        container.append($('<span/>', {
            class: "glyphicon glyphicon-comment"
        }));
        return container;
    };

    /**
     * Builds a Line as a combination of HTML Elements. Binds the necessary Events.
     *
     * @param lineCount
     * @param line
     * @returns {jQuery|HTMLElement}
     * @private
     */
    n.CodeCorrection.prototype._buildLine = function(lineCount, line) {
        var that = this;
        var htmlRow = $('<div/>', {
            class: 'clearfix qfqCodeLine',
            id: 'qfqC' + lineCount
        });
        htmlRow.on("click", function() { that._handleClick(htmlRow, lineCount);});
        var htmlLineNumber = $('<div/>', {
            class: 'pull-left qfqLineCount',
            text: lineCount
        });
        var cLine = line.replace('&', '&amp;')
            .replace(';', '&semi;')
            .replace('<', '&lt;')
            .replace('>', '&gt;')
            .replace(/\s/g, '&nbsp;')
            .replace('"', '&quot;')
            .replace('\'', '&apos;')
            .replace('\\', '&bsol;');
        cLine = this.syntaxHighlight.highlightLine(cLine);
        var htmlCodeLine = $('<div/>', {
            class: 'pull-right qfqCode'
        });
        htmlCodeLine.html(cLine);
        htmlLineNumber.appendTo(htmlRow);
        htmlCodeLine.appendTo(htmlRow);
        return htmlRow;
    };

    /**
     * Initializes a CommentContainer at a given jQuery Hook and returns
     * the CommentContainer object
     * @param $hook
     * @returns {QfqNS.CommentController}
     * @private
     */
    n.CodeCorrection.prototype._buildCommentContainer = function($hook) {
        var options = {
            readOnly: this.readOnly
        };
        var commentController = new n.CommentController();
        commentController.buildContainer($hook, options);
        commentController.setCurrentUser(this.currentUser);
        return commentController;
    };

    /**
     * References the CommentController in this.annotations Array
     * for easy access later.
     * @param lineCount
     * @param commentController
     * @returns {boolean}
     * @private
     */
    n.CodeCorrection.prototype._setCommentController = function(lineCount, commentController) {
        for (var i=0; i < this.annotations.length; i++) {
            if (this.annotations[i].lineNumber === lineCount) {
                this.annotations[i].commentController = commentController;
                return true;
            }
        }
        return false;
    };

    /**
     * Sets listeners for events generated by the CommentController object
     * @param commentController
     * @private
     */
    n.CodeCorrection.prototype._setListeners = function(commentController) {
        var that = this;
        commentController.on('comment.added', function(argument) {
            console.log("Catch event: " + that.annotations.length);
            console.log("With data: " + argument.data);
            that._handleNew(argument.data);
        });
        commentController.on('comment.edited', function() {
            that._updateJSON();
        });
        commentController.on('comment.removed', function(e) {
            console.log(e);
            if(that._checkUserRemoval(e.data.uid)) {
                console.log("Removed User uid: " + e.data.uid);
            }
            that._checkLineRemoval();
            that._updateJSON();
        });
    };

    n.CodeCorrection.prototype._checkLineRemoval = function() {
        var removeLines = [];
        for (var i = 0; i < this.annotations.length; i++) {
            var comments = this.annotations[i].commentController.exportComments();

            if(comments.length == 0) {
                removeLines.push(i);
            }

        }

        for (var ii = 0; ii < removeLines.length; ii++) {
            this.annotations.splice(removeLines[ii], 1);
        }
    };

    n.CodeCorrection.prototype._handleNew = function(eventData) {
        this._addCurrentUser();
        this._updateJSON();
    };

    n.CodeCorrection.prototype._checkUserRemoval = function(uid) {
        var removeUser = true;
        for (var i = 0; i < this.annotations.length; i++) {
            var comments = this.annotations[i].commentController.exportComments();
            for (var ii = 0; ii < comments.length; ii++) {
                if(comments[ii].uid === uid) {
                    removeUser = false;
                    return false;
                }
            }
        }
        if (removeUser) {
            for (var iii = 0; iii < this.users.length; iii++) {
                if (this.users[iii].uid === uid) {
                    this.users.splice(i, 1);
                }
            }
        }
        return true;
    };

    n.CodeCorrection.prototype._addCurrentUser = function() {
        if (!this.checkUserExists(this.currentUser.uid)) {
            this.users.push(this.currentUser);
        }
    };

    n.CodeCorrection.prototype.checkUserExists = function(uid) {
        for (var i = 0; i < this.users.length; i++) {
            if (this.users[i].uid === uid) {
                return true;
            }
        }
        return false;
    };

    n.CodeCorrection.prototype._updateJSON = function() {
        var jexport = {};
        jexport.annotations = [];
        for (var i = 0; i < this.annotations.length; i++) {
            var annotation = {
                lineNumber: this.annotations[i].lineNumber,
                comments: this.annotations[i].commentController.exportComments()
            };
            jexport.annotations.push(annotation);
        }
        jexport.users = this.users;
        this.$target.val(JSON.stringify(jexport));
        var that = this;
        if (this.page.qfqForm) {
            this.page.qfqForm.eventEmitter.emitEvent('form.changed',
                n.EventEmitter.makePayload(that, null));
            this.page.qfqForm.changeHandler();
            this.page.qfqForm.form.formChanged = true;
        } else {
            console.log(this.page);
            throw("Error: Couldn't initialize qfqForm - not possible to send form.changed event");
        }
    };


    /**
     * Places a comment editor under the hook.
     * @param $hook
     * @param lineCount
     * @private
     */
    n.CodeCorrection.prototype._handleClick = function($hook, lineCount) {
        var comments = {};
        if (this._hasComments(lineCount)) {
            comments = this._getComments(lineCount);
            comments.commentController.toggle();
            comments.commentController.emitEvent("new");
        } else {
            if (!this.readOnly) {
                comments.lineNumber = lineCount;
                comments.commentController = new n.CommentController();
                comments.commentController.buildContainer($hook, {readOnly: this.readOnly});
                comments.commentController.setCurrentUser(this.currentUser);
                comments.commentController.displayEditor();
                this._setListeners(comments.commentController);
                this.annotations.push(comments);
            }
        }
    };

    /**
     * Checks if a line already has comments
     * @param lineCount
     * @returns {boolean}
     * @private
     */
    n.CodeCorrection.prototype._hasComments = function(lineCount) {
        for (var i=0; i < this.annotations.length; i++) {
            if (this.annotations[i].lineNumber === lineCount) {
                return true;
            }
        }
        return false;
    };

    /**
     * Gets comments for a specific line or returns false if said
     * comments can't be found.
     * @param lineCount
     * @returns {*}
     * @private
     */
    n.CodeCorrection.prototype._getComments = function(lineCount) {
        for (var i=0; i < this.annotations.length; i++) {
            if (this.annotations[i].lineNumber === lineCount) {
                return this.annotations[i];
            }
        }
        return false;
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Displays Comment or an Editor to create a comment
     *
     * https://www.quora.com/What-is-the-best-way-to-check-if-a-property-or-variable-is-undefined
     *
     */
    n.Comment = function (comment, user, $container, options) {
        this.comment = comment;
        this.user = user;
        this.$parent = $container;
        this.$comment = {};
        this.$text = {};
        if (arguments.length === 3) {
            this.options = { readOnly: false };
        } else {
            this.options = options;
        }
        this.childrenController = {};
        this.eventEmitter = new EventEmitter();
        this.deleted = false;
    };

    n.Comment.prototype.on = n.EventEmitter.onMixin;

    n.Comment.prototype.display = function() {
        var displayElement;
        displayElement = this._buildComment();
        displayElement.appendTo(this.$parent);
        this.$comment = displayElement;
    };

    n.Comment.prototype.getParent = function() {
        return this.$parent;
    };

    n.Comment.prototype.height = function() {
        return this.$comment.height();
    };

    n.Comment.prototype._buildComment = function(allowEdit) {
        var $commentWrap = $('<div/>', {
            class: "qfqComment"
        });
        var $avatar = $('<img>', {
            src: this.user.avatar,
            class: "qfqCommentAvatar"
        });
        $avatar.appendTo($commentWrap);
        var $topLine = $('<div />', {
            class: "qfqCommentTopLine"
        });
        $('<span />', {
            class: "qfqCommentAuthor",
            text: this.user.name + ":"
        }).appendTo($topLine);
        $('<span />', {
            class: "qfqCommentDateTime",
            text: this.comment.dateTime
        }).appendTo($topLine);
        $topLine.appendTo($commentWrap);
        var $comment = $('<div />', {
            class: "qfqCommentText"
        });
        $comment.html(this.comment.comment);
        if (!this.options.readOnly) {
            $comment.append(this._getCommands());
        }
        this.$text= $comment;
        $comment.appendTo($commentWrap);
        return $commentWrap;
    };

    n.Comment.prototype._updateText = function(text) {
          this.$text.html(text);
          this.$text.append(this._getCommands());
    };

    n.Comment.prototype._getCommands = function () {
        var $commentCommands = $("<div />", {
            class: "qfqCommentCommands"
        });
        var that = this;
        $commentCommands.append(this._getCommand("Edit", "pencil", function(e) {
            that._editMe(e);
        }));
        $commentCommands.append(this._getCommand("Delete", "trash", function(e) {
            that._deleteMe(e);
        }));
        $commentCommands.append(this._getCommand("Reply", "comment", function(e) {
            that._replyToMe(e);
        }));
        return $commentCommands;
    };

    n.Comment.prototype._getCommand = function(description, icon, onClick) {
        var $command = $('<span />', {
            class: "glyphicon glyphicon-" + icon + " qfqCommentCommand",
            title: description
        });
        $command.bind("click", this, onClick);
        return $command;
    };

    n.Comment.prototype._deleteMe = function(e) {
        this.deleted = true;
        this.$comment.remove();
        this.eventEmitter.emitEvent('comment.deleted',
            n.EventEmitter.makePayload(this, this.comment));
    };

    n.Comment.prototype._editMe = function(e) {
        this.$comment.hide();
        var that = this;
        var editor = new QfqNS.Editor();
        var $editor = editor.buildEditor(this.comment.comment);
        editor.on("editor.submit", function(e) {
             that._updateComment(e);
        });
        this.$comment.after($editor);

    };

    n.Comment.prototype._replyToMe = function(e) {
        this.eventEmitter.emitEvent('comment.reply',
            n.EventEmitter.makePayload(this, this.comment));
    };

    n.Comment.prototype._updateComment = function(e) {
        this.comment.comment = e.data.text;
        this._updateText(e.data.text);
        this.$comment.show();
        e.data.$container.remove();
        this.eventEmitter.emitEvent('comment.edited',
            n.EventEmitter.makePayload(this, this.comment));
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */
/* @depend Comment.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Manages a group of comments
     *
     */
    n.CommentController = function () {
        this.comments = [];
        this.currentUser = {};
        this.$container = {};
        this.$parent = {};
        this.height = "auto";
        this.options = {};
        // Event Emitter is a Library qfq uses to emit custom Events.
        this.eventEmitter = new EventEmitter();
    };

    n.CommentController.prototype.on = n.EventEmitter.onMixin;

    n.CommentController.prototype.setCurrentUser = function(user) {
        this.currentUser = user;
    };

    /**
     * changeHandler emits custom events for actions.
     * Additionally writes log entries to console for easier
     * testing.
     * @private
     * @param event String containing possible change states
     * @return {boolean} true on success
     */
    n.CommentController.prototype._changeHandler = function(event, comment) {
        if (event === "edit") {
            this.eventEmitter.emitEvent('comment.edited',
                n.EventEmitter.makePayload(this, "edit"));
            console.log("[CommentController] Event comment.edit emitted");
            return true;
        } else if (event === "new") {
            this.eventEmitter.emitEvent('comment.added',
                n.EventEmitter.makePayload(this, comment));
            console.log("[CommentController] Event comment.add emitted");
            return true;
        } else if (event === "remove") {
            this.eventEmitter.emitEvent('comment.removed',
                n.EventEmitter.makePayload(this, comment));
        }
        console.error("[CommentController] Changehandler called without valid event");
        return false;
    };

    n.CommentController.prototype.emitEvent = function(event) {
        this._changeHandler(event);
    };

    n.CommentController.prototype.buildContainer = function($hook, options) {
        var $container = $("<div />", {
            class: "qfqCommentContainer"
        });
        this.options = options;
        $hook.after($container);
        this.$container = $container;
    };

    n.CommentController.prototype.hasComments = function() {
        if (this.comments.length > 0) {
            return true;
        } else {
            return false;
        }
    };

    n.CommentController.prototype.toggle = function() {
        this.$container.slideToggle("swing");
    };

    n.CommentController.prototype.getComment = function(reference) {
        if (reference < this.comments.length && reference >= 0) {
            return this.comments[reference];
        } else {
            console.error("[CommentController] Requested Comment doesn't exist");
            return false;
        }
    };

    n.CommentController.prototype.addComment = function(comment, user) {
        var commentObject = new n.Comment(comment, user, this.$container, this.options);
        commentObject.display();
        this.comments.push(commentObject);
        this._changeHandler("new", commentObject);
        this._setListeners(commentObject);
        this.updateHeight();
        return this.comments.length - 1;
    };

    n.CommentController.prototype._setListeners = function(commentObject) {
        var that = this;
        commentObject.on('comment.edited', function(e) {
            that.updateComment(e.data);
        });
        commentObject.on('comment.reply', function(e) {
            that.requestReply(e.data);
        });
        commentObject.on('comment.deleted', function(e) {
            that.removeComment(e);
        });
    };

    n.CommentController.prototype.displayComments = function() {
        for (var i = 0; this.comments; i++) {
            this.comments[i].display();
        }
        this.updateHeight();
    };

    n.CommentController.prototype.displayEditor = function() {
        if (!this.options.readOnly) {
            var editor = new n.Editor();
            var that = this;
            var $editor = editor.buildEditor();
            editor.on("editor.submit", function (editor) {
                that._handleEditorSubmit(editor);
            });
            $editor.appendTo(this.$container);
            editor.$textArea.focus();
        }
    };

    n.CommentController.prototype._handleEditorSubmit = function(editor) {
        var comment = this.buildCommentObject(editor.data.text);
        this.addComment(comment, this.currentUser);
        editor.data.destroy();
    };

    n.CommentController.prototype.buildCommentObject = function(text) {
        var comment = {};
        comment.comment = text.replace("&quot;", "'");
        comment.dateTime = new Date().toLocaleString('de-CH');
        comment.uid = this.currentUser.uid;
        return comment;
    };

    n.CommentController.prototype.getContainer = function() {
        return this.$container;
    };

    n.CommentController.prototype.removeComment = function(reference) {
        this._changeHandler("remove", reference);
    };

    n.CommentController.prototype.updateComment = function(data) {
        console.log("[Comment Changed] User: " + data.uid +
            " Text:" + data.comment.substring(0, 20) + "...");
        this.emitEvent("edit");
    };

    n.CommentController.prototype.requestReply = function(data) {
        this.displayEditor();
    };

    n.CommentController.prototype.updateHeight = function() {
        //this.height = this.$container.height();
        //this.$container.css("max-height", this.height);
    };

    n.CommentController.prototype.importComments = function(comments, users) {
        for (var i=0; i < comments.length; i++) {
            var user = this._searchUsersByUid(users, comments[i].uid);
            this.addComment(comments[i], user);
        }
        if (comments.length === 0) {
            this.displayEditor();
        }
    };

    n.CommentController.prototype.exportComments = function() {
        var comments = [];
        for(var i=0; i < this.comments.length; i++) {
            if (!this.comments[i].deleted) {
                comments.push(this.comments[i].comment);
            }
        }
        return comments;
    };

    n.CommentController.prototype._searchUsersByUid = function (users, uid) {
        for (var i=0; i < users.length; i++) {
            if (users[i].uid === uid) {
                return users[i];
            }
        }
    };

})(QfqNS);
/**
 * Created by raos on 6/28/17.
 */

/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.Dirty = function (dirtyUrl) {
        this.dirtyUrl = dirtyUrl;

        this.eventEmitter = new EventEmitter();
        this.successTimerId = null;
        this.deniedTimerId = null;
        this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;
    };

    n.Dirty.NO_LOCK_TIMEOUT = -1;

    n.Dirty.EVENTS = {
        STARTED: 'dirty.notify.started',
        SUCCESS: 'dirty.notify.success',
        ENDED: 'dirty.notify.ended',
        FAILED: 'dirty.notify.failed',
        DENIED: 'dirty.notify.denied',
        DENIED_TIMEOUT: 'dirty.timeout.denied',
        SUCCESS_TIMEOUT: 'dirty.timeout.success',

        RELEASE_STARTED: 'dirty.release.started',
        RELEASE_ENDED: 'dirty.release.ended',
        RELEASE_SUCCESS: 'dirty.release.success',
        RELEASE_FAILED: 'dirty.release.failed',

        RENEWAL_STARTED: 'dirty.renewal.started',
        RENEWAL_ENDED: 'dirty.renewal.ended',
        RENEWAL_SUCCESS: 'dirty.renewal.success',
        RENEWAL_DENIED: 'dirty.renewal.denied',
        RENEWAL_FAILED: 'dirty.renewal.failed',

        CHECK_STARTED: 'dirty.check.started',
        CHECK_SUCCESS: 'dirty.check.success',
        CHECK_FAILED: 'dirty.check.failed',
        CHECK_ENDED: 'dirty.check.ended',
    };

    n.Dirty.ENDPOINT_OPERATIONS = {
        /** Acquire Lock */
        LOCK: "lock",
        /** Release Lock */
        RELEASE: "release",
        /** Renew Lock */
        RENEW: "extend",
        /** Check Lock */
        CHECK: "check"
    };

    n.Dirty.MINIMUM_TIMER_DELAY_IN_SECONDS = 5;
    n.Dirty.MILLISECONDS_PER_SECOND = 1000;

    n.Dirty.prototype.on = n.EventEmitter.onMixin;

    /**
     * Notify the server that SIP is becoming dirty.
     *
     * @param sip {string} sip.
     * @public
     */
    n.Dirty.prototype.notify = function (sip, optionalQueryParameters) {
        var eventData;

        if (!this.dirtyUrl) {
            n.Log.debug("notify: cannot contact server, no dirtyUrl.");
            return;
        }
        eventData = n.EventEmitter.makePayload(this, null);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.STARTED, eventData);
        $.ajax({
            url: this.makeUrl(sip, n.Dirty.ENDPOINT_OPERATIONS.LOCK, optionalQueryParameters),
            type: 'GET',
            cache: false
        })
            .done(this.ajaxNotifySuccessHandler.bind(this))
            .fail(this.ajaxNotifyErrorHandler.bind(this));
    };

    /**
     * @private
     * @param data
     * @param textStatus
     * @param jqXHR
     */
    n.Dirty.prototype.ajaxNotifySuccessHandler = function (data, textStatus, jqXHR) {
        var eventData = n.EventEmitter.makePayload(this, data);
        if (data.status && data.status === "success") {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.SUCCESS, eventData);
        } else {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.DENIED, eventData);
        }

        this.setTimeoutIfRequired(data, eventData);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.ENDED, eventData);
    };

    /**
     * Set timers based on server response. Timers are only set when `ajaxData` contains a `lock_timeout` attribute.
     * `lock_timeout` is expected to hold the timeout in seconds.
     *
     * @param ajaxData data received from server
     * @param eventData data passed when event is emitted.
     * @private
     */
    n.Dirty.prototype.setTimeoutIfRequired = function (ajaxData, eventData) {
        var timeoutInMilliseconds;

        if (!ajaxData.lock_timeout || ajaxData.lock_timeout < n.Dirty.MINIMUM_TIMER_DELAY_IN_SECONDS) {
            this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;
            return;
        }

        this.lockTimeoutInMilliseconds = ajaxData.lock_timeout * n.Dirty.MILLISECONDS_PER_SECOND;

        if (ajaxData.status &&
            ajaxData.status === "success") {
            this.clearSuccessTimeoutTimerIfSet();

            this.successTimerId = setTimeout((function (that, data) {
                return function () {
                    that.eventEmitter.emitEvent(n.Dirty.EVENTS.SUCCESS_TIMEOUT, data);
                };
            })(this, eventData), this.lockTimeoutInMilliseconds);
        } else {
            this.clearDeniedTimeoutTimerIfSet();

            this.deniedTimerId = setTimeout((function (that, data) {
                return function () {
                    that.eventEmitter.emitEvent(n.Dirty.EVENTS.DENIED_TIMEOUT, data);
                };
            })(this, eventData), this.lockTimeoutInMilliseconds);
        }
    };

    /**
     * @public
     */
    n.Dirty.prototype.clearSuccessTimeoutTimerIfSet = function () {
        if (this.successTimerId) {
            clearTimeout(this.successTimerId);
            this.successTimerId = null;
        }
    };

    /**
     * @private
     */
    n.Dirty.prototype.clearDeniedTimeoutTimerIfSet = function () {
        if (this.deniedTimerId) {
            clearTimeout(this.deniedTimerId);
            this.deniedTimerId = null;
        }
    };


    /**
     * @private
     * @param jqXHR
     * @param textStatus
     * @param errorThrown
     */
    n.Dirty.prototype.ajaxNotifyErrorHandler = function (jqXHR, textStatus, errorThrown) {
        var eventData = n.EventEmitter.makePayload(this, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        });

        this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;

        this.eventEmitter.emitEvent(n.Dirty.EVENTS.FAILED, eventData);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.ENDED, eventData);
    };

    /**
     * Release lock.
     *
     * @param sip {string} sip
     * @public
     */
    n.Dirty.prototype.release = function (sip, optionalQueryParameters, async) {
        var eventData;
        if (async === undefined) {
            async = true;
        }

        if (!this.dirtyUrl) {
            n.Log.debug("release: cannot contact server, no dirtyUrl.");
            return;
        }
        eventData = n.EventEmitter.makePayload(this, null);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RELEASE_STARTED, eventData);
        $.ajax({
            url: this.makeUrl(sip, n.Dirty.ENDPOINT_OPERATIONS.RELEASE, optionalQueryParameters),
            type: 'GET',
            cache: false,
            async: async
        })
            .done(this.ajaxReleaseSuccessHandler.bind(this))
            .fail(this.ajaxReleaseErrorHandler.bind(this));
    };

    /**
     * @private
     * @param data
     * @param textStatus
     * @param jqXHR
     */
    n.Dirty.prototype.ajaxReleaseSuccessHandler = function (data, textStatus, jqXHR) {
        n.Log.debug("Dirty Release: Response received.");
        var eventData = n.EventEmitter.makePayload(this, data);
        if (data.status && data.status === "success") {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.RELEASE_SUCCESS, eventData);
        } else {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.RELEASE_FAILED, eventData);
        }

        this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;

        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RELEASE_ENDED, eventData);
    };

    /**
     * @private
     * @param jqXHR
     * @param textStatus
     * @param errorThrown
     */
    n.Dirty.prototype.ajaxReleaseErrorHandler = function (jqXHR, textStatus, errorThrown) {
        var eventData = n.EventEmitter.makePayload(this, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        });

        this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;

        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RELEASE_FAILED, eventData);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RELEASE_ENDED, eventData);
    };

    /**
     * Request renewal of lock.
     *
     * @param sip {string} sip.
     * @public
     */
    n.Dirty.prototype.renew = function (sip, optionalQueryParameters) {
        var eventData;

        if (!this.dirtyUrl) {
            n.Log.debug("renew: cannot contact server, no dirtyUrl.");
            return;
        }
        eventData = n.EventEmitter.makePayload(this, null);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RENEWAL_STARTED, eventData);
        $.ajax({
            url: this.makeUrl(sip, n.Dirty.ENDPOINT_OPERATIONS.RENEW, optionalQueryParameters),
            type: 'GET',
            cache: false
        })
            .done(this.ajaxRenewalSuccessHandler.bind(this))
            .fail(this.ajaxRenewalErrorHandler.bind(this));
    };

    /**
     * @private
     * @param data
     * @param textStatus
     * @param jqXHR
     */
    n.Dirty.prototype.ajaxRenewalSuccessHandler = function (data, textStatus, jqXHR) {
        var eventData = n.EventEmitter.makePayload(this, data);
        if (data.status && data.status === "success") {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.RENEWAL_SUCCESS, eventData);
        } else {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.RENEWAL_DENIED, eventData);
        }
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RENEWAL_ENDED, eventData);

        this.setTimeoutIfRequired(data, eventData);
    };

    /**
     * @private
     * @param jqXHR
     * @param textStatus
     * @param errorThrown
     */
    n.Dirty.prototype.ajaxRenewalErrorHandler = function (jqXHR, textStatus, errorThrown) {
        var eventData = n.EventEmitter.makePayload(this, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        });

        this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;

        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RENEWAL_FAILED, eventData);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.RENEWAL_ENDED, eventData);
    };

    /**
     * Create URL to endpoint.
     *
     * @param sip {string} sip of the request
     * @param operation {string} operation
     * @returns {string} complete url
     * @private
     */
    n.Dirty.prototype.makeUrl = function (sip, operation, optionalQueryParameters) {
        var queryString, mergedQueryParameterObject;

        mergedQueryParameterObject = $.extend(
            optionalQueryParameters,
            {
                s: sip,
                action: operation
            }
        );

        queryString = $.param(mergedQueryParameterObject);
        return this.dirtyUrl + "?" + queryString;
    };

    /**
     * Check with the server if record is already locked.
     *
     * @param sip {string} sip.
     * @public
     */
    n.Dirty.prototype.check = function (sip, optionalQueryParameters) {
        var eventData;

        if (!this.dirtyUrl) {
            n.Log.debug("notify: cannot contact server, no dirtyUrl.");
            return;
        }
        eventData = n.EventEmitter.makePayload(this, null);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.CHECK_STARTED, eventData);
        $.ajax({
            url: this.makeUrl(sip, n.Dirty.ENDPOINT_OPERATIONS.CHECK, optionalQueryParameters),
            type: 'GET',
            cache: false
        })
            .done(this.ajaxCheckSuccessHandler.bind(this))
            .fail(this.ajaxCheckErrorHandler.bind(this));
    };

    /**
     * @private
     * @param data
     * @param textStatus
     * @param jqXHR
     */
    n.Dirty.prototype.ajaxCheckSuccessHandler = function (data, textStatus, jqXHR) {
        var eventData = n.EventEmitter.makePayload(this, data);
        if (data.status && data.status === "success") {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.CHECK_SUCCESS, eventData);
        } else {
            this.eventEmitter.emitEvent(n.Dirty.EVENTS.CHECK_FAILED, eventData);
        }

        this.setTimeoutIfRequired(data, eventData);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.CHECK_ENDED, eventData);
    };

    /**
     * @private
     * @param jqXHR
     * @param textStatus
     * @param errorThrown
     */
    n.Dirty.prototype.ajaxCheckErrorHandler = function (jqXHR, textStatus, errorThrown) {
        var eventData = n.EventEmitter.makePayload(this, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        });

        this.lockTimeoutInMilliseconds = n.Dirty.NO_LOCK_TIMEOUT;

        this.eventEmitter.emitEvent(n.Dirty.EVENTS.CHECK_FAILED, eventData);
        this.eventEmitter.emitEvent(n.Dirty.EVENTS.CHECK_ENDED, eventData);
    };
})(QfqNS);

/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Display content of a file
     *
     *
     * @param {object} options option object has following properties
     * @param {string} options.webworker Path to webworker JS file
     * @param {string} options.filePath file that has to be displayed
     * @param {number} [options.interval] time interval in milliseconds.
     * @param {string} [options.highlight] Path to highlight instructions, optional
     * @param {string} [options.targetId] HTML id of the target where it should be displayed
     * @param {boolean} [options.isContinuous] Appends the text instead of replacing it
     *
     * @constructor
     */
    n.DisplayFile = function (options) {
        this.options = options;
        this.options.webworker = options.webworker ||
            "typo3conf/ext/qfq/Resources/Public/Javascript/Worker/GetFileContent.js";
    };

    n.DisplayFile.prototype.show = function() {
        var that = this;
        if (this.options.filePath) {
            if (this.options.interval) {
                var webWorker = new Worker(this.options.webworker);
                webWorker.postMessage([this.options.filePath, this.options.interval]);

                webWorker.onmessage = function(e) {
                      that._handleResponse(e);
                };
            }
        } else {
            console.alert("No filePath supplied");
          }
    };

    n.DisplayFile.prototype._handleResponse = function(e) {
        var result = e.data;
        if (this.options.targetId) {
            var $target = $("#" + this.options.targetId);
            if (this.options.isContinuous) {
                $target.append(result);
            } else {
                $target.text(result);
            }
        }
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */
/* @depend ElementUpdate.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     *
     */
    n.DragAndDrop = function ($hook) {
        this.$container = $hook;
        this.eventEmitter = new EventEmitter();
        this.dropZones = [];
        this.elements = [];
        this.active = false;
        this.api = $hook.data("dnd-api");
        this.draggedId = "";
        this.lastChild = "";
        this.$tempObject = {};
    };

    n.DragAndDrop.prototype.on = n.EventEmitter.onMixin;

    n.DragAndDrop.prototype.buildDropArea = function(position, relatedId, otherPos) {
        var that = this;
        var $dropArea = {};

        if (this.$container.data("columns")) {
            $dropArea = $("<tr />", {
                class: "qfqDropTarget"
            });
            var $fluff = $("<td />", {
                class: "qfqDropTarget",
                colspan: this.$container.data("columns")
            });
            $fluff.appendTo($dropArea);
        } else {
            $dropArea = $("<div />", {
                class: "qfqDropTarget"
            });
        }

        $dropArea.data("position", position);
        $dropArea.data("related", relatedId);
        $dropArea.data("other-pos", otherPos);
        $dropArea.on("dragenter", function(e) {
            e.preventDefault();
            $dropArea.addClass("qfqTargetDisplay");
        });
        $dropArea.on("dragover", function(e) {
            e.preventDefault();
            e.originalEvent.dataTransfer.dropEffect = "move";
        });
        $dropArea.on("drop", function(e) {
            e.originalEvent.preventDefault();
            that._moveObjectBefore(e, $dropArea);
        });

        return $dropArea;
    };

    n.DragAndDrop.prototype.setDropZones = function($objects) {
        var that = this;

        $objects.each(function() {
            var $dropZone = $(this);
            $dropZone.on("dragenter", function(e) {
                e.preventDefault();
                $dropZone.addClass("qfqTargetDisplay");
                e.Effect = "all";
                that._handleDragEnter(e);
            });
            $dropZone.on("dragleave", function(e) {
                e.preventDefault();
                $dropZone.removeClass("qfqTargetDisplay");
                that._handleDragLeave(e);
            });
            $dropZone.on("dragover", function(e) {
                e.preventDefault();
                e.stopPropagation();
            });
            $dropZone.on("drop", function(e) {
                e.preventDefault();
                e.stopPropagation();
                that._dropHandler(e);
            });
            $dropZone.css("z-index", 5);
            that.dropZones.push($dropZone);
        });
    };

    n.DragAndDrop.prototype.setElements = function($objects) {
        var that = this;

        $objects.each(function() {
            var $element = $(this);
            $element.prop("draggable", true);
            $element.on("dragstart", function(e) {
                that.draggedId = $element[0].id;
            });
            that.elements.push($element);
        });
    };

    n.DragAndDrop.prototype._handleDragEnter = function(event) {
        var $tempObject = $("#" + this.draggedId).clone();
        $tempObject.css("opacity", 0.5);
        $tempObject.css("z-index", 0);
        $tempObject.off();
        if (this.$tempObject[0]) {
            if ($tempObject[0].id !== this.$tempObject[0].id) {
                this.$tempObject = $tempObject;
                this.$tempObject.appendTo($("#" + event.currentTarget.id));
            }
        } else {
            this.$tempObject = $tempObject;
            this.$tempObject.appendTo($("#" + event.currentTarget.id));
        }
    };

    n.DragAndDrop.prototype._handleDragLeave = function(event) {
        if(this.$tempObject[0]) {
            this.$tempObject.remove();
            this.$tempObject = {};
        }
    };

    n.DragAndDrop.prototype.makeBasketCase = function() {
        var dzSelector = this.$container.data("dnd-dropzone") || false;
        var elSelector = this.$container.data("dnd-element") || false;

        if (elSelector) {
            this.setElements($("." + elSelector));
        }
        if (dzSelector) {
            this.setDropZones($("." + dzSelector));
        }
    };

    n.DragAndDrop.prototype._dropHandler = function(event) {
        if(this.$tempObject[0]) {
            this.$tempObject.remove();
            this.$tempObject = {};
        }
        $("#" + this.draggedId).appendTo($("#" + event.currentTarget.id));
    };

    n.DragAndDrop.prototype._buildOrderDropZones = function($object, e) {
        this.removeDropAreas();

        //if ($object[0].id !== this.draggedId) {
            //if ($object.data("dnd-position") !== $("#" + this.draggedId).data("dnd-position") + 1) {
                var $dropArea = this.buildDropArea("before", $object.data("dnd-id"), $object.data("dnd-position"));
                //$dropArea.hide();
                $object.before($dropArea);
                //$dropArea.slideDown(500, 'swing');

                var $lastDrop = this.buildDropArea("after", $object.data("dnd-id"), $object.data("dnd-position"));
                //$lastDrop.hide();
                $object.after($lastDrop);
                //$lastDrop.slideDown(500, 'swing');
                //$lastDrop.appendTo(this.$container);
            //}

            /*if ($object[0].id === this.lastChild) {
                var $lastDrop = this.buildDropArea("after", $object.data("dnd-id"), $object.data("dnd-position"));
                $lastDrop.appendTo(this.$container);
            }*/
        //}
    };

    n.DragAndDrop.prototype.removeDropAreas = function() {
        if (this.$container.data("column")) {
            this.$container.children(".qfqTempTable").remove();
        }

        this.$container.children(".qfqDropTarget").remove();
    };

    n.DragAndDrop.prototype.removeDropTarget = function(e) {
        console.log(e);
    };

    n.DragAndDrop.prototype.makeSortable = function() {
        var that = this;
        var numberOfChildren = this.$container.children().length;
        var count = 0;

        this.$container.children().each( function() {
            count++;

            var child = $(this);
            if (numberOfChildren === count) {
                that.lastChild = child[0].id;
            }
            child.data("dnd-position", count);
            child.prop("draggable", true);
            child.on("dragstart", function(e) {
                e.originalEvent.dataTransfer.setData("text", child[0].id);
                that.draggedId = child[0].id;
                that.active = true;
                e.originalEvent.dataTransfer.effectAllowed = "move";
            });
            child.on("dragenter", function(e) {
                if (that.active) {
                    that._buildOrderDropZones($(this), e);
                }
            });
            child.on("dragend", function() {
                that.active = false;
                that.removeDropAreas();
            });
        });
    };

    n.DragAndDrop.prototype._moveObjectBefore = function(e, $hook) {
        var id = e.originalEvent.dataTransfer.getData("text");
        var $object = $("#" + id);
        var posTo = $hook.data("position");

        if (posTo === "after") {
            this.lastChild = $object[0].id;
        }

        $hook.before(document.getElementById(id));
        this._buildOrderUpdate($object, $hook.data("position"), $hook.data("related"), $hook.data("other-pos"));
        this.removeDropAreas();
    };

    n.DragAndDrop.prototype._buildOrderUpdate = function($object, position, otherId, otherPos) {
        var jObject = {};
        jObject.dragId = $object.data("dnd-id");
        jObject.dragPosition = $object.data("dnd-position");
        jObject.setTo = position;
        jObject.hoverId = otherId;
        jObject.hoverPosition = otherPos;
        this._sendToAPI(jObject);
    };

    n.DragAndDrop.prototype._sendToAPI = function(object) {
        var that = this;
        $.getJSON(this.api, object, function(data) {
            that._successHandler(data);
        });
    };

    n.DragAndDrop.prototype._successHandler = function(data) {
        if (data.status === "error") {
            var alert = new n.Alert({
                type: data.status,
                message: data.message,
                modal: true,
                buttons: [{
                    label: "Ok", eventName: "ok"
                }]
            });
            alert.show();
            console.error(data.message);
        } else {
            console.log("status:" + data.status + " message: " + data.message);
            if (data['element-update']) {
                var configuration = data['element-update'];
                n.ElementUpdate.updateAll(configuration);
            }
        }
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */
/* @depend Element/ElementBuilder.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {

    n.Droplet = function (url, color) {
        this._$trigger = {};
        this.position = {};
        this._$container = {};
        this.visible = false;
        this.eventEmitter = new EventEmitter();
        this.content = {};
        this.forms = [];
        this.url = url;
        this.color = color;
    };

    n.Droplet.prototype.on = n.EventEmitter.onMixin;

    n.Droplet.prototype.setTrigger = function($trigger) {
        this._$trigger = $trigger;
        var that = this;
        this._$trigger.click(function() {that.toggleDroplet();});
    };

    n.Droplet.prototype.setContainer = function($container) {
        this._$container = $container;
    };

    n.Droplet.prototype.setPosition = function(left, top) {
        this.position = {
            left: left,
            top: top
        };
    };

    n.Droplet.prototype.getContent = function() {
        var that = this;
        var response = jQuery.getJSON(this.url, function(data) {
            that._processResponse(data);
        });
        this._$container.text("Getting Data...");
    };

    n.Droplet.prototype._processResponse = function(data) {
        this._$container.text('');
        for(var i=0; i < data.elements.length; i++) {
            var element = data.elements[i];
            var $element = new n.ElementBuilder(element);
            this._$container.append($element.display());
            var that = this;
            if (element.type === "form") this.forms.push($element);
        }

        this.forms[0].on('form.submit.success',
            function() { that.toggleDroplet(); });
    };

    n.Droplet.prototype.createContainerBellowTrigger = function () {
        this.setPosition(
            this._$trigger.offset().left,
            this._$trigger.offset().top + this._$trigger.outerHeight()
        );
        var $container = $("<div />", {
            class: "qfq-droplet-container qfq-droplet-" + this.color
        });
        $container.css({
            position: 'absolute',
            zIndex: '100',
            top: this.position.top + 10 + "px",
            left: this.position.left + "px"
        });

        $(document.body).append($container);
        $container.addClass("qfq-droplet-container");
        $container.hide();
        return $container;
    };

    n.Droplet.prototype.getContainer = function() {
        if (this._$container) {
            console.error("No container has been created");
        } else {
            return this._$container;
        }
    };

    n.Droplet.prototype.toggleDroplet = function () {
        if (this.visible) {
            this._$container.hide();
            this.visible = false;
            this.forms = [];
        } else {
            this.eventEmitter.emitEvent('droplet.toggle',
                n.EventEmitter.makePayload(this, "toggle"));
            this._$container.show();
            this.visible = true;
            if (this.url) {
                this.getContent();
            }
        }
    };

    n.Droplet.prototype.hideDroplet = function() {
        if (this.visible) {
            this._$container.hide();
            this.visible = false;
        }
    };

})(QfqNS);

/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */
/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */
/* @depend Droplet.js */
/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
(function (n) {
    'use strict';

    n.DropletController = function() {
        this.droplets = [];
        this.eventEmitter = new EventEmitter();
    };

    n.DropletController.prototype.setUpDroplets = function() {
        var that = this;

        $(".qfq-droplet").each(function() {
            var url = false;
            var color = "grey";
            if ($(this).data("content")) {
                url = $(this).data("content");
            }
            if ($(this).data("color")) {
                color = $(this).data("color");
            }
            var droplet = new QfqNS.Droplet(url, color);
            droplet.setTrigger($(this));
            droplet.setContainer(droplet.createContainerBellowTrigger());

            that.droplets.push(droplet);
            droplet.on('droplet.toggle', function() { that.hideDroplets(); });
        });
    };

    n.DropletController.prototype.getDroplet = function(reference) {
        if (reference < this.droplets.length && reference >= 0) {
            return this.droplets[reference];
        }
    };

    n.DropletController.prototype.hideDroplets = function() {
        for(var i=0; i < this.droplets.length; i++) {
            this.droplets[i].hideDroplet();
        }
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */
/* @depend Comment.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Manages Text Editor for Comments
     *
     */
    n.Editor = function () {
        this.$container = {};
        this.$textArea = {};
        this.$submitButton = {};
        this.text = "";
        // Event Emitter is a Library qfq uses to emit custom Events.
        this.eventEmitter = new EventEmitter();
    };

    n.Editor.prototype.on = n.EventEmitter.onMixin;


    n.Editor.prototype.buildEditor = function(text) {
        var that = this;
        this.$container = $("<div />", {
            class: "qfqEditorContainer"
        });
        this.$textArea = $("<div />", {
            class: "qfqEditor",
            contenteditable: true,
            tabindex: 0
        });
        if (text) {
            this.$textArea.html(text);
        }
        this.$textArea.keydown(function() { that.activateSubmit(); });
        this._addEditorControls();
        var controls = $("<div />", {
            class: "qfqEditorControls"
        });
        var submitButton = $("<button />", {
            class: "btn btn-primary",
            disabled: true,
            text: "Send"
        });
        submitButton.on("click", function() { that._handleClick();});
        submitButton.appendTo(controls);
        this.$submitButton = submitButton;

        this.$textArea.appendTo(this.$container);
        controls.appendTo(this.$container);
        return this.$container;
    };

    n.Editor.prototype.activateSubmit = function() {
        this.$submitButton.attr("disabled", false);
    };

    n.Editor.prototype.destroy = function() {
          this.$container.remove();
    };

    n.Editor.prototype._handleClick = function() {
        var that = this;
        var text = this.$textArea.text().replace(/\s/g, '');
        if (text === "") {
            var alert = new n.Alert({
                message: "Please input text before sending.",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Ok", eventName: "ok"}
                ]
            });
            alert.show();
        } else {
            this.text = this.$textArea.html();
            this.eventEmitter.emitEvent('editor.submit',
                n.EventEmitter.makePayload(this, that));
        }
    };

    n.Editor.prototype._playingWithSelection = function() {
        var selection = window.getSelection();
        console.log(selection);
        if (!selection.isCollapsed) {
            var currentNode = selection.anchorNode;
            var count = 1;
            if (selection.anchorNode.nextSibling !== null) {
                if (currentNode.nextSibling.isSameNode(selection.focusNode)) {
                    console.log("Selected 2 nodes");
                } else {
                    while (!currentNode.isSameNode(selection.focusNode)) {
                        count++;
                        if (currentNode.nextSibling !== null) {
                            currentNode = currentNode.nextSibling;
                        } else {
                            console.error("whoops");
                            break;
                        }
                    }
                    console.log("Selected " + count + " nodes");
                }
            } else {
                if (selection.focusNode.nextSibling !== null) {
                    if (selection.focusNode.nextSibling.isSameNode(selection.anchorNode)) {
                        console.log("Selected 2 nodes");
                    } else {
                        currentNode = selection.focusNode;
                        while (!currentNode.isSameNode(selection.focusNode)) {
                            count++;
                            if (currentNode.previousSibling !== null) {
                                currentNode = currentNode.previousSibling;
                            } else {
                                console.error("whoops");
                                break;
                            }
                        }
                        console.log("Selected " + count + " nodes");
                    }
                } else {
                    console.log(selection.toString());
                    selection.deleteFromDocument();
                }
            }
        } else {
            console.log("Selected one node");
        }
    };

    n.Editor.prototype._addEditorControls = function() {
        var that = this;
        var $addCode = $("<span />", {
            class: "glyphicon glyphicon-console qfqEditorControl qfqCodeAdd"
        });
        $addCode.on("click", function() { that._addCodeElement();});
        $addCode.appendTo(this.$container);
        var $addList = $("<span />", {
            class: "glyphicon glyphicon-list qfqEditorControl qfqCodeList"
        });
        $addList.on("click", function() { that._addListElement();});
        $addList.appendTo(this.$container);
        this._addStandardTextElement();
    };

    n.Editor.prototype._addCodeElement = function() {
        var $code = $("<code />", {
            class: "qfqCodeElement",
            text: "Write your code here"
        });
        $code.appendTo(this.$textArea);
        this._addStandardTextElement();
    };

    n.Editor.prototype._addListElement = function() {
        var $unsortedList = $("<ul />");
        var selection = window.getSelection();
        console.log(selection);
        var $listElement = $("<li />", {
            text: "Write your list"
        });
        $listElement.appendTo($unsortedList);
        $unsortedList.appendTo(this.$textArea);
        this._addStandardTextElement();
    };

    n.Editor.prototype._addStandardTextElement = function() {
        var $regular = $("<p />", {
                text: "\xA0"
            }
        );
        $regular.appendTo(this.$textArea);
    };

}(QfqNS));
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global console */

/* @depend Utils.js */

var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Update HTML elements by a given id. Supports adding, setting, and removing attributes as well as setting the
     * text enclosed by the element.
     *
     * @type {{}}
     */
    n.ElementUpdate = {};


    /**
     * Update all elements according to configuration.
     *
     * @param config JSON configuration
     * @public
     */
    n.ElementUpdate.updateAll = function (config) {
        for (var idName in config) {
            if (!config.hasOwnProperty(idName)) {
                continue;
            }

            n.ElementUpdate.update(idName, config[idName]);
        }
        // Have to reapply QMORE
        n.initializeQmore();
    };

    /**
     *
     * @param elementId id of the element to update
     * @param config configuration
     */
    n.ElementUpdate.update = function (elementId, config) {
        var $element = n.ElementUpdate.$getElementById(elementId);

        if (config.subElements && config.typeToReplace) {
            n.ElementUpdate.handleReplaceElement($element, config.subElements, config.typeToReplace);
        }

        if (config.attr) {
            n.ElementUpdate.handleAttributeUpdate($element, config.attr);
        }

        if (config.content || config.content === '') {
            n.ElementUpdate.setElementText($element, config.content);
        }

    };

    n.ElementUpdate.$getElementById = function (id) {
        return $("#" + n.escapeJqueryIdSelector(id));
    };

    n.ElementUpdate.handleAttributeUpdate = function ($element, attributes) {
        var attributeValue;
        for (var attributeName in attributes) {
            if (!attributes.hasOwnProperty(attributeName)) {
                continue;
            }

            attributeValue = attributes[attributeName];

            if (attributeValue === null || attributeValue === false || attributeValue === "false") {
                n.ElementUpdate.deleteAttribute($element, attributeName);
            } else {
                n.ElementUpdate.setAttribute($element, attributeName, attributeValue);
            }
        }
    };

    n.ElementUpdate.setAttribute = function ($element, attributeName, attributeValue) {
        // Skip ICJ visual inputs - they manage their own values through the ICJ editor
        if ($element.data('icjTextarea') === 'true' || $element.hasClass('qfq-icj-input')) {
            return;
        }
        $element.attr(attributeName, attributeValue);
        if (attributeName.toLowerCase() === "value") {
            $element.val(attributeValue);
            $element.trigger('qfqChange');
        }
    };

    n.ElementUpdate.deleteAttribute = function ($element, attributeName) {
        $element.removeAttr(attributeName);
    };

    n.ElementUpdate.setElementText = function ($element, text) {
        $element.empty().append($.parseHTML(text));
    };

    /**
     * Replaces existing form elements with new ones to support dynamic updates for elements that generate multiple sub-elements
     * and whose content may change based on SQL queries.
     *
     * Example:
     * Before: handleReplaceElement($element = <div>, subElements = [<label>Text10</label>, <label>Text11</label>, <label>Text12</label>, <label>Text13</label>], typeToReplace = 'label')
     *
     * Original HTML:
     * <div>
     *     <label>Text1</label>
     *     <label>Text2</label>
     *     <label>Text3</label>
     *     <label>Text4</label>
     *     <label>Text5</label>
     *     <label>Text6</label>
     *     <div>
     *         <span>Other Element</span>
     *     </div>
     * </div>
     *
     * After replacement:
     * <div>
     *     <label>Text10</label>
     *     <label>Text11</label>
     *     <label>Text12</label>
     *     <label>Text13</label>
     *     <div>
     *         <span>Other Element</span>
     *     </div>
     * </div>
     *
     * @param $element      The jQuery object containing the element whose children will be replaced.
     * @param {Array} subElements    An array of new sub-elements that will replace the existing ones.
     * @param {string} typeToReplace The tag type (e.g., 'label') to be replaced.
     */
    n.ElementUpdate.handleReplaceElement = function ($element, subElements, typeToReplace) {
        // Remove children of type `typeToReplace` from the wrapper element.
        $element.children(typeToReplace).remove();
        // Prepend new elements in reverse order to maintain their original order.
        for (let i = subElements.length - 1; i >= 0; i--) {
            $element.prepend(subElements[i]);
        }
    }

    n.ElementUpdate.handleChangedElements = function (elements) {
        Object.entries(elements).forEach(([key, value]) => {
            switch(key) {
                case "delete-buttons":
                    var recordList = new n.QfqRecordList(n.form.qfqForm.apiDeleteUrl, true);

                    // Handle delete buttons
                    Object.entries(value).forEach(([buttonKey, buttonValue]) => {

                        if (buttonValue.formType) {
                            switch (buttonValue.formType) {
                                case "multi":
                                    const $button = n.ElementUpdate.updateMultiFormDeleteBtn(buttonValue);
                                    recordList.connectClickHandler($button);
                                    break;
                                // Placeholder for other form types (single)
                            }
                        }
                    });
                    break;

                // Placeholder for other element types (if needed)
                default:
                    console.log("Unhandled button type:", key);
            }
        });
    };

    n.ElementUpdate.updateMultiFormDeleteBtn = function (element) {
        // Get parent row
        var $parentRow = $("[name='" + element.htmlName + "']")
            .closest("tr");

        // Replace the delete button with new html
        $parentRow.find(".deleteRowBtn")
            .replaceWith(element.html);

        // Get the new button and add record class to parent row
        var $button = $parentRow.find(".record-delete");
        $parentRow.addClass("record");

        return $button;
    }
})(QfqNS);

/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */


/* global $ */
/* @depend Utils.js */

var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * @name addFields
     * @function
     * @global
     * @deprecated use appendTemplate().
     *
     */
    n.addFields = function (templateSelector, targetSelector, maxLines) {
        n.appendTemplate(templateSelector, targetSelector, maxLines);
        // New added fields are not initialized, need to be initialized.
        n.initializeQfqClearMe();
        n.initializeDatetimepicker(true);
        QfqNS.TypeAhead.install(null);
        QfqNS.AiAssist.install();
        QfqNS.AiChat.install();
        n.initializeCheckmarks(n.lastAppendElement);
    };

    /**
     * Append a child created from a template to a target. All occurrences of `%d` in attribute values will be replaced
     * by the number of children of target.
     *
     * @name appendTemplate
     * @public
     * @global
     * @function
     *
     * @param {string} templateSelector jQuery selector uniquely identifying the template.
     * @param {string} targetSelector jQuery selector uniquely identifying the target container.
     * @param {number} maxChildren do not allow more than `maxChildren` of children in target.
     *
     */
    n.appendTemplate = function (templateSelector, targetSelector, maxChildren) {
        var responsibleButton;
        var escapedTemplateSelector = n.escapeJqueryIdSelector(templateSelector);
        var escapedTargetSelector = n.escapeJqueryIdSelector(targetSelector);

        var $template = $(escapedTemplateSelector);

        var $target = $(escapedTargetSelector);

        var lines = n.countLines(escapedTargetSelector) + 1;

        if (lines >= maxChildren + 1) {
            return;
        }

        var deserializedTemplate = n.deserializeTemplateAndRetainPlaceholders($template.text());
        n.expandRetainedPlaceholders(deserializedTemplate, lines);

        // Change ids to their real ones
        var elements = deserializedTemplate[0].childNodes;
        elements.forEach(function(element) {
            n.adjustIds(element, lines);
        });


        // Store the button object, so we can easily access it when this `line` is removed by the user
        responsibleButton = n.getResponsibleButtonFromTarget($target);
        if (responsibleButton) {
            deserializedTemplate.data('field.template.addButton', responsibleButton);
        }

        $target.append(deserializedTemplate);
        n.informFormOfChange($target);

        // Store last append element
        n.lastAppendElement = deserializedTemplate;

        lines = n.countLines(escapedTargetSelector);
        if (lines >= maxChildren && responsibleButton) {
            n.disableButton(responsibleButton);
        }
    };

    n.adjustIds = function (item, startId) {
        if (item.id !== undefined && item.id !== '' && !item.classList.contains("hidden") && (item.tagName === "INPUT" || item.tagName === "DIV")) {
            var id = item.id;
            var parts = id.split('-');

            if (item.tagName === "INPUT") {
                parts[parts.length -1] = startId;
            } else {
                parts[parts.length -2] = startId;
            }

            item.id = parts.join('-');
        }

        // Get inner children and adjust id
        if (item.childNodes !== undefined && item.childNodes.length > 0) {
            n.adjustIds(item.childNodes, startId);
        }

        // Recursive function to iterate over elements without childNodes, which itself can contain childNodes.
        if (item.length > 0 && item.childNodes === undefined) {
            item.forEach(function(element) {
                n.adjustIds(element, startId);
            });
        }
    };

    n.getResponsibleButtonFromTarget = function ($target) {
        var $button;
        var buttonSelector = $target.data('qfq-line-add-button');
        if (!buttonSelector) {
            return null;
        }

        $button = $(buttonSelector);
        if ($button.length === 0) {
            return null;
        }

        return $button[0];
    };

    n.disableButton = function (button) {
        $(button).attr('disabled', 'disabled');
    };

    n.enableButton = function (button) {
        $(button).removeAttr('disabled');
    };

    n.informFormOfChange = function ($sourceOfChange) {
        var $enclosingForm = $sourceOfChange.closest("form");
        $enclosingForm.change();
    };

    /**
     * @name initializeFields
     * @global
     * @function
     * @deprecated use initializeTemplateTarget()
     */
    n.initializeFields = function (element, templateSelectorData) {
        n.initializeTemplateTarget(element, templateSelectorData);
    };

    /**
     * When the template target contains no children, it initializes the target element by appending the first child
     * created from the template.
     *
     * @name initializeTemplateTarget
     * @global
     * @function
     *
     * @param {HTMLElement} element the target HTMLElement to be initialized
     * @param {string} [templateSelectorData=qfq-line-template] name of the `data-` attribute containing the jQuery
     * selector
     * selecting the template
     */
    n.initializeTemplateTarget = function (element, templateSelectorData) {
        var responsibleButton;
        var $element = $(element);
        var templateSelector, escapedTemplateSelector, $template, deserializedTemplate;

        templateSelector = $element.data(templateSelectorData || 'qfq-line-template');
        escapedTemplateSelector = n.escapeJqueryIdSelector(templateSelector);

        $template = $(escapedTemplateSelector);

        if ($element.children().length > 0) {
            n.setPlaceholderRetainers($template.text(), element);
            return;
        }

        deserializedTemplate = n.deserializeTemplateAndRetainPlaceholders($template.text());
        n.expandRetainedPlaceholders(deserializedTemplate, 1);

        deserializedTemplate.find('.qfq-delete-button').remove();
        $element.append(deserializedTemplate);
    };

    /**
     * @name removeFields
     * @global
     * @function
     * @deprecated use removeThisChild()
     */
    n.removeFields = function (target) {
        n.removeThisChild(target);
    };

    /**
     * Remove the element having a class `qfq-line`. Uses `eventTarget` as start point for determining the closest
     * element.
     *
     * @name removeFields
     * @global
     * @function
     * @param {HTMLElement} eventTarget start point for determining the closest `.qfq-line`.
     */
    n.removeThisChild = function (eventTarget) {
        var $line = $(eventTarget).closest('.qfq-line');
        var $container = $line.parent();
        var buttonToEnable = $line.data('field.template.addButton');

        var parts = $line[0].childNodes[0].id.split('-');
        var startId = parts[parts.length - 2];
        var arrayCount = startId - 1;

        function adjustChildIds (actualChild, startId) {
            actualChild.childNodes.forEach(function(element) {
                n.adjustIds(element, startId);
            });
        }

        $line.remove();

        n.reExpandLineByLine($container);
        n.informFormOfChange($container);
        if (buttonToEnable) {
            n.enableButton(buttonToEnable);
        }

        for (var i = arrayCount; i < $container[0].childNodes.length; i++) {
            var actualChild = $container[0].childNodes[i];
                adjustChildIds(actualChild, startId);
            startId++;
        }
    };

    /**
     * Takes a template as string and deserializes it into DOM. Any attributes having a value containing `%d` will be
     *
     * @private
     * @param template
     *
     */
    n.deserializeTemplateAndRetainPlaceholders = function (template) {
        var $deserializedTemplate = $(template);

        $deserializedTemplate.find("*").each(function () {
            var $element = $(this);
            $.each(this.attributes, function () {
                if (this.value.indexOf('%d') !== -1) {
                    $element.data(this.name, this.value);
                }
            });

            if (n.isTextRetainable($element)) {
                $element.data('element-text', $element.text());
            }
        });

        return $deserializedTemplate;
    };


    /**
     * Set placeholder retainer on existing lines
     * @private
     * @param template
     * @param element container element
     */
    n.setPlaceholderRetainers = function (template, element) {
        var responsibleButton;
        var $deserializedTemplate = $(template);

        var $flatTemplate = $deserializedTemplate.find('*');

        var $element = $(element);

        var $childrenOfElement = $element.children();

        responsibleButton = n.getResponsibleButtonFromTarget($element);

        $childrenOfElement.each(function () {
            var $this = $(this);

            // Add button reference
            $this.data('field.template.addButton', responsibleButton);

            // We use .find() to increase chances of $flatTemplate and $childrenOfChild having the same ordering.
            var $childrenOfChild = $this.find('*');
            $childrenOfChild.each(function (childIndex) {
                var correspondingTemplateElement, $correspondingTemplateElement, $child;

                if (childIndex >= ($flatTemplate.length - 1)) {
                    // the current child element has no corresponding element in the template, so no use of trying to
                    // copy over retaining information.
                    return;
                }

                $child = $(this);

                correspondingTemplateElement = $flatTemplate[childIndex];
                $correspondingTemplateElement = $(correspondingTemplateElement);

                // Create the retainers on the child for each child
                $.each(correspondingTemplateElement.attributes, function () {
                    if (this.value.indexOf('%d') !== -1) {
                        $child.data(this.name, this.value);
                    }
                });

                if (n.isTextRetainable($correspondingTemplateElement)) {
                    $child.data('element-text', $correspondingTemplateElement.text());
                }
            });
        });
    };

    /**
     * @private
     */
    n.expandRetainedPlaceholders = function ($elements, value) {
        $elements.find('*').each(function () {
            var $element = $(this);
            $.each(this.attributes, function () {
                var retainedPlaceholder = $element.data(this.name);
                if (retainedPlaceholder) {
                    this.value = n.replacePlaceholder(retainedPlaceholder, value);
                }
            });

            if (n.hasRetainedText($element)) {
                $element.text(n.replacePlaceholder($element.data('element-text'), value));
            }

        });
    };

    /**
     * @private
     * @param $element
     * @returns {*}
     */
    n.isTextRetainable = function ($element) {
        return $element.is("label") && $element.text().indexOf('%d') !== -1 && $element.children().length === 0;
    };

    /**
     * @private
     * @param $element
     * @returns {boolean}
     */
    n.hasRetainedText = function ($element) {
        return $element.data('element-text') !== undefined;
    };

    /**
     * @private
     * @param targetSelector
     * @returns {jQuery}
     */
    n.countLines = function (targetSelector) {
        var escapedTargetSelector = n.escapeJqueryIdSelector(targetSelector);
        return $(targetSelector).children().length;
    };

    /**
     * @private
     */
    n.replacePlaceholder = function (haystack, by) {
        return haystack.replace(/%d/g, by);
    };

    /**
     * @private
     * @param $container
     */
    n.reExpandLineByLine = function ($container) {
        $container.children().each(function (index) {
            var $element = $(this);
            n.expandRetainedPlaceholders($element, index + 1);
        });
    };

    /**
     * Initialize checkmark elements for checkboxes and radio buttons.
     * @private
     */
    n.initializeCheckmarks = function (element) {

        var $target = $(element);

        $target.find(".radio-inline").each(function() {
            if (!$(this).find(".checkmark").length) {
                $(this).append($("<span>", { class: "checkmark", aria: "hidden"}));
            }
        });

        $target.find(".checkbox-inline").each(function() {
            if (!$(this).find(".checkmark").length) {
                $(this).append($("<span>", { class: "checkmark", aria: "hidden"}));
            }
        });

        $target.find(".radio").each(function() {
            if (!$(this).find(".checkmark").length) {
                $(this).append($("<span>", { class: "checkmark", aria: "hidden"}));
            }
        });

        $target.find(".checkbox").each(function() {
            if (!$(this).find(".checkmark").length) {
                $(this).append($("<span>", { class: "checkmark", aria: "hidden"}));
            }
        });

    };
})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */
/* global $ */
/* global EventEmitter */

/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Handle file deletion.
     *
     * Upon instantiation, it attaches to all `<button>` elements having the class `delete-file`, within the elements
     * selected by the `formSelector`.
     *
     * @param formSelector jQuery selector of the `<form>` this instance is responsible for.
     * @param targetUrl endpoint URL to send delete requests to.
     * @constructor
     *
     * @name QfqNS.FileDelete
     */
    n.FileDelete = function (formSelector, targetUrl) {
        this.formSelector = formSelector;
        this.targetUrl = targetUrl;
        this.eventEmitter = new EventEmitter();

        this.setupOnClickHandler();
    };

    n.FileDelete.prototype.on = n.EventEmitter.onMixin;

    n.FileDelete.prototype.setupOnClickHandler = function () {
        $(this.formSelector + " button.delete-file").click(this.buttonClicked.bind(this));
    };

    n.FileDelete.prototype.buttonClicked = function (event) {
        event.preventDefault();
        var alert = new n.Alert({
            message: "Do you want to delete the file?",
            type: "warning",
            modal: true,
            buttons: [
                {label: "OK", eventName: "ok"},
                {label: "Cancel", eventName: "cancel", focus: true}
            ]
        });
        alert.on('alert.ok', function () {
            this.performFileDelete(event);
        }.bind(this));
        alert.show();
    };

    n.FileDelete.prototype.performFileDelete = function (event) {
        this.eventEmitter.emitEvent('filedelete.started', n.EventEmitter.makePayload(event.delegateTarget, null));

        var data = this.prepareData(event.delegateTarget);

        $.ajax({
            url: this.targetUrl,
            type: 'POST',
            data: data,
            cache: false
        })
            .done(this.ajaxSuccessHandler.bind(this, event.delegateTarget))
            .fail(this.ajaxErrorHandler.bind(this, event.delegateTarget));
    };

    n.FileDelete.prototype.prepareData = function (htmlButton) {
        if (!htmlButton.hasAttribute("name")) {
            throw new Error("File delete button element requires 'name' attribute");
        }

        var fileDeleteName = htmlButton.getAttribute('name');

        var data = {
            s: $(htmlButton).data('sip'),
            name: fileDeleteName
        };

        return data;
    };

    n.FileDelete.prototype.ajaxSuccessHandler = function (uploadTriggeredBy, data, textStatus, jqXHR) {
        var eventData = n.EventEmitter.makePayload(uploadTriggeredBy, data, {
            textStatus: textStatus,
            jqXHR: jqXHR
        });
        this.eventEmitter.emitEvent('filedelete.delete.successful', eventData);
        this.eventEmitter.emitEvent('filedelete.ended', eventData);
    };

    n.FileDelete.prototype.ajaxErrorHandler = function (uploadTriggeredBy, jqXHR, textStatus, errorThrown) {
        var eventData = n.EventEmitter.makePayload(uploadTriggeredBy, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        });
        this.eventEmitter.emitEvent('filedelete.delete.failed', eventData);
        this.eventEmitter.emitEvent('filedelete.ended', eventData);
    };


})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */

/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     *
     * @param formSelector
     * @param targetUrl
     * @constructor
     * @name QfqNS.FileUpload
     */
    n.FileUpload = function (formSelector, targetUrl) {
        this.formSelector = formSelector;
        this.targetUrl = targetUrl;
        this.eventEmitter = new EventEmitter();

        this.setupOnChangeHandler();
    };

    n.FileUpload.prototype.on = n.EventEmitter.onMixin;

    /**
     *
     * @private
     */
    n.FileUpload.prototype.setupOnChangeHandler = function () {
        $(this.formSelector + " :file:not(.chat-file-input)").on('change', this.performFileUpload.bind(this));
    };

    /**
     * @private
     * @param event
     */
    n.FileUpload.prototype.performFileUpload = function (event) {
        this.eventEmitter.emitEvent('fileupload.started', n.EventEmitter.makePayload(event.target, null));

        var data = this.prepareData(event.target);

        $.ajax({
            url: this.targetUrl,
            type: 'POST',
            data: data,
            cache: false,
            processData: false,
            contentType: false
        })
            .done(this.ajaxSuccessHandler.bind(this, event.target))
            .fail(this.ajaxErrorHandler.bind(this, event.target));
    };

    n.FileUpload.prototype.prepareData = function (htmlFileInput) {
        if (!htmlFileInput.hasAttribute("name")) {
            throw new Error("File input element requires 'name' attribute");
        }

        var fileInputName = htmlFileInput.getAttribute('name');

        var data = new FormData();
        $.each(htmlFileInput.files, function (key, value) {
            data.append(fileInputName + '_' + key, value);
        });

        data.append('s', $(htmlFileInput).data('sip'));
        data.append('name', fileInputName);

        return data;
    };

    /**
     * @private
     * @param data
     * @param textStatus
     * @param jqXHR
     */

    n.FileUpload.prototype.ajaxSuccessHandler = function (uploadTriggeredBy, data, textStatus, jqXHR) {
        var eventData = n.EventEmitter.makePayload(uploadTriggeredBy, data, {
            textStatus: textStatus,
            jqXHR: jqXHR
        });
        this.eventEmitter.emitEvent('fileupload.upload.successful', eventData);
        this.eventEmitter.emitEvent('fileupload.ended', eventData);
    };

    /**
     * @private
     * @param jqXHR
     * @param textStatus
     * @param errorThrown
     */
    n.FileUpload.prototype.ajaxErrorHandler = function (uploadTriggeredBy, jqXHR, textStatus, errorThrown) {
        var eventData = n.EventEmitter.makePayload(uploadTriggeredBy, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        });
        this.eventEmitter.emitEvent('fileupload.upload.failed', eventData);
        this.eventEmitter.emitEvent('fileupload.ended', eventData);
    };


})(QfqNS);

/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Abstraction of `<form>`.
     *
     * @param formId
     * @param formChanged
     * @constructor
     * @name QfqNS.Form
     */
    n.Form = function (formId, formChanged) {
        this.formId = formId;
        this.eventEmitter = new EventEmitter();
        // In order to immediately emit events, we bind event handlers on `<form>` `change` and `input` and `paste` on
        // `<input> and `<textarea>`s. Without precaution, this will lead to emitting `form.changed` twice upon
        // `input` and `paste` events, since they eventually will raise a form `change` event.  We perform bookkeeping
        // using this flag, to avoid emitting `form.changed` twice when bubbling.
        //
        // Since we cannot predict the effect on disable bubbling of the `input` and `paste` events, we resort to this
        // home-brew solution.
        this.inputAndPasteHandlerCalled = false;
        this.filepondUploadProcessing = [];
        this.saveInProgress = false;
        this.saveAllowed = true;
        this.captcha = this.captcha = document.forms[this.formId].querySelector("cap-widget");
        if(this.captcha) {
            console.log("Found captcha", this.captcha);
            this.captcha.addEventListener("solve", (e) => this.captchaSuccessHandler(e));
            this.saveAllowed = false;
        }

        if (!document.forms[this.formId]) {
            throw new Error("Form '" + formId + "' does not exist.");
        }

        this.formChanged = !!formChanged;
        this.formChangedTimestampInMilliseconds = n.Form.NO_CHANGE_TIMESTAMP;
        this.$form = $(document.forms[this.formId]);
        this.$form.on("change", (event) => {
            if (this.filepondUploadProcessing.length === 0) {
                this.changeHandler(event);
            }
        });

        this.$form.on("invalid.bs.validator", this.validationError.bind(this));
        this.$form.on("valid.bs.validator", this.validationSuccess.bind(this));

        // On <input> elements, we specifically bind this events, in order to update the formChanged property
        // immediately, not only after loosing focus. Same goes for <textarea>
        this.$form.find("input, textarea").on("input paste", (event) => {
            // Only trigger if no filepond upload is in progress
            if (this.filepondUploadProcessing.length === 0) {
                this.inputAndPasteHandler(event);
            }
        });

        // Fire handler while using dateTimePickerType qfq
        function getDatetimePickerChanges(element) {
            $('div tbody').on('click', 'td.day:not(.disabled)', formObject.inputAndPasteHandler.bind(formObject));
            var timepickerElements = 'td a.btn[data-action="incrementHours"], td a.btn[data-action="incrementMinutes"], td a.btn[data-action="incrementSeconds"], td a.btn[data-action="decrementHours"], td a.btn[data-action="decrementMinutes"], td a.btn[data-action="decrementSeconds"]';
            $('div table').on('click', 'td.hour, td.minute, td a[data-action="clear"], '+timepickerElements, formObject.inputAndPasteHandler.bind(formObject));

            element.addEventListener('keydown', function(event) {
                if (event.key === 'Delete') {
                    formObject.inputAndPasteHandler(event);
                }
            });
        }

        // Function to trigger onfocus event again while element is already focused
        function triggerFocus(element) {
            var eventType = "onfocusin" in element ? "focusin" : "focus",
                bubbles = "onfocusin" in element,
                event;

            if ("createEvent" in document) {
                event = document.createEvent("Event");
                event.initEvent(eventType, bubbles, true);
            }
            else if ("Event" in window) {
                event = new Event(eventType, { bubbles: bubbles, cancelable: true });
            }

            element.focus();
            element.dispatchEvent(event);
        }

        var formObject = this;
        // Open datetimepicker over click event even if first element is already focused and get all changes of datetimepicker for dirty lock
        this.$form.find(".qfq-datepicker").on("click", function(){
            triggerFocus(this);
            if (formObject.filepondUploadProcessing.length === 0) {
                getDatetimePickerChanges(this);
            }
        });

        // Fire handler while using dateTimePickerType browser
        this.$form.find("input[type=datetime-local]").on("click", (event) => {
            // Only trigger if no filepond upload is in progress
            if (this.filepondUploadProcessing.length === 0) {
                this.inputAndPasteHandler(event);
            }
        });

        // Use ctrl+alt+s for saving form
        document.addEventListener('keydown', function(event) {
            if (event.ctrlKey && event.altKey && event.key === 's') {
                $("#save-button-" + this.formId + ":not([disabled=disabled])").click();
            }
        });

        this.$form.on('submit', function (event) {
            event.preventDefault();
        });
    };

    n.Form.NO_CHANGE_TIMESTAMP = -1;

    n.Form.prototype.on = n.EventEmitter.onMixin;

    /**
     * 
     * @param {*} event
     * 
     * @private
     * Handles captcha success
     */
    n.Form.prototype.captchaSuccessHandler = function(event) {
        this.token = event.detail.token;
        // Add/update hidden input so token is submitted with the form
        // let input = document.getElementById('cap-token-input');
        // if (!input) {
        //     input = document.createElement('input');
        //     input.type = 'hidden';
        //     input.name = 'cap-token';
        //     input.id = 'cap-token-input';
        //     document.forms[this.formId].appendChild(input);
        // }
        // input.value = this.token;
        this.saveAllowed = true;
        this.validate();
        this.changeHandler(event);
    }

    /**
     *
     * @param event
     *
     * @private
     */
    n.Form.prototype.changeHandler = function (event) {
        // Trim whitespace after change. Before validation happens.
        if (event.target.value !== undefined && event.target.type !== 'file') {
            var value = event.target.value;
            event.target.value = value.trim();
        }

        if (this.inputAndPasteHandlerCalled) {
            // reset the flag
            this.inputAndPasteHandlerCalled = false;
            // and return. The `form.changed` event has already been emitted by `Form#inputAndPasteHandler()`.
            return;
        }
        this.markChanged(event.target);
    };

    n.Form.prototype.inputAndPasteHandler = function (event) {
        this.inputAndPasteHandlerCalled = true;
        this.markChanged(event.target);
    };

    n.Form.prototype.getFormChanged = function () {
        return this.formChanged;
    };

    n.Form.prototype.getSaveAllowed = function () {
        return this.saveAllowed;
    }

    n.Form.prototype.markChanged = function (initiator) {
        if(!!initiator) {
            if(initiator.classList.contains("qfq-skip-dirty")) {
                return;
            }
        }
        this.setFormChangedState();
        this.eventEmitter.emitEvent('form.changed', n.EventEmitter.makePayload(this, null));
    };

    /**
     * @private
     */
    n.Form.prototype.setFormChangedState = function () {
        this.formChanged = true;
        this.formChangedTimestampInMilliseconds = Date.now();
    };

    n.Form.prototype.resetFormChanged = function () {
        this.resetFormChangedState();
        this.eventEmitter.emitEvent('form.reset', n.EventEmitter.makePayload(this, null));

    };

    /**
     * @private
     */
    n.Form.prototype.resetFormChangedState = function () {
        this.formChanged = false;
        this.formChangedTimestampInMilliseconds = n.Form.NO_CHANGE_TIMESTAMP;
    };

    n.Form.prototype.submitTo = function (to, queryParameters) {
        var submitUrl;

        this.eventEmitter.emitEvent('form.submit.before', n.EventEmitter.makePayload(this, null));
        submitUrl = this.makeUrl(to, queryParameters);

        // For better dynamic update compatibility (checkboxes). All input elements need to be not disabled for fully serializing by jquery.
        var form = $(this.$form[0]);
        // Get even disabled inputs
        var disabled = form.find(':input:disabled').removeAttr('disabled');
        var serializedForm = this.$form.serialize();
        // Reset disabled inputs
        disabled.attr('disabled','disabled');

        $.post(submitUrl, serializedForm)
            .done(this.ajaxSuccessHandler.bind(this))
            .fail(this.submitFailureHandler.bind(this));
    };

    n.Form.prototype.serialize = function () {
        return this.$form.serialize();
    };


    /**
     * @private
     * @param url base url
     * @param queryParameters additional query parameters
     * @returns {*}
     */
    n.Form.prototype.makeUrl = function (url, queryParameters) {
        var notFound = -1;
        var querySeparator = '?';
        var parameterSeparator = '&';
        var queryString;

        if (!queryParameters) {
            return url;
        }

        queryString = $.param(queryParameters);
        if (url.indexOf(querySeparator) === notFound) {
            return url + querySeparator + queryString;
        } else {
            return url + parameterSeparator + queryString;
        }
    };

    /**
     *
     * @param data
     * @param textStatus
     * @param jqXHR
     *
     * @private
     */
    n.Form.prototype.ajaxSuccessHandler = function (data, textStatus, jqXHR) {
        // If there are some needed element changes after save - update them here
        // Currently used for multiform delete buttons after save
        if (data['changed-elements'] !== undefined) {
            n.ElementUpdate.handleChangedElements(data['changed-elements']);
        }

        this.resetFormChangedState();
        this.eventEmitter.emitEvent('form.submit.successful',
            n.EventEmitter.makePayload(this, data, {
                textStatus: textStatus,
                jqXHR: jqXHR
            }));
        
        this.saveInProgress = false;
    };

    /**
     *
     *
     * @private
     */
    n.Form.prototype.submitFailureHandler = function (jqXHR, textStatus, errorThrown) {
        this.eventEmitter.emitEvent('form.submit.failed', n.EventEmitter.makePayload(this, null, {
            textStatus: textStatus,
            errorThrown: errorThrown,
            jqXHR: jqXHR
        }));
    };

    /**
     * @public
     * @returns {*}
     */
    n.Form.prototype.validate = function () {
        this.eventEmitter.emitEvent('form.validation.before', n.EventEmitter.makePayload(this, null));
        var isValid;
        var form = document.forms[this.formId];
        var $form = $(form);

        if ($form.data('bs.validator')) {
            $form.validator('validate');
            isValid = !$form.data('bs.validator').hasErrors();
        } else {
            isValid = form.checkValidity();
        }

        if(!this.saveAllowed) isValid = false;

        this.eventEmitter.emitEvent('form.validation.after', n.EventEmitter.makePayload(this, {validationResult: isValid}));

        return isValid;
    };

    /**
     * @private
     */
    n.Form.prototype.validationError = function (data) {
        this.eventEmitter.emitEvent('form.validation.failed', n.EventEmitter.makePayload(this, {element: data.relatedTarget}));
    };

    /**
     * @private
     */
    n.Form.prototype.validationSuccess = function (data) {
        this.eventEmitter.emitEvent('form.validation.success', n.EventEmitter.makePayload(this, {element: data.relatedTarget}));
    };

    /**
     * Uses standard HTML Validation in native javascript for input elements
     *
     * @public
     */
    n.Form.prototype.getFirstNonValidElement = function () {
        var index;
        var form = document.getElementById(this.formId);
        var inputs = form.elements;
        var elementNumber = inputs.length;

        for (index = 0; index < elementNumber; index++) {
            var element = inputs[index];
            if (!element.willValidate) {
                continue;
            }

            if (!element.checkValidity()) {
                return element;
            }
        }

        return null;
    };

})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

/**
 * A custom history to use for undo and redo functionality.
 **/

    n.History = function() {
        this.history = [];
        this.pointer = 0;
    };

    n.History.prototype.put = function(object) {
        if (this.history.length > 1) {
            if (this.canGoForward()) {
                console.log("trying to remove history");
                this._removeForwardHistory();
            }
        }
        if (JSON.stringify(this.history[this.pointer]) !== JSON.stringify(object)) {
            this.history.push(object);
            this.pointer = this.history.length - 1;
        }
        console.log(this);
    };

    n.History.prototype.back = function() {
        if (this.canGoBack()) {
            this.pointer = this.pointer - 1;
            console.log(this.pointer + "/" + this.history.length);
            console.log(this.history);
            return this.history[this.pointer];
        } else {
            console.log("At the beginning of history");
            return false;
        }
    };

    n.History.prototype.forward = function() {
        console.log(this.pointer);
        if (this.canGoForward()) {
            this.pointer = this.pointer + 1;
            return this.history[this.pointer];
        } else {
            console.log("At the end of history");
            return false;
        }
    };

    n.History.prototype.canGoBack = function() {
        return this.pointer > 0;
    };

    n.History.prototype.canGoForward = function() {
        return this.pointer < this.history.length - 1;
    };

    n.History.prototype._removeForwardHistory = function() {
        this.history.splice(this.pointer + 1, this.history.length - this.pointer);
    };



})(QfqNS);
var QfqNS = QfqNS || {};
$(document).ready(function() {
    (function(n) {
        $('.qfq-inline-edit').on('click', function() {
            var dataSip = $(this).data('sip');
            var $input = $(this).children('textarea');
            var $label = $(this).children('div.qfq-inline-edit-label');

            if ($input.length === 0) {
                // If the textarea doesn't exist, retrieve it
                retrieveTextbox(dataSip, $label);
            } else {
                $input.removeClass('hidden');
                $input.focus();
                $label.addClass('hidden');
            }
        });
    })(QfqNS);
});
function retrieveTextbox(dataSip, $label) {
    $.ajax({
        url: "typo3conf/ext/qfq/Classes/Api/inlineEditLoad.php?s=" + dataSip,
        method: "POST",
        success: function(response) {
            // Create the textbox and modify its properties
            var $textBox = createTextbox(response);
            $textBox.removeClass('qfq-auto-grow');
            $label.addClass('hidden');
            insertAfterLabel($label, $textBox);
            setTextboxHeight($textBox);
            enableAutoGrow($textBox);
            registerBlurHandler($textBox, dataSip, $label);
        },
        error: function() {
            console.error('inlineEditLoad.php API error');
        }
    });
}
function registerBlurHandler($textBox, dataSip, $label) {
    // Register the blur event handler for the textbox
    $textBox.on('blur', function() {
        var updatedValue = $(this).val();

        // Send an AJAX request to update the record in the database
        $.ajax({
            url: "typo3conf/ext/qfq/Classes/Api/inlineEditSave.php?s=" + dataSip,
            method: "POST",
            data: { 'updatedValue': updatedValue },
            success: function(response) {
                $textBox.addClass('hidden');
                $label.text(response).removeClass('hidden');
            },
            error: function() {
                $textBox.addClass('hidden');
                $label.removeClass('hidden');
            }
        });
    });
}
function createTextbox(response) {
    // Create a textbox element from the server response
    var $textBox = $(response);
    $textBox.addClass('qfq-inline-edit-input');
    $textBox.attr('spellcheck', 'false');
    return $textBox;
}
function insertAfterLabel($label, $textBox) {
    // Insert the textbox after the label and set focus on the textbox
    $label.after($textBox);
    $textBox.focus();
}
function setTextboxHeight($textBox) {
    // Set the height of the textbox to match its content
    $textBox.css('height', 'auto');
    $textBox.css('height', $textBox[0].scrollHeight + 'px');
}
function enableAutoGrow($textBox) {
    // Enable the textbox to auto-grow based on its content
    $textBox.on('input', function() {
        // Set the height of the textbox to match its content
        $textBox.css('height', 'auto');
        $textBox.css('height', $textBox[0].scrollHeight + 'px');
    });
}



/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend Alert.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Manages Text Editor for Comments
     */
    n.LocalStorage = function (key) {
        this.storage = {};
        this.key = key;
        this.storage[this.key] = {};

        // Event Emitter is a Library qfq uses to emit custom Events.
        this.eventEmitter = new EventEmitter();
        this._read();
        if (this.storage[this.key] !== "undefined") {
            this.storage[this.key] = {};
        }
    };

    n.LocalStorage.prototype.on = n.EventEmitter.onMixin;

    n.LocalStorage.prototype._read = function() {
        var o = JSON.parse(localStorage.getItem("qfq"));
        if(o) {
            this.storage = o;
        }
    };

    n.LocalStorage.prototype._write = function() {
        localStorage.setItem("qfq", JSON.stringify(this.storage));
        console.log(localStorage.getItem("qfq"));
    };

    n.LocalStorage.prototype.get = function(key) {
        if (this.storage[this.key][key] !== "undefined") {
            console.log(this.storage);
            return this.storage[this.key][key];
        } else {
            return false;
        }
    };

    n.LocalStorage.prototype.set = function(key, object) {
        if (this.storage[this.key][key] === "undefined") {
            this.storage[this.key][key] = {};
        }
        if(object) {
            this.storage[this.key][key] = object;
            this._write();
        }
    };

    n.LocalStorage.prototype.update = function() {
        this._read();
    };


}(QfqNS));
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global console */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     *
     * @type {{level: number, message: Function, debug: Function, warning: Function, error: Function}}
     *
     * @name QfqNS.Log
     */
    n.Log = {
        level: 3,
        message: function (msg) {
            if (this.level <= 0) {
                console.log('[message] ' + msg);
            }
        },
        debug: function (msg) {
            if (this.level <= 1) {
                console.log('[debug] ' + msg);
            }
        },
        warning: function (msg) {
            if (this.level <= 2) {
                console.log('[warning] ' + msg);
            }
        },
        error: function (msg) {
            if (this.level <= 3) {
                console.log('[error] ' + msg);
            }
        }

    };
})(QfqNS);
/* global $ */
/* @depend TablesorterController.js */

Function.prototype.bind = Function.prototype.bind || function (thisp) {
    var fn = this;

    return function () {
        return fn.apply(thisp, arguments);
    };
};

var QfqNS = QfqNS || {};

$(document).ready(function () {
    (function (n) {
        n.form = '';
        n.messenger = '';
        try {
            var tablesorterController = new n.TablesorterController();
            $('.tablesorter').each(function (i) {
                tablesorterController.setup($(this), i);
            }); // end .each()

            $('.tablesorter-filter').addClass('qfq-skip-dirty');
            $('select.qfq-tablesorter-menu-item').addClass('qfq-skip-dirty');
            $('.tablesorter-column-selector>label>input').addClass('qfq-skip-dirty');

            // This is needed because after changing table-view, class of input field is empty again
            $('button.qfq-column-selector').click(function () {
                $('.tablesorter-column-selector>label>input').addClass('qfq-skip-dirty');
            });

            // Ensures filter input/select use dynamic width
            $('.tablesorter-filter-row input').attr('size', 1);
            $('.tablesorter-filter-row select').attr('size', 1);

            // Initialize QfqInlineFilter for Report inline filtering
            if (n.Helper && n.Helper.QfqInlineFilter) {
                var qfqInlineFilter = new n.Helper.QfqInlineFilter();
                qfqInlineFilter.init();
            }

            var collection = document.getElementsByClassName("qfq-form");
            var qfqPages = [];
            for (const form of collection) {
                const page = new n.QfqPage(form.dataset);
                qfqPages.push(page);
            }

            // Get form object for later manipulations (example: filePond objects)
            if (qfqPages[0] !== undefined) {
                n.form = qfqPages[0].qfqForm.form;
            } else {
                // Readonly fabric for report
                $(".annotate-graphic").each(function () {
                    var qfqFabric = new QfqNS.Fabric();
                    var page = {}
                    qfqFabric.initialize($(this), page);
                });

                $(".annotate-text").each(function () {
                    var codeCorrection = new QfqNS.CodeCorrection();
                    var page = {}
                    codeCorrection.initialize($(this), page);
                });

            }
        } catch (e) {
            console.log(e);
        }

        $('.qfq-auto-grow').each(function () {
            var minHeight = $(this).attr("rows") * 14 + 18;
            var newHeight = $(this).prop('scrollHeight');
            var maxHeight = $(this).data('max-height') || 0;

            if ($(this).val === '' || newHeight < minHeight) {
                return;
            }

            if (newHeight < maxHeight || maxHeight === 0) {
                $(this).height(newHeight);
            } else {
                $(this).height(maxHeight);
            }
        });

        $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
            $('.qfq-auto-grow').each(function () {
                // Reset height before re-calculation
                $(this).css('height', 'auto');

                var minHeight = $(this).attr("rows") * 14 + 18;
                var newHeight = $(this).prop('scrollHeight');
                var maxHeight = $(this).data('max-height') || 0;

                if ($(this).val === '' || newHeight < minHeight) {
                    return;
                }

                if (newHeight < maxHeight || maxHeight === 0) {
                    $(this).height(newHeight);
                } else {
                    $(this).height(maxHeight);
                }
            });
        });

        $('.qfq-auto-grow').on('input paste', function () {
            var newHeight = $(this).prop('scrollHeight');
            var maxHeight = $(this).data('max-height') || 0;
            if ($(this).outerHeight() < newHeight) {
                if (newHeight < maxHeight || maxHeight === 0) {
                    $(this).height(newHeight);
                }
            }
        });

        n.initializeQfqClearMe = function () {
            $('.qfq-clear-me').each(function () {
                var myInput = $(this);
                if (!myInput.is("input,textarea") || myInput.parent().children().hasClass('qfq-clear-me-button')) {
                    return;
                }
                var closeButton = $("<span>", {
                    class: "qfq-clear-me-button",
                    html: "&times;"
                });
                if (myInput.hasClass('qfq-clear-me-table-sorter')) {
                    // Clear me is in table-sorter table and needs some different style
                    closeButton.attr('style', 'right: 3px;top: 11px;');
                }
                if (myInput.val() == '' || myInput.is('[disabled=disabled]')) {
                    closeButton.addClass("hidden");
                }
                closeButton.on("click", function (e) {
                    myInput.val('');
                    closeButton.addClass("hidden");
                    myInput.trigger('change');
                });
                $(this).after(closeButton);
                $(this).on("input", function () {
                    if (myInput.val() != '' && !myInput.is('[disabled=disabled]')) {
                        closeButton.removeClass("hidden");
                    } else {
                        closeButton.addClass("hidden");
                    }
                });

            });


            $('.qfq-clear-me-multiform').each(function () {
                var myInput = $(this);
                if (!myInput.is("input,textarea") || myInput.parent().children().hasClass('qfq-clear-me-button')) {
                    return;
                }
                var closeButton = $("<span>", {
                    class: "qfq-clear-me-multiform-button",
                    html: "&times;"
                });
                if (myInput.val() == '' || myInput.is('[disabled=disabled]')) {
                    closeButton.addClass("hidden");
                }
                closeButton.on("click", function (e) {
                    myInput.val('');
                    closeButton.addClass("hidden");
                    myInput.trigger('change');
                });
                $(this).after(closeButton);
                $(this).on("input", function () {
                    if (myInput.val() != '' && !myInput.is('[disabled=disabled]')) {
                        closeButton.removeClass("hidden");
                    } else {
                        closeButton.addClass("hidden");
                    }
                });

            });
        };

        n.initializeToggleInput = function () {
            document.querySelectorAll('.toggle-input-checkbox').forEach(checkbox => {
                const targetId = checkbox.dataset.toggleTarget;
                const defaultValue = checkbox.dataset.defaultValue || '';
                const target = document.getElementById(targetId);
                const isDynamicUpdate = checkbox.dataset.dynamicUpdate || false;
                if (!target) return;
                const originalValues = new Map(); // Tracks user-entered values
                const inputs = target.querySelectorAll('input, textarea, select');
                const applyToggle = () => {
                    const isChecked = checkbox.checked;

                    target.classList.toggle('qfq-toggle-hidden', !isChecked);
                    inputs.forEach((input, index) => {
                        const inputKey = input.name || input.id || `input-${index}`;

                        // === Handle <select> ===
                        if (input.tagName === 'SELECT') {

                            if (!isChecked) {
                                // Save original selection
                                const selected = Array.from(input.selectedOptions).map(o => o.value);
                                originalValues.set(inputKey, selected);


                                // If no matching default, insert a fallback <option> with the defaultValue
                                const hasMatch = Array.from(input.options).some(opt => opt.value === defaultValue);
                                if (!hasMatch) {
                                    let fallbackOption = input.querySelector('option[data-toggle-fallback]');
                                    if (!fallbackOption) {
                                        fallbackOption = document.createElement('option');
                                        fallbackOption.value = defaultValue;
                                        fallbackOption.text = '—';
                                        fallbackOption.setAttribute('data-toggle-fallback', 'true');
                                        input.prepend(fallbackOption);
                                    }
                                    fallbackOption.selected = true;
                                } else {
                                    // Apply default values
                                    Array.from(input.options).forEach(option => {
                                        option.selected = option.value === defaultValue;
                                    });
                                }

                            } else {
                                // Restore previous value
                                const fallbackOption = input.querySelector('option[data-toggle-fallback]');
                                if (fallbackOption) {
                                    fallbackOption.remove();
                                }
                                if (originalValues.has(inputKey)) {
                                    const saved = originalValues.get(inputKey);
                                    Array.from(input.options).forEach(option => {
                                        option.selected = saved.includes(option.value);
                                    });
                                }
                            }
                        } else {
                            // === Handle <input>, <textarea> ===
                            if (!isChecked) {

                                originalValues.set(inputKey, input.value);

                                input.value = defaultValue;
                            } else {
                                if (originalValues.has(inputKey)) {
                                    input.value = originalValues.get(inputKey);
                                }
                            }
                        }

                        if (isDynamicUpdate) {
                            n.form.qfqForm.formUpdateHandler();
                        }
                    });
                };

                // Initial state check
                applyToggle();
                // Bind to change event
                checkbox.addEventListener('change', applyToggle);
            });
        };


        n.initializeDatetimepicker = function (flagDynamicElement) {
            var selector = '.qfq-datepicker';
            if (flagDynamicElement) {
                selector = '.qfq-datepicker:empty';
            }

            $(selector).each(function () {
                var dates = {};
                var dateArray = {};
                var datesToFormat = ["minDate", "maxDate"];
                var correctAttributeNames = ["mindate", "maxdate"];
                var onlyTime = false;
                for (var i = 0; i < datesToFormat.length; i++) {
                    var date = false;
                    if ($(this).data(correctAttributeNames[i])) {
                        var cleanDate = $(this).data(correctAttributeNames[i]).split(" ")[0];
                        var cleanTime = $(this).data(correctAttributeNames[i]).split(" ")[1];
                        if (cleanDate.includes('.')) {
                            dateArray = cleanDate.split(".");
                            date = dateArray[1] + "/" + dateArray[0] + "/" + dateArray[2];
                        } else if (cleanDate.includes('-')) {
                            dateArray = cleanDate.split("-");
                            date = dateArray[1] + "/" + dateArray[2] + "/" + dateArray[0];
                        } else {
                            var today = new Date();
                            var dd = String(today.getDate()).padStart(2, '0');
                            var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0!
                            var yyyy = today.getFullYear();
                            date = mm + "/" + dd + "/" + yyyy + " " + cleanDate;
                            onlyTime = true;
                        }
                        if (cleanTime !== '' && cleanTime !== undefined) {
                            date = date + " " + cleanTime;
                        } else if (correctAttributeNames[i] === "maxdate" && !onlyTime) {
                            date = date + " 23:59:59";
                        }
                    }
                    dates[datesToFormat[i]] = date;
                }

                var options = {
                    locale: $(this).data("locale") || "en",
                    daysOfWeekDisabled: $(this).data("days-of-week-disabled") || [],
                    minDate: dates.minDate,
                    maxDate: dates.maxDate,
                    format: $(this).data("format") || "DD.MM.YYYY HH:mm",
                    viewMode: $(this).data("view-mode-default") || "days",
                    showClear: ($(this).data("show-clear-button") !== undefined) ? $(this).data("show-clear-button") : true,
                    calendarWeeks: ($(this).data("show-calendar-weeks") !== undefined) ? $(this).data("show-calendar-weeks") : false,
                    useCurrent: ($(this).data("use-current-datetime") !== undefined) ? $(this).data("use-current-datetime") : false,
                    sideBySide: ($(this).data("datetime-side-by-side") !== undefined) ? $(this).data("datetime-side-by-side") : false,
                };
                var currentDatePicker = $(this).datetimepicker(options);

                currentDatePicker.on('dp.error', function(event) {
                    // Clear the input field
                    $(this).find('input').val('');
                    $(this).data('DateTimePicker').clear();

                    // Display your custom error message
                    var alert = new QfqNS.Alert({
                        message: "Invalid Date. Min: " + dates.minDate + " Max: " + dates.maxDate,
                        type: "warning",
                        timeout: 5000
                    });
                    alert.show();
                });

                //  Markiere das formular als geändert, damit der Speichern-Button freigegeben wird
                $(this).on('dp.change', function(e) {
                    const $input = $(this).find('input');

                    if (QfqNS.form && typeof QfqNS.form.markChanged === 'function') {
                        QfqNS.form.markChanged($input[0]);
                    }
                });

            });
        };

        n.initializeIgnoreHistoryBtn = function () {
            // Attaching the event listener to the document
            document.addEventListener('click', function (event) {
                var element = event.target;

                // Traverse up to find the element with 'data-ignore-history'
                while (element && !element.hasAttribute('data-ignore-history')) {
                    element = element.parentNode;
                    if (element === document) {
                        return; // Exit if reached the document without finding the target
                    }
                }
                // Only execute if the element exists and is not Disabled
                if (element && !element.classList.contains('disabled')) {
                    handleIgnoreHistoryClick(event, element);
                }
            });
        };

        function handleIgnoreHistoryClick(event, element) {
            event.preventDefault();
            let alertButton = document.querySelector('.alert-interactive .btn-group button:first-child');
            let url = element.href;

            if (alertButton) {
                alertButton.onclick = function () {
                    if (url) {
                        window.location.replace(url);
                    }
                };
            } else {
                window.location.replace(url);
            }
        }

        n.initializeImageModal = function () {

            // Get images
            var images = document.getElementsByClassName('qfq-img-modal');
            var len = images.length;

            // Iterate through images and add modal functionality to each one of them
            for (let i = 0; i < len; i++) {
                let modalHtml = `
                    <div id="qfq-img-modal-container-${i}" class="modal qfq-img-modal-container" role="dialog">
                        <div class="modal-dialog qfq-img-modal-dialog" role="document">
                            <div class="modal-content qfq-img-modal-content">
                                <img id="qfq-img-modal-${i}" class="qfq-img-modal-img">
                            </div>
                        </div>
                    </div>`;

                let img = images[i];

                // Add modal HTML and attributes
                img.insertAdjacentHTML('afterend', modalHtml);
                img.setAttribute('data-toggle', 'modal');
                img.setAttribute('data-target', `#qfq-img-modal-container-${i}`);

                // Add source
                let modalImg = document.getElementById(`qfq-img-modal-${i}`);
                modalImg.src = img.src;
            }
        }

        n.initializeQfqClearMe();
        n.initializeDatetimepicker();
        n.initializeIgnoreHistoryBtn();
        n.initializeImageModal();
        n.initializeToggleInput();
        n.Helper.codemirror();
        n.Helper.calendar();
        n.Helper.selectBS();
        n.Helper.initializeStickyToolTip();
        n.Helper.initializeDownloadProcess();
        n.Helper.initializeDevPanel();
        n.Helper.initializeEmailSelect();
        n.Helper.initColorPicker(n.form);
        n.messenger = n.Helper.qfqMessenger({
            autoConnect: true
        });

        // Initialize chat elements
        let chatWindowsElements = document.getElementsByClassName("qfq-chat-window");
        let chatInstances = n.Helper.qfqChat(chatWindowsElements);

        if (typeof FilePond !== 'undefined') {
            FilePond.registerPlugin(FilePondPluginFileValidateSize);
            FilePond.registerPlugin(FilePondPluginFileValidateType);
        }

        // Initialize merge window
        let mergeWindow = document.querySelector('.merge-window');
        if (mergeWindow !== null) {
            let mergeInstance = n.Helper.qfqMerge(mergeWindow);
        }

        // Initialize form history
        let formHistoryBtn = document.querySelector('.qfq-history-btn');
        if (formHistoryBtn !== null) {
            n.Helper.qfqHistory(formHistoryBtn, n);
        }

        // Initialize SyncByRule buttons AND containers
        let syncByRuleElements = document.querySelectorAll('.qfq-syncByRule, .qfq-syncByRule-container');
        if (syncByRuleElements.length > 0) {
            n.Helper.qfqSyncByRule(syncByRuleElements, n);
        }

        // Initialize InputCollectionJson configuration editors
        let icjElements = document.querySelectorAll('textarea.qfq-icj');
        if (icjElements.length > 0) {
            n.Helper.qfqIcj(icjElements, n);
        }

        // Get currently shown upload elements to initialize them
        let activeTabContent = null;
        if (n.form) {
            const activeTabId = n.form.qfqForm.bsTabs ? n.form.qfqForm.bsTabs.currentTab : null;
            activeTabContent = document.getElementById(activeTabId);
        }
        if (activeTabContent) {
            n.Helper.initializeFilePondInContainer(activeTabContent, n.form);

            const reportUploadElements = document.querySelectorAll('div.fileuploadContainer');
            reportUploadElements.forEach(container => {
                n.Helper.initializeFilePondInContainer(container, n.form);
            });
        } else {
            // In case of forms without tabs or reports
            n.Helper.initializeFilePondInContainer(document, n.form);
        }
        n.initializeQmore();
    })(QfqNS);
});
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* @depend QfqEvents.js */
/* global EventEmitter */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';


    /**
     *
     * @constructor
     * @name QfqNS.PageState
     */
    n.PageState = function () {
        let hashValue = location.hash.slice(1).split(',')[0];
        // Check if hash value is not from tablesorter view
        const regex = /(=public:|=private:)/;
        if (regex.test(hashValue)) {
            hashValue = '';
        }

        this.pageState = hashValue;
        this.data = null;
        this.inPoppingHandler = false;
        this.eventEmitter = new EventEmitter();

        window.addEventListener("popstate", this.popStateHandler.bind(this));
    };

    n.PageState.prototype.on = n.EventEmitter.onMixin;
    /**
     *
     * @param event
     *
     * @private
     */
    n.PageState.prototype.popStateHandler = function (event) {

        n.Log.debug("Enter: PageState.popStateHandler()");

        this.inPoppingHandler = true;
        this.pageState = location.hash.slice(1);
        this.data = window.history.state;

        n.Log.debug("PageState.popStateHandler(): invoke user pop state handler(s)");

        this.eventEmitter.emitEvent('pagestate.state.popped', n.EventEmitter.makePayload(this, null));

        this.inPoppingHandler = false;
        n.Log.debug("Exit: PageState.popStateHandler()");

    };

    n.PageState.prototype.getPageState = function () {
        return this.pageState;
    };

    n.PageState.prototype.getPageData = function () {
        return this.data;
    };

    n.PageState.prototype.setPageState = function (state, data) {
        if (state.startsWith('#')) {
            this.pageState = state.slice(1);
            window.history.replaceState(data, null, state);
        } else {
            this.pageState = state;
            window.history.replaceState(data, null, '#' + state);
        }
        this.data = data;
    };

    n.PageState.prototype.newPageState = function (state, data) {
        if (state.startsWith('#')) {
            this.pageState = state.slice(1);
            window.history.pushState(data, null, state);
        } else {
            this.pageState = state;
            window.history.pushState(data, null, '#' + state);
        }

        this.data = data;
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     *
     * @type {{set: Function, get: Function, setSubTitle: Function}}
     *
     * @name QfqNS.PageTitle
     */
    n.PageTitle = {
        set: function (title) {
            if (title !== null) {
                document.title = title;
            }
        },
        get: function () {
            return document.title;
        },
        setSubTitle: function (subTitle) {
            var currentTitle = this.get();
            var subtitleStrippedOff = currentTitle.replace(/ - (.*)$/, '');
            document.title = subtitleStrippedOff + " - (" + subTitle + ")";
        }
    };
})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */
/* @depend ElementUpdate.js */
/* @depend Dirty.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    // TODO: This object is getting its own gravitational field. Start refactoring.
    /**
     * Represents a QFQ Form.
     *
     * QfqForm will autonomously fire a lock `extend` request when the lock expired, but the last change `t_c` has
     * been made during the lock period `t_l`. I.e. let `t_{current}` be the current time, an `extend` request is made
     * when
     *
     *    t_c + t_l > t_{current}
     *
     * holds.
     *
     * @param formId {string} value of the form's id attribute
     * @param submitTo {string} url where data will be submitted to
     * @param deleteUrl {string} url to call upon record deletion
     * @param dataRefreshUrl {string} url where to fetch new element values from
     * @param fileUploadTo {string} url used for file uploads
     * @param fileDeleteUrl {string} url used to delete files
     * @param dirtyUrl {string} url used to query
     * @param apiDeleteUrl {string} url used for multiform delete
     * @constructor
     *
     * @name QfqNS.QfqForm
     */
    n.QfqForm = function (formId, submitTo, deleteUrl, dataRefreshUrl, fileUploadTo, fileDeleteUrl, dirtyUrl, apiDeleteUrl) {
        this.formId = formId;
        this.submitTo = submitTo;
        this.deleteUrl = deleteUrl;
        this.dataRefreshUrl = dataRefreshUrl;
        this.fileUploadTo = fileUploadTo;
        this.fileDeleteUrl = fileDeleteUrl;
        this.dirtyUrl = dirtyUrl;
        this.apiDeleteUrl = apiDeleteUrl;
        this.dirtyFired = false;
        this.lockAcquired = false;
        this.formImmutableDueToConcurrentAccess = false;
        this.lockRenewalPhase = false;
        this.goToAfterSave = false;
        this.skipRequiredCheck = false;
        this.activateFirstRequiredTab = true;

        this.additionalQueryParameters = {
            'recordHashMd5': this.getRecordHashMd5()
        };

        if (!!$('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('enable-save-button')) {
            this.form = new n.Form(this.formId, false);
            this.getSaveButton().removeClass("disabled").removeAttr("disabled");
        } else {
            this.getSaveButton().addClass("disabled").attr("disabled", "disabled");
            this.form = new n.Form(this.formId, false);
        }

        if ($('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('required-off-but-mark')) {
            this.skipRequiredCheck = true;
        } else {
            this.skipRequiredCheck = false;
        }

        if (typeof $('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('activate-first-required-tab') !== 'undefined') {
            this.activateFirstRequiredTab = $('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('activate-first-required-tab');
        }

        this.infoLockedButton = this.infoLockedButton.bind(this);

        // This is required when displaying validation messages, in order to activate the tab, which has validation
        // issues
        this.bsTabs = null;
        this.lastButtonPress = null;

        this.eventEmitter = new EventEmitter();

        this.dirty = new n.Dirty(this.dirtyUrl);
        this.dirty.on(n.Dirty.EVENTS.SUCCESS, this.dirtyNotifySuccess.bind(this));
        this.dirty.on(n.Dirty.EVENTS.DENIED, this.dirtyNotifyDenied.bind(this));
        this.dirty.on(n.Dirty.EVENTS.FAILED, this.dirtyNotifyFailed.bind(this));
        this.dirty.on(n.Dirty.EVENTS.SUCCESS_TIMEOUT, this.dirtyTimeout.bind(this));
        this.dirty.on(n.Dirty.EVENTS.RENEWAL_DENIED, this.dirtyRenewalDenied.bind(this));
        this.dirty.on(n.Dirty.EVENTS.RENEWAL_SUCCESS, this.dirtyRenewalSuccess.bind(this));
        this.dirty.on(n.Dirty.EVENTS.CHECK_SUCCESS, this.dirtyCheckSuccess.bind(this));
        this.dirty.on(n.Dirty.EVENTS.CHECK_FAILED, this.dirtyCheckFailed.bind(this));

        this.form.on('form.changed', this.changeHandler.bind(this));
        this.form.on('form.reset', this.resetHandler.bind(this));
        this.form.on('form.submit.successful', this.submitSuccessDispatcher.bind(this));
        this.form.on('form.submit.failed', function (obj) {
            n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
        });

        this.getSaveButton().click(this.handleSaveClick.bind(this));
        this.getCloseButton().click(this.handleCloseClick.bind(this));
        this.getNewButton().click(this.handleNewClick.bind(this));
        this.getDeleteButton().click(this.handleDeleteClick.bind(this));
        this.getButtonPrevious().click(this.handlePreviousNextClick.bind(this));
        this.getNextButton().click(this.handlePreviousNextClick.bind(this));

        var that = this;
        $('.external-save').click(function(e) {
            var uri = $(this).data('target') || $(this).attr("href");
            that.callSave(uri);
            e.preventDefault();
            return false;
        });

        const multiToggles = document.querySelectorAll('[data-multi-toggle]');
        var initialized = false;
        var beautyContainer = null;
        multiToggles.forEach((multiToggle) => {
            const container = multiToggle.dataset.container || "tr"
            const element = multiToggle.closest(container).querySelector("" + multiToggle.dataset.multiToggle)
            if (!multiToggle.checked && element.value == '') { 
                element.classList.add("hidden")
            } else {
                multiToggle.checked = true
            }
            multiToggle.addEventListener("change", (e) => {
                if(multiToggle.checked) {
                    element.classList.remove("hidden");
                } else {
                    element.classList.add("hidden");
                }
            });

            if(multiToggle.dataset.beautyHack) {
                const groupContainer = multiToggle.closest("table");
                if (!initialized)  {
                    beautyContainer = document.createElement("div");
                    beautyContainer.classList.add("text");
                    groupContainer.parentElement.prepend(beautyContainer);
                    groupContainer.classList.add("hidden");
                    initialized = true;
                }
                const bElement = document.createElement("div");
                bElement.style.marginBottom = "25px"
                const cContainer = multiToggle.closest(container)
                beautyContainer.append(bElement);
                console.log("beautify", bElement);
                console.log("Container", element);
                Array.from(cContainer.children).forEach((child) => {
                    const myElement = document.createElement("div");
                    Array.from(child.children).forEach((bChild) => {
                        myElement.append(bChild);
                    })
                    bElement.append(myElement);
                    console.log("Children", child)
                });
            }
        });

        this.setupFormUpdateHandler();
        if (!!$('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('disable-return-key-submit')) {
            // Nothing to do
        } else {
            this.setupEnterKeyHandler();
        }

        this.fileUploader = new n.FileUpload('#' + this.formId, this.fileUploadTo);
        this.startUploadHandler = this.startUploadHandler.bind(this);
        this.fileUploader.on('fileupload.started', this.startUploadHandler);
        this.fileUploader.on('fileupload.upload.successful', that.fileUploadSuccessHandler);

        this.fileUploader.on('fileupload.upload.failed',
            function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
            });
        this.endUploadHandler = this.endUploadHandler.bind(this);
        this.fileUploader.on('fileupload.ended', this.endUploadHandler);

        this.fileDeleter = new n.FileDelete("#" + this.formId, this.fileDeleteUrl);
        this.fileDeleter.on('filedelete.delete.successful', this.fileDeleteSuccessHandler.bind(this));

        this.fileDeleter.on('filedelete.delete.failed',
            function (obj) {
                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
            });

        var configurationData = this.readFormConfigurationData();
        this.applyFormConfiguration(configurationData);

        n.Helper.tinyMce();
        n.Helper.codemirror();

        this.form.on('form.submit.before', n.Helper.tinyMce.prepareSave);
        this.form.on('form.validation.before', n.Helper.tinyMce.prepareSave);
        this.form.on('form.validation.failed', this.validationError);
        this.form.on('form.validation.success', this.validationSuccess);
        this.form.qfqForm = this;

        $(".radio-inline").append($("<span>", { class: "checkmark", aria: "hidden"}));
        $(".checkbox-inline").append($("<span>", { class: "checkmark", aria: "hidden"}));
        $(".radio").append($("<span>", { class: "checkmark", aria: "hidden"}));
        $(".checkbox").each(function() {
            if (!this.querySelector(".checkmark")) {
                $(this).append($("<span>", {class: "checkmark", aria: "hidden"}));
            }
        });

        // Feature process all rows
        $(".process-row-all input[type=checkbox]").on("click", function() {
            var checkboxes = document.querySelectorAll('input[name^="_processRow-"]');
            for (var i = 0; i < checkboxes.length; i++) {
                if ($(checkboxes[i]).is(':visible')) {
                    checkboxes[i].checked = $(this).is(':checked');
                }
            }
        });

        this.dirty.check(this.getSip(), this.getRecordHashMd5AsQueryParameter());
        this.handleMultiStateCheckBox();
    };

    n.QfqForm.prototype.on = n.EventEmitter.onMixin;

    n.QfqForm.prototype.dirtyNotifySuccess = function (obj) {
        this.lockAcquired = true;
        // Intentionally empty. May be used later on.
    };

    n.QfqForm.prototype.dirtyCheckSuccess = function (obj) {
        // Same user has Locked the form give the option to release Lock and reload Page
        if (obj.data.sameUser) {
            this.lockAcquired = true;
            const that = this;

            const alert = new n.Alert({
                message: obj.data.message + "<br>Do you want to release the lock and reload?",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Yes", eventName: "ok"},
                    {label: "No", eventName: "cancel", focus: true}
                ]
            });

            alert.on("alert.ok", function () {
                that.releaseLock(true);
                setTimeout(() => {
                    window.location.reload();
                }, 300);
            });

            alert.show();
        }
    };

    n.QfqForm.prototype.dirtyCheckFailed = function (obj) {
        var message = new n.Alert({
            message: obj.data.message,
            type: "error",
            timeout: n.Alert.constants.NO_TIMEOUT,
            modal: true,
            buttons: [{
                label: "Close",
                eventName: 'close'
            }]
        });
        message.show();
    };

    n.QfqForm.prototype.dirtyRenewalSuccess = function (obj) {
        this.lockAcquired = true;
    };

    /**
     * @public
     */
    n.QfqForm.prototype.releaseLock = function (async) {
        if (!this.lockAcquired) {
            n.Log.debug("releaseLock(): no lock acquired or already released.");
            return;
        }
        n.Log.debug("releaseLock(): releasing lock.");
        this.dirty.release(this.getSip(), this.getRecordHashMd5AsQueryParameter(), async);
        this.resetLockState();
    };

    n.QfqForm.prototype.resetLockState = function () {
        this.dirty.clearSuccessTimeoutTimerIfSet();
        this.dirtyFired = false;
        this.formImmutableDueToConcurrentAccess = false;
        this.lockRenewalPhase = false;
        this.lockAcquired = false;
    };

    n.QfqForm.prototype.dirtyRenewalDenied = function (obj) {
        var that = this;
        var messageButtons = [{
            label: "Reload",
            eventName: 'reload'
        }];
        if (obj.data.status == "conflict_allow_force") {
            messageButtons.push({
                label: "Continue",
                eventName: 'ignore'
            });
        }
        var alert = new n.Alert(
            {
                type: "error",
                message: obj.data.message,
                modal: true,
                buttons: messageButtons
            }
        );
        alert.on('alert.reload', function () {
            that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));
            window.location.reload(true);
        });
        alert.on('alert.ignore', function () {
            console.log("Ignored Recordlock");
        });
        alert.show();
    };

    n.QfqForm.prototype.dirtyTimeout = function (obj) {
        this.dirtyFired = false;
        this.lockAcquired = false;
        this.lockRenewalPhase = true;

        // Figure out whether the user made changes in the lock timeout period
        if (this.form.formChangedTimestampInMilliseconds + this.dirty.lockTimeoutInMilliseconds >
            Date.now()) {
            // Renew without user intervention.
            this.fireDirtyRequestIfRequired();
            // and bail out
            return;
        }
        var alert = new n.Alert(
            {
                message: "Exclusive access to document timed out.",
                type: "warning"
            }
        );
        alert.show();
    };

    n.QfqForm.prototype.dirtyNotifyDenied = function (obj) {
        var messageType;
        var isModal = true;
        var messageButtons = [{
            label: "Reload",
            eventName: 'reload'
        }];
        var message;
        var that = this;

        switch (obj.data.status) {
            case "conflict":
                messageType = "error";
                this.setButtonEnabled(this.getSaveButton(), false);
                this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
                this.setButtonEnabled(this.getDeleteButton(), false);
                this.formImmutableDueToConcurrentAccess = true;
                this.lockAcquired = false;
                break;
            case "conflict_allow_force":
                messageType = "warning";

                messageButtons = [{
                    label: "Continue",
                    eventName: 'ignore'
                }, {
                    label: "Cancel",
                    eventName: 'reload'
                }];
                break;
            case "error":
                messageType = "error";
                this.setButtonEnabled(this.getSaveButton(), false);
                this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
                this.setButtonEnabled(this.getDeleteButton(), false);
                // Do not make the form ask for saving changes.
                this.form.formChanged = false;
                this.formImmutableDueToConcurrentAccess = true;
                this.lockAcquired = false;
                break;
            default:
                n.Log.error('Invalid dirty status: \'' + obj.data.status + '\'. Assume messageType \'error\'');
                messageType = "error";
                break;
        }

        message = new n.Alert({
            message: obj.data.message,
            type: messageType,
            timeout: n.Alert.constants.NO_TIMEOUT,
            modal: isModal,
            buttons: messageButtons
        });
        message.on('alert.reload', function () {
            that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));
            window.location.reload(true);
        });
        message.show();
    };

    n.QfqForm.prototype.dirtyNotifyFailed = function () {
        this.dirtyFired = false;
        this.lockAcquired = false;
    };

    n.QfqForm.prototype.validationError = function (info) {
        var $formControl = $(info.data.element);
        var $messageContainer = $formControl.siblings('.hidden.with-errors');

        if ($messageContainer.length === 0) {
            if ($formControl.parent().hasClass('input-group') || $formControl.parent('.twitter-typeahead')) {
                $messageContainer = $formControl.parent().siblings('.hidden.with-errors');
            }
        }

        $messageContainer.data('qfq.hidden.message', true);
        $messageContainer.removeClass('hidden');
    };

    n.QfqForm.prototype.validationSuccess = function (info) {
        var $formControl = $(info.data.element);
        var $messageContainer = $formControl.siblings('.with-errors');

        if ($messageContainer.length === 0) {
            if ($formControl.parent().hasClass('input-group') || $formControl.parent('.twitter-typeahead')) {
                $messageContainer = $formControl.parent().siblings('.with-errors');
            }
        }

        if ($messageContainer.data('qfq.hidden.message') === true) {
            $messageContainer.addClass('hidden');
        }
    };

    /**
     * @private
     */
    n.QfqForm.prototype.setupEnterKeyHandler = function () {
        $("input").keyup(function (event) {

            // Prevent save if enter press comes from qfqChat search
            if ($(event.target).hasClass('chat-search-input')) {
                return;
            }

            if (this.formImmutableDueToConcurrentAccess) {
                return;
            }
            if (event.which === 13 && this.submitOnEnter()) {
                if (this.isFormChanged()) {
                    // Changed from: save&close to save
                    this.lastButtonPress = "save";
                    n.Log.debug("save click (enter)");
                    this.submit();
                }
                event.preventDefault();
            }
        }.bind(this));
    };


    /**
     *
     * @private
     */
    n.QfqForm.prototype.readFormConfigurationData = function () {
        var $configuredElements = $("#" + this.formId + " [data-hidden],#" + this.formId + " [data-disabled],#" + this.formId + " [data-required]");

        var configurationArray = [];
        $configuredElements.each(function (index, element) {
            try {
                var $element = $(element);
                if (!element.hasAttribute("name")) {
                    n.Log.warning("Element has configuration data, but no name. Skipping");
                    return;
                }

                var configuration = {};
                configuration['form-element'] = $element.attr('name');

                var hiddenVal = $element.data('hidden');
                if (hiddenVal !== undefined) {
                    configuration.hidden = n.Helper.stringToBool(hiddenVal);
                }

                var disabledVal = $element.data('disabled');
                if (disabledVal !== undefined) {
                    configuration.disabled = n.Helper.stringToBool(disabledVal);
                }

                var requiredVal = $element.data("required");
                if (requiredVal !== undefined) {
                    configuration.required = n.Helper.stringToBool(requiredVal);
                }

                configurationArray.push(configuration);
            } catch (e) {
                n.Log.error(e.message);
            }
        });

        return configurationArray;

    };

    /**
     * @public
     * @param bsTabs
     */
    n.QfqForm.prototype.setBsTabs = function (bsTabs) {
        this.bsTabs = bsTabs;
    };

    n.QfqForm.prototype._createError = function (message) {
        var messageButtons = [{
            label: "Ok",
            eventName: 'close'
        }];
        var alert = new n.Alert({ "message": message, "type": "error", modal: true, buttons: messageButtons});
        alert.show();
    };

    /**
     * @private
     */
    n.QfqForm.prototype.fileDeleteSuccessHandler = function (obj) {
        if (!obj.data.status) {
            throw Error("Response on file upload missing status");
        }

        if (obj.data.status === "error") {
            this._createError(obj.data.message);
            return;
        }

        var $button = $(obj.target);
        $button.prop("disabled", true);

        var $buttonParent = $button.parent();
        $buttonParent.addClass('hidden');

        var $inputFile = $buttonParent.siblings('label');
        $inputFile.children(':file').prop("disabled", false);
        $inputFile.removeClass('hidden');
        $inputFile.children(':file').removeClass('hidden');

        $inputFile.children(':file').val("");
        if ($inputFile.children(':file').data('required') == 'required') {
            $inputFile.children(':file').prop("required", true);
        }

        this.form.markChanged();
    };

    /**
     * @private
     */
    n.QfqForm.prototype.fileUploadSuccessHandler = function (obj) {
        if (!obj.data.status) {
            throw Error("Response on file upload missing status");
        }

        if (obj.data.status === "error") {
            //this._createError(obj.data.message);
            var messageButtons = [{
                label: "Ok",
                eventName: 'close'
            }];
            var alert = new n.Alert({ "message": obj.data.message, "type": obj.data.status, modal: true, buttons: messageButtons});
            alert.show();
            return false;
        }

        var $fileInput = $(obj.target);
        $fileInput.prop("disabled", true);
        $fileInput.addClass("hidden");
        $fileInput.parent().addClass("hidden");

        var $deleteContainer = $fileInput.parent().siblings('div.uploaded-file');


        var fileNamesString = obj.target.files[0].name;
        var $fileNameSpan = $deleteContainer.find("span.uploaded-file-name");
        $fileNameSpan.empty().append(fileNamesString);

        var $deleteButton = $deleteContainer.find("button");
        $deleteButton.prop("disabled", false);

        $deleteContainer.removeClass("hidden");
    };

    /**
     *
     * @param $button
     * @param enabled {boolean}
     *
     * @private
     */
    n.QfqForm.prototype.setButtonEnabled = function ($button, enabled) {
        if (!$button) {
            n.Log.error("QfqForm#setButtonEnabled(): no button provided.");
            return;
        }
        if (!enabled) {
            $button.addClass("disabled");
            $button.prop("disabled", true);
        } else {
            $button.removeClass("disabled");
            $button.prop("disabled", false);
        }
    };

    /* Dynamic Update Trigger */
    n.QfqForm.prototype.setupFormUpdateHandler = function () {
        // To support removing or adding elements dynamically first we first remove the event before re-binding
        $('textarea[data-load],input[data-load],select[data-load]').off('change dp.change');
        $('.qfq-datepicker[data-load]').off('dp.hide');

        // QFQ datepicker: trigger dynamic update only when picker closes (not on every selection)
        $('.qfq-datepicker[data-load]').on('dp.hide', this.formUpdateHandler.bind(this));

        // All other inputs: use change (fires on focus lost for text, on picker close for date)
        $('textarea[data-load],input[data-load],select[data-load]').on('change', this.formUpdateHandler.bind(this));
    };

    n.QfqForm.prototype.formUpdateHandler = function () {
        var that = this;
        if (this.formImmutableDueToConcurrentAccess) {
            return;
        }

        // For better dynamic update compatibility (checkboxes). All input elements need to be not disabled for fully serializing by jquery.
        var form = $(this.form.$form[0]);
        // Get all disabled inputs
        var disabled = form.find(':input:disabled').removeAttr('disabled');
        var serializedForm = this.form.serialize();
        // Reset disabled inputs
        disabled.attr('disabled','disabled');

        $.post(this.dataRefreshUrl, serializedForm, "json")
            .fail(n.Helper.showAjaxError)
            .done(function (data) {
                this.handleFormUpdate(data);
            }.bind(that));

    };

    n.QfqForm.prototype.handleFormUpdate = function (data) {
        if (!data.status) {
            throw new Error("Expected 'status' attribute to be present.");
        }

        if (data.status === "error") {
            this._createError("Error while updating form:<br>" +
                (data.message ? data.message : "No reason given"));
            return;
        }

        if (data.status === "success") {
            if (!data['form-update']) {
                throw new Error("'form-update' attribute missing in form update data");
            }


            this.applyFormConfiguration(data['form-update']);
            this.applyElementConfiguration(data['element-update']);
            // Add back this event to elements
            // This is done to Support Dynamically adding or removing elements like checkboxes.
            this.setupFormUpdateHandler();
            return;
        }

        throw new Error("Unexpected status: '" + data.status + "'");
    };

    /**
     * @private
     */
    n.QfqForm.prototype.destroyFormAndSetText = function (text) {
        this.form = null;
        $('#' + this.formId).replaceWith($("<p>").append(text));
        this.eventEmitter.emitEvent('qfqform.destroyed', n.EventEmitter.makePayload(this, null));
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleSaveClick = function () {

        // "save,force" if sqlValidate() should be ignored, default is "save"
        this.lastButtonPress = this.getSaveButton().attr('data-save-force') || "save" ;

        // Remove attribute
        this.getSaveButton().removeAttr('data-save-force');

        n.Log.debug("save click");
        this.checkHiddenRequired();
        this.getSaveButton().removeClass('btn-info');
        this.getSaveButton().addClass('btn-warning active disabled');
        if (!this.form.saveInProgress) {
            this.submit();
        }

        // Activate history button
        this.getHistoryButton().removeAttr('disabled');
    };

    n.QfqForm.prototype.callSave = function(uri) {
        if(this.isFormChanged()) {
            this.handleSaveClick();
            this.goToAfterSave = uri;
        } else {
            window.location = uri;
            return;
        }
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleCloseClick = function () {
        this.lastButtonPress = "close";
        if (this.form.getFormChanged()) {
            var alert = new n.Alert({
                message: "Unsaved Changes.",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Save", eventName: "yes"},
                    {label: "Discard", eventName: "no", focus: true},
                    {label: "Cancel", eventName: "cancel"}
                ]
            });
            var that = this;
            alert.on('alert.yes', function () {
                that.submit();
            });
            alert.on('alert.no', function () {
                that.releaseLock();
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                that.goBack();
            });
            alert.show();
        } else {
            this.goBack();
        }
    };
    /**
     * @private
     */
    n.QfqForm.prototype.handlePreviousNextClick = function (event) {
        // Prevent Link redirect
        event.preventDefault();
        this.lastButtonPress = "close";
        // Extract link form <a>
        var targetLink = event.currentTarget.href;
        if (this.form.getFormChanged()) {
            var alert = new n.Alert({
                message: "Unsaved Changes.",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Discard Changes", eventName: "no"},
                    {label: "Close", eventName: "cancel"}
                ]
            });
            var that= this;
            alert.on('alert.no', function () {
                that.releaseLock();
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                that.goBack();
            });
            alert.show();
        } else {
            // redirect to link if no form changes where made
            window.location.href = targetLink;
        }
    };

    n.QfqForm.prototype.submit = function (queryParameters) {
        var submitQueryParameters;
        console.log("Save in progress", submitQueryParameters);
        var alert;
        var submitReason;

        if (this.form.validate() !== true) {


            var element = this.form.getFirstNonValidElement();
            if (element.hasAttribute('name') && this.bsTabs) {
                var tabId = this.bsTabs.getContainingTabIdForFormControl(element.getAttribute('name'));
                if (tabId && this.activateFirstRequiredTab) {
                    this.bsTabs.activateTab(tabId);
                }


                var form = document.getElementById(this.formId);
                var inputs = form.elements;

                for (var i = 0; i < inputs.length; i++) {
                    var e = inputs[i];
                    if(!e.willValidate) {
                        continue;
                    }
                    if(!e.checkValidity()) {
                        var updateTabId = this.bsTabs.getContainingTabIdForFormControl(e.getAttribute('name'));
                        if(updateTabId != tabId) {
                            this.bsTabs.addDot(updateTabId);
                        }
                    }
                }
            }

            // Since we might have switched the tab, re-validate to highlight errors
            this.form.$form.validator('update');
            this.form.$form.validator('validate');

            this.form.$form.each(function() {
                if (!$(this).validate) {

                }
            });

            if (!this.skipRequiredCheck) {
                alert = new n.Alert("Form is incomplete.", "warning");
                alert.timeout = 3000;
                alert.show();
                return;
            }
        }

        // First, remove all validation states, in case a previous submit has set a validation state, thus we're not
        // stockpiling them.
        if (!this.skipRequiredCheck) {
            this.clearAllValidationStates();
        }

        submitReason = {
            "submit_reason": this.lastButtonPress === "close" ? "save,close" : this.lastButtonPress
        };

        // Change to "save" for following actions
        if (this.lastButtonPress === "save,force") {
            this.lastButtonPress = "save";
        }

        submitQueryParameters = $.extend({}, queryParameters, submitReason);
        this.form.submitTo(this.submitTo, submitQueryParameters);
        console.log("Submitting with", submitQueryParameters);
        this.form.saveInProgress = true;
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleNewClick = function (event) {
        event.preventDefault();

        this.lastButtonPress = "new";
        if (this.form.getFormChanged()) {
            var alert = new n.Alert({
                message: "Unsaved Changes.",
                type: "warning",
                modal: true,
                buttons: [
                    {label: "Save", eventName: "yes", focus: true},
                    {label: "Discard", eventName: "no"},
                    {label: "Cancel", eventName: "cancel"}
                ]
            });
            var that = this;
            alert.on('alert.no', function () {
                that.eventEmitter.emitEvent('qfqform.close-intentional', n.EventEmitter.makePayload(that, null));

                var anchorTarget = that.getNewButtonTarget();
                window.location = anchorTarget;
            });
            alert.on('alert.yes', function () {
                that.submit();
            });
            alert.show();
        } else {
            var anchorTarget = this.getNewButtonTarget();
            window.location = anchorTarget;
        }
        n.Log.debug("new click");
    };

    /**
     * @private
     */
    n.QfqForm.prototype.handleDeleteClick = function () {
        this.lastButtonPress = "delete";
        n.Log.debug("delete click");
        var alert = new n.Alert({
            message: "Do you really want to delete the record?",
            type: "warning",
            modal: true,
            buttons: [
                {label: "Yes", eventName: "ok"},
                {label: "No", eventName: "cancel", focus: true}
            ]
        });

        var that = this;
        alert.on('alert.ok', function () {
            $.post(that.appendQueryParametersToUrl(that.deleteUrl, that.getRecordHashMd5AsQueryParameter()))
                .done(that.ajaxDeleteSuccessDispatcher.bind(that))
                .fail(n.Helper.showAjaxError);
        });
        alert.show();
    };

    n.QfqForm.prototype.getRecordHashMd5AsQueryParameter = function () {

        return {
            'recordHashMd5': this.getRecordHashMd5(),
            'tabUniqId': this.getTabUniqId()
        };
    };

    /**
     *
     * @param data
     * @param textStatus
     * @param jqXHR
     *
     * @private
     */
    n.QfqForm.prototype.ajaxDeleteSuccessDispatcher = function (data, textStatus, jqXHR) {
        if (!data.status) {
            throw new Error("No 'status' property 'data'");
        }

        switch (data.status) {
            case "error":
                this.handleLogicDeleteError(data);
                break;
            case "success":
                this.handleDeleteSuccess(data);
                break;
            default:
                throw new Error("Status '" + data.status + "' unknown.");
        }
    };

    /**
     *
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleDeleteSuccess = function (data) {
        this.setButtonEnabled(this.getCloseButton(), false);
        this.setButtonEnabled(this.getDeleteButton(), false);
        this.setButtonEnabled(this.getSaveButton(), false);
        this.setButtonEnabled(this.getNewButton(), false);
        this.destroyFormAndSetText("Record has been deleted!");

        if (!data.redirect || data.redirect === "auto") {
            this.goBack();
            return;
        }

        if (data.redirect === "no") {
            this._createError("redirect=='no' not allowed");
            return;
        }

        if (data.redirect === "url" && data['redirect-url']) {
            window.location = data['redirect-url'];
        }

        if (data.redirect === "url-skip-history" && data['redirect-url']) {
            window.location.replace(data['redirect-url']);
        }
    };

    /**
     *
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleLogicDeleteError = function (data) {
        if (!data.message) {
            throw Error("Status is 'error' but required 'message' attribute is missing.");
        }
        this._createError(data.message);

        this.setButtonEnabled(this.getDeleteButton(), false);
    };

    /**
     * Called when form is saved.
     * Once a file is deleted that was previously saved, the input will be required. This checks if said input is hidden and removes required attribute.
     */
    n.QfqForm.prototype.checkHiddenRequired = function() {
        var $form = $(this.form.$form[0]);
        var $required = $form.find(':input:file:hidden[required]');
        if(!!$required[0]) {
           $required.prop("required", false);
        }
    };

    /**
     * Called when form is changed.
     *
     * @param obj {n.QfqForm}
     *
     * @private
     */
    n.QfqForm.prototype.changeHandler = function (obj) {
        if (this.formImmutableDueToConcurrentAccess) return;
        if (!this.form.getSaveAllowed()) return;
        this.getSaveButton().removeClass("disabled btn-default");
        this.getSaveButton().addClass(this.getSaveButtonAttentionClass());
        this.getSaveButton().removeAttr("disabled");
        this.fireDirtyRequestIfRequired();
    };

    n.QfqForm.prototype.fireDirtyRequestIfRequired = function () {
        if (this.dirtyFired) {
            return;
        }

        if (this.lockRenewalPhase) {
            this.dirty.renew(this.getSip(), this.getRecordHashMd5AsQueryParameter());
        } else {
            this.dirty.notify(this.getSip(), this.getRecordHashMd5AsQueryParameter());
        }
        this.dirtyFired = true;
    };

    /**
     *
     * @param obj {n.QfqForm}
     *
     * @private
     */
    n.QfqForm.prototype.resetHandler = function (obj) {
        this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
        this.getSaveButton().addClass('btn-default')

        // Only disable button if class enable-save-button not given
        if (!$('#' + QfqNS.escapeJqueryIdSelector(this.formId)).data('enable-save-button')) {
            this.getSaveButton().addClass("disabled");
            this.getSaveButton().attr("disabled", "disabled");
        }
        this.resetLockState();
    };

    n.QfqForm.prototype.deactivateSaveButton = function () {
        this.getSaveButton().addClass("disabled");
        //this.getSaveButton().attr("disabled", "disabled");
        this.getSaveButton().off('click');
        this.getSaveButton().on('click', this.infoLockedButton);
        this.getSaveButton().css("color", "#fff");
    };

    n.QfqForm.prototype.infoLockedButton = function(e) {
        var alert = new n.Alert({
            message: "Please wait until the upload finishes to save this form",
            buttons: [{ label: "Ok", eventName: "ok"}],
            modal: true
        });
        alert.show();
        e.preventDefault();
        return false;
    };

    n.QfqForm.prototype.activateSaveButton = function () {
        this.getSaveButton().off('click');
        this.getSaveButton().removeClass("disabled");
        //this.getSaveButton().removeAttr("disabled");
        this.getSaveButton().css("color", "");
        this.getSaveButton().click(this.handleSaveClick.bind(this));
    };

    n.QfqForm.prototype.getSaveButtonAttentionClass = function () {
        var $saveButton = this.getSaveButton();

        return $saveButton.data('class-on-change') || 'btn-info';
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getSaveButton = function () {
        return $("#save-button-" + this.formId);
    };

    /**
     *
     * @returns {*|jQuery|HTMLElement}
     *
     * @private
     */
     n.QfqForm.prototype.getHistoryButton = function () {
         return $("#form-history-" + this.formId);
     };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getCloseButton = function () {
        return $("#close-button-" + this.formId);
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getDeleteButton = function () {
        return $("#delete-button-" + this.formId);
    };

    /**
     *
     * @returns {jQuery|HTMLElement}
     *
     * @private
     */
    n.QfqForm.prototype.getNewButton = function () {
        return $("#form-new-button-" + this.formId);
    };



    /**
     * @private
     */
    n.QfqForm.prototype.submitSuccessDispatcher = function (obj) {
        if (!obj.data.status) {
            throw new Error("No 'status' property in 'data'");
        }

        switch (obj.data.status) {
            case "error":
                this.handleLogicSubmitError(obj.target, obj.data);
                break;
            case "success":
                this.handleSubmitSuccess(obj.target, obj.data);
                this.setupFormUpdateHandler();
                break;
            case "conflict":
                this.handleConflict(obj.target, obj.data);
                break;
            case "conflict_allow_force":
                this.handleOverrideableConflict(obj.target, obj.data);
                break;
            default:
                throw new Error("Status '" + obj.data.status + "' unknown.");
        }

    };

    /**
     *
     * @param form
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleLogicSubmitError = function (form, data) {
        if (!data.message) {
            throw Error("Status is 'error' but required 'message' attribute is missing.");
        }

        // Alert with force save option: data.text will be set for sqlValidate()
        if (data.text) {

            var forceButton = (data.force) ? { label: data.force, eventName: 'save-force' } : '';

            var alert = new n.Alert({
                message: data.text,
                type: data.level,
                buttons: [ { label: data.ok, eventName: 'ok' } ],
                modal: data.flagModal,
                timeout: data.timeout
            });

            if (forceButton) alert.buttons.unshift(forceButton);
            alert.on('alert.save-force', function () {
                $("#save-button-" + form.formId).attr('data-save-force', 'save,force');
                $("#save-button-" + form.formId).click();
            }.bind(form.formId));
            alert.show();

        } else {
            this._createError(data.message);
            if (data["field-name"] && this.bsTabs) {
                var tabId = this.bsTabs.getContainingTabIdForFormControl(data["field-name"]);
                if (tabId) {
                    this.bsTabs.activateTab(tabId);
                }

                this.setValidationState(data["field-name"], "error");
                this.setHelpBlockValidationMessage(data["field-name"], data["field-message"]);
            }
        }
    };

    /**
     *
     */
    n.QfqForm.prototype.handleConflict = function (form, data) {
        this.setButtonEnabled(this.getSaveButton(), false);
        this.getSaveButton().removeClass(this.getSaveButtonAttentionClass());
        this.setButtonEnabled(this.getDeleteButton(), false);
        this.formImmutableDueToConcurrentAccess = true;
        this.lockAcquired = false;
        this._createError(data.message);
    };

    n.QfqForm.prototype.handleOverrideableConflict = function (form, data) {
        var that = this;
        var alert = new n.Alert({
            message: data.message + 'Save anyway?',
            type: "warning",
            modal: true,
            buttons: [
                {label: "Yes", eventName: "yes"},
                {label: "No", eventName: "no", focus: true}
            ]
        });
        alert.on('alert.yes', function () {
            if (data.tokenForce) {
                that.submit({
                    tokenForce: data.tokenForce
                });
            } else {
                that.submit();
            }

        });
        alert.show();
    };

    /**
     *
     * @param form
     * @param data
     *
     * @private
     */
    n.QfqForm.prototype.handleSubmitSuccess = function (form, data) {
        n.Log.debug('Reset form state');
        this.getSaveButton().removeClass('btn-warning active disabled');
        this.getSaveButton().addClass('btn-default');
        form.resetFormChanged();
        this.resetLockState();

        switch (this.lastButtonPress) {
            case 'save&close':
                this.goBack();
                break;
            case 'save':
                if (data.message) {
                    var alert = new n.Alert(data.message);
                    alert.timeout = 3000;
                    alert.show();
                }

                // Skip other checks if external Save is called
                if (this.goToAfterSave) {
                    console.log("Called goToAfterSave = " + this.goToAfterSave);
                    window.location = this.goToAfterSave;
                    return;
                }

                // do we have to update the HTML Form?
                if (data['form-update']) {
                    this.applyFormConfiguration(data['form-update']);
                }

                if (data['element-update']) {this.applyElementConfiguration(data['element-update']);
                }

                if (data.redirect === "url" && data['redirect-url']) {
                    window.location = data['redirect-url'];
                    return;
                }

                if (data.redirect === "url-skip-history" && data['redirect-url']) {
                    window.location.replace(data['redirect-url']);
                    return;
                }

                if (data.redirect === "close") {
                    this.goBack();
                    return;
                }

                break;
            case 'close':
                if (!data.redirect || data.redirect === "no") {
                    return;
                }

                if (data.redirect === "auto" || data.redirect === "close") {
                    this.goBack();
                    return;
                }

                if (data.redirect === "url" && data['redirect-url']) {
                    window.location = data['redirect-url'];
                    return;
                }

                if (data.redirect === "url-skip-history" && data['redirect-url']) {
                    window.location.replace(data['redirect-url']);
                    return;
                }

                break;
            case 'new':
                var target = this.getNewButtonTarget();

                window.location.replace(target);
                return;

            default:
                if (data.redirect === "auto") {
                    this.goBack();
                    return;
                }

                if (data.redirect === "url" && data['redirect-url']) {
                    window.location = data['redirect-url'];
                    return;
                }

                if (data.redirect === "url-skip-history" && data['redirect-url']) {
                    window.location.replace(data['redirect-url']);
                    return;
                }

                break;
        }

        if(this.skipRequiredCheck) {
            this.form.$form.validator('update');
            this.form.$form.validator('validate');
        }
    };

    n.QfqForm.prototype.getNewButtonTarget = function () {
        return $('#form-new-button').attr('href');
    };

    n.QfqForm.prototype.getFormGroupByControlName = function (formControlName) {
        console.log("Form Control Name: " + formControlName);
        var $formControl = $("[name='" + formControlName + "']");
        if ($formControl.length === 0) {
            n.Log.debug("QfqForm.setValidationState(): unable to find form control with name '" + formControlName + "'");
            return null;
        }

        var iterator = $formControl[0];
        while (iterator !== null) {
            var $iterator = $(iterator);
            if ($iterator.hasClass('form-group')) {
                return $iterator;
            }

            iterator = iterator.parentElement;
        }

        return null;
    };

    n.QfqForm.prototype.setValidationState = function (formControlName, state) {
        var $formGroup = this.getFormGroupByControlName(formControlName);
        if ($formGroup) {
            $formGroup.addClass("has-" + state);
            $formGroup.addClass("testitest");
        }
    };

    n.QfqForm.prototype.resetValidationState = function (formControlName) {
        var $formGroup = this.getFormGroupByControlName(formControlName).find('input');
        $formGroup.removeClass("has-danger");
        $formGroup.removeClass("has-error");
        $formGroup.removeClass("has-success");
        $formGroup.removeClass("has-danger");
    };


    n.QfqForm.prototype.clearAllValidationStates = function () {
        // Reset any messages/states added by bootstrap-validator.
        this.form.$form.validator('reset');

        // Reset any states added by a call to QfqForm#setValidationState()
        $('.has-warning,.has-error,.has-success,.has-danger').removeClass("has-warning has-error has-success" +
            " has-danger");

        // Remove all messages received from server upon form submit.
        $('[data-qfq=validation-message]').remove();
    };

    /**
     *
     * @param formControlName
     * @param text
     */
    n.QfqForm.prototype.setHelpBlockValidationMessage = function (formControlName, text) {
        /*
         * Why is this method here and not in FormGroup? Adding this particular method to FormGroup is easy, however
         * QfqForm.clearAllValidationStates() would not find its proper place in FormGroup, since FormGroup operates
         * on one element. We would end up having the responsibilities spread across several classes, which would be
         * confusing.
         */
        var $formGroup = this.getFormGroupByControlName(formControlName);
        if (!$formGroup) return;

        var $helpBlockColumn;
        var $formGroupSubDivs = $formGroup.find("div");
        if ($formGroupSubDivs.length < 3) {
            $helpBlockColumn = $("<div>").addClass("col-md-4");
            $formGroup.append($helpBlockColumn);
        } else {
            $helpBlockColumn = $($formGroupSubDivs[2]);
        }

        $helpBlockColumn.append(
            $("<p>")
                .addClass("help-block")
                .attr("data-qfq", "validation-message")
                .append(text)
                .prepend($("<div>", { class: "arrow arrow-up"}))
        );
    };

    /**
     *
     * @param configuration {array} array of objects.
     */
    var typeAheadFlag = false;
    var loading = true;
    n.QfqForm.prototype.applyFormConfiguration = function (configuration) {
        var arrayLength = configuration.length;
        var countElementArray = 0;
        for (var i = 0; i < arrayLength; i++) {
            var configurationItem = configuration[i];
            var formElementName = configurationItem["form-element"];
            if (formElementName === undefined) {
                n.Log.error("configuration lacks 'form-element' attribute. Skipping.");
                continue;
            }
            if(formElementName === 'qfq-form-title') {
                $("." + formElementName).html(configurationItem.value);
            }
            // Insert download button for uploads after Form is saved.
            if (configurationItem["type-file"] && document.querySelector("button[name='delete-" + formElementName + "']") !== null) {
                var downloadButton = configurationItem["html-content"];
                var fileNameSpan = document.querySelector("button[name='delete-" + formElementName + "']").previousElementSibling;
                fileNameSpan.innerHTML = '';
                fileNameSpan.insertAdjacentHTML('afterbegin', downloadButton);
            }
            try {
                var element = n.Element.getElement(formElementName);
                // Cleaner way to set states for tinymce
                // This triggers the event on the unaccesable textarea
                // The tinymce registers a listener on the textarea
                // See helper/tinyMCE.js for details
                if (element.$element !== undefined) {
                    if (configurationItem.value !== undefined) {
                        // Skip ICJ visual inputs - they manage their own values
                        if (element.$element.hasClass('qfq-icj-input')) {
                            // Do nothing - ICJ handles its own values
                        } else if (element.$element.hasClass('qfq-typeahead')) {
                            if (configurationItem.value !== 0) {
                                element.setValue(configurationItem.value);
                            }
                            typeAheadFlag = true;
                        } else if (!element.$element.hasClass('qfq-codemirror')){
                            element.setValue(configurationItem.value);
                        }
                    }

                    if (element.$element.hasClass('qfq-tinymce')) {
                        var tinyMceId = element.$element.attr('id');
                        var tinyMceEditor = tinymce.get(tinyMceId);
                        var hadFocus = tinyMceEditor.hasFocus();
                        var cursorPath = null;

                        // Save cursor position as DOM path before replacing content
                        if (hadFocus) {
                            try {
                                var rng = tinyMceEditor.selection.getRng();
                                var node = rng.startContainer;
                                var body = tinyMceEditor.getBody();
                                var path = [];
                                var current = node;
                                while (current && current !== body) {
                                    var parent = current.parentNode;
                                    path.unshift(Array.prototype.indexOf.call(parent.childNodes, current));
                                    current = parent;
                                }
                                cursorPath = { path: path, offset: rng.startOffset };
                            } catch (e) { /* ignore */ }
                        }

                        tinyMceEditor.setContent(configurationItem.value);

                        // Restore cursor position by following the saved DOM path
                        if (hadFocus && cursorPath) {
                            try {
                                var targetNode = tinyMceEditor.getBody();
                                for (var idx = 0; idx < cursorPath.path.length; idx++) {
                                    if (targetNode.childNodes[cursorPath.path[idx]]) {
                                        targetNode = targetNode.childNodes[cursorPath.path[idx]];
                                    } else {
                                        targetNode = null;
                                        break;
                                    }
                                }
                                if (targetNode) {
                                    var maxOffset = targetNode.nodeType === 3 ? targetNode.length : targetNode.childNodes.length;
                                    var newRng = tinyMceEditor.dom.createRng();
                                    newRng.setStart(targetNode, Math.min(cursorPath.offset, maxOffset));
                                    newRng.collapse(true);
                                    tinyMceEditor.selection.setRng(newRng);
                                    tinyMceEditor.focus();
                                }
                            } catch (e) { /* ignore */ }
                        }

                        element.$element.trigger("blur", [configurationItem]);
                    }

                    if (element.$element.hasClass('qfq-codemirror')) {
                        var cm = document.getElementById(element.$element.attr('id')).nextSibling;
                        var editor = cm.CodeMirror;
                        if (editor !== undefined) {
                            editor.setValue(configurationItem.value);
                        }
                    }


                    if (configurationItem.readonly !== undefined) {
                        // Readonly and disabled is the same in our domain
                        element.setEnabled(!configurationItem.readonly);
                    }

                    if (configurationItem.disabled !== undefined) {
                        // Readonly and disabled is the same in our domain
                        element.setEnabled(!configurationItem.disabled);
                    }

                    if (configurationItem.hidden !== undefined) {
                        element.setHidden(configurationItem.hidden);
                    }

                    if (configurationItem.required !== undefined) {
                        element.setRequired(configuration.required);
                        if (element.$element) {
                            if (element.$element.is("select")) {
                                element.$element.prop('required', configurationItem.required);
                                element.$element.attr('data-required', 'yes');
                            }
                            if (element.$element.is("input[type=hidden]")) {
                                console.log("Update Hidden");
                                element.$element.prop("required", configurationItem.required);
                            }

                        }
                    }
                    countElementArray = 0;
                } else {
                    // checkboxes without being in form groups aren't triggered over dynamic update, we need to handle them separately.
                    // for checkboxes with itemList (multiple) we need the countElementArray to trigger the right box
                    if ($(element).attr('type') !== undefined) {
                        if ($(element).attr('type').toLowerCase() === 'checkbox') {
                            var elementLength = element.length;
                            if (elementLength > 1) {
                                if (elementLength !== countElementArray) {
                                    element = element[countElementArray];
                                    countElementArray++;

                                    if (elementLength === countElementArray) {
                                        countElementArray = 0;
                                    }
                                }
                            }

                            if (configurationItem.value !== undefined) {
                                $(element).prop('checked', configurationItem.value);
                            }
                        }
                    }

                    // Chats needs to be loaded with new data
                    // It refreshes the output
                    if (element.classList !== undefined && element.classList.contains('qfq-chat-window')) {
                        n.Helper.qfqChat.setInputState(element, configurationItem);
                    }
                }

            } catch (e) {
                n.Log.error(e.message);
            }
        }
    };

    n.QfqForm.prototype.applyElementConfiguration = function (configuration) {
        if (!configuration) {
            console.error("No configuration for Element Update found");
            return;
        }

        n.ElementUpdate.updateAll(configuration);

        // Trigger typeahead after dynamic update to show prefetch value, except empty values
        if (typeAheadFlag) {
            $('.tt-input[value!="0"]').blur();
        }
    };

    /**
     * @private
     * @param obj
     */
    n.QfqForm.prototype.startUploadHandler = function (obj) {
        $(obj.target).after(
            $('<i>').addClass('spinner')
        );
        this.deactivateSaveButton();
    };

    /**
     * @private
     * @param obj
     */
    n.QfqForm.prototype.endUploadHandler = function (obj) {
        var $siblings = $(obj.target).siblings();
        $siblings.filter("i").remove();
        this.activateSaveButton();
    };

    /**
     * Retrieve SIP as stored in hidden input field.
     *
     * @returns {string} sip
     */
    n.QfqForm.prototype.getSip = function () {
        return this.getValueOfHiddenInputField('s');
    };

    /**
     * Retrieve recordHashMd5 as stored in hidden input field.
     *
     * @returns {string} sip
     */
    n.QfqForm.prototype.getRecordHashMd5 = function () {
        return this.getValueOfHiddenInputField('recordHashMd5');
    };

    /**
     * Misuse the window.name attribute to set/get a tab uniq identifier.
     * Use millisecond timestamp as identifier: hopefully there are never more than one tab opened per millisecond in a single browser session.
     *
     * @returns {string} tab identifier
     */
    n.QfqForm.prototype.getTabUniqId = function () {

        if (!window.name.toString()) {
            // Misuse window.name as tab uniq identifier. Set window.name if it is empty.
            window.name = Date.now().toString();
        }

        return window.name;
    };

    n.QfqForm.prototype.getValueOfHiddenInputField = function (fieldName) {
        return $('#' + this.formId + ' input[name=' + fieldName + ']').val();
    };

    /**
     * @public
     */
    n.QfqForm.prototype.isFormChanged = function () {
        return this.form.formChanged;
    };

    /**
     * @private
     */
    n.QfqForm.prototype.submitOnEnter = function () {
        return !(!!this.form.$form.data('disable-return-key-submit'));
    };

    n.QfqForm.prototype.appendQueryParametersToUrl = function (url, queryParameterObject) {
        var queryParameterString = $.param(queryParameterObject);
        if (url.indexOf('?') !== -1) {
            return url + "&" + queryParameterString;
        }

        return url + "?" + queryParameterString;
    };

    /**
     * @private
     *
     * Go back in the history, or pop an alert when no history.
     */
    n.QfqForm.prototype.goBack = function () {
        var alert;

        if (window.history.length < 2) {
            alert = new n.Alert(
                {
                    type: "info",
                    message: "Please close the tab/window."
                }
            );

            alert.show();
            return;
        }

        window.history.back();
    };
    /**
     * Set state and add event listener to MultiState Checkbox.
     */
    n.QfqForm.prototype.handleMultiStateCheckBox = function() {
        // Retrieve all elements with the "multiStateCheckbox" class.
        let checkBoxDivs = document.getElementsByClassName("multiStateCheckbox");
        if (!checkBoxDivs.length) return; // Exit early if no checkbox elements are found.

        Array.from(checkBoxDivs).forEach(checkBoxDiv => {
            let checkBoxIcon = checkBoxDiv.querySelector(".multiStateCheckboxIcon");
            let checkBoxLabel = checkBoxDiv.closest("label").querySelector(".multiStateCheckboxSpan");
            let checkBoxInput = checkBoxDiv.closest("label").querySelector("input");

            // Parse the options provided in the data-options attribute.
            let options = JSON.parse(checkBoxDiv.getAttribute('data-options'));
            let currentIndex = 0;

            // Initialize the input value and icon style based on the first option.
            checkBoxInput.value = options[currentIndex].value;
            checkBoxIcon.style.color = options[currentIndex].iconColor;

            // Set the border color based on the first option's color.
            if (options[currentIndex].color === 'white'){
                checkBoxDiv.style.borderColor = "#ccc";
            } else {
                checkBoxDiv.style.borderColor = options[currentIndex].color || "#2196f3";
            }

            // If the checkbox is disabled, adjust appearance and exit early.
            if (checkBoxDiv.hasAttribute("disabled")) {
                checkBoxDiv.style.opacity = "0.5";
                checkBoxDiv.style.cursor = "not-allowed";
                return;
            }

            // Add a click event listener to cycle through available options.
            checkBoxDiv.addEventListener("click", () => {
                // Increment the index, cycling back to 0 if it exceeds the options length.
                currentIndex = (currentIndex + 1) % options.length;

                // icon class and color.
                checkBoxIcon.className = `fas ${options[currentIndex].icon}`;
                checkBoxIcon.style.color = options[currentIndex].iconColor;

                // background color and border color.
                checkBoxDiv.style.backgroundColor = options[currentIndex].color || "#2196f3";
                if (options[currentIndex].color === 'white') {
                    checkBoxDiv.style.borderColor = "#ccc";
                } else {
                    checkBoxDiv.style.borderColor = options[currentIndex].color || "#2196f3";
                }

                // Update data selected value attribute.
                checkBoxDiv.setAttribute("data-selected-value", options[currentIndex].value);
                // Update the displayed label.
                checkBoxLabel.innerText = options[currentIndex].label;

                // Set the value of the hidden input field and trigger a change event.
                $(checkBoxInput).val(options[currentIndex].value).trigger('change');

                this.form.markChanged();
            });
        });
    };



    n.QfqForm.prototype.getNextButton = function () {
        return $("#form-button-next");
    };

    n.QfqForm.prototype.getButtonPrevious = function () {
        return $("#form-button-previous");
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global console */
/* @depend QfqEvents.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     *
     * @param settings
     * @constructor
     *
     * @name QfqNS.QfqPage
     */
    n.QfqPage = function (settings) {
        console.log("Creating QFQPage", settings);
        this.qfqForm = {};
        this.settings = $.extend(
            {
                tabsId: "qfqTabs",
                formId: "qfqForm",
                submitTo: "typo3conf/ext/qfq/Classes/Api/save.php",
                deleteUrl: "typo3conf/ext/qfq/Classes/Api/delete.php",
                refreshUrl: "typo3conf/ext/qfq/Classes/Api/load.php",
                fileUploadTo: "typo3conf/ext/qfq/Classes/Api/upload.php",
                fileDeleteUrl: "typo3conf/ext/qfq/Classes/Api/filedelete.php",
                typeAheadUrl: "typo3conf/ext/qfq/Classes/Api/typeahead.php",
                dirtyUrl: "typo3conf/ext/qfq/Classes/Api/dirty.php",
                pageState: new n.PageState()
            }, settings
        );

        n.Log.level = settings.logLevel;

        this.intentionalClose = false;
        this.downloadLinkClicked = false;

        try {
            this.bsTabs = new n.BSTabs(this.settings.tabsId);

            var storedFormInfos = [];

            // get current state from session storage
            if(sessionStorage.getItem("formInfos") !== null) {
                storedFormInfos = JSON.parse(sessionStorage.getItem("formInfos"));
            }

            var currentForm = this.bsTabs.currentFormName;
            var currentRecordId = this.bsTabs.currentRecordId;
            var activeLastPill = this.bsTabs.currentActiveLastPill;

            var actualIndex = -1;
            var indexNr = 0;
            if(storedFormInfos.length !== 0){
                if(storedFormInfos[0] !== ''){
                    storedFormInfos.forEach(function callback(element) {
                        if(element === currentForm && storedFormInfos[indexNr+2] === currentRecordId) {
                            actualIndex = indexNr;
                        }
                        indexNr++;
                    });
                }
            }

            var currentState = this.settings.pageState.getPageState();

            // load from sessionStorage or from path given hash if not empty
            if(activeLastPill === "true") {
                if (actualIndex !== -1 && location.hash === "") {
                    currentState = storedFormInfos[actualIndex + 1];
                }
            }

            if (currentState !== "") {
                this.bsTabs.activateTab(currentState);
                n.PageTitle.setSubTitle(this.bsTabs.getTabName(currentState));
            } else {
                this.settings.pageState.setPageState(this.bsTabs.getCurrentTab(), n.PageTitle.get());
            }

            this.bsTabs.on('bootstrap.tab.shown', this.tabShowHandler.bind(this));
            this.bsTabs.on('bootstrap.tab.shown', function(e){
                // Initialize FilePond in the newly active tab content
                setTimeout(() => {
                    // Get the ID of the tab content
                    const tabContentId = e.target.currentTab;
                    const tabContent = document.querySelector(`#${tabContentId}`);
                    n.Helper.initializeFilePondInContainer(tabContent, n.form);
                }, 100);
            });

            this.settings.pageState.on('pagestate.state.popped', this.popStateHandler.bind(this));
        } catch (e) {
            n.Log.message(e.message);
            this.bsTabs = null;
        }

        try {
            this.qfqForm = new n.QfqForm(
                this.settings.formId,
                this.settings.submitTo,
                this.settings.deleteUrl,
                this.settings.refreshUrl,
                this.settings.fileUploadTo,
                this.settings.fileDeleteUrl,
                this.settings.dirtyUrl,
                settings.apiDeleteUrl)
            this.qfqForm.setBsTabs(this.bsTabs);
            this.qfqForm.on('qfqform.destroyed', this.destroyFormHandler.bind(this));

            var that = this;
            this.qfqForm.on('qfqform.close-intentional', function () {
                that.intentionalClose = true;
            });

            window.addEventListener("beforeunload", this.beforeUnloadHandler.bind(this));
            document.addEventListener('click', (function (that) {
                return function (e) {
                    var link = e.target.closest('a[href]');
                    if (link && (link.href.includes('download.php') || link.href.includes('/d.php') )) {
                        that.downloadLinkClicked = true;
                    }
                };
            })(this));
            // We have to use 'pagehide'. 'unload' is too late and the ajax request is lost.
            window.addEventListener("pagehide", (function (that) {
                return function () {
                    document.activeElement.blur();
                    that.qfqForm.releaseLock(true);
                };
            })(this));
            this.recordList = new n.QfqRecordList(settings.apiDeleteUrl);
        } catch (e) {
            n.Log.error(e.message);
            this.qfqForm = null;
        }

        var page = this;
        // Initialize Fabric to access form events
        try {
            console.log("Form ID: ", this.settings.formId)
            console.log("Looking for Fabric Element", $("#" + this.settings.formId + " .annotate-graphic"))
            if(!this.settings.formId) {
                throw new Error("QFQPage missing formId")
            }

            $("#" + this.settings.formId + " .annotate-graphic").each(function() {
                var qfqFabric = new QfqNS.Fabric();
                qfqFabric.initialize($(this), page);
            });

            $("#" + this.settings.formId + " .annotate-text").each(function() {
                var codeCorrection = new QfqNS.CodeCorrection();
                codeCorrection.initialize($(this), page);
            });

            /* initaliaze image-adjust */
            const imageEditors = document.querySelectorAll(".qfq-image-adjust")
            console.log("Image Adjust Elements", imageEditors)
            imageEditors.forEach(editor => {
                console.log("Current Editor", editor)
                const imageAdjust = new ImageAdjust()
                imageAdjust.initialize(editor, page, n)
            })
            
        } catch (e) {
            n.Log.error(e.message);
        }

        QfqNS.TypeAhead.install(this.settings.typeAheadUrl);
        QfqNS.AiAssist.install();
        QfqNS.AiChat.install();
        QfqNS.CharacterCount.initialize();
        //n.initializeDatetimepicker(false);
    };

    /**
     * @private
     * 
     * Releaselock has to be handled at the pagehide event, not here
     */
    n.QfqPage.prototype.beforeUnloadHandler = function (event) {
        var message = "\0/";
        n.Log.debug("beforeUnloadHandler()");

        if (this.downloadLinkClicked) {
            this.downloadLinkClicked = false;
            return;
        }

        if (this.qfqForm.isFormChanged() && !this.intentionalClose) {
            n.Log.debug("Changes detected - not regular close");
            document.activeElement.blur();
            this.qfqForm.releaseLock(true);
            event.returnValue = message;
            return message;
        }
    };

    /**
     * @private
     */
    n.QfqPage.prototype.destroyFormHandler = function (obj) {
        this.settings.qfqForm = null;
        $('#' + this.settings.tabsId).remove();
    };

    n.QfqPage.prototype.tabShowHandler = function (obj) {
        // tabShowHandler will be called every time the tab will be shown, regardless of whether or not this happens
        // because of BSTabs.activateTab() or user interaction.
        //
        // Therefore, we have to make sure, that tabShowHandler() does not save the page state while we're restoring
        // a previous state, i.e. we're called because of the popStateHandler() below.
        if (this.settings.pageState.inPoppingHandler) {
            n.Log.debug("Prematurely terminating QfqPage.tabShowHandler(): called due to page state" +
                " restoration.");
            return;
        }
        var currentTabId = obj.target.getCurrentTab();
        n.Log.debug('Saving state: ' + currentTabId);

        // Implementation save current state in session storage
        var storedFormInfos = [];

        if(sessionStorage.getItem("formInfos") !== null){
            storedFormInfos = JSON.parse(sessionStorage.getItem("formInfos"));
        }

        var currentForm = obj.target.currentFormName;
        var currentRecordId = obj.target.currentRecordId;
        var activeLastPill = obj.target.currentActiveLastPill;

        var actualIndex = -1;
        var indexNr = 0;
        if(activeLastPill === "true") {
            if (storedFormInfos.length !== 0) {
                if (storedFormInfos[0] !== '') {
                    storedFormInfos.forEach(function callback(element) {
                        if (element === currentForm && storedFormInfos[indexNr + 2] === currentRecordId) {
                            actualIndex = indexNr;
                        }
                        indexNr++;
                    });
                }
            }

            // fill sessionStorage, there are 3 ways for filling the sessionStorage: 1.If empty - first time filling, 2.If there is anything - add it to them, 3
            // 1.If array from storage is empty - fill it first time
            if (storedFormInfos.length === 0) {
                storedFormInfos[0] = currentForm;
                storedFormInfos[1] = currentTabId;
                storedFormInfos[2] = currentRecordId;

                // 2.If there is anything in storage but not the actual opened forms - add this new information to the existing array
            } else if (actualIndex === -1) {
                storedFormInfos[indexNr] = currentForm;
                storedFormInfos[indexNr + 1] = currentTabId;
                storedFormInfos[indexNr + 2] = currentRecordId;

                // 3.If actual openend form is included in sessionStorage - only change the array values of the existing informations
            } else {
                storedFormInfos[actualIndex] = currentForm;
                storedFormInfos[actualIndex + 1] = currentTabId;
                storedFormInfos[actualIndex + 2] = currentRecordId;
            }

            // Set sessionStorage with customized array
            sessionStorage.setItem("formInfos", JSON.stringify(storedFormInfos));
        }
        n.PageTitle.setSubTitle(obj.target.getTabName(currentTabId));
        this.settings.pageState.setPageState(currentTabId, n.PageTitle.get());
    };

    n.QfqPage.prototype.popStateHandler = function (obj) {
        this.bsTabs.activateTab(obj.target.getPageState());
        n.PageTitle.set(obj.target.getPageData());
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */
/* global $ */
/* global console */

var QfqNS = QfqNS || {};

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
(function (n) {
    'use strict';

    /**
     *
     * @param deleteUrl
     * @param element
     * @constructor
     *
     * @name QfqNS.QfqRecordList
     */
    n.QfqRecordList = function (deleteUrl, element = false) {
        console.log("initialized with this url", deleteUrl);
        this.deleteUrl = deleteUrl;
        this.deleteButtonClass = 'record-delete';
        this.recordClass = 'record';
        this.sipDataAttribute = 'sip';

        // Default handling if not specific element is passed
        if (!element) {
            this.connectClickHandler();
        }
    };

    /**
     * @private
     */
    n.QfqRecordList.prototype.connectClickHandler = function (element = false) {
        var boundClickHandler = this.clickHandler.bind(this);

        // For dynamic loaded elements, handle each of them separately
        if (element) {
            element.on('click', boundClickHandler);
        } else {
            // Default event handler for all delete buttons after page load
            $("." + this.deleteButtonClass).on('click', boundClickHandler);
        }
    };

    n.QfqRecordList.prototype.handleDeleteButtonClick = function (event) {
        var $eventTarget = $(event.delegateTarget);
        var $recordElement = this.getRecordElement(event.target);

        if ($recordElement.length !== 1) {
            throw new Error($recordElement.length + ' match(es) found for record class');
        }

        var sip = $eventTarget.data(this.sipDataAttribute);

        if (!sip) {
            throw new Error('No `sip` on delete button');
        }


        var alert = new n.Alert({
            message: "Do you really want to delete the record?",
            type: "warning",
            modal: true,
            buttons: [
                {label: "Yes", eventName: "ok"},
                {label: "No", eventName: "cancel", focus: true}
            ]
        });
        var that = this;
        alert.on('alert.ok', function () {
            $.post(that.deleteUrl + "?s=" + sip)
                .done(that.ajaxDeleteSuccessDispatcher.bind(that, $recordElement))
                .fail(n.Helper.showAjaxError);
        });
        alert.show();
    };

    /**
     *
     * @param $recordElement
     * @param data
     * @param textStatus
     * @param jqXHR
     *
     * @private
     */
    n.QfqRecordList.prototype.ajaxDeleteSuccessDispatcher = function ($recordElement, data, textStatus, jqXHR) {
        if (!data.status) {
            throw new Error("No 'status' property 'data'");
        }

        switch (data.status) {
            case "error":
                this.handleLogicDeleteError(data);
                break;
            case "success":
                this.handleDeleteSuccess($recordElement, data);
                break;
            default:
                throw new Error("Status '" + data.status + "' unknown.");
        }
    };

    n.QfqRecordList.prototype.handleDeleteSuccess = function ($recordElement, data) {
        if (data.redirect && data.redirect === "url" && data['redirect-url']) {
            window.location = data['redirect-url'];
            return;
        }
        if (data.redirect && data.redirect === "no") {
            var alert = new n.Alert("redirect=='no' not allowed", "error");
            alert.show();
        }

        var info = new n.Alert("Record successfully deleted", "info");
        info.timeout = 1500;
        info.show();
        $recordElement.fadeOut(function () {
            $recordElement.remove();
        });
    };

    n.QfqRecordList.prototype.getRecordElement = function (element) {
        return $(element).closest('.' + this.recordClass);
    };

    /**
     *
     * @param data
     *
     * @private
     */
    n.QfqRecordList.prototype.handleLogicDeleteError = function (data) {
        if (!data.message) {
            throw Error("Status is 'error' but required 'message' attribute is missing.");
        }
        var alert = new n.Alert(data.message, "error");
        alert.show();
    };

    n.QfqRecordList.prototype.clickHandler = function (event) {
        // Check if the event is triggered by the "Enter" key
        var enterKeyTriggered = event.originalEvent.detail === 0;
        // Check if the delete button is focused
        var isButtonFocused = $(event.target).is(":focus");
        // If the event is triggered by the "Enter" key and the delete button has the specific class, stop the event
        if (enterKeyTriggered && !isButtonFocused && $(event.target).hasClass(this.deleteButtonClass)) {
            event.preventDefault();
            event.stopPropagation();
            return;
        }
        // If the event is not stopped, call the handleDeleteButtonClick method
        this.handleDeleteButtonClick(event);
    };


})(QfqNS);
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    /**
     * Highlights Syntax with provided JSON
     *
     */
    n.SyntaxHighlighter = function () {
        this.highlightInstructions = {};
        this.uri = "";
        this.importDone = false;
        this.waitingForEnd = false;
        this.multiLineClass = "";
        this.line = '';
    };

    n.SyntaxHighlighter.prototype.importInstructions = function(json, callbackFn) {
        var that = this;
        console.log("Import instructions: " + json);
        $.getJSON(json, function(data) {
            that.highlightInstructions = data;
            that.importDone = true;
            if (callbackFn && typeof(callbackFn) === "function") {
                console.log("callback found");
                callbackFn();
            }
        });
    };

    n.SyntaxHighlighter.prototype.setLanguageUri = function(uri) {
        this.uri = uri;
    };


    n.SyntaxHighlighter.prototype.highlightLine = function(line) {
        this.line = line;
        if(!this.waitingForEnd) {
            if (this._multiLineHighlight()) {
                return this.line;
            }
            this._wordHighlight();
        } else {
            if (this._multiLineHighlight()) {
                return this.line;
            } else {
                this.line = this.wrapLine(this.multiLineClass, this.line);
                return this.line;
            }
        }
        return this.line;
    };

    n.SyntaxHighlighter.prototype._wordHighlight = function() {
        for (var i = 0; i < this.highlightInstructions.singleWord.length; i++) {
            var word = this.highlightInstructions.singleWord[i];
            var regex = new RegExp(word.regex, "g");
            var wrapClass = this.highlightInstructions.classes[word.styleId].name;
            this.line = this.wrapMatch(wrapClass, regex, this.line);
        }
    };

    n.SyntaxHighlighter.prototype._multiLineHighlight = function() {
        for (var i = 0; i < this.highlightInstructions.multiLine.length; i++) {
            var multiLine = this.highlightInstructions.multiLine[i];
            var regex = {};
            if (this.waitingForEnd) {
                regex = new RegExp(multiLine.end,"g");
            } else {
                regex = new RegExp(multiLine.start,"g");
            }

            if (regex.test(this.line)) {
                if(this.waitingForEnd) {
                    this.line = this.endWrap(this.multiLineClass, regex, this.line);
                    this.waitingForEnd = false;
                    this.multiLineClass = "";
                } else {
                    this.multiLineClass = this.highlightInstructions.classes[multiLine.styleId].name;
                    this.line = this.startWrap(this.multiLineClass, regex, this.line);
                    this.waitingForEnd = true;
                }
                return true;
            }
        }
        return false;
    };

    n.SyntaxHighlighter.prototype.wrapLine = function(spanClass, text) {
        var line = "<span class=\"" + spanClass + "\">" + text + "</span>";
        return line;
    };

    n.SyntaxHighlighter.prototype.startWrap = function(spanClass, regex, line) {
        var newLine = line.replace(regex, "<span class=\"" + spanClass + "\">$1$2</span>");
        return newLine;
    };

    n.SyntaxHighlighter.prototype.endWrap = function(spanClass, regex, line) {
        var newLine = line.replace(regex, "<span class=\"" + spanClass + "\">$1</span>$2");
        return newLine;
    };

    n.SyntaxHighlighter.prototype.wrapMatch = function (spanClass, regex, line) {
        var newLine = line.replace(regex, "$1<span class=\"" + spanClass + "\">$2</span>$3");
        return newLine;
    };

})(QfqNS);
/**
 * @author Elias Villiger <elias.villiger@uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend QfqEvents.js */

var QfqNS = QfqNS || {};
(function (n) {

    n.TablesorterController = function () {
        if (window.hasOwnProperty('tablesorterMockApi')) {
            this.tablesorterApiUrl = window.tablesorterMockApi;
        }
        $.tablesorter.themes.bootstrap.table = "";
    };

    // table: jquery selector of table!
    n.TablesorterController.prototype.setup = function (table, uniqueIdentifier) {
        var hasFilter = $(table).hasClass('tablesorter-filter');
        var hasPager = $(table).hasClass('tablesorter-pager');
        var hasColumnSelector = $(table).hasClass('tablesorter-column-selector');
        var hasViewSaver = $(table)[0].hasAttribute("data-tablesorter-view");
        let hasClearMe = $(table).hasClass('clear-filter')
        let hasCounter = $(table).hasClass('qfq-rowCounter');


        if (hasCounter) {
            updateRowCounter($(table)[0]);
            $(table).on("sortEnd filterEnd", function () {
                updateRowCounter(this);
            });
        }

        updateFilterColor($(table)[0]);
        $(table).on("filterEnd", function () {
            updateFilterColor(this);
        });

        // Get base url from table attribute. Needed for newer typo3 versions.
        if (!window.hasOwnProperty('tablesorterMockApi')) {
            this.tablesorterApiUrl = $(table).data("tablesorter-base-url") + 'typo3conf/ext/qfq/Classes/Api/setting.php';
        }

        var tablesorterConfig = $(table).data("tablesorterConfig");
        if (!tablesorterConfig) { // revert to default
            tablesorterConfig = {
                theme: "bootstrap",
                widthFixed: false,
                headerTemplate: "{content} {icon}",
                dateFormat: "ddmmyyyy",
                textExtraction: function(node) {
                    // Only for text-inputs, selects and textareas
                    var $input = $(node).find('input[type="text"], input[type="number"], select, textarea');
                    if ($input.length) {
                        return $input.val() || '';
                    }
                    // For everything else, (include radio/checkbox with Labels) sort text only in radio etc
                    return $(node).text();
                },
                widgets: ["uitheme", "filter", "saveSort", "columnSelector", "output"],
                widgetOptions: {
                    filter_columnFilters: hasFilter, // turn filters on/off with true/false
                    filter_reset: ".reset",
                    filter_cssFilter: "form-control",
                    filter_saveFilters: true,
                    columnSelector_mediaquery: false,
                    output_delivery: "download",
                    output_saveFileName: "tableExport.csv",
                    output_separator: ";"
                }
            };
        }

        $(table).tablesorter(tablesorterConfig);

        // Prevent buttons/links in table headers from triggering sort
        $(table).find('th .btn, th button, th a.btn').addClass('tablesorter-noSort');

        var tablesorterMenuWrapper;
        if (hasColumnSelector || hasViewSaver) {
            tablesorterMenuWrapper = this._doTablesorterMenuWrapper(table);
        }
        if (hasViewSaver) {
            this._doViewSaver(table, tablesorterMenuWrapper);
        }
        if (hasColumnSelector) {
            this._doColumnSelector(table, tablesorterMenuWrapper, uniqueIdentifier);
        }
        if (hasPager) {
            this._doPager(table, uniqueIdentifier);
        }
        if (hasColumnSelector || hasViewSaver) {
            var width = 10;
            var $elem = tablesorterMenuWrapper.children();
            for (var i = 0; i < $elem.length; i++) {
                child = $elem.children().eq(i);
                width += child.width();
            }
            console.log("element width", width);
            if (width < 10) width = 185;
            console.log("element found", $(table).children("caption").children(".pull-right"));
            $(table).children("caption").children(".pull-right").css("margin-right", width + "px");
        }
        if (hasClearMe) {
            // Add clear me class and styles required to correctly display clear me button.
            let thead = $(table).find('thead');
            thead.find('input').addClass('qfq-clear-me qfq-clear-me-table-sorter');
            thead.find('td').attr('style', 'position: relative;top: auto;');
        }


        // Funktion zur Gruppierung der Buttons
        function groupButtons(UID, tablesorterMenuWrapper) {

            // Selektiere den Column Selector und die Page Selectors
            const columnSelector = $('#qfq-column-selector-' + UID); // Spaltenauswahl-Button
            const viewSaver = tablesorterMenuWrapper.find('.btn-group.qfq-tablesorter-menu-item'); // Page-Selector-Dropdowns


            // Prüfe, ob mindestens eines der Elemente existiert
            if (!columnSelector.length && !viewSaver.length) {
                return;
            }

            const buttonGroup = $('<div>')
                .addClass('btn-group btn-group-sm') // Bootstrap-Klassen für kleinere Button-Gruppen
                .attr('role', 'group') // Rolle für Barrierefreiheit
                .attr('style', 'float: right;');

            // Füge den viewSaver Selector (falls vorhanden) zur Gruppe hinzu
            if (viewSaver.length) {

                viewSaver.css({
                    top: 'unset',
                    right: '46px'
                }).addClass('btn-group-sm');
                viewSaver.children('button').removeClass('form-control');
                buttonGroup.append(viewSaver);
            }

            // Füge den Column Selector (falls vorhanden) zur Gruppe hinzu
            if (columnSelector.length) {

                columnSelector.css({
                    top: 'unset',
                    right: '0px',
                    'border-top-right-radius': '100px',
                    'border-bottom-right-radius': '100px'
                });

                buttonGroup.append(columnSelector);
            }

            // Füge die erstellte Button Group in den Wrapper ein
            tablesorterMenuWrapper.append(buttonGroup);
        }
        // Rufe die Funktion auf, nachdem alle anderen Anpassungen fertig sind
        if (hasColumnSelector || hasViewSaver) {
            groupButtons(uniqueIdentifier, tablesorterMenuWrapper);
        }

        // Function to add a dynamic row count to the table
        function updateRowCounter(tableElement) {
            // Get the table header row
            let theadRow = tableElement.querySelector("thead tr");
            // Get the table body element
            let tbody = tableElement.querySelector("tbody");
            // Select only visible rows within tbody since table filter only hides the elements
            let visibleRows = $(tbody).find("tr:visible");

            // Add the column header if it does not exist
            if (!theadRow.querySelector(".row-number")) {
                let th = document.createElement("th");
                th.innerText = "#";
                th.classList.add("row-number", "sorter-false", "filter-false");
                theadRow.insertBefore(th, theadRow.firstChild);
            }

            // Loop over all visible columns
            visibleRows.each(function(index) {
                let firstCell = this.querySelector(".row-number-cell");
                if (!firstCell) {
                    firstCell = document.createElement("td");
                    firstCell.classList.add("row-number-cell");
                    this.insertBefore(firstCell, this.firstChild);
                }
                firstCell.innerText = index + 1;
            });
        }

        // Function to update border color of corresponding <th> when filter is set/removed
        function updateFilterColor(table) {
            let filters = $(table).find('.tablesorter-filter-row input, .tablesorter-filter-row select');

            // Loop through filter inputs
            filters.each(function() {

                if($(this).val() !== '') {
                    $(this).addClass('tablesorter-filter-input');
                } else {
                    $(this).removeClass('tablesorter-filter-input');
                }
            })
        }
    };


    n.TablesorterController.prototype.setTableView = function (table, newView, changedSelect) {
        // Guard: Check if table and tablesorter config exist
        if (!table || !table[0] || !table[0].config) {
            console.warn('TablesorterController.setTableView: Table or tablesorter config not initialized');
            return;
        }

        if (newView.hasOwnProperty('columnSelection') && $.tablesorter.hasWidget(table, 'columnSelector')) {
            table.trigger('refreshColumnSelector', [newView.columnSelection]);
        }
        if (newView.hasOwnProperty('filters') && $.tablesorter.hasWidget(table, 'filter')) {
            // correct filter array length if shorter than no. of columns
            var filters = $.tablesorter.getFilters(table);
            if (!filters) {
                return;
            }
            var columns = filters.length;
            var len = newView.filters.length;

            // Handle situation with saved views and empty subrecord or later changed database column count.
            if (len > columns) {
                return;
            }

            var arrayAppend = Array.apply(null, Array(columns - len)).map(function () {
                return "";
            });
            var filtersPadded = newView.filters.concat(arrayAppend);
            table.trigger('search', [filtersPadded]);
        }

        // Get and set last used view if its same from last time, otherwise use the one from setting API if view changed
        var config = table[0].config;
        if (newView.hasOwnProperty('sortList') && config.sortList) {
            if (config.sortList.length !== 0 && !changedSelect) {
                table.trigger('sorton', [config.sortList]);
            } else {
                table.trigger('sorton', [newView.sortList]);
            }
        }
    };

    n.TablesorterController.prototype.getTableView = function (table) {
        var view = {};

        // Guard: Check if table and tablesorter config exist
        if (!table || !table[0] || !table[0].config) {
            console.warn('TablesorterController.getTableView: Table or tablesorter config not initialized');
            return view;
        }

        var config = table[0].config;
        if ($.tablesorter.hasWidget(table, 'columnSelector') && config.selector && config.selector.states) {
            view.columnSelection = config.selector.states.map(function (e, i) {
                return e ? i : false;
            }).filter(function (e) {
                return e !== false;
            });
        }
        if ($.tablesorter.hasWidget(table, 'filter')) {
            view.filters = $.tablesorter.getFilters(table) || [];
        }
        view.sortList = config.sortList || [];
        return view;
    };

    n.TablesorterController.prototype._doTablesorterMenuWrapper = function (table) {
        // in forms we need some distance to the top
        var addClass = '';
        if ($(table).find("caption").length > 0) addClass += ' qfq-no-margin-top';
        if ($(table).prev().is("h1,h2,h3")) addClass += ' qfq-only-top';
        var tablesorterMenuWrap = '<div class="qfq-tablesorter-menu-wrapper' + addClass + '"></div>';

        return $(tablesorterMenuWrap).insertBefore($(table));
    };

    n.TablesorterController.prototype._doViewSaver = function (table, tablesorterMenuWrapper) {
        var tableViews = JSON.parse(table.attr("data-tablesorter-view"));
        var that = this;
        // decode views from base64 (sql injection prevention)
        tableViews.forEach(function (view) {
            view.view = JSON.parse(atob(view.view));
        });

        // add 'Clear' public view if not exists
        if (!tableViews.some(function (v) {
            return v.name === 'Clear' && v.public;
        })) {
            setDefault('Clear', true);
        }

        // set default view
        function setDefault(name, publicBool) {
            var filters = $.tablesorter.getFilters(table);
            var columnCount = filters ? filters.length : 0;
            var allColumns = Array(columnCount).fill(0).map(function (e, i) {
                return i;
            });
            var view = {
                name: name,
                public: publicBool,
                view: {columnSelection: allColumns, filters: [], sortList: []}
            };
            tableViews.push(view);
        }

        var lastSelect = '';
        var changedSelect = false;

        // create view select dropdown
        var options = '';
        tableViews.forEach(function (view) {
            options += '<option class="qfq-fontawesome" value="' + (view.public ? 'public:' : 'private:') + view.name + '" >' +
                (view.public ? '&#xf0c0; ' : '&nbsp;&#xf007;&nbsp;&nbsp;') + view.name + '</option>';
        });
        var viewSelectorHtml = '<select class="form-control qfq-view-editor qfq-fontawesome qfq-tablesorter-menu-item" style="right: 80px; width: unset; border-top-right-radius: 0px !important; border-bottom-right-radius: 0px !important; height: 30px !important; top:0px !important;">' +
            '<option disabled selected value>Table view</option>' + options + '</select>';
        var select = $(viewSelectorHtml).appendTo(tablesorterMenuWrapper);
        var tableId = table.attr("data-tablesorter-id");
        select.change(function () {
            var viewFromSelect = that._parseViewSelectValue($(this).val());
            var view = tableViews.find(function (v) {
                return v.name === viewFromSelect.name && v.public === viewFromSelect.public;
            });

            // check for changed select (used dropdown)
            var actualSelect = $(this).val();
            if (actualSelect !== lastSelect) {
                changedSelect = true;
            }
            lastSelect = actualSelect;

            that.setTableView(table, view.view, changedSelect);
            that._updateTablesorterUrlHash(table, $(this).val());

            // save view choice in local storage
            localStorage.setItem('tablesorterView_' + tableId, $(this).val());
        });

        // select view on page load: first priority from url hash, second priority localstorage, third private view named 'Default', fourth is public view 'Clear'
        // Default (third priority) + Clear (fourth priority)
        var publicDefaultExists = tableViews.some(function (v) {
            return v.name === 'Default' && v.public;
        });
        var setValue = publicDefaultExists ? "public:Default" : "public:Clear";

        // local storage (second priority)
        var localStorageView = localStorage.getItem('tablesorterView_' + tableId);
        if (localStorageView !== 'undefined' && select.children('option[value="' + localStorageView + '"]').length > 0) {
            setValue = localStorageView;
        }

        // url hash (first priority)
        var hashParameters = this._getTablesorterUrlHash();
        var value = hashParameters[tableId];
        if (typeof value !== 'undefined' && select.children('option[value="' + value + '"]').length > 0) {
            setValue = value;
        }

        // apply view change and get last view
        lastSelect = setValue;
        select.val(setValue).change();

        // create edit view dropdown
        var viewDropdownHtml = '<div class="btn-group qfq-tablesorter-menu-item" style="right: 53px;">' +
            '<button type="button" class="btn btn-default btn-group form-control qfq-view-editor dropdown-toggle" data-toggle="dropdown" style="border-radius: 0 0 0 0;" >' +
            '<i class="fa fa-pencil-alt"></i>' +
            '</button>' +
            '<ul class="dropdown-menu pull-right" role="menu">' +
            '<li><a href="#" data-save-private-view>Save Personal View</a></li>' +
            '<li><a href="#" data-save-public-view>Save Group View</a></li>' +
            '<li><a href="#" data-delete-view>Delete View</a></li>' +
            '</ul>' +
            '</div>';
        var viewDropdown = $(viewDropdownHtml).appendTo(tablesorterMenuWrapper);

        var SavePrivateViewButton = viewDropdown.find('[data-save-private-view]');
        SavePrivateViewButton.click(function () {
            var viewFromSelect = that._parseViewSelectValue(select.val());
            that._saveTableViewPrompt(table, viewFromSelect.name, false);
        });

        var SavePublicViewButton = viewDropdown.find('[data-save-public-view]');
        SavePublicViewButton.click(function () {
            var viewFromSelect = that._parseViewSelectValue(select.val());
            that._saveTableViewPrompt(table, viewFromSelect.name, true);
        });

        var DeleteViewButton = viewDropdown.find('[data-delete-view]');
        DeleteViewButton.click(function () {
            var viewFromSelect = that._parseViewSelectValue(select.val());
            viewFromSelect.mode = 'delete';
            var sip = table.attr("data-tablesorter-sip");
            $.post(that.tablesorterApiUrl + "?s=" + sip, viewFromSelect, function (response) {
                location.reload(true);
            }, 'json').fail(function (xhr, status, error) {
                that._alert('Error while trying to save view:<br>' + JSON.parse(xhr.responseText).message);
            });
        });


    };

    n.TablesorterController.prototype._doColumnSelector = function (table, tablesorterMenuWrapper, uniqueIdentifier) {
        var columnSelectorId = "qfq-column-selector-" + uniqueIdentifier;
        var columnSelectorTargetId = "qfq-column-selector-target-" + uniqueIdentifier;
        var columnSelectorHtml = '<button id="' + columnSelectorId + '" class="btn btn-default qfq-tablesorter-menu-item qfq-column-selector" ' +
            'type="button">' +
            '<span class="dropdown-text"><i class="fa fa-columns"></i></span>' +
            '<span class="caret"></span></button>' +
            '<div class="hidden"><div id="' + columnSelectorTargetId + '" class="qfq-column-selector-target"> </div></div>';
        $(columnSelectorHtml).appendTo(tablesorterMenuWrapper);
        $.tablesorter.columnSelector.attachTo($(table), '#' + columnSelectorTargetId);
        $('#' + columnSelectorId).popover({
            placement: 'left',
            container: 'body',
            html: true, // required if content has HTML
            content: $('#' + columnSelectorTargetId)
        });

        table.on('columnUpdate', function () {
            var config = table[0].config;

            if ($.tablesorter.hasWidget(table, 'filter')) {
                var visibleColumns = config.selector.states;
                var filters = $.tablesorter.getFilters(table);

                // Clear filters for hidden columns
                for (var i = 0; i < visibleColumns.length; i++) {
                    if (!visibleColumns[i]) {
                        filters[i] = '';
                    }
                }

                // Apply / Trigger updated filters
                table.trigger('search', [filters]);
            }
        });
    };

    n.TablesorterController.prototype._doPager = function (table, uniqueIdentifier) {
        var pagerId = "qfq-pager-" + uniqueIdentifier;
        var pagerHtml = `
        <div id="${pagerId}" class="qfq-tablesorter-group qfq-tablesorter-group-bottom">
            <div class="btn-group " role="group" >
                <!-- Erster Button mit abgerundeten Ecken -->
                <button type="button" class="btn btn-default first" style="border-top-left-radius: 100px; border-bottom-left-radius: 100px; padding: 4px;">
                    <span class="glyphicon glyphicon-step-backward"></span>
                </button>
                <!-- Innerer Button -->
                <button type="button" class="btn btn-default prev" style="border-radius: 0; padding: 4px;">
                    <span class="glyphicon glyphicon-backward" style="padding-right: 3px"></span>
                </button>
            </div>
            <span class="pagedisplay qfq-tablesorter-font" style="border-color: #d6d6d6; border-width: 1px 0px 1px 0px; border-top-style: solid; border-bottom-style: solid;  color: #555; padding-top: 6px; "></span>
            <div class="page-controll" style="width: unset; display: inline;">
                <select class="form-control input-sm pagesize qfq-tablesorter-font" title="Select page size" style="display: inline; width: unset;  border-radius: 0px 0px 0px 0px !important; padding-left: 2px;">
                    <option selected="selected" value="10">10</option>
                    <option value="25">25</option>
                    <option value="50">50</option>
                    <option value="100">100</option>
                    <option value="all">All Rows</option>
                </select><select class="form-control input-sm pagenum qfq-tablesorter-font" title="Select page number" style="display: inline; width: unset; border-radius: 0px 0px 0px 0px !important; padding-left: 2px;">
                </select>
            </div>
            <div class="btn-group" role="group">
                <!-- Innerer Button -->
                <button type="button" class="btn btn-default next" style="border-radius: 0; padding: 4px;">
                    <span class="glyphicon glyphicon-forward" style="padding-left: 3px"></span>
                </button>
                <!-- Letzter Button mit abgerundeten Ecken -->
                <button type="button" class="btn btn-default last" style="border-top-right-radius: 100px; border-bottom-right-radius: 100px; padding: 4px;">
                    <span class="glyphicon glyphicon-step-forward"></span>
                </button>
            </div>
        </div>
    `;
        $(pagerHtml).insertAfter($(table));
        $(table).tablesorterPager({
            container: $("#" + pagerId),
            cssGoto: ".pagenum",
            removeRows: false,
            output: '{startRow} - {endRow} / {filteredRows}'
        });
    };


    n.TablesorterController.prototype._setTablesorterUrlHash = function (parameters) {
        var hash = '';
        for (var key in parameters) {
            // remember last pill delivers own hash which has undefined value. Catch it correctly.
            if (parameters[key] === undefined) {
                hash += ',' + key;
            } else {
                hash += ',' + key + '=' + parameters[key];
            }
        }
        window.location.replace("#" + hash.substr(1));
    };

    n.TablesorterController.prototype._getTablesorterUrlHash = function () {
        var parameterList = window.location.hash.substr(1).split(',');
        var parameters = {};
        parameterList.forEach(function (par) {
            var keyValue = par.split(/=(.+)/);
            parameters[keyValue[0]] = keyValue[1];
        });
        delete parameters[""];
        return parameters;
    };

    n.TablesorterController.prototype._updateTablesorterUrlHash = function (table, value) {
        var tableId = table.attr("data-tablesorter-id");
        var hashParameters = this._getTablesorterUrlHash();
        hashParameters[tableId] = value;
        this._setTablesorterUrlHash(hashParameters);
    };

    n.TablesorterController.prototype._saveTableViewPrompt = function (table, viewNamePreset, isPublicView) {
        var tableId = table.attr("data-tablesorter-id");
        var viewName = prompt("Please enter a name for the view. If it already exists it will be overwritten.", viewNamePreset !== null ? viewNamePreset : "");

        // check if given view name is valid
        if (viewName === "") {
            this._alert("View not saved. Name is empty.");
            return;
        }
        if (viewName === null) {
            return;
        }
        var view = {name: viewName, public: isPublicView, tableId: tableId, view: this.getTableView(table)};

        // check if there are filters set on hidden columns.
        if (view.view.filters.some(function (f, i) {
            return f !== '' && !view.view.columnSelection.includes(i);
        })) {
            if (!confirm('There are filters set on hidden columns. Would you like to save anyway?')) {
                return;
            }
        }
        var that = this;
        var sip = table.attr("data-tablesorter-sip");
        view.view = btoa(JSON.stringify(view.view)); // encode view to base64 to prevent sql injections
        $.post(this.tablesorterApiUrl + "?s=" + sip, view, function (response) {
            that._updateTablesorterUrlHash(table, (view.public ? 'public:' : 'private:') + view.name);
            location.reload(true);
        }, 'json').fail(function (xhr, status, error) {
            that._alert('Error while trying to save view:<br>' + JSON.parse(xhr.responseText).message);
        });
    };

    n.TablesorterController.prototype._parseViewSelectValue = function (value) {
        var splitValue = value.split(/:(.+)/);
        return {name: splitValue[1], public: splitValue[0] === 'public'};
    };

    n.TablesorterController.prototype._alert = function (alertMessage) {
        var messageButtons = [{
            label: "Ok",
            eventName: 'close'
        }];
        var alert = new n.Alert({"message": alertMessage, "type": "error", modal: true, buttons: messageButtons});
        alert.show();
    };

})(QfqNS);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */
/* global console */
/* global Bloodhound */
/* global Math */

/* @depend Utils.js */

var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.TypeAhead = {};

    /**
     * Coerce corejs-typeahead into our use-case.
     *
     * @param typeahead_endpoint the endpoint to be called
     * @constructor
     */
    n.TypeAhead.install = function (typeahead_endpoint) {

        $('.qfq-typeahead:not(.tt-hint,.tt-input,.typeahead-installed)').each(function () {
            var bloodhoundConfiguration;

            var $element = $(this);

            if (typeahead_endpoint === null) {
                typeahead_endpoint = n.TypeAhead.getTypeAheadUrl($element);
            }

            // bloodhound is used to get the remote data (suggestions)
            bloodhoundConfiguration = {
                // We need to be notified on success, so we need a promise
                initialize: false,
                datumTokenizer: Bloodhound.tokenizers.obj.whitespace('key', 'value'),
                queryTokenizer: Bloodhound.tokenizers.whitespace,
                identify: function (obj) {
                    return obj.key.toString();
                },
                remote: {
                    url: n.TypeAhead.makeUrl(typeahead_endpoint, $element),
                    wildcard: '%QUERY'
                }
            };

            var url = n.TypeAhead.makeUrl(typeahead_endpoint, $element);
            url = url.replace('%QUERY', '');
            console.log(url);
            // Seems to trigger an empty query - why?
            //$.getJSON(url, {}, console.log);  // API by hand

            // initialize typeahead (either with or without tags)
            if ($element.data('typeahead-tags')) {
                n.TypeAhead.installWithTags($element, bloodhoundConfiguration);
            } else {
                n.TypeAhead.installWithoutTags(typeahead_endpoint, $element, bloodhoundConfiguration);
            }

            $element.addClass('typeahead-installed');
        });

    };

    n.TypeAhead.installWithTags = function ($element, bloodhoundConfiguration) {

        // initialize bloodhound (typeahead suggestion engine)
        var suggestions = new Bloodhound(bloodhoundConfiguration);
        suggestions.initialize();

        // create actual input field
        var $inputField = $('<input/>', {
            type: 'text',
            class: $element.attr('class')
        });
        $element.after($inputField);

        let inputContainer = $(`#` + $element.attr('id') + '-i');
        if (!inputContainer.length) {
            console.warn('Input container not found:', inputContainer);
        }

        // Function to sync `disabled`, `readonly`, and `background` styles
        function syncAttributes() {
            // Ensure the latest state of `disabled` is synced
            if ($element.is('[disabled]') || inputContainer.is('[disabled]')) {
                $inputField.attr('disabled', true);
            } else {
                $inputField.removeAttr('disabled');
            }

            // Ensure the latest state of `readonly` is synced
            if ($element.is('[readonly]') || inputContainer.is('[readonly]')) {
                $inputField.attr('readonly', true);
            } else {
                $inputField.removeAttr('readonly');
            }

            // Ensure the background style is set correctly
            if ($element.is('[disabled]') || $element.is('[readonly]') || inputContainer.is('[disabled]') || inputContainer.is('[readonly]')) {
                $inputField.css('background', '');
                $inputField.css('background-color', '');
                $inputField.css('background-color', '#eee !important');
                $inputField.css('cursor', 'not-allowed')
            } else {
                $inputField.css('background-color', '');
                $inputField.css('cursor', '')
            }
        }

        // Ensure attributes are applied correctly before observing changes
        syncAttributes();

        // Set up a MutationObserver to handle dynamic changes
        const observer = new MutationObserver(function (mutationsList) {
            let attributeChanged = false;
            for (const mutation of mutationsList) {
                if (mutation.attributeName === 'disabled' || mutation.attributeName === 'readonly') {
                    attributeChanged = true; // Detect relevant attribute changes
                }
            }

            if (attributeChanged) {
                syncAttributes(); // Sync
            }
        });

        // Observe changes on $element
        // Optionally observe `inputContainer` if it exists
        observer.observe($element[0], { attributes: true });
        if (inputContainer.length) {
            observer.observe(inputContainer[0], { attributes: true });
        }

        // prevent form submit when enter key is pressed
        $inputField.off('keyup');
        $inputField.on('keypress keyup', function (e) {
            var code = e.keyCode || e.which;
            if (code === 13) {
                e.preventDefault();
                return false;
            }
        });

        $element.off('keyup change');
        $element.on('keypress keyup change', function (e) {
            var code = e.keyCode || e.which;
            if (code === 13) {
                e.preventDefault();
                return false;
            }
        });


        // list to keep tracks of existing tags and those added during the current session
        // expected JSON format: [{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]
        var existingTags = $element.val() !== '' ? JSON.parse($element.val()) : [];

        // list of current typeahead suggestions
        var typeaheadList = existingTags.slice();

        // get list of possible keys a user can press to push a tag (list of keycodes)
        var delimiters = $element.data('typeahead-tag-delimiters');
        delimiters = delimiters !== undefined ? delimiters : [9, 13, 44];

        // validator function for pedantic mode
        var pedanticValidator = function (tag) {
            // check if tag is in typeahead suggestions
            var tagLookup = typeaheadList.filter(function (t) {return t.value.toLowerCase() === tag.toLowerCase();})[0];
            return tagLookup !== undefined;
        };

        // initialize tagsManager
        var tagApi = $inputField.tagsManager({
            deleteTagsOnBackspace: false,
            hiddenTagListName: '',
            tagClass: 'qfq-typeahead-tag',
            delimiters: delimiters,
            validator: !!$element.data('typeahead-pedantic') ? pedanticValidator : null,
        });

        // when tag is pushed, look up key and add it to existingTags
        tagApi.bind('tm:pushed', function (e, tag) {
            if(tag === "") return;
            var tagLookup = typeaheadList.filter(function (t) {return t.value.toLowerCase() === tag.toLowerCase();})[0];
            if (undefined === tagLookup) {
                existingTags.push({key: 0, value: tag});
            } else {
                existingTags.push({key: tagLookup.key, value: tagLookup.value});
            }

            const $lastTag = $element.parent().find('.tm-tag').last();

            // Add class to inner <span> with the value
            $lastTag.addClass('badge');

            // Only change where the tag get inserted if the parent is a 'input-group' (extraButtonInfo/Lock/Password)
            const parent = $element.parent(); // Get the parent of $element
            if (parent.hasClass('input-group')){
                const tagElement = parent.find('.tm-tag').last(); // Get the last added tag
                tagElement.detach().insertBefore(parent); // Move the tag before the parent
            }
        });

        // when the hidden input field changes, overwrite value with tag object list
        tagApi.bind('tm:hiddenUpdate', function (e, tags) {
            var tagObjects = $.map(tags, function (t) {
                return existingTags.filter(function (tt) {return tt.value === t;})[0];
            });
            $element.val(JSON.stringify(tagObjects));
        });

        // if value of hidden field is changed externally, update tagManager
        $element.on('qfqChange', function () {
            tagApi.tagsManager('disableHiddenUpdate', true);
            existingTags = $element.val() !== '' ? JSON.parse($element.val()) : [];
            tagApi.tagsManager('empty');
            $.each(existingTags, function (i, tag) {
               tagApi.tagsManager('pushTag', tag.value);
            });
            tagApi.tagsManager('disableHiddenUpdate', false);
        });

        // add existing tags
        tagApi.tagsManager('disableHiddenUpdate', true);
        $.each(existingTags, function (i, tag) {
            tagApi.tagsManager('pushTag', tag.value);
        });
        tagApi.tagsManager('disableHiddenUpdate', false);

        console.log(JSON.parse(JSON.stringify(n.TypeAhead.getMinLength($element))));

        function suggestionsWithDefaults(q, sync, async) {
            if (q === '') {
                sync(suggestions.index.all().slice(0, 2)); // slice(start,end)
                // suggestions.search('', sync, async);
            }
            else {
                suggestions.search(q, sync, async);
            }
        }
        $inputField.data('bloodhound', suggestions);

        // add typahead
        $inputField.typeahead({
                // options
                hint: n.TypeAhead.getHint($element),
                highlight: n.TypeAhead.getHighlight($element),
                minLength: n.TypeAhead.getMinLength($element)
            }, {
                display: 'value',
                source: suggestionsWithDefaults,
                limit: n.TypeAhead.getLimit($element),
                templates: {
                    suggestion: function (obj) {
                        return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
                    },
                    // No message if field is not set to pedantic.
                    notFound: (function ($_) {
                        return function (obj) {
                            if (!!$element.data('typeahead-pedantic'))
                                return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
                        };
                    })($inputField)
            }
        });

        // directly add tag when clicked on in typahead menu
        $inputField.bind('typeahead:selected', function (event, sugg) {
            tagApi.tagsManager("pushTag", sugg.value);
        });

        // update typahead list when typahead changes
        $inputField.bind('typeahead:render', function (event, sugg) {
            typeaheadList.length = 0;
            typeaheadList.push.apply(typeaheadList, sugg);
        });
    };

    n.TypeAhead.installWithoutTags = function (typeahead_endpoint, $element, bloodhoundConfiguration) {
        var $shadowElement;

        // Prefetch the value that is already in the field
        if ($element.val() !== '') {
            bloodhoundConfiguration.prefetch = {};
            bloodhoundConfiguration.prefetch.url = n.TypeAhead.makePrefetchUrl(typeahead_endpoint, $element.val(), $element);
            // Disable cache, we expect only a few entries. Caching gives sometimes strange behavior.
            bloodhoundConfiguration.prefetch.cache = false;
        }

        // create a shadow element with the same value. This seems to be important for the pedantic mode. (?)
        $shadowElement = n.TypeAhead.makeShadowElement($element);

        // prefetch data
        var suggestions = new Bloodhound(bloodhoundConfiguration);
        var promise = suggestions.initialize();

        // use shadow element to back fill field value, if it is in the fetched suggestions (why?)
        promise.done((function ($element, suggestions) {
            return function () {
                n.TypeAhead.fillTypeAheadFromShadowElement($element, suggestions);
            };
        })($element, suggestions));

        $element.typeahead({
                // options
                hint: n.TypeAhead.getHint($element),
                highlight: n.TypeAhead.getHighlight($element),
                minLength: n.TypeAhead.getMinLength($element)
            },
            {
                // dataset
                display: 'value',
                source: suggestions,
                limit: n.TypeAhead.getLimit($element),
                templates: {
                    suggestion: function (obj) {
                        return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
                    },
                    // No message if field is not set to pedantic.
                    notFound: (function ($_) {
                        return function (obj) {
                            if (!!$_.data('typeahead-pedantic'))
                                return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
                        };
                    })($element)
                }
            });

        $element.css('background-color', '')
        // bind select and autocomplete events
        $element.bind('typeahead:select typeahead:autocomplete', function (event, suggestion) {
            var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget));
            $shadowElement.val(suggestion.key);
        });

        // bind change event
        if (!!$element.data('typeahead-pedantic')) {
            // Typeahead pedantic: Only allow suggested inputs
            $element.bind('typeahead:change', n.TypeAhead.makePedanticHandler(suggestions));
            $element.on('keydown', (function (suggestions) {
                return function (event) {
                    if (event.which === 13) {
                        n.TypeAhead.makePedanticHandler(suggestions)(event);
                    }
                };
            })(suggestions));
            // The pedantic handler will test if the shadow element has a value set (the KEY). If not, the
            // typeahead element is cleared. Thus we have to guarantee that no value exists in the shadow
            // element the instant the user starts typing since we don't know the outcome of the search.
            //
            // If we don't clear the shadow element the instant the user starts typing, and simply let the
            // `typeahead:select` or `typeahead:autocomplete` handler set the selected value, the
            // user might do following steps and end up in an inconsistent state:
            //
            //  1. Use typeahead to select/autocomplete a suggestion
            //  2. delete the suggestion
            //  3. enter a random string
            //  4. submit form
            //
            // This would leave a stale value in the shadow element (from step 1.), and the pedantic handler
            // would not clear the typeahead element, giving the impression the value in the typeahead element will be submitted.
            $element.on('input', (function ($shadowElement) {
                return function () {
                    $shadowElement.val('');
                };
            })($shadowElement));
        } else {
            $element.bind('typeahead:change', function (event, suggestion) {
                var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget));
                // If none pendatic, suggestion key might not exist, so use suggestion instead.
                $shadowElement.val(suggestion.key || suggestion);
            });
        }
    };


    n.TypeAhead.makePedanticHandler = function (bloodhound) {
        return function (event) {
            var $typeAhead = $(event.delegateTarget);
            var $shadowElement = n.TypeAhead.getShadowElement($typeAhead);
            if ($shadowElement.val() === '') {
                $typeAhead.typeahead('val', '');

                // This triggers after saving and causes unnecessary dialog window (do you really want to leave...)
                //$typeAhead.closest('form').change();
            }
        };
    };

    n.TypeAhead.makeUrl = function (endpoint, element) {
        return endpoint + "?_ta_query=%QUERY" + "&sip=" + n.TypeAhead.getSip(element);
    };
    n.TypeAhead.makePrefetchUrl = function (endpoint, prefetchKey, element) {
        return endpoint + "?_ta_prefetch=" + encodeURIComponent(prefetchKey) + "&sip=" + n.TypeAhead.getSip(element);
    };

    n.TypeAhead.getLimit = function ($element) {
        return $element.data('typeahead-limit');
    };

    n.TypeAhead.getSip = function ($element) {
        return $element.data('typeahead-sip');
    };

    n.TypeAhead.getName = function ($element) {
        return $element.attr('name');
    };

    n.TypeAhead.getValue = function ($element) {
        return $element.val();
    };

    n.TypeAhead.getMinLength = function ($element) {
        return $element.data('typeahead-minlength') !== undefined ? $element.data('typeahead-minlength') : 2;
    };

    n.TypeAhead.getHighlight = function ($element) {
        return $element.data('typeahead-highlight') || true;
    };

    n.TypeAhead.getHint = function ($element) {
        return $element.data('typeahead-hint') || true;
    };

    n.TypeAhead.htmlEncode = function (value) {
        return $('<div/>').text(value).html();
    };

    n.TypeAhead.makeShadowElement = function ($element) {
        var $parent, inputName, inputValue, uniqueId, $shadowElement;

        $parent = $element.parent();
        inputName = $element.attr('name');
        $element.removeAttr('name');

        inputValue = $element.val();

        $shadowElement = $('<input>')
            .attr('type', 'hidden')
            .attr('name', inputName)
            .val(inputValue);

        $element.data('shadow-element', $shadowElement);

        $parent.append($shadowElement);

        return $shadowElement;
    };

    n.TypeAhead.getShadowElement = function ($element) {
        return $element.data('shadow-element');
    };

    n.TypeAhead.fillTypeAheadFromShadowElement = function ($element, bloodhound) {
        var results;
        var $shadowElement = n.TypeAhead.getShadowElement($element);
        var key = $shadowElement.val();
        if (key === '') {
            return;
        }

        results = bloodhound.get(key);
        if (results.length === 0) {
            return;
        }
        $element.typeahead('val', results[0].value);
    };

    n.TypeAhead.getTypeAheadUrl = function ($element) {
        return $element.data('typeahead-url');
    };
})(QfqNS);

// fix for safari to make the right input field clickable. Safari doesn't get the right sequence of z-index from the two typeahead input fields which are overlaid.
// z-index can not be set in qfq because the input field is generated by typeahead.bundle.min.js, changes are needed to do at the end of DOM after everything is loaded.
$(window).on("load",function() {
    $(document).ready(function(){
        $(".tt-input").each(function(){
            if($(this).css("z-index") !== "auto"){
                $(this).prev().css("z-index",1);
            }
        });
    });
});
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.escapeJqueryIdSelector = function (idSelector) {
        return idSelector.replace(/(:|\.)/, "\\$1");
    };

    n.initializeQmore = function () {
        var more = '[...]';
        var less = '[<<]';

        $('span.qfq-more-text').each(function () {
            // Check if a sibling button with class "qmore" already exists.
            if ($(this).siblings('button.qmore').length === 0) {
                var moreButtonHtml = '<button class="btn btn-link qmore" type="button" style="outline-width: 0px;">' + more + '</button>';
                var moreButton = $(moreButtonHtml).insertAfter($(this));
                moreButton.click(function () {
                    var moreText = $(this).siblings('span.qfq-more-text');
                    if ($(this).text() === more) {
                        $(this).text(less);
                        moreText.show();
                    } else {
                        $(this).text(more);
                        moreText.hide();
                    }
                });
            }
        });
    };

    /** Decode a Base64 Encoded file name that was Encoded by QFQ
     *
     * @param uniqueFileName
     * @returns {*|string}
     */
    n.decodeUniqueFileName = function (uniqueFileName) {
        // Separate the directory from the file name (if any)
        let dir = '';
        let file = uniqueFileName;
        if (uniqueFileName.indexOf('/') !== -1) {
            const parts = uniqueFileName.split('/');
            file = parts.pop();
            dir = parts.join('/');
        }

        // If the file does not contain the "_B64" marker.
        if (file.indexOf('_B64') === -1) {
            // return it as-is.
            return uniqueFileName;
        }


        //   ^(.*?)        -> Optionally capture any prefix ending with an underscore.
        //   _B64\d*_      -> Match "_B64" followed by optional digits and an underscore.
        //   (.+?)         -> Base64 encoded portion.
        //   (\.[^.]+)?$   -> file extension.
        const regex = /^(.*?)_B64\d*_(.+?)(\.[^.]+)?$/;
        const matches = file.match(regex);
        if (!matches) {
            // Pattern didn't match; it was not encoded by QFQ return original.
            return uniqueFileName;
        }


        // matches[1] = optional prefix, matches[2] = encoded file name part, matches[3] = extension (if any)
        let prefix = matches[1] || '';
        let encodedPart = matches[2];
        let extension = matches[3] || '';

        // Reverse replacement: replace all '-' with '/' and '_' with '='.
        encodedPart = encodedPart.replace(/-/g, '/');
        encodedPart = encodedPart.replace(/_/g, '=');

        // Decode the Base64 encoded portion.
        let decodedBaseName;
        try {
            decodedBaseName = atob(encodedPart);
        } catch (e) {
            // Decoding failed return original name.
            console.error("Base64 decoding error:", e);
            return uniqueFileName;
        }

        // Reassemble the decoded file name.
        return prefix ? (prefix + '_' + decodedBaseName + extension) : (decodedBaseName + extension);
    }

})(QfqNS);
window.MathJax = {
    tex: {
        inlineMath: [['$', '$'], ['\\(', '\\)']]
    },
    svg: {
        fontCache: 'global'
    }
};
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     *
     * @param jqHXR
     * @param textStatus
     * @param errorThrown
     *
     * @function QfqNS.Helper.showAjaxError
     */
    n.showAjaxError = function (jqHXR, textStatus, errorThrown) {
        var alert = new QfqNS.Alert("Error:<br> " +
            errorThrown, "error");
        alert.show();
    };

    /**
     *
     * @param string
     * @returns {*}
     *
     * @function QfqNS.Helper.stringBool
     */
    n.stringToBool = function (string) {
        if (typeof string !== "string") {
            return string;
        }
        var lowerCase = string.toLowerCase().trim();

        switch (lowerCase) {
            case "1":
            case "yes":
            case "y":
            case "t":
            case "true":
            case "enabled":
            case "enable":
                return true;
            case "0":
            case "no":
            case "n":
            case "f":
            case "false":
            case "disabled":
            case "disable":
                return false;
            default:
                return false;
        }
    };
})(QfqNS.Helper);
/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */

var QfqNS = QfqNS || {};

/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    let activeTooltips = new Map(); // Tracks tooltips by trigger element
    let pinnedTooltips = new Set(); // Stores pinned tooltips
    let hideTimeout = null; // Delay for hiding tooltips
    let showTimeout = null; // Delay for showing tooltips

    function createTooltipElement(tooltipText) {
        const tooltip = document.createElement('div');
        tooltip.classList.add('tooltip-container');
        tooltip.innerHTML = `
            <div class="tooltip-header">
                <button class="button pin-btn"><i class="fas fa-thumbtack"></i></button>
                <button class="button copy-btn"><i class="fas fa-clipboard"></i></button>
            </div>
            <div class="tooltip-content">${tooltipText.replace(/\n/g, '<br>')}</div>
        `;
        return tooltip;
    }

    function showTooltip(trigger, tooltipText) {
        if (activeTooltips.has(trigger)) {
            const { tooltip, popperInstance } = activeTooltips.get(trigger);
            tooltip.style.display = 'block';
            popperInstance.update();
            return tooltip;
        }

        const tooltip = createTooltipElement(tooltipText);
        document.body.appendChild(tooltip);

        const popperInstance = Popper.createPopper(trigger, tooltip, {
            placement: 'bottom',
            modifiers: [
                { name: 'preventOverflow', options: { boundary: 'viewport' } },
                { name: 'offset', options: { offset: [0, -3] } },
            ],
        });

        activeTooltips.set(trigger, { tooltip, popperInstance });
        addTooltipEventListeners(tooltip, trigger, popperInstance);


        tooltip.style.display = 'block';
        popperInstance.update();
        return tooltip;
    }

    function addTooltipEventListeners(tooltip, trigger, popperInstance) {
        const copyBtn = tooltip.querySelector('.copy-btn');
        const pinBtn = tooltip.querySelector('.pin-btn');

        copyBtn.addEventListener('click', () => {
            const textContent = tooltip.querySelector('.tooltip-content').innerText;

            if (navigator.clipboard && window.isSecureContext) {
                // Use modern clipboard API
                navigator.clipboard.writeText(textContent)
                    .then(() => showCopySuccess())
                    .catch(err => {
                        console.error('Clipboard API failed:', err);
                        fallbackCopyText(textContent);
                    });
            } else {
                // Use fallback for insecure context or unsupported browser
                fallbackCopyText(textContent);
            }
        });

        function fallbackCopyText(text) {
            const textarea = document.createElement('textarea');
            textarea.value = text;

            // Avoid scrolling to bottom
            textarea.style.position = 'fixed';
            textarea.style.top = '0';
            textarea.style.left = '0';
            textarea.style.opacity = '0';
            document.body.appendChild(textarea);
            textarea.focus();
            textarea.select();

            try {
                const success = document.execCommand('copy');
                if (success) {
                    showCopySuccess();
                } else {
                    console.warn('execCommand copy failed');
                }
            } catch (err) {
                console.error('Fallback copy failed: ', err);
            }

            document.body.removeChild(textarea);
        }

        function showCopySuccess() {
            copyBtn.innerHTML = '<i class="fas fa-check copied"></i>';
            setTimeout(() => {
                copyBtn.innerHTML = '<i class="fas fa-clipboard"></i>';
            }, 1000);
        }



        pinBtn.addEventListener('click', () => {
            if (pinnedTooltips.has(tooltip)) {
                tooltip.classList.remove('pinned');
                pinnedTooltips.delete(tooltip);
                pinBtn.innerHTML = '<i class="fas fa-thumbtack"></i>';
            } else {
                tooltip.classList.add('pinned');
                pinnedTooltips.add(tooltip);
                pinBtn.innerHTML = '<i class="fas fa-thumbtack copied"></i>';
            }
        });

        tooltip.addEventListener('mouseenter', () => {
            clearTimeout(hideTimeout);
        });

        tooltip.addEventListener('mouseleave', () => {
            if (!pinnedTooltips.has(tooltip)) {
                hideTooltipWithDelay(tooltip, popperInstance, trigger);
            }
        });
    }

    function hideTooltipWithDelay(tooltip, popperInstance, trigger) {
        hideTimeout = setTimeout(() => {
            if (!pinnedTooltips.has(tooltip)) {
                tooltip.style.display = 'none';
            }
        }, 200);
    }

    function initializeStickyToolTip() {
        document.querySelectorAll('.tooltip-trigger').forEach(trigger => {
            const tooltipText = trigger.getAttribute('title');
            if (tooltipText) {
                trigger.removeAttribute('title');
            }

            trigger.addEventListener('mouseenter', () => {
                clearTimeout(showTimeout);
                showTimeout = setTimeout(() => {
                    showTooltip(trigger, tooltipText);
                }, 500);
            });

            trigger.addEventListener('mouseleave', () => {
                clearTimeout(showTimeout);
                const tooltipData = activeTooltips.get(trigger);
                if (tooltipData && !pinnedTooltips.has(tooltipData.tooltip)) {
                    hideTooltipWithDelay(tooltipData.tooltip, tooltipData.popperInstance, trigger);
                }
            });
        });
    }

    n.initializeStickyToolTip = initializeStickyToolTip;
})(QfqNS.Helper);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global console */
/* global CodeMirror */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * Initializes codemirror. Only `<textarea/>` elements having the class `qfq-codemirror` are initialized.
     *
     * The codemirror configuration has to be provided in the `data-config` attribute as JSON. E.g.
     *
     *      <textarea class="qfq-codemirror" data-config='{ "mode": "qfq", "lineNumbers": true }'></textarea>
     *
     * @function
     */
    var codemirror = function () {
        if (typeof CodeMirror === 'undefined') {
            return;
        }
        // helper to add/remove overlay
        function toggleCmOverlay(cmWrapper, shouldBlock) {
            let $ov = cmWrapper.find('.cm-blocker');
            if (shouldBlock && !$ov.length) {
                $ov = $('<div class="cm-blocker"></div>').css({
                    position:   'absolute',
                    top:        0,
                    left:       0,
                    width:      '100%',
                    height:     '100%',
                    zIndex:     9,
                    background: '#cacaca',
                    cursor:     'not-allowed',
                    opacity:    0.3
                });
                cmWrapper.append($ov);
            } else if (!shouldBlock && $ov.length) {
                $ov.remove();
            }
        }

        $("textarea.qfq-codemirror:not(.cm-extern)").filter(function() {
            return !$(this).next().hasClass("CodeMirror");
        }).each(function () {
            var cmFocusOn = false;
            var oldElement = '';
            var config = {};
            var $this = $(this);
            var height = $this.data('height');
            var width = $this.data('width');
            var configData = $this.data('config');
            if (configData) {
                if (configData instanceof Object) {
                    config = configData;
                } else {
                    QfqNS.Log.warning("'data-config' is invalid: " + configData);
                }
            }
            var cm = CodeMirror.fromTextArea(this, configData);


            // --------------- EXTRA BUTTONS START ----------
            var $textarea = $(this);
            var $btns = $textarea
                .parent('div')
                .find('> .extra-buttons-code-mirror')
                .detach();

            if ($btns.length) {
                $btns.detach();

                // grab the CM wrapper & make it position:relative
                var $cmWrapper = $(cm.getWrapperElement())
                    .css('position','relative');

                // append buttons into wrapper
                $cmWrapper.append(
                    $btns
                        .css({
                            position:       'absolute',
                            top:            '5px',
                            right:          '5px',
                            zIndex:         '10',
                            display:        'flex',
                            justifyContent: 'flex-end',
                            fontSize:       '12px',
                        })
                        .find('button').css({
                        padding:    '2px 4px',
                        fontSize:   '12px',
                        lineHeight: '1',
                        height:     'auto',
                        minWidth:   'auto'
                    }).end()
                );
                var h = $btns.outerHeight(true);
                $cmWrapper
                    .find('.CodeMirror-scroll')
                    .css('padding-top', h + 'px');

                // —— LOCK BUTTON LOGIC ——
                var $lockBtn = $btns
                    .filter('.extraButtonLock')
                    .add($btns.find('.extraButtonLock'));
                if ($lockBtn.length) {
                    // track lock state
                    let isLocked = true;

                    // apply initial lock
                    cm.setOption('readOnly', 'nocursor');
                    toggleCmOverlay($cmWrapper, true);
                    $lockBtn.addClass('locked');

                    // on click
                    $lockBtn.on('click', function() {
                        isLocked = !isLocked;
                        if (isLocked) {
                            cm.setOption('readOnly', 'nocursor');
                            toggleCmOverlay($cmWrapper, true);
                            $lockBtn
                                .removeClass('unlocked')
                                .addClass('locked');
                        } else {
                            cm.setOption('readOnly', false);
                            toggleCmOverlay($cmWrapper, false);
                            $lockBtn
                                .removeClass('locked')
                                .addClass('unlocked');
                        }
                    });
                }
            }
            // --------------- EXTRA BUTTONS END ----------

                // Handle width and height settings
                height = height && parseInt(height) > 0 ? height : 'auto';
                width = width && parseInt(width) > 0 ? width : (width === 0 ? 'auto' : null);

                // Set Home key behavior to move to the start of the visual line
                CodeMirror.keyMap.default.Home = "goLineLeftSmart";

                // Set END key behavior to move to the end of  the visual line
                // CodeMirror.keyMap.default.End = "goLineRightSmart";

                cm.setSize(width, height);

            cm.on('change', (function ($form, $textArea) {
                return function (instance, changeObj) {
                    var actualValue = cm.getValue();
                    if (actualValue !== oldElement && cmFocusOn) {
                        $form.change();
                    }
                };
            })($(this).closest('form'), $this));

                    cm.on('focus', function () {
                        oldElement = $this.val();
                        cmFocusOn = true;
                    });

                    cm.on('blur', function () {
                        var actualValue = cm.getValue();
                        if (actualValue !== oldElement && cmFocusOn) {
                            $this.val(actualValue);
                            $this.trigger('change');
                        }
                        cmFocusOn = false;
                    });

                    // If codemirror has been loaded hidden, refresh once visible
                    $('a[data-toggle="tab"]').on('shown.bs.tab', function() {
                        if(cm.getGutterElement().clientHeight === 0) return;
                        if(cm.getGutterElement().style.getPropertyValue("height")) return;
                        cm.refresh();
                    });

                }
            );

    };

    n.codemirror = codemirror;

})(QfqNS.Helper);

// Separate js to force codemirror for reports in frontend. No Form needed with this.
$(document).ready(function () {
    document.activeElement.blur();
    var externWindow = $(".externWindow");
    var targetEditReportButton = $(".targetEditReport");
    var htmlContent = $('');

    // select all edit buttons from content records and remove onclick attribute to prevent showing content in primary window if clicked. onclick attribute is not removed in php to have the opportunity for old way of editing in front end.
    if (targetEditReportButton !== undefined) {
        for (var i = 0; targetEditReportButton.length > i; i++) {
            var currentTarget = targetEditReportButton[i];
            $(currentTarget).removeAttr("onclick");
        }
    }

    // We prepare the content for extern window and show it. Only if onclick doesn't exist. Compatibility for old way is given this way.
    $(targetEditReportButton).click(function () {
        var baseUrl = $(this).data('base-url');
        if (!$(this).is("[onclick]")) {
            var formContent = $($(this).next()[0].outerHTML);
            showHtmlEditor(formContent, baseUrl);
        }
    });

    //function to show editor window
    function showHtmlEditor(formContent, baseUrl) {
        $(formContent[0]).removeAttr("class");
        $(formContent[0]).removeAttr("style");
        $(formContent[0]).addClass("externWindow");
        var idNameForWindow = $(formContent[0]).attr('id');
        htmlContent = '<!DOCTYPE html>' + $("head").html() + $(formContent)[0].outerHTML;
        newWindow(idNameForWindow);
    }

    // function to create new window with given content for editing
    function newWindow(windowName) {
        var w = window.open('//' + location.host + location.pathname + '?tt-content=' + windowName, windowName, 'width=900,height=700');
        w.document.write(htmlContent);
        w.document.close();
    }

    // Show same content editor with refreshed data again after save. Control it with the given get parameter. First fetch only needed html content again (form) and open it in same window.
    var urlParams = new URLSearchParams(window.location.search);
    var ttContentParam = urlParams.get('tt-content');

    if (ttContentParam !== null && $(targetEditReportButton).next("#" + ttContentParam)[0] !== undefined) {
        var formContent = $($(targetEditReportButton).next("#" + ttContentParam)[0].outerHTML);
        showHtmlEditor(formContent);
    }

    // execute changes(post) and reload page with id of tt-content as get parameter. Staying in same window with new content.
    $(externWindow).submit(function () {
        $.post($(externWindow).attr('action'), $(externWindow).serializeArray())
            .done(function (data) {
                var badge;
                if (data.status === "error") {
                    data.message = data.message.replace(/(<([^>]+)>)/gi, "\n");
                    alert(data.message);
                    badge = '<span style="position:absolute; top:5px; right:5px; background-color: red;" class="badge badge-danger">Failed</span>';
                } else {
                    badge = '<span style="position:absolute; top:5px; right:5px; background-color: green;" class="badge badge-success">Saved</span>';
                }
                $(badge)
                    .insertBefore('.save-message')
                    .delay(3000)
                    .fadeOut(function () {
                        $(this).remove();
                    });
            });
        return false;
    });

    // enable CodeMirror for extern window
    $(externWindow).children(".qfq-codemirror").each(
        function () {
            var config = {};
            var $this = $(this);
            var configData = $this.data('config');
            if (configData) {
                if (configData instanceof Object) {
                    // jQuery takes care of decoding data-config to JavaScript object.
                    config = configData;
                } else {
                    QfqNS.Log.warning("'data-config' is invalid: " + configData);
                }
            }

            // Add viewportMargin to the configuration, makes whole content searchable
            configData.viewportMargin = Infinity;

            var cm = CodeMirror.fromTextArea(this, configData);
            cm.on('change', (function ($form, $textArea) {
                return function (instance, changeObj) {
                    $textArea.val(instance.getValue());
                    $form.change();
                };

            })($(this).closest('form'), $this));
            // This added class is needed to not fire the script more than once
            $(this).addClass("cm-done");
            // For the extern window we use the whole place to show CodeMirror
            $(externWindow).css('height', '100%');
            var heightHeader = parseFloat($(this).prev()[0].offsetHeight / $(externWindow).height ()) * 100;
            var heightMain = 100 - heightHeader;
            $(this).next().css('height', heightMain + '%');

            window.addEventListener('resize', function(e){
                var heightHeader = parseFloat($('.tt-content-bar')[0].offsetHeight / $(externWindow).height()) * 100;
                var heightMain = 100 - heightHeader;
                $('.CodeMirror').css('height', heightMain + '%');
            }, true);
        }
    );
});
/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    n.initColorPicker = function(formReference) {
        const triggers = document.querySelectorAll('.qfq-color-picker');
        if (this.dynamicUpdate) {
            this.form.qfqForm.formUpdateHandler();
        }

        triggers.forEach(el => {
            // Parse the config JSON from data-config
            let config;
            const form = el.closest('form');
            try {
                config = JSON.parse(el.dataset.config);
            } catch (e) {
                console.warn("Invalid data-config on element:", el);
                return;
            }
            const dynamicUpdate = config.dynamicUpdate === 'data-load'

            const pickr = Pickr.create({
                el: el,
                theme: config.theme || 'classic',
                default: config.default || '#000000',
                swatches: config.swatches || [],
                disabled: config.disabled || false,
                appClass: config.appClass || '',
                components: {
                    preview: config.preview !== false,
                    opacity: config.opacity !== false,
                    hue: true,
                    interaction: {
                        hex: config.hex !== false,
                        rgba: config.rgba !== false,
                        input: config.input !== false,
                        clear: config.clear !== false,
                        save: config.save !== false
                    }
                }
            });

            const hiddenInput = document.querySelector(`input[name="${config.dataTarget}"]`);

            pickr.on('save', (color) => {
                form.dispatchEvent(new Event('change', { bubbles: false }))
                pickr.hide();
                let hex = '';
                if (color) {
                    hex = color.toHEXA().toString();
                }
                if (hiddenInput) {
                    hiddenInput.value = hex;
                }
                if (dynamicUpdate) {
                    formReference.qfqForm.formUpdateHandler();
                }
            });
        });
    }
})(QfqNS.Helper);
/**
 * @author Zen Zalapski <zen.zalapski@math.uzh.ch>
 */

/**
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

/**
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    n.storeData = {};
    n.infoData = {};
    n.performanceData = {};
    n.generatedLinkText = '';
    n.logPollingInterval = null;
    n.tabContent = '';
    n.downloadButtonLinks = '';


    /**
     * Initializes the Developer Panel.
     *
     * This function sets up:
     * - DOM references for panel elements and tab content
     * - Backend API endpoints (via SIP injection)
     * - Fetching of initial data: stores, info, and performance
     * - Event listeners for toggling, closing, clearing dirty state, and applying changes
     * - Tab switching behavior with polling control
     *
     * Data is loaded from the backend and rendered via individual tab loaders.
     * Status pills are updated to reflect loading results.
     *
     */
    n.initializeDevPanel = function () {
        // Cache references to key DOM elements
        const toggleDevPanelBtn = document.getElementById('toggle-dev-panel');
        const closeDevPanelBtn = document.getElementById('close-dev-panel');
        const devPanel = document.getElementById('dev-panel');
        const tabs = document.querySelectorAll('.tab-dev-panel');
        n.tabContent = document.getElementById('tab-content-dev-panel');

        // Stop if the dev panel container is not found
        if (!devPanel) return;
        document.body.appendChild(toggleDevPanelBtn);
        document.body.appendChild(devPanel);

        // Extract and parse SIP-based endpoint and button data from the DOM
        n.endPointUrl = devPanel.getAttribute('data-endpoint-url') + '?s=';
        n.apiSips = JSON.parse(devPanel.getAttribute('data-api-sips'));
        n.downloadButtonLinks = JSON.parse(devPanel.getAttribute('data-download-buttons'));
        n.logPollingUrl = n.endPointUrl + n.apiSips.logs;

        // Load Store Data
        const storeUrl = n.endPointUrl + n.apiSips.store;
        fetch(storeUrl, { method: 'GET' })
            .then(r => {
                if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
                const contentType = r.headers.get('content-type');
                if (contentType && contentType.includes('application/json')) return r.json();
                throw new Error('Invalid JSON response');
            })
            .then(response => {
                if (response.status === 'success') {
                    n.storeData = response.data;
                    loadStores();
                    setTabStatus('stores', 'success');
                } else {
                    throw new Error(response.message);
                }
            })
            .catch(e => {
                console.error('Could not Load Store Data:', e);
                setTabStatus('stores', 'error', `Failed to load stores: ${e.message}`);
            });

        // Load Info Data
        const infoUrl = n.endPointUrl + n.apiSips.info;
        fetch(infoUrl, { method: 'GET' })
            .then(r => {
                if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
                const contentType = r.headers.get('content-type');
                if (contentType && contentType.includes('application/json')) return r.json();
                throw new Error('Invalid JSON response');
            })
            .then(response => {
                if (response.status === 'success') {
                    n.infoData = response.data;
                    setTabStatus('info', 'success');
                } else {
                    throw new Error(response.message);
                }
            })
            .catch(e => {
                console.error('Could not load Info Data:', e);
                setTabStatus('info', 'error', 'Failed to load info data');
            });

        // Load Performance Data
        const performanceUrl = n.endPointUrl + n.apiSips.performance;
        fetch(performanceUrl, { method: 'GET' })
            .then(r => {
                if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
                const contentType = r.headers.get('content-type');
                if (contentType && contentType.includes('application/json')) return r.json();
                throw new Error('Invalid JSON response');
            })
            .then(response => {
                if (response.status === 'success') {
                    n.performanceData = response.data;
                    setTabStatus('performance', 'success');
                } else {
                    throw new Error(response.message);
                }
            })
            .catch(e => {
                console.error('Could not load Performance Data:', e);
                setTabStatus('performance', 'error', 'Failed to load performance data');
            });

        // Clear Dirty Table Button
        document.getElementById('clear-dirty-dev-panel').addEventListener('click', async () => {
            const confirmClear = window.confirm('Are you sure you want to clear the dirty table?');
            if (!confirmClear) return;

            const button = document.getElementById('clear-dirty-dev-panel');
            const originalText = button.textContent;
            const clearDirtyUrl = n.endPointUrl + n.apiSips.clearDirty;

            try {
                const response = await fetch(clearDirtyUrl, { method: 'GET' });
                const data = await response.json();

                if (data.status === 'success') {
                    button.textContent = 'Cleared!';
                    button.classList.add('success');
                    button.disabled = true;
                    setTimeout(() => {
                        button.textContent = originalText;
                        button.classList.remove('success');
                        button.disabled = false;
                    }, 1500);
                } else {
                    console.error('Error clearing dirty table:', data.message);
                    button.textContent = 'Failed to Clear!';
                    button.classList.add('fail');
                    button.disabled = true;
                    setTimeout(() => {
                        button.textContent = originalText;
                        button.classList.remove('fail');
                        button.disabled = false;
                    }, 2500);
                }
            } catch (e) {
                console.error('Could not truncate Dirty Table:', e);
                button.textContent = 'API Request failed!';
                button.classList.add('fail');
                button.disabled = true;
                setTimeout(() => {
                    button.textContent = originalText;
                    button.classList.remove('fail');
                    button.disabled = false;
                }, 2500);
            }
        });

        // Apply Changes Button
        document.getElementById('apply-changes-dev-panel').addEventListener('click', () => {
            const confirmApply = window.confirm('Are you sure you want to apply changes made to the Stores?');
            if (!confirmApply) return;

            const applyChangesUrl = n.endPointUrl + n.apiSips.applyChanges;
            fetch(applyChangesUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(n.storeData)
            })
                .then(r => r.json())
                .then(response => {
                    if (response.status === 'success') {
                        window.location.reload();
                    } else {
                        console.error('Error applying changes:', response.message);
                        alert('Failed to apply changes: ' + response.message);
                    }
                })
                .catch(e => {
                    console.error("Can't apply changes:", e);
                });
        });

        // Toggle and close Dev Panel behavior
        toggleDevPanelBtn.addEventListener('click', () => {
            devPanel.style.right = devPanel.style.right === '0px' ? '-600px' : '0px';
        });

        closeDevPanelBtn.addEventListener('click', () => {
            devPanel.style.right = '-600px';
            clearInterval(n.logPollingInterval);
        });

        // Tab switching behavior
        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                tabs.forEach(t => t.classList.remove('active'));
                tab.classList.add('active');
                activateTab(tab.dataset.tab);
            });
        });
    };



    /**
     * Activates and loads the selected Dev Panel tab by name.
     *
     * This function clears any ongoing polling and then the correct
     * content is loader based on the provided tab name. If the tab name does not match any known tab,
     * it shows a simple placeholder message.
     *
     * @param tabName - The identifier of the tab to activate (e.g., 'stores', 'logs', etc.)
     */
    function activateTab(tabName) {
        // Stop any existing polling when switching tabs
        clearInterval(n.logPollingInterval);

        // Route the tab name to its corresponding loader
        switch (tabName) {
            case 'stores':
                loadStores();
                break;
            case 'logs':
                loadLogs();
                startPolling();
                break
            case 'info':
                loadInfo();
                break
            case 'new-link':
                loadNewLink();
                break
            case 'performance':
                loadPerformance();
                break
            default:
                // Fallback: show a simple placeholder if the tab name is unknown
                n.tabContent.innerHTML = `<p>${tabName.charAt(0).toUpperCase() + tabName.slice(1)} Content</p>`;
                break
        }
    }



    /**
     * Loads and renders the "Stores" tab in the Dev Panel.
     *
     * This function dynamically builds UI blocks for each store defined in `n.storeData`.
     * Each store includes:
     * - A collapsible header
     * - A search bar to filter keys
     * - A table listing all key-value pairs
     * - Input fields (editable unless marked as readOnly)
     *
     * User edits are directly stored in `n.storeData` for later submission.
     *
     */
    function loadStores() {
        // Clear the current content
        n.tabContent.innerHTML = '';

        // Loop over each store in the data
        for (const [storeName, storeValues] of Object.entries(n.storeData)) {
            // Create the main container for this store
            const storeItem = document.createElement('div');
            storeItem.classList.add('store-item');

            // Create and append the store header
            const storeHeader = document.createElement('div');
            storeHeader.classList.add('store-header-dev-panel');
            storeHeader.textContent = storeName;
            storeItem.appendChild(storeHeader);

            // Create the content area that holds search, table, etc.
            const storeContent = document.createElement('div');
            storeContent.classList.add('store-content');

            // Add a search input to filter keys
            const searchBar = document.createElement('input');
            searchBar.type = 'text';
            searchBar.classList.add('search-bar-dev-panel');
            searchBar.style.marginBottom = '5px';
            searchBar.placeholder = 'Search key...';
            storeContent.appendChild(searchBar);

            // Message to show if no matching keys are found
            const noResults = document.createElement('div');
            noResults.classList.add('no-results-dev-panel');
            noResults.textContent = 'No results found';
            storeContent.appendChild(noResults);

            // Table to hold key-value pairs
            const table = document.createElement('table');
            table.classList.add('store-table');

            // Spilt the actual store data and read-only flag
            const { data, readOnly } = storeValues;

            // Build each key-value row
            for (const [key, value] of Object.entries(data)) {
                const row = document.createElement('tr');

                // Key column
                const keyCell = document.createElement('td');
                keyCell.textContent = key;

                // Value column with input
                const valueCell = document.createElement('td');
                valueCell.style.textAlign = 'right';

                const valueInput = document.createElement('input');
                valueInput.type = 'text';
                valueInput.value = value;
                valueInput.disabled = readOnly;

                // Update user changes
                const storeRef = n.storeData[storeName].data;
                valueInput.addEventListener('input', (e) => {
                    storeRef[key] = e.target.value;
                });

                valueCell.appendChild(valueInput);
                row.appendChild(keyCell);
                row.appendChild(valueCell);
                table.appendChild(row);
            }

            // Append all components
            storeContent.appendChild(table);
            storeItem.appendChild(storeContent);
            n.tabContent.appendChild(storeItem);
        }

        // Enable collapsible behavior for each store
        setupExpand('.store-header-dev-panel');

        // Search filtering
        setupSearchFilter();
    }


    /**
     * Loads and renders the "Logs" tab in the Dev Panel.
     *
     * This function creates UI blocks for each type of log (QFQ, SQL, Mail),
     * including:
     * - A collapsible header
     * - A read-only textarea to display log content (populated during polling)
     * - A download link for each log file
     *
     * The textarea is keyed with `data-log-key`, and log content is updated via polling.
     *
     */
    function loadLogs() {
        // Clear previous tab content
        n.tabContent.innerHTML = '';

        // Define available log types and their internal keys
        const logs = [
            { name: "QFQ Log", key: "qfq" },
            { name: "SQL Log", key: "sql" },
            { name: "Mail Log", key: "mail" }
        ];

        // Create and render a log block for each log type
        logs.forEach(({ name, key }) => {
            // Wrapper div
            const logItem = document.createElement('div');
            logItem.classList.add('log-item-dev-panel');

            // Header for the log section (collapsible)
            const logHeader = document.createElement('div');
            logHeader.classList.add('log-header-dev-panel');
            logHeader.textContent = name;
            logItem.appendChild(logHeader);

            // Container for textarea and download link
            const logContent = document.createElement('div');
            logContent.classList.add('log-content-dev-panel');

            // Textarea where log content will be added during polling
            const logTextarea = document.createElement('textarea');
            logTextarea.readOnly = true;
            logTextarea.setAttribute('data-log-key', key);
            logContent.appendChild(logTextarea);

            // Create a download link for the log file
            const downloadLink = document.createElement('a');
            downloadLink.classList.add('download-button-dev-panel');
            downloadLink.textContent = `Download ${name}`;
            downloadLink.href = n.downloadButtonLinks[key] || '#';
            downloadLink.download = '';
            downloadLink.style.display = 'inline-block';

            // If no URL is set, prevent download and log error
            downloadLink.addEventListener('click', (e) => {
                if (!n.downloadButtonLinks[key]) {
                    e.preventDefault();
                    console.error(`No URL found for ${name}`);
                }
            });

            // Append the download link and assemble the component
            logContent.appendChild(downloadLink);
            logItem.appendChild(logContent);
            n.tabContent.appendChild(logItem);
        });

        // Enable collapsible behavior for each log header
        setupExpand('.log-header-dev-panel');
    }


    /**
     * Loads and displays system information in the "Info" tab of the Dev Panel.
     *
     * This function reads data from `n.infoData` (filled during panel initialization)
     * and renders it as a list of key-value pairs, where the key describes the type of information
     * (e.g., PHP version, Webserver) and the value provides the detail.
     *
     */
    function loadInfo() {
        // Clear any existing content
        n.tabContent.innerHTML = '';

        // Create the container that will show all info
        const infoList = document.createElement('div');
        infoList.className = 'info-list-dev-panel';

        // Loop over each key-value pair in the info data
        for (const [key, value] of Object.entries(n.infoData)) {
            const item = document.createElement('div');
            item.innerHTML = `<strong>${key}:</strong> ${value}`;
            infoList.appendChild(item);
        }

        // Insert the populated list into the tab content area
        n.tabContent.appendChild(infoList);
    }


    /**
     * Loads and initializes the "Gen Link" tab in the Dev Panel.
     *
     * This tab provides functionality to:
     * - Input custom link data
     * - Generate a link by sending it to the backend
     * - Display the generated link
     * - Copy the link to clipboard
     *
     *
     * @function loadNewLink
     */
    function loadNewLink() {
        // backend API endpoint for generating the link
        const linkUrl = n.endPointUrl + n.apiSips.link;

        // HTML structure
        n.tabContent.innerHTML = `
        <div class="new-link-container">
            <input type="text" id="link-input" placeholder="Enter link details">
            <button id="generate-link-dev-panel">Generate Link</button>
            <div class="generated-link" id="generated-link-dev-panel">${n.generatedLinkText}</div>
            <button id="copy-link-dev-panel">Copy Link</button>
        </div>
    `;

        // Set up the event listener for generating the link
        document.getElementById('generate-link-dev-panel').addEventListener('click', () => {
            const linkInput = document.getElementById('link-input').value;

            // Send the input data to the backend via POST
            fetch(linkUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ link: linkInput })
            })
                .then(r => r.json())
                .then(response => {
                    if (response.status === 'success') {
                        // Save and display the generated link
                        n.generatedLinkText = response.data.link;
                        document.getElementById('generated-link-dev-panel').textContent = response.data.link;
                        setTabStatus('new-link', 'success');
                    } else {
                        throw new Error(response.message);
                    }
                })
                .catch(e => {
                    console.error("Can't create link:", e);
                    document.getElementById('generated-link-dev-panel').textContent = 'Failed to generate link';
                    setTabStatus('new-link', 'error', `Failed to generate link: ${e.message}`);
                });

            // Show placeholder text immediately while waiting for the response
            n.generatedLinkText = linkInput || 'Generated Link';
            document.getElementById('generated-link-dev-panel').textContent = n.generatedLinkText;
        });

        // Set up the event listener for copying the generated link to the clipboard
        document.getElementById('copy-link-dev-panel').addEventListener('click', () => {
            const textToCopy = document.getElementById('generated-link-dev-panel').textContent;
            const copyBtn = document.getElementById('copy-link-dev-panel');
            const originalText = copyBtn.textContent;

            // Use clipboard API if available
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(textToCopy).then(() => {
                    copyBtn.textContent = 'Copied!';
                    copyBtn.disabled = true;
                    setTimeout(() => {
                        copyBtn.textContent = originalText;
                        copyBtn.disabled = false;
                    }, 1500);
                }).catch(err => {
                    console.error('Failed to copy: ', err);
                });
            } else {
                // Fallback for older browsers: use a temporary textarea element
                const textArea = document.createElement('textarea');
                textArea.value = textToCopy;
                document.body.appendChild(textArea);
                textArea.select();
                try {
                    const successful = document.execCommand('copy');
                    if (successful) {
                        copyBtn.textContent = 'Copied!';
                        copyBtn.disabled = true;
                        setTimeout(() => {
                            copyBtn.textContent = originalText;
                            copyBtn.disabled = false;
                        }, 1500);
                    } else {
                        console.error('Fallback: Copy command was unsuccessful');
                    }
                } catch (err) {
                    console.error('Fallback: Oops, unable to copy', err);
                }
                document.body.removeChild(textArea);
            }
        });
    }


    /**
     * Sets up toggle functionality for expandable sections in the Dev Panel (e.g., stores or logs).
     *
     * This function attaches a click event listener to all elements matching the provided selector.
     * When clicked, it toggles the visibility of the next sibling element
     *
     */
    function setupExpand(selector) {
        // Select all expandable header elements
        document.querySelectorAll(selector).forEach(header => {
            // Attach click listener to each header
            header.addEventListener('click', () => {
                const content = header.nextElementSibling;
                // Toggle the visibility of the content
                content.style.display = content.style.display === 'block' ? 'none' : 'block';
            });
        });
    }


    /**
     * Initializes search filtering for each store's key list in the Dev Panel.
     *
     * This function attaches an `input` event listener to all elements with the class `.search-bar-dev-panel`.
     * As the user types into a search bar, it filters the table rows based on whether
     * the row's key matches the search input. If no matches are found, a "no results" message is shown.
     *
     */
    function setupSearchFilter() {
        // Select all search bars within the Dev Panel
        document.querySelectorAll('.search-bar-dev-panel').forEach(searchBar => {
            // Attach an input listener to each search bar
            searchBar.addEventListener('input', (e) => {
                const filter = e.target.value.toLowerCase();
                const storeContent = e.target.parentElement;
                const rows = storeContent.querySelectorAll('table tr');
                const noResults = storeContent.querySelector('.no-results-dev-panel');
                let anyVisible = false;

                // Loop through each row to check if the key cell matches the search value
                rows.forEach(row => {
                    const keyCell = row.querySelector('td');
                    if (keyCell) {
                        const text = keyCell.textContent.toLowerCase();
                        const matches = text.includes(filter);
                        row.style.display = matches ? '' : 'none';
                        if (matches) anyVisible = true;
                    }
                });

                // Show "no results" message only if no rows are visible
                noResults.style.display = anyVisible ? 'none' : 'block';
            });
        });
    }


    /**
     * Starts polling for log data every second and updates the textareas in the "Logs" tab.
     *
     * If an error occurs during polling (e.g. network or parsing issue), the interval is cleared
     * and polling is stopped. The status pill for the "Logs" tab is updated to error.
     *
     */
    function startPolling() {
        // Clear any existing polling interval to avoid duplicates
        clearInterval(n.logPollingInterval);

        // Start a new polling interval
        n.logPollingInterval = setInterval(() => {
            fetch(n.logPollingUrl, { method: 'GET' })
                .then(r => r.json())
                .then(response => {
                    const data = response.data;

                    // Update each matching textarea with new log content
                    Object.keys(data).forEach(key => {
                        const textarea = document.querySelector(`[data-log-key="${key}"]`);
                        if (textarea) {
                            textarea.value = data[key];
                        }
                        // Mark the tab status as successfully updated
                        setTabStatus('logs', 'success');
                    });
                })
                .catch(e => {
                    // On error, stop polling and mark tab as errored
                    console.error('Polling Failed: Stopping Log Polling');
                    clearInterval(n.logPollingInterval);
                    setTabStatus('logs', 'error', `Failed to load logs: ${e.message}`);
                });
        }, 1000); // Poll every 1000 ms
    }


    /**
     * Dynamically builds and loads the "Performance" tab content in the Dev Panel.
     *
     * This function reads SQL performance data from `n.performanceData`, which is filled
     * during initialization. It creates a table where each row represents a SQL query and its execution time.
     * The query level and a short preview are shown by default, with the ability to toggle full query text.
     * A visual bar indicates the relative execution time based on the maximum time observed.
     *
     */
    function loadPerformance() {
        n.tabContent.innerHTML = '';

        if (!Array.isArray(n.performanceData) || n.performanceData.length === 0) {
            n.tabContent.innerHTML = '<p>No performance data available.</p>';
            return;
        }

        n.performanceData.forEach((block, blockIndex) => {
            const blockEntries = Object.entries(block);
            if (blockEntries.length === 0) return;

            // Create the header and make it clickable
            const title = document.createElement('h3');
            title.className = 'toggle-block-header';
            title.innerHTML = `<span class="arrow">▼</span> Report ${blockIndex + 1}`;
            title.style.cursor = 'pointer';

            // Create a container to hold the block's table (so we can toggle it)
            const container = document.createElement('div');
            container.className = 'block-container';

            // Table creation
            const table = document.createElement('table');
            table.className = 'performance-table';

            const headerRow = document.createElement('tr');
            ['Query (Level)', 'Time (ms)', 'Relative Time'].forEach(text => {
                const th = document.createElement('th');
                th.textContent = text;
                headerRow.appendChild(th);
            });
            table.appendChild(headerRow);

            const maxTime = Math.max(...blockEntries.map(([, data]) => data.time));

            blockEntries.forEach(([level, { query, time }]) => {
                const row = document.createElement('tr');

                const queryCell = document.createElement('td');
                queryCell.textContent = `${level}: ${query.substring(0, 20)}${query.length > 20 ? '…' : ''}`;
                queryCell.style.cursor = 'pointer';
                queryCell.title = 'Click to show full query';

                const expandRow = document.createElement('tr');
                const expandCell = document.createElement('td');
                expandCell.colSpan = 3;
                expandCell.className = 'expanded-query-cell';
                expandCell.style.display = 'none';
                expandCell.textContent = query;
                expandRow.appendChild(expandCell);

                queryCell.addEventListener('click', () => {
                    const isHidden = expandCell.style.display === 'none';
                    expandCell.style.display = isHidden ? 'table-cell' : 'none';
                    queryCell.title = isHidden ? 'Click to hide full query' : 'Click to show full query';
                });

                const timeCell = document.createElement('td');
                timeCell.textContent = `${time}ms`;

                const barCell = document.createElement('td');
                const bar = document.createElement('div');
                bar.className = 'performance-bar-dev-panel';
                bar.style.width = `${(time / maxTime) * 100}%`;
                barCell.appendChild(bar);

                row.appendChild(queryCell);
                row.appendChild(timeCell);
                row.appendChild(barCell);

                table.appendChild(row);
                table.appendChild(expandRow);
            });

            container.appendChild(table);

            // Handle toggle on click
            title.addEventListener('click', () => {
                const isHidden = container.style.display === 'none';
                container.style.display = isHidden ? 'block' : 'none';

                const arrow = title.querySelector('.arrow');
                arrow.textContent = isHidden ? '▼' : '▶';
            });

            // Append everything
            n.tabContent.appendChild(title);
            n.tabContent.appendChild(container);
        });
    }




    /**
     * Updates the visual status indicator (pill) for a given Dev Panel tab.
     *
     * This function sets the CSS class and tooltip (`title`) of the status pill
     * element with a specific tab, showing whether the tab's
     * data is loading, successfully loaded, or failed to load.
     *
     * @param tabKey - The key of the tab (e.g. 'stores', 'logs', etc.), used to find the pill element by ID.
     * @param status - The status type: 'success', 'loading', or 'error'. Determines pill color.
     * @param title='' - Optional custom tooltip text. If not provided, a default based on `status` is used.
     */
    function setTabStatus(tabKey, status, title = '') {
        // Get the pill element by its dynamic ID
        const pill = document.getElementById(`status-${tabKey}`);
        if (!pill) return;

        // Set the CSS class
        pill.className = `status-pill ${status}`;

        // Set the tooltip text: use provided title or fallback to default by status
        pill.title = title || ({
            success: 'Loaded successfully',
            error: 'Failed to load',
            loading: 'Loading...'
        }[status] || '');
    }

})(QfqNS.Helper)

var QfqNS = QfqNS || {};

QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';
    n.initializeDownloadProcess = async function () {
        // Queue variables to start downloads serially (session lock)
        let queueTasks = [];
        let isSessionLock = false;
        $('a.modalDownload').each(function () {
            $(this).on('click', async function (e) {
                e.preventDefault();

                // Set up variables from the link and its child span
                let link = $(this).attr('href');
                // Generate jobId (easiest way to share jobId between JS and PHP)
                let jobId = 'qfq' + Date.now();
                let dataSpan = $(this).children('span').first();
                let dataText = dataSpan.attr('data-text');
                let dataTitle = dataSpan.attr('data-title');
                // Read downloadMode early so PHP can access it via URL parameter
                let downloadMode = dataSpan.attr('data-download-mode');
                // Build endpoint URLs by appending modalDownload action parameters
                let prepFileLink = link + "&modalDownload=prepFile&jobId=" + jobId;
                let status = link + "&modalDownload=progress&jobId=" + jobId;
                let timing = link + "&modalDownload=timing&downloadMode=" + downloadMode;
                let deleteProcessFile = link + "&modalDownload=deleteFile&jobId=" + jobId;

                try {
                    // Create or reuse the shared toast container (fixed overlay, top-right corner)
                    let toastBox = document.getElementById('downloadToastBox');
                    if (!toastBox) {
                        toastBox = document.createElement("div");
                        toastBox.id = "downloadToastBox";
                        toastBox.className = "toastBox";
                        // Default positioning for the toast container
                        toastBox.style.position = 'fixed';
                        toastBox.style.top = '1px';
                        toastBox.style.right = '13px';
                        toastBox.style.overflow = 'auto';
                        toastBox.style.maxHeight = '350px';
                        toastBox.style.width = '220px';
                        toastBox.style.zIndex = '110';
                        document.body.appendChild(toastBox);
                    }

                    const toastFrame = document.createElement("div");
                    toastFrame.className = "panel panel-default";
                    toastFrame.style.marginBottom = '4px';
                    toastBox.appendChild(toastFrame);

                    const toastTitleBar = document.createElement("div");
                    toastTitleBar.className = 'panel-heading';
                    toastTitleBar.style.padding = '2px 4px';
                    toastTitleBar.style.display = 'flex';
                    toastTitleBar.style.justifyContent = 'space-between';
                    toastTitleBar.style.alignItems = 'flex-start';
                    toastFrame.appendChild(toastTitleBar);

                    const toastTitle = document.createElement("div");
                    toastTitle.textContent = dataTitle;
                    toastTitle.style.overflow = 'hidden';
                    toastTitle.style.wordBreak = 'break-word';
                    toastTitle.style.minWidth = '0';
                    toastTitleBar.appendChild(toastTitle);

                    const button = document.createElement("button");
                    button.className = "btn";
                    button.style.width = "15px";
                    button.style.height = "15px";
                    button.style.padding = "0";
                    button.style.margin = "3px";
                    button.style.display = "flex";
                    button.style.alignItems = "center";
                    button.style.justifyContent = "center";
                    button.style.flexShrink = "0";
                    toastTitleBar.appendChild(button);

                    const icon = document.createElement("span");
                    icon.className = "glyphicon glyphicon-remove";
                    button.appendChild(icon);

                    const panelBody = document.createElement("div");
                    panelBody.className = "panel-body";
                    panelBody.style.padding = '5px';
                    toastFrame.appendChild(panelBody);

                    const toastProgress = document.createElement("div");
                    toastProgress.className = 'progress';
                    toastProgress.role = 'progressbar';
                    toastProgress.ariaLabel = 'Animated striped';
                    toastProgress.style.margin = '0px';
                    toastProgress.style.height = '10px';
                    panelBody.appendChild(toastProgress);

                    const toastProgressBar = document.createElement("div");
                    toastProgressBar.className = 'progress-bar progress-bar-success progress-bar-striped active';
                    toastProgressBar.style.width = '0%';
                    toastProgressBar.setAttribute("role", "progressbar");
                    toastProgressBar.setAttribute("aria-valuenow", "0");
                    toastProgressBar.setAttribute("aria-valuemin", "0");
                    toastProgressBar.setAttribute("aria-valuemax", "100");
                    toastProgress.appendChild(toastProgressBar);

                    const toastStatus = document.createElement("div");
                    panelBody.appendChild(toastStatus);

                    const toastElement = document.createElement("div");
                    if (dataText && dataText !== 'Please wait') {
                        toastElement.textContent = dataText;
                    }
                    panelBody.appendChild(toastElement);

                    // Cancel button: remove from queue if pending, or abort active fetch
                    let controllerRef = {controller: null};
                    button.addEventListener('click', function () {
                        const index = queueTasks.findIndex(t => t.jobId === jobId);
                        if (index !== -1) {
                            // Job is still waiting in queue — remove it without starting
                            queueTasks.splice(index, 1);
                            toastFrame.remove();
                        } else {
                            // Job is already running — abort the active fetch
                            if (controllerRef.controller) {
                                controllerRef.controller.abort();
                            } else {
                                // No active request (error state) — just close the toast
                                toastFrame.remove();
                                if (toastBox.children.length === 0) {
                                    toastBox.remove();
                                }
                            }
                        }
                    });

                    // Add job to the queue with all required references
                    queueTasks.push({
                        jobId: jobId,
                        prepFileLink: prepFileLink,
                        status: status,
                        link: link,
                        toastStatus: toastStatus,
                        toastProgress: toastProgress,
                        toastProgressBar: toastProgressBar,
                        toastFrame: toastFrame,
                        toastBox: toastBox,
                        timing: timing,
                        deleteProcessFile: deleteProcessFile,
                        controllerRef: controllerRef,
                    })

                    if (!isSessionLock) {
                        await processNext();
                    }
                }
                catch (c) {
                    // Display error in console if toast setup fails
                    console.error('Download failed: ' + (c));
                }
            });
        });

        // Process queue serially — required because PHP session is locked per request
        async function processNext() {
            if (queueTasks.length === 0) {
                isSessionLock = false;
                return;
            }
            isSessionLock = true;
            const task = queueTasks.shift();

            // Stop flag for the polling loop; declared before the try so the outer catch
            // can halt polling and keep the queue alive if anything below throws (no deadlock).
            let polling = true;

            try {
                // Fetch estimated generation time to drive progress bar animation
                const timingRes = await fetch(task.timing, {method: 'GET'});
                const timingData = await timingRes.json();

                // Poll to keep session alive and advance progress bar based on estimated timing
                const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
                //Guard: missing/0 timing -> default 3s so step never becomes NaN/Infinity.
                const estimate = Number(timingData.timing) > 0 ? Number(timingData.timing) : 3;
                const step = 100 / estimate;
                const pollFile = async () => {
                    let increase = 0;
                    while (polling) {
                        try {
                            await fetch(task.status, {method: 'GET'});

                            // Advance progress bar by one step, capped at 100%
                            const pct = Math.min(increase + step, 100);
                            task.toastProgressBar.setAttribute("aria-valuenow", String(pct));
                            task.toastProgressBar.style.width = pct + "%";
                            increase += step;

                        } catch (e) {
                            console.error("Polling error: ", e);
                        }
                        await sleep(1000);
                    }
                };
                pollFile().catch(e => console.error(e));

                // Trigger file generation on PHP side and wait for the file path in the JSON response
                const controller = new AbortController();
                task.controllerRef.controller = controller;
                let dataJson;

                try {
                    const response = await fetch(task.prepFileLink, {method: 'GET', signal: controller.signal});
                    dataJson = await response.json();

                    if (dataJson.success === false) {
                        polling = false;
                        task.controllerRef.controller = null;
                        task.toastFrame.style.borderColor = 'orange';
                        task.toastProgressBar.className = 'progress-bar progress-bar-warning';
                        task.toastStatus.textContent = dataJson.error || 'Download failed';
                        // Stop polling, but keep the toast visible so the user can read the error
                        await processNext();
                        return;
                    }

                } catch (e) {
                    // User cancelled the job via the close button — clean up and continue queue
                    if (e.name === 'AbortError') {
                        await fetch(task.deleteProcessFile, {method: 'GET'});
                        polling = false;
                        task.toastFrame.remove();
                        if (task.toastBox.children.length === 0) {
                            task.toastBox.remove();
                        }
                        await processNext();
                        return;
                    }
                    throw e;
                }

                // Stop polling once file generation is complete
                polling = false;

                // Trigger browser download by navigating to the file download URL
                window.location = task.link + "&mode=" + dataJson.mode + "&modalDownload=downloadFile";

                // Always set progress to 100% even if generation was faster than 1 polling interval
                task.toastProgressBar.setAttribute("aria-valuenow", "100");
                task.toastProgressBar.style.width = "100%";

                await fetch(task.deleteProcessFile, {method: 'GET'});

                if (queueTasks.length >= 1) {
                    // More jobs pending — remove this toast immediately and process next
                    task.toastFrame.remove();
                    await processNext().catch(e => console.error(e));
                } else {
                    // Last job — keep toast visible briefly, then remove and finalize
                    setTimeout(async () => {
                        task.toastFrame.remove();
                        if (task.toastBox.children.length === 0) {
                            task.toastBox.remove();
                        }
                        await processNext().catch(e => console.error(e));
                    }, 3000);
                }
            } catch (e) {
                // Any unhandled failure (timing/deleteFile fetch, prepFile non-abort error, ...):
                // stop polling, surface the error on the toast, and keep the queue alive so the
                // session is not permanently locked (isSessionLock would otherwise stay true).
                console.error('Download failed: ', e);
                polling = false;
                task.controllerRef.controller = null;
                task.toastFrame.style.borderColor = 'orange';
                task.toastProgressBar.className = 'progress-bar progress-bar-warning';
                task.toastStatus.textContent = 'Download failed';
                await processNext();
            }
        }
    }
})(QfqNS.Helper);
/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    let copyBtn;

    function initializeEmailSelect() {
        if (!document.getElementById('emailModal')) {
            const modalHtml = `
<div id="emailModal" class="email-custom-modal hidden">
  <div class="email-custom-modal-backdrop"></div>
  <div class="email-custom-modal-content">
    <div class="email-custom-modal-header">
      <span class="email-custom-modal-title">Select Email Recipients</span>
      <button class="email-custom-modal-close" id="emailModalClose">&times;</button>
    </div>
    <div class="email-custom-modal-body">
      <form id="emailSelectorForm">
        <div id="emailCounter" style="text-align: right; margin-bottom: 6px;">Selected: 0</div>
        <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px; margin-left: 1em;">
          <input type="checkbox" id="selectAllEmails" checked title="Select All" style="margin-left: 0.5em;">
          <div id="sortEmails" title="Sort" style="width: 20px; height: 20px; display: flex; justify-content: center; align-items: center; cursor: pointer;">
            <span id="sortArrow">\u25BC</span>
          </div>
          <input type="text" id="emailFilter" placeholder="Filter emails..." class="form-control" style="flex-grow: 1;">
        </div>
        <div id="emailList" style="max-height: 300px; overflow-y: auto;"></div>
        <div style="margin-top: 10px; display: flex; justify-content: flex-end; gap: 10px;">
        <div class="btn-group" role="group"> 
        <button type="button" id="copyEmails" class="btn btn-default">Copy Emails</button>
          <button type="button" id="sendEmail" class="btn btn-primary">Send Email to Selected</button>
        </div>
        </div>
      </form>
    </div>
  </div>
</div>`;
            document.body.insertAdjacentHTML('beforeend', modalHtml);
            bindCopyButtonHandler();
            document.getElementById('emailModalClose').addEventListener('click', hideModal);
            document.querySelector('.email-custom-modal-backdrop').addEventListener('click', hideModal);
        }

        const emailListContainer = document.getElementById('emailList');
        const sendBtn = document.getElementById('sendEmail');
        const filterInput = document.getElementById('emailFilter');
        const selectAllCheckbox = document.getElementById('selectAllEmails');
        const emailCounter = document.getElementById('emailCounter');

        document.querySelectorAll('.email-popup-trigger').forEach(trigger => {
            trigger.addEventListener('click', function (e) {
                e.preventDefault();

                let emails;
                try {
                    emails = JSON.parse(this.getAttribute('data-emails') || '[]');
                } catch (err) {
                    alert('Invalid email data');
                    return;
                }

                const storageKey = 'emailSelection_' + btoa(emails.join(','));
                let emailSelections = {};
                let sortAsc = true;
                const sortArrow = document.getElementById('sortArrow');
                const sortBox = document.getElementById('sortEmails');

                const savedSelections = JSON.parse(localStorage.getItem(storageKey) || '[]');
                emails.forEach(email => {
                    emailSelections[email] = savedSelections.length === 0 || savedSelections.includes(email);
                });

                function bindCheckboxEvents() {
                    emailListContainer.querySelectorAll('input[type="checkbox"]').forEach(cb => {
                        cb.addEventListener('change', function () {
                            emailSelections[cb.value] = cb.checked;
                            updateCounterAndStorage();
                        });
                    });
                }

                function updateCounterAndStorage() {
                    const selected = Object.keys(emailSelections).filter(email => emailSelections[email]);
                    emailCounter.textContent = 'Selected: ' + selected.length;
                    localStorage.setItem(storageKey, JSON.stringify(selected));
                }

                function renderEmailList() {
                    const filterValue = filterInput.value.toLowerCase();
                    const sorted = [...emails].sort((a, b) =>
                        sortAsc ? a.localeCompare(b) : b.localeCompare(a)
                    );
                    const filtered = sorted.filter(email => email.toLowerCase().includes(filterValue));

                    emailListContainer.innerHTML = '';
                    filtered.forEach((email, i) => {
                        const id = `email_${i}`;
                        const isChecked = emailSelections[email];
                        emailListContainer.insertAdjacentHTML('beforeend', `
                            <div class="checkbox">
                                <label>
                                    <input type="checkbox" id="${id}" value="${email}" ${isChecked ? 'checked' : ''}>
                                    ${email}
                                </label>
                            </div>
                        `);
                    });

                    bindCheckboxEvents();
                    updateCounterAndStorage();
                }

                selectAllCheckbox.addEventListener('change', function () {
                    const checked = this.checked;
                    Object.keys(emailSelections).forEach(email => {
                        emailSelections[email] = checked;
                    });
                    renderEmailList();
                });

                filterInput.value = '';
                selectAllCheckbox.checked = true;
                sortArrow.textContent = '▼';

                filterInput.addEventListener('input', renderEmailList);

                sortBox.addEventListener('click', function () {
                    sortAsc = !sortAsc;
                    sortArrow.textContent = sortAsc ? '▲' : '▼';
                    renderEmailList();
                });

                sendBtn.onclick = function () {
                    const selected = Object.keys(emailSelections).filter(email => emailSelections[email]);
                    if (!selected.length) {
                        flashButtonState(sendBtn, '⚠ None selected', 'Send Email to Selected', 'btn btn-warning');
                        return;
                    }
                    window.location.href = 'mailto:' + selected.join(',');
                    flashButtonState(sendBtn, '✔ Sent', 'Send Email to Selected', 'btn btn-success');
                    hideModal();
                };

                renderEmailList();
                showModal();
            });
        });
    }

    function showModal() {
        document.getElementById('emailModal').classList.remove('hidden');
    }

    function hideModal() {
        document.getElementById('emailModal').classList.add('hidden');
    }

    function bindCopyButtonHandler() {
        copyBtn = document.getElementById('copyEmails');

        if (!copyBtn) return;

        copyBtn.addEventListener('click', () => {
            const checkboxes = document.querySelectorAll('#emailList input[type="checkbox"]');
            const selected = Array.from(checkboxes)
                .filter(cb => cb.checked)
                .map(cb => cb.value);

            if (!selected.length) {
                flashButtonState(copyBtn, '⚠ None selected', 'Copy Emails', 'btn btn-warning');
                return;
            }

            const emailString = selected.join(',');

            if (navigator.clipboard && window.isSecureContext) {
                navigator.clipboard.writeText(emailString)
                    .then(() => {
                        flashButtonState(copyBtn, '✔ Copied!', 'Copy Emails', 'btn btn-success');
                    })
                    .catch(() => {
                        fallbackCopy(emailString);
                    });
            } else {
                fallbackCopy(emailString);
            }
        });
    }

    function fallbackCopy(text) {
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.top = '0';
        textArea.style.left = '0';
        textArea.style.width = '1px';
        textArea.style.height = '1px';
        textArea.style.opacity = '0';

        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();

        try {
            const successful = document.execCommand('copy');
            flashButtonState(
                copyBtn,
                successful ? '✔ Copied!' : '❌ Failed',
                'Copy Emails',
                successful ? 'btn btn-success' : 'btn btn-danger'
            );
        } catch (err) {
            flashButtonState(copyBtn, '❌ Error', 'Copy Emails', 'btn btn-danger');
            console.error('Fallback copy failed:', err);
        }

        document.body.removeChild(textArea);
    }

    function flashButtonState(button, text, originalText, cssClass, timeout = 2000) {
        const originalClass = button.className;
        button.textContent = text;
        button.className = cssClass;
        setTimeout(() => {
            button.textContent = originalText;
            button.className = originalClass;
        }, timeout);
    }

    n.initializeEmailSelect = initializeEmailSelect;

})(QfqNS.Helper);

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */

var QfqNS = QfqNS || {};

/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    n.filePond = function createFileUpload(inputElement, form) {
        // Retrieve all needed data and configurations
        this.inputElement = inputElement;
        this.form = form;
        this.pond = null;
        this.dynamicUpdate = inputElement.getAttribute('data-load');
        const configData = inputElement.getAttribute('data-config');
        this.configuration = configData ? JSON.parse(configData) : [];
        this.normalizeConfiguration();

        this.saveButton = false;
        if (this.configuration.form) {
            const formId = this.inputElement.closest('form.qfq-form').id;
            const saveButtonId = 'save-button-' + formId;
            this.saveButton = document.querySelector('#' + saveButtonId);

            // No save button for formModeGlobal=readonly
            if(this.saveButton) {
                this.saveButtonIcon = this.saveButton.querySelector('span');
                this.saveButtonLabel = this.saveButton.innerText;
            }
        }

        const apiUrls = inputElement.getAttribute('data-api-urls');
        this.apiUrls = apiUrls ? JSON.parse(apiUrls) : [];

        const sipValues = inputElement.getAttribute('data-sips');
        this.sipValues = sipValues ? JSON.parse(sipValues) : [];

        //Initialize existing preloaded files
        this.filePondFiles = this.getPreloadedFiles(this.inputElement);

        // Initialize flags
        this.lastUploadId = null;
        this.currentFieldId = false;
        this.deletedFileId = true;
        this.lastSipTmp = false;
    };

    n.filePond.prototype.createFilePondObject = function() {
        // Create the FilePond instance
        let lastServerErrorMessage = null;
        const pond = FilePond.create(this.inputElement, {
            allowMultiple: this.configuration.multiUpload,
            allowRemove: this.configuration.deleteOption,
            allowRevert: true,
            maxFileSize: this.configuration.maxFileSize,
            allowFileSizeValidation: this.configuration.activeSizeValidation,
            acceptedFileTypes: this.configuration.accept,
            labelFileProcessingComplete: this.configuration.completeMessage,
            fileValidateTypeDetectType: (source, type) => new Promise((resolve, reject) => {
                // Get file extension
                const extension = source.name.split('.').pop().toLowerCase();
                const mimeType = source.type;

                // Split the comma-separated string and handle it as an array
                let acceptedType = null;
                const acceptedTypes = this.configuration.accept.split(',');

                // Iterate over the accepted types and find a case-insensitive match
                acceptedTypes.forEach(accepted => {
                    if (accepted.trim().toLowerCase() === '.' + extension || accepted.trim().toLowerCase() === mimeType.toLowerCase()) {
                        acceptedType = accepted.trim();
                    }
                });

                // Check if the file extension or mime type is in the allowed extensions list
                if (this.configuration.extensions.map(ext => ext.toLowerCase()).includes(extension)) {
                    resolve(acceptedType); // If yes, use the detected type
                } else if (this.configuration.mimes.includes(mimeType)) {
                        resolve(type); // Direct match found
                    } else {
                        // Check for wildcard MIME type matches if no direct match is found
                        // example image/png split to image/*
                        const mimeTypeBase = mimeType.split('/')[0] + '/*';
                        if (this.configuration.mimes.includes(mimeTypeBase)) {
                            resolve(type); // Wildcard match found
                        } else {
                            reject(type); // No valid MIME type or wildcard match found
                        }
                    }
            }),
            allowFileTypeValidation: this.configuration.activeTypeValidation,
            allowImagePreview: this.configuration.imageEditor,
            allowImageEdit: this.configuration.imageEditor,
            allowDrop: this.configuration.allowUpload,
            allowBrowse: this.configuration.allowUpload,
            allowPaste: this.configuration.allowUpload,
            labelIdle: this.configuration.text,
            maxFiles: this.configuration.maxFiles,
            allowReorder: false,
            imagePreviewMaxHeight: 150,
            styleButtonRemoveItemPosition: 'right',
            credits: false,
            dropValidation: false,
            maxParallelUploads: 1,
            files: this.filePondFiles,
            iconRemove: '<i class="fas fa-trash" style="color: white;"></i>',
            server: {
                process: {
                    url: this.apiUrls.upload + "?s=" + this.sipValues.upload,
                    method: 'POST',
                    withCredentials: false,
                    headers: {},
                    ondata: (formData) => {
                        return this.setOnData(formData);
                    },
                    onload: (response) => {
                        // response is the JSON string returned by the server
                        const res = JSON.parse(response);
                        if (this.lastUploadId === null) {
                            this.lastUploadId = res.groupId;

                        }
                        // Here you can handle the unique file ID as needed
                        // console.log('File uploaded successfully:', res.uniqueFileId);
                        // console.log('Upload Id:', res.groupId);
                        // console.log('sipTmp:', res.sipTmp);
                        this.lastSipTmp = res.sipTmp;

                        return res.uniqueFileId; // Must return the unique file ID to FilePond
                    },
                    onerror: (response) => {
                        // Handle error here
                        console.error('Error during upload:', response);

                        // Get Error Message
                        lastServerErrorMessage = "Upload failed.";
                        try {
                            const parsed = JSON.parse(response);
                            if (parsed && parsed.message) {
                                lastServerErrorMessage = parsed.message;
                            }
                        } catch (e) {
                            console.warn("Could not parse error response:");
                        }
                    }
                },
                revert: (uniqueFileId, load, error) => {
                    this.setRevert(uniqueFileId, load, error);
                },
                remove: (uniqueFileId, load, error) => {
                    this.setRemove(uniqueFileId, load, error);
                },
                load: (source, load, error, progress, abort, headers) => {
                    console.log('loaded');
                }
            },
            onprocessfile: (error, fileItem) => {
                if (error) {
                    console.error('Error processing file:', error);
                    if (error.code === 413) {
                        let alert = new QfqNS.Alert({
                            message: "Upload Failed: Image width and/or height too big.",
                            type: "error",
                            timeout: 5000
                        });
                        alert.show();
                    }
                    if (error.code === 422) {
                        const message = lastServerErrorMessage || "Upload failed.";
                        let alert = new QfqNS.Alert({
                            message: message,
                            type: "error",
                            timeout: 5000
                        });
                        alert.show();
                    }
                    if (error.code === 500) {
                        let alert = new QfqNS.Alert({
                            message: "Upload failed.",
                            type: "error",
                            timeout: 5000
                        });
                        alert.show();
                    }
                    return;
                }
                if (this.dynamicUpdate) {
                    this.form.qfqForm.formUpdateHandler();
                }

                setTimeout(() => {
                    const foundIndicators = this.findFalseIndicators();
                    // Changing style
                    foundIndicators.forEach(indicator => {
                        indicator.style.opacity = 0;
                    });
                }, 1000);
            },
            onremovefile: (error, file) => {
                if (error) {
                    console.error('Error removing file:', error);
                    return;
                }
                if (this.dynamicUpdate) {
                    this.form.qfqForm.formUpdateHandler();
                }
                this.deletedFileId = true; // Reset fileId when a file is removed

                if (this.configuration.fileNote && this.noteInputElement) {
                    this.updateNoteInputState();
                }
            },
            onaddfile: (err, fileItem) => {
                if (err) {
                    console.error('Error adding file:', err);
                    return;
                }


                const file = fileItem.file;
                // Upload is of type image pre check configured max Dimensions (#21337)
                if (file instanceof Blob && file.type && file.type.startsWith('image/')) {
                    // Get Max Dimensions from Config
                    const maxWidth = this.configuration.maxImageWidth;
                    const maxHeight = this.configuration.maxImageHeight;

                    const img = new Image();
                    try {
                        const objectURL = URL.createObjectURL(file);

                        img.onload = () => {

                            if (img.width > maxWidth || img.height > maxHeight) {
                                pond.removeFile(fileItem.id); // Remove the file from FilePond
                                let alert = new QfqNS.Alert({
                                    message: `Image too large. Max: ${maxWidth}x${maxHeight}px, Uploaded: ${img.width}x${img.height}px`,
                                    type: "error",
                                    timeout: 5000
                                });
                                alert.show();
                            }
                            URL.revokeObjectURL(objectURL);

                        };
                        img.onerror = () => {
                            console.error("Could not load image for dimension check.");
                            URL.revokeObjectURL(objectURL);
                        };
                        // Load Image
                        img.src = objectURL;
                    } catch (e) {
                        // When Preloading no information for image check
                    }

                }

                // Wait for the file item to be added to the DOM
                setTimeout(() => {
                    // Access the file item's element using FilePond's internal API
                    const item = pond.getFile(fileItem.id);
                    if (!item || !item.file || !item.id) {
                        console.error('The file item is missing information.');
                        return;
                    }

                    const fileSize = item.file.size;

                    if (!fileSize || isNaN(fileSize)) {
                        // Get file UI element
                        const fileElement = document.querySelector(`#filepond--item-${item.id}`);
                        const sizeInfo = fileElement.querySelector('.filepond--file-info-sub');

                        if (sizeInfo) {
                            sizeInfo.style.display = 'none';
                        }
                    }

                    // If it's a form-element and downloadButton is not given then there is no download button needed.
                    if (this.configuration.allowDownload && (!this.configuration.form || this.configuration.form && this.configuration.downloadButton !== false)) {
                        this.createDownloadButton(fileItem, item);
                    }

                    if (this.configuration.fileNote && this.noteInputElement) {
                        this.updateNoteInputState();
                    }
                }, 100);
            },
            oninit: () => {
                // Change the styling for filePond uploads in form
                const rootElement = pond.element;
                const dropLabel = rootElement.querySelector('.filepond--drop-label');
                const listElement = rootElement.querySelector('.filepond--list');
                if (this.configuration.form) {
                    rootElement.id = this.configuration.formId;
                    if (listElement) {
                        listElement.classList.add('filepond--list-form');
                    }
                }

                if (dropLabel && this.configuration.dropBackground !== undefined) {
                    dropLabel.classList.add('filepond--drop-label-form');
                }

                // Check if the root element has the class 'hidden-input-style'
                if (rootElement.classList.contains('hidden-input') && dropLabel) {
                    dropLabel.style.marginTop = '10px'; // Apply the margin-top style
                }


                if (this.configuration.form && this.configuration.downloadButton === false) {
                    const sizeInfo = rootElement.querySelector('.filepond--file-info-sub');
                    if (sizeInfo !== null) {
                        sizeInfo.style.display = 'none';
                    }
                }
                // Extra Buttons
                if (this.configuration.form) {
                    const filePondFile = rootElement.querySelector('.filepond--file');
                    if (filePondFile) {
                        filePondFile.insertAdjacentHTML(
                            'beforeend',
                            this.configuration.extraButton
                        );
                    }
                }

                if (this.configuration.fileNote && this.noteInputElement) {
                    this.updateNoteInputState();
                }
            }
        });

        this.pond = pond;

        // Append a note field below the FilePond input if enabled
        if (this.configuration.fileNote) {
            const noteWrapper = document.createElement('div');
            noteWrapper.className = 'filepond-note-wrapper';
            noteWrapper.style.display = 'flex';
            noteWrapper.style.alignItems = 'center';
            noteWrapper.style.gap = '10px';
            noteWrapper.style.marginBottom = '7px';

            // Create label
            const noteLabel = document.createElement('label');
            noteLabel.innerText = this.configuration.fileNote;
            noteLabel.style.margin = '0';
            noteLabel.style.minWidth = '100px';
            noteLabel.style.flexShrink = '0';

            // Create input
            const noteInput = document.createElement('input');
            noteInput.type = 'text';
            noteInput.name = this.configuration.fileNoteInputName;
            noteInput.className = 'filepond-note-input';
            noteInput.style.flex = '1';
            noteInput.style.padding = '4px';
            noteInput.style.border = '1px solid #ccc';
            noteInput.style.borderRadius = '4px';
            noteInput.style.boxSizing = 'border-box';
            noteInput.maxLength = 255;

            // Pre-fill value if given
            if (this.configuration.fileNoteText) {
                noteInput.value = this.configuration.fileNoteText;
            }

            noteWrapper.appendChild(noteLabel);
            noteWrapper.appendChild(noteInput);

            // Insert note under FilePond root element
            const pondElement = this.pond.element;
            const parent = pondElement.parentElement;

            parent.insertBefore(noteWrapper, pondElement.nextSibling);

            // access
            this.noteInputElement = noteInput;
            this.noteWrapper = noteWrapper;

            this.updateNoteInputState = () => {
                const fileCount = this.pond.getFiles().length;
                if (!this.noteInputElement) return;
                if (fileCount === 0) {
                    this.noteInputElement.value = '';
                    this.noteInputElement.readOnly = true;
                    this.noteInputElement.classList.add('readonly');
                    this.noteWrapper.style.marginTop = '0'
                } else {
                    this.noteInputElement.readOnly = false;
                    this.noteInputElement.classList.remove('readonly');
                    this.noteWrapper.style.marginTop = '-0.8em'
                }
            };
        }

    };

    // This function will return an array of all processing complete indicators
    // that have a sibling with the revert button processing class.
    n.filePond.prototype.getPreloadedFiles = function () {
        const preloadedData = this.inputElement.getAttribute('data-preloadedFiles');
        const preloadedFiles = preloadedData ? JSON.parse(preloadedData) : [];
        return preloadedFiles.length > 0 ? preloadedFiles.map(file => ({

            source: file.id,
            options: {
                type: 'local',
                file: {
                    name: QfqNS.decodeUniqueFileName(file.pathFileName.split('/').pop()),
                    size: Number(file.fileSize) || 0,
                    type: file.mimeType
                },
                metadata: {
                    poster: file.pathFileName
                }
            }
        })) : [];
    };

    // This function will return an array of all processing complete indicators
    // that have a sibling with the revert button processing class.
    n.filePond.prototype.findFalseIndicators = function () {
        const indicatorsWithRevertSibling = [];

        // Select all processing complete indicators
        const indicators = document.querySelectorAll('.filepond--processing-complete-indicator');

        indicators.forEach(indicator => {
            // Check if the revert button processing class exists as a sibling
            const revertButton = indicator.closest('.filepond--item').querySelector('.filepond--file-action-button.filepond--action-revert-item-processing');
            if (revertButton) {
                indicatorsWithRevertSibling.push(indicator);
            }
        });

        return indicatorsWithRevertSibling;
    };

    n.filePond.prototype.setOnData = function (formData) {
        if (this.lastUploadId) {
            formData.append('groupId', this.lastUploadId);
        }
        // Add your own variables here
        formData.append('pathFileName', this.configuration.pathFileName);
        formData.append('pathDefault', this.configuration.pathDefault);
        formData.append('recordData', this.configuration.recordData);
        if (this.lastUploadId == null) {
            formData.append('groupId', this.configuration.groupId);
        }
        if (this.deletedFileId) {
            formData.append('uploadId', 0);
        } else {
            formData.append('uploadId', this.configuration.uploadId);
        }
        formData.append('table', this.configuration.table);

        // Return the modified FormData object
        return formData;
    };

    n.filePond.prototype.setRevert = function (uniqueFileId, load, error) {
        const formData = new FormData();
        formData.append('uploadId', uniqueFileId);
        formData.append('table', this.configuration.table);

        // The uniqueFileId parameter is the ID returned by the server during the 'process' call
        // This ID can be used to identify and delete the file on the server
        const xhr = new XMLHttpRequest();
        xhr.open('POST', this.apiUrls.upload + `?s=${this.sipValues.delete}`);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.onload = () => {
            if (xhr.status === 200) {
                const response = JSON.parse(xhr.responseText);
                // Update the new fileId from the response, if necessary
                //this.currentFieldId = response.uniqueFileId;
                load();
            } else {
                error('oh no');
            }
        };
        xhr.send(formData);
    };

    n.filePond.prototype.setRemove = function (uniqueFileId, load, error) {
        const formData = new FormData();
        formData.append('uploadId', uniqueFileId);
        formData.append('table', this.configuration.table);

        // The uniqueFileId parameter is the ID returned by the server during the 'process' call
        // This ID can be used to identify and delete the file on the server
        const xhr = new XMLHttpRequest();
        xhr.open('POST', this.apiUrls.upload + `?s=${this.sipValues.delete}`);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.onload = () => {
            if (xhr.status === 200) {
                const response = JSON.parse(xhr.responseText);
                // Update the new fileId from the response, if necessary
                //this.currentFieldId = response.uniqueFileId;
                load();
            } else {
                error('oh no');
            }
        };
        xhr.send(formData);
        this.configuration.downloadButton = '';
    };

    // Trying to load the pictures for preview for preloaded files. Currently not working. Maybe not needed if ImageEditor is implemented.
    n.filePond.prototype.setServerLoad = function (source, load, error, progress, abort, headers) {
        if (this.allowImagePreview) {
            const fetchRequest = new Request(this.apiUrls.download + `?type=preview&fileId=` + source);
            fetch(fetchRequest).then(response => {
                if (response.ok && response.headers.get('Content-Length') > 0) {
                    response.blob().then(blob => {
                        if (blob.size > 0) {
                            load(blob);
                        } else {
                            load('');
                        }
                    });
                } else {
                    load('');
                }
            }).catch(err => {
                // Log the error but don't trigger FilePond's error state
                console.error(err.message);
                load('');
            });
            return {
                abort: () => {
                }
            };
        }

    };

    // Create new downloadButton and append it to existing upload element.
    //
    n.filePond.prototype.createDownloadButton = function (fileItem, item) {
        // Create a download button
        const downloadButton = document.createElement('button');

        // Find the filepond--file-wrapper element for this file item
        const fileElementPanel = this.getFilePondElementPanel(item);

        if (fileElementPanel) {
            let removeButton = fileElementPanel.querySelector('.filepond--file-action-button.filepond--action-remove-item');
            const fileInfo = fileElementPanel.querySelector('.filepond--file-info');

            downloadButton.classList.add('filepond--file-action-button');
            downloadButton.classList.add('filepond--action-download-item');
            downloadButton.type = 'button';
            downloadButton.setAttribute('data-align', 'right');
            downloadButton.style.marginRight = '30px';


            // Create an icon element for the downward arrow
            const icon = document.createElement('i');

            if(this.configuration.glyphicon === ''){
                icon.classList.add('fas', 'fa-arrow-down');
                icon.style.color = 'white';
            } else {
                icon.classList.add('glyphicon', this.configuration.glyphicon);
            }


            // Append the icon to the button
            downloadButton.appendChild(icon);
            // Append download button text if downloadButton config given. Only in case of form-element possible.
            if (this.configuration.downloadButton !== false && this.configuration.downloadButton !== undefined) {
                const downloadButtonText = document.createElement('span');
                downloadButtonText.innerText = this.configuration.downloadButton;
                downloadButton.appendChild(downloadButtonText);
                downloadButton.title = this.configuration.tooltip;
                downloadButton.class = "glyphicon glyphicon-ok"

                downloadButtonText.style.width = 'fit-content';
                downloadButtonText.style.height = 'fit-content';
                downloadButtonText.style.position = 'unset';
                downloadButtonText.style.marginLeft = '5px';
                downloadButtonText.style.marginRight = '5px';
                downloadButtonText.style.clipPath = 'inset(100%)';

                icon.style.marginLeft = '4px';
                downloadButton.style.left = '5px';
                downloadButton.style.marginRight = 'unset';
                downloadButton.style.display = 'flex';
                downloadButton.style.alignItems = 'center';
                downloadButton.style.width = 'auto';
                downloadButton.style.borderRadius = '6px';


                // If download button text was given then don't display file name
                if (downloadButtonText.innerText.length > 0) {
                    fileInfo.style.display = 'none';
                } else {
                    fileInfo.style.paddingLeft = '2em';
                }
            }

            // Append the download button to the filepond--file element
            fileElementPanel.appendChild(downloadButton);
        }

        // Set up the click event listener to trigger the download
        downloadButton.addEventListener('click', () => {
            let sipParameter = this.sipValues.download;
            if (this.lastSipTmp !== false && this.lastSipTmp !== undefined) {
                sipParameter = this.lastSipTmp;
            }

            // Implement the download action here
            const downloadUrl = this.apiUrls.download + `?s=${sipParameter}&sipDownloadKey=${this.configuration.sipDownloadKey}&uploadId=${fileItem.serverId}`;
            window.open(downloadUrl, '_blank');
        });
    };

    // Get fileElement panel to customize.
    //
    n.filePond.prototype.getFilePondElementPanel = function (item) {
        const fileElement = document.querySelector(`#filepond--item-${item.id}`);
        const fileWrapper = fileElement.querySelector(`.filepond--file-wrapper`);
        if (fileWrapper) {
            const fileElementPanel = fileWrapper.querySelector('.filepond--file');
            if (fileElementPanel) {
                return fileElementPanel;
            } else {
                console.error('The filepond--file element was not found.');
            }
        } else {
            console.error('The filepond--file-wrapper element was not found.');
        }
    };

    // Normalize 'null' string values to actual nulls and string 'true' to boolean
    //
    n.filePond.prototype.normalizeConfiguration = function () {
        Object.keys(this.configuration).forEach(key => {
            if (this.configuration[key] === 'null') {
                this.configuration[key] = null;
            } else if (this.configuration[key] === 'true') {
                this.configuration[key] = true;
            } else if (this.configuration[key] === 'false') {
                this.configuration[key] = false;
            }
        });

        if (this.configuration.recordData === undefined) {
            this.configuration.recordData = '';
        }

        this.configuration.activeTypeValidation = this.configuration.accept !== null;
        this.configuration.activeSizeValidation = this.configuration.maxFileSize !== null;

        this.configuration.extensions = this.filterFileTypes(this.configuration.accept, true);
        this.configuration.mimes = this.filterFileTypes(this.configuration.accept);

    };

    n.filePond.prototype.filterFileTypes = function (acceptList, extension = false) {
        const cleanedTypes = acceptList.replace(/\s+/g, '').split(',');
        const mimeTypes = cleanedTypes.filter(type => type.includes('/'));
        const extensions = cleanedTypes.filter(type => !type.includes('/')).map(ext => ext.substring(1));

        if (extension) {
            return extensions;
        } else {
            return mimeTypes;
        }
    }

    n.initializeFilePondInContainer = function (container, form) {
        const inputElements = container.querySelectorAll('input[type="file"].fileupload');
        const inputElementsMulti = container.querySelectorAll('input[type="file"].file-uploadMulti');
        const visibleInputElements = Array.from(inputElements).filter(input => input.offsetParent !== null);

        // visibleInputElements.forEach(inputElement => {
        //     let fileObject = new n.filePond(inputElement);
        //     fileObject.createFilePondObject();


            // Initializes FilePond on each input element event hidden element
        inputElements.forEach(inputElement => {

            if (inputElement.offsetParent === null) {
                // Add class or adjust style for non-visible input elements
                inputElement.classList.add('hidden-input');
            }

                let fileObject = new n.filePond(inputElement, form);
                fileObject.createFilePondObject();


            // Call the form change after file remove
            if (form !== '') {
                fileObject.pond.on('addfile', function (error, file) {

                    // Disable save button from form and change label
                    if (fileObject.saveButton) {
                        if (file.file.size !== undefined && file.file.type !== undefined && file.status === 9) {
                            form.filepondUploadProcessing.push(fileObject);
                            fileObject.saveButton.disabled = true;
                            fileObject.saveButton.classList.remove('btn-info');
                            if (fileObject.saveButtonIcon !== null) {
                                fileObject.saveButtonIcon.classList.remove('glyphicon-ok');
                                fileObject.saveButtonIcon.classList.add('glyphicon-cog');
                            } else {
                                const processLabel = document.createElement('span');
                                processLabel.className = 'tmp-process-btn glyphicon glyphicon-cog';
                                fileObject.saveButton.innerText = '';
                                fileObject.saveButton.appendChild(processLabel);
                            }

                        }
                    }
                });

                fileObject.pond.on('removefile', function (file) {
                    const element = fileObject.pond.element;
                    if (form.filepondUploadProcessing.length === 0) {
                        form.inputAndPasteHandlerCalled = true;
                        form.markChanged(element);
                    }
                });

                fileObject.pond.on('processfile', function (file) {
                    const element = fileObject.pond.element;

                    let index = form.filepondUploadProcessing.indexOf(fileObject);
                    if (index !== -1) {
                        form.filepondUploadProcessing.splice(index, 1);
                    }

                    if (form.filepondUploadProcessing.length === 0) {
                        // reset save button icon after finalized upload
                        if (fileObject.saveButton) {
                            if (fileObject.saveButtonIcon !== null) {
                                fileObject.saveButtonIcon.classList.remove('glyphicon-cog');
                                fileObject.saveButtonIcon.classList.add('glyphicon-ok');
                            } else {
                                fileObject.saveButton.querySelector('.tmp-process-btn').remove();
                                fileObject.saveButton.innerText = fileObject.saveButtonLabel;
                            }

                        }

                        form.inputAndPasteHandlerCalled = true;
                        form.markChanged(element);
                    }
                });
            }
        });

        inputElementsMulti.forEach(inputElement => {

            if (inputElement.offsetParent === null) {
                // Add class or adjust style for non-visible input elements
                inputElement.classList.add('hidden-input');
            }
            let fileObject = new n.filePondFormMulti(inputElement)
            fileObject.createFilePondObjectMulti()

            if (form !== '') {
                fileObject.pond.on('addfile', function (error, file) {

                    // Disable save button from form and change label
                    if (fileObject.saveButton) {
                        if (file.file.size !== undefined && file.file.type !== undefined && file.status === 9) {
                            form.filepondUploadProcessing.push(fileObject);
                            fileObject.saveButton.disabled = true;
                            fileObject.saveButton.classList.remove('btn-info');
                            if (fileObject.saveButtonIcon !== null) {
                                fileObject.saveButtonIcon.classList.remove('glyphicon-ok');
                                fileObject.saveButtonIcon.classList.add('glyphicon-cog');
                            } else {
                                const processLabel = document.createElement('span');
                                processLabel.className = 'tmp-process-btn glyphicon glyphicon-cog';
                                fileObject.saveButton.innerText = '';
                                fileObject.saveButton.appendChild(processLabel);
                            }

                        }
                    }
                });

                fileObject.pond.on('removefile', function (file) {
                    const element = fileObject.pond.element;
                    if (form.filepondUploadProcessing.length === 0) {
                        form.inputAndPasteHandlerCalled = true;
                        form.markChanged(element);
                    }
                });

                fileObject.pond.on('processfile', function (file) {
                    const element = fileObject.pond.element;

                    let index = form.filepondUploadProcessing.indexOf(fileObject);
                    if (index !== -1) {
                        form.filepondUploadProcessing.splice(index, 1);
                    }

                    if (form.filepondUploadProcessing.length === 0) {
                        // reset save button icon after finalized upload
                        if (fileObject.saveButton) {
                            if (fileObject.saveButtonIcon !== null) {
                                fileObject.saveButtonIcon.classList.remove('glyphicon-cog');
                                fileObject.saveButtonIcon.classList.add('glyphicon-ok');
                            } else {
                                fileObject.saveButton.querySelector('.tmp-process-btn').remove();
                                fileObject.saveButton.innerText = fileObject.saveButtonLabel;
                            }

                        }

                        form.inputAndPasteHandlerCalled = true;
                        form.markChanged(element);
                    }
                });
            }


        });
    }

    n.filePondFormMulti = function createFileUploadMulti(inputElement) {
        // Retrieve all needed data and configurations
        this.inputElement = inputElement;
        this.pond = null;

        const configData = inputElement.getAttribute('data-config');
        this.configuration = configData ? JSON.parse(configData) : [];
        this.normalizeConfiguration();

        this.saveButton = false;
        if (this.configuration.form) {
            const formId = this.inputElement.closest('form.qfq-form').id;
            const saveButtonId = 'save-button-' + formId;
            this.saveButton = document.querySelector('#' + saveButtonId);

            // No save button for formModeGlobal=readonly
            if(this.saveButton) {
                this.saveButtonIcon = this.saveButton.querySelector('span');
                this.saveButtonLabel = this.saveButton.innerText;
            }
        }

        const apiUrls = inputElement.getAttribute('data-api-urls');
        this.apiUrls = apiUrls ? JSON.parse(apiUrls) : [];

        const sipValues = inputElement.getAttribute('data-sips');
        this.sipValues = sipValues ? JSON.parse(sipValues) : [];

        //Initialize existing preloaded files
        this.filePondFiles = this.getPreloadedFilesMulti(this.inputElement);

        // Initialize flags
        this.lastUploadId = null;
        this.currentFieldId = false;
        this.deletedFileId = true;
        this.lastSipTmp = false;
    };

    n.filePondFormMulti.prototype.normalizeConfiguration = function () {
        Object.keys(this.configuration).forEach(key => {
            if (this.configuration[key] === 'null') {
                this.configuration[key] = null;
            } else if (this.configuration[key] === 'true') {
                this.configuration[key] = true;
            } else if (this.configuration[key] === 'false') {
                this.configuration[key] = false;
            }
        });

        if (this.configuration.recordData === undefined) {
            this.configuration.recordData = '';
        }

        this.configuration.activeTypeValidation = this.configuration.accept !== null;
        this.configuration.activeSizeValidation = this.configuration.maxFileSize !== null;

        this.configuration.extensions = this.filterFileTypes(this.configuration.accept, true);
        this.configuration.mimes = this.filterFileTypes(this.configuration.accept);

    };

    n.filePondFormMulti.prototype.filterFileTypes = function (acceptList, extension = false) {
        const cleanedTypes = acceptList.replace(/\s+/g, '').split(',');
        const mimeTypes = cleanedTypes.filter(type => type.includes('/'));
        const extensions = cleanedTypes.filter(type => !type.includes('/')).map(ext => ext.substring(1));

        if (extension) {
            return extensions;
        } else {
            return mimeTypes;
        }
    }

    n.filePondFormMulti.prototype.getPreloadedFilesMulti = function (inputElement) {
        const preloadedData = this.inputElement.getAttribute('data-preloadedFiles');
        return preloadedData ? stringToFilePondItemArray(preloadedData) : [];
    }

    const stringToFilePondItemArray = function (str) {

        // Split the string by commas to separate individual objects
        const objectsArray = str.split(",");

        // Trim any extra spaces and split each object by colons
        return objectsArray.map(item => {

            // Remove any leading/trailing whitespace
            const trimmedItem = item.trim();
            // Split the string into parts using ':'
            const parts = trimmedItem.split(":");

            // Map parts to an object with appropriate keys.
            return {
                source: parts[0],
                options: {
                    type: 'local',
                    file: {
                        name: parts[1].split('/').pop(),
                        size: parts[2],
                        mimeType: parts[3]
                    },
                    metadata: {
                        poster: parts[1]
                    }
                },
            };
        });

    }

    n.filePondFormMulti.prototype.createFilePondObjectMulti = function () {
        let lastServerErrorMessage = null;
        const pond = FilePond.create(this.inputElement, {
                allowMultiple: this.configuration.multiUpload,
                allowRemove: this.configuration.deleteOption,
                allowRevert: true,
                maxFileSize: this.configuration.maxFileSize,
                allowFileSizeValidation: this.configuration.activeSizeValidation,
                acceptedFileTypes: this.configuration.accept,
                labelFileProcessingComplete: this.configuration.completeMessage,
                fileValidateTypeDetectType: (source, type) => new Promise((resolve, reject) => {
                    // Get file extension
                    const extension = source.name.split('.').pop().toLowerCase();
                    const mimeType = source.type;

                    // Split the comma-separated string and handle it as an array
                    let acceptedType = null;
                    const acceptedTypes = this.configuration.accept.split(',');

                    // Iterate over the accepted types and find a case-insensitive match
                    acceptedTypes.forEach(accepted => {
                        if (accepted.trim().toLowerCase() === '.' + extension || accepted.trim().toLowerCase() === mimeType.toLowerCase()) {
                            acceptedType = accepted.trim();
                        }
                    });

                    // Check if the file extension or mime type is in the allowed extensions list
                    if (this.configuration.extensions.map(ext => ext.toLowerCase()).includes(extension)) {
                        resolve(acceptedType); // If yes, use the detected type
                    } else if (this.configuration.mimes.includes(mimeType)) {
                        resolve(type); // Direct match found
                    } else {
                        // Check for wildcard MIME type matches if no direct match is found
                        // example image/png split to image/*
                        const mimeTypeBase = mimeType.split('/')[0] + '/*';
                        if (this.configuration.mimes.includes(mimeTypeBase)) {
                            resolve(type); // Wildcard match found
                        } else {
                            reject(type); // No valid MIME type or wildcard match found
                        }
                    }
                }),
                allowFileTypeValidation: this.configuration.activeTypeValidation,
                allowImagePreview: this.configuration.imageEditor,
                allowImageEdit: this.configuration.imageEditor,
                allowDrop: this.configuration.allowUpload,
                allowBrowse: this.configuration.allowUpload,
                allowPaste: this.configuration.allowUpload,
                labelIdle: this.configuration.text,
                maxFiles: this.configuration.maxFiles,
                allowReorder: false,
                imagePreviewMaxHeight: 150,
                styleButtonRemoveItemPosition: 'right',
                credits: false,
                dropValidation: false,
                maxParallelUploads: 1,
                files: this.filePondFiles,
                iconRemove: '<i class="fas fa-trash" style="color: white;"></i>',
                server: {
                    process: {
                        url: this.apiUrls.upload + "?s=" + this.sipValues.upload,
                        method: 'POST',
                        withCredentials: false,
                        headers: {},
                        ondata: (formData) => {
                            return this.setOnData(formData);
                        },
                        onload: (response) => {
                            const res = JSON.parse(response);
                            console.log(res);
                            return res.fileIndex;
                        },
                        onerror: (response) => {
                            // Handle error here
                            console.error('Error during upload:', response);
                            // Get Error Message
                            lastServerErrorMessage = "Upload failed.";
                            try {
                                const parsed = JSON.parse(response);
                                if (parsed && parsed.message) {
                                    lastServerErrorMessage = parsed.message;
                                }
                            } catch (e) {
                                console.warn("Could not parse error response:");
                            }
                        }
                    },
                    revert: (uniqueFileId, load, error) => {
                        this.setRevert(uniqueFileId, load, error);
                    },
                    remove: (uniqueFileId, load, error) => {
                        this.setRemove(uniqueFileId, load, error);
                    },
                    load: (source, load, error, progress, abort, headers) => {
                        console.log('loaded');
                    }
                },
                onprocessfile: (error, fileItem) => {
                    if (error) {
                        console.error('Error processing file:', error);
                        if (error.code === 413) {
                            let alert = new QfqNS.Alert({
                                message: "Upload Failed: Image width and/or height too big.",
                                type: "error",
                                timeout: 5000
                            });
                            alert.show();
                        }
                        if (error.code === 422) {
                            const message = lastServerErrorMessage || "Upload failed.";
                            let alert = new QfqNS.Alert({
                                message: message,
                                type: "error",
                                timeout: 5000
                            });
                            alert.show();
                        }
                        if (error.code === 500) {
                            let alert = new QfqNS.Alert({
                                message: "Upload failed.",
                                type: "error",
                                timeout: 5000
                            });
                            alert.show();
                        }
                        return;
                    }

                    setTimeout(() => {
                        const foundIndicators = this.findFalseIndicators();

                        // Changing style
                        foundIndicators.forEach(indicator => {
                            indicator.style.opacity = 0;
                        });
                    }, 1000);

                },
                onremovefile: (error, file) => {
                    if (error) {
                        console.error('Error removing file:', error);
                        return;
                    }
                    this.deletedFileId = true; // Reset fileId when a file is removed
                },
                onaddfile: (err, fileItem) => {
                    if (err) {
                        console.error('Error adding file:', err);
                        // Wait for the file item to be added to the DOM
                    }

                    const file = fileItem.file;
                    // Upload is of type image pre-check configured max Dimensions (#21337)
                    if (file instanceof Blob && file.type && file.type.startsWith('image/')) {
                        // Get Max Dimensions from Config
                        const maxWidth = this.configuration.maxImageWidth;
                        const maxHeight = this.configuration.maxImageHeight;

                        const img = new Image();
                        try {
                            const objectURL = URL.createObjectURL(file);

                            img.onload = () => {

                                if (img.width > maxWidth || img.height > maxHeight) {
                                    pond.removeFile(fileItem.id); // Remove the file from FilePond
                                    let alert = new QfqNS.Alert({
                                        message: `Image too large. Max: ${maxWidth}x${maxHeight}px, Uploaded: ${img.width}x${img.height}px`,
                                        type: "error",
                                        timeout: 5000
                                    });
                                    alert.show();
                                }
                                URL.revokeObjectURL(objectURL);

                            };
                            img.onerror = () => {
                                console.error("Could not load image for dimension check.");
                                URL.revokeObjectURL(objectURL);
                            };
                            // Load Image
                            img.src = objectURL;
                        } catch (e) {
                            // When Preloading no information for image check
                        }
                    }

                    setTimeout(() => {
                        // Access the file item's element using FilePond's internal API
                        const item = pond.getFile(fileItem.id);
                        if (!item || !item.file || !item.id) {
                            console.error('The file item is missing information.');
                            return;
                        }


                        const fileSize = item.file.size;

                        if (!fileSize || isNaN(fileSize)) {
                            // Get file UI element
                            const fileElement = document.querySelector(`#filepond--item-${item.id}`);
                            const sizeInfo = fileElement.querySelector('.filepond--file-info-sub');

                            if (sizeInfo) {
                                sizeInfo.style.display = 'none';
                            }
                        }

                        // If it's a form-element and downloadButton is not given then there is no download button needed.
                        if (this.configuration.allowDownload && (!this.configuration.form || this.configuration.form && this.configuration.downloadButton !== false)) {
                            this.createDownloadButton(fileItem, item);
                        }
                    }, 200);
                },
                oninit: () => {
                    // Change the styling for filePond uploads in form
                    const rootElement = pond.element;
                    const dropLabel = rootElement.querySelector('.filepond--drop-label');
                    const listElement = rootElement.querySelector('.filepond--list');
                    if (this.configuration.form) {
                        rootElement.id = this.configuration.formId;
                        if (listElement) {
                            listElement.classList.add('filepond--list-form');
                        }
                    }

                    if (dropLabel && this.configuration.dropBackground !== undefined) {
                        dropLabel.classList.add('filepond--drop-label-form');
                    }

                    // Check if the root element has the class 'hidden-input-style'
                    if (rootElement.classList.contains('hidden-input') && dropLabel) {
                        dropLabel.style.marginTop = '10px'; // Apply the margin-top style
                    }


                    if (this.configuration.form && this.configuration.downloadButton === false) {
                        const sizeInfo = rootElement.querySelector('.filepond--file-info-sub');
                        if (sizeInfo !== null) {
                            sizeInfo.style.display = 'none';
                        }
                    }

                    if (rootElement.hasAttribute('data-disabled') && rootElement.getAttribute('data-disabled') === 'disabled') {
                        rootElement.parentElement.style.cursor = 'not-allowed';
                    }
                }
            }
        )

        this.pond = pond;
    }

    n.filePondFormMulti.prototype.setRemove = function (uniqueFileId, load, error) {
        const formData = new FormData();
        formData.append('uploadId', uniqueFileId);
        formData.append('table', this.configuration.table);

        // The uniqueFileId parameter is the ID returned by the server during the 'process' call
        // This ID can be used to identify and delete the file on the server
        const xhr = new XMLHttpRequest();
        xhr.open('POST', this.apiUrls.upload + `?s=${this.sipValues.delete}&fileIndex=${uniqueFileId}`);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.onload = () => {
            if (xhr.status === 200) {
                const response = JSON.parse(xhr.responseText);
                // Update the new fileId from the response, if necessary
                //this.currentFieldId = response.uniqueFileId;
                load();
            } else {
                error('oh no');
            }
        };
        xhr.send(formData);
        this.configuration.downloadButton = '';
    }

    n.filePondFormMulti.prototype.setRevert = function (uniqueFileId, load, error) {
        const formData = new FormData();
        formData.append('uploadId', uniqueFileId);
        formData.append('table', this.configuration.table);

        // The uniqueFileId parameter is the ID returned by the server during the 'process' call
        // This ID can be used to identify and delete the file on the server
        const xhr = new XMLHttpRequest();
        xhr.open('POST', this.apiUrls.upload + `?s=${this.sipValues.delete}&fileIndex=${uniqueFileId}`);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.onload = () => {
            if (xhr.status === 200) {
                const response = JSON.parse(xhr.responseText);
                // Update the new fileId from the response, if necessary
                //this.currentFieldId = response.uniqueFileId;
                load();
            } else {
                error('oh no');
            }
        };
        xhr.send(formData);
    }

    n.filePondFormMulti.prototype.setOnData = function (formData) {
        console.log(formData);

        // Add your own variables here
        formData.append('pathFileName', this.configuration.pathFileName);
        formData.append('pathDefault', this.configuration.pathDefault);
        formData.append('recordData', this.configuration.recordData);
        formData.append('table', this.configuration.table);

        // Return the modified FormData object
        return formData;
    }

    n.filePondFormMulti.prototype.createDownloadButton = function (fileItem, item) {
        // Create a download button
        const downloadButton = document.createElement('button');

        // Find the filepond--file-wrapper element for this file item
        const fileElementPanel = this.getFilePondElementPanel(item);

        if (fileElementPanel) {
            let removeButton = fileElementPanel.querySelector('.filepond--file-action-button.filepond--action-remove-item');
            const fileInfo = fileElementPanel.querySelector('.filepond--file-info');

            downloadButton.classList.add('filepond--file-action-button');
            downloadButton.classList.add('filepond--action-download-item');
            downloadButton.type = 'button';
            downloadButton.setAttribute('data-align', 'right');
            downloadButton.style.marginRight = '30px';

            // Create an icon element for the downward arrow
            const icon = document.createElement('i');

            if(this.configuration.glyphicon === ''){
                icon.classList.add('fas', 'fa-arrow-down');
                icon.style.color = 'white';
            } else {
                icon.classList.add('glyphicon', this.configuration.glyphicon);
            }


            // Append the icon to the button
            downloadButton.appendChild(icon);

            // Append download button text if downloadButton config given. Only in case of form-element possible.
            if (this.configuration.downloadButton !== false && this.configuration.downloadButton !== undefined) {
                const downloadButtonText = document.createElement('span');
                downloadButtonText.innerText = this.configuration.downloadButton;
                downloadButton.appendChild(downloadButtonText);
                downloadButton.title = this.configuration.tooltip;
                downloadButton.class = "glyphicon glyphicon-ok"

                downloadButtonText.style.width = 'fit-content';
                downloadButtonText.style.height = 'fit-content';
                downloadButtonText.style.position = 'unset';
                downloadButtonText.style.marginLeft = '5px';
                downloadButtonText.style.marginRight = '5px';
                downloadButtonText.style.clipPath = 'inset(100%)';

                icon.style.marginLeft = '4px';
                downloadButton.style.left = '5px';
                downloadButton.style.marginRight = 'unset';
                downloadButton.style.display = 'flex';
                downloadButton.style.alignItems = 'center';
                downloadButton.style.width = 'auto';
                downloadButton.style.borderRadius = '6px';

                if (downloadButtonText.innerText.length > 0) {
                    fileInfo.style.display = 'none';
                } else {
                    fileInfo.style.paddingLeft = '2em';
                }
            }

            // Append the download button to the filepond--file element
            fileElementPanel.appendChild(downloadButton);
        }

        // Set up the click event listener to trigger the download
        downloadButton.addEventListener('click', () => {
            let sipParameter = this.sipValues.download;
            if (this.lastSipTmp !== false && this.lastSipTmp !== undefined) {
                sipParameter = this.lastSipTmp;
            }

            // Implement the download action here
            const downloadUrl = this.apiUrls.download + `?s=${sipParameter}&fileIndex=${fileItem.serverId}`;
            window.open(downloadUrl, '_blank');
        });
    };

    n.filePondFormMulti.prototype.getFilePondElementPanel = function (item) {
        const fileElement = document.querySelector(`#filepond--item-${item.id}`);
        const fileWrapper = fileElement.querySelector(`.filepond--file-wrapper`);
        if (fileWrapper) {
            const fileElementPanel = fileWrapper.querySelector('.filepond--file');
            if (fileElementPanel) {
                return fileElementPanel;
            } else {
                console.error('The filepond--file element was not found.');
            }
        } else {
            console.error('The filepond--file-wrapper element was not found.');
        }
    };

    n.filePondFormMulti.prototype.findFalseIndicators = function () {
        const indicatorsWithRevertSibling = [];

        // Select all processing complete indicators
        const indicators = document.querySelectorAll('.filepond--processing-complete-indicator');

        indicators.forEach(indicator => {
            // Check if the revert button processing class exists as a sibling
            const revertButton = indicator.closest('.filepond--item').querySelector('.filepond--file-action-button.filepond--action-revert-item-processing');
            if (revertButton) {
                indicatorsWithRevertSibling.push(indicator);
            }
        });

        return indicatorsWithRevertSibling;
    };

})(QfqNS.Helper);


/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global console */
/* global FullCalendar */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * Initializes fullcalendar.js.
     *
     * The Full Calendar configuration has to be provided in the `data-config` attribute as JSON. E.g.
     *
     *      <div class="qfq-calendar" data-config='{ "plugins": [ 'dayGrid' ] }'></textarea>
     *
     * @function
     */

    // Constants
    const CALENDAR_SELECTOR = 'div.qfq-calendar';
    const CONTAINER_SELECTOR = '.qfq-calendar-container';
    const STORAGE_KEY_PREFIX = 'fullcalendar-date-';
    const DEFAULT_POPUP_OPTIONS = 'width=800,height=600,left=100,top=100,noopener,noreferrer';

    /**
     * FullCalendar initialization module
     */
    const CalendarModule = {
        // Store scroll position at module level, initialize with current position
        lastScrollPosition: { x: 0, y: 0 },
        /**
         * Initialize all calendars on the page
         */
        init(preserveScroll = false) {
            if (!this.isFullCalendarLoaded()) {
                console.warn("FullCalendar JavaScript files not loaded.");
                return;
            }

            $(CALENDAR_SELECTOR).each((index, element) => {
                this.initializeCalendar($(element));
            });

            // Restore page scroll position after initialization
            if (preserveScroll) {
                const { x, y } = this.lastScrollPosition;

                requestAnimationFrame(() => {
                    window.scrollTo(x, y);
                    setTimeout(() => {
                        if (window.scrollX !== x || window.scrollY !== y) {
                            window.scrollTo(x, y);
                        }
                    }, 50);
                });
            }
        },

        /**
         * Check if FullCalendar library is loaded
         */
        isFullCalendarLoaded() {
            return typeof FullCalendar !== 'undefined';
        },

        /**
         * Initialize a single calendar element
         */
        initializeCalendar($element) {
            // Destroy existing calendar if already initialized
            if ($element.hasClass('fc')) {
                $element.fullCalendar('destroy');
            }

            const originalConfig = this.parseConfigData($element);
            const config = this.buildConfig($element, originalConfig);
            $element.fullCalendar(config);
        },

        /**
         * Build calendar configuration from element data
         */
        buildConfig($element, originalConfig) {
            const pageId = originalConfig.pageId || 0;

            // Create final config by merging all parts
            const finalConfig = {
                ...originalConfig,
                ...this.getViewConfig(originalConfig, pageId),
                ...this.getSelectionConfig(originalConfig),
                ...this.getTooltipConfig(originalConfig),
                ...this.getEventHandlers(pageId)
            };

            // Remove our internal properties that shouldn't go to FullCalendar
            if ('pageId' in finalConfig) {
                delete finalConfig.pageId;
            }

            return finalConfig;
        },

        /**
         * Parse configuration from data attribute
         */
        parseConfigData($element) {
            const configData = $element.data('config');

            if (!configData) {
                return {};
            }

            if (configData instanceof Object) {
                return configData;
            }

            console.warn(`Invalid 'data-config': ${configData}`);
            return {};
        },

        /**
         * Get view-related configuration
         */
        getViewConfig(config, pageId) {
            const viewConfig = {};

            // Handle localStorage restore for websocket reloads
            if (config.defaultDate === false) {
                const savedDate = this.getSavedDate(pageId);
                if (savedDate) {
                    viewConfig.defaultDate = savedDate;
                }
            }

            return viewConfig;
        },

        /**
         * Get selection-related configuration
         */
        getSelectionConfig(config) {
            const selectionConfig = {};

            // Add selection restriction if specified
            if (config.restrictSelection !== undefined && config.restrictSelection !== false) {
                selectionConfig.selectAllow = (selectInfo) =>
                    selectInfo.start.isSame(selectInfo.end, config.restrictSelection);
            }

            // Add URL forwarding for selections
            if (config.urlForward !== undefined && config.urlForwardMode !== undefined) {
                selectionConfig.select = (start, end) =>
                    this.handleSelection(start, end, config);
            }
            return selectionConfig;
        },

        /**
         * Get tooltip-related configuration
         */
        getTooltipConfig(config) {
            const tooltipConfig = {};

            // Add tooltip if description is specified
            tooltipConfig.eventRender = function(event, element) {
                // Check extendedProps if you're storing description there
                let description = event.description || (event.extendedProps && event.extendedProps.description);

                if (description && description !== false) {
                    $(element).tooltip({
                        title: description,
                        placement: 'top',
                        trigger: 'hover',
                        container: 'body'
                    });
                }
            };

            return tooltipConfig;
        },

        /**
         * Get event handlers
         */
        getEventHandlers(pageId) {
            return {
                viewRender: (view) => {
                    // For month view, save the date representing the month, not view.start
                    if (view.name === 'month') {
                        // Get the 15th of the month being displayed
                        const monthDate = view.start.clone().add(15, 'days');
                        this.saveDate(pageId, monthDate);
                    } else {
                        this.saveDate(pageId, view.start);
                    }
                }
            };
        },

        /**
         * Handle calendar selection with URL forwarding
         */
        handleSelection(start, end, config) {
            const formatDate = (date) => date.format('YYYY-MM-DD HH:mm:ss');
            const isNewWindow = config.urlForwardMode === 'new';

            // Adjust end date for day-level selections (subtract 1 day)
            // This is necessary because FullCalendar treats end as exclusive
            const adjustedEnd = this.isAllDaySelection(start, end) ?
                end.clone().subtract(1, 'day') : end;

            let url = this.buildSelectionUrl(config.urlForward, {
                start: formatDate(start),
                end: formatDate(adjustedEnd)
            });

            if (config.customParams !== undefined && config.customParams.trim() !== '') {
                url += `&${config.customParams}`;
            }

            if (isNewWindow) {
                window.open(url, '_blank', DEFAULT_POPUP_OPTIONS);
            } else {
                window.location.href = url;
            }
        },

        /**
         * Check if selection is all-day (day-level selection)
         */
        isAllDaySelection(start, end) {
            // Check if both times are at midnight (00:00:00)
            return start.format('HH:mm:ss') === '00:00:00' &&
                end.format('HH:mm:ss') === '00:00:00';
        },

        /**
         * Build URL with parameters
         */
        buildSelectionUrl(url, params) {
            const queryParams = Object.entries(params)
                .filter(([_, value]) => value)
                .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
                .join('&');

            return `${url}&${queryParams}`;
        },

        /**
         * Save calendar date to localStorage
         */
        saveDate(pageId, date) {
            const key = STORAGE_KEY_PREFIX + pageId;
            localStorage.setItem(key, date.format('YYYY-MM-DD'));
        },

        /**
         * Get saved date from localStorage
         */
        getSavedDate(pageId) {
            const key = STORAGE_KEY_PREFIX + pageId;
            return localStorage.getItem(key);
        },

        /**
         * Sets up a debounced scroll listener to continuously track scroll position.
         */
        setupScrollListener() {
            let scrollTimeout;
            window.addEventListener('scroll', () => {
                clearTimeout(scrollTimeout);
                scrollTimeout = setTimeout(() => {
                    this.lastScrollPosition = {
                        x: window.scrollX || window.pageXOffset || 0,
                        y: window.scrollY || window.pageYOffset || 0
                    };
                }, 150);
            }, { passive: true });
        },

        /**
         * Setup MutationObserver for dynamic content
         */
        setupObserver() {
            const observer = new MutationObserver((mutations) => {
                if (this.shouldReinitialize(mutations)) {
                    this.init(true);
                }
            });

            // Observe all calendar containers
            document.querySelectorAll(CONTAINER_SELECTOR).forEach(container => {
                observer.observe(container, {
                    childList: true,
                    subtree: true
                });
            });
        },

        /**
         * Check if mutations contain new calendar elements
         */
        shouldReinitialize(mutations) {
            return mutations.some(mutation => {
                if (mutation.type !== 'childList') return false;

                return Array.from(mutation.addedNodes).some(node => {
                    if (node.nodeType !== 1) return false;

                    const $node = $(node);
                    return $node.is(CALENDAR_SELECTOR) ||
                        $node.find(CALENDAR_SELECTOR).length > 0;
                });
            });
        }
    };

    // Initialize on DOM ready
    document.addEventListener('DOMContentLoaded', () => {
        CalendarModule.setupScrollListener();
        CalendarModule.setupObserver();
    });

    // Export to namespace
    n.calendar = (preserveScroll = false) => CalendarModule.init(preserveScroll);

})(QfqNS.Helper);

/**
 * @author Zhoujie li <zhoujie.li@math.uzh.ch>
 * @Date: 13/05/2024
 */

/* global $ */
/* global tinymce */
/* global CodeMirror */

/**
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
var multiFormRowCounter = 0;  // Initialize a counter to keep track of added rows

/**
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    // Event listener for DOMContentLoaded to ensure the DOM is fully loaded before executing the script
    document.addEventListener('DOMContentLoaded', function () {


        // Function to add a new row
        function addRow() {
            const validator = $('form').data('bs.validator');
            const dummyRow = document.querySelector('.table-multi-form tr.dummy-row');
            if (!dummyRow) {
                return; // Exit if no dummy row found
            }

            // Clear any existing TinyMCE instances in the dummy row
            clearTinyMCEInstances(dummyRow);
            // Clear any existing CodeMirror instances in the dummy row
            clearCodeMirrorInstances(dummyRow);

            // Clone the dummy row and update its class
            const newRow = dummyRow.cloneNode(true);
            newRow.classList.remove('dummy-row');

            // Clone Hidden Input Elements (#21312)
            const hiddenInputs = document.querySelectorAll('input[type="hidden"][name$="-0-0"]');
            hiddenInputs.forEach(input => {
                const clone = input.cloneNode(true);
                if (newRow) {
                    newRow.insertBefore(clone, newRow.firstChild);
                }
            });

            // Class record is used for the form validation selector
            newRow.classList.add('record');
            updateFormElementNames(newRow); // Update the form element names with new indices
            multiFormRowCounter++;

            // Append the new row to the table
            const table = document.querySelector('.table-multi-form tbody');
            if (table) {
                table.appendChild(newRow);
            }

            // Initialize form element listeners and CodeMirror for the new row
            initializeFormElementListeners(newRow);
            initializeCodeMirrorForRow(newRow);
            // Initialize TinyMCE for new row using the same config as the rest of QFQ
            QfqNS.Helper.tinyMce();
            attachTinyMCERowListener(newRow);
            // Update the validator so required fields get handled correctly
            validator.update();
        }

        function attachTinyMCERowListener(row) {
            const textareas = row.querySelectorAll('textarea.qfq-tinymce');
            textareas.forEach((textarea) => {
                const editor = tinymce.get(textarea.id);
                if (editor) {
                    editor.on('input', () => setProcessRowCheckbox(row));
                } else {
                    // Editor not yet initialized (existing rows on page load):
                    // wait for TinyMCE to add it, then attach the listener.
                    const handler = function (e) {
                        if (e.editor.id === textarea.id) {
                            e.editor.on('input', () => setProcessRowCheckbox(row));
                            tinymce.off('AddEditor', handler);
                        }
                    };
                    tinymce.on('AddEditor', handler);
                }
            });
        }

        // Function to update the names of form elements in a row
        function updateFormElementNames(row) {
            const formElements = row.querySelectorAll('input, select, textarea');
            formElements.forEach((formElement) => {
                if (formElement.name) {
                    const parts = formElement.name.split('-');
                    if (parts && parts.length === 3) {
                        const newIndex = multiFormRowCounter; // parseInt(parts[2]) + multiFormRowCounter;
                        formElement.name = parts[0] + '-' + parts[1] + '-' + newIndex;
                    }
                }

                // Update the id if the element has the class qfq-tinymce
                if (formElement.id && formElement.classList.contains('qfq-tinymce')) {
                    const idParts = formElement.id.split('-');
                    if (idParts.length > 1) {
                        const lastPart = idParts[idParts.length - 1];
                        // Ensure the last part is a number
                        const lastIndex = parseInt(lastPart);
                        idParts[idParts.length - 1] = lastIndex + multiFormRowCounter + 1;
                        // Add '-tinymce' to the id
                        formElement.id = idParts.join('-')
                    }
                }
            });
        }

        // Function to initialize CodeMirror for a given row
        function initializeCodeMirrorForRow(row) {
            const textareas = row.querySelectorAll('textarea.qfq-codemirror');
            textareas.forEach((textarea) => {
                if (typeof CodeMirror !== 'undefined') {
                    const configData = textarea.dataset.config ? JSON.parse(textarea.dataset.config) : {};
                    const cm = CodeMirror.fromTextArea(textarea, configData);
                    let height = textarea.dataset.height;
                    let width = textarea.dataset.width;

                    // Ensure height and width are valid
                    height = height && parseInt(height) > 0 ? height : 'auto';
                    width = width && parseInt(width) > 0 ? width : (width === 0 ? 'auto' : null);

                    cm.setSize(width, height);

                    // Add change event listener to update the hidden textarea and check the checkbox
                    cm.on('change', function () {
                        textarea.value = cm.getValue();
                        setProcessRowCheckbox(row);
                    });
                }
            });
        }

        // Function to clear existing CodeMirror instances in a row
        function clearCodeMirrorInstances(row) {
            const codeMirrorDivs = row.querySelectorAll('.CodeMirror');
            codeMirrorDivs.forEach(div => {
                const cmInstance = div.CodeMirror;
                if (cmInstance) {
                    cmInstance.toTextArea();
                    cmInstance.setValue(''); // Clear the CodeMirror content
                }
                div.remove();
            });
        }

        // Function to delete a row
        function deleteRow(event) {
            if (event.target.classList.contains('deleteRowBtn') || event.target.closest('.deleteRowBtn')) {
                const rowDelete = event.target.closest('tr');
                const validator = $('form').data('bs.validator');
                if (rowDelete) {
                    // Update the validator so required fields get handled correctly
                    validator.update();
                    rowDelete.remove();
                }
            }
        }

        // Function to initialize existing rows on page load
        function initializeExistingRows() {
            const existingRows = document.querySelectorAll('.table-multi-form tbody tr');
            existingRows.forEach(row => {
                updateFormElementNames(row);
                initializeFormElementListeners(row);
                initializeCodeMirrorForRow(row); // Initialize CodeMirror for existing rows
                attachTinyMCERowListener(row);   // Bridge TinyMCE edits to the row's process-row checkbox
                multiFormRowCounter++;
            });
        }

        function setProcessRowCheckbox (row) {
            //const checkbox = row.querySelector('input[type="checkbox"]');
            const checkbox = row.querySelector('input[name^="_processRow-"]');
            if (checkbox ) {
                checkbox.checked = true;
            }
        }

        // Function to initialize form element listeners in a row
        function initializeFormElementListeners(row) {
            const formElements = row.querySelectorAll('input:not([name^="_processRow-"]), select, textarea');
            formElements.forEach((formElement) => {
                formElement.addEventListener('input', () => setProcessRowCheckbox(row));
            });
        }

        // Function to clear existing TinyMCE instances in a row
        function clearTinyMCEInstances(row) {
            const textareas = row.querySelectorAll('textarea.qfq-tinymce');
            textareas.forEach((textarea) => {
                const editor = tinymce.get(textarea.id);
                if (editor) {
                    editor.remove();
                }
            });
        }

        // Add event listeners for delete row and checkbox change events on the table body
        const tableBody = document.querySelector('.table-multi-form tbody');
        if (tableBody) {
            tableBody.addEventListener('click', deleteRow);
        }

        // Add event listener to the add row button
        let addRowBtn = document.querySelector('button.addRowButton[id^="addRowBtn-qfq-form"]');
        if (addRowBtn) {
            addRowBtn.addEventListener('click', addRow);
        }

        // Initialize existing rows on page load only if the table has the class 'table-multi-form'
        if (document.querySelector('.table-multi-form')) {
            initializeExistingRows();
        }
    });
})(QfqNS.Helper);

/**
 * @author Roman Fehr <roman.fehr@math.uzh.ch>
 */



/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */



var QfqNS = QfqNS || {};
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';
    n.showError = function (message, title, options) {
        if (!message || !title) {
            return;
        }
        options = options || {};
        const config = {
            message: message,
            title: title,
            type: options.type || 'error',
            modal: options.modal !== undefined ? options.modal : true,
            buttons: options.buttons || [
                {label: 'OK', eventName: 'ok', focus: true }
            ],
            timeout: options.timeout || 0
        };
        new QfqNS.Alert(config).show();
    };
})(QfqNS.Helper);
/**
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 */

/* global console */
/* global qfqChat */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    let qfqChat = function (chatWindowsElements) {
        let chatInstances = [];
        if (chatWindowsElements !== undefined) {
            for (let i = 0; i < chatWindowsElements.length; i++) {
                if (chatWindowsElements[i].dataset.chatInitialized) continue;
                chatWindowsElements[i].dataset.chatInitialized = 'true';

                let chatInstance = new ChatWindow(chatWindowsElements[i]);
                chatInstances.push(chatInstance);

                if (!chatInstance.chatLoaded) {
                    chatInstance.getMessagesFromServer(chatInstance, 'first');
                }
            }
        }
        return chatInstances;
    }

    class TooltipManager {
        constructor(toolbarObject) {
            this.chatType = toolbarObject.chatType;
            if (this.chatType === 'thread') {
                this.chat = {};
                this.chat.chatMessages = toolbarObject.chatObject.element;
                this.chat.topBtnClicked = toolbarObject.chatObject.topBtnClicked;
                this.chat.isLoadingMessages = null;
                this.chat.openThread = false;
                this.chat.topBtnClicked = false;
                this.chat.connection = toolbarObject.chatObject.connection;
                this.chat.loadToolbarApi = toolbarObject.chatObject.loadToolbarApi;
                this.chat.typeAheadSip = toolbarObject.chatObject.typeAheadSip;
                this.chat.typeAheadUrl = toolbarObject.chatObject.typeAheadUrl;
                this.chat.messageId = toolbarObject.chatObject.messageId;
                this.chat.videoCallStartApi = toolbarObject.chatObject.videoCallStartApi;
                this.chat.jitsiBaseUrl = toolbarObject.chatObject.jitsiBaseUrl;
                this.chat.chatToolbar = null;
                this.chat.firstThreadContainer = null;
            } else {
                this.chat = toolbarObject.chatObject;
            }

            this.toolbarObject = toolbarObject;
            this.messageId = toolbarObject.messageId;
            this.toolbar = toolbarObject.toolbar;
            this.tagAddApiUrl = toolbarObject.config.tagAddApiUrl;
            this.tagDelApiUrl = toolbarObject.config.tagDelApiUrl;
            this.doneApiUrl = toolbarObject.config.doneApiUrl;
            this.tooltips = [];
            this.topBtn = null;
            this.initTooltips();
        }

        initTooltips() {
            const buttons = this.toolbar.querySelectorAll('.chat-toolbar-users-tag-more, .chat-toolbar-my-tag, .chat-toolbar-tag-btn, .chat-toolbar-users-done-more, .chat-toolbar-done-btn');
            buttons.forEach(button => {
                const tooltip = button.nextElementSibling;
                if (tooltip !== null && tooltip.classList.contains('chat-toolbar-popup')) {
                    const popperInstance = this.createPopperInstance(button, tooltip); // Create a Popper instance for each tooltip
                    button.dataset.isTooltipShown = 'false';
                    button.addEventListener('click', (event) => {
                        this.toggleTooltip(tooltip, popperInstance, button);
                        event.stopPropagation();
                    });
                    this.tooltips.push({button, tooltip, popperInstance});
                }
            });

            const topBtn = this.toolbar.querySelector('.chat-toolbar-top-btn');
            if (topBtn) {
                topBtn.addEventListener('click', (event) => {
                    this.hideAllTooltipsExcept(null);

                    this.chat.topBtnClicked = true;

                    if (this.chatType !== 'thread') {
                        this.chat.loadNextMessages(this.chat);
                    }

                    this.chat.chatMessages.scrollTo({top: 0, behavior: 'smooth'});
                    this.checkOverflow();

                });
                this.topBtn = topBtn;
            }

            const addTagBtn = this.toolbar.querySelector('.chat-toolbar-tag-btn');
            if (addTagBtn) {
                addTagBtn.addEventListener('click', (event) => {
                    let typeaheadInput = addTagBtn.nextSibling.querySelector('.tt-input');
                    typeaheadInput.focus();

                    typeaheadInput.addEventListener('keydown', function (event) {
                        if (event.key === 'Enter' || event.keyCode === 13) {
                            let typeaheadInputBtn = typeaheadInput.parentElement.nextSibling;
                            typeaheadInputBtn.click();
                        }
                    });
                });
            }

            const tagDeleteBtn = this.toolbar.querySelectorAll('.chat-toolbar-tag-popup-delete');
            tagDeleteBtn.forEach(button => {
                if (button) {
                    button.addEventListener('click', (event) => {
                        this.hideAllTooltipsExcept(null);

                        let tagId = button.getAttribute('data-tag-id');
                        let deleteUrl = this.tagDelApiUrl + '&tagValue=' + encodeURIComponent(tagId) + '&cId=' + encodeURIComponent(this.messageId);
                        this.sendRequest(deleteUrl, function (error, response) {
                            if (error) {
                                console.error('Error deleting tag:', error);
                                QfqNS.Helper.showError('Failed to delete tag. Please try again.', 'Chat');
                            } else {
                                let config = response['chat-update'].toolbarConfig;
                                let chatRoom = response['chat-update'].chatRoom;
                                this.toolbarObject.initToolbar(config);
                                this.initTooltips();
                                this.checkOverflow();
                                // Live notification published server-side by Chat::setTag — no client publish needed.
                            }
                        }.bind(this));
                    });
                }
            });

            const typeaheadInputBtn = this.toolbar.querySelector('.chat-toolbar-typeahead-btn');
            if (typeaheadInputBtn) {
                typeaheadInputBtn.addEventListener('mousedown', function (event) {
                    event.preventDefault();
                });

                typeaheadInputBtn.addEventListener('click', (event) => {
                    let typeaheadInput = event.target.closest('.chat-toolbar-typeahead').querySelector('.tt-input');
                    let inputId = event.target.closest('.chat-toolbar-typeahead').querySelector('.chat-toolbar-typeahead > input');

                    let tagValue = typeaheadInput.value;

                    // Get id from typeahead if it was selected from user
                    if (inputId !== null) {
                        let typeaheadValue = inputId.value;

                        if (!isNaN(typeaheadValue) && typeaheadValue.trim() !== '') {
                            // typeaheadValue is numeric
                            tagValue = typeaheadValue;
                        }
                    }

                    if (tagValue) {
                        let addTagUrl = this.tagAddApiUrl + '&tagValue=' + encodeURIComponent(tagValue) + '&cId=' + encodeURIComponent(this.messageId);
                        this.sendRequest(addTagUrl, function (error, response) {
                            if (error) {
                                console.error('Error adding tag:', error);
                                QfqNS.Helper.showError('Failed to add tag. Please try again.', 'Chat');
                            } else {
                                if (response['chat-update'] === undefined || response['chat-update'].length === 0) {
                                    return;
                                }

                                let config = response['chat-update'].toolbarConfig;
                                let chatRoom = response['chat-update'].chatRoom;
                                this.toolbarObject.initToolbar(config);
                                this.initTooltips();
                                this.checkOverflow();
                                typeaheadInput.value = '';
                                // Live notification published server-side by Chat::setTag — no client publish needed.
                            }
                        }.bind(this));
                    }
                    this.hideAllTooltipsExcept(null);
                });
            }

            const doneBtn = this.toolbar.querySelector('.chat-toolbar-done-btn');
            if (doneBtn) {
                doneBtn.addEventListener('click', (event) => {
                    this.hideAllTooltipsExcept(null);

                    let tagId = doneBtn.getAttribute('data-tag-id');
                    let doneApiUrl = this.doneApiUrl + '&tagValue=' + encodeURIComponent(tagId) + '&cId=' + encodeURIComponent(this.messageId);
                    this.sendRequest(doneApiUrl, function (error, response) {
                        if (error) {
                            console.error('Error switching done:', error);
                            QfqNS.Helper.showError('Failed to update status. Please try again.', 'Chat');
                        } else {
                            let result = response['chat-update'].result;
                            let chatRoom = response['chat-update'].chatRoom;

                            if (result === 'noCiD or missing tagValue') {
                                console.log('cId is missing');
                                return;
                            }

                            if (result !== 'deleted') {
                                this.toggleDoneBtn(doneBtn, result);
                            } else {
                                this.toggleDoneBtn(doneBtn, null, true);
                            }
                            // Live notification published server-side by Chat::setDone — no client publish needed.
                        }
                    }.bind(this));
                });

                this.doneBtn = doneBtn;
            }

            const callBtn = this.toolbar.querySelector('.chat-toolbar-call-btn');
            if (callBtn) {
                callBtn.addEventListener('click', (event) => {
                    this.hideAllTooltipsExcept(null);
                    if (callBtn.disabled) return;

                    const apiUrl = this.chat.videoCallStartApi;
                    const baseUrl = this.chat.jitsiBaseUrl;
                    if (!apiUrl || !baseUrl) {
                        console.error('Video-Call: missing apiUrl or jitsiBaseUrl');
                        QfqNS.Helper.showError('Invalid Video-Call URL.', 'Video-Call');
                        return;
                    }
                    callBtn.disabled = true;
                    this.sendRequest(apiUrl, function (error, response) {
                        callBtn.disabled = false;
                        if (error) {
                            console.error('Video-Call start failed: ', error);
                            QfqNS.Helper.showError('Failed to start Video-Call. Please try again.', 'Video-Call');
                            return;
                        }
                        if (response && response.status === 'error') {
                            console.error('Video-Call start failed: ' + response.message);
                            QfqNS.Helper.showError(response.message || 'Failed to start Video-Call. Please try again.', 'Video-Call');
                            return;
                        }
                        const payload = response && response['chat-update'];
                        const roomName = payload && payload.roomName;
                        if (!roomName) {
                            console.error('Video-Call: missing roomName in response ', response);
                            QfqNS.Helper.showError('Invalid Video-Call response. Please try again.', 'Video-Call');
                            return;
                        }
                        const fullUrl = baseUrl.replace(/\/$/, '') + '/' + roomName;
                        const popup = window.open(fullUrl, '_blank', 'popup=true,width=1280,height=720');
                        if (!popup) {
                            console.warn('Video-Call: popup blocked. URL: ' + fullUrl);
                            QfqNS.Helper.showError('Pop-up was blocked. Please allow pop-ups for this site.', 'Video-Call');
                        }
                    }.bind(this));
                });
            }

            this.chat.chatMessages.addEventListener('scroll', () => {
                if (this.chat.openThread) {
                    return;
                }
                if (this.chat.topBtnClicked) {
                    if (this.chat.chatMessages.scrollTop === 0) {
                        this.chat.topBtnClicked = false;
                    }
                    this.checkOverflow();
                    return;
                }
                this.checkScrollPosition();
                this.checkOverflow();
            });

            // Initialize typeahead for tag input
            QfqNS.TypeAhead.install(this.chat.typeAheadUrl);
        }

        toggleDoneBtn(doneBtn, result, active = false) {
            if (!active) {
                doneBtn.setAttribute('data-tag-id', result);
                doneBtn.title = 'Marked as done';
                doneBtn.firstChild.style.setProperty('color', 'black');
                doneBtn.classList.add('chat-toolbar-done-btn-success');
                doneBtn.blur();
            } else {
                doneBtn.setAttribute('data-tag-id', 'false');
                doneBtn.title = 'Undone';
                doneBtn.firstChild.style.setProperty('color', 'black');
                doneBtn.classList.remove('chat-toolbar-done-btn-success');
            }
        }

        // Load next 10 previous messages if scroll on top is reached
        checkScrollPosition() {
            // Check if the user has scrolled to the top
            if (this.chat.chatMessages.scrollTop === 0) {
                if (this.chat.isLoadingMessages) {
                    return;
                }

                if (this.chat.isLoadingMessages !== null) {
                    this.chat.isLoadingMessages = true;
                }

                let oldScrollHeight = this.chat.chatMessages.scrollHeight;

                if (this.chat.isLoadingMessages !== null) {
                    this.chat.loadNextMessages(this.chat);
                }

                setTimeout(() => {
                    let newScrollHeight = this.chat.chatMessages.scrollHeight;
                    this.chat.chatMessages.scrollTop += (newScrollHeight - oldScrollHeight);
                    if (this.chat.isLoadingMessages !== null) {
                        this.chat.isLoadingMessages = false;
                    }
                }, 100);
            }
        }

        // Hide or show the top button
        checkOverflow() {
            setTimeout(() => {
                let nextSibling = this.topBtn.nextSibling;
                if (this.chat.chatMessages.scrollTop > 5) {
                    if (nextSibling !== null) {
                        nextSibling.classList.remove('chat-toolbar-first-element');
                    }
                    this.topBtn.classList.add('chat-toolbar-first-element');
                    this.topBtn.style.display = 'block';
                } else if (this.chat.flagMoreRecords) {
                    if (nextSibling !== null) {
                        nextSibling.classList.remove('chat-toolbar-first-element');
                    }
                    this.topBtn.classList.add('chat-toolbar-first-element');
                    this.topBtn.style.display = 'block';
                } else {
                    if (nextSibling !== null) {
                        nextSibling.classList.add('chat-toolbar-first-element');
                        this.topBtn.classList.remove('chat-toolbar-first-element');
                    }
                    this.topBtn.style.display = 'none';
                }
            }, 200);
        }

        toggleToolbarDisplay() {
            if (this.toolbar.style.visibility === 'visible' || this.toolbar.style.visibility === '') {
                this.toolbar.style.visibility = 'hidden';
            } else {
                this.toolbar.style.visibility = 'visible';
            }
        }

        sendRequest(url, callback) {
            let xhr = new XMLHttpRequest();
            xhr.open("POST", url, true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

            xhr.onload = function () {
                if (xhr.status === 200) {
                    try {
                        let response = JSON.parse(xhr.responseText);
                        callback(null, response);
                    } catch (e) {
                        console.error('Toolbar request: invalid response from server.', e);
                        callback(new Error('Invalid server response'));
                    }
                } else {
                    console.error('Toolbar request HTTP error: ' + xhr.status);
                    callback(new Error('Error in chat saving'));
                }
            };

            xhr.onerror = function () {
                console.log('Request failed.');
                callback(new Error('Request failed'));
            };

            xhr.send();
        }

        toggleTooltip(tooltip, popperInstance, button) {
            let isShown = button.dataset.isTooltipShown === 'true';
            if (isShown) {
                this.hide(tooltip, popperInstance);
            } else {
                this.hideAllTooltipsExcept(button);
                this.show(tooltip, popperInstance);
            }
            button.dataset.isTooltipShown = !isShown;
        }

        hideAllTooltipsExcept(currentButton) {
            this.tooltips.forEach(({button, tooltip, popperInstance}) => {
                if (button !== currentButton) {
                    this.hide(tooltip, popperInstance);
                    button.dataset.isTooltipShown = 'false';
                }
            });
        }

        /* Start popup js functions*/
        createPopperInstance(button, tooltip) {
            return Popper.createPopper(button, tooltip, {
                placement: 'bottom',
                modifiers: [
                    {
                        name: 'offset',
                        options: {
                            offset: [0, 8],
                        },
                    },
                ],
            });
        }

        show(tooltip, popperInstance) {
            tooltip.setAttribute('data-show', '');
            popperInstance.setOptions((options) =>
                Object.assign({}, options, {
                    modifiers: options.modifiers.concat([{name: 'eventListeners', enabled: true}])
                })
            );
            popperInstance.update();
        }

        hide(tooltip, popperInstance) {
            tooltip.removeAttribute('data-show');

            // Clean all input field inside a tooltip -> tag typeahead
            const inputs = tooltip.querySelectorAll('input');
            inputs.forEach(input => {
                input.value = '';
            });

            popperInstance.setOptions((options) =>
                Object.assign({}, options, {
                    modifiers: options.modifiers.concat([{name: 'eventListeners', enabled: false}])
                })
            );
        }
    }

    class ChatToolbar {
        constructor(chatObject, config, chatType = 'regular') {
            this.chatObject = chatObject;
            if (chatType === 'thread') {
                this.parentElement = chatObject.element;
            } else {
                this.parentElement = chatObject.chatMessages;
            }
            this.chatType = chatType;
            this.config = config;
            this.messageId = config.messageId;

            // In case of first room messsage
            if (this.messageId === 0 && chatType === 'regular') {
                this.messageId = chatObject.messageId;
            }
            this.toolbar = this.createToolbar();
            this.initToolbar(this.config);
            this.toolbarManager = new TooltipManager(this);

            // Close all open popup divs after clicking somewhere in window
            window.addEventListener('click', (event) => {
                if (!this.toolbar.contains(event.target)) {
                    this.toolbarManager.hideAllTooltipsExcept(null);
                }
            });
        }

        createToolbar() {
            let toolbar = this.parentElement.querySelector(':scope > .chat-toolbar');
            if (!toolbar) {
                toolbar = document.createElement('div');
                toolbar.className = 'chat-toolbar btn-group';
                if (this.chatType === 'thread') {
                    toolbar.classList.add('chat-toolbar-thread');
                }
                this.parentElement.insertBefore(toolbar, this.parentElement.firstChild);

                let nextSibling = toolbar.nextSibling;
                let clearFloat = document.createElement('div');
                clearFloat.className = 'clearfix';

                if (nextSibling && nextSibling.classList.contains('chat-thread-open-btn')) {
                    nextSibling.after(clearFloat);
                } else {
                    toolbar.after(clearFloat);
                }
            }
            return toolbar;
        }

        initToolbar(config) {
            this.toolbar.innerHTML = '';

            // Create all toolbar elements
            const elements = this.createToolbarElements(config);

            // Filter out popup divs.
            let btnElements = elements.filter(el => el.classList.contains('btn') || el.classList.contains('chat-toolbar-users-tag'));
            btnElements.forEach((el, index) => {
                // Add classes for first and last elements. Skip the first one > top btn. Is dynamically handled.
                if (index === 1) el.classList.add('chat-toolbar-first-element');
                if (index === btnElements.length - 1) el.classList.add('chat-toolbar-last-element');
            });

            // Append elements to the toolbar in order
            elements.forEach((el, index) => {
                this.addElement(el);
            });
        }


        createToolbarElements(config) {
            const elements = [];

            // If optionTag is set to 'my', filter out tags not created by the user
            if (config.optionTag === 'my') {
                let tagsArray = Array.isArray(config.tags) ? config.tags : [config.tags];

                config.cleanedTags = tagsArray.filter(tag =>
                    (tag.pIdCreator == config.pIdCreator || tag.username === config.username) &&
                    tag.pIdCreator !== '' && tag.username !== ''
                );
            } else {
                config.cleanedTags = config.tags;
            }

            // Add top btn if configured
            if (config.topBtn) {
                elements.push(this.createTopBtn());
            }

            // Add video-call btn
            if (this.chatType !== 'thread') {
                elements.push(this.createCallButton());
            }

            // Show more tags button
            if (config.optionTag !== 'off' && config.cleanedTags.length > 3) {
                // Get all array elements from starting index 4.
                let restTags = config.cleanedTags.slice(3);

                let moreTagsObject = this.createMoreTagsButton(restTags, config.pIdCreator);
                elements.push(moreTagsObject.button);
                elements.push(moreTagsObject.popup);
            }

            // Add up to 3 tags
            if (config.optionTag !== 'off') {
                config.cleanedTags.slice(0, 3).forEach(tag => {
                    let tagObject = this.createTag(tag, config.pIdCreator, config.username);

                    elements.push(tagObject.button);
                    if (tagObject.popup !== null) {
                        elements.push(tagObject.popup);
                    }
                });
            }

            // Add tag btn typeahead
            if (config.optionTag !== 'off') {
                let tagBtnObject = this.createAddTagButton();
                elements.push(tagBtnObject.button);
                elements.push(tagBtnObject.popup);
            }

            // Show more done button
            if (config.optionTagDone === 'all' && config.usersDone.length > 3) {
                // Get all array elements from starting index 4.
                let restDones = config.usersDone.slice(3);

                let moreDonesObject = this.createMoreDonesButton(restDones);
                elements.push(moreDonesObject.button);
                elements.push(moreDonesObject.popup);
            }

            // Add up to 3 users done
            if (config.optionTagDone === 'all') {
                let usersDoneArray = Array.isArray(config.usersDone) ? config.usersDone : [config.usersDone];
                usersDoneArray.slice(0, 3).forEach(userDone => {
                    elements.push(this.createUserDoneDiv(userDone));
                });
            }

            if (config.optionTagDone !== 'off') {
                elements.push(this.createDoneButton(config.activeDone));
            }

            return elements;
        }

        addElement(element) {
            this.toolbar.appendChild(element);
        }

        removeElement(selector) {
            const element = this.toolbar.querySelector(selector);
            if (element) {
                this.toolbar.removeChild(element);
            }
        }

        createTopBtn() {
            const button = document.createElement('button');
            button.className = 'chat-toolbar-top-btn btn btn-default';
            button.style.display = 'none';

            let iconElement = document.createElement('i');
            iconElement.className = 'fas fa-arrow-up';
            iconElement.style.setProperty('color', '#000000');
            button.appendChild(iconElement);

            return button;
        }

        createMoreTagsButton(usersTag, pIdCreator) {
            let moreTagsObject = {};

            const button = document.createElement('button');
            button.className = 'chat-toolbar-users-tag-more btn btn-default';
            button.setAttribute("aria-describedby", "tooltip");

            var iconElement = document.createElement('i');
            iconElement.className = 'fas fa-ellipsis-v';
            iconElement.style.setProperty('color', '#000000');
            button.appendChild(iconElement);

            const tagsContainer = document.createElement('div');
            tagsContainer.innerHTML = 'Tags:<hr>';
            usersTag.forEach(userTag => {
                if (userTag.pIdCreator == pIdCreator) {
                    const tagDeleteBtn = this.createDeleteButton(userTag.id, userTag.username + ': ' + userTag.value);
                    tagDeleteBtn.className = tagDeleteBtn.className + ' btn-block';
                    tagDeleteBtn.setAttribute('data-tag-id', userTag.id);
                    tagsContainer.appendChild(tagDeleteBtn);
                } else {
                    const tagText = document.createElement('div');
                    tagText.textContent = userTag.username + ': ' + userTag.value;
                    tagsContainer.appendChild(tagText);
                }
            });

            let popupDiv = this.createPopupElement(tagsContainer.outerHTML);

            moreTagsObject.button = button;
            moreTagsObject.popup = popupDiv;

            return moreTagsObject;
        }

        createTag(tag, pIdCreator, username) {
            let tagObject = {};
            let popupDivDelete = null;

            const div = document.createElement('div');
            div.className = 'chat-toolbar-users-tag';
            div.textContent = tag.value.length > 5 ? tag.value.substring(0, 5) + '.' : tag.value;

            if (tag.pIdCreator == pIdCreator) {
                div.title = tag.value;
                div.className = div.className + ' chat-toolbar-my-tag btn alert-info';
                div.setAttribute('data-tag-id', tag.id);

                popupDivDelete = this.createPopupElement('', 'tag', tag.id);
            } else {
                div.title = tag.username + ': ' + tag.value;
            }

            tagObject.button = div;
            tagObject.popup = popupDivDelete;

            return tagObject;
        }

        createAddTagButton() {
            let tagBtnObject = {};
            let popupTypeahead = null;

            const button = document.createElement('button');
            button.className = 'chat-toolbar-tag-btn btn btn-default';
            // ... set up event listener for pop up
            var iconElement = document.createElement('i');
            iconElement.className = 'fas fa-tag';
            iconElement.style.setProperty('color', '#000000');
            button.appendChild(iconElement);

            popupTypeahead = this.createPopupElement('', 'typeahead');

            tagBtnObject.button = button;
            tagBtnObject.popup = popupTypeahead;

            return tagBtnObject;
        }

        createMoreDonesButton(usersDone) {
            let moreDonesObject = {};

            const button = document.createElement('button');
            button.className = 'chat-toolbar-users-done-more btn btn-default';
            button.setAttribute("aria-describedby", "tooltip");

            var iconElement = document.createElement('i');
            iconElement.className = 'fas fa-ellipsis-v';
            iconElement.style.setProperty('color', '#000000');
            button.appendChild(iconElement);

            const donesContainer = document.createElement('div');
            donesContainer.innerHTML = 'Done:<hr>';
            usersDone.forEach(userDone => {
                donesContainer.appendChild(document.createTextNode(userDone.username));
                donesContainer.appendChild(document.createElement('br'));
            });

            let popupDiv = this.createPopupElement(donesContainer.outerHTML);

            moreDonesObject.button = button;
            moreDonesObject.popup = popupDiv;

            return moreDonesObject;
        }

        createPopupElement(content, type = 'text', userTagId = 0) {
            let popupDiv = document.createElement('div');
            popupDiv.className = "chat-toolbar-popup";
            popupDiv.setAttribute("role", "tooltip");

            switch (type) {
                case 'tag':
                    // Create delete button for single tag
                    const button = this.createDeleteButton(userTagId, content);
                    popupDiv.appendChild(button);
                    break;
                case 'typeahead':
                    const divContainer = document.createElement('div');
                    divContainer.className = 'chat-toolbar-typeahead';
                    const typeaheadInput = document.createElement('input');
                    typeaheadInput.style.color = 'black';
                    typeaheadInput.className = 'qfq-typeahead';
                    typeaheadInput.setAttribute('data-typeahead-sip', this.chatObject.typeAheadSip);
                    typeaheadInput.setAttribute('data-typeahead-limit', '10');
                    typeaheadInput.setAttribute('data-typeahead-min-length', '1');

                    const confirmBtn = document.createElement('button');
                    confirmBtn.className = 'chat-toolbar-typeahead-btn btn btn-default';

                    var iconElement = document.createElement('i');
                    iconElement.className = 'fas fa-check';
                    confirmBtn.appendChild(iconElement);

                    divContainer.appendChild(typeaheadInput);
                    divContainer.appendChild(confirmBtn);
                    popupDiv.appendChild(divContainer);
                    break;
                default:
                    popupDiv.innerHTML = content;
            }

            let arrowDiv = document.createElement("div");
            arrowDiv.className = "chat-toolbar-popup-arrow";
            arrowDiv.setAttribute("data-popper-arrow", "");
            popupDiv.appendChild(arrowDiv);

            return popupDiv;
        }

        createDeleteButton(userTagId, textContent = '') {
            const button = document.createElement('button');
            button.className = 'chat-toolbar-tag-popup-delete chat-toolbar-popup-delete-block btn btn-default';
            button.setAttribute('data-tag-id', userTagId);

            if (textContent !== '') {
                button.textContent = textContent;
                // how to add one space between the text content and following child iconElement?
                const space = document.createTextNode(' ');
                button.appendChild(space);
            }

            var iconElement = document.createElement('i');
            iconElement.className = 'far fa-trash-alt';
            button.appendChild(iconElement);

            return button;
        }

        createUserDoneDiv(userDone) {
            const div = document.createElement('div');
            div.className = 'chat-toolbar-users-done chat-toolbar-done-btn-success';
            div.title = userDone.username;
            div.textContent = userDone.username.substring(0, 3) + '.';
            return div;
        }

        createDoneButton(activeDone) {
            const button = document.createElement('button');
            button.className = 'chat-toolbar-done-btn btn btn-default';
            button.setAttribute('data-tag-id', activeDone);

            var iconElement = document.createElement('i');
            iconElement.className = 'fas fa-check';

            if (activeDone) {
                button.title = 'Marked as done';
                iconElement.style.setProperty('color', 'black');
                button.classList.add('chat-toolbar-done-btn-success');
            } else {
                button.title = 'Undone';
                iconElement.style.setProperty('color', 'black');
            }

            button.appendChild(iconElement);
            return button;
        }

        createCallButton() {
            const button = document.createElement('button');
            button.className = 'chat-toolbar-call-btn btn btn-default';
            button.title = 'start video call';
            const iconElement = document.createElement('i');
            iconElement.className = 'fas fa-video';
            iconElement.style.setProperty('color', '#000000');
            button.appendChild(iconElement);
            return button;
        }

        getConfig(messageId, chatRoom = false) {
            let apiRefreshUrl = this.chatObject.loadToolbarApi + '&messageId=' + encodeURIComponent(messageId) + '&chat_room=' + encodeURIComponent(chatRoom);
            this.toolbarManager.sendRequest(apiRefreshUrl, function (error, response) {
                if (error) {
                    console.error('Error fetching toolbar config:', error);
                    QfqNS.Helper.showError('Failed to load toolbar. Please try again.', 'Chat');
                } else {
                    let config = response['chat-update'];
                    this.initToolbar(config);
                    this.toolbarManager.initTooltips();
                }
            }.bind(this));
        }
    }

    /**
     *
     */
    class ChatWindow {
        constructor(chatWindowElement) {
            this.chatWindow = chatWindowElement;
            this.elementName = this.chatWindow.parentNode.name;
            this.chatSearch = this.chatWindow.querySelector(".chat-search");
            this.searchInput = this.chatWindow.querySelector(".chat-search-input");
            this.searchBtn = this.chatWindow.querySelector(".chat-search-btn");
            this.chatMessages = this.chatWindow.querySelector(".chat-messages");
            this.topBtn = this.chatWindow.querySelector(".chat-top-symbol");
            this.activateSearchBtn = this.chatWindow.querySelector(".chat-search-activate");
            this.chatSpinner = this.chatMessages.querySelector(".chat-loader-container");
            this.chatInputContainer = this.chatWindow.parentNode.querySelector(".chat-input-container");
            this.chatInput = this.chatInputContainer.querySelector(".chat-input-field")
            this.chatSendBtn = this.chatInputContainer.querySelector(".chat-submit-button")
            this.chatAttachBtn = this.chatInputContainer.querySelector(".chat-attach-button")
            this.chatFileInput = this.chatInputContainer.querySelector(".chat-file-input")
            this.chatStagingTiles = this.chatWindow.parentNode.querySelector(".chat-staging-tiles");
            this.chatDropZone = this.chatWindow.parentNode.querySelector(".chat-drop-zone");
            this.chatConfig = JSON.parse(this.chatWindow.getAttribute("data-chat-config"));
            this.currentUsername = this.chatConfig.username;

            // All relevant sip urls
            this.websocketUrl = this.chatWindow.getAttribute("data-websocket-url");
            this.messengerToken = this.chatWindow.getAttribute("data-messenger-token") || '';
            this.messengerChannel = this.chatWindow.getAttribute("data-messenger-channel") || '';
            this.messengerGroup = this.chatWindow.getAttribute("data-messenger-group") || 'qfq';
            this.messenger = null;
            this.loadApi = this.chatWindow.getAttribute("data-load-api");
            this.saveApi = this.chatWindow.getAttribute("data-save-api");
            this.attachmentApi = this.chatWindow.getAttribute("data-attachment-api");
            this.reactionAddApi = this.chatWindow.getAttribute("data-reaction-add-api");
            this.reactionDeleteApi = this.chatWindow.getAttribute("data-reaction-delete-api");
            this.emojiDataSource = this.chatWindow.getAttribute("data-emoji-data-source");
            this.videoCallStartApi = this.chatWindow.getAttribute("data-video-call-start-api");
            this.jitsiBaseUrl = this.chatWindow.getAttribute("data-jitsi-base-url") || '';
            this.loadToolbarApi = this.chatWindow.getAttribute("data-toolbar-load-api");
            this.typeAheadUrl = this.chatWindow.getAttribute("data-typeahead-url");
            this.typeAheadSip = this.chatWindow.getAttribute("data-typeahead-sip");

            this.chatContainer = {};
            this.threadContainer = {};
            this.markedThreads = [];
            this.currentSearchIndex = 0;
            this.searchResults = [];
            this.lastSearchTerm = '';
            this.chatRefresh = false;
            this.chatLoaded = false;
            this.flagMoreRecords = false;
            this.topBtnClicked = false;
            this.isLoadingMessages = false;
            this.openThread = false;
            this.lastThreadBtn = null;
            this.currentThread = null;
            this.firstThreadContainer = null;
            this.messageId = null;
            this.resetDoneOnNewMessage = false;
            this.stagingFiles = [];
            this.isSending = false;

            this.init();
        }

        init() {
            this.searchBtn.addEventListener("click", (event) => {
                event.preventDefault();
                this.handleSearch();
            });

            this.searchInput.addEventListener("keyup", (event) => {
                if (event.keyCode !== 13 && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
                    event.preventDefault();
                    event.stopPropagation();
                    this.handleSearch();
                }
            });

            this.activateSearchBtn.addEventListener('click', () => {

                if (this.chatSearch.style.display === 'block') {
                    // If visible, start the hiding process
                    this.searchInput.value = '';
                    this.handleSearch();
                    this.chatSearch.style.opacity = '0';
                    this.chatSearch.style.display = 'none';
                    this.handleThreadBtnMarker('remove');
                } else {
                    // If hidden, show the div
                    this.chatSearch.style.display = 'block';
                    this.chatSearch.style.opacity = '1';
                }
            });

            document.addEventListener("visibilitychange", function () {
                if (this.chatRefresh) {
                    this.scrollToBottom();
                    this.chatRefresh = false;
                }
            }.bind(this));

            window.addEventListener("beforeunload", () => {
                if (this.messenger) {
                    this.messenger.close();
                }
            });

            this.chatSendBtn.addEventListener('click', () => {
                let that = this;
                if (that.stagingFiles.length > 0) {
                    //files staged attachment loop
                    qfqChat.submitStaging(that);
                } else {
                    qfqChat.submit(that);
                }
            });

            //Chat textarea: own autosize (grow + shrink).
            if (this.chatInput) {
                let that = this;
                this.chatInput.classList.remove('qfq-auto-grow');
                this.chatInput.addEventListener('input', () => {
                    qfqChat.autoResizeInput(that.chatInput);
                });
                this.chatInput.addEventListener('keydown', (event) => {
                    // Enter sends, Shift+Enter inserts a newline.
                    if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
                        event.preventDefault();
                        that.chatSendBtn.click();
                    }
                });
            }

            // Attachment: click on paperclip opens hidden file picker; change triggers upload.
            if (this.chatAttachBtn && this.chatFileInput) {
                let that = this;
                this.chatAttachBtn.addEventListener('click', () => {
                    this.chatFileInput.click();
                });
                //Picker
                this.chatFileInput.addEventListener('change', () => {
                    qfqChat.addFilesToStaging(that, that.chatFileInput.files);
                    that.chatFileInput.value = '';
                });

                //Drag & Drop
                this.chatDropZone.addEventListener('dragover', (event) => {
                    if (Array.from(event.dataTransfer.types).indexOf('Files') === -1) {
                        return;
                    }
                    event.preventDefault();
                    that.chatDropZone.classList.add('chat-drag-over');
                });
                this.chatDropZone.addEventListener('dragleave', (event) => {
                    if (!that.chatDropZone.contains(event.relatedTarget)) {
                        that.chatDropZone.classList.remove('chat-drag-over');
                    }
                });
                this.chatDropZone.addEventListener('drop', (event) => {
                    event.preventDefault();
                    that.chatDropZone.classList.remove('chat-drag-over');
                    qfqChat.addFilesToStaging(that, event.dataTransfer.files);
                });

                //Clipboard paste
                this.chatInput.addEventListener('paste', (event) => {
                    let items = (event.clipboardData && event.clipboardData.items) || [];
                    let images = [];
                    for (let i = 0; i < items.length; i++) {
                        if (items[i].type.startsWith('image/')) {
                            let blob = items[i].getAsFile();
                            if (blob) {
                                let ext = (blob.type.split('/')[1] || 'png');
                                images.push(new File([blob], 'screenshot-' + Date.now() + '.' + ext, {type: blob.type}));
                            }
                        }
                    }
                    if (images.length > 0) {
                        event.preventDefault();
                        qfqChat.addFilesToStaging(that, images);
                    }
                });
            }

            this.chatMessages.addEventListener('click', function (event) {
                let threadButton = event.target.closest('.chat-thread-btn');
                let plusButton = event.target.closest('.chat-thread-open-btn');
                let button = threadButton;
                let reactionBtn = event.target.closest('.chat-reaction-btn');

                //Click reaction button, takes message id
                if (reactionBtn) {
                    event.stopPropagation();
                    let chatContainer = reactionBtn.closest('.chat-container');
                    let messageId = chatContainer.getAttribute('data-message-id');
                    this.openReactionPicker(reactionBtn, messageId);
                    return;
                }

                //Click on existing reaction badge, toggle own vote
                let reactionBadge = event.target.closest('.chat-reaction-badge');
                if (reactionBadge) {
                    event.stopPropagation();
                    let chatContainer = reactionBadge.closest('.chat-container');
                    let messageId = chatContainer.getAttribute('data-message-id');
                    let reactionCode = reactionBadge.dataset.reactionCode;
                    if (reactionBadge.classList.contains('chat-reaction-badge-own')) {
                        this.removeReaction(messageId, reactionCode);
                    } else {
                        this.sendReaction(messageId, reactionCode);
                    }
                    return;
                }

                if (button === null) {
                    button = plusButton;
                }

                if (button) {
                    event.stopPropagation();

                    this.lastThreadBtn = button;

                    // Get the parent chat-container of the clicked button
                    let chatContainer = button.closest('.chat-container');
                    let threadId = '';

                    if (chatContainer === null) {
                        chatContainer = button.closest('.chat-thread-container');
                        threadId = chatContainer.getAttribute('data-thread-id');
                    } else {
                        threadId = chatContainer.getAttribute('data-message-id');
                    }

                    if (plusButton === null) {
                        button.classList.toggle('chat-thread-btn-clicked');
                    }

                    if (!this.openThread) {
                        this.openThread = true;
                        this.chatInput.setAttribute('data-thread-id', threadId);
                        this.scrollPosition = this.chatMessages.scrollTop;
                        this.currentThread = chatContainer;
                        if (plusButton !== null) {
                            this.chatMessages.classList.toggle('chat-messages-no-overflow');
                            chatContainer.classList.toggle('chat-thread-open');
                            plusButton.firstElementChild.className = 'fas fa-minus';
                        }

                        this.toolbar.toolbarManager.toggleToolbarDisplay();
                    } else {
                        this.chatInput.removeAttribute('data-thread-id');
                        this.openThread = false;
                        if (plusButton !== null) {
                            this.chatMessages.classList.toggle('chat-messages-no-overflow');
                            chatContainer.classList.toggle('chat-thread-open');
                            chatContainer.scrollTop = 0;
                            plusButton.firstElementChild.className = 'fas fa-plus';
                            plusButton.blur()
                        }
                        this.currentThread = null;
                        this.toolbar.toolbarManager.toggleToolbarDisplay();
                    }

                    // Toggle mute on all chat containers except the parent
                    this.chatMessages.querySelectorAll('.chat-container, .chat-thread-container').forEach(function (container) {
                        if (container !== chatContainer) {
                            if (container.classList.contains('chat-thread-container')) {
                                // Hide 'chat-thread-container' elements
                                container.classList.toggle('chat-container-muted');
                            }
                            // Check if the element is a 'chat-container' and its parent is NOT 'chat-thread-container'
                            else if (container.classList.contains('chat-container') && !container.parentElement.classList.contains('chat-thread-container')) {
                                // Hide 'chat-container' elements that are not inside a 'chat-thread-container'
                                container.classList.toggle('chat-container-muted');
                            }
                        }
                    });

                    if (!this.openThread) {
                        this.chatMessages.scrollTop = this.scrollPosition;
                    }
                }
            }.bind(this));

            this.scrollToBottom();

            //Close reaction picker on any pointerdown outside of it
            document.addEventListener('pointerdown', (event) => {
                let openPicker = document.querySelector('.chat-reaction-picker');
                if (!openPicker) return;
                if (openPicker.contains(event.target)) return;
                if (event.target.closest('.chat-reaction-btn')) return;
                openPicker.remove();
            });

            // Build up nchan-messages connection. The upstream Messenger
            // handles heartbeat + auto-reconnect; we only subscribe to the
            // chat channel and dispatch incoming 'ping' / 'toolbar-ping'
            // notifications into the existing refresh handlers.
            const Nchan = window.NchanMessenger || null;
            if (Nchan && this.websocketUrl && this.messengerChannel && this.messengerToken) {
                let urlBase;
                try {
                    // UrlFactory internally converts https→wss / http→ws.
                    // Pass https/http, not wss/ws, to avoid double-conversion
                    // (wss→ws would cause mixed-content DOMException).
                    let httpUrl = this.websocketUrl
                        .replace(/^wss:/, 'https:')
                        .replace(/^ws:/, 'http:');
                    if (!httpUrl.endsWith('/')) httpUrl += '/';
                    urlBase = new URL(httpUrl);
                } catch (e) {
                    console.error('qfqChat: invalid data-websocket-url', this.websocketUrl, e);
                    return;
                }

                const channels = [this.messengerChannel];
                const urlFactory = new Nchan.UrlFactory(urlBase, this.messengerGroup, {
                    pub: [],        // Chat is listener-only; publishing is done server-side
                    sub: channels
                });
                this.messenger = new Nchan.Messenger(urlFactory, {logger: console});
                this.messenger._channels.sub = [...channels];
                this.messenger._channels.pub = [...channels];
                this.messenger.tokens.add(this.messengerToken);

                this.messenger.subscribe({
                    handleMessage: (msg) => {
                        // Ignore messages that arrive before the initial chat
                        // load is complete — nchan re-delivers the last buffered
                        // message on subscribe, which would trigger a premature
                        // ping-refresh on chatContainers that aren't set up yet.
                        if (!this.chatLoaded) return;

                        const name = msg && msg.name;
                        const payload = (msg && msg.payload) || {};
                        if (name === 'ping') {
                            this.getMessagesFromServer(this, 'ping');

                            if (this.resetDoneOnNewMessage) {
                                this.refreshToolbar(payload.cId, payload.chatRoom);
                            }

                            // Set flag if users tab is not active.
                            if (document.visibilityState !== 'visible') {
                                this.chatRefresh = true;
                            }
                        } else if (name === 'toolbar-ping') {
                            this.refreshToolbar(payload.cId, payload.chatRoom);
                        } else if (name === 'reaction-ping') {
                            this.getMessagesFromServer(this, 'ping');
                        }
                    }
                }, channels);

                this.messenger.open().then(
                    () => console.log('qfqChat: connected to channel ' + this.messengerChannel),
                    (err) => {
                        // AbortError is expected on page reload — the old page's
                        // beforeunload handler closes the messenger which rejects
                        // the pending open() promise. Auto-reconnect handles it.
                        if (err && err.name === 'AbortError') return;
                        console.error('qfqChat: connection failed:', err);
                    }
                );
            } else if (!this.websocketUrl) {
                console.log('No messenger base URL found. Set websocketUrl in QFQ config if needed.');
            } else if (!this.messengerToken) {
                console.warn('qfqChat: no data-messenger-token on chat window — live updates disabled');
            }
        }

        refreshToolbar(cId, chatRoom = false) {
            if (this.toolbar.messageId == cId && chatRoom) {
                this.toolbar.getConfig(cId, chatRoom);
                this.toolbar.toolbarManager.checkOverflow();
            } else if (this.threadContainer[cId] !== undefined) {
                this.threadContainer[cId].toolbar.getConfig(cId, chatRoom);
                this.threadContainer[cId].toolbar.toolbarManager.checkOverflow();
            }
        }

        // Scroll to the bottom of chat window.
        scrollToBottom() {
            this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
        }

        // Handle search with given criteria.
        handleSearch() {
            let filter = this.searchInput.value;

            if (filter !== this.lastSearchTerm) {
                this.performSearch(filter);
                this.lastSearchTerm = filter;
            }

            // Reset if empty search
            if (filter === '') {
                this.resetFilter(filter);
                this.handleThreadBtnMarker('remove');
            }

            this.scrollToCurrentResult();
        }

        // Execute the search logic.
        performSearch(filter) {
            // Update this.searchResults based on the search
            // Reset this.currentSearchIndex to 0
            var messages = this.chatMessages.querySelectorAll(".chat-message-content");

            function escapeRegExp(string) {
                return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            }

            // Store original HTML content if not done already
            messages.forEach(content => {
                if (!content.getAttribute('data-original-html')) {
                    content.setAttribute('data-original-html', content.innerHTML);
                }
            });

            if (!filter) {
                messages.forEach(content => {
                    content.innerHTML = content.getAttribute('data-original-html');
                });
                return;
            }

            this.handleThreadBtnMarker('remove');

            var searchRegEx = new RegExp(escapeRegExp(filter), "gi");

            // Reset only if the search term has changed
            if (filter !== this.lastSearchTerm) {
                this.resetFilter(filter);
            }

            messages.forEach(function (content) {
                content.innerHTML = content.getAttribute('data-original-html');

                let messageText = content.querySelector(".chat-message-text");
                if (!messageText) {
                    // If .message-text does not exist, create it and move the content into it
                    messageText = document.createElement("div");
                    messageText.className = "chat-message-text";
                    while (content.firstChild && content.firstChild !== content.querySelector('.chat-time')) {
                        messageText.appendChild(content.firstChild);
                    }
                    content.prepend(messageText);
                }

                let originalHTML = messageText.innerHTML;
                let matchFound = false;
                messageText.innerHTML = originalHTML.replace(searchRegEx, function (match) {
                    matchFound = true;
                    this.searchResults.push({element: content, match: match});
                    return `<span class="chat-highlight">${match}</span>`;
                }.bind(this));

                this.handleThreadBtnMarker('set', matchFound, content);
            }, this);
        }

        // Scroll to currently searched result
        scrollToCurrentResult() {
            if (this.searchResults.length > 0 && this.searchResults[this.currentSearchIndex]) {
                const selectedElement = this.searchResults[this.currentSearchIndex].element;
                const chatMessagesRect = this.chatMessages.getBoundingClientRect();
                const selectedElementRect = selectedElement.getBoundingClientRect();

                // Calculate relative position of the element inside the chatMessages container
                const relativeTop = selectedElementRect.top - chatMessagesRect.top;
                const targetScrollTop = this.chatMessages.scrollTop + relativeTop - (this.chatMessages.clientHeight / 2);

                // Use scrollTo with smooth behavior
                this.chatMessages.scrollTo({top: targetScrollTop, behavior: 'smooth'});

                this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length;
            }

            this.updateSearchInfo();
        }

        // Load the next 10 messages if scrolling is on top and there exists more records.
        loadNextMessages(that) {
            if (this.chatMessages.scrollTop === 0 && this.flagMoreRecords) {
                this.getMessagesFromServer(that, 'refresh');
            }
        }

        updateSearchInfo() {
            const infoElement = this.chatWindow.querySelector(".chat-search-info");
            if (this.searchResults.length > 0) {
                const currentIndex = this.currentSearchIndex === 0 ? this.searchResults.length : this.currentSearchIndex;
                infoElement.textContent = ` ${currentIndex}/${this.searchResults.length}`;
            } else {
                infoElement.textContent = '';
            }
        }

        // Reset the search filter input
        resetFilter(filter) {
            this.currentSearchIndex = 0;
            this.searchResults = [];
            this.lastSearchTerm = filter;
        }

        // Set up the html for the individual message bubble.
        createNewMessagePlain(key, message, actualChatState, dbColumnNames) {
            let chatConfig = {};
            let bubbleClass = 'chat-left-bubble';
            let username = '';
            let threadBtn = null;

            if (message[dbColumnNames.pIdCreator] == actualChatState.pIdCreator) {
                bubbleClass = 'chat-right-bubble alert-info';
            } else {
                username = message[dbColumnNames.username];
                if (this.thread) {
                    threadBtn = document.createElement('button');
                    threadBtn.className = 'chat-thread-btn';
                    let iconElement = document.createElement('i');
                    iconElement.className = 'fa fa-comment';
                    threadBtn.appendChild(iconElement);
                }
            }

            //Reaction trigger: smiley button, shown on hover
            let reactionBtn = document.createElement('button');
            reactionBtn.type = 'button';
            reactionBtn.className = 'chat-reaction-btn';
            let reactionIcon = document.createElement('i');
            reactionIcon.className = 'fa fa-smile';
            reactionBtn.appendChild(reactionIcon);

            let timestamp = new Date(message[dbColumnNames.created]);
            let formattedMessageDate = timestamp.toLocaleDateString('de-DE', {
                day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit'
            });

            let currentTimestamp = new Date();
            let currentDate = currentTimestamp.toISOString().split('T')[0];
            let messageDate = timestamp.toISOString().split('T')[0];
            let formattedDateTime = timestamp.toLocaleDateString('de-DE', {
                day: '2-digit', month: '2-digit', year: 'numeric'
            });
            if (currentDate === messageDate) {
                formattedDateTime = timestamp.toLocaleTimeString('de-DE', {
                    hour: '2-digit', minute: '2-digit'
                });
            }

            chatConfig = {
                bubbleClass: bubbleClass,
                title: formattedMessageDate,
                type: message[dbColumnNames.type],
                message: message[dbColumnNames.message],
                fileName: message[dbColumnNames.fileName],
                pathFileName: message[dbColumnNames.pathFileName],
                downloadUrl: message[dbColumnNames.downloadUrl],
                thumbnailUrl: message[dbColumnNames.thumbnailUrl],
                mimeType: message[dbColumnNames.mimeType],
                fileSize: message[dbColumnNames.fileSize],
                chatTime: formattedDateTime,
                username: username
            };

            let chatContainerElement = document.createElement('div');
            chatContainerElement.className = 'chat-container ' + chatConfig.bubbleClass;
            chatContainerElement.title = chatConfig.title;
            chatContainerElement.setAttribute('data-message-id', key);
            let actionsWrapper = document.createElement('div');
            actionsWrapper.className = 'chat-message-actions';
            if (threadBtn !== null) {
                actionsWrapper.appendChild(threadBtn);
            }
            actionsWrapper.appendChild(reactionBtn);
            chatContainerElement.appendChild(actionsWrapper);

            if (chatConfig.username !== '') {
                let chatMessageUsernameElement = document.createElement('div');
                chatMessageUsernameElement.className = 'chat-message-user';
                chatMessageUsernameElement.textContent = chatConfig.username;

                chatContainerElement.appendChild(chatMessageUsernameElement);

            }

            let chatPointedCornerElement = document.createElement('div');
            chatPointedCornerElement.className = 'chat-pointed-corner';
            chatContainerElement.appendChild(chatPointedCornerElement);

            let chatMessageElement = document.createElement('div');
            chatMessageElement.className = 'chat-message-content';

            let chatMessageTextElement = document.createElement('div');
            chatMessageTextElement.className = 'chat-message-text';

            // 'file' matches CHAT_COLUMN_TYPE_FILE constant in Constants.php
            if (chatConfig.type === 'file') {
                let isImage = chatConfig.mimeType && chatConfig.mimeType.startsWith('image/');
                //Thumbnail preview and click open Lightbox
                if (isImage && chatConfig.thumbnailUrl) {
                    let img = document.createElement('img');
                    img.src = chatConfig.thumbnailUrl;
                    img.alt = chatConfig.fileName;
                    img.className = 'chat-image-preview';
                    img.loading = 'lazy';
                    img.dataset.originalUrl = chatConfig.downloadUrl;
                    chatMessageTextElement.appendChild(img);
                    img.addEventListener('click', function () {
                        qfqChat.openLightbox(img.dataset.originalUrl, img.alt);
                    });
                }
                // Attachment: render as download link. href points to a
                // SIP-protected endpoint generated server-side by Chat::createChat.
                let attachLink = document.createElement('a');
                attachLink.href = chatConfig.downloadUrl || '#';
                attachLink.target = '_blank';
                attachLink.className = 'chat-attachment-link';

                let icon = document.createElement('i');
                icon.className = 'fas ' + qfqChat.fileGlyph(chatConfig.mimeType);
                attachLink.appendChild(icon);

                let nameSpan = document.createElement('span');
                nameSpan.textContent = ' ' + chatConfig.fileName;
                attachLink.appendChild(nameSpan);

                let sizeSpan = document.createElement('span');
                sizeSpan.className = 'chat-attachment-size';
                sizeSpan.textContent = ' (' + qfqChat.formatFileSize(chatConfig.fileSize) + ')';
                attachLink.appendChild(sizeSpan);

                chatMessageTextElement.appendChild(attachLink);
                if (chatConfig.message) {
                    let caption = document.createElement('div');
                    caption.className = 'chat-attachment-caption';
                    caption.innerHTML = qfqChat.renderMessageText(chatConfig.message);
                    qfqChat.processCodeBlocks(caption);
                    chatMessageTextElement.appendChild(caption);
                }
            } else if (chatConfig.type === 'videoCall') {
                const roomName = chatConfig.message;
                const baseUrl = this.jitsiBaseUrl || '';
                const fullUrl = baseUrl && roomName ? baseUrl.replace(/\/$/, '') + '/' + roomName : '';
                const card = document.createElement('div');
                card.className = 'chat-call-invitation';

                const icon = document.createElement('i');
                icon.className = 'fas fa-video chat-call-icon';
                card.appendChild(icon);

                const label = document.createElement('span');
                label.className = 'chat-call-label';
                label.textContent = 'Invitation to a video-call';
                card.appendChild(label);

                const joinBtn = document.createElement('button');
                joinBtn.type = 'button';
                joinBtn.className = 'chat-call-join-btn btn btn-primary';
                joinBtn.textContent = 'Join';
                joinBtn.dataset.url = fullUrl;
                joinBtn.disabled = !fullUrl;
                joinBtn.addEventListener('click', function () {
                    if (!this.dataset.url) {
                        console.error('Video-Call: missing URL on join button');
                        QfqNS.Helper.showError('Invalid Video-Call URL.', 'Video-Call');
                        return;
                    }
                    const popup = window.open(this.dataset.url, '_blank', 'popup=true,width=1280,height=720');
                    if (!popup) {
                        console.warn('Video-Call: popup blocked. URL: ' + this.dataset.url);
                        QfqNS.Helper.showError('Pop-up was blocked. Please allow pop-ups for this site.', 'Video-Call');
                    }
                });
                card.appendChild(joinBtn);
                chatMessageTextElement.appendChild(card);
            } else {
                chatMessageTextElement.innerHTML = qfqChat.renderMessageText(chatConfig.message);
                qfqChat.processCodeBlocks(chatMessageTextElement);
            }
            chatMessageElement.appendChild(chatMessageTextElement);

            let chatMessageTimeElement = document.createElement('span');
            chatMessageTimeElement.className = 'chat-time';
            chatMessageTimeElement.textContent = chatConfig.chatTime;
            chatMessageElement.appendChild(chatMessageTimeElement);

            chatContainerElement.appendChild(chatMessageElement);

            if (!this.chatContainer[key]) {
                this.chatContainer[key] = {};
            }

            this.chatContainer[key].element = chatContainerElement;
            this.chatContainer[key].pIdCreator = message[dbColumnNames.pIdCreator];
            this.chatContainer[key].username = message[dbColumnNames.username];
            this.chatContainer[key].message = chatConfig.message;
        }

        // Create the html chat content with response data elements from server
        // The messages will be automatically ordered and set in right place over the given id as key.
        // This function is very flexible and can manage messages which aren't in right order returned from server.
        // Server always gets a list from currently showing messages which allows us to response only the none existing ones.
        refreshChat(element, chatItems, actualChatState, dbColumnNames, load_mode) {
            let messageIdKey = dbColumnNames.id;
            let threadIdKey = dbColumnNames.cIdThread;
            let lastMessageThreadContainer = null;
            let activeThread = actualChatState.activeThread;

            // Convert chatItems object to an array
            let chatItemsArray = Object.values(chatItems);

            // Sort chatItems by messageId (assuming it's numeric)
            chatItemsArray.sort((a, b) => parseInt(a[messageIdKey], 10) - parseInt(b[messageIdKey], 10));

            let messageElement = element.querySelector('.chat-messages');
            let noMessageBanner = element.querySelector('.chat-no-message');

            // Hide noMessageBanner if new messages exist and it is visible
            if (chatItemsArray.length > 0 && noMessageBanner && noMessageBanner.style.display !== 'none') {
                noMessageBanner.style.display = 'none';
            }

            // Insert each new message at the correct position
            chatItemsArray.forEach(chatItem => {
                let isThread = false;
                let isThreadContainerNew = false;
                let threadId = chatItem[threadIdKey];
                let messageId = parseInt(chatItem[messageIdKey], 10);
                let toolbarConfig = null;

                if (chatItem.toolbarConfig !== undefined && chatItem.toolbarConfig !== null) {
                    toolbarConfig = chatItem.toolbarConfig;
                    toolbarConfig = JSON.parse(chatItem.toolbarConfig);
                }

                if (!this.chatContainer[messageId]) {
                    // Create new chat message element
                    this.createNewMessagePlain(messageId, chatItem, actualChatState, dbColumnNames);
                    this.chatContainer[messageId].threadId = threadId;

                    // Create thread container if it doesn't exist and threadId exists.
                    if (threadId !== 0) {
                        isThread = true;
                        if (this.threadContainer[threadId] === undefined) {
                            let muted = false;

                            if (this.openThread && load_mode === 'ping') {
                                muted = true;
                            }
                            this.createThreadContainer(threadId, toolbarConfig, muted);
                            toolbarConfig = null;
                        } else if (toolbarConfig !== null) {
                            // In case thread was already created but not the first one delivered with toolbar configuration
                            if (load_mode !== 'ping') {
                                let currentHeight = this.threadContainer[threadId].element.offsetHeight;
                                let newHeight = currentHeight + 22;
                                this.threadContainer[threadId].element.style.height = newHeight + 'px';
                            }

                            this.initThreadContainer(threadId, toolbarConfig);
                            toolbarConfig = null;
                        }

                        lastMessageThreadContainer = this.threadContainer[threadId].element;
                    } else {
                        lastMessageThreadContainer = null;
                    }
                }

                // Now check if the first message with messageId equal current threadId was created.
                if (this.threadContainer[threadId] !== undefined && !this.threadContainer[threadId].messageIds.includes(threadId)) {
                    this.createFirstThreadMessage(threadId);
                    isThreadContainerNew = true;
                } else if (threadId !== 0) {
                    this.threadContainer[threadId].messageIds.push(messageId);
                }

                let chatContainerElement = this.chatContainer[messageId].element;

                // Only append if it's not already in the DOM
                if (!messageElement.contains(chatContainerElement)) {

                    // if a new thread container is needed then add thread container to the chat room.
                    if (isThreadContainerNew) {
                        this.addThreadToChat(messageElement, threadId, activeThread);
                    }

                    this.addNewMessage(messageElement, chatContainerElement, isThread, threadId, messageId);

                    if (toolbarConfig !== null) {
                        this.toolbar = new ChatToolbar(this, toolbarConfig);
                        this.toolbar.toolbarManager.checkOverflow();
                    }
                }
            });

            if (load_mode !== 'refresh' && load_mode !== 'ping') {
                if (this.openThread) {
                    // Fresh started thread isn't set as currentThread.
                    if (this.currentThread.classList.contains('chat-container')) {
                        this.currentThread = this.currentThread.parentElement;
                    }
                    messageElement = this.currentThread;
                }
                if (messageElement !== null) {
                    messageElement.scrollTo({top: messageElement.scrollHeight, behavior: 'smooth'});
                }
            }
            this.refreshReactions();
        }

        // Load message data from server with given chat configuration.
        // Result is returned as json. - ping
        getMessagesFromServer(that, load_mode) {
            // Server request over load.php only if spinner exists // place holder !== null
            if ((that.chatSpinner === undefined || that.chatSpinner === null) && load_mode === 'first') {
                return;
            }

            // Get the set of existing message ids to prevent them loading on serverside - performance increase
            let messageIds = Array.from(that.chatWindow.querySelectorAll('.chat-messages div[data-message-id]')).map(el => parseInt(el.getAttribute('data-message-id'), 10));
            let messageIdList = messageIds.join(',');

            messageIdList = qfqChat.htmlEncode(messageIdList);
            let serializedForm = `load_mode=${load_mode}&messageIdList=${encodeURIComponent(messageIdList)}`;

            // In case user started with empty chat and another client wrote a new message
            if (load_mode === 'ping' && that.messageId === null) {
                serializedForm = serializedForm + `&flagFirstMsg=true`;
            }

            // Loading spinner only if page loads the first time
            if (load_mode === 'first') {
                that.chatSpinner.style.display = 'block';
            }

            // Create an XMLHttpRequest to send the data
            let xhr = new XMLHttpRequest();
            xhr.open("POST", that.loadApi, true);
            xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

            xhr.onload = function () {
                // Always clear the spinner once any response arrives (null-safe for empty chats).
                if (that.chatSpinner) {
                    that.chatSpinner.style.display = 'none';
                }
                if (xhr.status !== 200) {
                    qfqChat.handleLoadError(that, load_mode, 'HTTP ' + xhr.status);
                    return;
                }
                try {
                    let response = JSON.parse(xhr.responseText);
                    let chatItems = response['chat-update'].chat;
                    let dbColumnNames = response['chat-update'].dbColumnNames;
                    that.reactionsMap = response['chat-update'].reactions || {};
                    let toolbarConfig = JSON.parse(response['chat-update'].toolbarConfig);

                    // Save chat room messageId (head) and active thread flag
                    if (load_mode === 'first' || load_mode === 'ping' && that.messageId === null) {
                        that.messageId = response['chat-update'].firstMsgId;
                        that.thread = response['chat-update'].thread;
                        that.resetDoneOnNewMessage = response['chat-update'].optionTagDoneResetOnNewMessage;
                    }

                    let actualState = that.getActualStateInfo(response);
                    that.refreshChat(that.chatWindow, chatItems, actualState, dbColumnNames, load_mode);

                    // Initialize Toolbar config for chat room
                    if ((load_mode === 'first' || that.chatSpinner === null) && toolbarConfig !== null) {
                        that.toolbar = new ChatToolbar(that, toolbarConfig);
                        that.toolbar.toolbarManager.checkOverflow();

                        if (that.threadContainer[that.messageId] !== undefined) {
                            that.firstThreadContainer = that.threadContainer[that.messageId];
                            that.threadContainer[that.messageId].toolbar.toolbarManager.chat.chatToolbar = that.toolbar;
                        }
                    }

                    that.chatLoaded = true;
                } catch (e) {
                    qfqChat.handleLoadError(that, load_mode, e);
                }
            };

            xhr.onerror = function () {
                if (that.chatSpinner) {
                    that.chatSpinner.style.display = 'none';
                }
                qfqChat.handleLoadError(that, load_mode, 'network error');
            };

            xhr.send(serializedForm);
        }

        // Server information data from latest message.
        getActualStateInfo(response) {
            let actualState = [];
            actualState.xId = response['chat-update'].xId;
            actualState.pIdCreator = response['chat-update'].pIdCreator;
            if (response['chat-update'].flagMoreRecords !== null) {
                this.flagMoreRecords = response['chat-update'].flagMoreRecords;
            }
            actualState.activeThread = response['chat-update'].activeThread;

            return actualState;
        }

        createThreadContainer(threadId, toolbarConfig, muted = false) {
            this.threadContainer[threadId] = {};
            this.threadContainer[threadId].messageIds = [];
            let threadDiv = document.createElement('div');
            threadDiv.className = 'chat-thread-container';
            threadDiv.setAttribute('data-thread-id', threadId);

            if (muted) {
                threadDiv.classList.add('chat-container-muted');
            }

            this.threadContainer[threadId].element = threadDiv;
            this.threadContainer[threadId].topBtnClicked = false;
            this.threadContainer[threadId].toolbar = null;

            if (toolbarConfig !== null) {
                this.initThreadContainer(threadId, toolbarConfig);
            }
        }

        initThreadContainer(threadId, toolbarConfig) {
            this.threadContainer[threadId].messenger = this.messenger;
            this.threadContainer[threadId].messengerChannel = this.messengerChannel;
            this.threadContainer[threadId].loadToolbarApi = this.loadToolbarApi;
            this.threadContainer[threadId].typeAheadSip = this.typeAheadSip;
            this.threadContainer[threadId].typeAheadUrl = this.typeAheadUrl;
            this.threadContainer[threadId].messageId = this.messageId;
            this.threadContainer[threadId].toolbar = new ChatToolbar(this.threadContainer[threadId], toolbarConfig, 'thread');
            this.threadContainer[threadId].toolbar.toolbarManager.checkOverflow();
        }

        createFirstThreadMessage(threadId) {
            if (!this.chatContainer[threadId] || !this.threadContainer[threadId]) {
                return;
            }
            this.threadContainer[threadId].element.appendChild(this.chatContainer[threadId].element);
            this.threadContainer[threadId].firstElementHeight = this.chatContainer[threadId].element.offsetHeight + 10;
            this.threadContainer[threadId].messageIds.push(threadId);
        }

        addNewMessage(messageElement, chatContainerElement, isThread, threadId, messageId) {
            // Add new message to thread container or to chat room itself
            if (isThread) {
                let threadContainer = this.threadContainer[threadId].element;
                let insertAtIndex = Array.from(threadContainer.children).findIndex(el => parseInt(el.getAttribute('data-message-id'), 10) > messageId);

                if (insertAtIndex !== -1) {
                    threadContainer.children[insertAtIndex].before(chatContainerElement);
                } else {
                    threadContainer.appendChild(chatContainerElement);
                }

            } else {
                let insertAtIndex = Array.from(messageElement.children).findIndex(el => {
                    const elId = parseInt(el.getAttribute('data-message-id') || el.getAttribute('data-thread-id'), 10);
                    return elId > messageId;
                });

                if (this.openThread) {
                    chatContainerElement.classList.add('chat-container-muted');
                }

                if (insertAtIndex !== -1) {
                    messageElement.children[insertAtIndex].before(chatContainerElement);
                } else {
                    messageElement.appendChild(chatContainerElement);
                }
            }
        }

        addThreadToChat(messageElement, threadId, activeThread) {
            if (!this.chatContainer[threadId] || !this.threadContainer[threadId]) {
                return;
            }

            let firstMessageIndex = Array.from(messageElement.children).findIndex(el => {
                const elId = parseInt(el.getAttribute('data-message-id') || el.getAttribute('data-thread-id'), 10);
                return elId > threadId;
            });

            // Remove first message if it already exists and thread container is missing.
            let firstMessageElement = this.chatContainer[threadId].element;
            if (messageElement.contains(firstMessageElement)) {
                messageElement.removeChild(firstMessageElement);
            }

            // Create open button for thread
            let button = document.createElement('button');
            let iconElement = document.createElement('i');
            button.className = 'chat-thread-open-btn btn btn-default';
            this.threadContainer[threadId].element.insertBefore(button, this.threadContainer[threadId].element.firstChild);

            // Insert the new thread container at the place where the first message was deleted.
            if (firstMessageIndex === -1) {
                messageElement.appendChild(this.threadContainer[threadId].element);
            } else {
                messageElement.children[firstMessageIndex].before(this.threadContainer[threadId].element);
            }

            let offsetHeight = this.chatContainer[threadId].element.offsetHeight + 30;

            if (this.threadContainer[threadId].toolbar !== null) {
                offsetHeight = offsetHeight + 24;
            }

            if (!activeThread) {
                this.threadContainer[threadId].element.style.height = offsetHeight + 'px';
                iconElement.className = 'fas fa-plus';
            } else {
                iconElement.className = 'fas fa-minus';
                if (!this.threadContainer[threadId].element.style.height) {
                    this.threadContainer[threadId].element.style.height = offsetHeight + 'px';
                }
                this.threadContainer[threadId].element.classList.add('chat-thread-open');
                this.chatMessages.classList.toggle('chat-messages-no-overflow');
            }
            button.appendChild(iconElement);

            // Check if typeahead was initialized. In case that typeahead element is created over another user it will not be initialized if chat was already open.
            QfqNS.TypeAhead.install(this.typeAheadUrl);
            this.threadContainer[threadId].created = true;
        }

        handleThreadBtnMarker(mode, matchFound = false, content = null) {
            if (mode === 'set' && matchFound) {
                // Check if the parent's parent has the class 'chat-thread-container'
                let grandparent = content.parentElement ? content.parentElement.parentElement : null;
                if (grandparent && grandparent.classList.contains('chat-thread-container')) {
                    // Find the button with class 'chat-thread-open-btn' and add a new class
                    let openThreadButton = grandparent.querySelector('.chat-thread-open-btn');
                    if (openThreadButton) {
                        openThreadButton.classList.add('chat-thread-btn-marker');
                        this.markedThreads.push(openThreadButton);
                    }
                }
            }

            if (mode === 'remove') {
                if (this.markedThreads.length > 0) {
                    this.markedThreads.forEach(threadBtn => {
                        threadBtn.classList.remove('chat-thread-btn-marker');

                    });

                    this.markedThreads = this.markedThreads.filter(threadBtn => {
                        return threadBtn && threadBtn.classList.contains('chat-thread-btn-marker');
                    });
                }
            }
        }

        //Reaction Button function
        openReactionPicker(button, messageId) {
            //Singleton, only one picker open at a time
            let existing = document.querySelector('.chat-reaction-picker');
            if (existing) {
                let wasForSameMessage = existing.dataset.messageId === messageId;
                existing.remove();
                //Toggle, clicking same button close picker, don't re-open
                if (wasForSameMessage) {
                    return;
                }
            }
            //Create custom element instance
            let picker = document.createElement('emoji-picker');
            picker.className = 'chat-reaction-picker';
            picker.dataset.messageId = messageId;
            picker.setAttribute('data-source', this.emojiDataSource);
            //Listen for emoji selection
            picker.addEventListener('emoji-click', (event) => {
                let shortcodes = event.detail.emoji.shortcodes;
                if (!shortcodes || !shortcodes.length) {
                    return;
                }
                let shortcode = ':' + shortcodes[0] + ':';
                this.sendReaction(messageId, shortcode);
                picker.remove();
            });
            //Position near the trigger button
            document.body.appendChild(picker);
            let rect = button.getBoundingClientRect();
            picker.style.position = 'fixed';
            picker.style.top = (rect.bottom + 4) + 'px';
            picker.style.left = rect.left + 'px';
            picker.style.zIndex = '10000';

            //Clamp into viewport after the browser has rendered the picker
            requestAnimationFrame(() => {
                let pickerRect = picker.getBoundingClientRect();
                let margin = 8;
                //Horizontal: push left if it overflows the right edge
                if (pickerRect.right > window.innerWidth - margin) {
                    picker.style.left = (window.innerWidth - pickerRect.width - margin) + 'px';
                }
                //Vertical: flip above the button if it overflows the bottom edge
                if (pickerRect.bottom > window.innerHeight - margin) {
                    let above = rect.top - pickerRect.height - 4;
                    picker.style.top = (above >= margin ? above : margin) + 'px';
                }
            });
        }

        sendReaction(messageId, shortcode) {
            // Until then, log and return so the rest of the flow remains testable.
            if (!this.reactionAddApi) {
                console.warn('reactionAddApi attribute missing on chat-window');
                QfqNS.Helper.showError('Reactions are not available.', 'Reactions');
                return;
            }
            //Append cIdMessage and reactionCode to the pre-baked SIP URL.
            let url = this.reactionAddApi + '&reactionCode=' + encodeURIComponent(shortcode) + '&cId=' + encodeURIComponent(messageId);
            let xhr = new XMLHttpRequest();
            xhr.open('POST', url, true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

            xhr.onload = function () {
                if (xhr.status === 200) {
                    try {
                        let response = JSON.parse(xhr.responseText);
                        if (response.status === 'success') {
                            console.log('Reaction sent: ', shortcode, 'for message ', messageId);
                        } else {
                            console.error('Reaction add failed: ' + response.message);
                            QfqNS.Helper.showError(response.message || 'Failed to add reaction. Please try again.', 'Reactions');
                        }
                    } catch (e) {
                        console.error('Reaction add: invalid response from server.');
                        QfqNS.Helper.showError('Invalid server response. Please try again.', 'Reactions');
                    }
                } else {
                    console.error('Reaction request failed, status: ', xhr.status);
                    QfqNS.Helper.showError('Failed to add reaction. Please try again.', 'Reactions');
                }
            };
            xhr.onerror = function () {
                console.error('Reaction request network error');
                QfqNS.Helper.showError('Failed to add reaction. Please check your connection.', 'Reactions');
            };
            xhr.send();
        }

        removeReaction(messageId, shortcode) {
            if (!this.reactionDeleteApi) {
                console.warn('reactionDeleteApi not configured');
                QfqNS.Helper.showError('Reactions are not available.', 'Reactions');
                return;
            }
            let url = this.reactionDeleteApi + '&reactionCode=' + encodeURIComponent(shortcode) + '&cId=' + encodeURIComponent(messageId);
            let xhr = new XMLHttpRequest();
            xhr.open('POST', url, true);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

            xhr.onload = function () {
                if (xhr.status === 200) {
                    try {
                        let response = JSON.parse(xhr.responseText);
                        if (response.status === 'success') {
                            // Server publishes 'reaction-ping' via nchan; UI update via listener.
                            console.log('Reaction removed: ', shortcode, 'for message ', messageId);
                        } else {
                            console.error('Reaction delete failed: ' + response.message);
                            QfqNS.Helper.showError(response.message || 'Failed to remove reaction. Please try again.', 'Reactions');
                        }
                    } catch (e) {
                        console.error('Reaction delete: invalid response from server.');
                        QfqNS.Helper.showError('Invalid server response. Please try again.', 'Reactions');
                    }
                } else {
                    console.error('Reaction delete request failed, status: ', xhr.status);
                    QfqNS.Helper.showError('Failed to remove reaction. Please try again.', 'Reactions');
                }
            };
            xhr.onerror = function () {
                console.error('Reaction delete request network error');
                QfqNS.Helper.showError('Failed to remove reaction. Please check your connection.', 'Reactions');
            };
            xhr.send();
        }

        async loadEmojiMap() {
            //Cache, load only once per session
            if (this.emojiMap) {
                return this.emojiMap;
            }
            try {
                let response = await fetch(this.emojiDataSource);
                if (!response.ok) {
                    throw new Error('HTTP ' + response.status);
                }
                let data = await response.json();
                //Build lookup, shortcode -> glyph (every shortcode of every emoji is indexed)
                this.emojiMap = {};
                data.forEach(entry => {
                    if (entry.shortcodes && entry.emoji) {
                        entry.shortcodes.forEach(shortcode => {
                            if (!this.emojiMap[shortcode]) {
                                this.emojiMap[shortcode] = entry.emoji;
                            }
                        });
                    }
                });
                return this.emojiMap;
            } catch (error) {
                console.error('Failed to load emoji map: ', error);
                QfqNS.Helper.showError('Failed to load emoji list.', 'Reactions');
                this.emojiMap = {};
                return this.emojiMap;
            }
        }

        buildReactionsBlock(reactions) {
            //Aggregate by shortcode: count + user list
            let groups = {};
            reactions.forEach(reaction => {
                let code = reaction.message;
                if (!groups[code]) {
                    groups[code] = {count: 0, users: []};
                }
                groups[code].count++;
                groups[code].users.push(reaction.username);
            });

            //Build DOM block, one badge per reaction code
            let block = document.createElement('div');
            block.className = 'chat-reactions';

            Object.keys(groups).forEach(code => {
                let group = groups[code];
                let shortcodeKey = code.replace(/:/g, '');
                let glyph = (this.emojiMap && this.emojiMap[shortcodeKey]) || code;
                let badge = document.createElement('span');
                badge.className = 'chat-reaction-badge';
                let isOwn = group.users.indexOf(this.currentUsername) !== -1;
                if (isOwn) {
                    badge.classList.add('chat-reaction-badge-own');
                }
                badge.dataset.reactionCode = code;
                badge.title = group.users.join(', ');
                badge.textContent = glyph + ' ' + group.count;
                block.appendChild(badge);
            });
            return block;
        }

        async refreshReactions() {
            if (!this.reactionsMap) {
                return;
            }
            await this.loadEmojiMap();

            // Remove orphan reaction blocks: messages whose reactions have all been deleted
            // are no longer keys in reactionsMap, so the existing block would never get removed.
            this.chatWindow.querySelectorAll('.chat-reactions').forEach(block => {
                let container = block.closest('.chat-container');
                let messageId = container && container.getAttribute('data-message-id');
                if (!messageId || !this.reactionsMap[messageId]) {
                    block.remove();
                }
            });

            Object.keys(this.reactionsMap).forEach(messageId => {
                let container = this.chatWindow.querySelector('.chat-container[data-message-id="' + messageId + '"]');
                if (container === null) {
                    return;
                }
                //Re-render strategy, drop old reactions block, build fresh one
                let existing = container.querySelector('.chat-reactions');
                if (existing !== null) {
                    existing.remove();
                }
                let reactions = this.reactionsMap[messageId];
                if (reactions.length === 0) {
                    return;
                }
                let block = this.buildReactionsBlock(reactions);
                container.appendChild(block);
            });
        }
    }

    // Set the new input state which comes over dynamic update. The only feature that is dependent on the form functionality.
    qfqChat.setInputState = function (element, configItem) {
        let inputContainer = element.parentNode.querySelector(".chat-input-container");
        let inputElement = inputContainer.querySelector(".chat-input-field");
        inputElement.disabled = configItem.disabled;
        inputElement.required = configItem.required;
    }

    // Get the disabled required properties from input element. Can be useful on server side.
    qfqChat.getInputState = function (inputElement) {
        let elementMode = {};
        elementMode.required = inputElement.required;
        elementMode.disabled = inputElement.disabled;

        return JSON.stringify(elementMode);
    }

    // Handle chat message submit.
    qfqChat.submit = function (that) {
        let chatInput = that.chatInput.value;
        chatInput = chatInput.trim();
        chatInput = this.htmlEncode(chatInput);
        let inputState = this.getInputState(that.chatInput);
        let cId = that.messageId;
        let chatRoom = true;

        // Get thread id from input field if exists. Is append if user uses the thread btn to answer.
        let threadId = that.chatInput.getAttribute('data-thread-id');

        // Check if the current submitted message is the first one
        let flagFirstMsg = false;

        if (threadId !== null) {
            chatRoom = false;
            if (that.threadContainer[threadId] === undefined) {
                flagFirstMsg = true;
            }
        } else if (Object.keys(that.chatContainer).length === 0) {
            flagFirstMsg = true;
        }

        // Build client information for server actions.
        let serializedForm = `message=${encodeURIComponent(chatInput)}&chatRoomMsgId=${that.messageId}&element_mode=${inputState}&flagFirstMsg=${flagFirstMsg}`;
        if (threadId !== null) {
            serializedForm = `threadId=${threadId}&` + serializedForm;
            cId = threadId;
        }

        // saveApi has already a sip delivered inside.
        let submitUrl = that.saveApi;

        if (chatInput === '') {
            return;
        }

        // Prevent double-send: ignore a second submit while one is still in flight
        if (that.isSending) {
            return;
        }
        that.isSending = true;

        // Create an XMLHttpRequest to send the data
        let xhr = new XMLHttpRequest();
        xhr.open("POST", submitUrl, true);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

        xhr.onload = function () {
            // A response arrived — the send is complete, allow the next one.
            that.isSending = false;
            if (xhr.status === 200) {
                try {
                    let response = JSON.parse(xhr.responseText);
                    if (response.status === 'error') {
                        console.error('Chat save failed: ' + response.message);
                        QfqNS.Helper.showError(response.message || 'Failed to send message. Please try again.', 'Chat');
                        return;
                    }

                    if (that.lastThreadBtn !== null) {
                        // Remove the visible comment button after first thread answer was sent. Instead of this the plus button is showed.
                        that.lastThreadBtn.classList.remove('chat-thread-btn-clicked');
                    }

                    let chatItems = response['chat-update'].chat;
                    let dbColumnNames = response['chat-update'].dbColumnNames;
                    that.reactionsMap = response['chat-update'].reactions || {};
                    let messageId = response['chat-update'].messageId;
                    if (that.messageId === null && messageId !== null) {
                        that.messageId = messageId;
                        that.thread = response['chat-update'].thread;
                        that.resetDoneOnNewMessage = response['chat-update'].optionTagDoneResetOnNewMessage;
                    }

                    // Get most recent/latest message data. This equals the highest response message id
                    let actualState = that.getActualStateInfo(response);

                    // Create chat content
                    that.refreshChat(that.chatWindow, chatItems, actualState, dbColumnNames, 'save');

                    // Clear input field
                    that.chatInput.value = '';
                    qfqChat.autoResizeInput(that.chatInput);

                    // Reset done btn
                    if (that.resetDoneOnNewMessage) {
                        that.refreshToolbar(cId, chatRoom);
                    }

                    // Live notification published server-side by Chat::saveChatMessage — no client publish needed.
                    console.log('Chat successfully saved.');
                } catch (e) {
                    console.error('Chat save: invalid response from server.');
                    QfqNS.Helper.showError('Invalid server response. Please try again.', 'Chat');
                }
            } else {
                console.error('Chat save HTTP error: ' + xhr.status);
                QfqNS.Helper.showError('Failed to send message. Please try again.', 'Chat');
            }
        };

        xhr.onerror = function () {
            that.isSending = false;
            console.error('Chat save request failed.');
            QfqNS.Helper.showError('Failed to send message. Please check your connection.', 'Chat');
        };

        xhr.send(serializedForm);
    };

    qfqChat.STAGING_MAX_FILES = 10;
    qfqChat.STAGING_MAX_SIZE = 25 * 1024 * 1024;
    qfqChat.STAGING_MIME_WHITELIST = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', 'text/plain'];

    // Add files from any source (picker / drag&drop / paste) into the staging stack.
    // Client validation is UX-only; the server re-validates on send.
    qfqChat.addFilesToStaging = function (that, fileList) {
        let files = Array.from(fileList || []);
        if (files.length === 0) {
            return;
        }

        //Count limit (10) is binding on the client, reject thewhole action on overflow
        if (that.stagingFiles.length + files.length > qfqChat.STAGING_MAX_FILES) {
            QfqNS.Helper.showError('You can attach at most ' + qfqChat.STAGING_MAX_FILES + ' files.', 'Attachment');
            return;
        }

        let errors = [];
        files.forEach(function (file) {
            if (file.size > qfqChat.STAGING_MAX_SIZE) {
                errors.push(file.name + ': file too big (max 25MB)');
                return;
            }
            if (file.type && qfqChat.STAGING_MIME_WHITELIST.indexOf(file.type) === -1) {
                errors.push(file.name + ': file type not allowed. Allowed types are: ' + qfqChat.STAGING_MIME_WHITELIST);
                return;
            }
            let entry = {
                file: file,
                isImage: file.type.startsWith('image'),
                isCaptionCarrier: false,
                caption: '',
                objectUrl: null
            };
            that.stagingFiles.push(entry);
            qfqChat.renderStagingTile(that, entry);
        });
        //One bundelet alert per phase
        if (errors.length > 0) {
            QfqNS.Helper.showError(errors.join('\n'), 'Attachment');
        }
    };

    //Map a MIME-Type to a Font Awesome 5 file glyph.
    qfqChat.fileGlyph = function (mimeType) {
        mimeType = mimeType || '';
        if (mimeType.startsWith('image/')) {
            return 'fa-file-image';
        }
        if (mimeType.startsWith('application/pdf')) {
            return 'fa-file-pdf';
        }
        if (mimeType.startsWith('text/plain')) {
            return 'fa-file-alt';
        }
        return 'fa-file';
    };

    //Build one Staging tile and append it to the preview area.
    qfqChat.renderStagingTile = function (that, entry) {
        let tile = document.createElement('div');
        tile.className = 'chat-staging-tile';
        tile.title = entry.file.name;
        entry.tile = tile;

        if (entry.isImage) {
            entry.objectUrl = URL.createObjectURL(entry.file);
            let img = document.createElement('img');
            img.className = 'chat-staging-thumb';
            img.src = entry.objectUrl;
            tile.classList.add('chat-staging-tile-image');
            tile.appendChild(img);
        } else {
            let icon = document.createElement('i');
            icon.className = 'fas ' + qfqChat.fileGlyph(entry.file.type);
            let name = document.createElement('span');
            name.className = 'chat-staging-name';
            name.textContent = entry.file.name;
            tile.classList.add('chat-staging-tile-file');
            tile.appendChild(icon);
            tile.appendChild(name);
        }

        let remove = document.createElement('button');
        remove.type = 'button';
        remove.className = 'chat-staging-remove';
        remove.innerHTML = '&times;';
        remove.addEventListener('click', () => {
            qfqChat.removeFromStaging(that, entry, tile);
        });
        tile.appendChild(remove);

        that.chatStagingTiles.appendChild(tile);
    };

    //Remove a staging entry
    qfqChat.removeFromStaging = function (that, entry, tile) {
        let idx = that.stagingFiles.indexOf(entry);
        if (idx !== -1) {
            that.stagingFiles.splice(idx, 1);
        }
        if (entry.objectUrl) {
            URL.revokeObjectURL(entry.objectUrl);
            entry.objectUrl = null;
        }
        tile.remove();
    };

    //send one staged file.
    qfqChat.sendOneAttachment = function (that, entry) {
        return new Promise(function (resolve) {
            let formData = new FormData();
            formData.append('file', entry.file);

            let threadId = that.chatInput.getAttribute('data-thread-id') || 0;
            formData.append('cIdThread', threadId);

            //Caption travels only with the carrier entry
            if (entry.isCaptionCarrier) {
                formData.append('caption', entry.caption);
            }

            let xhr = new XMLHttpRequest();
            xhr.open('POST', that.attachmentApi, true);

            xhr.onload = function () {
                if (xhr.status === 200) {
                    try {
                        let response = JSON.parse(xhr.responseText);
                        if (response.status === 'success') {
                            resolve({result: 'success'});
                        } else {
                            //server rejected (type/size)
                            resolve({result: 'permanent', message: response.message || 'rejected'});
                        }
                    } catch (e) {
                        resolve({result: 'transient', message: 'invalid server response'});
                    }
                } else {
                    resolve({result: 'transient', message: 'HTTP ' + xhr.status});
                }
            };
            xhr.onerror = function () {
                resolve({result: 'transient', message: 'network error'});
            };
            xhr.send(formData);
        });
    };

    qfqChat.autoResizeInput = function (el) {
        if (!el) return;
        el.style.height = 'auto';
        let cs = window.getComputedStyle(el);
        let border = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
        el.style.height = (el.scrollHeight + border) + 'px';
    };

    // Central handling for a failed chat load: hide spinner (null-safe), tell the user, allow recovery.
    qfqChat.handleLoadError = function (that, load_mode, reason) {
        console.error('Chat load failed (' + load_mode + '): ', reason);
        if (that.chatSpinner) {
            that.chatSpinner.style.display = 'none';
        }
        QfqNS.Helper.showError('Chat could not be loaded. Please reload the page to try again.', 'Chat');
    };

    //Orchestrate sequenziel upload of all staged files. Caption-carrier per entry stack fate, one bundled error alert, UI-locking overlay + counter
    qfqChat.submitStaging = async function (that) {
        if (!that.attachmentApi) {
            QfqNS.Helper.showError('Attachments are not available.', 'Attachment');
            return;
        }

        // Caption-carrier: if text was typed, the FIRST stack entry carries it. Clear the field immediately, caption now lives in the entry.
        let captionText = that.chatInput.value.trim();
        if (captionText !== '') {
            that.stagingFiles[0].isCaptionCarrier = true;
            that.stagingFiles[0].caption = captionText;
            that.chatInput.value = '';
        }
        qfqChat.autoResizeInput(that.chatInput)

        //UI lock
        let chatFieldset = that.chatWindow.parentNode;
        let overlay = document.createElement('div');
        overlay.className = 'chat-upload-overlay';
        overlay.innerHTML = '<i class="fas fa-spinner fa-spin fa-3x"></i>';
        let counter = document.createElement('div');
        counter.className = 'chat-upload-counter';
        overlay.appendChild(counter);
        chatFieldset.appendChild(overlay);

        // Snapshot: the real stack mutates on success; the loop needs a stable list + count.
        let snapshot = that.stagingFiles.slice();
        let total = snapshot.length;
        let errors = [];

        for (let i = 0; i < total; i++) {
            let entry = snapshot[i];
            counter.textContent = 'File ' + (i + 1) + ' of ' + total;
            let outcome = await qfqChat.sendOneAttachment(that, entry);
            if (outcome.result === 'success') {
                qfqChat.removeFromStaging(that, entry, entry.tile);
            } else if (outcome.result === 'permanent') {
                qfqChat.removeFromStaging(that, entry, entry.tile);
                errors.push(entry.file.name + ': ' + outcome.message);
            } else {
                errors.push(entry.file.name + ': ' + outcome.message);
            }
        }
        overlay.remove();
        if (errors.length > 0) {
            QfqNS.Helper.showError(errors.join('\n'), 'Upload');
        }
    };

    // Format file size in bytes to human-readable string (e.g. 2300456 → "2.2 MB").
    qfqChat.formatFileSize = function (bytes) {
        let n = parseInt(bytes, 10);
        if (isNaN(n) || n < 0) {
            return '';
        }
        if (n < 1024) {
            return n + ' B';
        }
        if (n < 1024 * 1024) {
            return (n / 1024).toFixed(1) + ' KB';
        }
        if (n < 1024 * 1024 * 1024) {
            return (n / (1024 * 1024)).toFixed(1) + ' MB';
        }
        return (n / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
    };

    // Encode message input to prevent JS scripting attacks.
    qfqChat.htmlEncode = function (str) {
        let div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML.replace(/&nbsp;/g, '\u00A0');
    };

    qfqChat.renderMessageText = function (text) {
        if (text === null || text === undefined) {
            return '';
        }
        const codeBlockRegex = /```([\w+#]*)\n?([\s\S]*?)```/g;
        let html = '';
        let lastIndex = 0;
        let match;

        while ((match = codeBlockRegex.exec(text)) !== null) {
            //Plain text segment before this code block
            const before = text.substring(lastIndex, match.index);
            html += before.replace(/\n/g, '<br>');

            //Code block: sanitize language tag, escape body
            const lang = match[1].replace(/[^a-zA-Z0-9_+#-]/g, '');
            const code = match[2];
            const langClass = lang ? ' class="language-' + lang + '"' : '';
            html += '<pre><code' + langClass + '>' + code + '</code></pre>';
            lastIndex = codeBlockRegex.lastIndex;
        }
        //Trailing plain text after the last code block
        const tail = text.substring(lastIndex);
        html += tail.replace(/\n/g, '<br>');
        return html;
    };

    // Post-process rendered code blocks: apply highlight.js syntax coloring
    // and attach a "copy to clipboard" button to each <pre>.
    qfqChat.processCodeBlocks = function (container) {
        if (!container) {
            return;
        }
        container.querySelectorAll('pre').forEach(function (preEl) {
            const codeEl = preEl.querySelector('code');
            if (!codeEl) {
                return;
            }

            // Capture original code text before hljs rewrites the innerHTML.
            const originalCode = codeEl.textContent;

            // Apply syntax highlighting. If no language-XYZ class is set,
            // hljs falls back to auto-detection.
            if (window.hljs) {
                window.hljs.highlightElement(codeEl);
            }

            // Copy-to-clipboard button
            const copyBtn = document.createElement('button');
            copyBtn.type = 'button';
            copyBtn.className = 'chat-code-copy-btn';
            copyBtn.title = 'Code kopieren';
            const copyIcon = document.createElement('i');
            copyIcon.className = 'fa fa-copy';
            copyBtn.appendChild(copyIcon);

            copyBtn.addEventListener('click', function () {
                if (navigator.clipboard && window.isSecureContext) {
                    navigator.clipboard.writeText(originalCode).then(function () {
                        copyIcon.className = 'fa fa-check';
                        setTimeout(function () {
                            copyIcon.className = 'fa fa-copy';
                        }, 1500);
                    });
                }
            });

            preEl.appendChild(copyBtn);
        });
    };

    qfqChat.openLightbox = function (url, alt) {
        if (!url) {
            return;
        }
        let overlay = document.createElement('div');
        overlay.className = 'chat-lightbox-overlay';

        let img = document.createElement('img');
        img.src = url;
        img.alt = alt || '';
        img.className = 'chat-lightbox-image';
        overlay.appendChild(img);

        let closeBtn = document.createElement('button');
        closeBtn.type = 'button';
        closeBtn.className = 'chat-lightbox-close';
        let closeIcon = document.createElement('i');
        closeIcon.className = 'fa fa-times';
        closeBtn.appendChild(closeIcon);
        overlay.appendChild(closeBtn);

        let escHandler;
        let closeLightbox = function () {
            document.body.removeChild(overlay);
            document.removeEventListener('keydown', escHandler);
        };
        escHandler = function (e) {
            if (e.key === 'Escape') {
                closeLightbox();
            }
        };
        closeBtn.addEventListener('click', closeLightbox);
        overlay.addEventListener('click', function (e) {
            if (e.target === overlay) {
                closeLightbox();
            }
        });
        document.addEventListener('keydown', escHandler);
        document.body.appendChild(overlay);
    };

    n.qfqChat = qfqChat;

})(QfqNS.Helper);

/**
 * QFQ Clipboard Helper
 * Provides clipboard operations with Figma-style HTML embedding for multi-format support
 *
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 */

/* global console */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * QfqClipboard Class
     * Handles clipboard operations with support for multiple formats
     */
    class QfqClipboard {
        constructor(options) {
            options = options || {};
            this.metaName = options.metaName || 'qfq-data';
            this.validator = options.validator || this.defaultValidator;
        }

        /**
         * Default validator for QFQ data
         */
        defaultValidator(data) {
            if (!data || typeof data !== 'object') {
                return false;
            }
            return data.__qfq === true || (data.version && data.records);
        }

        /**
         * Set custom validator function
         * @param {Function} validatorFn - Function that takes data and returns boolean
         */
        setValidator(validatorFn) {
            this.validator = validatorFn;
        }

        /**
         * Copy data to clipboard in multiple formats (Figma-style)
         * - text/html: Contains hidden data + visible table for Word/Excel
         * - text/plain: JSON string for fallback
         *
         * @param {object} data - Data object to copy
         * @param {object} options - Optional settings
         * @param {Function} options.htmlBuilder - Custom HTML builder function
         * @param {boolean|number|string} options.pretty - Pretty print JSON (true = 2 spaces, number = custom spaces, string = custom indent like '\t')
         * @param {boolean} options.plainOnly - If true, only copy plain text JSON (no HTML)
         * @returns {Promise<void>}
         */
        async copy(data, options) {
            options = options || {};

            // Determine indentation for JSON
            var indent = null;
            if (options.pretty === true) {
                indent = 2;
            } else if (typeof options.pretty === 'number' || typeof options.pretty === 'string') {
                indent = options.pretty;
            }

            // JSON for plain text (with optional pretty print)
            var jsonString = JSON.stringify(data, null, indent);

            // Plain only mode - skip HTML
            if (options.plainOnly) {
                await this.copyText(jsonString);
                return;
            }

            // Encode data as base64 (always compact for HTML embedding)
            var jsonStringCompact = JSON.stringify(data);
            var base64 = btoa(unescape(encodeURIComponent(jsonStringCompact)));

            // Build HTML with hidden data + visible content
            var html;
            if (options.htmlBuilder) {
                html = options.htmlBuilder(data, base64, this.metaName);
            } else {
                html = this.buildDefaultHtml(data, base64);
            }

            // Try modern Clipboard API with multiple formats
            if (navigator.clipboard && navigator.clipboard.write) {
                try {
                    var clipboardItem = new ClipboardItem({
                        'text/html': new Blob([html], { type: 'text/html' }),
                        'text/plain': new Blob([jsonString], { type: 'text/plain' })
                    });
                    await navigator.clipboard.write([clipboardItem]);
                    return;
                } catch (err) {
                    console.warn('ClipboardItem write failed, falling back:', err.message);
                }
            }

            // Fallback: writeText (only plain text)
            if (navigator.clipboard && navigator.clipboard.writeText) {
                await navigator.clipboard.writeText(jsonString);
                return;
            }

            // Legacy fallback for older browsers
            this.legacyCopy(jsonString);
        }

        /**
         * Copy plain text to clipboard
         *
         * @param {string} text - Text to copy
         * @returns {Promise<void>}
         */
        async copyText(text) {
            if (navigator.clipboard && navigator.clipboard.writeText) {
                await navigator.clipboard.writeText(text);
                return;
            }

            this.legacyCopy(text);
        }

        /**
         * Legacy copy fallback using execCommand
         *
         * @param {string} text - Text to copy
         */
        legacyCopy(text) {
            var textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.style.position = 'fixed';
            textarea.style.opacity = '0';
            document.body.appendChild(textarea);
            textarea.select();

            try {
                document.execCommand('copy');
            } catch (err) {
                throw new Error('Failed to copy to clipboard');
            } finally {
                document.body.removeChild(textarea);
            }
        }

        /**
         * Read and validate QFQ data from clipboard
         * Tries HTML format first (with embedded data), then plain text JSON
         *
         * @returns {Promise<object|null>} Parsed data or null if invalid
         */
        async read() {
            // Try modern Clipboard API with read()
            if (navigator.clipboard && navigator.clipboard.read) {
                try {
                    var items = await navigator.clipboard.read();

                    for (var i = 0; i < items.length; i++) {
                        var item = items[i];

                        // Try HTML first (Figma-style embedded data)
                        if (item.types.includes('text/html')) {
                            var htmlBlob = await item.getType('text/html');
                            var html = await htmlBlob.text();
                            var qfqData = this.extractFromHtml(html);
                            if (qfqData) {
                                return qfqData;
                            }
                        }

                        // Fallback: Try plain text JSON
                        if (item.types.includes('text/plain')) {
                            var textBlob = await item.getType('text/plain');
                            var text = await textBlob.text();
                            var jsonData = this.parseJson(text);
                            if (jsonData) {
                                return jsonData;
                            }
                        }
                    }
                } catch (err) {
                    console.warn('Clipboard read() failed:', err.message);
                }
            }

            // Fallback: readText()
            if (navigator.clipboard && navigator.clipboard.readText) {
                try {
                    var plainText = await navigator.clipboard.readText();
                    return this.parseJson(plainText);
                } catch (err) {
                    console.warn('Clipboard readText() failed:', err.message);
                }
            }

            // Last resort: prompt
            var userInput = prompt('Please paste the JSON data:');
            return userInput ? this.parseJson(userInput) : null;
        }

        /**
         * Read plain text from clipboard
         *
         * @returns {Promise<string|null>}
         */
        async readText() {
            if (navigator.clipboard && navigator.clipboard.readText) {
                try {
                    return await navigator.clipboard.readText();
                } catch (err) {
                    console.warn('Clipboard readText() failed:', err.message);
                }
            }

            return prompt('Please paste the text:');
        }

        /**
         * Check if clipboard contains valid QFQ data
         * Note: May fail due to permission restrictions outside user interaction
         * Does NOT use prompt fallback - only checks if API is available
         *
         * @returns {Promise<boolean>}
         */
        async hasValidData() {
            // Only check if modern Clipboard API is available
            if (!navigator.clipboard) {
                return true; // Assume valid if we can't check
            }

            try {
                // Try read() first (for HTML data)
                if (navigator.clipboard.read) {
                    var items = await navigator.clipboard.read();

                    for (var i = 0; i < items.length; i++) {
                        var item = items[i];

                        if (item.types.includes('text/html')) {
                            var htmlBlob = await item.getType('text/html');
                            var html = await htmlBlob.text();
                            var qfqData = this.extractFromHtml(html);
                            if (qfqData) {
                                return true;
                            }
                        }

                        if (item.types.includes('text/plain')) {
                            var textBlob = await item.getType('text/plain');
                            var text = await textBlob.text();
                            var jsonData = this.parseJson(text);
                            if (jsonData) {
                                return true;
                            }
                        }
                    }
                    return false;
                }

                // Fallback to readText()
                if (navigator.clipboard.readText) {
                    var plainText = await navigator.clipboard.readText();
                    return this.parseJson(plainText) !== null;
                }

                return true; // Assume valid if we can't check
            } catch (err) {
                console.warn('Clipboard validation failed:', err.message);
                return true; // Assume valid if we can't check
            }
        }

        /**
         * Extract QFQ data from HTML (Figma-style)
         *
         * @param {string} html - HTML string
         * @returns {object|null} Parsed data or null
         */
        extractFromHtml(html) {
            // Look for qfq-data meta tag
            var regex = new RegExp('name="' + this.metaName + '"\\s+content="([^"]+)"');
            var match = html.match(regex);
            if (!match) {
                return null;
            }

            try {
                // Decode base64 and parse JSON
                var json = decodeURIComponent(escape(atob(match[1])));
                var data = JSON.parse(json);

                // Validate it's QFQ data
                if (this.validator(data)) {
                    return data;
                }
            } catch (e) {
                console.warn('Failed to extract data from HTML:', e.message);
            }

            return null;
        }

        /**
         * Parse and validate QFQ JSON from plain text
         *
         * @param {string} text - JSON string
         * @returns {object|null} Parsed data or null
         */
        parseJson(text) {
            if (!text || !text.trim()) {
                return null;
            }

            try {
                var data = JSON.parse(text);
                if (this.validator(data)) {
                    return data;
                }
            } catch (e) {
                // Not valid JSON
            }

            return null;
        }

        /**
         * Build default HTML with embedded data and table
         *
         * @param {object} data - Data object
         * @param {string} base64 - Base64 encoded JSON
         * @returns {string} HTML string
         */
        buildDefaultHtml(data, base64) {
            var records = data.records || [];
            var tableHtml = '';

            if (records.length > 0) {
                var firstRecord = records[0].data || records[0];
                var columns = Object.keys(firstRecord);

                var headerRow = columns.map(function(col) {
                    return '<th>' + this.escapeHtml(col) + '</th>';
                }, this).join('');

                var dataRows = records.map(function(record) {
                    var rowData = record.data || record;
                    return '<tr>' + columns.map(function(col) {
                        var value = rowData[col] !== undefined && rowData[col] !== null ? rowData[col] : '';
                        return '<td>' + this.escapeHtml(String(value)) + '</td>';
                    }, this).join('') + '</tr>';
                }, this).join('');

                tableHtml = '<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">' +
                    '<thead><tr>' + headerRow + '</tr></thead>' +
                    '<tbody>' + dataRows + '</tbody>' +
                    '</table>';
            }

            return '<!DOCTYPE html>' +
                '<html>' +
                '<head>' +
                '<meta charset="utf-8">' +
                '<meta name="' + this.metaName + '" content="' + base64 + '">' +
                '</head>' +
                '<body>' +
                tableHtml +
                '<p><small>QFQ Export - v' + (data.version || '1.0') + ' - ' + records.length + ' record(s)</small></p>' +
                '</body>' +
                '</html>';
        }

        /**
         * Escape HTML special characters
         *
         * @param {string} text - Text to escape
         * @returns {string} Escaped text
         */
        escapeHtml(text) {
            var div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }
    }

    // Export class and default instance
    n.Helper.QfqClipboard = QfqClipboard;
    n.Helper.qfqClipboard = new QfqClipboard();

})(QfqNS);
/**
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 */

/* global console */
/* global qfqChat */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    let qfqHistory = function (historyBtnElement, n) {
        if (historyBtnElement !== undefined) {
            new HistoryView(historyBtnElement, n);
        }
    }

    class HistoryView {
        constructor(historyBtnElement, n) {
            this.modalTitle = historyBtnElement.getAttribute("data-history-title");

            // Get existing history view template
            let template = document.getElementsByClassName('qfq-history-view')[0];
            if (template === undefined) {
                template = this.createModalTemplate();
                document.body.appendChild(template);
            }

            this.historyView = template;
            this.historyViewBody = this.historyView.getElementsByClassName('modal-body')[0];
            this.historyBtn = historyBtnElement;
            this.baseUrl = historyBtnElement.getAttribute("data-base-url");
            this.restCallUrl = this.baseUrl + 'typo3conf/ext/qfq/Classes/Api/load.php?s=';
            this.dataSipRules = historyBtnElement.getAttribute("data-sip");
            this.tablesorterViewJson = historyBtnElement.getAttribute("data-tablesorter-view-json");
            this.formElements = {};
            this.formId = null;
            this.header = null;
            this.rows = {};
            this.takeBtn = null;
            this.data = null;
            this.formLabelData = null;
            this.formNames = null;
            this.form = n.form;
            this.table = null;
            this.checkboxes = [];
            this.tablesorterController = new n.TablesorterController();

            this.initHistoryBtn();
        }

        initHistoryBtn() {
            let that = this;
            this.historyBtn.addEventListener('click', function () {
                that.initView();
                setTimeout(function () {
                    that.initCheckboxes();
                }, 500);
            });
        }

        // Initialize the basic history view
        async initView() {
            let url = this.restCallUrl + this.dataSipRules;
            // Create a new html content for the modal window
            this.historyViewBody.innerHTML = 'Loading...';
            $(this.historyView).modal('show');

            let data = await this.makeApiCall(url);
            // extract formLabelData and formNames from data
            this.formLabelData = data.formLabelData;
            this.formNames = data.formNames;
            this.formId = data.formId;

            // Separate the formLabelData, formNames and formId from the data
            delete data.formLabelData;
            delete data.formNames;
            delete data.formId;
            this.data = data;

            // Map the formLabelData to keep order
            this.formLabelData = new Map(this.formLabelData);

            // Put historyHtml in to the modal window
            this.historyViewBody.innerHTML = this.buildHistoryView();
            this.table = $(this.historyViewBody).find('table');
            this.tablesorterController.setup($(this.historyViewBody).find('table'));
            this.takeBtn = this.buildTakeBtn();
            this.historyViewBody.appendChild(this.takeBtn);
            this.initTakeBtn(this);
            this.initShowMoreTextBtn();
        }

        initCheckboxes() {
            // Get all main checkboxes
            let mainCheckboxes = this.historyView.getElementsByClassName('main-checkbox');

            // Get all other checkboxes
            this.checkboxes = this.historyView.getElementsByClassName('checkbox');
            let checkboxes = this.checkboxes;

            // Add event listener to each main checkbox
            Array.from(mainCheckboxes).forEach(cb => {
                cb.addEventListener('click', function () {
                    let created = cb.getAttribute('data-created');
                    Array.from(checkboxes).forEach(c => {
                        const td = c.parentNode;
                        if (c.getAttribute('data-created') === created && td.clientWidth !== 0) {
                            c.checked = cb.checked;
                        } else if (cb.checked === true) {
                            c.checked = false;
                        }
                    });

                    // Uncheck all the other main checkboxes
                    Array.from(mainCheckboxes).forEach(c => {
                        if (c.getAttribute('data-created') !== created) {
                            c.checked = false;
                        }
                    });
                });
            });

            // Add event listener to other checkboxes.
            // If one is checked then the others with same data-name should be unchecked
            Array.from(checkboxes).forEach(cb => {
                cb.addEventListener('click', function () {
                    let name = cb.getAttribute('data-name');
                    let created = cb.getAttribute('data-created');
                    Array.from(checkboxes).forEach(c => {
                        if (c.getAttribute('data-name') === name && c.getAttribute('data-created') !== created) {
                            c.checked = false;
                        }
                    });
                });
            });

        }

        initTakeBtn(that) {
            this.takeBtn.addEventListener('click', function () {
                let data = {};
                let output = '';

                Array.from(that.checkboxes).forEach(cb => {
                    let name = cb.getAttribute('data-name');

                    if (cb.checked) {
                        data[name] = cb.value;
                    }
                });

                output = that.buildSelectedOutput(data);


                if (confirm(output)) {

                    console.log("Checked data: ", data);

                    that.setFormValues(data);
                    that.form.inputAndPasteHandler(that.form);
                    // Close the modal
                    $(that.historyView).modal('hide');
                }
            });
        }

        initShowMoreTextBtn() {
            // Get all more text buttons
            let moreTextBtns = this.historyView.getElementsByClassName('btn-link');

            // Add event listener to each more text button
            Array.from(moreTextBtns).forEach(btn => {
                btn.addEventListener('click', function () {
                    // Get the previous sibling span element
                    const span = btn.previousElementSibling;
                    if (span.style.display === 'block') {
                        span.style.display = 'none';
                        btn.textContent = '[...]';
                    } else {
                        span.style.display = 'block';
                        btn.textContent = '[<<]';
                    }

                });
            });
        }

        async makeApiCall(url) {
            const response = await fetch(url, {method: 'POST'});
            let data = null;
            if (!response.ok) {
                throw new Error();
            } else {
                data = await response.json();
            }

            return data['record-history-data'];
        }

        buildHistoryView() {
            let data = this.data;
            let formLabelData = this.formLabelData;
            let dataObj = data;
            let html = '';
            let tablesorterConfig = this.getTableSorterConfigJson();
            let tablesorterView = JSON.parse(this.tablesorterViewJson);
            let tablesorterHtml = tablesorterView.data;
            html += '<div class="table-responsive"><table id="qfq-history-' + this.formId + '" ' + tablesorterHtml + ' data-tablesorter-config=\'' + tablesorterConfig + '\' class="table table-bordered table-hover qfq-table-50 tablesorter tablesorter-filter tablesorter-pager tablesorter-column-selector table-condensed">';

            let actualFormValues = this.getCurrentFormValues();
            actualFormValues = this.reorderArray(formLabelData, actualFormValues);
            let tablesorterHeader = this.buildTablesorterHeader(formLabelData, actualFormValues);
            let tablesorterBody = this.buildTablesorterBody(dataObj);
            html += tablesorterHeader;
            html += tablesorterBody;
            html += '</table></div>';

            return html;
        }

        // formLabelData is an array of label and name: [label1 => name1, label2 => name2, label3 => name3]
        // build header with that array
        buildTablesorterHeader(formLabelData, actualFormValues) {
            let header = '<thead class="qfq-sticky">';
            let headerRow = '<tr>';

            // Summary checkbox position
            headerRow += '<th class="filter-false sorter-false"></th>';
            headerRow += '<th>Created</th>';
            headerRow += '<th>feUser</th>';

            // Add the name as data-label attribute to the cell inside the iterating formLabelData
            formLabelData.forEach((label, name) => {
                if (label === '') {
                    label = '(' + name + ')';
                }
                headerRow += '<th data-name="' + name + '">' + label + '</th>';
            });

            headerRow += '</tr>';
            header += headerRow;

            header += this.buildFirstRow(actualFormValues);
            header += '</thead>';

            return header;
        }

        // dataObj is an array of arrays: [created => [formdata => [value = test1, label = label1], feUser => username1], created2 => [formdata => [value = test2, label = label2], feUser => username2]]
        // build row with that array. first cell is created date. second cell is feUser value. Rest of the cells are formdata values
        buildTablesorterBody(dataObj) {
            let body = '<tbody>';
            let rows = '';
            let that = this;

            Object.entries(dataObj).forEach(([created, row], index, array) => {
                let feUser = row.feUser;
                let formdata = row.formData;
                let unchangedData = row.unchangedData;

                let rowHtml = '<tr>';
                // Add a checkbox in the first cell
                rowHtml += '<td><input type="checkbox" class="main-checkbox" data-created="' + created + '"></td>';
                rowHtml += '<td>' + created + '</td>';
                rowHtml += '<td>' + feUser + '</td>';

                // Add a checkbox in each cell at the end of the cell
                Object.entries(formdata).forEach(([key, fd], fdIndex, fdArray) => {
                    let plainText = that.stripTags(fd.value);
                    let strippedText = that.truncateWithMoreText(plainText);
                    let changedData = fd.name in unchangedData  ? false : true;
                    let tooltip = '';

                    if (strippedText === '') {

                        // If data is unchanged by another form, use empty string
                        strippedText = changedData ? '<span style="font-style: italic;">empty</span>' : '';
                        tooltip = changedData ? '' : ' title="Value of this field was not changed."';

                    } else if (strippedText === '*hide in log*') {

                        // If data is hidden in log, use 'hidden in log'
                        strippedText = changedData ? '<span style="font-style: italic;" title="Value of this field is excluded from the log.">hidden in log</span>' : '';

                        // No checkbox will be used
                        changedData = false;
                    }

                    // Only use checkbox if data has been changed
                    let checkbox = changedData ? that.buildCheckbox(created, fd.name, fd.value) : '';

                    // Compare with next row's value if it exists
                    let comparisonHtml = '';
                    if (index < array.length - 1) {
                        let nextRow = array[index + 1][1];
                        let nextFd = nextRow.formData[key];
                        if (nextFd && fd.value !== nextFd.value && changedData) {
                            strippedText = '<strong>' + strippedText + '</strong>';
                        }

                    } else {
                        strippedText = '<strong>' + strippedText + '</strong>';
                    }

                    rowHtml += '<td' + tooltip + '><div style="display: flex; align-items:flex-start; gap: 5px">' + checkbox + ' ' + strippedText + '</div></td>';
                });

                rowHtml += '</tr>';
                rows += rowHtml;
            });

            body += rows;
            body += '</tbody>';
            return body;
        }

        buildCheckbox(created, name, value) {
            let checkbox = '';
            checkbox += '<input type="checkbox" class="checkbox" data-name="' + name + '" data-created="' + created + '" value="' + value + '">';

            return checkbox;
        }

        buildFirstRow(actualFormValues) {
            let that = this;
            let row = '<tr class="static-row">';
            // Add a checkbox in the first cell
            row += '<th class="filter-false sorter-false"></th>';
            row += '<th class="filter-false sorter-false">Current</th>';
            row += '<th class="filter-false sorter-false"></th>';
            actualFormValues.forEach((value, key) => {
                let plainText = that.stripTags(value);
                let strippedText = that.truncateWithMoreText(plainText);
                if (strippedText === '') {
                    strippedText = '<span style="font-style: italic;">empty</span>';
                }
                row += '<th class="filter-false sorter-false">' + strippedText + '</th>';
            });
            row += '</tr>';

            return row;
        }

        buildTakeBtn() {
            let takeBtn = document.createElement('button');
            takeBtn.setAttribute('id', 'take-btn');
            takeBtn.setAttribute('class', 'btn btn-default');
            takeBtn.innerHTML = 'Restore selected values';
            return takeBtn;
        }


        getCurrentFormValues() {
            // Get all elements of the form which has name given from array formNames
            let form = this.form.$form[0];
            let formNames = this.formNames;
            let formValues = [];
            // iterate over formNames
            Object.values(formNames).forEach(name => {
                let formElement = form.querySelector('[name="' + name + '"]');
                let type = 'input';
                this.formElements[name] = {};

                if (formElement !== null) {
                    let value = '';
                    // Check for type radio
                    if (formElement.getAttribute('type') === 'radio') {
                        formElement = form.querySelector(`[name="${name}"]:checked`);
                        if (formElement === null) {
                            formElement = form.querySelector(`[name="${name}"]`);
                        }
                        type = 'radio';
                    }

                    // Check for codemirror
                    if (formElement.classList.contains('qfq-codemirror')) {
                        type = 'codemirror';
                    }

                    // Check for tinyMce
                    if (formElement.classList.contains('qfq-tinymce')) {
                        type = 'tinymce';
                    }

                    // Single checkbox and typeahead handling. Usually hidden input
                    if (formElement.getAttribute('type') === 'hidden') {

                        // Different between checkbox and typeahead
                        // Get the parent div element and check for first child with class twitter-typeahead
                        const parent = formElement.parentNode;
                        const typeahead = parent.querySelector('.twitter-typeahead');

                        if (typeahead !== null && parent.classList.contains('qfq-form-body') === false) {
                            type = 'typeahead';
                            value = formElement.value;
                        } else {
                            // Get the other element with the same name. Exclude the hidden input (Checkbox)
                            const formElementNew = form.querySelector(`[name="${name}"]:not([type="hidden"])`);

                            if (formElementNew !== null) {
                                type = 'checkbox';

                                if (formElementNew.checked === false) {
                                    value = formElement.value;
                                } else {
                                    value = formElementNew.value;
                                }
                                formElement = formElementNew;
                            } else {
                                // uploads are handled here
                                value = formElement.value;
                            }
                        }
                    } else {
                        value = formElement.value;
                    }

                    this.formElements[name].element = formElement;
                    this.formElements[name].value = value;
                    this.formElements[name].type = type;
                    formValues.push({name: name, value: value});
                } else {
                    // In case of checkboxes
                    formElement = form.querySelectorAll(`[name="${name}[]"]`);
                    if (formElement !== null) {
                        let values = [];
                        formElement.forEach(el => {
                            // Get checkmark element
                            const checkmark = el.nextElementSibling;
                            if (el.checked === true) {
                                values.push(el.value);
                            }
                        });

                        this.formElements[name].element = formElement;
                        this.formElements[name].value = values;
                        this.formElements[name].type = 'checkbox';

                        formValues.push({name: name, value: values});
                    }
                }
            });

            return formValues;
        }

        reorderArray(referenceArray, arrayToReorder) {
            const result = new Map();
            const placeholder = '';

            // Convert arrayToReorder to a Map for easier lookup
            const arrayToReorderMap = new Map(
                arrayToReorder.map(item => [item.name, item.value])
            );

            // Process all keys from the reference array (Map)
            for (const [key, value] of referenceArray) {
                if (arrayToReorderMap.has(key)) {
                    result.set(key, arrayToReorderMap.get(key));
                } else {
                    result.set(key, placeholder);
                }
            }

            return result;
        }

        setFormValues(data) {
            let that = this;
            Object.entries(data).forEach(([name, value]) => {
                if (this.formElements[name]) {
                    const {element, type} = this.formElements[name];

                    switch (type) {
                        case 'radio':
                            const radioButton = element;
                            if (radioButton) {
                                radioButton.Attributes.remove('checked');

                                // Get the radio button with the same name and value
                                const parent = radioButton.closest('div');
                                const radioButtonNew = parent.querySelector(`input[name="${name}"][value="${value}"]`);

                                // Get parent label element
                                const label = radioButtonNew.closest('label');
                                console.log('label: ', label);
                                label.click();
                                radioButtonNew.checked = true;
                            }
                            break;

                        case 'checkbox':
                            if (element instanceof NodeList) {
                                element.forEach(checkbox => {
                                    if (Array.isArray(value)) {
                                        checkbox.checked = value.includes(checkbox.value);
                                    } else if (typeof value === 'string') {
                                        checkbox.checked = value.split(',').includes(checkbox.value);
                                    } else {
                                        checkbox.checked = value == checkbox.value;
                                    }
                                });
                            } else {
                                element.checked = value === element.value;
                            }
                            break;

                        case 'codemirror':
                            if (element) {
                                // Select next sibling element with class CodeMirror
                                const codemirror = element.nextElementSibling.CodeMirror;
                                element.value = value;
                                codemirror.setValue(value);
                            }
                            break;

                        case 'tinymce':
                            if (tinymce && tinymce.get(element.id)) {
                                tinymce.get(element.id).setContent(value);
                            }
                            break;

                        default:
                            element.value = value;
                    }


                    this.formElements[name].value = value;
                }
            });

        }

        makeTdTextBold(checkbox) {
            // Get the parent td element
            const td = checkbox.closest('td');

            const textNode = td.childNodes[0];

            if (textNode && textNode.nodeType === Node.TEXT_NODE) {
                // Create a new strong element
                const strongElement = document.createElement('strong');

                // Set the text content of the strong element
                strongElement.textContent = textNode.textContent.trim();

                // Replace the text node with the strong element
                td.insertBefore(strongElement, textNode);
                td.removeChild(textNode);
            }
        }

        resetBoldTdText(checkboxes) {
            checkboxes.forEach(checkbox => {
                // Get the parent td element
                const td = checkbox.closest('td');

                const strongElement = td.querySelector('strong');

                if (strongElement) {
                    // Create a new text node
                    const textNode = document.createTextNode(strongElement.textContent);

                    // Replace the strong element with the text node
                    td.insertBefore(textNode, strongElement);
                    td.removeChild(strongElement);
                }
            });
        }

        getTableSorterConfigJson() {
            let tablesorterConfig = {
                theme: "bootstrap",
                widthFixed: true,
                headerTemplate: "{content} {icon}",
                dateFormat: "ddmmyyyy",
                widgets: ["uitheme", "filter", "columnSelector", "output"],
                widgetOptions: {
                    filter_columnFilters: true,
                    filter_reset: ".reset",
                    filter_cssFilter: "form-control",
                    filter_saveFilters: true,
                    columnSelector_mediaquery: false,
                    output_delivery: "download",
                    output_saveFileName: "tableExport.csv",
                    output_separator: ";"
                }
            };

            return JSON.stringify(tablesorterConfig);
        }

        truncateWithMoreText(input, maxlen = 30, plaintext = false) {
            if (typeof input !== 'string') {
                return input;
            }

            if (maxlen < 1) {
                maxlen = 1;
            }

            if (input.length > maxlen) {
                const visibleText = input.slice(0, maxlen);
                const hiddenText = input.slice(maxlen);
                if (plaintext) {
                    return `${visibleText}...`;
                } else {
                    return `${visibleText}<span class="qfq-more-text">${hiddenText}</span><button class="btn btn-link" type="button" style="outline-width: 0px;">[...]</button>`;
                }
            } else {
                return input;
            }
        }

        stripTags(input, allowed) {
            if (typeof input !== 'string') {
                return input;
            }
            allowed = (((allowed || "") + "").toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('');
            const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
            return input.replace(tags, function ($0, $1) {
                return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : '';
            });
        }

        // Create modal template for the history view
        createModalTemplate() {
            let modal = document.createElement('div');
            modal.setAttribute('class', 'modal fade qfq-history-view');
            modal.setAttribute('role', 'dialog');

            let modalDialog = document.createElement('div');
            modalDialog.setAttribute('class', 'modal-dialog');

            let modalContent = document.createElement('div');
            modalContent.setAttribute('class', 'modal-content');

            let modalHeader = document.createElement('div');
            modalHeader.setAttribute('class', 'modal-header');

            let modalTitle = document.createElement('h2');
            modalTitle.setAttribute('class', 'modal-title');
            modalTitle.style.display = 'contents';
            let titleContent = '';
            if (this.modalTitle) {
                titleContent = 'History: ' + this.modalTitle;
            } else {
                titleContent = 'History';
            }
            modalTitle.textContent = titleContent;

            let modalCloseBtn = document.createElement('button');
            modalCloseBtn.setAttribute('type', 'button');
            modalCloseBtn.setAttribute('class', 'close');
            modalCloseBtn.setAttribute('data-dismiss', 'modal');

            let modalCloseBtnSpan = document.createElement('span');
            modalCloseBtnSpan.setAttribute('aria-hidden', 'true');
            modalCloseBtnSpan.innerHTML = '&times;';

            let modalBody = document.createElement('div');
            modalBody.setAttribute('class', 'modal-body');

            modalCloseBtn.appendChild(modalCloseBtnSpan);
            modalHeader.appendChild(modalTitle);
            modalHeader.appendChild(modalCloseBtn);
            modalContent.appendChild(modalHeader);
            modalContent.appendChild(modalBody);
            modalDialog.appendChild(modalContent);
            modal.appendChild(modalDialog);

            return modal;
        }

        buildSelectedOutput(data) {
            let labels = this.formLabelData;
            let output = 'Restore following selected values:\n';
            output += '---------------------------------\n';
            Object.entries(data).forEach(([name, value]) => {
                let strippedValue = this.stripTags(value);
                let truncatedValue = this.truncateWithMoreText(strippedValue, 80, true);

                let displayLabel = labels.get(name) || ('(' + name + ')');

                output += '- ' + displayLabel + ': ' + truncatedValue + '\n';
            });
            output += '---------------------------------\n';

            return output;
        }


    }

    n.qfqHistory = qfqHistory;

})(QfqNS.Helper);
/**
 * QFQ Inline Filter - Handles inline filter elements for faceted filtering
 *
 * Usage:
 * - Add class "qfq-inline-filter" to input elements
 * - Add data-target attribute with CSS selector of target container
 * - Add data-param attribute with parameter name
 * - SIP is retrieved from window.qfqFilterSips[target] (generated at page render end)
 *
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 */

/* global console */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * QfqInlineFilter Class
     * Handles inline filter elements with data-sip
     */
    class QfqInlineFilter {
        constructor() {
            this.debounceTimers = {};
            this.apiPath = 'typo3conf/ext/qfq/Classes/Api/';
            this.initialValues = {};  // Store initial filter values for reset
            this.loadingTargets = {};  // Track which targets are currently loading (prevent loops)
            this.initialized = false;  // Prevent double initialization
        }

        /**
         * Check if element is a multi-select (original, transformed wrapper, or multi-checkbox)
         * @param {HTMLElement} element
         * @returns {boolean}
         */
        isMultiSelect(element) {
            return element.classList.contains('qfq-multi-select-container') ||
                   element.classList.contains('qfq-inline-multiselect-wrapper') ||
                   element.classList.contains('qfq-inline-multi-checkbox');
        }

        /**
         * Check if element is a transformed multi-select wrapper
         * @param {HTMLElement} element
         * @returns {boolean}
         */
        isTransformedMultiSelect(element) {
            return element.classList.contains('qfq-inline-multiselect-wrapper');
        }

        /**
         * Check if element is a radio button container
         * Supports both regular radio containers and Bootstrap btn-group style
         * @param {HTMLElement} element
         * @returns {boolean}
         */
        isRadioContainer(element) {
            return element.classList.contains('qfq-inline-radio') ||
                   (element.classList.contains('btn-group') && element.querySelector('input[type="radio"]'));
        }

        /**
         * Get value from a filter element (handles regular inputs, checkboxes, radios, and multi-selects)
         * @param {HTMLElement} filter
         * @returns {string}
         */
        getFilterValue(filter) {
            if (this.isMultiSelect(filter)) {
                // For transformed wrapper, use the dropdown list checkboxes
                var selector = this.isTransformedMultiSelect(filter) ?
                    '.qfq-inline-multiselect-list input[type="checkbox"]:checked' :
                    'input[type="checkbox"]:checked';
                var checkboxes = filter.querySelectorAll(selector);
                var values = [];
                checkboxes.forEach(function(cb) {
                    values.push(cb.value);
                });
                return values.join(',');
            }

            // Radio container: return value of checked radio
            if (this.isRadioContainer(filter)) {
                var checkedRadio = filter.querySelector('input[type="radio"]:checked');
                return checkedRadio ? checkedRadio.value : '';
            }

            // Single checkbox: return checked value or unchecked value from data attribute
            if (filter.type === 'checkbox') {
                if (filter.checked) {
                    return filter.value;
                }
                // Use data-unchecked attribute for unchecked value
                return filter.dataset.unchecked || '';
            }

            return filter.value || '';
        }

        /**
         * Set value on a filter element (handles regular inputs, checkboxes, radios, and multi-selects)
         * @param {HTMLElement} filter
         * @param {string} value - Comma-separated values for multi-select
         */
        setFilterValue(filter, value) {
            var self = this;
            if (this.isMultiSelect(filter)) {
                var values = value ? value.split(',') : [];
                var selector = this.isTransformedMultiSelect(filter) ?
                    '.qfq-inline-multiselect-list input[type="checkbox"]' :
                    'input[type="checkbox"]';
                var checkboxes = filter.querySelectorAll(selector);
                checkboxes.forEach(function(cb) {
                    cb.checked = values.indexOf(cb.value) !== -1;
                });
                // Sync with original container and update trigger
                if (this.isTransformedMultiSelect(filter)) {
                    this.syncMultiSelectState(filter);
                }
            } else if (this.isRadioContainer(filter)) {
                // Radio container: check the radio with matching value
                var radios = filter.querySelectorAll('input[type="radio"]');
                radios.forEach(function(radio) {
                    radio.checked = (radio.value === value);
                });
                // Update Bootstrap btn-group "active" class on labels
                if (filter.classList.contains('btn-group')) {
                    var labels = filter.querySelectorAll('label');
                    labels.forEach(function(label) {
                        var radio = label.querySelector('input[type="radio"]');
                        if (radio && radio.checked) {
                            label.classList.add('active');
                        } else {
                            label.classList.remove('active');
                        }
                    });
                }
            } else if (filter.type === 'checkbox') {
                // Single checkbox: check if value matches the checked value
                filter.checked = (value === filter.value);
            } else {
                filter.value = value;
            }
        }

        /**
         * Reset a filter element (handles regular inputs, checkboxes, radios, and multi-selects)
         * @param {HTMLElement} filter
         */
        resetFilter(filter) {
            if (this.isMultiSelect(filter)) {
                var selector = this.isTransformedMultiSelect(filter) ?
                    '.qfq-inline-multiselect-list input[type="checkbox"]' :
                    'input[type="checkbox"]';
                var checkboxes = filter.querySelectorAll(selector);
                checkboxes.forEach(function(cb) {
                    cb.checked = false;
                });
                // Sync with original container and update trigger
                if (this.isTransformedMultiSelect(filter)) {
                    this.syncMultiSelectState(filter);
                }
            } else if (this.isRadioContainer(filter)) {
                // Radio container: uncheck all radios
                var radios = filter.querySelectorAll('input[type="radio"]');
                radios.forEach(function(radio) {
                    radio.checked = false;
                });
                // Remove Bootstrap btn-group "active" class from labels
                if (filter.classList.contains('btn-group')) {
                    var labels = filter.querySelectorAll('label.active');
                    labels.forEach(function(label) {
                        label.classList.remove('active');
                    });
                }
            } else if (filter.type === 'checkbox') {
                // Single checkbox: uncheck it
                filter.checked = false;
            } else if (filter.tagName === 'SELECT') {
                filter.selectedIndex = 0;
            } else {
                filter.value = '';
            }
        }

        /**
         * Initialize filter functionality
         */
        init() {
            // Prevent double initialization (can happen with AJAX-loaded content)
            if (this.initialized) {
                console.warn('QfqInlineFilter: Already initialized, skipping');
                return;
            }
            this.initialized = true;

            this.transformMultiSelectFilters();
            this.storeInitialValues();  // Store initial values before URL overwrites them
            this.bindEvents();
            this.addResetButtons();
            this.loadFiltersFromUrl();

            // Always trigger initial content load (with or without filter values)
            this.triggerAllFilters();
        }

        /**
         * Store initial filter values for reset functionality
         * Called after transform but before URL loading
         */
        storeInitialValues() {
            var self = this;
            var filters = document.querySelectorAll('.qfq-inline-filter');

            filters.forEach(function(filter) {
                var paramName = filter.dataset.param;
                if (paramName) {
                    self.initialValues[paramName] = self.getFilterValue(filter);
                }
            });
        }

        /**
         * Transform multi-select containers into dropdown-style filters
         */
        transformMultiSelectFilters() {
            var self = this;
            var multiSelects = document.querySelectorAll('.qfq-multi-select-container.qfq-inline-filter');

            multiSelects.forEach(function(container) {
                // Skip if already transformed
                if (container.dataset.transformed) return;
                container.dataset.transformed = 'true';

                // Create wrapper
                var wrapper = document.createElement('div');
                wrapper.className = 'qfq-inline-multiselect-wrapper';

                // Get label from data attribute
                var labelText = container.dataset.label || '';
                var triggerText = labelText || 'All';

                // Create trigger button
                var trigger = document.createElement('button');
                trigger.type = 'button';
                trigger.className = 'form-control qfq-inline-multiselect-trigger';
                trigger.innerHTML = '<span class="qfq-inline-multiselect-text">' + triggerText + '</span> <span class="caret"></span>';

                // Create dropdown panel
                var dropdown = document.createElement('div');
                dropdown.className = 'qfq-inline-multiselect-dropdown';

                // Create header with toggle checkbox and label
                var header = document.createElement('div');
                header.className = 'qfq-inline-multiselect-header';

                // Build header: toggle checkbox + label
                var toggleCheckbox = document.createElement('input');
                toggleCheckbox.type = 'checkbox';
                toggleCheckbox.className = 'qfq-inline-select-toggle';

                var titleSpan = document.createElement('span');
                titleSpan.className = 'qfq-inline-multiselect-title';
                titleSpan.textContent = labelText;

                header.appendChild(toggleCheckbox);
                header.appendChild(titleSpan);

                // Create list container
                var listContainer = document.createElement('div');
                listContainer.className = 'qfq-inline-multiselect-list';

                // Move checkboxes to list, restructure for better layout
                var checkboxes = container.querySelectorAll('label.checkbox');
                checkboxes.forEach(function(label) {
                    var item = document.createElement('div');
                    item.className = 'qfq-inline-multiselect-item';

                    var checkbox = label.querySelector('input[type="checkbox"]');
                    var textContent = '';
                    var countText = '';

                    // Extract text and count
                    label.childNodes.forEach(function(node) {
                        if (node.nodeType === Node.TEXT_NODE) {
                            var text = node.textContent.trim();
                            var match = text.match(/^(.+?)\s*\((\d+)\)$/);
                            if (match) {
                                textContent = match[1];
                                countText = match[2];
                            } else {
                                textContent = text;
                            }
                        }
                    });

                    // Build new item structure
                    var newCheckbox = checkbox.cloneNode(true);
                    var labelText = document.createElement('span');
                    labelText.className = 'qfq-inline-multiselect-label';
                    labelText.textContent = textContent;

                    var countSpan = document.createElement('span');
                    countSpan.className = 'qfq-inline-multiselect-count';
                    countSpan.textContent = countText;
                    if (checkbox.dataset.count !== undefined) {
                        countSpan.dataset.count = checkbox.dataset.count;
                    }

                    item.appendChild(newCheckbox);
                    item.appendChild(labelText);
                    item.appendChild(countSpan);
                    listContainer.appendChild(item);
                });

                // Assemble dropdown
                dropdown.appendChild(header);
                dropdown.appendChild(listContainer);

                // Copy data attributes to wrapper (SIP comes from window.qfqFilterSips)
                wrapper.dataset.target = container.dataset.target;
                wrapper.dataset.param = container.dataset.param;
                wrapper.dataset.baseUrl = container.dataset.baseUrl || '';
                wrapper.dataset.label = container.dataset.label || '';
                if (container.dataset.countSip) {
                    wrapper.dataset.countSip = container.dataset.countSip;
                }

                // Replace container with new structure
                container.parentNode.insertBefore(wrapper, container);
                wrapper.appendChild(trigger);
                wrapper.appendChild(dropdown);
                container.style.display = 'none';
                container.classList.remove('qfq-inline-filter'); // Remove from original to avoid duplicate selectors
                wrapper.appendChild(container); // Keep original for form submission

                // Add wrapper class for CSS targeting
                wrapper.classList.add('qfq-inline-filter');

                // Event: Toggle dropdown
                trigger.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    dropdown.classList.toggle('open');
                });

                // Event: Close on outside click
                document.addEventListener('click', function(e) {
                    if (!wrapper.contains(e.target)) {
                        dropdown.classList.remove('open');
                    }
                });

                // Event: Toggle all checkboxes via header checkbox
                toggleCheckbox.addEventListener('change', function() {
                    var isChecked = toggleCheckbox.checked;
                    listContainer.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
                        cb.checked = isChecked;
                    });
                    self.syncMultiSelectState(wrapper);
                    self.handleFilterChange(wrapper);
                });

                // Event: Checkbox change
                listContainer.addEventListener('change', function(e) {
                    if (e.target.type === 'checkbox') {
                        self.syncMultiSelectState(wrapper);
                        self.handleFilterChange(wrapper);
                    }
                });

                // Initial state update
                self.updateToggleCheckboxState(wrapper);
                self.updateMultiSelectTrigger(wrapper);
            });
        }

        /**
         * Sync transformed multi-select state with original container
         */
        syncMultiSelectState(wrapper) {
            var originalContainer = wrapper.querySelector('.qfq-multi-select-container');
            var listContainer = wrapper.querySelector('.qfq-inline-multiselect-list');

            if (!originalContainer || !listContainer) return;

            // Sync checkbox states
            var newCheckboxes = listContainer.querySelectorAll('input[type="checkbox"]');
            var originalCheckboxes = originalContainer.querySelectorAll('input[type="checkbox"]');

            newCheckboxes.forEach(function(newCb, index) {
                if (originalCheckboxes[index]) {
                    originalCheckboxes[index].checked = newCb.checked;
                }
            });

            // Update toggle checkbox state (checked/unchecked/indeterminate)
            this.updateToggleCheckboxState(wrapper);

            this.updateMultiSelectTrigger(wrapper);
        }

        /**
         * Update toggle checkbox state based on list checkbox states
         * @param {HTMLElement} wrapper
         */
        updateToggleCheckboxState(wrapper) {
            var toggleCheckbox = wrapper.querySelector('.qfq-inline-select-toggle');
            var listContainer = wrapper.querySelector('.qfq-inline-multiselect-list');

            if (!toggleCheckbox || !listContainer) return;

            var checkboxes = listContainer.querySelectorAll('input[type="checkbox"]');
            var total = checkboxes.length;
            var checked = listContainer.querySelectorAll('input[type="checkbox"]:checked').length;

            if (checked === 0) {
                toggleCheckbox.checked = false;
                toggleCheckbox.indeterminate = false;
            } else if (checked === total) {
                toggleCheckbox.checked = true;
                toggleCheckbox.indeterminate = false;
            } else {
                toggleCheckbox.checked = false;
                toggleCheckbox.indeterminate = true;
            }
        }

        /**
         * Update multi-select trigger button text
         */
        updateMultiSelectTrigger(wrapper) {
            var trigger = wrapper.querySelector('.qfq-inline-multiselect-trigger');
            var textSpan = trigger.querySelector('.qfq-inline-multiselect-text');
            var listContainer = wrapper.querySelector('.qfq-inline-multiselect-list');
            var label = wrapper.dataset.label || 'All';

            var checked = listContainer.querySelectorAll('input[type="checkbox"]:checked');
            var total = listContainer.querySelectorAll('input[type="checkbox"]').length;

            if (checked.length === 0) {
                // Nothing selected - show label
                textSpan.textContent = label;
                textSpan.className = 'qfq-inline-multiselect-text';
            } else {
                // Show selected labels (partial or all selected)
                var labels = [];
                checked.forEach(function(cb) {
                    var item = cb.closest('.qfq-inline-multiselect-item');
                    var labelEl = item.querySelector('.qfq-inline-multiselect-label');
                    if (labelEl) labels.push(labelEl.textContent);
                });

                if (labels.length <= 2) {
                    textSpan.textContent = labels.join(', ');
                } else {
                    textSpan.textContent = labels.slice(0, 2).join(', ') + ' +' + (labels.length - 2);
                }
                textSpan.className = 'qfq-inline-multiselect-text has-selection';
            }
        }

        /**
         * Add reset buttons to filter-panel containers
         */
        addResetButtons() {
            var self = this;
            var filterPanels = document.querySelectorAll('.filter-panel');

            filterPanels.forEach(function(panel) {
                // Check if reset button already exists
                if (panel.querySelector('.qfq-inline-reset')) {
                    return;
                }

                // Create reset button
                var resetBtn = document.createElement('button');
                resetBtn.type = 'button';
                resetBtn.className = 'btn btn-default qfq-inline-reset';
                resetBtn.textContent = 'Reset';

                // Handle click
                resetBtn.addEventListener('click', function() {
                    self.resetFilters(panel);
                });

                // Append to filter-panel
                panel.appendChild(resetBtn);
            });
        }

        /**
         * Reset all filters in a panel to their initial values and reload content
         * @param {HTMLElement} panel - The filter-panel container
         */
        resetFilters(panel) {
            var self = this;
            var filters = panel.querySelectorAll('.qfq-inline-filter');
            var target = null;
            var initialParams = {};

            // Reset all filter values to their initial state
            filters.forEach(function(filter) {
                var paramName = filter.dataset.param;
                var initialValue = self.initialValues[paramName] || '';

                // Restore initial value
                self.setFilterValue(filter, initialValue);

                // Collect non-empty initial values for URL
                if (initialValue !== '') {
                    initialParams[paramName] = initialValue;
                }

                // Get target from first filter
                if (!target) {
                    target = filter.dataset.target;
                }
            });

            // Update URL with initial parameters (or clear if all empty)
            this.updateUrl(initialParams);

            // Reload content
            if (target) {
                this.triggerFiltersForTarget(target);
            }
        }

        /**
         * Bind event listeners
         */
        bindEvents() {
            var self = this;

            // Bind events directly to filter elements (not document-level)
            // This avoids performance issues with tablesorter events bubbling up
            this.bindFilterEvents();

            // Browser back/forward
            window.addEventListener('popstate', function() {
                self.loadFiltersFromUrl();
                self.triggerAllFilters();
            });
        }

        /**
         * Bind events directly to filter elements
         * Called on init and can be called again for dynamically added filters
         */
        bindFilterEvents() {
            var self = this;
            var filters = document.querySelectorAll('.qfq-inline-filter:not([data-events-bound])');

            filters.forEach(function(filter) {
                // Mark as bound to prevent double-binding
                filter.dataset.eventsBound = 'true';

                // Change event for selects, checkboxes, radios
                filter.addEventListener('change', function(e) {
                    // For containers with checkboxes/radios, the event target is the input
                    // but the filter is the container - already handled correctly
                    self.handleFilterChange(filter);
                });

                // Input event for text fields (debounced)
                if (filter.type === 'text' || filter.type === 'search') {
                    filter.addEventListener('input', function(e) {
                        self.handleFilterInputDebounced(filter);
                    });
                }

                // Bootstrap btn-group click handler
                if (filter.classList.contains('btn-group')) {
                    filter.addEventListener('click', function(e) {
                        var label = e.target.closest('label');
                        if (label) {
                            // Small delay to let Bootstrap update the checked state
                            setTimeout(function() {
                                self.handleFilterChange(filter);
                            }, 10);
                        }
                    });
                }
            });
        }

        /**
         * Handle filter change (immediate)
         * @param {HTMLElement} element - Changed filter element
         */
        handleFilterChange(element) {
            var target = element.dataset.target;
            this.triggerFiltersForTarget(target);
        }

        /**
         * Handle filter input with debounce (for text fields)
         * @param {HTMLElement} element - Changed filter element
         */
        handleFilterInputDebounced(element) {
            var self = this;
            var target = element.dataset.target;

            clearTimeout(this.debounceTimers[target]);
            this.debounceTimers[target] = setTimeout(function() {
                self.triggerFiltersForTarget(target);
            }, 300);
        }

        /**
         * Collect all filters for a target and trigger AJAX request
         * @param {string} target - CSS selector of target container
         */
        triggerFiltersForTarget(target) {
            var self = this;
            var filters = document.querySelectorAll('.qfq-inline-filter[data-target="' + target + '"]');
            var params = {};
            var baseUrl = null;

            filters.forEach(function(filter) {
                var paramName = filter.dataset.param;
                var value = self.getFilterValue(filter);

                if (value !== '') {
                    params[paramName] = value;
                }

                // Get baseUrl from first filter
                if (baseUrl === null) {
                    baseUrl = filter.dataset.baseUrl || '';
                }
            });

            // Get SIP from global registry (generated at end of page render)
            var sip = window.qfqFilterSips && window.qfqFilterSips[target];

            if (sip) {
                this.updateUrl(params);
                this.loadContent(sip, params, target, baseUrl);
            }
        }

        /**
         * Load content via AJAX
         *
         * @param {string} sip - SIP token for API call
         * @param {object} params - Filter parameters
         * @param {string} targetSelector - CSS selector of target container
         * @param {string} baseUrl - Base URL from data-base-url attribute
         */
        loadContent(sip, params, targetSelector, baseUrl) {
            var self = this;

            // Prevent concurrent requests for the same target (loop prevention)
            if (this.loadingTargets[targetSelector]) {
                console.warn('QfqInlineFilter: Request already in progress for', targetSelector);
                return;
            }

            var targetElement = document.querySelector(targetSelector);
            if (!targetElement) {
                console.warn('QfqInlineFilter: Target element not found:', targetSelector);
                return;
            }

            // Set loading lock
            this.loadingTargets[targetSelector] = true;
            targetElement.classList.add('qfq-inline-loading');

            // Build API URL with baseUrl
            var apiUrl = baseUrl + this.apiPath + 'dataReport.php?s=' + sip;

            // Build request body with filter params
            // Note: allowedValues for validation is included in the combined SIP (tamper-proof)
            var requestBody = new URLSearchParams(params);

            fetch(apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: requestBody
            })
            .then(function(response) {
                if (!response.ok) {
                    throw new Error('HTTP error! status: ' + response.status);
                }
                return response.text();
            })
            .then(function(responseText) {
                targetElement.classList.remove('qfq-inline-loading');

                // Check if response is JSON (error) or HTML (success)
                try {
                    var jsonResponse = JSON.parse(responseText);
                    // If we get here, it's JSON - likely an error response
                    if (jsonResponse.status === 'error') {
                        self.showError(jsonResponse.message);
                        self.loadingTargets[targetSelector] = false;
                        return;
                    }
                } catch (e) {
                    // Not JSON, treat as HTML content (success case)
                }

                // Success: update content
                targetElement.innerHTML = responseText;
                self.reinitTablesorter(targetElement);
                // Bind events to any new filter elements in loaded content
                self.bindFilterEvents();
                // Update counts after content load (if enabled)
                self.refreshCounts(targetSelector, params);

                // Release loading lock
                self.loadingTargets[targetSelector] = false;
            })
            .catch(function(err) {
                targetElement.classList.remove('qfq-inline-loading');
                self.showError('Request failed: ' + err.message);
                console.error('QfqInlineFilter error:', err);
                // Release loading lock
                self.loadingTargets[targetSelector] = false;
            });
        }

        /**
         * Show error message using QfqNS.Alert
         * @param {string} message - Error message to display
         */
        showError(message) {
            if (typeof QfqNS !== 'undefined' && QfqNS.Alert) {
                var alert = new QfqNS.Alert({
                    message: message,
                    type: 'error',
                    modal: false
                });
                alert.show();
            } else {
                console.error('QfqInlineFilter:', message);
            }
        }

        /**
         * Refresh counts for all filters targeting the same container (batch request)
         * Uses first filter's countSip as master SIP (triggers FILTER_COUNT_MODE),
         * all countSips are passed in JSON body (similar to syncByRule pattern)
         *
         * @param {string} targetSelector - CSS selector of target container
         * @param {object} params - Current filter parameters
         */
        refreshCounts(targetSelector, params) {
            var self = this;
            var filters = document.querySelectorAll('.qfq-inline-filter[data-target="' + targetSelector + '"]');

            // Collect all filters with countSip
            var filterMap = {};  // paramName -> {element, sip}
            var baseUrl = '';
            var masterSip = '';

            filters.forEach(function(filter) {
                var countSip = filter.dataset.countSip;
                if (!countSip) return;

                var paramName = filter.dataset.param;
                filterMap[paramName] = {
                    element: filter,
                    sip: countSip
                };

                // Add updating class for visual feedback
                filter.classList.add('qfq-count-updating');

                // Use first filter's SIP as master and get baseUrl
                if (!masterSip) {
                    masterSip = countSip;
                    baseUrl = filter.dataset.baseUrl || '';
                }
            });

            // No filters with countSip found
            if (!masterSip) return;

            // Build batch request: use master SIP in URL, all countSips in JSON body
            var countSips = {};
            Object.keys(filterMap).forEach(function(paramName) {
                countSips[paramName] = filterMap[paramName].sip;
            });

            // Master SIP triggers FILTER_COUNT_MODE, countSips in body for batch processing
            var apiUrl = baseUrl + self.apiPath + 'load.php?s=' + masterSip;

            fetch(apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    countSips: countSips,
                    filterParams: params
                })
            })
            .then(function(response) { return response.json(); })
            .then(function(data) {
                // Remove updating class from all filters
                Object.keys(filterMap).forEach(function(paramName) {
                    filterMap[paramName].element.classList.remove('qfq-count-updating');
                });

                // API returns {status: "success", filter-counts: {paramName: {...}, ...}}
                if (data.status === 'success' && data['filter-counts']) {
                    Object.keys(filterMap).forEach(function(paramName) {
                        var counts = data['filter-counts'][paramName];
                        if (counts) {
                            self.updateFilterCounts(filterMap[paramName].element, counts);
                        }
                    });
                }
            })
            .catch(function(err) {
                // Remove updating class on error
                Object.keys(filterMap).forEach(function(paramName) {
                    filterMap[paramName].element.classList.remove('qfq-count-updating');
                });
                console.warn('QfqInlineFilter: Count refresh failed:', err);
            });
        }

        /**
         * Update counts in filter UI
         * @param {HTMLElement} filter - Filter element to update
         * @param {object} counts - Count data from server
         */
        updateFilterCounts(filter, counts) {
            var self = this;
            var paramName = filter.dataset.param;

            if (self.isTransformedMultiSelect(filter)) {
                // Update transformed multi-select dropdown items
                var items = filter.querySelectorAll('.qfq-inline-multiselect-item');
                items.forEach(function(item) {
                    var checkbox = item.querySelector('input[type="checkbox"]');
                    var countSpan = item.querySelector('.qfq-inline-multiselect-count');
                    if (checkbox && countSpan && counts[checkbox.value] !== undefined) {
                        countSpan.textContent = counts[checkbox.value];
                        checkbox.dataset.count = counts[checkbox.value];
                    }
                });
            } else if (self.isMultiSelect(filter)) {
                // Update original multi-select checkbox labels
                var checkboxes = filter.querySelectorAll('input[type="checkbox"]');
                checkboxes.forEach(function(checkbox) {
                    var value = checkbox.value;
                    if (value && counts[value] !== undefined) {
                        var label = checkbox.parentElement;
                        if (label && label.tagName === 'LABEL') {
                            // Get text content without the checkmark span
                            var textNode = Array.from(label.childNodes).find(function(node) {
                                return node.nodeType === Node.TEXT_NODE;
                            });
                            if (textNode) {
                                var text = textNode.textContent.replace(/\s*\(\d+\)$/, '');
                                textNode.textContent = text + ' (' + counts[value] + ')';
                            }
                        }
                        checkbox.dataset.count = counts[value];
                    }
                });
            } else if (filter.tagName === 'SELECT') {
                // Update select options
                Array.from(filter.options).forEach(function(option) {
                    var value = option.value;
                    if (value && counts[value] !== undefined) {
                        var label = option.textContent.replace(/\s*\(\d+\)$/, '');
                        option.textContent = label + ' (' + counts[value] + ')';
                        option.dataset.count = counts[value];
                    }
                });
            } else if (self.isRadioContainer(filter)) {
                // Update radio button labels
                var radios = filter.querySelectorAll('input[type="radio"]');
                radios.forEach(function(radio) {
                    var value = radio.value;
                    if (value && counts[value] !== undefined) {
                        var label = radio.parentElement;
                        if (label && label.tagName === 'LABEL') {
                            // Get text content without the checkmark span
                            var textNode = Array.from(label.childNodes).find(function(node) {
                                return node.nodeType === Node.TEXT_NODE;
                            });
                            if (textNode) {
                                var text = textNode.textContent.replace(/\s*\(\d+\)$/, '');
                                textNode.textContent = text + ' (' + counts[value] + ')';
                            }
                        }
                        radio.dataset.count = counts[value];
                    }
                });
            } else if (filter.type === 'checkbox' || filter.type === 'text' || filter.type === 'search') {
                // Update count badge for checkbox or text input
                var badge = document.querySelector('.qfq-inline-count-badge[data-count-for="' + paramName + '"]');
                if (badge && counts.total !== undefined) {
                    // Checkbox shows count in parentheses, text input shows plain number
                    badge.textContent = filter.type === 'checkbox' ? '(' + counts.total + ')' : counts.total;
                    filter.dataset.count = counts.total;
                }
            }
        }

        /**
         * Update URL with filter parameters
         * @param {object} params - Filter parameters
         */
        updateUrl(params) {
            var urlParams = new URLSearchParams();
            Object.keys(params).forEach(function(key) {
                if (params[key] !== '') {
                    urlParams.set(key, params[key]);
                }
            });

            var newUrl = window.location.pathname;
            if (urlParams.toString()) {
                newUrl += '?' + urlParams.toString();
            }
            history.pushState(params, '', newUrl);
        }

        /**
         * Load filter values from URL
         */
        loadFiltersFromUrl() {
            var self = this;
            var urlParams = new URLSearchParams(window.location.search);

            urlParams.forEach(function(value, key) {
                var filter = document.querySelector('.qfq-inline-filter[data-param="' + key + '"]');
                if (filter) {
                    self.setFilterValue(filter, value);
                }
            });
        }

        /**
         * Trigger all filters (used after loading from URL)
         */
        triggerAllFilters() {
            var self = this;
            var targets = new Set();

            document.querySelectorAll('.qfq-inline-filter').forEach(function(filter) {
                targets.add(filter.dataset.target);
            });

            targets.forEach(function(target) {
                self.triggerFiltersForTarget(target);
            });
        }

        /**
         * Re-initialize tablesorter for dynamically loaded content
         * @param {HTMLElement} container - Container with new content
         */
        reinitTablesorter(container) {
            var tables = container.querySelectorAll('.tablesorter');

            if (tables.length > 0 && typeof QfqNS !== 'undefined' && QfqNS.TablesorterController) {
                var controller = new QfqNS.TablesorterController();
                tables.forEach(function(table, i) {
                    if (!table.classList.contains('tablesorter-initialized')) {
                        controller.setup($(table), 'filter-' + i);
                        table.classList.add('tablesorter-initialized');
                    }
                });
            }
        }
    }

    // Export class (initialization happens in Main.js)
    n.Helper.QfqInlineFilter = QfqInlineFilter;

})(QfqNS);

/**
 * QFQ InputCollectionJson Configuration Editor
 * Transforms textarea.qfq-icj elements into a user-friendly configuration editor.
 *
 * @author QFQ Team
 * @version 1.0
 */

/* global console */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * QfqConfigTemplate - A lightweight FlexForm-like configuration editor
     *
     * Self-contained with no external CSS dependencies.
     *
     * Usage:
     *   <textarea class="qfq-icj" name="config" data-icj-template="# cat=...">
     *     {"category": {"varName": {"value": "..."}}}
     *   </textarea>
     *
     * - Template in data-icj-template attribute (plain string)
     * - JSON values in textarea content (this gets submitted with the form)
     * - On value change, textarea content is auto-updated
     *
     *
     * Supports two data modes:
     *
     * 1. WITH TEMPLATE: Template defines structure, JSON only needs values
     *    JSON: { "category": { "varName": { "value": "..." } } }
     *    If JSON is empty, values are auto-generated from template defaults.
     *
     * 2. WITHOUT TEMPLATE (JSON-only): Use reserved keys for metadata
     *    JSON: {
     *      "category": {
     *        "varName": {
     *          "value": "...",
     *          "__label": "Human Label",     // optional, fallback: key name
     *          "__cat": "subcategory",       // optional
     *          "__desc": "Description text", // optional
     *          "__type": "select",           // optional: text|select|checkbox|number|textarea
     *          "__options": ["a", "b"],      // required if __type is select
     *          "__required": true,           // optional: mark field as required
     *          "__readonly": true            // optional: mark field as readonly
     *        }
     *      }
     *    }
     */
    class QfqConfigTemplate {

        constructor(config) {
            this.categories = new Map();
            this.variables = new Map();
            this.container = null;
            this.textarea = null;
            this.values = {};
            this.template = '';
            this.mode = 'editor';
            this.wrapper = null;
            this.hasTemplate = false;
            this.loadingEl = null;
            this.validationError = null;
            this.isDisabled = false;
            this.isRequired = false;
            this.requiredError = '';
            this.rawContent = '';

            if (config instanceof HTMLTextAreaElement) {
                this._initFromTextarea(config);
            } else if (typeof config === 'object' && config !== null) {
                this._initFromConfig(config);
            }
        }

        _initFromTextarea(textarea) {
            this.textarea = textarea;

            // Check if disabled/readonly
            this.isDisabled = textarea.disabled || textarea.dataset.disabled === 'yes';

            // Check if required
            this.isRequired = textarea.dataset.required === 'yes' || textarea.hasAttribute('required');
            this.requiredError = textarea.dataset.requiredError || '';

            // Template from data-icj-template attribute (plain string)
            // Note: Do NOT convert \n to actual newlines here - values may contain literal \n
            // that should be preserved. The template uses actual line breaks as separators.
            var templateAttr = textarea.dataset.icjTemplate;
            if (templateAttr) {
                this.template = templateAttr.trim();
            }
            this.hasTemplate = this.template.length > 0;

            // JSON values from textarea content
            var icjContent = (textarea.value || '').trim();
            this.rawContent = icjContent;
            if (icjContent) {
                try {
                    this.values = JSON.parse(icjContent);
                } catch (e) {
                    console.error('QfqConfigTemplate: Invalid JSON in textarea content', e);
                    this.values = {};
                    this.validationError = 'Invalid JSON syntax.';
                }
            }

            // Handle empty/missing JSON
            if (!this.validationError) {
                var icjIsEmpty = Object.keys(this.values).length === 0;

                if (this.hasTemplate) {
                    // With template: parse template first, then validate/generate JSON
                    this._parseTemplate();

                    if (icjIsEmpty) {
                        // Auto-generate JSON from template defaults
                        this._generateIcjFromTemplate();
                        // Update textarea with generated JSON
                        if (this.textarea) {
                            this.textarea.value = this.getValuesIcj();
                        }
                    } else {
                        // Validate existing JSON structure
                        this.validationError = this._validateIcjStructure(this.values);
                    }
                } else {
                    // JSON-only mode: validate JSON
                    if (icjIsEmpty) {
                        this.validationError = 'No template and no JSON data provided.';
                    } else {
                        this.validationError = this._validateIcjStructure(this.values);
                        if (!this.validationError) {
                            this._parseIcjOnly();
                        }
                    }
                }
            }
        }

        _initFromConfig(config) {
            this.template = (config.template || '').trim();
            this.values = config.values || {};
            this.container = config.container || null;
            this.mode = config.mode || 'editor';
            this.hasTemplate = this.template.length > 0;

            var icjIsEmpty = Object.keys(this.values).length === 0;

            if (this.hasTemplate) {
                this._parseTemplate();

                if (icjIsEmpty) {
                    this._generateIcjFromTemplate();
                } else {
                    this.validationError = this._validateIcjStructure(this.values);
                }
            } else {
                if (icjIsEmpty) {
                    this.validationError = 'No template and no JSON data provided.';
                } else {
                    this.validationError = this._validateIcjStructure(this.values);
                    if (!this.validationError) {
                        this._parseIcjOnly();
                    }
                }
            }
        }

        /**
         * Generate JSON values from parsed template defaults
         */
        _generateIcjFromTemplate() {
            this.values = {};

            for (var entry of this.variables) {
                var name = entry[0];
                var variable = entry[1];
                // Handle null, undefined, or empty string category
                var category = variable.category || '';

                if (!this.values[category]) {
                    this.values[category] = {};
                }

                this.values[category][name] = { value: variable.defaultValue };
            }
        }

        /**
         * Validate JSON structure
         * Expected: { "category": { "varName": { "value": "..." } } }
         */
        _validateIcjStructure(json) {
            if (typeof json !== 'object' || json === null || Array.isArray(json)) {
                return 'JSON must be an object.';
            }

            if (Object.keys(json).length === 0) {
                // Empty JSON is handled separately in init methods
                return null;
            }

            var hasValidVariables = false;

            for (var category in json) {
                if (!json.hasOwnProperty(category)) continue;
                var vars = json[category];

                if (typeof vars !== 'object' || vars === null || Array.isArray(vars)) {
                    return 'Category "' + (category || '(empty)') + '" must be an object.';
                }

                for (var varName in vars) {
                    if (!vars.hasOwnProperty(varName)) continue;
                    var varData = vars[varName];

                    if (typeof varData !== 'object' || varData === null || Array.isArray(varData)) {
                        return 'Variable "' + varName + '" must be an object with at least a "value" property.';
                    }

                    if (!varData.hasOwnProperty('value')) {
                        return 'Variable "' + varName + '" is missing "value" property.';
                    }

                    hasValidVariables = true;
                }
            }

            // JSON-only mode: must have at least one valid variable
            if (!this.hasTemplate && !hasValidVariables) {
                return 'No valid variables found. Each variable needs at least a "value" property.';
            }

            return null; // No error
        }

        /**
         * Render error message with example
         */
        _renderError(message) {
            var wrapper = document.createElement('div');
            wrapper.className = 'qfq-icj-template qfq-icj-error';

            var errorBox = document.createElement('div');
            errorBox.className = 'qfq-icj-error-box';

            var errorMsg = document.createElement('div');
            errorMsg.className = 'qfq-icj-error-msg';
            errorMsg.textContent = 'Error: ' + message;

            var exampleLabel = document.createElement('div');
            exampleLabel.className = 'qfq-icj-error-label';
            exampleLabel.textContent = 'Expected JSON structure:';

            var example = document.createElement('pre');
            example.className = 'qfq-icj-error-example';
            example.textContent = '{\n' +
                '  "category": {\n' +
                '    "variableName": {\n' +
                '      "value": "your-value",\n' +
                '      "__label": "Label (optional)",\n' +
                '      "__desc": "Description (optional)",\n' +
                '      "__type": "text|select|checkbox|number (optional)",\n' +
                '      "__options": ["option1", "option2"] (for select),\n' +
                '      "__required": true (optional),\n' +
                '      "__readonly": true (optional)\n' +
                '    }\n' +
                '  }\n' +
                '}';

            errorBox.appendChild(errorMsg);
            errorBox.appendChild(exampleLabel);
            errorBox.appendChild(example);

            // Show current (invalid) content if available
            if (this.rawContent) {
                var currentLabel = document.createElement('div');
                currentLabel.className = 'qfq-icj-error-label';
                currentLabel.textContent = 'Current content:';

                var currentContent = document.createElement('pre');
                currentContent.className = 'qfq-icj-error-current';
                currentContent.textContent = this.rawContent;

                var hint = document.createElement('div');
                hint.className = 'qfq-icj-error-hint';
                hint.textContent = 'Note: Content may have been modified by QFQ encode or checkType settings. ' +
                    'Recommended: FE.encode = none, FE.checkType = all';

                errorBox.appendChild(currentLabel);
                errorBox.appendChild(currentContent);
                errorBox.appendChild(hint);
            }

            wrapper.appendChild(errorBox);

            return wrapper;
        }

        _parseIcjOnly() {
            var self = this;

            for (var category in this.values) {
                if (!this.values.hasOwnProperty(category)) continue;
                var vars = this.values[category];

                if (typeof vars !== 'object' || vars === null) continue;

                for (var name in vars) {
                    if (!vars.hasOwnProperty(name)) continue;
                    var data = vars[name];

                    if (typeof data !== 'object' || data === null) continue;

                    var inputType = 'text';
                    var options = [];

                    if (data.__type) {
                        inputType = data.__type;
                        if (inputType === 'select' && Array.isArray(data.__options)) {
                            options = data.__options;
                        }
                    } else if (Array.isArray(data.__options)) {
                        inputType = 'select';
                        options = data.__options;
                    }

                    var variable = {
                        name: name,
                        defaultValue: data.value !== undefined ? String(data.value) : '',
                        category: category,
                        subcategory: data.__cat || null,
                        label: data.__label || self._formatKeyAsLabel(name),
                        description: data.__desc || '',
                        inputType: inputType,
                        options: options,
                        type: inputType,
                        required: data.__required === true || data.__required === 'true',
                        readonly: data.__readonly === true || data.__readonly === 'true'
                    };

                    self.variables.set(name, variable);

                    if (!self.categories.has(category)) {
                        self.categories.set(category, []);
                    }
                    self.categories.get(category).push(variable);
                }
            }
        }

        _parseTemplate() {
            var self = this;
            var normalizedTemplate = this.template.replace(/\r/g, '').replace(/\\r/g, '');
            var lines = normalizedTemplate.split('\n');
            var currentMeta = null;

            for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();

                if (line.startsWith('#')) {
                    var meta = self._parseMetaComment(line);
                    if (meta) currentMeta = meta;
                    continue;
                }

                var varMatch = line.match(/^(\w+)\s*=\s*(.*)$/);
                if (varMatch && currentMeta) {
                    var name = varMatch[1];
                    var defaultValue = varMatch[2];

                    var variable = {
                        name: name,
                        defaultValue: defaultValue.trim(),
                        category: currentMeta.category,
                        subcategory: currentMeta.subcategory,
                        type: currentMeta.type,
                        inputType: currentMeta.inputType,
                        options: currentMeta.options,
                        label: currentMeta.label,
                        description: currentMeta.description
                    };

                    self.variables.set(name, variable);

                    // Use empty string for no category (enables no-tabs mode)
                    var catKey = currentMeta.category || '';
                    if (!self.categories.has(catKey)) {
                        self.categories.set(catKey, []);
                    }
                    self.categories.get(catKey).push(variable);

                    currentMeta = null;
                }
            }
        }

        _parseMetaComment(line) {
            var content = line.replace(/^#\s*/, '');

            if (content.indexOf('cat=') === -1 && content.indexOf('type=') === -1 && content.indexOf('label=') === -1) {
                return null;
            }

            var meta = {
                category: null,
                subcategory: null,
                type: 'string',
                inputType: 'text',
                options: [],
                label: '',
                description: ''
            };

            var catMatch = content.match(/cat=([^;]+)/);
            if (catMatch) {
                var catParts = catMatch[1].trim().split('/');
                meta.category = catParts[0];
                meta.subcategory = catParts[1] || null;
            }

            var typeMatch = content.match(/type=([^;]+)/);
            if (typeMatch) {
                var typeStr = typeMatch[1].trim();
                meta.type = typeStr;

                var optionsMatch = typeStr.match(/options\[([^\]]+)\]/);
                if (optionsMatch) {
                    meta.inputType = 'select';
                    meta.options = optionsMatch[1].split(',').map(function(o) { return o.trim(); });
                } else if (typeStr === 'boolean') {
                    meta.inputType = 'checkbox';
                } else if (typeStr === 'int+' || typeStr === 'int') {
                    meta.inputType = 'number';
                } else if (typeStr === 'textarea' || typeStr.indexOf('user[') === 0) {
                    meta.inputType = 'textarea';
                } else {
                    meta.inputType = 'text';
                }
            }

            var labelMatch = content.match(/label=([^:]+)(?::(.+))?$/);
            if (labelMatch) {
                meta.label = labelMatch[1].trim();
                meta.description = labelMatch[2] ? labelMatch[2].trim() : '';
            }

            return meta;
        }

        _formatKeyAsLabel(key) {
            return key
                .replace(/([A-Z])/g, ' $1')
                .replace(/[_-]/g, ' ')
                .replace(/^\w/, function(c) { return c.toUpperCase(); })
                .trim();
        }

        getValue(name, category) {
            // If category provided, look there first (fixes duplicate field names across categories)
            if (category !== undefined && this.values[category] && this.values[category][name] && this.values[category][name].value !== undefined) {
                return String(this.values[category][name].value);
            }

            // Fallback to searching all categories (for backward compatibility)
            for (var cat in this.values) {
                if (!this.values.hasOwnProperty(cat)) continue;
                var vars = this.values[cat];
                if (vars && vars[name] && vars[name].value !== undefined) {
                    return String(vars[name].value);
                }
            }

            if (this.values[name] !== undefined) {
                return String(this.values[name]);
            }

            var variable = this.variables.get(name);
            return variable ? variable.defaultValue : '';
        }

        setValue(name, value, category) {
            // If category is provided, use it directly (fixes duplicate field names across categories)
            if (category !== undefined) {
                if (!this.values[category]) {
                    this.values[category] = {};
                }
                // Always preserve existing __* properties (metadata like __readonly, __required, __label, etc.)
                var existing = this.values[category][name] || {};
                this.values[category][name] = Object.assign({}, existing, { value: value });
                return;
            }

            // Fallback: try to find category from variables Map
            var variable = this.variables.get(name);
            if (!variable) return;

            // Preserve empty string category, only default to 'config' if undefined
            var cat = variable.category !== undefined ? variable.category : 'config';

            if (!this.values[cat]) {
                this.values[cat] = {};
            }

            // Always preserve existing __* properties (metadata like __readonly, __required, __label, etc.)
            var existingVal = this.values[cat][name] || {};
            this.values[cat][name] = Object.assign({}, existingVal, { value: value });
        }

        getValues() {
            return this.values;
        }

        getValuesIcj() {
            // First collect all current values from inputs
            this.collectValues();

            // Always return full values including __* metadata properties
            return JSON.stringify(this.values);
        }

        getFilledTemplate() {
            if (!this.hasTemplate) return '';

            var self = this;
            var result = this.template;

            for (var entry of this.variables) {
                var name = entry[0];
                var value = self.getValue(name);
                var regex = new RegExp('^(' + name + '\\s*=\\s*).*$', 'm');
                result = result.replace(regex, '$1' + value);
            }

            return result;
        }

        render(container, mode, cssClasses) {
            var self = this;
            mode = mode || 'editor';
            cssClasses = cssClasses || '';

            var target = container || this.container;

            // Auto-create container after textarea if not provided
            if (!target && this.textarea) {
                target = document.createElement('div');
                this.textarea.parentNode.insertBefore(target, this.textarea.nextSibling);
                // Hide the original textarea - it's used for form submission only
                this.textarea.style.display = 'none';
            }

            if (!target) {
                console.error('QfqConfigTemplate: No container specified and no textarea to attach to');
                return null;
            }

            if (mode === 'template' && !this.hasTemplate) {
                console.warn('QfqConfigTemplate: Template mode requires a template. Falling back to editor.');
                mode = 'editor';
            }

            this.mode = mode;

            // Remove previous wrapper
            if (this.wrapper) {
                this.wrapper.remove();
            }

            // Remove previous loading spinner
            if (this.loadingEl) {
                this.loadingEl.remove();
            }

            // Create main wrapper (hidden initially)
            var mainWrapper = document.createElement('div');
            mainWrapper.className = 'qfq-icj-wrapper' + (cssClasses ? ' ' + cssClasses : '');
            mainWrapper.style.display = 'none';

            // Create loading spinner
            this.loadingEl = document.createElement('div');
            this.loadingEl.className = 'qfq-icj-loading';
            this.loadingEl.innerHTML = '<div class="qfq-icj-spinner"></div>';
            target.appendChild(this.loadingEl);

            // Render content (use setTimeout to allow spinner to display)
            setTimeout(function() {
                // Check for validation error
                if (self.validationError) {
                    self.wrapper = self._renderError(self.validationError);
                } else if (mode === 'template') {
                    self.wrapper = self._renderTemplateMode();
                } else {
                    self.wrapper = self._renderEditorMode();
                }

                mainWrapper.appendChild(self.wrapper);
                target.appendChild(mainWrapper);

                // Hide spinner, show content
                if (self.loadingEl) {
                    self.loadingEl.remove();
                    self.loadingEl = null;
                }
                mainWrapper.style.display = 'block';

                // Dispatch ready event
                document.dispatchEvent(new CustomEvent('qfq-icj-ready', {
                    detail: { editor: self, error: self.validationError }
                }));
            }, 0);

            return mainWrapper;
        }

        _renderEditorMode() {
            var self = this;
            var wrapper = document.createElement('div');
            wrapper.className = 'qfq-icj-template qfq-icj-mode-editor';

            // Check if we should skip tabs (single empty category or single category)
            var categoryKeys = Array.from(this.categories.keys());
            var skipTabs = categoryKeys.length === 1 && (categoryKeys[0] === '' || categoryKeys[0] === '_default');

            if (skipTabs) {
                // No tabs - render fields directly
                var content = document.createElement('div');
                content.className = 'qfq-icj-no-tabs';

                var variables = this.categories.get(categoryKeys[0]);
                this._renderCategoryContent(content, variables);

                wrapper.appendChild(content);
            } else {
                // Render with tabs
                var tabBar = document.createElement('div');
                tabBar.className = 'qfq-icj-tab-bar';

                var tabContents = document.createElement('div');
                tabContents.className = 'qfq-icj-tab-contents';

                var isFirst = true;
                var tabClickHandler = function(e) { self._switchTab(e, wrapper); };
                for (var entry of this.categories) {
                    var category = entry[0];
                    var catVariables = entry[1];
                    var tabId = 'cat-' + (category || 'default') + '-' + Math.random().toString(36).substr(2, 9);

                    var tabBtn = document.createElement('button');
                    tabBtn.type = 'button';
                    tabBtn.className = 'qfq-icj-tab-btn' + (isFirst ? ' active' : '');
                    tabBtn.textContent = self._formatCategoryName(category || 'General');
                    tabBtn.dataset.tabId = tabId;
                    tabBtn.addEventListener('click', tabClickHandler);
                    tabBar.appendChild(tabBtn);

                    var tabContent = document.createElement('div');
                    tabContent.className = 'qfq-icj-tab-content' + (isFirst ? ' active' : '');
                    tabContent.dataset.tabId = tabId;

                    self._renderCategoryContent(tabContent, catVariables);
                    tabContents.appendChild(tabContent);

                    isFirst = false;
                }

                wrapper.appendChild(tabBar);
                wrapper.appendChild(tabContents);
            }

            return wrapper;
        }

        _renderTemplateMode() {
            var self = this;
            var wrapper = document.createElement('div');
            wrapper.className = 'qfq-icj-template qfq-icj-mode-template';

            var pre = document.createElement('pre');
            pre.className = 'qfq-icj-template-view';

            var lines = this.template.split('\n');

            for (var i = 0; i < lines.length; i++) {
                var line = lines[i];
                var lineEl = document.createElement('div');
                lineEl.className = 'qfq-icj-template-line';

                var varMatch = line.match(/^(\w+)(\s*=\s*)(.*)$/);
                if (varMatch) {
                    var name = varMatch[1];
                    var separator = varMatch[2];
                    var variable = this.variables.get(name);

                    if (variable) {
                        var nameSpan = document.createElement('span');
                        nameSpan.className = 'qfq-icj-tpl-var-name';
                        nameSpan.textContent = name;

                        var sepSpan = document.createElement('span');
                        sepSpan.className = 'qfq-icj-tpl-separator';
                        sepSpan.textContent = separator;

                        var input = self._createTemplateInput(variable);

                        lineEl.appendChild(nameSpan);
                        lineEl.appendChild(sepSpan);
                        lineEl.appendChild(input);
                    } else {
                        lineEl.textContent = line;
                    }
                } else if (line.startsWith('#')) {
                    lineEl.className += ' qfq-icj-tpl-comment';
                    lineEl.textContent = line;
                } else {
                    lineEl.textContent = line || '\u00A0';
                }

                pre.appendChild(lineEl);
            }

            wrapper.appendChild(pre);

            return wrapper;
        }

        _createTemplateInput(variable) {
            var self = this;
            var isFieldRequired = this.isRequired || this._getFieldFlag(variable, 'required');
            var isFieldReadonly = this._getFieldFlag(variable, 'readonly');
            var currentValue = this.getValue(variable.name, variable.category);
            var input;

            if (variable.inputType === 'select') {
                input = document.createElement('select');
                input.className = 'qfq-icj-tpl-input qfq-icj-tpl-select';
                for (var j = 0; j < variable.options.length; j++) {
                    var option = variable.options[j];
                    var opt = document.createElement('option');
                    opt.value = option;
                    opt.textContent = option;
                    opt.selected = option === currentValue;
                    input.appendChild(opt);
                }
            } else if (variable.inputType === 'checkbox') {
                input = document.createElement('select');
                input.className = 'qfq-icj-tpl-input qfq-icj-tpl-select';
                ['1', '0'].forEach(function(val) {
                    var opt = document.createElement('option');
                    opt.value = val;
                    opt.textContent = val;
                    opt.selected = val === currentValue;
                    input.appendChild(opt);
                });
            } else if (variable.inputType === 'textarea') {
                input = document.createElement('textarea');
                input.className = 'qfq-icj-tpl-input qfq-icj-tpl-textarea qfq-skip-dirty';
                input.value = currentValue;
                input.rows = 4;
                input.style.maxHeight = '200px';
                input.style.overflowY = 'auto';
                input.style.resize = 'vertical';
                input.dataset.icjTextarea = 'true';
            } else {
                input = document.createElement('input');
                input.type = variable.inputType === 'number' ? 'number' : 'text';
                input.className = 'qfq-icj-tpl-input';
                input.value = currentValue;
                input.size = Math.max(20, currentValue.length + 5);
            }

            input.dataset.variableName = variable.name;
            input.dataset.variableCategory = variable.category !== undefined ? variable.category : '';
            input.title = variable.description || variable.label || '';

            // Disable input if editor is in readonly mode or field is individually readonly
            if (this.isDisabled || isFieldReadonly) {
                input.disabled = true;
            }

            // Set required if editor is in required mode or field is individually required
            if (isFieldRequired) {
                input.required = true;
                input.dataset.required = 'yes';
                input.dataset.requiredError = this.requiredError || 'Required';
                input.dataset.error = 'Error';
            }

            input.addEventListener('change', function(e) { self._handleChange(e, variable); });
            input.addEventListener('input', function(e) {
                self._handleChange(e, variable);
                if (e.target.tagName === 'INPUT' && e.target.type === 'text') {
                    e.target.size = Math.max(20, e.target.value.length + 5);
                }
            });

            return input;
        }

        _renderCategoryContent(container, variables) {
            var self = this;
            var subcategories = new Map();

            for (var i = 0; i < variables.length; i++) {
                var variable = variables[i];
                var subcat = variable.subcategory || '_default';
                if (!subcategories.has(subcat)) {
                    subcategories.set(subcat, []);
                }
                subcategories.get(subcat).push(variable);
            }

            for (var entry of subcategories) {
                var subcatName = entry[0];
                var vars = entry[1];

                if (subcatName !== '_default' && subcategories.size > 1) {
                    var header = document.createElement('div');
                    header.className = 'qfq-icj-subcategory-header';
                    header.textContent = self._formatCategoryName(subcatName);
                    container.appendChild(header);
                }

                for (var j = 0; j < vars.length; j++) {
                    var field = self._renderField(vars[j]);
                    container.appendChild(field);
                }
            }
        }

        _switchTab(event, wrapper) {
            var tabId = event.target.dataset.tabId;
            this._activateTabById(tabId, wrapper);
        }

        _activateTabById(tabId, wrapper) {
            wrapper.querySelectorAll('.qfq-icj-tab-btn').forEach(function(btn) {
                btn.classList.toggle('active', btn.dataset.tabId === tabId);
            });

            wrapper.querySelectorAll('.qfq-icj-tab-content').forEach(function(content) {
                content.classList.toggle('active', content.dataset.tabId === tabId);
            });
        }

        /**
         * Activate the tab containing the given element
         * @param {HTMLElement} element - The element to find and activate its tab
         */
        activateTabForElement(element) {
            if (!this.wrapper) return;

            // Find the tab-content containing this element
            var tabContent = element.closest('.qfq-icj-tab-content');
            if (tabContent && tabContent.dataset.tabId) {
                this._activateTabById(tabContent.dataset.tabId, this.wrapper);
            }
        }

        /**
         * Get a field flag (__readonly, __required) from JSON values
         * This works in both template and JSON-only mode
         */
        _getFieldFlag(variable, flagName) {
            // First check variable property (set in JSON-only mode)
            if (variable[flagName]) return true;

            // Then check JSON values directly (works in template mode too)
            var category = variable.category !== undefined ? variable.category : '';
            if (this.values[category] && this.values[category][variable.name]) {
                var data = this.values[category][variable.name];
                var icjFlag = data['__' + flagName];
                return icjFlag === true || icjFlag === 'true';
            }
            return false;
        }

        _renderField(variable) {
            var self = this;
            var isFieldRequired = this.isRequired || this._getFieldFlag(variable, 'required');
            var isFieldReadonly = this._getFieldFlag(variable, 'readonly');
            var field = document.createElement('div');
            // Add form-group class for Bootstrap Validator error display
            field.className = 'qfq-icj-field form-group';

            var label = document.createElement('label');
            label.className = 'qfq-icj-label' + (isFieldRequired ? ' qfq-icj-required' : '');
            // Include category in ID to ensure uniqueness (field names like 'enabled' appear in multiple categories)
            var catPrefix = variable.category ? variable.category + '-' : '';
            label.htmlFor = 'qfq-icj-' + catPrefix + variable.name;
            label.textContent = variable.label || variable.name;
            if (variable.description) label.title = variable.description;

            var inputWrapper = document.createElement('div');
            inputWrapper.className = 'qfq-icj-input-wrapper';

            var input;
            var currentValue = this.getValue(variable.name, variable.category);

            switch (variable.inputType) {
                case 'select':
                    input = document.createElement('select');
                    for (var j = 0; j < variable.options.length; j++) {
                        var option = variable.options[j];
                        var opt = document.createElement('option');
                        opt.value = option;
                        opt.textContent = option;
                        opt.selected = option === currentValue;
                        input.appendChild(opt);
                    }
                    break;

                case 'checkbox':
                    input = document.createElement('input');
                    input.type = 'checkbox';
                    input.checked = currentValue === '1' || currentValue === 'true' || currentValue === true;
                    break;

                case 'number':
                    input = document.createElement('input');
                    input.type = 'number';
                    input.value = currentValue;
                    break;

                case 'textarea':
                    input = document.createElement('textarea');
                    // Convert literal \n to actual newlines for display in textarea
                    input.value = currentValue.replace(/\\n/g, '\n');
                    input.rows = 4;
                    input.style.maxHeight = '200px';
                    input.style.overflowY = 'auto';
                    input.style.resize = 'vertical';
                    // Mark as ICJ field to prevent QFQ form handlers from processing it
                    input.classList.add('qfq-skip-dirty');
                    input.dataset.icjTextarea = 'true';
                    break;

                default:
                    input = document.createElement('input');
                    input.type = 'text';
                    input.value = currentValue;
                    break;
            }

            // Use catPrefix from above for unique ID
            input.id = 'qfq-icj-' + catPrefix + variable.name;
            // Don't set name attribute - the original hidden textarea holds the full JSON for submission
            // Setting name would cause these inputs to be submitted separately
            // input.name = variable.name;
            // Add form-control class for Bootstrap Validator compatibility
            // Use classList.add to preserve any classes added in the switch (like qfq-skip-dirty for textareas)
            input.classList.add('qfq-icj-input', 'form-control');
            input.dataset.variableName = variable.name;
            input.dataset.variableCategory = variable.category !== undefined ? variable.category : '';

            // Disable input if editor is in readonly mode or field is individually readonly
            if (this.isDisabled || isFieldReadonly) {
                input.disabled = true;
            }

            // Set required if editor is in required mode or field is individually required
            if (isFieldRequired) {
                input.required = true;
                input.dataset.required = 'yes';
                input.dataset.requiredError = this.requiredError || 'Required';
                input.dataset.error = 'Error';
            }

            input.addEventListener('change', function(e) { self._handleChange(e, variable); });
            input.addEventListener('input', function(e) { self._handleChange(e, variable); });

            inputWrapper.appendChild(input);

            // Add help-block for Bootstrap Validator error messages
            var helpBlock = document.createElement('div');
            helpBlock.className = 'help-block with-errors';
            inputWrapper.appendChild(helpBlock);

            if (variable.description) {
                var desc = document.createElement('small');
                desc.className = 'qfq-icj-description';
                desc.textContent = variable.description;
                inputWrapper.appendChild(desc);
            }

            field.appendChild(label);
            field.appendChild(inputWrapper);

            return field;
        }

        _handleChange(event, variable) {
            var value;

            if (variable.inputType === 'checkbox' && event.target.type === 'checkbox') {
                value = event.target.checked ? '1' : '0';
            } else if (variable.inputType === 'textarea') {
                // Convert actual newlines back to literal \n for storage
                value = event.target.value.replace(/\n/g, '\\n');
            } else {
                value = event.target.value;
            }

            // Pass category to setValue to fix duplicate field names across categories
            this.setValue(variable.name, value, variable.category);

            // Update textarea value for form submission
            if (this.textarea) {
                this.textarea.value = this.getValuesIcj();

                // Manually trigger dirty state (don't pass textarea because it has qfq-skip-dirty)
                if (typeof QfqNS !== 'undefined' && QfqNS.form && typeof QfqNS.form.markChanged === 'function') {
                    QfqNS.form.markChanged();
                }
            }

            document.dispatchEvent(new CustomEvent('qfq-icj-change', {
                detail: { name: variable.name, value: value, allValues: this.values }
            }));
        }

        _formatCategoryName(name) {
            return name
                .replace(/([A-Z])/g, ' $1')
                .replace(/[_-]/g, ' ')
                .replace(/^\w/, function(c) { return c.toUpperCase(); })
                .trim();
        }

        collectValues() {
            if (!this.wrapper) return this.values;

            var self = this;
            var inputs = this.wrapper.querySelectorAll('[data-variable-name]');
            inputs.forEach(function(input) {
                var name = input.dataset.variableName;
                // Use category from dataset to fix duplicate field names across categories
                var category = input.dataset.variableCategory;

                var value;
                if (input.type === 'checkbox') {
                    value = input.checked ? '1' : '0';
                } else {
                    value = input.value;
                }

                // Pass category to setValue
                self.setValue(name, value, category);
            });

            return this.values;
        }

        collectValuesIcj() {
            this.collectValues();
            return this.getValuesIcj();
        }

        updateTextarea() {
            if (this.textarea) {
                this.collectValues();
                this.textarea.value = this.getValuesIcj();
            }
        }
    }

    /**
     * Initialize JSON editors for all textarea.qfq-icj elements
     *
     * @param {NodeList|Array} icjElements - Elements with class .qfq-icj
     * @param {object} n - Namespace reference
     */
    // Store all editors for later access
    var icjEditors = [];

    var qfqIcj = function (icjElements, n) {
        if (icjElements !== undefined && icjElements.length > 0) {
            icjElements.forEach(function(textarea) {
                if (textarea.tagName === 'TEXTAREA') {
                    // Add qfq-skip-dirty to prevent dirty detection during initialization
                    textarea.classList.add('qfq-skip-dirty');
                    var editor = new QfqConfigTemplate(textarea);
                    editor.render(null, 'editor', 'qfq-form-grey-2');
                    icjEditors.push(editor);
                }
            });

            // Update validator to include dynamically created inputs
            // The render() method uses setTimeout, so inputs are added to DOM asynchronously
            // We need to wait for the qfq-icj-ready event
            var $form = $(icjElements[0]).closest('form');
            if ($form.length) {
                var updateValidator = function() {
                    var validator = $form.data('bs.validator');
                    if (validator) {
                        $form.validator('update');
                        return true;
                    }
                    return false;
                };

                // Wait for qfq-icj-ready event (fired after inputs are added to DOM)
                document.addEventListener('qfq-icj-ready', function onReady() {
                    document.removeEventListener('qfq-icj-ready', onReady);

                    // Try immediately in case validator is already initialized
                    if (!updateValidator()) {
                        // If window is already loaded, use setTimeout as fallback
                        if (document.readyState === 'complete') {
                            setTimeout(updateValidator, 100);
                        } else {
                            // Wait for window.load when validator gets initialized
                            $(window).on('load.qfqIcj', function() {
                                updateValidator();
                                $(window).off('load.qfqIcj');
                            });
                        }
                    }

                    // Listen for save button click to activate tab with first invalid JSON input
                    var activateFirstInvalidTab = function() {
                        // Find first invalid JSON input - check both :invalid and empty required
                        var firstInvalid = null;
                        $form.find('.qfq-icj-input[required]').each(function() {
                            if (!this.validity.valid || (this.value === '' && this.required)) {
                                firstInvalid = this;
                                return false; // break
                            }
                        });

                        if (firstInvalid) {
                            // Find the tab content containing this element
                            var tabContent = firstInvalid.closest('.qfq-icj-tab-content');

                            if (tabContent && tabContent.dataset.tabId) {
                                // Find the wrapper
                                var wrapper = firstInvalid.closest('.qfq-icj-template');
                                if (wrapper) {
                                    var tabId = tabContent.dataset.tabId;
                                    // Activate tab directly
                                    wrapper.querySelectorAll('.qfq-icj-tab-btn').forEach(function(btn) {
                                        btn.classList.toggle('active', btn.dataset.tabId === tabId);
                                    });
                                    wrapper.querySelectorAll('.qfq-icj-tab-content').forEach(function(content) {
                                        content.classList.toggle('active', content.dataset.tabId === tabId);
                                    });
                                }
                            }

                            // Focus the element after tab switch
                            var elementToFocus = firstInvalid;
                            setTimeout(function() {
                                elementToFocus.focus();
                            }, 100);
                        }
                    };

                    // Listen on save button click using event delegation
                    // Also try document-level delegation as fallback
                    $(document).on('click', '[id^="save-button"]', activateFirstInvalidTab);
                });
            }
        }
    };

    // Export QfqConfigTemplate class
    n.Helper.QfqConfigTemplate = QfqConfigTemplate;

    // Export initialization function
    n.Helper.qfqIcj = qfqIcj;

})(QfqNS);
/**
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 */

/* global console */
/* global qfqChat */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    let qfqMerge = function (mergeWindowElement) {
        //let chatWindowsElements = document.getElementsByClassName("qfq-chat-window");
        let mergeWindow = null;
        if (mergeWindowElement !== undefined) {
            mergeWindow = new MergeWindow(mergeWindowElement);
        }
        return mergeWindow;
    }

    class MergeWindow {
        constructor(mergeWindowElement) {
            let that = this;
            this.mergeWindow = mergeWindowElement;
            this.baseUrl = mergeWindowElement.getAttribute("data-base-url");
            this.restCallUrl = this.baseUrl + 'typo3conf/ext/qfq/Classes/Api/dataReport.php?s=';
            this.dataSipRules = mergeWindowElement.getAttribute("data-sip-rules");
            this.merged = false;
            this.primaryElements = {};

            let primaryDeleteBtn = mergeWindowElement.querySelectorAll("button[disabled='disabled'][data-primary]");
            if (primaryDeleteBtn !== null) {
                Array.from(primaryDeleteBtn).forEach(btn => {
                    const primaryId = btn.getAttribute('data-primary');
                    const sip = btn.getAttribute('data-sip');
                    const sipUpdate = btn.getAttribute('data-sip-update');
                    const sipDelete = btn.getAttribute('data-sip-delete');
                    this.primaryElements[primaryId] = {};
                    this.primaryElements[primaryId].deleteBtn = btn;
                    this.primaryElements[primaryId].id = primaryId;
                    this.primaryElements[primaryId].sipUrl = this.restCallUrl + sip;
                    this.primaryElements[primaryId].sipUpdate = sipUpdate;
                    this.primaryElements[primaryId].sipDelete = sipDelete;
                    this.primaryElements[primaryId].relatedCheckboxes = mergeWindowElement.querySelectorAll(`input[type='checkbox'][data-id='${primaryId}']`);
                });
            }

            this.allCheckboxes = mergeWindowElement.querySelectorAll(".merge-window input[type='checkbox']");
            this.mergeBtn = mergeWindowElement.querySelector('#merge-window-btn');
            this.primaryCheckboxes = Array.from(mergeWindowElement.querySelectorAll('[name="checkAll"]'));
            this.checkboxes = mergeWindowElement.querySelectorAll("input[type='checkbox'][data-id]");
            this.ruleBtn = mergeWindowElement.querySelector('#merge-window-update-rule-btn');

            this.init();
        }

        init() {
            let that = this;

            // Iterate over primary elements and initialize delete btn
            Object.values(this.primaryElements).forEach(element => {
                // Check for existing merge data and enable delete buttons
                if (that.checkData(element.id)) {
                    element.deleteBtn.disabled = false;
                }

                element.deleteBtn.addEventListener('click', async function () {
                    if (that.checkData(element.id) && !that.merged) {
                        if (confirm("Are you sure you want to delete this data?")) {
                            console.log('Delete data click');
                            try {
                                const SQL_DELETE_PRIMARY = await that.makeApiCall(element.sipUrl, this, 'delete');
                            } catch (error) {
                            }
                        }
                    }
                });
            });

            // Initialize rule btn
            if (this.ruleBtn !== null) {
                this.ruleBtn.addEventListener('click', async function () {
                    try {
                        const sip_rules_url = that.restCallUrl + that.dataSipRules;
                        const SQL_INSERT = await that.makeApiCall(sip_rules_url, this, 'updateRules');
                    } catch (error) {
                    }
                });
            }

            // Initialize rest
            this.initTableContent();
            this.initCheckboxes(this);
            if (this.mergeBtn !== null) {
                this.initMergeBtn(this);
            }
        }

        checkData(dataId) {
            let foundCheckboxes = this.primaryElements[dataId].relatedCheckboxes;
            return foundCheckboxes.length === 0;
        }

        initTableContent() {
            this.mergeWindow.querySelectorAll('.merge-table-content').forEach(content => {
                let wrapper = content.parentElement;
                let button = wrapper.nextElementSibling;
                let icon = button.querySelector('i');
                if (content.scrollHeight > 150) {
                    button.classList.remove('hidden');
                } else {
                    button.classList.add('hidden');
                }
                button.addEventListener('click', function () {
                    if (!content.style.maxHeight || content.style.maxHeight === '150px') {
                        content.style.maxHeight = content.scrollHeight + 'px';
                        icon.classList.remove('fa-angle-double-down');
                        icon.classList.add('fa-angle-double-up');
                    } else {
                        content.style.maxHeight = '150px';
                        icon.classList.add('fa-angle-double-down');
                        icon.classList.remove('fa-angle-double-up');
                    }
                });
            });
        }

        initCheckboxes(that) {
            // Function to set initial state of a checkbox
            const setInitialState = (el) => {
                if (el.hasAttribute('checked')) {
                    el.dataset.state = "checked";
                    el.checked = true;
                } else if (el.dataset.state === "indeterminate") {
                    el.indeterminate = true;
                    // Note: indeterminate checkboxes might also be checked or unchecked in terms of their value submission.
                    // Adjust the following line if you have a convention for this.
                } else {
                    el.dataset.state = "unchecked";
                    el.checked = false;
                    el.unchecked = true;
                }
            };

            this.allCheckboxes.forEach(el => {
                // Set initial state for checkboxes
                setInitialState(el);
            });

            this.primaryCheckboxes.forEach(el => el.addEventListener('click', (e) => {
                let currentState = el.dataset.state || "unchecked";
                let nextState = currentState === "checked" ? "indeterminate" : (currentState === "indeterminate" ? "unchecked" : "checked");
                el.dataset.state = nextState;
                el.indeterminate = nextState === "indeterminate";
                el.checked = nextState === "checked";

                that.primaryElements[el.id].relatedCheckboxes.forEach(cb => {
                    cb.dataset.state = nextState;
                    cb.indeterminate = nextState === "indeterminate";
                    cb.checked = nextState === "checked";
                    cb.unchecked = nextState === "unchecked";

                    let parentNode = cb.parentNode.parentNode;
                    parentNode.classList.remove('bg-success', 'bg-danger', 'bg-warning');
                    if (nextState === "checked") {
                        parentNode.classList.add('bg-success');
                    } else if (nextState === "indeterminate") {
                        parentNode.classList.add('bg-warning');
                    } else {
                        parentNode.classList.add('bg-danger');
                    }
                });
            }));

            Object.values(that.primaryElements).forEach(element => {
                Array.from(element.relatedCheckboxes).forEach(cb => cb.addEventListener('click', () => {
                    let currentState = cb.dataset.state || "unchecked";
                    let nextState = currentState === "checked" ? "indeterminate" : (currentState === "indeterminate" ? "unchecked" : "checked");
                    cb.dataset.state = nextState;
                    cb.indeterminate = nextState === "indeterminate";
                    cb.checked = nextState === "checked";
                    cb.unchecked = nextState === "unchecked";

                    let parentNode = cb.parentNode.parentNode;
                    parentNode.classList.remove('bg-success', 'bg-danger', 'bg-warning');
                    if (nextState === "checked") {
                        parentNode.classList.add('bg-success');
                    } else if (nextState === "indeterminate") {
                        parentNode.classList.add('bg-warning');
                    } else {
                        parentNode.classList.add('bg-danger');
                    }
                }));
            });
        }

        initMergeBtn(that) {
            this.mergeBtn.addEventListener('click', async function () {
                if (that.merged) {
                    return;
                }

                if (confirm("Are you sure you want to process the selected checkboxes?")) {
                    let primaryId = that.mergeWindow.querySelector('#primaryId').value;
                    let checkedIds = {};
                    let uncheckedIds = {};
                    let columnSearch = {};

                   that.checkboxes.forEach(cb => {
                        let dataTable = cb.getAttribute('data-table');
                        let dataId = cb.getAttribute('id');
                        let columns = cb.getAttribute('data-column-found');

                        if (columnSearch[dataTable] === undefined) {
                            columnSearch[dataTable] = columns;
                        }

                        if (cb.checked) {
                            checkedIds[dataTable] = checkedIds[dataTable] ? checkedIds[dataTable] + ',' + dataId : dataId;
                        } else if (cb.unchecked) {
                            uncheckedIds[dataTable] = uncheckedIds[dataTable] ? uncheckedIds[dataTable] + ',' + dataId : dataId;
                        }
                    });

                    console.log("Checked IDs: ", checkedIds);
                    console.log("Unchecked IDs: ", uncheckedIds);

                    await that.processMerge(that, primaryId, checkedIds, columnSearch, 'update');
                    await that.processMerge(that, primaryId, uncheckedIds, columnSearch, 'delete');
                }
            });
        }

        async processMerge(that, primaryId, ids, columnSearch, type) {
            for (let dataTable in ids) {
                if (ids.hasOwnProperty(dataTable)) {
                    const dateValues = '&tableName=' + dataTable + '&columnIdList=' + ids[dataTable] + '&columnSearch=' + columnSearch[dataTable];
                    let sip = null;
                    if (type === 'update') {
                        console.log(`Processing UPDATE ${dataTable} with IDs: ${ids[dataTable]}`);
                        sip = that.primaryElements[primaryId].sipUpdate;
                    } else {
                        console.log(`Processing DELETE FROM ${dataTable} with IDs: ${ids[dataTable]}`);
                        sip = that.primaryElements[primaryId].sipDelete;
                    }
                    const url_sip = that.restCallUrl + sip + dateValues;
                    try {
                        const SQL_EXECUTE = await that.makeApiCall(url_sip, that.mergeBtn, 'update');
                    } catch (error) {
                    }
                }
            }
        }

        async makeApiCall(url, btn, type = '', body = '') {
            btn.classList.remove('btn-default');
            btn.classList.add('btn-warning');
            const response = await fetch(url, {method: 'POST', body: body});

            if (!response.ok) {
                btn.classList.remove('btn-default');
                btn.classList.remove('btn-warning');
                btn.classList.add('btn-danger');
                throw new Error();
            } else {
                btn.classList.remove('btn-default');
                btn.classList.remove('btn-warning');
                btn.classList.add('btn-success');

                // Case of update delete records
                if (type === 'update') {
                    this.merged = true;
                    this.allCheckboxes.forEach(checkbox => {
                        checkbox.disabled = true;
                    });
                }

                // Case of delete primary record
                if (type === 'delete') {
                    let idP = btn.getAttribute('data-id');
                    let optionElement = this.mergeWindow.querySelector('select[name="primaryId"] option[value="' + idP + '"]');
                    optionElement.remove();
                }
            }

            const data = await response.text();
            console.log(data);
            return data;
        }
    }

    n.qfqMerge = qfqMerge;

})(QfqNS.Helper);
/**
 * QFQ wrapper around the upstream nchan-messages JS Messenger.
 * Consumes the IIFE bundle exposed as window.NchanMessenger (built by esbuild
 * from node_modules/nchan-messages/js/index.js and concatenated first into
 * qfq.debug.js). Preserves the pre-existing DOM conventions used by Reports:
 *   .messenger-output, .messenger-input
 *   data-ws-group, data-ws-pub, data-ws-sub, data-ws-params, data-baseurl,
 *   data-ws-action, data-ws-payload
 * Adds data-ws-token for session-bound auth (minted server-side via the
 * _messengerToken report column).
 *
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 */

/* global console */
/* global qfqMessenger */
/* global $ */

/**
 * Qfq Namespace
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

/**
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    // Connection states from upstream nchan-messages ConnectionState enum.
    const STATUS_TO_STRING = ['waiting', 'connecting', 'configuring', 'connected', 'closing', 'closed'];

    // Pull upstream classes from the esbuild-bundled IIFE
    const Nchan = window.NchanMessenger || null;
    if (!Nchan) {
        // Degrade gracefully — let Reports that don't use messenger still work
        n.qfqMessenger = function () {
            console.warn('NchanMessenger bundle missing — qfqMessenger is disabled');
            return null;
        };
        return;
    }
    const { Messenger, UrlFactory } = Nchan;

    /**
     * MessengerElement - Base class for messenger input and output elements
     */
    class MessengerElement {
        constructor(element, messenger) {
            this.element = element;
            this.messenger = messenger;
            this.pubChannels = this.parseChannels(element.dataset.wsPub);
            this.subChannels = this.parseChannels(element.dataset.wsSub);
            this.wsGroupId = element.dataset.wsGroup || 'default';
            this.params = element.dataset.wsParams || '';
            this.baseUrl = element.dataset.baseurl || window.location.origin;
            this.action = element.dataset.wsAction || 'message';
            this.payload = element.dataset.wsPayload || '';
            this.tablesorterController = new QfqNS.TablesorterController();
        }

        parseChannels(channelStr) {
            return channelStr ? channelStr.split(',').map(ch => ch.trim()).filter(ch => ch) : [];
        }

        initialize() {
            // to be implemented by subclasses
        }
    }

    /**
     * MessengerOutput - Handles displaying messages
     */
    class MessengerOutput extends MessengerElement {
        constructor(element, messenger) {
            super(element, messenger);
            this.initialize();
        }

        initialize() {
            // Subscribe with channel filter — only receives messages from
            // this element's specific sub-channels. The upstream Messenger
            // maps nchan channel indices to names via _channels.sub (populated
            // from the onsub hook's 'hello' ctl message) and dispatches only
            // to subscribers registered for the matching channel.
            // Falls back to receiving all messages if _channels.sub isn't
            // populated (e.g. dev setup without nchan_subscribe_request).
            const channels = this.subChannels.length > 0 ? this.subChannels : undefined;
            this.messenger.subscribe({
                handleMessage: (msg) => {
                    let mode = msg.name || '';
                    switch (mode) {
                        case 'report':
                            this.getReportOutput(msg);
                            break;
                        case 'messenger':
                            this.appendMessage(msg);
                            break;
                        default:
                            this.replaceOutput(msg);
                    }
                },
                handleRequest: (request) => this.handleRequest(request)
            }, channels);
        }

        replaceOutput(message) {
            this.element.innerHTML = '';

            if (message.payload) {
                if (typeof message.payload === 'string') {
                    if (message.isHtml || (message.payload.trim().startsWith('<') && message.payload.trim().endsWith('>'))) {
                        this.element.insertAdjacentHTML('beforeend', message.payload);
                    } else {
                        const textNode = document.createTextNode(message.payload);
                        this.element.appendChild(textNode);
                    }
                } else if (typeof message.payload === 'object') {
                    if (message.payload.html) {
                        this.element.insertAdjacentHTML('beforeend', message.payload.html);
                    } else if (message.payload.text) {
                        const textNode = document.createTextNode(message.payload.text);
                        this.element.appendChild(textNode);
                    } else {
                        const pre = document.createElement('pre');
                        pre.textContent = JSON.stringify(message.payload, null, 2);
                        this.element.appendChild(pre);
                    }
                }
            } else if (message.html) {
                this.element.insertAdjacentHTML('beforeend', message.html);
            } else if (message.text) {
                const textNode = document.createTextNode(message.text);
                this.element.appendChild(textNode);
            } else {
                const textNode = document.createTextNode('Empty message');
                this.element.appendChild(textNode);
            }
        }

        getReportOutput(message) {
            const apiReport = this.baseUrl + (this.baseUrl.endsWith('/') ? '' : '/') + 'typo3conf/ext/qfq/Classes/Api/dataReport.php?uid=' + message.payload + '&' + this.params;

            fetch(apiReport, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: new URLSearchParams({})
            })
                .then(response => response.text())
                .then(data => {
                    this.element.innerHTML = '';
                    this.element.insertAdjacentHTML('beforeend', data);
                    if ($(this.element).find('table.tablesorter').length > 0) {
                        this.tablesorterController.setup($(this.element).find('table.tablesorter'));
                    }
                })
                .catch(error => {
                    console.error('Error:', error);
                });
        }

        appendMessage(message) {
            const msgElement = document.createElement('div');
            msgElement.className = `message message-${message.type || 'unknown'}`;

            const timestamp = message.timestamp ?
                new Date(message.timestamp).toLocaleTimeString() :
                new Date().toLocaleTimeString();

            const channel = message.channel ? `[${message.channel}] ` : '';
            const name = message.name ? `<strong>${message.name}:</strong> ` : '';

            let payloadContent = '';
            if (message.payload) {
                if (typeof message.payload === 'object') {
                    payloadContent = JSON.stringify(message.payload, null, 2);
                } else {
                    payloadContent = message.payload;
                }
            }

            msgElement.innerHTML = `
                <span class="timestamp">${timestamp}</span>
                <span class="channel">${channel}</span>
                ${name}
                <pre class="payload">${payloadContent}</pre>
            `;

            this.element.appendChild(msgElement);
            this.element.scrollTop = this.element.scrollHeight;
        }

        async handleRequest(request) {
            this.replaceOutput({
                type: 'request',
                timestamp: request.timestamp,
                channel: request.channel,
                payload: {
                    id: request.requestId,
                    reply: request.requester,
                    payload: request.payload
                }
            });

            const progressWait = request.payload.progress | 0;
            const responseWait = request.payload.response | 0;

            await new Promise(res => setTimeout(res, progressWait));
            await request.notify('Processing request...');

            await new Promise(res => setTimeout(res, responseWait));
            await request.respond('Request completed');
        }
    }

    /**
     * MessengerInput - Handles user input for sending messages
     */
    class MessengerInput extends MessengerElement {
        constructor(element, messenger) {
            super(element, messenger);

            this.payloadChannelMap = null;
            if (this.action === 'report' && this.payload.includes('|') &&
                this.pubChannels.length > 1) {
                this.preprocessReportPayload();
            }

            this.initialize();
        }

        initialize() {
            this.createInputControls();
            this.setupEventListeners();
        }

        createInputControls() {
            let inputType = 'default';
            if (this.element != null && this.element.classList.contains('drop-zone')) {
                inputType = 'drop-zone';
            }

            switch (inputType) {
                case 'drop-zone':
                    this.createDragAndDropControls();
                    break;
                default:
                    this.createMessengerInputControls();
                    break;
            }
        }

        createDragAndDropControls() {
            const dropArea = this.element;
            if (!dropArea.innerHTML.trim()) {
                const dropZone = document.createElement('div');
                dropZone.className = 'ws-drop-area';

                dropZone.innerHTML = `
            <div class="drop-message">
                <span class="drop-icon">📋</span>
                <p>Drag and drop</p>
            </div>
            <div class="dropped-items-container"></div>
        `;
                dropArea.appendChild(dropZone);

                const handleDragOver = function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    dropZone.classList.add('active');
                }.bind(this);

                const handleDragLeave = function() {
                    dropZone.classList.remove('active');
                }.bind(this);

                dropZone.addEventListener('dragover', handleDragOver);
                dropZone.addEventListener('dragleave', handleDragLeave);
                dropZone.addEventListener('drop', function(){
                    dropZone.classList.remove('active');
                });
            }

            this.element.addEventListener('drop', (e) => {
                e.preventDefault();
                e.stopPropagation();
                const content = e.dataTransfer.getData("text/plain");
                const contentElement = document.getElementById(content);
                if (contentElement) {
                    contentElement.getAttribute("data-record-id");
                }
                console.log(content);

                this.sendMessage(false);
            });
        }

        createMessengerInputControls() {
            const connectionControls = document.createElement('div');
            connectionControls.className = 'connection-controls';
            connectionControls.innerHTML = `
                <button class="connect-btn btn btn-default">Connect</button>
                <button class="disconnect-btn btn btn-default">Disconnect</button>
                <span class="status-text">Status: ${STATUS_TO_STRING[this.messenger.state] || 'unknown'}</span>
            `;
            this.element.appendChild(connectionControls);

            const messageInput = document.createElement('div');
            messageInput.className = 'message-input';
            messageInput.innerHTML = `
                <div class="input-row">
                    <label>Name: <input type="text" class="action-input" value="message"></label>
                </div>
                <div class="input-row">
                    <label>Payload: <textarea class="payload-input" rows="4" placeholder="JSON or text"></textarea></label>
                </div>
                <div class="input-row">
                    <button class="send-message-btn btn btn-default">Send Message</button>
                    <button class="send-request-btn btn btn-default">Send Request</button>
                </div>
            `;
            this.element.appendChild(messageInput);

            if (this.pubChannels.length > 1) {
                const channelSelect = document.createElement('div');
                channelSelect.className = 'channel-select';
                channelSelect.innerHTML = `
                    <label>Publish Channel:
                        <select class="channel-selector">
                            ${this.pubChannels.map(ch => `<option value="${ch}">${ch}</option>`).join('')}
                        </select>
                    </label>
                `;
                messageInput.insertBefore(channelSelect, messageInput.querySelector('.input-row:last-child'));
            }

            this.element.querySelector('.connect-btn').addEventListener('click', () => {
                this.messenger.open().then(
                    () => this.updateStatus('Connected'),
                    (err) => this.updateStatus(`Connection failed: ${err}`)
                );
            });

            this.element.querySelector('.disconnect-btn').addEventListener('click', () => {
                this.messenger.close().then(
                    () => this.updateStatus('Disconnected'),
                    (err) => this.updateStatus(`Disconnection failed: ${err}`)
                );
            });

            this.element.querySelector('.send-message-btn').addEventListener('click', () => {
                this.sendMessage(false);
            });

            this.element.querySelector('.send-request-btn').addEventListener('click', () => {
                this.sendMessage(true);
            });
        }

        setupEventListeners() {
            this.messenger.onstatechange = (evt) => {
                this.updateStatus(STATUS_TO_STRING[evt.currentValue] || 'unknown');
            };
        }

        updateStatus(status) {
            const statusText = this.element.querySelector('.status-text');
            if (statusText) {
                statusText.textContent = `Status: ${status}`;
            }
        }

        sendMessage(isRequest) {
            const nameInput = this.element.querySelector('.action-input');
            const name = nameInput ? nameInput.value : this.action;
            const payloadInput = this.element.querySelector('.payload-input');
            let payload = payloadInput ? payloadInput.value : this.payload;

            if (name === 'report' && this.payloadChannelMap) {
                this.handleMultiplePublish(name);
                return;
            }

            if (payload.trim().startsWith('{')) {
                try {
                    payload = JSON.parse(payload);
                } catch (e) {
                    // Keep as string if parsing fails
                }
            }

            let channels = this.pubChannels;
            const channelSelector = this.element.querySelector('.channel-selector');
            if (channelSelector) {
                channels = [channelSelector.value];
            }

            if (isRequest) {
                this.messenger.request(name, payload, { channels }).then(
                    async (response) => {
                        response.onprogress = (evt) => {
                            console.log('Progress:', evt.data);
                        };
                        const result = await response.payload();
                        console.log('Response:', result);
                    },
                    (err) => console.error('Request failed:', err)
                );
            } else {
                this.messenger.publish(name, payload, { channels });
            }

            if (payloadInput !== null) {
                payloadInput.value = '';
            }
        }

        preprocessReportPayload() {
            const payloadParts = this.payload.split('|');
            this.payloadChannelMap = [];

            payloadParts.forEach((part, index) => {
                if (index < this.pubChannels.length) {
                    let parsedPart = part;
                    if (part.trim().startsWith('{')) {
                        try {
                            parsedPart = JSON.parse(part);
                        } catch (e) {
                            // Keep as string if parsing fails
                        }
                    }

                    this.payloadChannelMap.push({
                        payload: parsedPart,
                        channel: this.pubChannels[index]
                    });
                }
            });
        }

        handleMultiplePublish(name) {
            this.payloadChannelMap.forEach(mapping => {
                this.messenger.publish(name, mapping.payload, {
                    channels: [mapping.channel]
                });
            });
        }
    }

    /**
     * MessengerApplication - Main application class.
     * Scans the DOM for .messenger-output/.messenger-input elements, groups
     * them by data-ws-group, mints one upstream Messenger per group with the
     * union of pub/sub channels and all supplied auth tokens.
     */
    class MessengerApplication {
        constructor(config = {}) {
            this.config = config;
            this.messengers = new Map();
            this.elements = [];
            this.endpoint = config.endpoint || '/messenger';
            this.baseUrl = config.baseUrl || window.location.origin;
        }

        initialize() {
            const outputElements = document.querySelectorAll('.messenger-output');
            const inputElements = document.querySelectorAll('.messenger-input');

            this.setupMessengersForElements([...outputElements, ...inputElements]);

            this.setupOutputElements(outputElements);
            this.setupInputElements(inputElements);

            if (this.config.autoConnect !== false) {
                this.connectAll();
            }
        }

        setupMessengersForElements(elements) {
            const groups = new Map();

            elements.forEach(el => {
                const wsGroupId = el.dataset.wsGroup || 'default';
                if (!groups.has(wsGroupId)) {
                    groups.set(wsGroupId, {
                        pub: new Set(),
                        sub: new Set(),
                        tokens: new Set(),
                        baseUrl: null
                    });
                }
                const bucket = groups.get(wsGroupId);

                if (el.dataset.wsPub) {
                    el.dataset.wsPub.split(',').map(c => c.trim()).filter(Boolean).forEach(c => bucket.pub.add(c));
                }
                if (el.dataset.wsSub) {
                    el.dataset.wsSub.split(',').map(c => c.trim()).filter(Boolean).forEach(c => bucket.sub.add(c));
                }
                if (el.dataset.wsToken) {
                    bucket.tokens.add(el.dataset.wsToken);
                }
                if (!bucket.baseUrl && el.dataset.baseurl) {
                    bucket.baseUrl = el.dataset.baseurl;
                }
            });

            groups.forEach((bucket, wsGroupId) => {
                // Resolve baseHref through window.location.origin so relative
                // values from TYPO3 ({{baseUrl:Y}} often yields '/subdir/')
                // work, not only absolute URLs. Empty falls back to origin.
                const baseHrefRaw = bucket.baseUrl || this.baseUrl || window.location.origin;
                let urlBase;
                try {
                    const absBase = new URL(baseHrefRaw, window.location.origin);
                    const baseWithSlash = absBase.href.endsWith('/') ? absBase.href : absBase.href + '/';
                    // Strip a leading slash so the endpoint resolves under the
                    // base path instead of replacing it.
                    const endpointPath = this.endpoint.replace(/^\//, '').replace(/\/?$/, '/');
                    urlBase = new URL(endpointPath, baseWithSlash);
                } catch (e) {
                    console.error('qfqMessenger: invalid base URL for group', wsGroupId, baseHrefRaw, e);
                    return;
                }

                const urlFactory = new UrlFactory(urlBase, wsGroupId, {
                    pub: [...bucket.pub],
                    sub: [...bucket.sub]
                });

                const messenger = new Messenger(urlFactory, { logger: console });

                // Workaround: without nchan_subscribe_request (disabled because
                // nchan's subrequest handler crashes on nginx <= 1.30), the
                // upstream Messenger never receives the 'hello' ctl message that
                // populates _channels. Set them manually so handleSocketMessage
                // passes incoming messages to processMessage instead of queuing
                // them forever in _received.
                messenger._channels.sub = [...bucket.sub];
                messenger._channels.pub = [...bucket.pub];

                if (bucket.tokens.size > 0) {
                    messenger.tokens.add(...bucket.tokens);
                } else {
                    console.warn(`qfqMessenger: group "${wsGroupId}" has no data-ws-token — auth will be rejected by nchan`);
                }

                this.messengers.set(wsGroupId, messenger);
            });
        }

        setupOutputElements(outputElements) {
            outputElements.forEach(el => {
                const wsGroupId = el.dataset.wsGroup || 'default';
                const messenger = this.messengers.get(wsGroupId);
                if (messenger) {
                    const output = new MessengerOutput(el, messenger);
                    this.elements.push(output);
                }
            });
        }

        setupInputElements(inputElements) {
            inputElements.forEach(el => {
                const wsGroupId = el.dataset.wsGroup || 'default';
                const messenger = this.messengers.get(wsGroupId);
                if (messenger) {
                    const input = new MessengerInput(el, messenger);
                    this.elements.push(input);
                }
            });
        }

        connectAll() {
            this.messengers.forEach((messenger, wsGroupId) => {
                messenger.open().then(
                    () => console.log(`Messenger ${wsGroupId} connected`),
                    (err) => console.error(`Messenger ${wsGroupId} connection failed:`, err)
                );
            });
        }

        disconnectAll() {
            this.messengers.forEach((messenger, wsGroupId) => {
                messenger.close().then(
                    () => console.log(`Messenger ${wsGroupId} disconnected`),
                    (err) => console.error(`Messenger ${wsGroupId} disconnection failed:`, err)
                );
            });
        }

        getMessenger(wsGroupId = 'default') {
            return this.messengers.get(wsGroupId);
        }
    }

    function qfqMessenger(config = {}) {
        const app = new MessengerApplication(config);
        app.initialize();
        return app;
    }

    n.qfqMessenger = qfqMessenger;

})(QfqNS.Helper);

/**
 * QFQ SyncByRule
 * Export/Import functionality with clipboard support and validation error handling
 *
 * Requires: qfqClipboard.js
 *
 * @author Enis Nuredini <enis.nuredini@math.uzh.ch>
 * @version 1.1
 */

/* global console */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * SyncByRule Error Types (must match server-side constants)
     */
    var SYNC_BY_RULE_ERROR_TYPES = {
        DATA_TYPE_MISMATCH: 'dataTypeMismatch',
        TABLE_NOT_FOUND: 'tableNotFound',
        CHECKSUM_INVALID: 'checksumInvalid',
        CHECKSUM_VERIFICATION_FAILED: 'checksumVerificationFailed'
    };

    /**
     * Error messages for different error types
     */
    var SYNC_BY_RULE_ERROR_MESSAGES = {
        dataTypeMismatch: 'Data type is not matching.',
        tableNotFound: 'Data type is not matching.',
        checksumInvalid: 'Data integrity check failed. The data may have been modified.',
        checksumVerificationFailed: 'Could not verify data integrity with source server.'
    };

    /**
     * Initialize SyncByRule for button and container elements
     *
     * @param {NodeList|Array} syncByRuleElements - Elements with class .qfq-syncByRule or .qfq-syncByRule-container
     * @param {object} n - Namespace reference
     */
    var qfqSyncByRule = function (syncByRuleElements, n) {
        if (syncByRuleElements !== undefined && syncByRuleElements.length > 0) {
            syncByRuleElements.forEach(function(element) {
                if (element.classList.contains('qfq-syncByRule-container')) {
                    // Container mode
                    new SyncByRuleContainerHandler(element, n);
                } else {
                    // Button mode (existing behavior)
                    new SyncByRulePasteHandler(element, n);
                }
            });
        }
    };

    /**
     * SyncByRule Core API
     * Provides programmatic access to export/import without UI
     */
    class SyncByRuleAPI {
        constructor(baseUrl) {
            this.baseUrl = baseUrl || '';
            this.apiBaseUrl = this.baseUrl + 'typo3conf/ext/qfq/Classes/Api/load.php';
        }

        /**
         * Execute Export - Returns JSON string
         *
         * @param {string} sip - SIP identifier
         * @param {object|null} ruleData - Optional rule data to send with request
         * @returns {Promise<string>} JSON string with exported data
         * @throws {Error} If export fails
         */
        /**
         * Execute Export - Returns JSON string
         *
         * @param {string} sip - SIP identifier
         * @param {object|null} ruleData - Optional rule data to send with request
         * @returns {Promise<string>} JSON string with exported data
         * @throws {Error} If export fails
         */
        async executeExport(sip, ruleData = null) {
            var body = {};
            if (ruleData && ruleData.ruleSips) {
                // Send array of rule SIPs
                body.ruleSips = ruleData.ruleSips;
            }

            var response = await fetch(this.apiBaseUrl + '?s=' + sip, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined
            });

            if (!response.ok) {
                throw new Error('HTTP error! status: ' + response.status);
            }

            var result = await response.json();

            if (result.status !== 'success') {
                throw new Error(result.message || 'Export failed');
            }

            var jsonData = result.syncByRuleData;

            if (!jsonData) {
                throw new Error('No data returned from copy');
            }

            return jsonData;
        }

        /**
         * Execute Import - Returns import result
         *
         * @param {string} sip - SIP identifier
         * @param {string|object} jsonData - JSON string or object to import
         * @returns {Promise<object>} Import result with success, imported items, idMapping
         * @throws {SyncByRuleValidationError} If validation fails (dataType mismatch, table not found, checksum invalid)
         * @throws {Error} If import fails for other reasons
         */
        async executeImport(sip, jsonData) {
            var jsonString = typeof jsonData === 'string' ? jsonData : JSON.stringify(jsonData);

            // Validate JSON
            try {
                JSON.parse(jsonString);
            } catch (e) {
                throw new Error('Invalid JSON: ' + e.message);
            }

            var response = await fetch(this.apiBaseUrl + '?s=' + sip, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    jsonData: jsonString
                })
            });

            if (!response.ok) {
                throw new Error('HTTP error! status: ' + response.status);
            }

            var result = await response.json();

            // Check for validation errors
            if (result.syncByRuleErrorType) {
                var errorType = result.syncByRuleErrorType;
                var errorMessage = result.syncByRuleErrorMessage ||
                    SYNC_BY_RULE_ERROR_MESSAGES[errorType] ||
                    'Validation error';

                throw new SyncByRuleValidationError(errorMessage, errorType);
            }

            if (result.status !== 'success') {
                throw new Error(result.message || 'Import failed');
            }

            var importResult = result.syncByRuleData;

            if (!importResult || !importResult.success) {
                throw new Error('Import failed');
            }

            return importResult;
        }
    }

    /**
     * Custom error class for SyncByRule validation errors
     */
    class SyncByRuleValidationError extends Error {
        constructor(message, errorType) {
            super(message);
            this.name = 'SyncByRuleValidationError';
            this.errorType = errorType;
        }

        /**
         * Get user-friendly error message based on error type
         */
        getUserMessage() {
            return SYNC_BY_RULE_ERROR_MESSAGES[this.errorType] || this.message;
        }

        /**
         * Get alert type for notification styling
         */
        getAlertType() {
            switch (this.errorType) {
                case SYNC_BY_RULE_ERROR_TYPES.DATA_TYPE_MISMATCH:
                case SYNC_BY_RULE_ERROR_TYPES.TABLE_NOT_FOUND:
                    return 'warning';
                case SYNC_BY_RULE_ERROR_TYPES.CHECKSUM_INVALID:
                case SYNC_BY_RULE_ERROR_TYPES.CHECKSUM_VERIFICATION_FAILED:
                    return 'danger';
                default:
                    return 'danger';
            }
        }
    }

    /**
     * SyncByRuleContainerHandler Class
     * Handles container with multiple rule elements
     */
    class SyncByRuleContainerHandler {
        constructor(containerElement, n) {
            this.container = containerElement;
            this.namespace = n;
            this.type = this.container.getAttribute('data-sync-by-rule-type');
            this.sip = this.container.getAttribute('data-sip');
            this.baseUrl = this.container.getAttribute('data-base-url') || '';

            // Create API instance
            this.api = new SyncByRuleAPI(this.baseUrl);

            // Get clipboard helper
            this.clipboard = n.Helper.qfqClipboard;

            // Find trigger button within container
            this.button = this.container.querySelector('.qfq-syncByRule-trigger');

            if (this.button) {
                this.originalHtml = this.button.innerHTML;
                this.originalClasses = this.button.className;
                this.button.addEventListener('click', this.handleClick.bind(this));
            }

            // Initialize checkbox handling
            this.initCheckboxHandling();
        }

        /**
         * Initialize checkbox event listeners and initial button state
         */
        initCheckboxHandling() {
            var self = this;
            var checkboxes = this.container.querySelectorAll('.qfq-syncByRule-element input[type="checkbox"]');
            this.selectAllCheckbox = this.container.querySelector('.qfq-syncByRule-checkbox-all');

            if (checkboxes.length > 0 && this.button) {
                // Set initial state (disabled since no checkboxes are checked)
                this.updateButtonState();

                // Add change listener to each checkbox
                checkboxes.forEach(function(checkbox) {
                    checkbox.addEventListener('change', function() {
                        self.updateButtonState();
                        self.updateSelectAllState();
                    });
                });

                // Handle "Select All" checkbox
                if (this.selectAllCheckbox) {
                    this.selectAllCheckbox.addEventListener('change', function() {
                        self.toggleAllCheckboxes(this.checked);
                    });
                }
            }
        }

        /**
         * Toggle all checkboxes on or off
         */
        toggleAllCheckboxes(checked) {
            var checkboxes = this.container.querySelectorAll('.qfq-syncByRule-element input[type="checkbox"]');
            checkboxes.forEach(function(checkbox) {
                checkbox.checked = checked;
            });
            this.updateButtonState();
        }

        /**
         * Update "Select All" checkbox state based on individual checkboxes
         */
        updateSelectAllState() {
            if (!this.selectAllCheckbox) {
                return;
            }

            var checkboxes = this.container.querySelectorAll('.qfq-syncByRule-element input[type="checkbox"]');
            var total = checkboxes.length;
            var checkedCount = Array.from(checkboxes).filter(function(cb) {
                return cb.checked;
            }).length;

            if (checkedCount === 0) {
                this.selectAllCheckbox.checked = false;
                this.selectAllCheckbox.indeterminate = false;
            } else if (checkedCount === total) {
                this.selectAllCheckbox.checked = true;
                this.selectAllCheckbox.indeterminate = false;
            } else {
                this.selectAllCheckbox.checked = false;
                this.selectAllCheckbox.indeterminate = true;
            }
        }

        /**
         * Update button disabled state based on checkbox selection
         */
        updateButtonState() {
            if (!this.button) {
                return;
            }

            var checkboxes = this.container.querySelectorAll('.qfq-syncByRule-element input[type="checkbox"]');
            var hasChecked = Array.from(checkboxes).some(function(cb) {
                return cb.checked;
            });

            if (hasChecked) {
                this.button.classList.remove('disabled');
            } else {
                this.button.classList.add('disabled');
            }
        }

        /**
         * Collect SIP strings from container elements
         * If checkboxes are present, only collect checked elements
         *
         * @returns {string[]|null} Array of SIP strings or null if none found
         */
        collectRules() {
            var ruleElements = this.container.querySelectorAll('.qfq-syncByRule-element');

            if (ruleElements.length === 0) {
                console.warn('SyncByRuleContainerHandler: No .qfq-syncByRule-element found');
                return null;
            }

            // Check if any element has a checkbox
            var hasCheckboxes = Array.from(ruleElements).some(function(el) {
                return el.querySelector('input[type="checkbox"]') !== null;
            });

            var ruleSips = [];

            ruleElements.forEach(function(el) {
                // If checkboxes are present, only include checked elements
                if (hasCheckboxes) {
                    var checkbox = el.querySelector('input[type="checkbox"]');
                    if (!checkbox || !checkbox.checked) {
                        return; // Skip unchecked elements
                    }
                }

                // Now data-rule contains a SIP string (encrypted), not JSON
                var ruleSip = el.getAttribute('data-rule');

                if (!ruleSip) {
                    console.warn('SyncByRuleContainerHandler: Element missing data-rule attribute', el);
                    return;
                }

                ruleSips.push(ruleSip);
            });

            if (ruleSips.length === 0) {
                return null;
            }

            return ruleSips;
        }

        /**
         * Handle button click
         */
        async handleClick(e) {
            e.preventDefault();

            // Don't execute if button is disabled (class or attribute)
            if (this.button.disabled || this.button.classList.contains('disabled')) {
                return;
            }

            var self = this;

            // Collect merged rules
            var ruleSips = this.collectRules();

            if (!ruleSips) {
                this.showNotification('warning', 'No elements selected for copy');
                return;
            }

            // Disable button during operation
            var wasDisabled = this.button.disabled;
            this.button.disabled = true;

            try {
                if (this.type === 'export') {
                    await this.handleExport(ruleSips);
                } else if (this.type === 'import') {
                    await this.handleImport(ruleSips);
                } else {
                    throw new Error('Unknown sync-by-rule-type: ' + this.type);
                }

            } catch (error) {
                console.error('SyncByRule container error:', error);

                // Handle validation errors specially
                if (error instanceof SyncByRuleValidationError) {
                    this.showNotification(error.getAlertType(), error.getUserMessage());
                } else {
                    this.showNotification('error', error.message || 'An error occurred');
                }
            } finally {
                // Re-enable button
                this.button.disabled = wasDisabled;
            }
        }

        /**
         * Handle Export with collected rule SIPs
         */
        async handleExport(ruleSips) {
            // Use API to get JSON, passing rule SIPs array
            var jsonData = await this.api.executeExport(this.sip, { ruleSips: ruleSips });
            var data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;

            // Add QFQ marker
            data.__qfq = true;

            // Copy to clipboard
            await this.clipboard.copy(data, { pretty: '\t', plainOnly: true });

            // Show success message
            this.showNotification('success', 'Copied to clipboard!');
        }

        /**
         * Handle Import with merged rules
         */
        async handleImport(mergedRule) {
            // Read from clipboard
            var qfqData = await this.clipboard.read();

            if (!qfqData) {
                throw new Error('No valid QFQ data in clipboard');
            }

            // Remove QFQ marker
            delete qfqData.__qfq;

            // Use API to import
            var importResult = await this.api.executeImport(this.sip, qfqData);

            var importedCount = (importResult.imported && importResult.imported.length) || 0;
            this.showNotification('success', 'Paste successful! Pasted ' + importedCount + ' element(s)');

            console.log('Import result:', importResult);
        }

        /**
         * Show notification to user
         */
        showNotification(type, message) {
            var alertType = 'info';
            if (type === 'success') {
                alertType = 'success';
            } else if (type === 'error' || type === 'danger') {
                alertType = 'danger';
            } else if (type === 'warning') {
                alertType = 'warning';
            }

            var alert = new QfqNS.Alert({
                message: message,
                type: alertType,
                timeout: 3000
            });
            alert.show();
        }
    }

    /**
     * SyncByRulePasteHandler Class
     * Handles individual export/import button with UI feedback
     */
    class SyncByRulePasteHandler {
        constructor(buttonElement, n) {
            this.button = buttonElement;
            this.namespace = n;
            this.type = this.button.getAttribute('data-sync-by-rule-type');
            this.sip = this.button.getAttribute('data-sip');
            this.baseUrl = this.button.getAttribute('data-base-url');
            this.originalHtml = this.button.innerHTML;
            this.originalClasses = this.button.className;
            this.hasValidClipboard = true;

            // Create API instance
            this.api = new SyncByRuleAPI(this.baseUrl);

            // Get clipboard helper
            this.clipboard = n.Helper.qfqClipboard;

            // Bind click handler
            this.button.addEventListener('click', this.handleClick.bind(this));

            // For import buttons: Add clipboard validation on hover/focus
            if (this.type === 'import') {
                this.initClipboardValidation();
            }
        }

        /**
         * Initialize clipboard validation for import buttons
         */
        initClipboardValidation() {
            var self = this;

            this.button.addEventListener('mouseenter', function() {
                self.validateClipboard();
            });

            this.button.addEventListener('focus', function() {
                self.validateClipboard();
            });

            window.addEventListener('focus', function() {
                if (self.button.matches(':hover')) {
                    self.validateClipboard();
                }
            });
        }

        /**
         * Validate clipboard content
         */
        async validateClipboard() {
            try {
                var isValid = await this.clipboard.hasValidData();
                this.setImportAllowed(isValid);
            } catch (err) {
                console.warn('Clipboard validation failed:', err.message);
                this.setImportAllowed(true);
            }
        }

        /**
         * Update button state based on clipboard validity
         */
        setImportAllowed(allowed) {
            this.hasValidClipboard = allowed;

            if (allowed) {
                this.button.classList.remove('qfq-import-forbidden');
                this.button.removeAttribute('title');
            } else {
                this.button.classList.add('qfq-import-forbidden');
                this.button.setAttribute('title', 'Clipboard contains no valid QFQ data');
            }
        }

        /**
         * Handle button click
         */
        async handleClick(e) {
            e.preventDefault();

            // Don't execute if button is disabled (class or attribute)
            if (this.button.disabled || this.button.classList.contains('disabled')) {
                return;
            }

            var self = this;
            var validatedData = null;

            // For import: Validate clipboard on click
            if (this.type === 'import') {
                try {
                    validatedData = await this.clipboard.read();
                    if (!validatedData) {
                        this.setImportAllowed(false);
                        this.showNotification('warning', 'Clipboard contains no valid QFQ data');
                        setTimeout(function() {
                            self.setImportAllowed(true);
                        }, 5000);
                        return;
                    }
                } catch (err) {
                    this.showNotification('error', 'Cannot read clipboard: ' + err.message);
                    return;
                }
            }

            // Disable button during operation
            var wasDisabled = this.button.disabled;
            this.button.disabled = true;

            try {
                if (this.type === 'export') {
                    await this.handleExport();
                } else if (this.type === 'import') {
                    await this.handleImport(validatedData);
                } else {
                    throw new Error('Unknown copy-paste-type: ' + this.type);
                }

            } catch (error) {
                console.error('SyncByRule error:', error);

                // Handle validation errors specially
                if (error instanceof SyncByRuleValidationError) {
                    this.showNotification(error.getAlertType(), error.getUserMessage());
                } else {
                    this.showNotification('error', error.message || 'An error occurred');
                }
            } finally {
                // Re-enable button
                this.button.disabled = wasDisabled;
            }
        }

        /**
         * Handle Export with UI feedback
         */
        async handleExport() {
            // Use API to get JSON
            var jsonData = await this.api.executeExport(this.sip);
            var data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;

            // Add QFQ marker
            data.__qfq = true;

            // Copy to clipboard with multiple formats
            await this.clipboard.copy(data, { pretty: '\t', plainOnly: true });

            // Show success message
            this.showNotification('success', 'Copied to clipboard!');
        }

        /**
         * Handle Import with UI feedback
         * @param {object} qfqData - Pre-validated QFQ data from clipboard
         */
        async handleImport(qfqData) {
            if (!qfqData) {
                throw new Error('No valid QFQ data in clipboard');
            }

            // Remove QFQ marker before sending to server
            delete qfqData.__qfq;

            // Use API to import
            var importResult = await this.api.executeImport(this.sip, qfqData);

            // Show success message with details
            var importedCount = (importResult.imported && importResult.imported.length) || 0;
            this.showNotification('success', 'Paste successful! Pasted ' + importedCount + ' element(s)');

            // Log details
            console.log('Import result:', importResult);
        }

        /**
         * Show notification to user
         */
        showNotification(type, message) {
            var alertType = 'info';
            if (type === 'success') {
                alertType = 'success';
            } else if (type === 'error' || type === 'danger') {
                alertType = 'danger';
            } else if (type === 'warning') {
                alertType = 'warning';
            }

            var alert = new QfqNS.Alert({
                message: message,
                type: alertType,
                timeout: 3000
            });
            alert.show();
        }
    }

    // Create and export default API instance
    n.Helper.qfqSyncByRuleObj = new SyncByRuleAPI();

    // Export API class
    n.Helper.SyncByRuleAPI = SyncByRuleAPI;

    // Export validation error class
    n.Helper.SyncByRuleValidationError = SyncByRuleValidationError;

    // Export error types
    n.Helper.SYNC_BY_RULE_ERROR_TYPES = SYNC_BY_RULE_ERROR_TYPES;

    // Export button initialization function
    n.Helper.qfqSyncByRule = qfqSyncByRule;

})(QfqNS);
/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */

var QfqNS = QfqNS || {};

/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';

    /**
     * Initializes all FEs type select with the selectBS notation.
     * Only elements having the class "qfq-select-bs-parent" are initialized.
     *
     * @function
     */
    let selectBS = function () {

        // all selectBS <div> elements that contain the button and list used for the dropdown
        const dropdownParent = document.querySelectorAll(".qfq-select-bs-parent");

        let lastCursorPos = {x: 0, y: 0};
        let disabledClick = false;

        const observer = new MutationObserver(function (mutations) {
            mutations.forEach(function (mutation) {

                // selected option from dropdown
                let prevSelectedItem = mutation.target.querySelector(".qfq-select-bs-list > li[data-selected]");

                // focused option(s) from dropdown
                let prevFocusedItem = mutation.target.querySelector(".qfq-select-bs-list > li.qfq-item-focused");

                // remove class/style
                if (prevFocusedItem !== prevSelectedItem) {
                    prevFocusedItem.classList.remove('qfq-item-focused');
                }

                if (!prevSelectedItem) {
                    return;
                }

                prevSelectedItem.classList.add('qfq-item-focused');

                // dropdown is open
                if (mutation.target.classList.contains("open")) {

                    // scroll selected item into center of dropdown
                    prevSelectedItem.parentElement.scrollTop = prevSelectedItem.offsetTop - prevSelectedItem.parentElement.offsetHeight / 2;
                }
            });
        });

        dropdownParent.forEach(function (parentItem) {
            const enabledItems = parentItem.querySelectorAll(".qfq-select-bs-list > li:not(.qfq-item-disabled, .qfq-dropdown-item-disabled)");
            const disabledItems = parentItem.querySelectorAll(".qfq-select-bs-list > li.qfq-item-disabled, .qfq-select-bs-list > li.qfq-dropdown-item-disabled");
            const dropdownButton = parentItem.querySelector(".qfq-select-bs-button");

            // adding events to all options
            enabledItems.forEach(function (item) {

                // hover style
                item.addEventListener("mousemove", function (event) {

                    // set current cursor position
                    let currentCursorPos = {x: event.screenX, y: event.screenY};

                    // cursor has not been moved
                    if (currentCursorPos.x === lastCursorPos.x && currentCursorPos.y === lastCursorPos.y) {
                        return;
                    }

                    lastCursorPos = {x: event.screenX, y: event.screenY};

                    let previousFocusedItem = parentItem.querySelector(".qfq-select-bs-list > li.qfq-item-focused");
                    previousFocusedItem.classList.remove('qfq-item-focused');

                    item.classList.add('qfq-item-focused');
                });

                // insert value
                item.addEventListener("click", function (event) {
                    selectBS.insertValue(item);
                    parentItem.classList.toggle("open");
                    dropdownButton.focus();
                });
            });

            disabledItems.forEach(function (item) {

                // nothing happens
                item.addEventListener("click", function (event) {
                    disabledClick = true;
                    dropdownButton.focus();
                    event.preventDefault();
                    event.stopPropagation();
                });
            });

            dropdownButton.addEventListener("click", function (event) {
                parentItem.classList.toggle("open");
            });

            dropdownButton.addEventListener("blur", function(event) {

                // wait for other stuff to finish
                setTimeout(function() {
                    if (disabledClick) {
                        disabledClick = false;
                        return;
                    }

                    parentItem.classList.remove("open");
                }, 200);
            });

            dropdownButton.addEventListener("keydown", function (event) {
                let previousSelectedItem = parentItem.querySelector(".qfq-select-bs-list > li.qfq-item-focused");
                let previousIndex = Array.prototype.indexOf.call(enabledItems, previousSelectedItem);

                let selectedItem = null;
                let selectedItemSibling = null;
                let scroll = null;

                switch (event.key) {
                    case 'ArrowDown':
                        event.preventDefault();
                        selectedItem = (previousSelectedItem) ? enabledItems[previousIndex + 1] : enabledItems[0];
                        selectedItemSibling = selectedItem.nextSibling || null;

                        // determine position of item in viewport
                        scroll = selectedItem.getBoundingClientRect().bottom - selectedItem.parentElement.getBoundingClientRect().bottom + 3;

                        // scroll > 0 means that the item is outside the lower border
                        scroll = (scroll > 0) ? scroll : 0;
                        break;

                    case 'ArrowUp':
                        event.preventDefault();
                        selectedItem = (previousSelectedItem) ? enabledItems[previousIndex - 1] : null;
                        selectedItemSibling = selectedItem.previousSibling || null;

                        // determine position of item in viewport
                        scroll = selectedItem.getBoundingClientRect().top - selectedItem.parentElement.getBoundingClientRect().top - 3;

                        // scroll < 0 means that the item is outside the top border
                        scroll = (scroll < 0) ? scroll : 0;
                        break;

                    case 'Enter':
                        event.preventDefault();
                        selectBS.insertValue(previousSelectedItem);
                        parentItem.classList.toggle("open");
                        return;

                    case 'Escape':
                        parentItem.classList.remove("open");
                        return;

                    default:
                        return;
                }

                // dropdown is closed, sibling exists
                if (dropdownButton === document.activeElement && !parentItem.classList.contains("open") && selectedItemSibling && selectedItem) {
                    selectBS.insertValue(selectedItem);
                    if (previousSelectedItem) previousSelectedItem.classList.toggle('qfq-item-focused');
                    selectedItem.classList.toggle("qfq-item-focused");

                // dropdown is open, sibling exists
                } else if (dropdownButton === document.activeElement && parentItem.classList.contains("open") && selectedItemSibling && selectedItem) {

                    if (previousSelectedItem) previousSelectedItem.classList.toggle('qfq-item-focused');
                    selectedItem.classList.toggle("qfq-item-focused");

                    // scroll if item is outside of viewport
                    selectedItem.parentElement.scrollBy(0, scroll);
                }
            });

            // mutation observer
            observer.observe(parentItem, {
                attributes: true,
                attributeFilter: ["class"]
            });
        });
    }

    // insert value of option into button and hidden input
    selectBS.insertValue = function(element) {
        let dropdownParent = element.closest(".qfq-select-bs-parent");
        let button = dropdownParent.querySelector("button");
        let input = dropdownParent.querySelector("input");
        button.innerHTML = element.innerHTML + ` <span class="caret"></span>`;
        input.value = element.getAttribute("value");
        let event = new Event("input");
        input.dispatchEvent(event);

        // mark (only) current option as selected
        if (!element.getAttribute("data-selected")) {
            if (element.parentElement.querySelector("li[data-selected]")) {
                element.parentElement.querySelector("li[data-selected]").removeAttribute("data-selected");
            }
            element.setAttribute("data-selected", "");
        }
    };

    n.selectBS = selectBS;

})(QfqNS.Helper);
/* ===================================================
 * tagmanager.js v3.0.2
 * http://welldonethings.com/tags/manager
 * ===================================================
 * Copyright 2012 Max Favilli
 *
 * Licensed under the Mozilla Public License, Version 2.0 You may not use this work except in compliance with the License.
 *
 * http://www.mozilla.org/MPL/2.0/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ========================================================== */
(function ($) {

    "use strict";

    var defaults = {
            prefilled: null,
            CapitalizeFirstLetter: false,
            preventSubmitOnEnter: true,     // deprecated
            isClearInputOnEsc: true,        // deprecated
            externalTagId: false,
            prefillIdFieldName: 'Id',
            prefillValueFieldName: 'Value',
            AjaxPush: null,
            AjaxPushAllTags: null,
            AjaxPushParameters: null,
            delimiters: [9, 13, 44],        // tab, enter, comma
            backspace: [8],
            maxTags: 0,
            hiddenTagListName: null,        // deprecated
            hiddenTagListId: null,          // deprecated
            replace: true,
            output: null,
            deleteTagsOnBackspace: true,    // deprecated
            tagsContainer: null,
            tagCloseIcon: '<i class="fas fa-trash"></i>',
            tagClass: '',
            validator: null,
            onlyTagList: false,
            tagList: null,
            fillInputOnTagRemove: false,
            AjaxPushDataType: 'json' //allow plugin to send data using different encodings (xml, json, script, text, html)
        },

        publicMethods = {
            pushTag: function (tag, ignoreEvents, externalTagId, ignoreValidator) {
                var $self = $(this), opts = $self.data('opts'), alreadyInList, tlisLowerCase, max, tagId,
                    tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx, newTagId, newTagRemoveId, escaped,
                    html, $el, lastTagId, lastTagObj;

                tag = privateMethods.trimTag(tag, opts.delimiterChars);

                if (!tag || tag.length <= 0) {
                    return;
                }

                // check if restricted only to the tagList suggestions
                if (opts.onlyTagList && undefined !== opts.tagList) {

                    //if the list has been updated by look pushed tag in the tagList. if not found return
                    if (opts.tagList) {
                        var $tagList = opts.tagList;

                        // change each array item to lower case
                        $.each($tagList, function (index, item) {
                            $tagList[index] = item.toLowerCase();
                        });
                        var suggestion = $.inArray(tag.toLowerCase(), $tagList);

                        if (-1 === suggestion) {
                            //console.log("tag:" + tag + " not in tagList, not adding it");
                            return;
                        }
                    }

                }

                if (opts.CapitalizeFirstLetter && tag.length > 1) {
                    tag = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
                }

                // call the validator (if any) and do not let the tag pass if invalid
                if (!ignoreValidator && opts.validator && !opts.validator(tag) && !opts.disableHiddenUpdate) {
                    $self.trigger('tm:invalid', tag);
                    return;
                }

                // dont accept new tags beyond the defined maximum
                if (opts.maxTags > 0 && tlis.length >= opts.maxTags) {
                    return;
                }

                alreadyInList = false;
                //use jQuery.map to make this work in IE8 (pure JS map is JS 1.6 but IE8 only supports JS 1.5)
                tlisLowerCase = jQuery.map(tlis, function (elem) {
                    return elem.toLowerCase();
                });

                idx = $.inArray(tag.toLowerCase(), tlisLowerCase);

                if (-1 !== idx) {
                    // console.log("tag:" + tag + " !!already in list!!");
                    alreadyInList = true;
                }

                if (alreadyInList) {
                    $self.trigger('tm:duplicated', tag);
                    if (opts.blinkClass) {
                        for (var i = 0; i < 6; ++i) {
                            $("#" + $self.data("tm_rndid") + "_" + tlid[idx]).queue((function (_$, loc_opts, next) {
                                _$(this).toggleClass(loc_opts.blinkClass);
                                next();
                            }).bind(this, $, opts)).delay(100);
                        }
                    } else {
                        $("#" + $self.data("tm_rndid") + "_" + tlid[idx]).stop()
                            .animate({backgroundColor: opts.blinkBGColor_1}, 100)
                            .animate({backgroundColor: opts.blinkBGColor_2}, 100)
                            .animate({backgroundColor: opts.blinkBGColor_1}, 100)
                            .animate({backgroundColor: opts.blinkBGColor_2}, 100)
                            .animate({backgroundColor: opts.blinkBGColor_1}, 100)
                            .animate({backgroundColor: opts.blinkBGColor_2}, 100);
                    }
                } else {
                    if (opts.externalTagId === true) {
                        if (externalTagId === undefined) {
                            $.error('externalTagId is not passed for tag -' + tag);
                        }
                        tagId = externalTagId;
                    } else {
                        max = Math.max.apply(null, tlid);
                        max = max === -Infinity ? 0 : max;

                        tagId = ++max;
                    }
                    if (!ignoreEvents) {
                        $self.trigger('tm:pushing', [tag, tagId]);
                    }
                    tlis.push(tag);
                    tlid.push(tagId);

                    if (!ignoreEvents)
                        if (opts.AjaxPush !== null && opts.AjaxPushAllTags == null) {
                            if ($.inArray(tag, opts.prefilled) === -1) {
                                $.post(opts.AjaxPush, $.extend({tag: tag}, opts.AjaxPushParameters), null, opts.AjaxPushDataType);
                            }
                        }

                    // console.log("tagList: " + tlis);

                    newTagId = $self.data("tm_rndid") + '_' + tagId;
                    newTagRemoveId = $self.data("tm_rndid") + '_Remover_' + tagId;
                    escaped = $("<span/>").text(tag).html();

                    html = '<span class="' + privateMethods.tagClasses.call($self) + '" id="' + newTagId + '">';
                    html += '<span>' + escaped + '</span>';
                    html += '<a href="#" class="tm-tag-remove" id="' + newTagRemoveId + '" TagIdToRemove="' + tagId + '">';
                    html += opts.tagCloseIcon + '</a></span> ';
                    $el = $(html);


                    var typeAheadMess = $self.parents('.twitter-typeahead')[0] !== undefined;
                    if (opts.tagsContainer !== null) {
                        $(opts.tagsContainer).append($el);
                    } else {
                        if (tlid.length > 1) {
                            if (typeAheadMess) {
                                lastTagId = $self.data("tm_rndid") + '_' + --tagId;
                                jQuery('#' + lastTagId).after($el);
                            } else {
                                lastTagObj = $self.siblings("#" + $self.data("tm_rndid") + "_" + tlid[tlid.length - 2]);
                                lastTagObj.after($el);
                            }
                        } else {
                            if (typeAheadMess) {
                                $self.parents('.twitter-typeahead').before($el);
                            } else {
                                $self.before($el);
                            }
                        }
                    }

                    $el.find("#" + newTagRemoveId).on("click", $self, function (e) {
                        e.preventDefault();
                        var TagIdToRemove = parseInt($(this).attr("TagIdToRemove"));
                        privateMethods.spliceTag.call($self, TagIdToRemove, e.data);
                    });

                    if (!ignoreEvents) {
                        $self.trigger('tm:pushed', [tag, tagId]);
                    }

                    privateMethods.refreshHiddenTagList.call($self);

                    privateMethods.showOrHide.call($self);
                    //if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) {
                    //  obj.hide();
                    //}
                }
                $self.val("");

                // empty field
                (this).typeahead('val', '');
            },

            popTag: function () {
                var $self = $(this), tagId, tagBeingRemoved,
                    tlis = $self.data("tlis"),
                    tlid = $self.data("tlid");

                if (tlid.length > 0) {
                    tagId = tlid.pop();

                    tagBeingRemoved = tlis[tlis.length - 1];
                    $self.trigger('tm:popping', [tagBeingRemoved, tagId]);
                    tlis.pop();

                    // console.log("TagIdToRemove: " + tagId);
                    $("#" + $self.data("tm_rndid") + "_" + tagId).remove();
                    privateMethods.refreshHiddenTagList.call($self);
                    $self.trigger('tm:popped', [tagBeingRemoved, tagId]);

                    privateMethods.showOrHide.call($self);
                    // console.log(tlis);
                }
            },

            empty: function () {
                var $self = $(this), tlis = $self.data("tlis"), tlid = $self.data("tlid"), tagId;

                while (tlid.length > 0) {
                    tagId = tlid.pop();
                    tlis.pop();
                    // console.log("TagIdToRemove: " + tagId);
                    $("#" + $self.data("tm_rndid") + "_" + tagId).remove();
                    privateMethods.refreshHiddenTagList.call($self);
                    // console.log(tlis);
                }
                $self.trigger('tm:emptied', null);

                privateMethods.showOrHide.call($self);
                //if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
                //  obj.show();
                //}
            },

            tags: function () {
                var $self = this, tlis = $self.data("tlis");
                return tlis;
            }
        },

        privateMethods = {
            showOrHide: function () {
                var $self = this, opts = $self.data('opts'), tlis = $self.data("tlis");

                if (opts.maxTags > 0 && tlis.length < opts.maxTags) {
                    $self.show();
                    $self.trigger('tm:show');
                }

                if (opts.maxTags > 0 && tlis.length >= opts.maxTags) {
                    $self.hide();
                    $self.trigger('tm:hide');
                }
            },

            tagClasses: function () {
                var $self = $(this), opts = $self.data('opts'), tagBaseClass = opts.tagBaseClass,
                    inputBaseClass = opts.inputBaseClass, cl;
                // 1) default class (tm-tag)
                cl = tagBaseClass;
                // 2) interpolate from input class: tm-input-xxx --> tm-tag-xxx
                if ($self.attr('class')) {
                    $.each($self.attr('class').split(' '), function (index, value) {
                        if (value.indexOf(inputBaseClass + '-') !== -1) {
                            cl += ' ' + tagBaseClass + value.substring(inputBaseClass.length);
                        }
                    });
                }
                // 3) tags from tagClass option
                cl += (opts.tagClass ? ' ' + opts.tagClass : '');
                return cl;
            },

            trimTag: function (tag, delimiterChars) {
                var i;
                tag = $.trim(tag);
                // truncate at the first delimiter char
                i = 0;
                for (i; i < tag.length; i++) {
                    if ($.inArray(tag.charCodeAt(i), delimiterChars) !== -1) {
                        break;
                    }
                }
                return tag.substring(0, i);
            },

            refreshHiddenTagList: function () {
                var $self = $(this), opts = $self.data('opts'), tlis = $self.data("tlis"), lhiddenTagList = $self.data("lhiddenTagList");
                if (!opts.disableHiddenUpdate) {

                    if (lhiddenTagList) {
                        $(lhiddenTagList).val(tlis.join($self.data('opts').baseDelimiter)).change();
                        $self.trigger('tm:hiddenUpdate', [tlis, $(lhiddenTagList)]);
                    }

                    $self.trigger('tm:refresh', tlis.join($self.data('opts').baseDelimiter));
                }
            },

            killEvent: function (e) {
                e.cancelBubble = true;
                e.returnValue = false;
                e.stopPropagation();
                e.preventDefault();
            },

            keyInArray: function (e, ary) {
                return $.inArray(e.which, ary) !== -1;
            },

            applyDelimiter: function (e) {
                var $self = $(this);
                publicMethods.pushTag.call($self, $(this).val());
                e.preventDefault();
            },

            prefill: function (pta) {
                var $self = $(this);
                var opts = $self.data('opts');
                $.each(pta, function (key, val) {
                    if (opts.externalTagId === true) {
                        publicMethods.pushTag.call($self, val[opts.prefillValueFieldName], true, val[opts.prefillIdFieldName], true);
                    } else {
                        publicMethods.pushTag.call($self, val, true, false, true);
                    }
                });
            },

            pushAllTags: function (e, tag) {
                var $self = $(this), opts = $self.data('opts'), tlis = $self.data("tlis");
                if (opts.AjaxPushAllTags) {
                    if (e.type !== 'tm:pushed' || $.inArray(tag, opts.prefilled) === -1) {
                        $.post(opts.AjaxPush, $.extend({tags: tlis.join(opts.baseDelimiter)}, opts.AjaxPushParameters));
                    }
                }
            },

            spliceTag: function (tagId) {
                var $self = this, tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx = $.inArray(tagId, tlid),
                    tagBeingRemoved;

                // console.log("TagIdToRemove: " + tagId);
                // console.log("position: " + idx);

                if (-1 !== idx) {
                    tagBeingRemoved = tlis[idx];
                    $self.trigger('tm:splicing', [tagBeingRemoved, tagId]);
                    $("#" + $self.data("tm_rndid") + "_" + tagId).remove();
                    tlis.splice(idx, 1);
                    tlid.splice(idx, 1);
                    privateMethods.refreshHiddenTagList.call($self);
                    $self.trigger('tm:spliced', [tagBeingRemoved, tagId]);
                    // console.log(tlis);
                }

                privateMethods.showOrHide.call($self);
                //if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
                //  obj.show();
                //}
            },

            init: function (options) {
                var opts = $.extend({}, defaults, options), delimiters, keyNums;

                opts.hiddenTagListName = (opts.hiddenTagListName === null) ? 'hidden-' + this.attr('name') : opts.hiddenTagListName;

                delimiters = opts.delimeters || opts.delimiters; // 'delimeter' is deprecated
                keyNums = [9, 13, 17, 18, 19, 37, 38, 39, 40]; // delimiter values to be handled as key codes
                opts.delimiterChars = [];
                opts.delimiterKeys = [];

                $.each(delimiters, function (i, v) {
                    if ($.inArray(v, keyNums) !== -1) {
                        opts.delimiterKeys.push(v);
                    } else {
                        opts.delimiterChars.push(v);
                    }
                });

                opts.baseDelimiter = String.fromCharCode(opts.delimiterChars[0] || 44);
                opts.tagBaseClass = 'tm-tag';
                opts.inputBaseClass = 'tm-input';
                opts.disableHiddenUpdate = false;

                if (!$.isFunction(opts.validator)) {
                    opts.validator = null;
                }

                this.each(function () {
                    var $self = $(this), hiddenObj = '', rndid = '',
                        albet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

                    // prevent double-initialization of TagManager
                    if ($self.data('tagManager')) {
                        return false;
                    }
                    $self.data('tagManager', true);

                    for (var i = 0; i < 5; i++) {
                        rndid += albet.charAt(Math.floor(Math.random() * albet.length));
                    }

                    $self.data("tm_rndid", rndid);

                    // store instance-specific data in the DOM object
                    $self.data('opts', opts)
                        .data('tlis', []) //list of string tags
                        .data('tlid', []); //list of ID of the string tags

                    if (opts.output === null) {
                        hiddenObj = $('<input/>', {
                            type: 'hidden',
                            name: opts.hiddenTagListName
                        });
                        $self.after(hiddenObj);
                        $self.data("lhiddenTagList", hiddenObj);
                    } else {
                        $self.data("lhiddenTagList", $(opts.output));
                    }

                    if (opts.AjaxPushAllTags) {
                        $self.on('tm:spliced', privateMethods.pushAllTags);
                        $self.on('tm:popped', privateMethods.pushAllTags);
                        $self.on('tm:pushed', privateMethods.pushAllTags);
                    }

                    // hide popovers on focus and keypress events
                    $self.on('focus keypress', function (e) {
                        if ($(this).popover) {
                            $(this).popover('hide');
                        }
                    });

                    // handle ESC (keyup used for browser compatibility)
                    if (opts.isClearInputOnEsc) {
                        $self.on('keyup', function (e) {
                            if (e.which === 27) {
                                // console.log('esc detected');
                                $(this).val('');
                                privateMethods.killEvent(e);
                            }
                        });
                    }

                    $self.on('keypress', function (e) {
                        // push ASCII-based delimiters
                        if (privateMethods.keyInArray(e, opts.delimiterChars)) {
                            privateMethods.applyDelimiter.call($self, e);
                        }
                    });

                    $self.on('keydown', function (e) {
                        // disable ENTER
                        if (e.which === 13) {
                            if (opts.preventSubmitOnEnter) {
                                privateMethods.killEvent(e);
                            }
                        }

                        // push key-based delimiters (includes <enter> by default)
                        if (privateMethods.keyInArray(e, opts.delimiterKeys)) {
                            privateMethods.applyDelimiter.call($self, e);
                        }
                    });

                    // BACKSPACE (keydown used for browser compatibility)
                    if (opts.deleteTagsOnBackspace) {
                        $self.on('keydown', function (e) {
                            if (privateMethods.keyInArray(e, opts.backspace)) {
                                // console.log("backspace detected");
                                if ($(this).val().length <= 0) {
                                    publicMethods.popTag.call($self);
                                    privateMethods.killEvent(e);
                                }
                            }
                        });
                    }

                    // on tag pop fill back the tag's content to the input field
                    if (opts.fillInputOnTagRemove) {
                        $self.on('tm:popped', function (e, tag) {
                            $(this).val(tag);
                        });
                    }

                    $self.change(function (e) {
                        if (!/webkit/.test(navigator.userAgent.toLowerCase())) {
                            $self.focus();
                        } // why?

                        /* unimplemented mode to push tag on blur
                         else if (tagManagerOptions.pushTagOnBlur) {
                         console.log('change: pushTagOnBlur ' + tag);
                         pushTag($(this).val());
                         } */
                        privateMethods.killEvent(e);
                    });

                    if (opts.prefilled !== null) {
                        if (typeof (opts.prefilled) === "object") {
                            privateMethods.prefill.call($self, opts.prefilled);
                        } else if (typeof (opts.prefilled) === "string") {
                            privateMethods.prefill.call($self, opts.prefilled.split(opts.baseDelimiter));
                        } else if (typeof (opts.prefilled) === "function") {
                            privateMethods.prefill.call($self, opts.prefilled());
                        }
                    } else if (opts.output !== null) {
                        if ($(opts.output) && $(opts.output).val()) {
                            var existing_tags = $(opts.output);
                        }
                        privateMethods.prefill.call($self, $(opts.output).val().split(opts.baseDelimiter));
                    }

                });

                return this;
            }
        };

    $.fn.tagsManager = function (method) {
        var $self = $(this), opts = $self.data('opts');

        if (!(0 in this)) {
            return this;
        }

        if (publicMethods[method]) {
            return publicMethods[method].apply($self, Array.prototype.slice.call(arguments, 1));
        } else if (typeof method === 'object' || !method) {
            return privateMethods.init.apply(this, arguments);
        } else if (method === 'disableHiddenUpdate') {
            opts.disableHiddenUpdate = arguments[1];
        } else {
            $.error('Method ' + method + ' does not exist.');
            return false;
        }
    };

}(jQuery));
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */
/* global console */
/* global tinymce */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq Helper Namespace
 *
 * @namespace QfqNS.Helper
 */
QfqNS.Helper = QfqNS.Helper || {};

(function (n) {
    'use strict';


    /**
     * Initialize tinyMce. All HTML elements having the `qfq-tinymce` class will be initialized. TinyMce configuration
     * is provided in the `data-config` attribute. Each element further requires the `id` attribute to be present.
     *
     * @function
     */
    var tinyMce = function () {

        if (typeof tinymce === 'undefined') {
            //QfqNS.log.error("tinymce not loaded, cannot initialize Qfq tinymce.");
            return;
        }
        function showImageModal(imageUrl) {
            let $modal = $('#tinymce-image-modal');

            // Create modal if it doesn't exist
            if (!$modal.length) {
                $modal = $(`
            <div id="tinymce-image-modal" style="
                position: fixed;
                top: 0; left: 0; width: 100%; height: 100%;
                background: rgba(0,0,0,0.8);
                display: flex; align-items: center; justify-content: center;
                z-index: 9999;
            ">
                <img id="tinymce-image-modal-img" style="
                    max-width: 90%; max-height: 90%; border: 4px solid white;
                    box-shadow: 0 0 20px rgba(0,0,0,0.5);
                "/>
            </div>
        `).appendTo('body');

                $modal.on('click', function () {
                    $modal.fadeOut(200);
                });
            }

            $('#tinymce-image-modal-img').attr('src', imageUrl);
            $modal.fadeIn(200);
        }

        $(".qfq-tinymce").each(
            function () {
                var config = {};
                var myEditor = {};
                var $this = $(this);
                var tinyMCEId = $this.attr('id');
                var defaultImageClass = $this.attr('defaultImageClass');
                var defaultImageBorder = $this.attr('imageBorderDefault');

                if (!tinyMCEId) {
                    QfqNS.Log.warning("TinyMCE container does not have an id attribute. Ignoring.");
                    return;
                }

                // Skip elements that already have a TinyMCE instance
                if (tinymce.get(tinyMCEId)) {
                    return;
                }

                var configData = $this.data('config');
                if (configData) {
                    if (configData instanceof Object) {

                        // Iterate through properties to check if they are valid JSON
                        for (const key in configData) {
                            if (typeof configData[key] === 'string') {
                                try {

                                    // Attempt to parse the string
                                    const parsedValue = JSON.parse(configData[key]);

                                    // Check if parsedValue is an object or an array of objects
                                    if (typeof parsedValue === 'object' || Array.isArray(parsedValue)) {

                                        // Replace with parsed object
                                        configData[key] = parsedValue;
                                    }
                                } catch {
                                    // Do nothing
                                }
                            }
                        }

                        // jQuery takes care of decoding data-config to JavaScript object.
                        config = configData;
                    } else {
                        QfqNS.Log.warning("'data-config' is invalid: " + configData);
                    }
                }

                config.selector = "#" + QfqNS.escapeJqueryIdSelector(tinyMCEId);
                config.setup = function (editor) {
                    myEditor = editor;
                    var element = document.getElementById(QfqNS.escapeJqueryIdSelector(tinyMCEId));
                    var isReadOnly = $(element).hasClass("readonly");

                    // QFQ AI assist: register one 'AI' dropdown (toolbar token 'qfqAi') if the 'Translate' becomes a submenu with the offered target languages.
                    var aiSip = $(element).data('ai-sip');
                    if (!isReadOnly && aiSip && QfqNS.AiAssist) {
                        var aiUrl = $(element).data('ai-url');
                        var aiModes = String($(element).data('ai-assist') || '').split(',');
                        var aiLanguages = String($(element).data('ai-translate-languages') || '').split(',')
                            .map(function (l) { return l.trim(); })
                            .filter(function (l) { return l !== ''; });

                        var aiRun = function (aiMode, aiTargetLanguage) {
                            // If the menu was opened from the overflow toolbar drawer ('...'), close the drawer - it does not close on its own.
                            if (editor.queryCommandState('ToggleToolbarDrawer')) {
                                editor.execCommand('ToggleToolbarDrawer');
                            }
                            QfqNS.AiAssist.tinyMceAction(editor, aiUrl, aiSip, aiMode, aiTargetLanguage);
                        };

                        editor.ui.registry.addMenuButton('qfqAi', {
                            text: 'AI',
                            tooltip: 'AI assist',
                            fetch: function (callback) {
                                var items = [];
                                aiModes.forEach(function (aiMode) {
                                    aiMode = aiMode.trim();
                                    var aiConfig = QfqNS.AiAssist.modeConfig[aiMode];
                                    if (!aiConfig) {
                                        return;
                                    }
                                    if (aiMode === 'translate' && aiLanguages.length > 1) {
                                        items.push({
                                            type: 'nestedmenuitem',
                                            text: aiConfig.menuLabel,
                                            getSubmenuItems: function () {
                                                return aiLanguages.map(function (language) {
                                                    return {
                                                        type: 'menuitem',
                                                        text: '→ ' + QfqNS.AiAssist.languageLabel(language),
                                                        onAction: function () {
                                                            aiRun(aiMode, language);
                                                        }
                                                    };
                                                });
                                            }
                                        });
                                    } else {
                                        items.push({
                                            type: 'menuitem',
                                            text: aiConfig.menuLabel,
                                            onAction: function () {
                                                aiRun(aiMode, '');
                                            }
                                        });
                                    }
                                });
                                callback(items);
                            }
                        });
                    }

                    // if form is opened in readonly mode disable buttons and make element gray.
                    if (isReadOnly) {
                        editor.mode.set("readonly"); // Make TinyMCE read-only

                        // Apply styles directly
                        editor.on('init', function () {
                            // Disable and style TextArea
                            var iframe = document.querySelector("iframe#" + tinyMCEId + "_ifr");
                            if (iframe) {
                                var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                                var body = iframeDoc.body;
                                body.style.backgroundColor = "#f4f4f4";  // Light gray
                                body.style.color = "#888";              // Gray text
                                body.style.cursor = "not-allowed";      // Disable text selection
                                body.style.pointerEvents = "none";      // Disable interactions
                            }
                            // Style Toolbar
                            var toolbar = document.querySelector(".mce-toolbar-grp");
                            if (toolbar) {
                                toolbar.style.backgroundColor = "#f4f4f4";
                                toolbar.style.pointerEvents = "none";  // Prevent clicks

                            }
                            // Disable all toolbar buttons
                            var buttons = document.querySelectorAll(".mce-btn");
                            buttons.forEach(button => {
                                button.setAttribute("disabled", "true");
                                button.style.pointerEvents = "none";  // Prevents clicking
                                button.style.opacity = "0.5";  // Make them faded
                                button.style.backgroundColor = "#f4f4f4";
                            });
                            // Footer / Statusbar visual changes
                            var statusbar = document.querySelector(".mce-statusbar");
                            if (statusbar) {
                                statusbar.style.pointerEvents = "none";  // Prevent interactions
                                statusbar.style.backgroundColor = "#f4f4f4";
                            }
                        });
                    }

                    const $wrapper = $("#" + tinyMCEId + "-i").children(".extra-buttons-tinyMCE");
                    if ($wrapper.length) {

                        const $origInfoBtn = $wrapper.find(".extraButtonInfo");
                        const $origLockBtn = $wrapper.find(".extraButtonLock");

                        // Register Info Button if it exists
                        if ($origInfoBtn.length) {
                            editor.ui.registry.addButton("extraInfo", {
                                icon: "help",
                                tooltip: "Show Info",
                                onAction: function () {
                                    $("#" + tinyMCEId + "-extra-info").slideToggle("swing");
                                }
                            });

                            // Optionally remove the original button from the DOM
                            $origInfoBtn.detach();
                        }

                        // Register Lock Button if it exists
                        if ($origLockBtn.length) {
                            editor.ui.registry.addToggleButton("extraLock", {
                                icon: "lock",
                                tooltip: "Toggle Readonly",
                                onAction: function (api) {
                                    const isLocked = editor.mode.get() === "readonly";
                                    editor.mode.set(isLocked ? "design" : "readonly");

                                    // This toggles the active state
                                    api.setActive(!isLocked);
                                },
                                onSetup: function (api) {
                                    api.setActive(editor.mode.get() === "readonly");
                                }
                            });

                            $origLockBtn.detach();
                        }
                    }


                    var counterFlag = false;
                    if ($this.data('character-count-id') !== undefined) {
                        counterFlag = true;
                    }
                    var maxLength = config.maxLength; // set the maximum length here
                    var maxLengthDisplay = maxLength;
                    if (maxLength === '' || maxLength === 0) {
                        maxLength = false;
                        maxLengthDisplay = "∞";
                    }

                    var pattern = /[^\r\n\t\v\f ]/g;

                    editor.on('init', function() {
                        if (maxLength) {
                            editor.dom.setAttrib(editor.getBody(), 'maxlength', maxLength);
                        }

                        // initialize counter after first load
                        if (counterFlag) {
                            var content = editor.getContent({format: 'text'});
                            var charCount = (content.match(pattern) || []).length;
                            var characterCountTarget = "#" + $this.data('character-count-id');

                            $this.data('character-count-display', $(characterCountTarget));
                            $this.data('character-count-display').text(charCount + "/" + maxLengthDisplay);
                        }

                        editor.getBody().addEventListener('dblclick', function (e) {
                            const target = e.target;
                            if (target && target.nodeName === 'IMG') {
                                showImageModal(target.src);
                            }
                        });

                        const maxAttempts = 10;
                        const retryDelay = 300;
                        let attemptCount = 0;

                        const enableLockBtn = (lockButton) => {
                            lockButton.style.pointerEvents = "auto";
                            lockButton.removeAttribute("aria-disabled");
                            lockButton.classList.remove("tox-tbtn--disabled");
                        };

                        const observeLockBtn = (lockButton) => {
                            const observer = new MutationObserver(() => {
                                enableLockBtn(lockButton);
                            });
                            observer.observe(lockButton, {
                                attributes: true,
                                attributeFilter: ["aria-disabled"]
                            });
                            return observer;
                        };

                        const waitForLockButton = () => {
                            const lockButton = editor.getContainer().querySelector('.tox-tbtn[data-mce-name="extralock"]');
                            if (!lockButton) {
                                attemptCount++;
                                if (attemptCount < maxAttempts) {
                                    setTimeout(waitForLockButton, retryDelay);
                                }
                                return;
                            }

                            // Enable button and start observing
                            enableLockBtn(lockButton);
                            let observer = observeLockBtn(lockButton);

                            // Ensure this is kept in sync when the mode changes
                            editor.on("SetMode", function (e) {
                                if (e.mode === "readonly") {
                                    enableLockBtn(lockButton);
                                    observer.disconnect();
                                    observer = observeLockBtn(lockButton);
                                    lockButton.setAttribute("aria-pressed", "true");
                                } else {
                                    lockButton.setAttribute("aria-pressed", "false");
                                }
                            });

                            // Set readonly initially if desired
                            editor.mode.set("readonly");
                        };

                        waitForLockButton();

                    });

                    // Needed for accurate character count. Update counter after input or keyup.
                    editor.on('keyup', function() {
                        if (counterFlag) {
                            var content = editor.getContent({format: 'text'});
                            var charCount = (content.match(pattern) || []).length;
                            $this.data('character-count-display').text(charCount + "/" + maxLengthDisplay);
                        }
                    });

                    // Activate save button on every change (like regular input fields)
                    editor.on('input', function() {
                        element.value = editor.getContent();
                        $(element.closest("form")).trigger("change");
                    });

                    editor.on('Change', function() {
                        element.value = editor.getContent();
                        $(element.closest("form")).trigger("change");
                    });

                    editor.on('Dirty', function() {
                        element.value = editor.getContent();
                        $(element.closest("form")).trigger("change");
                    });

                    // Preventing input of more than maxLength
                    editor.on('keydown', function(e) {
                        if (maxLength) {
                            var content = editor.getContent({format: 'text'}); // get content without HTML tags
                            var charCount = (content.match(pattern) || []).length;
                            if (charCount >= maxLength && e.keyCode !== 8) { // prevent input when maximum length is reached
                                e.preventDefault();
                                e.stopPropagation();
                            }
                        }
                    });

                    // Apply Default Image Classes if given and none is set
                    if (defaultImageClass || defaultImageBorder) {
                        editor.on('SetContent', function () {
                            const images = editor.getBody().querySelectorAll('img');

                            images.forEach(img => {

                                if (defaultImageClass) {
                                    const classList = img.classList;

                                    const hasKnownClass =
                                        classList.contains('img-auto-tinyMCE') ||
                                        classList.contains('img-small-tinyMCE') ||
                                        classList.contains('img-medium-tinyMCE') ||
                                        classList.contains('img-large-tinyMCE') ||
                                        classList.contains('img-full-tinyMCE');

                                    if (!hasKnownClass) {
                                        img.classList.add(defaultImageClass);
                                    }
                                }

                                if (defaultImageBorder) {
                                    const hasStyle = img.hasAttribute('style');
                                    const isDefaulted = img.hasAttribute('data-border-default-applied');

                                    // Only apply default if no style and not already defaulted
                                    if (!hasStyle && !isDefaulted) {
                                        img.style = defaultImageBorder;
                                        img.setAttribute('data-border-default-applied', '1');
                                    }
                                }
                            });
                        });
                    }

                    // Remember content on focus to detect changes on blur
                    var oldContent = '';
                    editor.on('focus', function () {
                        oldContent = editor.getContent();
                    });

                    // Trigger dynamic update on focus lost, only if content changed (like regular input fields)
                    editor.on('blur', function () {
                        element.value = tinymce.get(QfqNS.escapeJqueryIdSelector(tinyMCEId)).getContent();
                        if (element.value !== oldContent) {
                            $(config.selector).trigger('change');
                        }
                        $(element).trigger('focusout');
                    });

                    /* Remove ReadOnly Again - we have to implement tinymce differently
                       to make it easier to change such  */
                    var me = editor;
                    var $parent = $(config.selector);
                    
                    $parent.on("blur", function(e, configuration) {
                        if(configuration.disabled || configuration.readonly) {
                            me.mode.set("readonly");
                            $(this).siblings(".mce-tinymce").addClass("qfq-tinymce-readonly");
                        } else {
                            me.mode.set("design");
                            $(this).siblings(".mce-tinymce").removeClass("qfq-tinymce-readonly");
                        }
                    });
                };

                var defaults = {
                    relative_urls : false,
                    remove_script_host : false,
                    license_key: 'gpl'
                };

                var tinyConfig = Object.assign(defaults, config);

                tinymce.init(tinyConfig);
                if($(this).is('[disabled]')) {
                    myEditor.mode.set("readonly");
                }
            }
        );
    };

    /**
     * Force update of the shadowed `<textarea>`. Usually called before a form submit.
     */
    tinyMce.prepareSave = function () {
        if (typeof tinymce === 'undefined') {
            return;
        }

        tinymce.triggerSave();
    };

    n.tinyMce = tinyMce;


})(QfqNS.Helper);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* @depend FormGroup.js */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';

    /**
     * Checkbox (`<checkbox>`) Form Group.
     *
     * @param $element
     * @constructor
     * @name QfqNS.Element.Checkbox
     */
    function Checkbox($element) {
        n.FormGroup.call(this, $element);

        var type = "checkbox";

        if (!this.isType(type)) {
            throw new Error("$element is not of type 'checkbox'");
        }

        // We allow one Form Group to have several checkboxes. Therefore, we have to remember which checkbox was
        // selected if possible.
        if ($element.length === 1 && $element.attr('type') === type) {
            this.$singleElement = $element;
        } else {
            this.$singleElement = null;
        }
    }

    Checkbox.prototype = Object.create(n.FormGroup.prototype);
    Checkbox.prototype.constructor = Checkbox;

    Checkbox.prototype.setValue = function (val) {
        if (this.$singleElement) {
            this.$singleElement.prop('checked', val);
        } else {
            this.$element.prop('checked', val);
        }
    };

    Checkbox.prototype.getValue = function () {
        if (this.$singleElement) {
            return this.$singleElement.prop('checked');
        } else {
            return this.$element.prop('checked');
        }
    };

    n.Checkbox = Checkbox;

})(QfqNS.Element);

/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 */

/* global $ */
/* global EventEmitter */
/* @depend ../QfqEvents.js */


/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};

(function (n) {
    'use strict';

    n.ElementBuilder = function(obj, parent) {
        this.type  = obj.type;
        this.class = obj.class || false;
        this.text  = obj.text || false;
        this.tooltip = obj.tooltip || false;
        this.label = obj.label || false;
        this.value = obj.value || false;
        this.width = obj.width || false;
        this.name = obj.name || false;
        this.onClick = obj.onClick || false;
        this.parent = parent || false;
        this.submitTo = obj.submitTo || false;
        this.checked = obj.checked || false;
        this.$element = {};
        this.children = [];
        this.eventEmitter = new EventEmitter();


        if (obj.children) {
            for (var i = 0; i < obj.children.length; i++) {
                var iparent = this;
                if (this.type !== "form") {
                    iparent = this.parent;
                }
                var element = new n.ElementBuilder(obj.children[i], iparent);

                this.children.push(element);
            }
        }

    };

    n.ElementBuilder.prototype.on = n.EventEmitter.onMixin;

    n.ElementBuilder.prototype.display = function() {
        var $element = {};

        if (this.type === "form") {
            $element = this._buildForm();
            var that = this;
            $element.submit(function(event) {
                event.preventDefault();
                that.handleSubmit();
            });
            this.$form = $element;
        }

        if (this.type === "row") $element = this._buildRow();
        if (this.type === "checkbox" ||
            this.type === "radio" ||
            this.type === "hidden") $element = this._buildInput();
        if (this.type === "label") $element = this._buildLabel();

        if (this.children) {
            for (var i = 0; i < this.children.length; i++) {
                var $child = this.children[i].display();
                $element.append($child);
            }
        }

        this.$element = $element;
        return $element;
    };

    n.ElementBuilder.prototype._buildRow = function() {
        var options = {
            class: "row" + this._getOption(this.class),
            text: this._getOption(this.text)
        };
        return $("<div />", options);
    };

    n.ElementBuilder.prototype._buildInput = function() {
        var $block = {};

        var options = {
            class: this._getOption(this.class),
            type: this._getOption(this.type),
            name: this._getOption(this.name),
            value: this._getOption(this.value)
        };

        if (this.type === "checkbox" || this.type === "radio") {
            options.checked = this.checked;
        }

        if (this.type !== "hidden") {
            $block = this._buildBlock(this.width);
        } else {
            options.required = false;
        }

        var $input = $("<input />", options);

        if (this.onClick === "submit") {
            var that = this;
            $input.on("click", function() {
                that.submit();
            });
        }

        if (this.type !== "hidden") {
            $block.append($input);
            return $block;
        } else {
            $input.removeAttr("pattern");
            return $input;
        }
    };

    n.ElementBuilder.prototype._buildLabel = function() {
        var $block = this._buildBlock(this.width, "qfq-label");
        var options = {
            class: "control-label" + this._prepareClass(this.class, true),
            text: this._getOption(this.text)
        };
        var $label = $("<span />", options);
        $block.append($label);
        return $block;
    };

    n.ElementBuilder.prototype._buildBlock = function(size, cssClass) {
        var options = {
            class: "col-md-" + size + this._prepareClass(cssClass, true)
        };
        return $("<div />", options);
    };

    n.ElementBuilder.prototype._prepareClass = function(value, isAddition) {
        if (isAddition) return " " + value;
        return "" + value;
    };

    n.ElementBuilder.prototype._buildForm = function() {
        return $("<form />");
    };

    n.ElementBuilder.prototype._getOption = function(o) {
        if (o !== undefined && o) {
            return o;
        } else {
            return "";
        }
    };

    n.ElementBuilder.prototype.submit = function() {
          if (this.type !== "form") {
              this.parent.$element.submit();
          } else {
              this.$element.submit();
          }
    };

    n.ElementBuilder.prototype.handleSubmit = function() {
        $.post(this.submitTo, this.$element.serialize())
            .done(this.submitSuccessHandler.bind(this))
            .fail(this.submitFailureHandler.bind(this));
    };

    n.ElementBuilder.prototype.submitSuccessHandler = function(data, textStatus, jqXHR) {
        var configuration = data['element-update'];
        n.ElementUpdate.updateAll(configuration);
        this.eventEmitter.emitEvent('form.submit.success',
            n.EventEmitter.makePayload(this, "submit"));
    };

    n.ElementBuilder.prototype.submitFailureHandler = function (data, textStatus, jqXHR) {
        console.error("Submit failed");
    };

})(QfqNS);

/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';

    /**
     * Factory for FormGroupS.
     *
     * @param name
     * @returns {*}
     * @function QfqNS.Element.getElement
     */
    n.getElement = function (name) {
        var elementName;
        var $element = $('[name="' + QfqNS.escapeJqueryIdSelector(name) + '"]:not([type="hidden"])');
        if ($element.length === 0) {
            // Get right element for tinymce with given name (is hidden input field)
            $element = $('[name="' + QfqNS.escapeJqueryIdSelector(name) + '"]');
            $element = getTypeAheadInput($element);
            if ($element === undefined) {
                throw Error('No element with name "' + name + '" found.');
            }
        }

        // Handle <select> and <textarea>
        elementName = $element[0].nodeName.toLowerCase();
        if (elementName === "select") {
            return new n.Select($element);
        }

        if (elementName === "textarea") {
            return new n.TextArea($element);
        }

        // Handle chat element
        if ($element.hasClass('qfq-chat')) {
            return $element[0].querySelector('.qfq-chat-window');
        }

        // Since it is neither a <select> nor a <textarea>, we assume it is an <input> element. Thus we analyze the
        // type attribute
        if (!$element[0].hasAttribute('type')) {
            return new n.Textual($element);
        }

        var type = $element[0].getAttribute('type').toLowerCase();

        if (type === 'checkbox') {
            var $formGroup = $('#' + $element.attr('id') + '-i');
            if (!$formGroup || $formGroup.length === 0) {
                type = 'nonFormGroupCheckbox';
            }
        }

        switch (type) {
            case 'checkbox':
                return new n.Checkbox($element);
            case 'nonFormGroupCheckbox':
            case "file":
                return $element;
            case 'radio':
                return new n.Radio($element);
            case 'text':
            case 'number':
            case "email":
            case "url":
            case "password":
            case "datetime":
            case "datetime-local":
            case "date":
            case "month":
            case "time":
            case "week":
            case "search":
                return new n.Textual($element);
            default:
                throw new Error("Don't know how to handle <input> of type '" + type + "'");
        }

        // Get typeahead input element
        function getTypeAheadInput (elem) {
            var selector = '.twitter-typeahead';
            var sibling = elem.prev();
            var siblingCount = elem.siblings().length;
            // If the sibling matches our selector, use it
            // If not, jump to the next sibling and continue the loop
            for (var i = 0; i < siblingCount; i++) {
                if (sibling.is(selector)) return sibling.children('.tt-input');
                sibling = sibling.prev();
            }
        }
    };
})(QfqNS.Element);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';


    /**
     * Radio (`<radio>`) Form Group.
     *
     * @param $element
     * @constructor
     * @name QfqNS.Element.Radio
     */
    function Radio($element) {
        n.FormGroup.call(this, $element);

        if (!this.isType("radio")) {
            throw new Error("$element is not of type 'radio'");
        }
    }

    Radio.prototype = Object.create(n.FormGroup.prototype);
    Radio.prototype.constructor = Radio;

    Radio.prototype.setValue = function (val) {
        this.$element.prop('checked', false);
        this.$element.filter('[value="' + val.replace(/"/g, "\\\"") + '"]').prop('checked', true);
    };

    Radio.prototype.getValue = function () {
        return this.$element.filter(':checked').val();
    };

    n.Radio = Radio;

})(QfqNS.Element);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';


    /**
     * Select (`<select>`) Form Group.
     *
     * @param $element
     * @constructor
     * @name QfqNS.Element.Select
     */
    function Select($element) {
        n.FormGroup.call(this, $element, 'select');

        if (!this.isType("select")) {
            throw new Error("$element is not of type 'select'");
        }
    }

    Select.prototype = Object.create(n.FormGroup.prototype);
    Select.prototype.constructor = Select;

    /**
     * Set the value or selection of a `<select>` tag
     *
     * @param {string|array} val when passing a string, the corresponding <option> tag will get selected. If passed
     * array of objects, `<select>` will have its `<option>` tags set correspondingly.
     */
    Select.prototype.setValue = function (val) {
        if (['string', 'number'].indexOf(typeof(val)) !== -1) {
            this.setSelection(val);
        } else if (Array.isArray(val)) {
            this.$element.empty();

            // Fill array with new <select> elements first and add it to the dom in one step, instead of appending
            // each '<select>' separately.
            var selectArray = [];
            val.forEach(function (selectObj) {
                var $option = $('<option>')
                    .attr('value', selectObj.value ? selectObj.value : selectObj.text)
                    .prop('selected', selectObj.selected ? selectObj.selected : false)
                    .append(selectObj.text);
                selectArray.push($option);
            });
            this.$element.append(selectArray);
        } else {
            throw Error('Unsupported type of argument in Select.setValue: "' + typeof(val) + '". Expected either' +
                ' "string" or "array"');
        }
    };

    /**
     *
     * @param val
     *
     * @private
     */
    Select.prototype.setSelection = function (val) {
        this.clearSelection();

        // First, see if we find an <option> tag having an attribute 'value' matching val. If that doesn't work,
        // fall back to comparing text content of <option> tags.
        var $selectionByValue = this.$element.find('option[value="' + val.replace(/"/g, "\\\"") + '"]');
        if ($selectionByValue.length > 0) {
            $selectionByValue.prop('selected', true);
        } else {
            this.$element.find('option').each(function () {
                var $element = $(this);
                if ($element.text() === val) {
                    $element.prop('selected', true);
                }

                return true;
            });
        }
    };

    /**
     * @private
     */
    Select.prototype.clearSelection = function () {
        this.$element.find(':selected').each(function () {
            $(this).prop('selected', false);
        });
    };

    Select.prototype.getValue = function () {
        var returnValue = [];
        this.$element.find(':selected').each(
            function () {
                if (this.hasAttribute('value')) {
                    returnValue.push(this.getAttribute('value'));
                } else {
                    returnValue.push($(this).text());
                }
            }
        );

        return returnValue;
    };

    n.Select = Select;

})(QfqNS.Element);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */
/* global $ */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';


    /**
     * Textarea (`<textarea>`) Form Group.
     *
     * @param $element
     * @constructor
     * @name QfqNS.Element.TextArea
     */
    function TextArea($element) {
        n.FormGroup.call(this, $element, 'textarea');

        if (!this.isType("textarea")) {
            throw new Error("$element is not of type 'textarea'");
        }
    }

    TextArea.prototype = Object.create(n.FormGroup.prototype);
    TextArea.prototype.constructor = TextArea;

    /**
     * Set the value or selection of a `<textarea>` tag
     *
     * @param {string} val content of the textarea
     */
    TextArea.prototype.setValue = function (val) {
        this.$element.val(val);
    };

    TextArea.prototype.getValue = function () {
        return this.$element.val();
    };

    n.TextArea = TextArea;

})(QfqNS.Element);

/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';


    /**
     * Textual `<input>` form groups.
     *
     * @param $element
     * @constructor
     * @name QfqNS.Element.Textual
     */
    function Textual($element) {
        n.FormGroup.call(this, $element);

        var textualTypes = [
            'text',
            'datetime',
            'datetime-local',
            'date',
            'month',
            'time',
            'week',
            'number',
            'range',
            'email',
            'url',
            'search',
            'tel',
            'password',
            'hidden'
        ];
        var textualTypesLength = textualTypes.length;
        var isTextual = false;

        for (var i = 0; i < textualTypesLength; i++) {
            if (this.isType(textualTypes[i])) {
                isTextual = true;
                break;
            }
        }

        if (!isTextual) {
            throw new Error("$element is not of type 'text'");
        }
    }

    Textual.prototype = Object.create(n.FormGroup.prototype);
    Textual.prototype.constructor = Textual;

    Textual.prototype.setValue = function (val) {
        // Typeahead delivers an array with refreshed value after save progress.
        if (Array.isArray(val)) {
            this.setTypeAheadInput(val);
        } else {
            this.$element.val(val);
        }
    };

    // Apply and see changed value after save (no page reload needed), typeahead elements needs to be handled separately.
    Textual.prototype.setTypeAheadInput = function (value) {
        this.$element.val(value[0].key);
        // Display new value and correctly set new key in hidden input element.
        this.$element.eq(1).typeahead('val', value[0].value);
        var hiddenElement =  this.$element.eq(0).closest('div').find('input[type=hidden]');
        hiddenElement.val(value[0].key);
    };

    Textual.prototype.getValue = function () {
        return this.$element.val();
    };

    n.Textual = Textual;

})(QfqNS.Element);
/**
 * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
 */

/**
 * Qfq Namespace
 *
 * @namespace QfqNS
 */
var QfqNS = QfqNS || {};
/**
 * Qfq.Element Namespace
 *
 * @namespace QfqNS.Element
 */
QfqNS.Element = QfqNS.Element || {};

(function (n) {
    'use strict';

    /**
     * Known values of `type` attribute of `<input>` elements
     *
     * @type {string[]}
     */
    n.knownElementTypes = [
        'text',
        'password',
        'checkbox',
        'radio',
        'button',
        'submit',
        'reset',
        'file',
        'hidden',
        'image',
        'datetime',
        'datetime-local',
        'date',
        'month',
        'time',
        'week',
        'number',
        'range',
        'email',
        'url',
        'search',
        'tel',
        'color'
    ];

    /*
     * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input for input types ignoring the readonly
     * attribute.
     */
    n.readOnlyIgnored = [
        'hidden',
        'range',
        'checkbox',
        'radio',
        'file',
        'select'
    ];

})(QfqNS.Element);
class ImageAdjust {

    initialize(parentElement, qfqPage, n) {
        this.qfqPage = qfqPage
        this.n = n
        const canvas = document.createElement('canvas')
        this.parentElement = parentElement
        const elementSize = parentElement.getBoundingClientRect()
        this.options = parentElement.dataset
        canvas.width = this.options.outputWidth
        canvas.height = this.options.outputHeight
        this.rotation = 0
        this.canvasElement = canvas
        this._addControls()
        this.parentElement.append(canvas)
        this.canvas = this.__canvas = new fabric.Canvas(canvas, {
            isDrawingMode: false,
            selection: false,
            isDragging: false,
            enableRetinaScaling: false,
            centeredScaling: true,
            stateful: true,
            backgroundColor: '#fff'
        })
        var that = this
        this.canvas.on('mouse:down', function(e) { that.panStart(e) })
        this.canvas.on('mouse:move', function(e) { that.panMove(e) })
        this.canvas.on('mouse:up', function(e) { that.panEnd() })
        this.canvas.on('mouse:wheel', function(opt) { that.dynamicZoom(opt) })
        this.zoom = false
        this._setImage()
        this._addMargins()
    }

    _setImage() {
        fabric.util.loadImage(this.options.image, img => {
            this.image = new fabric.Image(img)
            this._sizeImageToCanvas()
            this.image.set({
                // Some options to make the image static
                selectable: false,
                evented: false,
                lockMovementX: true,
                lockMovementY: true,
                lockRotation: true,
                lockScalingX: true,
                lockScalingY: true,
                lockUniScaling: true,
                hasControls: false,
                imageSmoothing: true,
                hasBorders: false,
            })
            this.canvas.add(this.image)
            this.canvas.centerObject(this.image)
            this.image.setCoords()
            this.canvas.renderAll()
            this.importJSON()
        },  { crossOrigin: 'anonymous' })     
    }

    _sizeImageToCanvas() {
        this.scale = Math.max(this.canvas.width / this.image.width, this.canvas.height / this.image.height)
        this.image.scale(this.scale)

    }

    _addControls() {
        const wrap = document.createElement("div")
        this.parentElement.append(wrap)
        wrap.classList.add('qfq-image-adjust-buttons', 'btn-group')
        const buttons = this._getButtons()
        for (const button of buttons) {
            const element = this._createButton(button)
            wrap.append(element)
        }
        if(this.options.imgPreviewId) {
            const element = this.createElement({
                classList: ["btn","btn-default"],
                icon: {
                    classList: ["fas", "fa-camera-retro"]
                },
                text: "",
                click: this.showImage.bind(this)
            })
            wrap.append(element)
        }
    }

    _createButton(button) {
        const element = document.createElement("button")
        element.classList.add(...button.classList)
        const icon = document.createElement("i")
        if(button.icon.classList.length > 0) icon.classList.add(...button.icon.classList)
        element.textContent = button.text
        element.setAttribute("title", button.title)
        element.append(icon)
        element.addEventListener('click', (e) => { button.click(e) })
        return element
    }

    _addMargins() {
        const container = document.querySelector(".canvas-container")
        const margins = JSON.parse(this.options.darkenMargins)
        for(const [key, value] of Object.entries(margins)) {
            console.log(key, value)
            const margin = document.createElement("div")
            margin.classList.add("qfq-ia-margin-" + key)
            if(key === 'left' || key == 'right') {
                margin.style.width = value + "px"
                margin.style.height = this.canvas.height - margins.top - margins.bottom + "px"
                margin.style.top = margins.top + "px"
            } else {
                margin.style.height = value + "px"
                margin.style.width = this.canvas.width + "px"
            }
            container.append(margin)
        }
    }

    exportJSON() {
        const settings = {
            scale: this.scale,
            //coords: this.image._getCoords(true),
            //viewport: this.canvas.viewportTransform,
            left: this.image.left,
            top: this.image.top,
            rotation: this.rotation,
            flipX: this.image.flipX,
            flipY: this.image.flipY
        }
        const output = document.getElementById(this.options.fabricJsonId)
        if(output) output.value = JSON.stringify(settings)
    }

    importJSON() {
        const input = document.getElementById(this.options.fabricJsonId)
        if(input && input.value.length > 0) {
            const unescape = decodeURIComponent(input.value)
            console.log("Trying to parse settings", unescape)
            const settings = JSON.parse(unescape)
            console.log("Imported Settings", settings)
            this._applySettings(settings)
        }
    }

    _applySettings(settings) {
        this.scale = settings.scale
        this.image.rotate(settings.rotation)
        this.rotation = settings.rotation || 0
        this.image.left = settings.left || 0
        this.image.top = settings.top || 0
        this.image.flipX = settings.flipX
        this.image.flipY = settings.flipY
        //this.canvas.setViewportTransform(this.canvas.viewportTransform)
        this.image.scale(settings.scale)
        //this.image.aCoords = settings.coords
        //this.image.setCoords()
        this.canvas.renderAll()
    }

    flipX() {
        this.image.flipX = !this.image.flipX
        this.canvas.renderAll()
        this.changeHandler()
    }

    flipY() {
        this.image.flipY = !this.image.flipY
        this.canvas.renderAll()
        this.changeHandler()
    }

    rotate() {
        this.rotation += 90
        console.log(this.rotation)
        this.image.rotate(this.rotation)
        this.canvas.renderAll()
        this.changeHandler()
    }

    zoomIn(e) {
        const scale = this.scale + 0.05;
        this._setScale(scale)
    }

    zoomOut(e) {
        const scale = this.scale - 0.05;
        this._setScale(scale)
    }
    
    toggleZoom(e) {
        this.zoom = !this.zoom
        e.target.classList.toggle("active")
    }

    dynamicZoom(opt) {
        if(!this.zoom) return
        const delta = opt.e.deltaY * -0.0001
        const scale = this.scale + delta;
        this._setScale(scale)
        opt.e.preventDefault()
        opt.e.stopPropagation()
    }

    fitImageMin() {
        const scale = Math.min(this.canvas.width / this.image.width, this.canvas.height / this.image.height)
        this._setScale(scale, true)
    }

    fitImageMax() {
        const scale = Math.max(this.canvas.width / this.image.width, this.canvas.height / this.image.height)
        this._setScale(scale, true)
    }

    panLeft(e) {
        const pan = ""
    }

    panRight(e) {
        const pan = ""
    }

    panStart(opt) {
        const e = opt.e
        this.canvas.isDragging = true
        this.canvas.lastPosX = e.clientX
        this.canvas.lastPosY = e.clientY
    }

    panMove(opt) {
        if(!this.canvas.isDragging) return
        const e = opt.e
        /*
        const vpt = this.canvas.viewportTransform
        vpt[4] += e.clientX - this.canvas.lastPosX
        vpt[5] += e.clientY - this.canvas.lastPosY
        */
        this.image.left += e.clientX - this.canvas.lastPosX
        this.image.top += e.clientY - this.canvas.lastPosY
        this.canvas.requestRenderAll()
        this.canvas.lastPosX = e.clientX
        this.canvas.lastPosY = e.clientY
    }

    panEnd(e) {
        //this.canvas.setViewportTransform(this.canvas.viewportTransform);
        this.canvas.isDragging = false;
        this.changeHandler()
    }

    changeHandler() {
        this._exportImage(false)
        this.exportJSON()
        if (this.qfqPage.qfqForm) {
            this.qfqPage.qfqForm.eventEmitter.emitEvent('form.changed', this.n.EventEmitter.makePayload(this, null))
            this.qfqPage.qfqForm.changeHandler()
            this.qfqPage.qfqForm.form.formChanged = true
        } else {
            console.log("Error: Couldn't initialize qfqForm - not possible to send form.changed event")
        }
    }

    _setScale(scale, center = false) {
        if(scale > 2.0) scale = 2.0
        if(scale < 0.001) scale = 0.001
        this.image.scale(scale)
        this.scale = scale
        if(center) this.canvas.centerObject(this.image)
        this.image.setCoords()
        this.canvas.renderAll()
        this.changeHandler()
    }

    export() {
        console.log("called export")
        this._exportImage(false)
    }

    showImage() {
        console.log("called show image")
        this._exportImage(true)
    }

    _getButtons() {
        return [{
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fa", "fa-search-plus"]
            },
            text: "",
            title: "Zoom in",
            click: this.zoomIn.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fa", "fa-search-minus"]
            },
            text: "",
            title: "Zoom out",
            click: this.zoomOut.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fa", "fa-search"]
            },
            text: "",
            title: "Toggle Zoom via Mousewheel",
            click: this.toggleZoom.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fas", "fa-arrows-alt-h"]
            },
            text: "",
            title: "Flip image horizontal",
            click: this.flipX.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fas", "fa-arrows-alt-v"]
            },
            text: "",
            title: "Flip image vertical",
            click: this.flipY.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fas", "fa-compress-alt"]
            },
            text: "",
            title: "Scale image to display whole image",
            click: this.fitImageMin.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fas", "fa-expand-alt"]
            },
            text: "",
            title: "Scale image to fill canvas",
            click: this.fitImageMax.bind(this)
        },
        {
            classList: ["btn","btn-default"],
            icon: {
                classList: ["fas", "fa-redo"]
            },
            text: "",
            title: "Rotate image clockwise",
            click: this.rotate.bind(this)
        }]
    }

    _exportImage(asImage) {
        const dataURL = this.canvas.toDataURL({
            format: this.options.outputFormat,
            quality: this.options.outputQualityJpeg
        })
        
        const selector = asImage ? this.options.imgPreviewId : this.options.base64Id
        const output = document.getElementById(selector)
        if(output) {
            if(asImage) {
                output.src = dataURL
            } else {
                output.value = dataURL
            }
            
        } else {
            console.log("Target " + (asImage ? "img tag" : "input") + " not found", this.options)
        }
    }

}

/* init without qfq 
document.addEventListener("DOMContentLoaded", function() {
    const imageEditors = document.querySelectorAll(".qfq-image-adjust")
    console.log("Image Adjust Elements", imageEditors)
    imageEditors.forEach(editor => {
        console.log("Current Editor", editor)
        const imageAdjust = new ImageAdjust()
        imageAdjust.initialize(editor, {})
    })
})
*/