Markdown en Jira con Tampermonkey

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

目前為 2024-11-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Markdown en Jira con Tampermonkey
// @license MIT
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @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);
let commentBox;
let checked = true;
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], "switch");
            }
        }
        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") {
                document.querySelector(".field").remove()
                document.querySelector("#create-issue-submit").removeAttribute("disabled")
            } 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#description")
            res(el);
        }, 10);
    })
}
// Función para crear una interfaz de Markdown en Jira
function switchOnClick () {
    console.log(document.querySelector(".switch").getAttribute("checked"))
    if(document.querySelector(".switch").getAttribute("checked") == "checked") {
        saveJira(commentBox)
        checked = false
        document.querySelector(".switch").removeAttribute("checked")
        document.querySelector("#create-issue-submit").removeAttribute("disabled")
    } else {
        saveMarkdown(commentBox)
        checked = true
        document.querySelector(".switch").setAttribute("checked", "checked")
        document.querySelector("#create-issue-submit").setAttribute("disabled", "")

    }
}
async function createMarkdownInterface() {
    commentBox = await detectCommentBox();
    const {button, div, input, label} = van.tags
    console.log(commentBox)
    if (!commentBox) {
        console.error("No se encontró el campo de comentarios.");
    } else {
        if (!document.querySelector(".switch") && !commentBox.classList.contains("richeditor-cover")){
            const markdownContainer = div({style: "border-right: 1px solid #dfe1e5; border-left: 1px solid #dfe1e5; padding: 0.4em;", class: "field"},label({style: "padding: 0.4em"},"Jira"),input({type:"checkbox", class:"switch is-outlined is-info", id:"switchExample", name: "switchExample", onclick: () => switchOnClick()}),label({for:"switchExample"},"Markdown"));
            commentBox.before(markdownContainer);
            if (checked) {
                document.querySelector("#create-issue-submit").setAttribute("disabled", "")
                document.querySelector(".switch").setAttribute("checked", "checked")
            }
        }
    }

    // Creamos el contenedor usando VanJS


    // Insertamos el contenedor antes del campo de comentario


    // Ocultamos el campo de comentarios original (opcional)
}

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

    // 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;
    console.log(markdownText)
    const markdownFormattedText = jiraToMarkdown(markdownText);
    console.log(markdownFormattedText)

    // 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((?:\|.*?)+\|)[ \t]*\n((?:\|\s*?-{3,}\s*?)+\|)[ \t]*\n((?:(?:\|.*?)+\|[ \t]*\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('||')}||\n${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 `\n${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}|`;
        // });
    };
(() => {
  // 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;
})();