// ==UserScript==
// @name Custom NYT Connections Puzzle
// @namespace https://github.com/amitmadnawat
// @version 1.8.2
// @description Use customized words in any NYT Connections puzzle (Daily and Archive)
// @author Amit Madnawat
// @license MIT
// @match https://www.nytimes.com/games/connections*
// @exclude https://www.nytimes.com/games-assets/*
// @icon https://www.nytimes.com/games-assets/v2/assets/icons/connections.svg
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
"use strict";
const customGroups = [
{ name: "Subway", words: ["Underground", "Metro", "Footlong", "Tube"] },
{ name: "Law", words: ["Civil", "Criminal", "Divorce", "Corporate"] },
{ name: "Bunny", words: ["Easter", "Hare", "Playboy", "Carrot"] },
{ name: "Sadhika", words: ["Will", "You", "Marry", "Me"] }
];
// Track whether we should show or hide the results screen
let showResultsScreenFlag = false;
let resultsScreenDelay = 15000;
(function injectCSS() {
const css = `
[data-testid="card-label"]{
display:flex;
justify-content:center;
align-items:center;
text-align:center;
white-space:nowrap; /* prefer single-line; script will allow wrap if needed */
overflow:visible;
padding:4px;
line-height:1;
}
@media (max-width:480px){
[data-testid="card-label"]{ padding:3px; }
}
/* remove pseudo commas for list items inside containers where we add .no-after */
.no-after li::after{ content:none !important; }
.no-after::after{ content:none !important; }
/* Dynamic results screen visibility */
.pz-moment__congrats.on-stage {
display: flex !important;
}
.pz-game-screen.on-stage {
display: flex !important;
}
.pz-game-field > article > section > a { display: none !important; }
/* Remove promo link under connections board */
[data-testid="connections-board"] { section { a { display: none !important; } } }
`;
const s = document.createElement("style");
s.id = "nyt-connections-size-only-css";
s.textContent = css;
document.head.appendChild(s);
})();
function getDateFromUrl() {
try {
const m = window.location.pathname.match(/\/games\/connections\/(\d{4}-\d{2}-\d{2})/);
return m ? m[1] : null;
} catch (e) {
return null;
}
}
function formatLocalDate(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
async function fetchJsonV2(date) {
const url = `https://www.nytimes.com/svc/connections/v2/${date}.json`;
const resp = await fetch(url, { credentials: "include" });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
function findLabelTextNode(label) {
for (const n of label.childNodes) {
if (n.nodeType === Node.TEXT_NODE && n.nodeValue && n.nodeValue.trim().length) return n;
}
return null;
}
function walkTextNodes(root, fn) {
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
let node;
while ((node = walker.nextNode())) fn(node);
}
// Build maps from API data
function buildMapsFromApi(data) {
const wordMap = {};
const titleMap = {};
data.categories.forEach((cat, catIdx) => {
const customGroup = customGroups[catIdx] || { name: `Group ${catIdx + 1}`, words: [] };
if (cat.title) titleMap[cat.title.trim().toLowerCase()] = customGroup.name;
if (Array.isArray(cat.cards)) {
cat.cards.forEach((card, cIdx) => {
const orig = String(card.content || "").trim().toLowerCase();
const custom = customGroup.words[cIdx] || customGroup.words[0] || "";
if (orig) wordMap[orig] = custom;
});
}
});
return { wordMap, titleMap };
}
// Apply mapped custom words to visible cards
function applyCardReplacements(wordMap) {
const labels = document.querySelectorAll('[data-testid="card-label"]');
labels.forEach((label) => {
const origText = label.textContent.trim();
const key = origText.toLowerCase();
if (key && key in wordMap) {
const textNode = findLabelTextNode(label);
if (textNode) textNode.nodeValue = wordMap[key];
else label.textContent = wordMap[key];
}
});
}
// Per-word shrink logic
// Per-word max-size buckets (px). Tuned small for phones; only font-size/line-height changed.
function maxSizeForLength(len) {
if (len <= 4) return 16;
if (len <= 6) return 14;
if (len <= 8) return 12;
if (len <= 12) return 11;
return 10;
}
// Shrink-to-fit for a single label: modify only font-size and line-height when needed.
function shrinkLabelToFit(label) {
const prevVis = label.style.visibility || "";
label.style.visibility = "hidden";
const textNode = findLabelTextNode(label);
if (!textNode) {
label.style.visibility = prevVis;
return;
}
const text = textNode.nodeValue.trim();
const maxPx = maxSizeForLength(text.length);
const minPx = 9;
let fitted = false;
let size = Math.min(maxPx, 20);
while (size >= minPx) {
label.style.fontSize = size + "px";
const fits = label.scrollWidth <= label.clientWidth + 1;
if (fits) {
fitted = true;
break;
}
size -= 1;
}
if (!fitted) {
label.style.whiteSpace = "normal";
const twoLineSize = Math.max(minPx, Math.min(11, maxPx));
label.style.fontSize = twoLineSize + "px";
label.style.lineHeight = "1.05";
} else {
label.style.lineHeight = "1";
}
label.style.visibility = prevVis;
}
let autosizeTimer = null;
function autosizeAllLabels() {
clearTimeout(autosizeTimer);
autosizeTimer = setTimeout(() => {
const labels = document.querySelectorAll('[data-testid="card-label"]');
labels.forEach((label) => shrinkLabelToFit(label));
}, 30);
}
function observeLabelChanges() {
const labels = document.querySelectorAll('[data-testid="card-label"]');
labels.forEach((label) => {
if (label.__autosize_obs) return;
const obs = new MutationObserver(() => autosizeAllLabels());
obs.observe(label, { childList: true, characterData: true, subtree: true });
label.__autosize_obs = obs;
});
}
function formatPurpleRow() {
setTimeout(() => {
const solvedCategoryContainer = document.querySelectorAll('[data-testid="solved-category-container"]');
if (!solvedCategoryContainer) return;
const purpleRowWordList = customGroups[3].words;
const purpleRow = Array.from(solvedCategoryContainer).find((category) =>
Array.from(category.querySelectorAll('li[data-testid="solved-category-card"]')).every((wordListItem) =>
purpleRowWordList.includes(wordListItem.textContent)
)
);
if (!purpleRow || purpleRow.length === 0) return;
const purpleRowWordListItems = purpleRow.querySelectorAll('li[data-testid="solved-category-card"]');
purpleRowWordListItems.forEach((wordListItem, index, arr) => {
const word = wordListItem.textContent;
if (purpleRowWordList.includes(word)) {
wordListItem.textContent = "";
if (index !== arr.length - 1) {
wordListItem.style.display = "none";
}
}
});
if (purpleRowWordListItems[3]) purpleRowWordListItems[3].textContent = purpleRowWordList.join(" ") + "?";
}, 50);
}
// Ensure purple formatting persists by observing the purple container
function ensurePersistentPurpleFormatting(container) {
if (!container) return;
if (container.__purple_obs_attached) return;
const obs = new MutationObserver(() => {
setTimeout(() => {
formatPurpleRow();
autosizeAllLabels();
}, 45);
});
obs.observe(container, { childList: true, subtree: true, characterData: true });
container.__purple_obs_attached = true;
}
function hideResultsScreen() {
const congratsElement = document.querySelector(".pz-moment__congrats");
if (congratsElement) congratsElement.classList.remove("on-stage");
const gameScreen = document.querySelector(".pz-game-screen");
if (gameScreen) gameScreen.classList.add("on-stage");
}
function showResultsScreen() {
const congratsElement = document.querySelector(".pz-moment__congrats");
if (congratsElement) congratsElement.classList.add("on-stage");
const gameScreen = document.querySelector(".pz-game-screen");
if (gameScreen) gameScreen.classList.remove("on-stage");
}
function observeGameCompletion() {
const completionObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (
node.classList &&
(node.classList.contains("pz-moment__congrats") ||
(node.querySelector && node.querySelector(".pz-moment__congrats")))
) {
if (!showResultsScreenFlag) {
hideResultsScreen();
} else {
showResultsScreen();
}
}
}
});
}
});
});
completionObserver.observe(document.body, { childList: true, subtree: true });
}
// New: temporary on-stage CSS injection/removal to avoid flash when on-stage classes are added
let _tempOnStageTimer = null;
function addTempOnStageCss(ms) {
try {
// If already present, refresh timer only
if (document.getElementById("nyt-temp-onstage")) {
if (_tempOnStageTimer) {
clearTimeout(_tempOnStageTimer);
}
_tempOnStageTimer = setTimeout(() => {
const el = document.getElementById("nyt-temp-onstage");
if (el) el.remove();
_tempOnStageTimer = null;
}, ms);
return;
}
const css = `
/* Temporary rules to prevent flash while on-stage classes are settling */
.pz-moment__congrats, .pz-moment__congrats.on-stage {
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
.pz-game-screen, .pz-game-screen.on-stage {
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
}
`;
const el = document.createElement("style");
el.id = "nyt-temp-onstage";
el.textContent = css;
document.head.appendChild(el);
_tempOnStageTimer = setTimeout(() => {
const rr = document.getElementById("nyt-temp-onstage");
if (rr) rr.remove();
_tempOnStageTimer = null;
}, ms);
} catch (e) {
_tempOnStageTimer = null;
}
}
// Observe attribute changes to detect when 'on-stage' class is added to relevant elements
const onStageObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== "attributes" || mutation.attributeName !== "class") continue;
const target = mutation.target;
if (!(target instanceof Element)) continue;
try {
if (target.matches && (target.matches(".pz-moment__congrats") || target.matches(".pz-game-screen"))) {
if (target.classList && target.classList.contains("on-stage")) {
addTempOnStageCss(resultsScreenDelay);
}
}
} catch (e) {
/* ignore */
}
}
});
// Start observing for class changes
onStageObserver.observe(document.body, { attributes: true, subtree: true, attributeFilter: ["class"] });
// Add button click handler to toggle results screen visibility
function setupResultsScreenToggle() {
const button = document.querySelector('div.pz-game-field section button[class^="ActionButton-module_archiveGameOverButton"]');
if (button && !button.__resultsToggleAdded) {
button.__resultsToggleAdded = true;
button.addEventListener('click', function(e) {
// Toggle the visibility state
showResultsScreenFlag = !showResultsScreenFlag;
resultsScreenDelay = 20;
// Apply the appropriate visibility
if (showResultsScreenFlag) {
showResultsScreen();
} else {
hideResultsScreen();
}
// Prevent default behavior if needed
e.stopPropagation();
return false;
});
}
}
function updateArchiveTitle() {
const h2Title = document.querySelector('h2.pz-moment__title');
const updatedTitle = 'Connections';
if (h2Title && h2Title.textContent !== updatedTitle) {
h2Title.textContent = updatedTitle;
}
}
function updateBotLinkContent() {
const botDiv = document.querySelector('[data-testid="bot-link-cta"]');
const updatedHTML = `<p>How tough was today's puzzle?<br><span class="botLinkHref">Find out with Connections Bot ›</span></p>`;
if (botDiv && botDiv.lastChild.innerHTML !== updatedHTML) {
botDiv.lastChild.innerHTML = updatedHTML;
}
}
function hideArchiveLinkButton() {
const archiveLinkButton = document.querySelector('section[class^="Board-module_archiveGameOverButtonGroup"] a[class^="ActionButton-module_archiveGameOverButton"]');
if (archiveLinkButton) {
archiveLinkButton.style.setProperty('display', 'none', 'important');
}
}
function removeArchiveGameOverLinkOnce() {
function tryRemove() {
const link = document.querySelector('div.pz-game-field a[class^="ActionButton-module_archiveGameOverButton"]');
if (link) {
if (typeof link.remove === "function") link.remove();
else if (link.parentNode) link.parentNode.removeChild(link);
return true;
}
return false;
}
if (tryRemove()) return;
const observer = new MutationObserver(function () {
if (tryRemove()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
if (document.readyState === "loading") {
window.addEventListener("DOMContentLoaded", function onLoad() {
tryRemove();
window.removeEventListener("DOMContentLoaded", onLoad);
});
} else {
tryRemove();
}
}
const mainObserver = new MutationObserver((mutations, obs) => {
const labels = document.querySelectorAll('[data-testid="card-label"]');
if (labels.length === 16) {
obs.disconnect();
(async () => {
try {
let puzzleDate = getDateFromUrl();
if (!puzzleDate) puzzleDate = formatLocalDate(new Date());
const data = await fetchJsonV2(puzzleDate);
if (!data || !Array.isArray(data.categories)) {
console.error("Unexpected API response format for", puzzleDate);
return;
}
const maps = buildMapsFromApi(data);
applyCardReplacements(maps.wordMap);
autosizeAllLabels();
observeLabelChanges();
// initial purple attempt and persistent formatting
const purpleContainer = (function tryFormat() {
formatPurpleRow();
// attempt to find the container quickly (may be set by formatPurpleRow)
return document.querySelector('[data-testid="solved-category-container"].no-after') || null;
})();
if (purpleContainer) ensurePersistentPurpleFormatting(purpleContainer);
// Start light observer for congrats additions
observeGameCompletion();
// Also call hide once (in case congrats already present)
hideResultsScreen();
// Setup results screen toggle button
setupResultsScreenToggle();
// Call so the archive link is removed as soon as the main observer detects the board ready
removeArchiveGameOverLinkOnce();
hideArchiveLinkButton();
// Update Bot link text on the results page
updateBotLinkContent();
// reveal observer: replace titles/words and react to solved container additions
const revealObserver = new MutationObserver((mutList) => {
mutList.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
// If reveal modal inserted, replace category titles
const titleElements = node.querySelectorAll(".category-title");
if (titleElements && titleElements.length) {
titleElements.forEach((el, idx) => {
if (idx < customGroups.length) el.textContent = customGroups[idx].name;
});
}
// If solved-category-container added or updated, run formatting and attach persistence
if ((node.matches && node.matches('[data-testid="solved-category-container"]')) || (node.querySelector && node.querySelector('[data-testid="solved-category-container"]'))) {
const found = (function f() {
formatPurpleRow();
return document.querySelector('[data-testid="solved-category-container"].no-after') || null;
})();
if (found) {
ensurePersistentPurpleFormatting(found);
autosizeAllLabels();
}
}
// If congrats element added anywhere, hide it and show game
if (node.classList && (node.classList.contains("pz-moment__congrats") || (node.querySelector && node.querySelector(".pz-moment__congrats")))) {
if (!showResultsScreenFlag) {
hideResultsScreen();
} else {
showResultsScreen();
}
}
// Check if the results toggle button was added
if (node.querySelector && node.querySelector('div.pz-game-field section button[class^="ActionButton-module_archiveGameOverButton"]')) {
setupResultsScreenToggle();
}
if (node.querySelector && node.querySelector('div.pz-game-field a[class^="ActionButton-module_archiveGameOverButton"]')) {
removeArchiveGameOverLinkOnce();
}
hideArchiveLinkButton();
// Update Bot link text on the results page
updateBotLinkContent();
// Walk text nodes for replacements
walkTextNodes(node, (textNode) => {
const t = String(textNode.nodeValue || "").trim();
if (!t) return;
const lower = t.toLowerCase();
if (lower in maps.titleMap) {
textNode.nodeValue = maps.titleMap[lower];
return;
}
for (const [origWord, customWord] of Object.entries(maps.wordMap)) {
if (t.toLowerCase() === origWord.toLowerCase()) {
textNode.nodeValue = customWord;
break;
}
}
});
});
});
autosizeAllLabels();
});
revealObserver.observe(document.body, { childList: true, subtree: true });
window.addEventListener("resize", autosizeAllLabels);
} catch (err) {
console.error("Failed to fetch NYT connections JSON for date:", err);
}
})();
}
});
// Update the h2 title on the first page for archive puzzles
updateArchiveTitle();
mainObserver.observe(document.body, { childList: true, subtree: true });
console.log("NYT Connections customizer loaded");
})();