Greasy Fork is available in English.
AtCoderの順位表の「所属」列をスリムにするUserScript
// ==UserScript==
// @name Atcoder Shorter Affiliation
// @namespace https://github.com/e6nlaq/atcoder-shorter-affiliation
// @version 1.0.0
// @author e6nlaq
// @description AtCoderの順位表の「所属」列をスリムにするUserScript
// @license MIT
// @copyright (C) 2026 e6nlaq
// @match https://atcoder.jp/contests/*/standings*
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setValue
// ==/UserScript==
(function () {
'use strict';
function ansiRegex({ onlyFirst = false } = {}) {
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`;
const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
const pattern = `${osc}|${csi}`;
return new RegExp(pattern, onlyFirst ? void 0 : "g");
}
const regex = ansiRegex();
function stripAnsi(string) {
if (typeof string !== "string") {
throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
}
if (!string.includes("\x1B") && !string.includes("")) {
return string;
}
return string.replace(regex, "");
}
const ambiguousRanges = [161, 161, 164, 164, 167, 168, 170, 170, 173, 174, 176, 180, 182, 186, 188, 191, 198, 198, 208, 208, 215, 216, 222, 225, 230, 230, 232, 234, 236, 237, 240, 240, 242, 243, 247, 250, 252, 252, 254, 254, 257, 257, 273, 273, 275, 275, 283, 283, 294, 295, 299, 299, 305, 307, 312, 312, 319, 322, 324, 324, 328, 331, 333, 333, 338, 339, 358, 359, 363, 363, 462, 462, 464, 464, 466, 466, 468, 468, 470, 470, 472, 472, 474, 474, 476, 476, 593, 593, 609, 609, 708, 708, 711, 711, 713, 715, 717, 717, 720, 720, 728, 731, 733, 733, 735, 735, 768, 879, 913, 929, 931, 937, 945, 961, 963, 969, 1025, 1025, 1040, 1103, 1105, 1105, 8208, 8208, 8211, 8214, 8216, 8217, 8220, 8221, 8224, 8226, 8228, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8254, 8254, 8308, 8308, 8319, 8319, 8321, 8324, 8364, 8364, 8451, 8451, 8453, 8453, 8457, 8457, 8467, 8467, 8470, 8470, 8481, 8482, 8486, 8486, 8491, 8491, 8531, 8532, 8539, 8542, 8544, 8555, 8560, 8569, 8585, 8585, 8592, 8601, 8632, 8633, 8658, 8658, 8660, 8660, 8679, 8679, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719, 8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741, 8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780, 8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835, 8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978, 9312, 9449, 9451, 9547, 9552, 9587, 9600, 9615, 9618, 9621, 9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665, 9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734, 9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794, 9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 9886, 9887, 9919, 9919, 9926, 9933, 9935, 9939, 9941, 9953, 9955, 9955, 9960, 9961, 9963, 9969, 9972, 9972, 9974, 9977, 9979, 9980, 9982, 9983, 10045, 10045, 10102, 10111, 11094, 11097, 12872, 12879, 57344, 63743, 65024, 65039, 65533, 65533, 127232, 127242, 127248, 127277, 127280, 127337, 127344, 127373, 127375, 127376, 127387, 127404, 917760, 917999, 983040, 1048573, 1048576, 1114109];
const fullwidthRanges = [12288, 12288, 65281, 65376, 65504, 65510];
const wideRanges = [4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783, 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934, 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024, 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175, 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289, 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871, 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108, 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101589, 101631, 101662, 101760, 101874, 110576, 110579, 110581, 110587, 110589, 110590, 110592, 110882, 110898, 110898, 110928, 110930, 110933, 110933, 110948, 110951, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374, 127374, 127377, 127386, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589, 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968, 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334, 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716, 128716, 128720, 128722, 128725, 128728, 128732, 128735, 128747, 128748, 128756, 128764, 128992, 129003, 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129674, 129678, 129734, 129736, 129736, 129741, 129756, 129759, 129770, 129775, 129784, 131072, 196605, 196608, 262141];
const isInRange = (ranges, codePoint) => {
let low = 0;
let high = Math.floor(ranges.length / 2) - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const i = mid * 2;
if (codePoint < ranges[i]) {
high = mid - 1;
} else if (codePoint > ranges[i + 1]) {
low = mid + 1;
} else {
return true;
}
}
return false;
};
const minimumAmbiguousCodePoint = ambiguousRanges[0];
const maximumAmbiguousCodePoint = ambiguousRanges.at(-1);
const minimumFullWidthCodePoint = fullwidthRanges[0];
const maximumFullWidthCodePoint = fullwidthRanges.at(-1);
const minimumWideCodePoint = wideRanges[0];
const maximumWideCodePoint = wideRanges.at(-1);
const commonCjkCodePoint = 19968;
const [wideFastPathStart, wideFastPathEnd] = findWideFastPathRange(wideRanges);
function findWideFastPathRange(ranges) {
let fastPathStart = ranges[0];
let fastPathEnd = ranges[1];
for (let index = 0; index < ranges.length; index += 2) {
const start = ranges[index];
const end = ranges[index + 1];
if (commonCjkCodePoint >= start && commonCjkCodePoint <= end) {
return [start, end];
}
if (end - start > fastPathEnd - fastPathStart) {
fastPathStart = start;
fastPathEnd = end;
}
}
return [fastPathStart, fastPathEnd];
}
const isAmbiguous = (codePoint) => {
if (codePoint < minimumAmbiguousCodePoint || codePoint > maximumAmbiguousCodePoint) {
return false;
}
return isInRange(ambiguousRanges, codePoint);
};
const isFullWidth = (codePoint) => {
if (codePoint < minimumFullWidthCodePoint || codePoint > maximumFullWidthCodePoint) {
return false;
}
return isInRange(fullwidthRanges, codePoint);
};
const isWide = (codePoint) => {
if (codePoint >= wideFastPathStart && codePoint <= wideFastPathEnd) {
return true;
}
if (codePoint < minimumWideCodePoint || codePoint > maximumWideCodePoint) {
return false;
}
return isInRange(wideRanges, codePoint);
};
function validate(codePoint) {
if (!Number.isSafeInteger(codePoint)) {
throw new TypeError(`Expected a code point, got \`${typeof codePoint}\`.`);
}
}
function eastAsianWidth(codePoint, { ambiguousAsWide = false } = {}) {
validate(codePoint);
if (isFullWidth(codePoint) || isWide(codePoint) || ambiguousAsWide && isAmbiguous(codePoint)) {
return 2;
}
return 1;
}
const segmenter = new Intl.Segmenter();
const zeroWidthClusterRegex = new RegExp("^(?:\\p{Default_Ignorable_Code_Point}|\\p{Control}|\\p{Format}|\\p{Mark}|\\p{Surrogate})+$", "v");
const leadingNonPrintingRegex = new RegExp("^[\\p{Default_Ignorable_Code_Point}\\p{Control}\\p{Format}\\p{Mark}\\p{Surrogate}]+", "v");
const rgiEmojiRegex = new RegExp("^\\p{RGI_Emoji}$", "v");
const unqualifiedKeycapRegex = /^[\d#*]\u20E3$/;
const extendedPictographicRegex = new RegExp("\\p{Extended_Pictographic}", "gu");
function isDoubleWidthNonRgiEmojiSequence(segment) {
if (segment.length > 50) {
return false;
}
if (unqualifiedKeycapRegex.test(segment)) {
return true;
}
if (segment.includes("")) {
const pictographics = segment.match(extendedPictographicRegex);
return pictographics !== null && pictographics.length >= 2;
}
return false;
}
function baseVisible(segment) {
return segment.replace(leadingNonPrintingRegex, "");
}
function isZeroWidthCluster(segment) {
return zeroWidthClusterRegex.test(segment);
}
function trailingHalfwidthWidth(segment, eastAsianWidthOptions) {
let extra = 0;
if (segment.length > 1) {
for (const char of segment.slice(1)) {
if (char >= "" && char <= "") {
extra += eastAsianWidth(char.codePointAt(0), eastAsianWidthOptions);
}
}
}
return extra;
}
function stringWidth(input, options = {}) {
if (typeof input !== "string" || input.length === 0) {
return 0;
}
const {
ambiguousIsNarrow = true,
countAnsiEscapeCodes = false
} = options;
let string = input;
if (!countAnsiEscapeCodes && (string.includes("\x1B") || string.includes(""))) {
string = stripAnsi(string);
}
if (string.length === 0) {
return 0;
}
if (/^[\u0020-\u007E]*$/.test(string)) {
return string.length;
}
let width = 0;
const eastAsianWidthOptions = { ambiguousAsWide: !ambiguousIsNarrow };
for (const { segment } of segmenter.segment(string)) {
if (isZeroWidthCluster(segment)) {
continue;
}
if (rgiEmojiRegex.test(segment) || isDoubleWidthNonRgiEmojiSequence(segment)) {
width += 2;
continue;
}
const codePoint = baseVisible(segment).codePointAt(0);
width += eastAsianWidth(codePoint, eastAsianWidthOptions);
width += trailingHalfwidthWidth(segment, eastAsianWidthOptions);
}
return width;
}
var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
function applyTruncation() {
const maxLength = _GM_getValue("maxLength", 32);
const spans = document.querySelectorAll(
".standings-affiliation"
);
for (const span of Array.from(spans)) {
if (!span.dataset.originalText) {
span.dataset.originalText = span.textContent || "";
}
const text = span.dataset.originalText;
const currentWidth = stringWidth(text);
if (currentWidth > maxLength) {
let truncatedText = "";
let runningWidth = 0;
const ellipsis = "…";
const ellipsisWidth = stringWidth(ellipsis);
const targetWidth = maxLength - ellipsisWidth;
for (const char of text) {
const charWidth = stringWidth(char);
if (runningWidth + charWidth > targetWidth) break;
runningWidth += charWidth;
truncatedText += char;
}
const newText = truncatedText + ellipsis;
if (span.textContent !== newText) {
span.textContent = newText;
span.title = text;
}
} else {
if (span.textContent !== text) {
span.textContent = text;
span.removeAttribute("title");
}
}
}
}
function init() {
_GM_registerMenuCommand("表示幅の設定 (AtCoder Shorter Affiliation)", () => {
const currentN = _GM_getValue("maxLength", 32);
const input = prompt(
"所属の表示を省略する表示幅(半角基準)を入力してください:",
String(currentN)
);
if (input !== null) {
const newN = parseInt(input, 10);
if (!Number.isNaN(newN) && newN > 0) {
_GM_setValue("maxLength", newN);
applyTruncation();
} else {
alert("有効な正整数を入力してください。");
}
}
});
const observer = new MutationObserver(() => {
applyTruncation();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
applyTruncation();
}
init();
})();