Skip to content

Instantly share code, notes, and snippets.

@Fronix
Last active January 14, 2026 17:09
Show Gist options
  • Select an option

  • Save Fronix/04a96cfa77c134a6612b936304dba6f6 to your computer and use it in GitHub Desktop.

Select an option

Save Fronix/04a96cfa77c134a6612b936304dba6f6 to your computer and use it in GitHub Desktop.
numberpadToTekkenNotations
// ==UserScript==
// @name Dustloop Notation to Tekken (Preserve Colors, Toggle)
// @namespace https://dustloop.com/
// @version 1.5.0
// @description Translate Dustloop numpad notation into Tekken shorthand inside code and colorful-text spans, preserving styling, with a toggle.
// @match https://www.dustloop.com/*
// @match https://dustloop.com/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(() => {
// Cleanup if re-injected
try { window.__tekkenNotationToggleCleanup?.(); } catch (_) {}
const OPTS = {
// Motions to shorthand
// 236 => QCF, 214 => QCB, 623 => DP
motionMode: "shorthand",
// Fallback formatting
style: "slash", // "slash": d/f, d/b | "letters": df, db
uppercaseDirs: false,
motionSep: ",",
dropNeutralPlus: true,
};
const DIR_MAP_SLASH = {
"1": "d/b", "2": "d", "3": "d/f",
"4": "b", "5": "n", "6": "f",
"7": "u/b", "8": "u", "9": "u/f",
};
const DIR_MAP_LETTERS = {
"1": "db", "2": "d", "3": "df",
"4": "b", "5": "n", "6": "f",
"7": "ub", "8": "u", "9": "uf",
};
function mapDir(d) {
const m = OPTS.style === "letters" ? DIR_MAP_LETTERS : DIR_MAP_SLASH;
let out = m[d] || d;
return OPTS.uppercaseDirs ? out.toUpperCase() : out;
}
function isButtonChar(ch) {
return /[A-Za-z]/.test(ch);
}
function isRepeatOrCountContext(s, i) {
const prev = s[i - 1] || "";
if (prev === "x" || prev === "X") return true;
const nearby = s.slice(Math.max(0, i - 4), Math.min(s.length, i + 6));
if (/\(x\d/i.test(nearby) || /\(xN/i.test(nearby)) return true;
return false;
}
function motionToShorthand(seq) {
if (seq === "236") return "QCF";
if (seq === "214") return "QCB";
if (seq === "623") return "DP";
return null;
}
function normalizeNamedMotions(s) {
// Normalize glued forms like QCFH -> QCF+H
s = s.replace(/\bQCF(?=[A-Za-z])/gi, "QCF+");
s = s.replace(/\bQCB(?=[A-Za-z])/gi, "QCB+");
s = s.replace(/\bDP(?=[A-Za-z])/gi, "DP+");
return s;
}
function translateBracketedDirections(s) {
// Only direction digit holds/releases
s = s.replace(/\[([1-9])\]/g, (_, d) => `[${mapDir(d)}]`);
s = s.replace(/\]([1-9])\[/g, (_, d) => `]${mapDir(d)}[`);
return s;
}
function translateDirectionsAndMotions(s) {
let out = "";
let i = 0;
while (i < s.length) {
const ch = s[i];
if (/[1-9]/.test(ch) && !isRepeatOrCountContext(s, i)) {
let j = i;
while (j < s.length && /[1-9]/.test(s[j]) && !isRepeatOrCountContext(s, j)) j++;
const seq = s.slice(i, j);
const next = s[j] || "";
const needsPlus = isButtonChar(next);
if (seq.length > 1) {
if (OPTS.motionMode === "shorthand") {
const shorthand = motionToShorthand(seq);
if (shorthand) {
out += shorthand;
if (needsPlus) out += "+";
} else {
const mapped = seq.split("").map(mapDir);
out += mapped.join(OPTS.motionSep);
if (needsPlus) out += "+";
}
} else {
const mapped = seq.split("").map(mapDir);
out += mapped.join(OPTS.motionSep);
if (needsPlus) out += "+";
}
} else {
const single = mapDir(seq);
const isNeutral = OPTS.uppercaseDirs ? single === "N" : single === "n";
if (!(OPTS.dropNeutralPlus && isNeutral && needsPlus)) {
out += single;
if (needsPlus) out += "+";
}
}
i = j;
continue;
}
out += ch;
i++;
}
return out;
}
function translateString(text) {
let t = String(text);
t = normalizeNamedMotions(t);
t = translateBracketedDirections(t);
t = translateDirectionsAndMotions(t);
return t;
}
function containsNotation(text) {
// Conservative "should translate" test
return /([1-9]{2,}|[1-9]\s*[A-Za-z]|\[[1-9]\]|\][1-9]\[|\bQCF\b|\bQCB\b|\bDP\b|\bQCF(?=[A-Za-z])|\bQCB(?=[A-Za-z])|\bDP(?=[A-Za-z]))/i.test(text);
}
function shouldSkipElement(el) {
const tag = (el.nodeName || "").toLowerCase();
if (["script", "style", "noscript", "textarea", "input"].includes(tag)) return true;
if (el.closest && el.closest("script,style,noscript,textarea,input")) return true;
return false;
}
function getColorfulClass(el) {
if (!el || !el.classList) return null;
for (const c of el.classList) {
if (c.indexOf("colorful-text-") === 0) return c;
}
return null;
}
function mergeAdjacentColorSpans(container) {
// Merge adjacent spans with same colorful-text-* class to avoid split digits like "2""3""6H"
let node = container.firstChild;
while (node) {
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "SPAN") {
const cls = getColorfulClass(node);
if (cls) {
let next = node.nextSibling;
while (
next &&
next.nodeType === Node.ELEMENT_NODE &&
next.tagName === "SPAN" &&
getColorfulClass(next) === cls
) {
node.textContent += next.textContent;
const rm = next;
next = next.nextSibling;
rm.remove();
}
}
}
node = node.nextSibling;
}
}
// Toggle support, store original HTML for modified elements
const originalHtmlByEl = new Map();
let enabled = true;
function translateOneElement(el) {
if (shouldSkipElement(el)) return false;
// Preserve exact original markup for toggling back
if (!originalHtmlByEl.has(el)) originalHtmlByEl.set(el, el.innerHTML);
// Merge adjacent colorful spans inside code blocks so motions are contiguous
if (el.nodeName.toLowerCase() === "code") {
mergeAdjacentColorSpans(el);
}
let changed = false;
// If the element is itself a colorful span, translate its textContent directly
const elColorClass = getColorfulClass(el);
if (elColorClass) {
const before = el.textContent;
if (!containsNotation(before)) return false;
const after = translateString(before);
if (after !== before) {
el.textContent = after;
changed = true;
}
return changed;
}
// Otherwise, translate any colorful spans inside it (preserve coloring)
const spans = el.querySelectorAll?.('span[class^="colorful-text-"]') || [];
for (const sp of spans) {
const before = sp.textContent;
if (!containsNotation(before)) continue;
const after = translateString(before);
if (after !== before) {
sp.textContent = after;
changed = true;
}
}
// Translate remaining text nodes within code blocks too (for commas, arrows, spaces, or raw notation)
if (el.nodeName.toLowerCase() === "code") {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
acceptNode(n) {
const v = n.nodeValue || "";
if (!v.trim()) return NodeFilter.FILTER_REJECT;
if (!containsNotation(v)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
while (walker.nextNode()) {
const tn = walker.currentNode;
const before = tn.nodeValue;
const after = translateString(before);
if (after !== before) {
tn.nodeValue = after;
changed = true;
}
}
}
return changed;
}
function translateAllTargets() {
// 1) All code blocks (including ComboText, and any other code)
const codes = Array.from(document.querySelectorAll("code"));
// 2) All colorful spans anywhere
const colorfulSpans = Array.from(document.querySelectorAll('span[class^="colorful-text-"]'));
// Deduplicate, some spans are inside code
const targets = new Set([...codes, ...colorfulSpans]);
let changedCount = 0;
for (const el of targets) {
const beforeText = el.textContent || "";
if (!beforeText.trim()) continue;
if (!containsNotation(beforeText) && el.nodeName.toLowerCase() !== "code") continue;
if (translateOneElement(el)) changedCount++;
}
return changedCount;
}
function restoreAll() {
let restored = 0;
for (const [el, html] of originalHtmlByEl.entries()) {
if (!el || !el.isConnected) continue;
el.innerHTML = html;
restored++;
}
originalHtmlByEl.clear();
return restored;
}
const observer = new MutationObserver(() => {
if (!enabled) return;
translateAllTargets();
});
function makeButton() {
const existing = document.getElementById("__tekken_notation_toggle_btn");
if (existing) existing.remove();
const btn = document.createElement("button");
btn.id = "__tekken_notation_toggle_btn";
btn.type = "button";
btn.textContent = "Tekken Notation: ON";
btn.style.position = "fixed";
btn.style.top = "10px";
btn.style.right = "10px";
btn.style.zIndex = "2147483647";
btn.style.padding = "6px 10px";
btn.style.fontSize = "12px";
btn.style.background = "#222";
btn.style.color = "#fff";
btn.style.border = "1px solid #555";
btn.style.borderRadius = "4px";
btn.style.cursor = "pointer";
btn.style.opacity = "0.9";
btn.addEventListener("click", () => {
enabled = !enabled;
if (enabled) {
const n = translateAllTargets();
btn.textContent = "Tekken Notation: ON";
console.log(`Tekken Notation ON, updated ${n} elements.`);
} else {
const n = restoreAll();
btn.textContent = "Tekken Notation: OFF";
console.log(`Tekken Notation OFF, restored ${n} elements.`);
}
});
document.body.appendChild(btn);
return btn;
}
function start() {
if (!document.body) return;
makeButton();
translateAllTargets();
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
}
start();
window.__tekkenNotationToggleCleanup = () => {
try { observer.disconnect(); } catch (_) {}
try { document.getElementById("__tekken_notation_toggle_btn")?.remove(); } catch (_) {}
try { restoreAll(); } catch (_) {}
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment