Markdown en Jira con Tampermonkey

Permite escribir en Markdown en issues y comentarios de Jira usando J2M y VanJS.

// ==UserScript==
// @name         Markdown en Jira con Tampermonkey
// @license MIT
// @namespace    http://tampermonkey.net/
// @version      1.3.2
// @description  Permite escribir en Markdown en issues y comentarios de Jira usando J2M y VanJS.
// @include      http://*
// @resource     REMOTE_CSS https://cdn.jsdelivr.net/npm/bulma-switch@2.0.4/dist/css/bulma-switch.min.css
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @grant        GM_addStyle
// ==/UserScript==
'use strict';
const myCss = GM_getResourceText("REMOTE_CSS");
GM_addStyle(myCss);
const boxes = {
    commentBox: null,
    checkedComment: false,
    dialogCommentBox: null,
    dialogEditCommentBox: null,
    dialogCheckedComment: false,
    dialogCheckedEditComment: false,
    dialogEditComment: false,
    descriptionBox: null,
    checkedDescription: false
}
let mantein = false;
var MutationObserver = window.MutationObserver;
var myObserver = new MutationObserver (mutationHandler);
var obsConfig = {
    childList: true, attributes: true,
    subtree: true, attributeFilter: ['class']
};
myObserver.observe (document, obsConfig);
function mutationHandler (mutationRecords) {

    mutationRecords.forEach ( function (mutation) {

        if (    mutation.type               == "childList"
            &&  typeof mutation.addedNodes  == "object"
            &&  mutation.addedNodes.length
           ) {
            for (var J = 0, L = mutation.addedNodes.length;  J < L;  ++J) {
                checkForCSS_Class (mutation.addedNodes[J], "textarea");
                checkForCSS_Class (mutation.addedNodes[J], "aui-nav-selected");
            }
        }
        else if (mutation.type == "attributes") {
            checkForCSS_Class (mutation.target, "textarea");
            checkForCSS_Class (mutation.target, "aui-nav-selected");
        }
    } );
}

function checkForCSS_Class (node, className) {
    //-- Only process element nodes
    if (node.nodeType === 1) {
        if (node.classList.contains (className) ) {

            console.log (
                'New node with class "' + className + '" = ', node
            );
            if (className == "aui-nav-selected" && node.getAttribute("data-mode") === "wysiwyg") {
                const dialogCreate = document.querySelector("#create-issue-dialog")
                const dialogEdit = document.querySelector("#edit-issue-dialog")
                const dialogEditComment = document.querySelector("#edit-comment")
                if (dialogCreate || dialogEdit) {
                    const commentDialog = document.querySelector(".jira-dialog #comment-wiki-edit .aui-nav-selected")
                    const description = document.querySelector(".jira-dialog #description-wiki-edit .aui-nav-selected")
                    if (commentDialog) {
                        console.log(commentDialog)
                        if (commentDialog.getAttribute("data-mode") === "wysiwyg") {
                            if (document.querySelector(".field.dialog-comment")) {
                                document.querySelector(".field.dialog-comment").remove()
                            }
                        }
                        if (description.getAttribute("data-mode") === "wysiwyg") {
                            document.querySelector(".field.description").remove()
                        }
                    } else {
                        document.querySelector(".field.description").remove()
                    }
                } else if (dialogEditComment){
                    const commentDialog = document.querySelector(".jira-dialog #comment-wiki-edit .aui-nav-selected")
                    if (commentDialog.getAttribute("data-mode") === "wysiwyg") {
                        if (document.querySelector(".dialog-edit-comment")) {
                            document.querySelector(".dialog-edit-comment").remove()
                        }
                    }
                } else {
                    document.querySelector(".field.comment").remove()
                }
                const create = document.querySelector("#create-issue-submit")
                const edit = document.querySelector("#edit-issue-submit")
                const comment = document.querySelector("#issue-comment-add-submit")
                if (create) {
                    create.removeAttribute("disabled")
                } else if (edit) {
                    edit.removeAttribute("disabled")
                } else if (comment) {
                    comment.removeAttribute("disabled")
                }
                mantein = true
            } else {
                createMarkdownInterface();
            }
        }
    }
}
// Comprobamos si J2M está disponible

