URL Replacer/Redirector

Redirect specific sites by replacing part of the URL.

当前为 2025-05-29 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name URL Replacer/Redirector
  3. // @namespace https://github.com/theborg3of5/Userscripts/
  4. // @version 2.0
  5. // @description Redirect specific sites by replacing part of the URL.
  6. // @author Gavin Borg
  7. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  8. // @match https://greasyfork.org/en/scripts/403100-url-replacer-redirector
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_deleteValue
  13. // @grant GM.getValue
  14. // @grant GM.setValue
  15. // ==/UserScript==
  16.  
  17. // Grant GM_*value for legacy Greasemonkey, GM.*value for Greasemonkey 4+
  18.  
  19. // Settings conversion should be removed after a while, including:
  20. // - convertOldStyleSettings()
  21. // - Grant for GM_deleteValue
  22.  
  23. // Our configuration instance - this loads/saves settings and handles the config popup.
  24. const Config = new GM_config();
  25.  
  26. (async function ()
  27. {
  28. 'use strict';
  29.  
  30. // Find the site that we matched
  31. const startURL = window.location.href;
  32. const currentSite = getUserSiteForURL(startURL);
  33. // Add a menu item to the menu to launch the config
  34. GM_registerMenuCommand('Configure redirect settings', () => Config.open());
  35. // Set up and load config
  36. let configSettings = buildConfigSettings(currentSite);
  37. await initConfigAsync(configSettings); // await because we need to read from the resulting (async-loaded) values
  38.  
  39. // Convert old-style settings if we find them.
  40. await convertOldStyleSettings(configSettings);
  41. // Get replacement settings for the current URL
  42. const replaceSettings = getSettingsForSite(currentSite);
  43. if (!replaceSettings)
  44. {
  45. return;
  46. }
  47.  
  48. // Build new URL
  49. const newURL = transformURL(startURL, replaceSettings);
  50. // Redirect to the new URL
  51. if (startURL === newURL)
  52. {
  53. logToConsole("Current URL is same as redirection target: " + newURL);
  54. return
  55. }
  56. window.location.replace(newURL);
  57. })();
  58.  
  59. // Get the site (entry from user includes/matches) that matches the current URL.
  60. function getUserSiteForURL(startURL)
  61. {
  62. for (const site of getUserSites())
  63. {
  64. // Use a RegExp so we check case-insensitively
  65. let siteRegex = "";
  66. if (site.startsWith("/"))
  67. {
  68. siteRegex = new RegExp(site.slice(1, -1), "i"); // If the site starts with a /, treat it as a regex (but remove the leading/trailing /)
  69. }
  70. else
  71. {
  72. siteRegex = new RegExp(site.replace(/\*/g, "[^ ]*"), "i"); // Otherwise replace * wildcards with regex-style [^ ]* wildcards
  73. }
  74.  
  75. if (siteRegex.test(startURL)) {
  76. return site; // First match always wins
  77. }
  78. }
  79. }
  80.  
  81. // We support both includes and matches, but only the user-overridden ones of each.
  82. function getUserSites()
  83. {
  84. return GM_info.script.options.override.use_matches.concat(GM_info.script.options.override.use_includes);
  85. }
  86.  
  87. // Perform the replacements specified by the given settings.
  88. function transformURL(startURL, siteSettings)
  89. {
  90. const { prefix, suffix, targetStrings, replacementStrings } = siteSettings;
  91.  
  92. // Transform the URL
  93. let newURL = startURL;
  94. for (let i = 0; i < targetStrings.length; i++)
  95. {
  96. let toReplace = prefix + targetStrings[i] + suffix;
  97. const replaceWith = prefix + replacementStrings[i] + suffix;
  98.  
  99. // Use a RegEx to allow case-insensitive matching
  100. toReplace = new RegExp(escapeRegex(toReplace), "i"); // Escape any regex characters - we don't support actual regex matching.
  101.  
  102. newURL = newURL.replace(toReplace, replaceWith);
  103. }
  104.  
  105. return newURL;
  106. }
  107.  
  108. // From https://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript/3561711#3561711
  109. function escapeRegex(string)
  110. {
  111. return string.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&');
  112. }
  113.  
  114. // Log a message to the console with a prefix so we know it's from this script.
  115. function logToConsole(message)
  116. {
  117. console.log("URL Replacer/Redirector: " + message);
  118. }
  119.  
  120. //#region Config handling
  121. // Build the settings object for GM_config.init()
  122. function buildConfigSettings(currentSite)
  123. {
  124. // Build fields for each site
  125. const fields = buildSiteFields(currentSite);
  126.  
  127. const styles = `
  128. /* Float the target strings fields to the left so that they can line up with their corresponding replacements */
  129. div[id*=${fieldTargetStrings("")}] {
  130. float: left;
  131. }
  132.  
  133. /* We use one section sub-header on the current site to call it out. We're overriding the
  134. default settings from the framework (which include the ID), so !important is needed for
  135. most of these properties. */
  136. .section_desc {
  137. float: right !important;
  138. background: #00FF00 !important;
  139. color: black !important;
  140. width: fit-content !important;
  141. font-weight: bold !important;
  142. padding: 4px !important;
  143. margin: 0px auto !important;
  144. border-top: none !important;
  145. border-radius: 0px 0px 10px 10px !important;
  146. }";
  147. `.replaceAll("\n", ""); // This format is nicer to read but the newlines cause issues in the config framework, so remove them
  148.  
  149. return {
  150. id: "URLReplacerRedirectorConfig",
  151. title: "URL Replacer/Redirector Config",
  152. fields: fields,
  153. css: styles,
  154. };
  155. }
  156.  
  157. // Build the specific fields in the config
  158. function buildSiteFields(currentSite)
  159. {
  160. let fields = {};
  161. for (const site of getUserSites())
  162. {
  163. // Section headers are the site URL as the user entered them
  164. const sectionName = [site];
  165. if (currentSite === site)
  166. {
  167. sectionName.push("This site"); // If this is the matched site, add a subheader to call it out
  168. }
  169.  
  170. fields[fieldPrefix(site)] = {
  171. section: sectionName, // Section definition just goes on the first field inside
  172. type: "text",
  173. label: "Prefix",
  174. labelPos: "left",
  175. size: 75,
  176. title: "This string (if set) must appear directly before the target string in the URL.",
  177. }
  178. fields[fieldSuffix(site)] = {
  179. type: "text",
  180. label: "Suffix",
  181. labelPos: "left",
  182. size: 75,
  183. title: "This string (if set) must appear directly after the target string in the URL.",
  184. }
  185. fields[fieldTargetStrings(site)] = {
  186. type: "textarea",
  187. label: "Targets",
  188. labelPos: "above",
  189. title: "Enter one target per line. Each target will be replaced by its corresponding replacement.",
  190. }
  191. fields[fieldReplacementStrings(site)] = {
  192. type: "textarea",
  193. label: "Replacements",
  194. labelPos: "above",
  195. title: "Enter one replacement per line. Each replacement with replace its corresponding target.",
  196. }
  197. fields[fieldClearSite(site)] = {
  198. type: "button",
  199. label: "Clear redirects for this site",
  200. title: "Clear all fields for this site, removing all redirection.",
  201. save: false, // Don't save this field, it's just a button
  202. click: function (siteToClear)
  203. {
  204. return () => {
  205. Config.set(fieldPrefix(siteToClear), "");
  206. Config.set(fieldSuffix(siteToClear), "");
  207. Config.set(fieldTargetStrings(siteToClear), "");
  208. Config.set(fieldReplacementStrings(siteToClear), "");
  209. }
  210. }(site), // Immediately invoke this wrapper with the current site so the inner function can capture it
  211. }
  212. }
  213.  
  214. return fields;
  215. }
  216.  
  217. // This is just a Promise wrapper for GM_config.init that allows us to await initialization.
  218. async function initConfigAsync(settings)
  219. {
  220. return new Promise((resolve) =>
  221. {
  222. // Have the init event (which should fire once config is done loading) resolve the promise
  223. settings["events"] = {init: resolve};
  224.  
  225. Config.init(settings);
  226. });
  227. }
  228.  
  229. // Get the settings for the given site.
  230. function getSettingsForSite(site)
  231. {
  232. if (!site)
  233. {
  234. console.log("No matching site found for URL");
  235. return null;
  236. }
  237.  
  238. // Retrieve and return the settings
  239. return {
  240. prefix: Config.get(fieldPrefix(site)),
  241. suffix: Config.get(fieldSuffix(site)),
  242. targetStrings: Config.get(fieldTargetStrings(site)).split("\n"),
  243. replacementStrings: Config.get(fieldReplacementStrings(site)).split("\n"),
  244. }
  245. }
  246.  
  247. //#region Field name "constants" based on their corresponding sites
  248. // These are also the keys used with [GM_]Config.get/set.
  249. function fieldPrefix(site)
  250. {
  251. return "Prefix_" + site;
  252. }
  253. function fieldSuffix(site)
  254. {
  255. return "Suffix_" + site;
  256. }
  257. function fieldTargetStrings(site)
  258. {
  259. return "TargetString_" + site;
  260. }
  261. function fieldReplacementStrings(site)
  262. {
  263. return "ReplacementString_" + site;
  264. }
  265. function fieldClearSite(site)
  266. {
  267. return "ClearSite_" + site;
  268. }
  269. //#endregion Field name "constants" based on their corresponding sites
  270. //#endregion Config handling
  271.  
  272. // Convert settings from the old style (simple GM_setValue/GM_getValue storage, 1 config for all
  273. // sites) to the new style (GM_config, one set of settings per site).
  274. async function convertOldStyleSettings(gmConfigSettings)
  275. {
  276. // Check the only really required setting (for the script to do anything)
  277. const replaceAry = GM_getValue("replaceTheseStrings");
  278. if (!replaceAry)
  279. {
  280. return; // No old settings to convert, done.
  281. }
  282. logToConsole("Old-style settings found");
  283.  
  284. // Safety check: if we ALSO have new-style settings, leave it alone.
  285. if (GM_getValue("URLReplacerRedirectorConfig"))
  286. {
  287. logToConsole("New-style settings already exist, not converting old settings.");
  288. return;
  289. }
  290. const replacePrefix = GM_getValue("replacePrefix");
  291. const replaceSuffix = GM_getValue("replaceSuffix");
  292. // Old style: 1 config for ALL sites
  293. // New style: 1 config PER site
  294. // So, the conversion is just to copy the config onto each site.
  295. logToConsole("Starting settings conversion...");
  296. for (const site of getUserSites())
  297. {
  298. // Split replaceAry into targets (keys) and replacements (values)
  299. let targetsAry = [];
  300. let replacementsAry = [];
  301. for (const target in replaceAry)
  302. {
  303. targetsAry.push(target);
  304. replacementsAry.push(replaceAry[target]);
  305. }
  306.  
  307. Config.set(fieldPrefix(site), replacePrefix);
  308. Config.set(fieldSuffix(site), replaceSuffix);
  309. Config.set(fieldTargetStrings(site), targetsAry.join("\n"));
  310. Config.set(fieldReplacementStrings(site), replacementsAry.join("\n"));
  311. }
  312.  
  313. // Save new settings
  314. Config.save();
  315. logToConsole("New-style settings saved.");
  316. // Remove the old-style settings so we don't do this again each time.
  317. GM_deleteValue("replaceTheseStrings");
  318. GM_deleteValue("replacePrefix");
  319. GM_deleteValue("replaceSuffix");
  320. logToConsole("Old-style settings removed.");
  321. logToConsole("Conversion complete.");
  322. }