您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 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.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, '') // 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; })();