// ==UserScript==
// @name Markdown en Jira con Tampermonkey
// @license MIT
// @namespace http://tampermonkey.net/
// @version 1.3.0
// @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 = false;
let mantein = false;
let descriptionBox;
let checkedDescription = 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], "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") {
const dialogCreate = document.querySelector("#create-issue-dialog")
const dialogEdit = document.querySelector("#edit-issue-dialog")
if (dialogCreate || dialogEdit) {
document.querySelector(".field.description").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(dialog) {
// 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(dialog) {
// 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 switchOnClickDescription () {
if (document.querySelector(".switch.description")) {
if(document.querySelector(".switch.description").getAttribute("checked") == "checked") {
saveMarkdown(descriptionBox)
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) {
edit.removeAttribute("disabled")
}
} else {
saveJira(descriptionBox)
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", "")
}
}
}
}
function switchOnClickComment () {
if (document.querySelector(".switch.comment")) {
if(document.querySelector(".switch.comment").getAttribute("checked") == "checked") {
saveMarkdown(commentBox)
checked = false
document.querySelector(".switch.comment").removeAttribute("checked")
const comment = document.querySelector("#issue-comment-add-submit")
comment.removeAttribute("disabled")
} else {
saveJira(commentBox)
checked = true
document.querySelector(".switch.comment").setAttribute("checked", "checked")
const comment = document.querySelector("#issue-comment-add-submit")
comment.setAttribute("disabled", "")
}
}
}
async function createMarkdownInterface() {
const {button, div, input, label} = van.tags
commentBox = await detectCommentBox();
console.log(commentBox)
descriptionBox = await detectDescriptionBox();
if (!commentBox) {
console.error("No se encontró el campo de comentarios.");
} else {
if (!document.querySelector(".switch.comment") && !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"));
commentBox.before(markdownContainer);
if (mantein) {
if (checked) {
const comment = document.querySelector("#issue-comment-add-submit")
comment.setAttribute("disabled", "")
document.querySelector(".switch.comment").setAttribute("checked", "checked")
}
mantein = false
}
}
}
if (!descriptionBox) {
console.error("No se encontró el campo de descripcion.");
} else {
if (!document.querySelector(".switch.description") && !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"));
descriptionBox.before(markdownContainer);
const create = document.querySelector("#create-issue-submit")
const edit = document.querySelector("#edit-issue-submit")
if (mantein) {
if (checkedDescription) {
const create = document.querySelector("#create-issue-submit")
const edit = document.querySelector("#edit-issue-submit")
document.querySelector(".switch.description").setAttribute("checked", "checked")
if (create) {
create.setAttribute("disabled", "")
} else if(edit) {
edit.setAttribute("disabled", "")
checkedDescription = false
}
mantein = false
}
}
if (checkedDescription && mantein) {
const create = document.querySelector("#create-issue-submit")
const edit = document.querySelector("#edit-issue-submit")
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;
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]+\|\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, '')
// 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}|`;
// });
};
(() => {
// 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;
})();