// Selección del campo de comentarios de Jira
function detectCommentBox() {
    // Aquí puedes ajustar el selector para el área de texto de comentarios de Jira3
    return new Promise((res, rej) => {
        setTimeout(() => {
            const el = document.querySelector(".textarea.long-field.wiki-textfield.long-field.mentionable.wiki-editor-initialised.wiki-edit-wrapped#comment");
            res(el);
        }, 10);
    })
}
function detectDescriptionBox() {
    // Aquí puedes ajustar el selector para el área de texto de comentarios de Jira3
    return new Promise((res, rej) => {
        setTimeout(() => {
            const el = document.querySelector(".textarea.long-field.wiki-textfield.long-field.mentionable.wiki-editor-initialised.wiki-edit-wrapped#description");
            res(el);
        }, 10);
    })
}
function detectDialogCommentBox() {
    // Aquí puedes ajustar el selector para el área de texto de comentarios de Jira3
    return new Promise((res, rej) => {
        setTimeout(() => {
            const el = document.querySelector("#edit-issue-dialog .textarea.long-field.wiki-textfield.mentionable.wiki-editor-initialised.wiki-edit-wrapped#comment");
            res(el);
        }, 10);
    })
}
function detectEditCommentBox() {
    // Aquí puedes ajustar el selector para el área de texto de comentarios de Jira3
    return new Promise((res, rej) => {
        setTimeout(() => {
            const el = document.querySelector("#edit-comment .textarea.long-field.wiki-textfield.mentionable.wiki-editor-initialised.wiki-edit-wrapped#comment");
            res(el);
        }, 10);
    })
}
// Función para crear una interfaz de Markdown en Jira
function switchOnClickDescription () {
    if (document.querySelector(".switch.description")) {
        if(document.querySelector(".switch.description").getAttribute("checked") == "checked") {
            saveMarkdown(boxes.descriptionBox)
            boxes.checkedDescription = false
            document.querySelector(".switch.description").removeAttribute("checked")
            const create = document.querySelector("#create-issue-submit")
            const edit = document.querySelector("#edit-issue-submit")
            if (create) {
                create.removeAttribute("disabled")
            } else if (edit) {
                if(document.querySelector(".switch.comment-dialog")) {
                    if(!document.querySelector(".switch.comment-dialog").getAttribute("checked")) {
                        edit.removeAttribute("disabled")
                    }
                } else {
                    edit.removeAttribute("disabled")
                }
                
            }
            const options = document.querySelectorAll(".jira-dialog #description-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.removeAttribute("style")
                }
            })
        } else {
            saveJira(boxes.descriptionBox)
            boxes.checkedDescription = true
            document.querySelector(".switch.description").setAttribute("checked", "checked")
            const create = document.querySelector("#create-issue-submit")
            const edit = document.querySelector("#edit-issue-submit")
            if (create) {
                create.setAttribute("disabled", "")
            } else if (edit) {
                edit.setAttribute("disabled", "")
            }
            const options = document.querySelectorAll(".jira-dialog #description-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.setAttribute("style", "pointer-events: none;cursor: default;color: #a5adba;")
                }
            })
        }
    }
}
function switchOnClickComment () {
    if (document.querySelector(".switch.comment")) {
        if(document.querySelector(".switch.comment").getAttribute("checked") == "checked") {
            saveMarkdown(boxes.commentBox)
            boxes.checkedComment = false
            document.querySelector(".switch.comment").removeAttribute("checked")
            const comment = document.querySelector("#issue-comment-add-submit")
            comment.removeAttribute("disabled")
            const options = document.querySelectorAll("#comment-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.removeAttribute("style")
                }
            })
        } else {
            saveJira(boxes.commentBox)
            boxes.checkedComment = true
            document.querySelector(".switch.comment").setAttribute("checked", "checked")
            const comment = document.querySelector("#issue-comment-add-submit")
            comment.setAttribute("disabled", "")
            const options = document.querySelectorAll("#comment-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.setAttribute("style", "pointer-events: none;cursor: default;color: #a5adba;")
                }
            })
        }
    }
}
function switchOnClickDialogComment () {
    if (document.querySelector(".switch.comment-dialog")) {
        if(document.querySelector(".switch.comment-dialog").getAttribute("checked") == "checked") {
            saveMarkdown(boxes.dialogCommentBox)
            boxes.dialogCheckedComment = false
            document.querySelector(".switch.comment-dialog").removeAttribute("checked")
            const edit = document.querySelector("#edit-issue-submit")
            if (!document.querySelector(".switch.description").getAttribute("checked")) {
                edit.removeAttribute("disabled")
            }
            const options = document.querySelectorAll(".jira-dialog #comment-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.removeAttribute("style")
                }
            })
        } else {
            saveJira(boxes.dialogCommentBox)
            boxes.dialogCheckedComment = true
            document.querySelector(".switch.comment-dialog").setAttribute("checked", "checked")
            const comment = document.querySelector("#edit-issue-submit")
            comment.setAttribute("disabled", "")
            const options = document.querySelectorAll(".jira-dialog #comment-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.setAttribute("style", "pointer-events: none;cursor: default;color: #a5adba;")
                }
            })
        }
    }
}
function switchOnClickDialogEditComment () {
    if (document.querySelector(".switch.comment-edit-dialog ")) {
        if(document.querySelector(".switch.comment-edit-dialog ").getAttribute("checked") == "checked") {
            saveMarkdown(boxes.dialogEditCommentBox)
            boxes.dialogCheckedEditComment = false
            document.querySelector(".switch.comment-edit-dialog").removeAttribute("checked")
            const edit = document.querySelector("#edit-issue-submit")
            if (!document.querySelector(".switch.description").getAttribute("checked")) {
                edit.removeAttribute("disabled")
            }
            const options = document.querySelectorAll(".jira-dialog #comment-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.removeAttribute("style")
                }
            })
        } else {
            saveJira(boxes.dialogEditCommentBox)
            boxes.dialogCheckedEditComment = true
            document.querySelector(".switch.comment-edit-dialog ").setAttribute("checked", "checked")
            const comment = document.querySelector("#comment-edit-submit")
            comment.setAttribute("disabled", "")
            const options = document.querySelectorAll(".jira-dialog #comment-wiki-edit .aui-nav a")
            options.forEach(option => {
                if (option.innerHTML === "Visual") {
                    option.setAttribute("style", "pointer-events: none;cursor: default;color: #a5adba;")
                }
            })
        }
    }
}
async function createMarkdownInterface() {
    const {div, input, label} = van.tags
    boxes.commentBox = await detectCommentBox();
    boxes.dialogCommentBox = await detectDialogCommentBox();
    boxes.descriptionBox = await detectDescriptionBox();
    boxes.dialogEditCommentBox = await detectEditCommentBox();
    if (!boxes.commentBox) {
        console.error("No se encontró el campo de comentarios.");
    } else {
        if (!document.querySelector(".switch.comment") && !document.querySelector(".jira-dialog") && !boxes.commentBox.classList.contains("richeditor-cover")){
            const markdownContainer = div({style: "border-right: 1px solid #dfe1e5; border-left: 1px solid #dfe1e5; padding: 0.4em;", class: "field comment"},label({style: "padding: 0.4em"},"Jira"),input({type:"checkbox", class:"switch comment is-outlined is-info", id:"switchComment", name: "switchComment", onclick: () => switchOnClickComment()}),label({for:"switchComment"},"Markdown"));
            boxes.commentBox.before(markdownContainer);
            if (mantein) {
                if (boxes.checkedComment) {
                    const comment = document.querySelector("#issue-comment-add-submit")
                    comment.setAttribute("disabled", "")
                    document.querySelector(".switch.comment").setAttribute("checked", "checked")
                }
                mantein = false
            }
            if (boxes.commentBox.addEventListener) {
                boxes.commentBox.addEventListener('input', function(event) {
                    if(document.querySelector(".switch.comment").getAttribute("checked") == "checked") {
                        event.stopPropagation();
                    }
                }, false);
            }
        }
    }
    if (!boxes.dialogCommentBox) {
        console.error("No se encontró el campo de comentarios.");
    } else {
        if (!document.querySelector(".switch.comment-dialog") && !boxes.dialogCommentBox.classList.contains("richeditor-cover")){
            const markdownContainer = div({style: "border-right: 1px solid #dfe1e5; border-left: 1px solid #dfe1e5; padding: 0.4em;", class: "field dialog-comment"},label({style: "padding: 0.4em"},"Jira"),input({type:"checkbox", class:"switch comment-dialog is-outlined is-info", id:"switchDialogComment", name: "switchDialogComment", onclick: () => switchOnClickDialogComment()}),label({for:"switchDialogComment"},"Markdown"));
            boxes.dialogCommentBox.before(markdownContainer);
            if (mantein) {
                if (boxes.dialogCheckedComment) {
                    document.querySelector(".switch.comment-dialog").setAttribute("checked", "checked")
                }
                mantein = false
            }
        }
    }
    if (!boxes.dialogEditCommentBox) {
        console.error("No se encontró el campo de comentarios.");
    } else {
        if (!document.querySelector(".switch.comment-edit-dialog") && !boxes.dialogEditCommentBox.classList.contains("richeditor-cover")){
            const markdownContainer = div({style: "border-right: 1px solid #dfe1e5; border-left: 1px solid #dfe1e5; padding: 0.4em;", class: "field dialog-edit-comment"},label({style: "padding: 0.4em"},"Jira"),input({type:"checkbox", class:"switch comment-edit-dialog is-outlined is-info", id:"switchDialogEditComment", name: "switchDialogEditComment", onclick: () => switchOnClickDialogEditComment()}),label({for:"switchDialogEditComment"},"Markdown"));
            boxes.dialogEditCommentBox.before(markdownContainer);
            if (mantein) {
                if (boxes.dialogCheckedEditComment) {
                    document.querySelector(".switch.comment-dialog").setAttribute("checked", "checked")
                }
                mantein = false
            }
        }
    }
    if (!boxes.descriptionBox) {
        console.error("No se encontró el campo de descripcion.");
    } else {
        if (!document.querySelector(".switch.description") && !boxes.descriptionBox.classList.contains("richeditor-cover")){
            const markdownContainer = div({style: "border-right: 1px solid #dfe1e5; border-left: 1px solid #dfe1e5; padding: 0.4em;", class: "field description"},label({style: "padding: 0.4em"},"Jira"),input({type:"checkbox", class:"switch description is-outlined is-info", id:"switchDescription", name: "switchDescription", onclick: () => switchOnClickDescription()}),label({for:"switchDescription"},"Markdown"));
            boxes.descriptionBox.before(markdownContainer);
            const create = document.querySelector("#create-issue-submit")
            const edit = document.querySelector("#edit-issue-submit")
            if (mantein) {
                if (boxes.checkedDescription) {
                    document.querySelector(".switch.description").setAttribute("checked", "checked")
                    if (create) {
                        create.setAttribute("disabled", "")
                    } else if(edit) {
                        edit.setAttribute("disabled", "")
                        boxes.checkedDescription = false
                    }
                    mantein = false
                }
            }
            if (boxes.checkedDescription && mantein) {
                if (create) {
                    create.setAttribute("disabled", "")
                    document.querySelector(".switch.description").setAttribute("checked", "checked")
                }
            }
        }
    }
}

