Renders exported Speedrun.com JSON files as a readable UI with flags
// ==UserScript==
// @name Speedrun.com JSON UI Viewer
// @namespace local
// @version 1.1
// @description Renders exported Speedrun.com JSON files as a readable UI with flags
// @author local
// @license MIT
// @match file:///*speedrun*.json*
// @match *://*/*speedrun*.json*
// @run-at document-end
// @grant none
// ==/UserScript==
/*
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files, to deal in the Software
without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
*/
(function () {
'use strict';
function readJsonFromPage() {
const text =
document.body?.innerText ||
document.querySelector('pre')?.innerText ||
'';
try {
const json = JSON.parse(text);
return Array.isArray(json) ? json : null;
} catch (e) {
return null;
}
}
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function link(url, text, className) {
if (!url) return el('span', className, text || '');
const a = document.createElement('a');
a.href = url;
a.textContent = text || url;
a.className = className || '';
a.target = '_blank';
a.rel = 'noopener noreferrer';
return a;
}
function flag(url, name) {
if (!url) return null;
const img = document.createElement('img');
img.src = url;
img.alt = name || '';
img.title = name || '';
img.className = 'src-flag';
return img;
}
function addStyles() {
const style = document.createElement('style');
style.textContent = `
:root {
color-scheme: dark;
font-family: Inter, Arial, sans-serif;
background: #111318;
color: #e8e8e8;
}
body {
margin: 0;
background: #111318;
color: #e8e8e8;
}
.src-viewer {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.src-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.src-title h1 {
margin: 0;
font-size: 24px;
}
.src-count {
color: #aaa;
font-size: 14px;
}
.src-table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border-radius: 10px;
background: #191c22;
box-shadow: 0 0 0 1px #2a2e38;
}
.src-table th,
.src-table td {
padding: 10px 12px;
border-bottom: 1px solid #2a2e38;
text-align: left;
vertical-align: top;
font-size: 14px;
}
.src-table th {
background: #222631;
color: #f1f1f1;
font-weight: 700;
position: sticky;
top: 0;
z-index: 2;
}
.src-table tr:hover td {
background: #20242c;
}
.src-rank {
width: 56px;
text-align: center;
color: #ffd36a;
font-weight: 700;
}
.src-time {
color: #7dd3fc;
font-weight: 700;
white-space: nowrap;
}
.src-muted {
color: #aaa;
}
.src-link {
color: #8ab4ff;
text-decoration: none;
}
.src-link:hover {
text-decoration: underline;
}
.src-inline {
display: inline-flex;
align-items: center;
gap: 6px;
}
.src-flag {
height: 12px;
width: auto;
border-radius: 2px;
flex: none;
}
.src-level-card {
margin-bottom: 14px;
padding: 16px;
border-radius: 10px;
background: #191c22;
box-shadow: 0 0 0 1px #2a2e38;
}
.src-level-header {
margin-bottom: 12px;
font-size: 18px;
font-weight: 700;
}
.src-places {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.src-place {
padding: 12px;
border-radius: 8px;
background: #222631;
}
.src-place-title {
margin-bottom: 6px;
color: #ffd36a;
font-weight: 700;
}
.src-vars {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.src-var {
padding: 3px 7px;
border-radius: 999px;
background: #2a2e38;
color: #ddd;
font-size: 12px;
white-space: nowrap;
}
@media (max-width: 800px) {
.src-viewer {
padding: 12px;
}
.src-table {
display: block;
overflow-x: auto;
}
.src-places {
grid-template-columns: 1fr;
}
}
`;
document.head.appendChild(style);
}
function formatVariableName(key) {
return key
.replace(/_/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase());
}
function renderVariables(item) {
const box = el('div', 'src-vars');
const variables = item.variables || {};
Object.keys(variables).forEach(key => {
box.appendChild(
el('span', 'src-var', `${formatVariableName(key)}: ${variables[key]}`)
);
});
return box;
}
function inlineWithFlag(flagUrl, flagName, child) {
const box = el('span', 'src-inline');
const img = flag(flagUrl, flagName);
if (img) box.appendChild(img);
box.appendChild(child);
return box;
}
function makePlayerCell(item) {
const td = el('td');
const player = link(item.user_link, item.player || '', 'src-link');
td.appendChild(
inlineWithFlag(item.user_flag_url, item.user_flag_name, player)
);
return td;
}
function makePlatformCell(item) {
const td = el('td');
const text = el('span', '', item.platform || '');
td.appendChild(
inlineWithFlag(item.platform_flag_url, item.platform_flag_name, text)
);
return td;
}
function makeLinkedCell(url, text, className) {
const td = el('td');
if (url) {
td.appendChild(link(url, text, className || 'src-link'));
} else {
td.appendChild(el('span', className || '', text));
}
return td;
}
function renderNormalLeaderboard(data) {
const table = el('table', 'src-table');
const thead = el('thead');
const headerRow = el('tr');
['#', 'Player', 'Time', 'Date', 'Platform', 'Variables', 'Run'].forEach(name => {
headerRow.appendChild(el('th', '', name));
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = el('tbody');
data.forEach(item => {
const tr = el('tr');
tr.appendChild(el('td', 'src-rank', item.rank || ''));
tr.appendChild(makePlayerCell(item));
tr.appendChild(makeLinkedCell(item.run_link, item.time || '', 'src-time'));
tr.appendChild(el('td', 'src-muted', item.date || ''));
tr.appendChild(makePlatformCell(item));
const varsCell = el('td');
varsCell.appendChild(renderVariables(item));
tr.appendChild(varsCell);
const runCell = el('td');
runCell.appendChild(link(item.run_link, item.run_link ? 'Open run' : '', 'src-link'));
tr.appendChild(runCell);
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
}
function renderPlace(placeData, label) {
const box = el('div', 'src-place');
box.appendChild(el('div', 'src-place-title', label));
const player = el('div');
player.appendChild(
inlineWithFlag(
placeData.user_flag_url,
placeData.user_flag_name,
link(placeData.user_link, placeData.player || 'Unknown', 'src-link')
)
);
box.appendChild(player);
box.appendChild(el('div', 'src-time', placeData.time || ''));
if (placeData.run_link) {
const run = el('div');
run.appendChild(link(placeData.run_link, 'Open run', 'src-link'));
box.appendChild(run);
}
return box;
}
function renderLevelTop(data) {
const wrapper = el('div');
data.forEach(item => {
const card = el('div', 'src-level-card');
const header = el('div', 'src-level-header');
header.appendChild(link(item.level_link, item.level || 'Unknown level', 'src-link'));
card.appendChild(header);
const places = el('div', 'src-places');
places.appendChild(renderPlace(item.first || {}, '1st place'));
places.appendChild(renderPlace(item.second || {}, '2nd place'));
places.appendChild(renderPlace(item.third || {}, '3rd place'));
card.appendChild(places);
wrapper.appendChild(card);
});
return wrapper;
}
function render(data) {
document.body.innerHTML = '';
document.title = 'Speedrun.com JSON Viewer';
addStyles();
const app = el('div', 'src-viewer');
const title = el('div', 'src-title');
title.appendChild(el('h1', '', 'Speedrun.com JSON Viewer'));
title.appendChild(el('div', 'src-count', `${data.length} rows`));
app.appendChild(title);
if (data[0]?.type === 'level_top') {
app.appendChild(renderLevelTop(data));
} else {
app.appendChild(renderNormalLeaderboard(data));
}
document.body.appendChild(app);
}
const data = readJsonFromPage();
if (!data) return;
render(data);
})();