Markdown en Jira con Tampermonkey

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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/[email protected]/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;
})();