// Función para guardar el contenido del editor Markdown al campo de Jira
function saveMarkdown(commentBox) {
    const markdownText = commentBox.value;
    const jiraFormattedText = markdownToJira(markdownText);

    // Insertamos el texto convertido en el campo de comentario de Jira
    commentBox.value = jiraFormattedText;

    // Opcional: hacemos visible el campo de Jira para mostrar el texto convertido
}
function saveJira(commentBox) {
    const markdownText = commentBox.value;
    const markdownFormattedText = jiraToMarkdown(markdownText);

    // Insertamos el texto convertido en el campo de comentario de Jira
    commentBox.value = markdownFormattedText;
}

// Inicia la interfaz cuando la página esté lista
// Función para convertir Markdown a formato Jira
function markdownToJira(str) {
    const map = {
        // cite: '??',
        del: '-',
        ins: '+',
        sup: '^',
        sub: '~',
    };

    return (
        str
        // Tables
        .replace(
            /^(\|[^\n]+\|\r?\n)((?:\|\s*:?[-]+:?\s*)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$/gm,
            (match, headerLine, separatorLine, rowstr) => {
                const headers = headerLine.match(/[^|]+(?=\|)/g);
                const separators = separatorLine.match(/[^|]+(?=\|)/g);
                if (headers.length !== separators.length) return match;

                const rows = rowstr.split('\n');
                if (rows.length === 2 && headers.length === 1)
                    // Panel
                    return `{panel:title=${headers[0].trim()}}\n${rowstr
                        .replace(/^\|(.*)[ \t]*\|/, '$1')
                        .trim()}\n{panel}\n`;

                return `||${headers.join('||')}||${rowstr}`;
            }
        )
        // Bold, Italic, and Combined (bold+italic)
        .replace(/([*_]+)(\S.*?)\1/g, (match, wrapper, content) => {
            switch (wrapper.length) {
                case 1:
                    return `_${content}_`;
                case 2:
                    return `*${content}*`;
                case 3:
                    return `_*${content}*_`;
                default:
                    return wrapper + content + wrapper;
            }
        })
        // All Headers (# format)
        .replace(/^([#]+)(.*?)$/gm, (match, level, content) => {
            return `h${level.length}.${content}`;
        })
        // Headers (H1 and H2 underlines)
        .replace(/^(.*?)\n([=-]+)$/gm, (match, content, level) => {
            return `h${level[0] === '=' ? 1 : 2}. ${content}`;
        })
        // Ordered lists
        .replace(/^([ \t]*)\d+\.\s+/gm, (match, spaces) => {
            return `${Array(Math.floor(spaces.length / 3) + 1)
                .fill('#')
                .join('')} `;
        })
        // Un-Ordered Lists
        .replace(/^([ \t]*)\*\s+/gm, (match, spaces) => {
            return `${Array(Math.floor(spaces.length / 2 + 1))
                .fill('*')
                .join('')} `;
        })
        // Headers (h1 or h2) (lines "underlined" by ---- or =====)
        // Citations, Inserts, Subscripts, Superscripts, and Strikethroughs
        .replace(new RegExp(`<(${Object.keys(map).join('|')})>(.*?)</\\1>`, 'g'), (match, from, content) => {
            const to = map[from];
            return to + content + to;
        })
        // Other kind of strikethrough
        .replace(/(\s+)~~(.*?)~~(\s+)/g, '$1-$2-$3')
        // Named/Un-Named Code Block
        .replace(/```(.+\n)?((?:.|\n)*?)```/g, (match, synt, content) => {
            let code = '{code}';
            if (synt) {
                code = `{code:${synt.replace(/\n/g, '')}}\n`;
            }
            return `${code}${content}{code}`;
        })
        // Inline-Preformatted Text
        .replace(/`([^`]+)`/g, '{{$1}}')
        // Images
        .replace(/!\[[^\]]*\]\(([^)]+)\)/g, '!$1!')
        // Named Link
        .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]')
        // Un-Named Link
        .replace(/<([^>]+)>/g, '[$1]')
        // Single Paragraph Blockquote
        .replace(/^>/gm, 'bq.')
    );
};

// Función para convertir de Jira a Markdown (opcional, si necesitas edición reversible)
function jiraToMarkdown(str){
    return (
        str
        // Un-Ordered Lists
        .replace(/^[ \t]*(\*+)\s+/gm, (match, stars) => {
            return `${Array(stars.length).join('  ')}* `;
        })
        // Ordered lists
        .replace(/^[ \t]*(#+)\s+/gm, (match, nums) => {
            return `${Array(nums.length).join('   ')}1. `;
        })
        // Headers 1-6
        .replace(/^h([0-6])\.(.*)$/gm, (match, level, content) => {
            return Array(parseInt(level, 10) + 1).join('#') + content;
        })
        // Bold
        .replace(/\*(\S.*)\*/g, '**$1**')
        // Italic
        .replace(/_(\S.*)_/g, '*$1*')
        // Monospaced text
        .replace(/\{\{([^}]+)\}\}/g, '`$1`')
        // Citations (buggy)
        // .replace(/\?\?((?:.[^?]|[^?].)+)\?\?/g, '<cite>$1</cite>')
        // Inserts
        .replace(/\+([^+]*)\+/g, '<ins>$1</ins>')
        // Superscript
        .replace(/\^([^^]*)\^/g, '<sup>$1</sup>')
        // Subscript
        .replace(/~([^~]*)~/g, '<sub>$1</sub>')
        // Strikethrough
        .replace(/(\s+)-(\S+.*?\S)-(\s+)/g, '$1~~$2~~$3')
        // Code Block
        .replace(
            /\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}([^]*?)\n?\{code\}/gm,
            '```$2$5\n```'
        )
        // Pre-formatted text
        .replace(/{noformat}/g, '```')
        // Un-named Links
        .replace(/\[([^|]+?)\]/g, '<$1>')
        // Images
        .replace(/!(.+)!/g, '![]($1)')
        // Named Links
        .replace(/\[(.+?)\|(.+?)\]/g, '[$1]($2)')
        // Single Paragraph Blockquote
        .replace(/^bq\.\s+/gm, '> ')
        // Remove color: unsupported in md
        .replace(/\{color:[^}]+\}([^]*?)\{color\}/gm, '$1')
        // panel into table
        .replace(/\{panel:title=([^}]*)\}\n?([^]*?)\n?\{panel\}/gm, '\n| $1 |\n| --- |\n| $2 |')
        // table header
        .replace(/^[ \t]*((?:\|\|.*?)+\|\|)[ \t]*$/gm, (match, headers) => {
            const singleBarred = headers.replace(/\|\|/g, '|');
            return `${singleBarred}\n${singleBarred.replace(/\|[^|]+/g, '| --- ')}`;
        })
        // remove leading-space of table headers and rows
        .replace(/^[ \t]*\|/gm, '|')
    );
    // // remove unterminated inserts across table cells
    // .replace(/\|([^<]*)<ins>(?![^|]*<\/ins>)([^|]*)\|/g, (_, preceding, following) => {
    //     return `|${preceding}+${following}|`;
    // })
    // // remove unopened inserts across table cells
    // .replace(/\|(?<![^|]*<ins>)([^<]*)<\/ins>([^|]*)\|/g, (_, preceding, following) => {
    //     return `|${preceding}+${following}|`;
    // });
};
//libreria de vanilla js
(() => {
    // van.js
    var protoOf = Object.getPrototypeOf;
    var changedStates;
    var derivedStates;
    var curDeps;
    var curNewDerives;
    var alwaysConnectedDom = { isConnected: 1 };
    var gcCycleInMs = 1e3;
    var statesToGc;
    var propSetterCache = {};
    var objProto = protoOf(alwaysConnectedDom);
    var funcProto = protoOf(protoOf);
    var _undefined;
    var addAndScheduleOnFirst = (set, s, f, waitMs) => (set ?? (setTimeout(f, waitMs), /* @__PURE__ */ new Set())).add(s);
    var runAndCaptureDeps = (f, deps, arg) => {
        let prevDeps = curDeps;
        curDeps = deps;
        try {
            return f(arg);
        } catch (e) {
            console.error(e);
            return arg;
        } finally {
            curDeps = prevDeps;
        }
    };
    var keepConnected = (l) => l.filter((b) => b._dom?.isConnected);
    var addStatesToGc = (d) => statesToGc = addAndScheduleOnFirst(statesToGc, d, () => {
        for (let s of statesToGc)
            s._bindings = keepConnected(s._bindings), s._listeners = keepConnected(s._listeners);
        statesToGc = _undefined;
    }, gcCycleInMs);
    var stateProto = {
        get val() {
            curDeps?._getters?.add(this);
            return this.rawVal;
        },
        get oldVal() {
            curDeps?._getters?.add(this);
            return this._oldVal;
        },
        set val(v) {
            curDeps?._setters?.add(this);
            if (v !== this.rawVal) {
                this.rawVal = v;
                this._bindings.length + this._listeners.length ? (derivedStates?.add(this), changedStates = addAndScheduleOnFirst(changedStates, this, updateDoms)) : this._oldVal = v;
            }
        }
    };
    var state = (initVal) => ({
        __proto__: stateProto,
        rawVal: initVal,
        _oldVal: initVal,
        _bindings: [],
        _listeners: []
    });
    var bind = (f, dom) => {
        let deps = { _getters: /* @__PURE__ */ new Set(), _setters: /* @__PURE__ */ new Set() }, binding = { f }, prevNewDerives = curNewDerives;
        curNewDerives = [];
        let newDom = runAndCaptureDeps(f, deps, dom);
        newDom = (newDom ?? document).nodeType ? newDom : new Text(newDom);
        for (let d of deps._getters)
            deps._setters.has(d) || (addStatesToGc(d), d._bindings.push(binding));
        for (let l of curNewDerives)
            l._dom = newDom;
        curNewDerives = prevNewDerives;
        return binding._dom = newDom;
    };
    var derive = (f, s = state(), dom) => {
        let deps = { _getters: /* @__PURE__ */ new Set(), _setters: /* @__PURE__ */ new Set() }, listener = { f, s };
        listener._dom = dom ?? curNewDerives?.push(listener) ?? alwaysConnectedDom;
        s.val = runAndCaptureDeps(f, deps, s.rawVal);
        for (let d of deps._getters)
            deps._setters.has(d) || (addStatesToGc(d), d._listeners.push(listener));
        return s;
    };
    var add = (dom, ...children) => {
        for (let c of children.flat(Infinity)) {
            let protoOfC = protoOf(c ?? 0);
            let child = protoOfC === stateProto ? bind(() => c.val) : protoOfC === funcProto ? bind(c) : c;
            child != _undefined && dom.append(child);
        }
        return dom;
    };
    var tag = (ns, name, ...args) => {
        let [props, ...children] = protoOf(args[0] ?? 0) === objProto ? args : [{}, ...args];
        let dom = ns ? document.createElementNS(ns, name) : document.createElement(name);
        for (let [k, v] of Object.entries(props)) {
            let getPropDescriptor = (proto) => proto ? Object.getOwnPropertyDescriptor(proto, k) ?? getPropDescriptor(protoOf(proto)) : _undefined;
            let cacheKey = name + "," + k;
            let propSetter = propSetterCache[cacheKey] ??= getPropDescriptor(protoOf(dom))?.set ?? 0;
            let setter = k.startsWith("on") ? (v2, oldV) => {
                let event = k.slice(2);
                dom.removeEventListener(event, oldV);
                dom.addEventListener(event, v2);
            } : propSetter ? propSetter.bind(dom) : dom.setAttribute.bind(dom, k);
            let protoOfV = protoOf(v ?? 0);
            k.startsWith("on") || protoOfV === funcProto && (v = derive(v), protoOfV = stateProto);
            protoOfV === stateProto ? bind(() => (setter(v.val, v._oldVal), dom)) : setter(v);
        }
        return add(dom, children);
    };
    var handler = (ns) => ({ get: (_, name) => tag.bind(_undefined, ns, name) });
    var update = (dom, newDom) => newDom ? newDom !== dom && dom.replaceWith(newDom) : dom.remove();
    var updateDoms = () => {
        let iter = 0, derivedStatesArray = [...changedStates].filter((s) => s.rawVal !== s._oldVal);
        do {
            derivedStates = /* @__PURE__ */ new Set();
            for (let l of new Set(derivedStatesArray.flatMap((s) => s._listeners = keepConnected(s._listeners))))
                derive(l.f, l.s, l._dom), l._dom = _undefined;
        } while (++iter < 100 && (derivedStatesArray = [...derivedStates]).length);
        let changedStatesArray = [...changedStates].filter((s) => s.rawVal !== s._oldVal);
        changedStates = _undefined;
        for (let b of new Set(changedStatesArray.flatMap((s) => s._bindings = keepConnected(s._bindings))))
            update(b._dom, bind(b.f, b._dom)), b._dom = _undefined;
        for (let s of changedStatesArray)
            s._oldVal = s.rawVal;
    };
    var van_default = {
        tags: new Proxy((ns) => new Proxy(tag, handler(ns)), handler()),
        hydrate: (dom, f) => update(dom, bind(f, dom)),
        add,
        state,
        derive
    };

    // van.forbundle.js
    window.van = van_default;
})();