// ==UserScript==
// @name Greasy Fork Enhance
// @name:zh-CN Greasy Fork 增强
// @namespace http://tampermonkey.net/
// @version 0.8.8
// @description Enhance your experience at Greasyfork.
// @description:zh-CN 增进 Greasyfork 浏览体验。
// @match https://greasyfork.org/*
// @author PRO
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addValueChangeListener
// @require https://update.greasyfork.org/scripts/470224/1459364/Tampermonkey%20Config.js
// @icon https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo16.png
// @icon64 https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo96.png
// @license gpl-3.0
// ==/UserScript==
(function () {
'use strict';
// Judge if the script should run
const no_run = [".json", ".js"];
let is_run = true;
const idPrefix = "greasyfork-enhance-";
const name = GM_info.script.name;
no_run.forEach((suffix) => {
if (window.location.pathname.endsWith(suffix)) {
is_run = false;
}
});
if (!is_run) return;
// Config
const configDesc = {
$default: {
autoClose: false,
},
filterAndSearch: {
name: "🔎 Filter and Search",
type: "folder",
items: {
shortcut: {
name: "Shortcut",
title: "Enable keyboard shortcuts",
type: "bool",
value: true,
},
regexFilter: {
name: "Regex filter",
title: "Use regex to filter out matching scripts",
value: "",
},
searchSyntax: {
name: "*Search syntax",
title: "Enable partial search syntax for Greasy Fork search bar",
type: "bool",
value: true,
},
},
},
codeblocks: {
name: "📝 Code blocks",
type: "folder",
items: {
autoHideCode: {
name: "Auto hide code",
title: "Hide long code blocks by default",
type: "bool",
value: true,
},
autoHideRows: {
name: "Min rows to hide",
title: "Minimum number of rows to hide",
type: "int",
value: 10,
processor: "int_range-1-",
},
tabSize: {
name: "Tab size",
title: "Set Tab indentation size",
type: "int",
value: 4,
processor: "int_range-0-",
},
animation: {
name: "Animation",
title: "Enable animation for toggling code blocks",
type: "bool",
value: true,
},
metadata: {
name: "Metadata",
title: "Parses certain script metadata and displays it on the script code page",
type: "bool",
value: false,
},
}
},
display: {
name: "🎨 Display",
type: "folder",
items: {
hideButtons: {
name: "Hide buttons",
title: "Hide floating buttons added by this script",
type: "bool",
value: false,
},
stickyPagination: {
name: "Sticky pagination",
title: "Make pagination bar sticky",
type: "bool",
value: true,
},
flatLayout: {
name: "Flat layout",
title: "Use flat layout for script list and descriptions",
type: "bool",
value: false,
},
showVersion: {
name: "Show version",
title: "Show version number in script list",
type: "bool",
value: false,
},
alwaysShowNotification: {
name: "Always show notification",
title: "Always show the notification widget",
type: "bool",
value: false,
}
},
},
other: {
name: "🔧 Other",
type: "folder",
items: {
shortLink: {
name: "Short link",
title: "Display a shortened link to current script",
type: "bool",
value: true,
},
libAlternativeUrl: {
name: "Alternative URLs for library",
title: "Show a list of alternative URLs for a given library",
type: "bool",
value: false,
},
imageProxy: {
name: "*Image proxy",
title: "Use `wsrv.nl` as proxy for user-uploaded images",
type: "bool",
value: false,
},
lazyImage: {
name: "*Lazy image",
title: "Load user images lazily",
type: "bool",
value: false,
},
debug: {
name: "Debug",
title: "Enable debug mode",
type: "bool",
value: false,
},
}
}
};
const config = new GM_config(configDesc);
const configProxy = config.proxy;
// CSS
const dynamicStyle = {
"codeblocks.animation": `
/* Toggle code animation */
pre > code { transition: height 0.5s ease-in-out 0s; }
/* Adapted from animate.css - https://animate.style/ */
:root { --animate-duration: 1s; --animate-delay: 1s; --animate-repeat: 1; }
.animate__animated { animation-duration: var(--animate-duration); animation-fill-mode: both; }
.animate__animated.animate__fastest { animation-duration: calc(var(--animate-duration) / 3); }
@keyframes tada {
from { transform: scale3d(1, 1, 1); }
10%, 20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
30%, 50%, 70%, 90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
40%, 60%, 80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
to { transform: scale3d(1, 1, 1); }
}
.animate__tada { animation-name: tada; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.animate__fadeIn { animation-name: fadeIn; }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
.animate__fadeOut { -webkit-animation-name: fadeOut; animation-name: fadeOut; }
`,
"display.hideButtons": `div#float-buttons { display: none; }`,
"display.stickyPagination": `.sidebarred-main-content > .pagination { position: sticky; bottom: 0; backdrop-filter: blur(5px); padding: 0.5em; }`,
"display.flatLayout": `
.script-list > li {
&:not(.ad-entry) { padding-right: 0; }
article {
display: flex; flex-direction: row; justify-content: space-between; align-items: center;
> .script-meta-block {
width: 40%; column-gap: 0;
> .inline-script-stats {
margin: 0;
> dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
}
> h2 {
width: 60%; overflow: hidden; text-overflow: ellipsis; margin-right: 0.5em; padding-right: 0.5em; border-right: 1px solid #88888888;
> .script-link { white-space: nowrap; }
> .script-description { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
}
&[data-script-type="library"] > article {
> h2 { width: 80%; }
> .script-meta-block { width: 20%; column-count: 1; }
}
}
@media (max-width: 600px) {
.script-list > li {
&[data-script-type="library"] > article > div.script-meta-block { width: 40%; }
&:not([data-script-type="library"]) > article {
display: block;
> h2 { width: unset; border-right: none; }
> .script-meta-block { column-count: 2; }
}
> article > div.script-meta-block { width: unset; column-gap: 0; }
}
}
.showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type { display: none; }
#script-info .script-meta-block { float: right; column-count: 1; max-width: 300px; border-left: 1px solid #DDDDDD; margin-left: 1em; padding-left: 1em; }
#additional-info { width: calc(100% - 2em - 2px); }
`,
"display.showVersion": `.script-list > li[data-script-version]::before { content: "@" attr(data-script-version); position: absolute; translate: 0 -1em; color: grey; font-size: smaller; }`,
};
// Functions
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
const body = $("body");
function log(...args) {
if (configProxy["other.debug"]) {
console.log(`[${name}]`, ...args);
}
}
function sanitify(s) {
// Remove emojis (such a headache)
s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
// Trim spaces and newlines
s = s.trim();
// Replace spaces
s = s.replaceAll(" ", "-");
s = s.replaceAll("%20", "-");
// No more multiple "-"
s = s.replaceAll(/-+/g, "-");
return s;
}
function process(outline, node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
const text = node.textContent;
if (!node.id) { // If the node has no id
node.id = sanitify(text); // Then assign id
}
// Add anchors
const anchor = node.appendChild(document.createElement('a'));
anchor.className = 'anchor';
anchor.href = '#' + node.id;
const link = outline.appendChild(document.createElement("li"))
.appendChild(document.createElement("a"));
link.href = "#" + node.id;
link.text = text;
return true;
}
async function animate(node, animation) {
return new Promise((resolve, reject) => {
node.classList.add("animate__animated", "animate__" + animation);
if (node.getAnimations().length == 0) {
node.classList.remove("animate__animated", "animate__" + animation);
reject("No animation available");
}
node.addEventListener('animationend', e => {
e.stopPropagation();
node.classList.remove("animate__animated", "animate__" + animation);
resolve("Animation ended");
}, { once: true });
});
}
async function transition(node, height) {
return new Promise((resolve, reject) => {
node.style.height = height;
if (node.getAnimations().length == 0) {
resolve("No transition available");
}
node.addEventListener('transitionend', e => {
e.stopPropagation();
resolve("Transition ended");
}, { once: true });
});
}
function copyCode() {
const code = this.parentNode.nextElementSibling;
const text = code.textContent;
navigator.clipboard.writeText(text).then(() => {
this.textContent = "Copied!";
animate(this, "tada").then(() => {
this.textContent = "Copy code";
}, () => {
window.setTimeout(() => {
this.textContent = "Copy code";
}, 1000);
});
});
}
function toggleCode() {
const code = this.parentNode.nextElementSibling;
if (code.style.height == "0px") {
code.style.willChange = "height";
transition(code, code.getAttribute("data-height")).then(() => {
code.style.willChange = "";
});
animate(this, "fadeOut").then(() => {
this.textContent = "Hide code";
animate(this, "fadeIn");
}, () => {
this.textContent = "Hide code";
});
} else {
code.style.willChange = "height";
transition(code, "0px").then(() => {
code.style.willChange = "";
});
animate(this, "fadeOut").then(() => {
this.textContent = "Show code";
animate(this, "fadeIn");
}, () => {
this.textContent = "Show code";
});
}
}
function createToolbar() {
const toolbar = document.createElement("div");
const copy = toolbar.appendChild(document.createElement("a"));
const toggle = toolbar.appendChild(document.createElement("a"));
copy.textContent = "Copy code";
copy.className = "code-operation";
copy.title = "Copy code to clipboard";
copy.addEventListener("click", copyCode);
toggle.textContent = "Hide code";
toggle.classList.add("code-operation", "animate__fastest");
toggle.title = "Toggle code display";
toggle.addEventListener("click", toggleCode);
// Css
toolbar.className = "code-toolbar";
return toolbar;
}
function injectCSS(id, css) {
const style = document.head.appendChild(document.createElement("style"));
style.id = idPrefix + id;
style.textContent = css;
}
function cssHelper(id, enable) {
const current = document.getElementById(idPrefix + id);
if (current) {
current.disabled = !enable;
} else if (enable) {
injectCSS(id, dynamicStyle[id]);
}
}
// Basic css
injectCSS("basic", `
html { scroll-behavior: smooth; }
a.anchor::before { content: "#"; }
a.anchor { opacity: 0; text-decoration: none; padding: 0px 0.5em; transition: all 0.25s ease-in-out; }
h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor { opacity: 1; transition: all 0.25s ease-in-out; }
a.button { margin: 0.5em 0 0 0; display: flex; align-items: center; justify-content: center; text-decoration: none; color: black; background-color: #a42121ab; border-radius: 50%; width: 2em; height: 2em; font-size: 1.8em; font-weight: bold; }
div.code-toolbar { display: flex; gap: 1em; }
a.code-operation { cursor: pointer; font-style: italic; }
div.lum-lightbox { z-index: 2; }
#float-buttons { position: fixed; bottom: 1em; right: 1em; display: flex; flex-direction: column; user-select: none; z-index: 1; }
aside.panel { display: none; }
.dynamic-opacity { transition: opacity 0.2s ease-in-out; opacity: 0.2; }
.dynamic-opacity:hover { opacity: 0.8; }
input[type=file] { border-style: dashed; border-radius: 0.5em; border-color: gray; padding: 0.5em; background: rgba(169, 169, 169, 0.4); transition-property: border-color, background; transition-duration: 0.25s; transition-timing-function: ease-in-out; }
input[type=file]:hover { border-color: black; background: rgba(169, 169, 169, 0.6); }
input[type=file]::file-selector-button { border: 1px solid; border-radius: 0.3em; transition: background 0.25s ease-in-out; background: rgba(169, 169, 169, 0.7); }
input[type=file]::file-selector-button:hover { background: rgba(169, 169, 169, 1); }
table { border: 1px solid #8d8d8d; border-collapse: collapse; width: auto; }
table td, table th { padding: 0.5em 0.75em; vertical-align: middle; border: 1px solid #8d8d8d; }
@media (any-hover: none) { .dynamic-opacity { opacity: 0.8; } .dynamic-opacity:hover { opacity: 0.8; } }
@media screen and (min-width: 767px) {
aside.panel { display: contents; line-height: 1.5; }
ul.outline { position: sticky; float: right; padding: 0 0 0 0.5em; margin: 0 0.5em -99vh; max-height: 80vh; border: 1px solid #BBBBBB; border-left: 2px solid #F2E5E5; box-shadow: 0 0 5px #ddd; background: linear-gradient(to right, #fcf1f1, #FFF 1em); list-style: none; width: 10.5%; color: gray; border-radius: 5px; overflow-y: scroll; z-index: 1; }
ul.outline > li { overflow: hidden; text-overflow: ellipsis; }
ul.outline > li > a { color: gray; white-space: nowrap; text-decoration: none; }
}
pre > code { overflow: hidden; display: block; }
ul { padding-left: 1.5em; }
.script-list > .regex-filtered { display: none; }
#greasyfork-enhance-regex-filter-tip { float: right; color: grey; }
@media screen and (max-width: 800px) { #greasyfork-enhance-regex-filter-tip { display: none; } }`);
// Aside panel & Anchors
const isScript = /^\/[^\/]+\/scripts/;
const isSpecificScript = /^\/[^\/]+\/scripts\/\d+/;
const isDisccussion = /^\/[^\/]+\/discussions/;
const path = window.location.pathname;
if ((!isScript.test(path) && !isDisccussion.test(path)) || isSpecificScript.test(path)) {
const panel = body.insertBefore(document.createElement("aside"), $("body > div.width-constraint"));
panel.className = "panel";
const reference_node = $("body > div.width-constraint > section");
const outline = panel.appendChild(document.createElement("ul"));
outline.classList.add("outline");
outline.classList.add("dynamic-opacity");
outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
outline.style.marginTop = outline.style.top;
let flag = false;
$$("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
flag = process(outline, node) || flag; // Not `flag || process(node)`!
});
if (!flag) {
panel.remove();
}
}
// Navigate to hash
const hash = window.location.hash.slice(1);
if (hash) {
const ele = document.getElementById(decodeURIComponent(hash));
if (ele) {
ele.scrollIntoView();
}
}
// Buttons
const buttons = body.appendChild(document.createElement("div"));
buttons.id = "float-buttons";
const goToTop = buttons.appendChild(document.createElement("a"));
goToTop.classList.add("button");
goToTop.classList.add("dynamic-opacity");
goToTop.href = "#top";
goToTop.text = "↑";
// Double click to get to top
body.addEventListener("dblclick", (e) => {
if (e.target === body) {
goToTop.click();
}
});
// Fix current tab link
const tab = $("ul#script-links > li.current");
if (tab) {
const link = tab.appendChild(document.createElement("a"));
link.href = window.location.pathname;
link.appendChild(tab.firstChild);
}
const parts = window.location.pathname.split("/");
if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
const banner = $("header#main-header div#site-name");
const img = banner.querySelector("img");
const text = banner.querySelector("#site-name-text > h1");
const link1 = document.createElement("a");
link1.href = window.location.pathname;
img.parentNode.replaceChild(link1, img);
link1.appendChild(img);
const link2 = document.createElement("a");
link2.href = window.location.pathname;
link2.textContent = text.textContent;
text.textContent = "";
text.appendChild(link2);
}
// Toolbar for code blocks
const codeBlocks = document.getElementsByTagName("pre");
for (const codeBlock of codeBlocks) {
if (codeBlock.firstChild.tagName === "CODE") {
const height = getComputedStyle(codeBlock.firstChild).getPropertyValue("height");
codeBlock.firstChild.style.height = height;
codeBlock.firstChild.setAttribute("data-height", height);
codeBlock.insertAdjacentElement("afterbegin", createToolbar());
}
}
// Filter and Search
// Shortcut
function submitOnCtrlEnter(e) {
const form = this.form;
if (!form) return;
// Ctrl + Enter to submit
if (e.ctrlKey && e.key === "Enter") {
form.submit();
}
}
function handleShortcut(e) {
const ele = document.activeElement;
// Ignore key combinations
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
return;
}
// Do not interfere with input elements
if (ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.getAttribute("contenteditable") === "true") {
if (e.key === "Escape") {
e.preventDefault();
ele.blur(); // Escape to blur
}
return;
}
// Do not interfere with input methods
if (e.isComposing || e.keyCode === 229) {
return;
}
// Focus on search bar
switch (e.key) {
case "Enter": {
const input = $("input[type=search]") || $("input[type=text]") || $("textarea");
if (input) {
e.preventDefault();
input.focus();
}
break;
}
case "ArrowLeft":
$("a.previous_page")?.click();
break;
case "ArrowRight":
$("a.next_page")?.click();
break;
}
}
let shortcutEnabled = false;
function shortcut(enable) {
const textAreas = $$("textarea");
if (!shortcutEnabled && enable) {
for (const textarea of textAreas) {
textarea.addEventListener("keyup", submitOnCtrlEnter);
}
document.addEventListener("keydown", handleShortcut);
shortcutEnabled = true;
} else if (shortcutEnabled && !enable) {
for (const textarea of textAreas) {
textarea.removeEventListener("keyup", submitOnCtrlEnter);
}
document.removeEventListener("keydown", handleShortcut);
shortcutEnabled = false;
}
}
shortcut(configProxy["filterAndSearch.shortcut"]);
// Regex filter
const regexFilterTip = $(".sidebarred > .sidebarred-main-content > .script-list#browse-script-list")
?.previousElementSibling?.appendChild?.(document.createElement("span"));
if (regexFilterTip) {
regexFilterTip.id = idPrefix + "regex-filter-tip";
regexFilterTip.title = `[${name}] Number of scripts filtered by regex`;
}
function setRegexFilterTip(content) {
if (regexFilterTip) {
regexFilterTip.textContent = content;
}
}
function regexFilterOne(regex, script) {
const info = script.querySelector("article > h2");
if (!info) return;
const name = info.querySelector(".script-link").textContent;
const result = regex.test(name);
script.classList.toggle("regex-filtered", result);
if (result) {
log("Filtered:", name);
}
return result;
}
function regexFilter(regexStr) {
const debug = configProxy["other.debug"];
const scripts = $$(".script-list > li");
if (regexStr === "" || scripts.length === 0) {
scripts.forEach(script => script.classList.remove("regex-filtered"));
setRegexFilterTip("");
return;
}
const regex = new RegExp(regexStr, "i");
let count = 0;
debug && console.groupCollapsed(`[${name}] Regex filtered scripts`);
scripts.forEach(script => {
if (regexFilterOne(regex, script)) {
count++;
}
});
setRegexFilterTip(`Filtered: ${count}/${scripts.length}`);
debug && console.groupEnd();
}
regexFilter(configProxy["filterAndSearch.regexFilter"]);
// Search syntax
const types = {
"script": "scripts",
"lib": "scripts/libraries",
"library": "scripts/libraries",
// "code": "scripts/code-search", // It uses a different search parameter `c` instead of `q`
"user": "users"
};
const langs = {
"js": "",
"javascript": "",
"css": "css",
"any": "all",
"all": "all"
};
const sorts = {
"rel": "",
"relevant": "",
"relevance": "",
"day": "daily_installs",
"daily": "daily_installs",
"daily_install": "daily_installs",
"daily_installs": "daily_installs",
"total": "total_installs",
"total_install": "total_installs",
"total_installs": "total_installs",
"score": "ratings",
"rate": "ratings",
"rating": "ratings",
"ratings": "ratings",
"created": "created",
"created_at": "created",
"updated": "updated",
"updated_at": "updated",
"name": "name",
"title": "name",
};
if (configProxy["filterAndSearch.searchSyntax"]) {
function parseString(input) {
// Regular expression to match key:value pairs, allowing for non-word characters in values
const regex = /\b(\w+:[^\s]+)\b/g;
// Extract all key:value pairs
const pairs = input.match(regex) || [];
// Remove the pairs from the input string
const cleanedString = input.replace(regex, '').replace(/\s{2,}/g, ' ').trim();
// Convert pairs to an object
const parsedPairs = pairs.reduce((acc, pair) => {
const [key, value] = pair.split(':');
acc[key.toLowerCase()] = value.toLowerCase(); // Case-insensitive
return acc;
}, {});
return { cleanedString, parsedPairs };
}
function processSearch(search) {
const form = search.form;
if (form.method !== "get") {
return;
}
form.addEventListener("submit", (e) => {
const { cleanedString, parsedPairs } = parseString(search.value);
if (cleanedString === search.value) return;
search.value = cleanedString;
if (!parsedPairs) return;
e.preventDefault();
const url = new URL(form.action, window.location.href);
url.searchParams.set("q", cleanedString);
if (parsedPairs["site"]) { // site:site-name
url.pathname = `/scripts/by-site/${parsedPairs["site"]}`;
} else if (parsedPairs["type"]) { // type:type, including "script", "lib"/"library", "code", "user"
const typeUrl = types[parsedPairs["type"]];
if (typeUrl) {
url.pathname = `/${typeUrl}`;
}
}
if (parsedPairs["lang"]) { // lang:language
const lang = langs[parsedPairs["lang"]];
if (lang === "") {
url.searchParams.delete("language");
} else if (lang) {
url.searchParams.set("language", lang);
}
}
if (parsedPairs["sort"]) { // sort:sort-by
const sort = sorts[parsedPairs["sort"]];
if (sort === "" || sort === "daily_installs" && cleanedString === "") {
url.searchParams.delete("sort");
} else if (sort) {
url.searchParams.set("sort", sort);
}
}
window.location.href = url.href;
});
}
const searches = $$("input[type=search][name=q]");
for (const search of searches) {
processSearch(search);
}
}
// Code blocks
// Auto hide code blocks
function autoHide() {
if (!configProxy["codeblocks.autoHideCode"]) {
for (const code_block of codeBlocks) {
const toggle = code_block.firstChild.lastChild;
if (!toggle) continue;
if (toggle.textContent === "Show code") {
toggle.click(); // Click the toggle button
}
}
} else {
for (const codeBlock of codeBlocks) {
const m = codeBlock.lastChild.textContent.match(/\n/g);
const rows = m ? m.length : 0;
const toggle = codeBlock.firstChild.lastChild;
if (!toggle) continue;
const hidden = toggle.textContent === "Show code";
if (rows >= configProxy["codeblocks.autoHideRows"] && !hidden || rows < configProxy["codeblocks.autoHideRows"] && hidden) {
codeBlock.firstChild.lastChild.click(); // Click the toggle button
}
}
}
}
document.addEventListener("readystatechange", (e) => {
if (e.target.readyState === "complete") {
autoHide();
}
}, { once: true });
// Tab size
function tabSize(value) {
const style = $("style#" + idPrefix + "tab-size") ?? document.head.appendChild(document.createElement("style"));
style.id = idPrefix + "tab-size";
style.textContent = `pre { tab-size: ${value}; }`;
}
tabSize(configProxy["codeblocks.tabSize"]);
// Metadata
function extractUserScriptMetadata(code) {
const result = {};
const userScriptRegex = /\/\/\s*=+\s*UserScript\s*=+\s*([\s\S]*?)\s*=+\s*\/UserScript\s*=+\s*/;
const match = code.match(userScriptRegex);
if (match) {// If the UserScript block is found
const content = match[1];// Extract the content within the UserScript block
const lines = content.split('\n'); // Split the content by newline
lines.forEach(line => {
// Regular expression to match "// @name value" pattern
const matchLine = line.trim().match(/^\/\/\s*@(\S+)\s+(.+)$/);
if (matchLine) {
const name = matchLine[1]; // Extract the name
const value = matchLine[2]; // Extract the value
switch (typeof result[name]) {
case "undefined": // First occurrence
result[name] = value;
break;
case "string": // Second occurrence
result[name] = [result[name], value];
break;
case "object": // Third or more occurrence
result[name].push(value);
break;
}
}
});
}
return result;
}
function metadata(enable) {
const id = idPrefix + "metadata";
const current = document.getElementById(id);
if (current && !enable) {
current.remove();
} else if (!current && enable) {
const scriptCodeBlock = document.querySelector(".code-container > pre.prettyprint.lang-js");
const description = $("div#script-content");
if (!window.location.pathname.endsWith("/code") || !scriptCodeBlock || !description) return;
const metaBlock = document.createElement("ul");
description.prepend(metaBlock);
metaBlock.id = id;
const script = scriptCodeBlock.querySelector("ol") ? Array.from(scriptCodeBlock.querySelectorAll("ol > li")).map(li => li.textContent).join("\n") : scriptCodeBlock.textContent;
const metadata = extractUserScriptMetadata(script);
const commonHosts = {
GreasyFork: /^https?:\/\/update\.greasyfork\.org\/scripts\/\d+\/(?<ver>\d+)\/(?<name>.+?)\.js$/,
JsDelivr: /^https?:\/\/cdn\.jsdelivr\.net\/(?<reg>\w+)\/(@[^/]+\/)?(?<name>[^@]+)@(?<ver>[^/]+)/,
Cloudflare: /^https?:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/(?<name>[^/]+)\/(?<ver>[^/]+)/,
};
const commonRegistries = {
npm: "NPM",
gh: "GitHub",
};
// We're interested in `@grant`, `@connect`, `@require`, `@resource`
const interestedMetadata = {};
const interestedKeys = {
grant: {
brief: "Required permissions",
display: (value) => {
const valueCode = document.createElement("code");
valueCode.textContent = value;
if (value !== "none") {
const valueLink = document.createElement("a");
valueLink.appendChild(valueCode);
valueLink.href = `https://www.tampermonkey.net/documentation.php#api:${valueCode.textContent}`;
valueLink.title = `See documentation about ${valueCode.textContent}`;
return valueLink;
} else {
return valueCode;
}
}
},
connect: {
brief: "Allowed URLs to connect",
display: (value) => {
const valueCode = document.createElement("code");
valueCode.textContent = value;
return valueCode;
}
},
require: {
brief: "External libraries",
display: (value) => {
const valueLink = document.createElement("a");
valueLink.href = value;
valueLink.textContent = value;
for (const [host, regex] of Object.entries(commonHosts)) {
const match = value.match(regex);
if (match) {
const { name, ver, reg } = match.groups;
const optionalRegistry = commonRegistries[reg] ? `${commonRegistries[reg]} on ` : "";
valueLink.textContent = `${decodeURIComponent(name)}@${ver} (${optionalRegistry}${host})`;
break;
}
}
return valueLink;
}
},
resource: {
brief: "External resources",
display: (value) => {
const valueCode = document.createElement("code");
const [name, link] = value.split(" ");
const valueLink = document.createElement("a");
valueLink.appendChild(valueCode);
valueLink.href = link.trim();
valueCode.textContent = name.trim();
return valueLink;
}
}
};
for (const key in interestedKeys) {
const values = metadata[key] ?? [];
interestedMetadata[key] = Array.isArray(values) ? values : [values];
}
log("Interested Metadata:", interestedMetadata);
// Display
for (const [key, values] of Object.entries(interestedMetadata)) {
const keyInfo = interestedKeys[key];
const li = metaBlock.appendChild(document.createElement("li"));
const keyLink = li.appendChild(document.createElement("a"));
keyLink.href = `https://www.tampermonkey.net/documentation.php#meta:${key}`;
keyLink.title = keyInfo.brief;
keyLink.textContent = `@${key}`;
const separator = li.appendChild(document.createElement("span"));
separator.textContent = ": ";
for (const value of values) {
li.appendChild(keyInfo.display(value));
const separator = li.appendChild(document.createElement("span"));
separator.textContent = ", ";
}
if (values.length > 0) {
li.lastChild.remove(); // Remove the last separator
} else {
li.appendChild(document.createTextNode("none"));
}
}
}
}
metadata(configProxy["codeblocks.metadata"]);
// Display
// Flat layout
function flatLayout(enable) {
const meta_orig = $("#script-info > #script-content .script-meta-block");
const meta_mod = $("#script-info > .script-meta-block");
if (enable && meta_orig) {
const header = $("#script-info > header");
header.before(meta_orig);
} else if (!enable && meta_mod) {
const additional = $("#script-info > #script-content #additional-info");
additional.before(meta_mod);
}
}
flatLayout(configProxy["display.flatLayout"]);
// Always show notification
function alwaysShowNotification(enable) {
const nav = $("#nav-user-info");
const profile = nav?.querySelector(".user-profile-link");
const existing = nav.querySelector(".notification-widget");
if (!nav || !profile || existing && existing.textContent !== "0") return; // There's unread notification or user is not logged in
if (enable && !existing) {
const notification = nav.insertBefore(document.createElement("a"), profile);
notification.className = "notification-widget";
notification.textContent = "0";
notification.href = profile.querySelector("a").href + "/notifications";
} else if (!enable && existing) {
existing.remove();
}
}
alwaysShowNotification(configProxy["display.alwaysShowNotification"]);
// Other
// Short link
function shortLink(enable) {
const description = $("div#script-content");
const url = window.location.href;
const scriptId = url.match(/\/scripts\/(\d+)/)?.[1];
if (!scriptId || !description) return;
const id = idPrefix + "short-link";
const current = document.getElementById(id);
if (current && !enable) {
current.remove();
} else if (!current && enable) {
const short = `https://greasyfork.org/scripts/${scriptId}`;
const p = description.insertAdjacentElement("beforebegin", document.createElement("p"));
p.id = id;
p.textContent = "Short link: ";
const link = p.appendChild(document.createElement("a"));
link.href = short;
link.textContent = short;
const copy = p.appendChild(document.createElement("a"));
copy.textContent = "(Copy)";
copy.style.marginLeft = "1em";
copy.style.cursor = "pointer";
copy.title = "Copy short link to clipboard";
copy.addEventListener("click", () => {
if (copy.textContent === "(Copied!)") return;
navigator.clipboard.writeText(short).then(() => {
copy.textContent = "(Copied!)";
window.setTimeout(() => {
copy.textContent = "(Copy)";
}, 1000);
});
});
}
}
shortLink(configProxy["other.shortLink"]);
// Alternative URLs for library
function alternativeURLs(enable) {
if ($(".remove-attachments") || !$("div#script-content") || $("div#script-content > div#install-area")) return; // Not a library
const id = idPrefix + "lib-alternative-url";
const current = document.getElementById(id);
if (current && !enable) {
current.remove();
} else if (!current && enable) {
const description = $("div#script-content > p");
const trim = "// @require ";
const text = description?.querySelector("code")?.textContent;
if (!text || !text.startsWith(trim)) return; // Found no URL
const url = text.slice(trim.length);
const parts = url.split("/");
const scriptId = parts[4];
const scriptVersion = parts[5];
const fileName = parts[6];
const URLs = [
[`// @require https://update.greasyfork.org/scripts/${scriptId}/${fileName}`, "Latest version"],
[`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}?version=${scriptVersion}`, "Current version (Legacy)"],
[`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}`, "Latest version (Legacy)"],
];
const detail = document.createElement("p").appendChild(document.createElement("details"));
description.after(detail.parentElement);
detail.parentElement.id = id;
detail.appendChild(document.createElement("summary")).textContent = "Alternative URLs";
const list = detail.appendChild(document.createElement("ul"));
for (const [url, text] of URLs) {
const link = list.appendChild(document.createElement("li")).appendChild(document.createElement("code"));
link.textContent = url;
link.title = text;
}
}
}
alternativeURLs(configProxy["other.libAlternativeUrl"]);
// Image proxy
if (configProxy["other.imageProxy"]) {
const PROXY = "https://wsrv.nl/?url=";
const images = $$("a[href^='/rails/active_storage/blobs/redirect/'] > img[src^='https://greasyfork.']");
for (const img of images) {
img.src = PROXY + img.src;
const link = img.parentElement;
link.href = PROXY + link.href;
}
}
// Lazy image
if (configProxy["other.lazyImage"]) {
const images = $$(".user-content img");
for (const image of images) {
image.loading = "lazy";
}
}
// Initialize css
for (const prop in dynamicStyle) {
cssHelper(prop, configProxy[prop]);
}
// Dynamically respond to config changes
const callbacks = {
"filterAndSearch.shortcut": shortcut,
"filterAndSearch.regexFilter": regexFilter,
"codeblocks.autoHideCode": autoHide,
"codeblocks.autoHideRows": autoHide,
"codeblocks.tabSize": tabSize,
"codeblocks.metadata": metadata,
"display.flatLayout": flatLayout,
"display.alwaysShowNotification": alwaysShowNotification,
"other.shortLink": shortLink,
"other.libAlternativeUrl": alternativeURLs,
};
config.addEventListener("set", e => {
const callback = callbacks[e.detail.prop];
if (callback && (e.detail.before !== e.detail.after)) {
callback(e.detail.after);
}
if (e.detail.prop in dynamicStyle) {
cssHelper(e.detail.prop, e.detail.after);
}
});
})();