您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Formatters
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/540511/1625874/GGn%20Formatters.js
// ==UserScript== // @name GGn Formatters // @version 8 // @description Formatters // @author ingts (some by ZeDoCaixao and letsclay) // @match https://gazellegames.net/ // ==/UserScript== /** * @param {string} str * @param {string=} alias * @returns {string} */ function formatTitle(str, alias) { if (!str) return '' const japaneseLowercase = new Map([ ["ga", ["が", "ガ"]], ["no", ["の", "ノ"]], ["wa", ["わ", "ワ"]], ["mo", ["も", "モ"]], ["kara", ["から", "カラ"]], ["made", ["まで", "マデ"]], ["to", ["と", "ト"]], ["ya", ["や", "ヤ"]], ["de", ["で", "デ"]], ["ni", ["に", "ニ"]], ["so", ["そ", "ソ"]], ["na", ["な", "ナ"]], ["i", ["い", "イ"]], ["u", ["う", "ウ"]], ["e", ["え", "エ"]], ["o", ["お", "オ"]], ["san", ["さん"]], ["sama", ["さま"]], ["kun", ["くん"]], ["chan", ["ちゃん"]] ]) const smallWords = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|v.?|vs.?|via)$/i const alphanumericPattern = /([A-Za-z0-9\u00C0-\u00FF])/ const wordSeparators = /([ :–—-]|[^a-zA-Z0-9'’])/ const allUppercase = new Set(['rpg', 'fps', 'tps', 'rts', 'tbs', 'mmo', 'mmorpg', 'arpg', 'jrpg', 'pvp', 'pve', 'ntr', 'td', 'vr', 'npc', 'ost']) return str .replace(/\s/g, ' ') .replace(/ -(.*)- /, ': $1 ') .replace('—', ' - ') .replace(/ ?~$/, '') .replace(/-$/, '') .replace(/^-/, '') .replace(/ ~ ?/, ': ') .replace(/ - ?/, ': ') .replace(/[™®©]/g, '') .replace(' : ', ': ') .toLowerCase().trim() .split(wordSeparators) .map((current, index, array) => { const isFirstWord = index === 0 const isLastWord = index === array.length - 1 if (allUppercase.has(current.trim()) || /\b([IVX])(X{0,3}I{0,3}|X{0,2}VI{0,3}|X{0,2}I?[VX])(?![A-Za-z'])\b/i.test(current)) { return current.toUpperCase() } if (alias && !isFirstWord) { const jpWords = japaneseLowercase.get(current) if (jpWords?.some(w => alias.includes(w))) return current } if ( /* Check for small words */ current.search(smallWords) > -1 && /* Ignore first and last word */ !isFirstWord && !isLastWord && /* Ignore title end and subtitle start */ array[index - 3] !== ':' && array[index + 1] !== ':' && /* Ignore small words that start a hyphenated phrase */ (array[index + 1] !== '-' || (array[index - 1] === '-' && array[index + 1] === '-')) ) { return current } /* Capitalize the first letter */ return current.replace(alphanumericPattern, match => match.toUpperCase()) }) .join('') } let destructiveEditsEnabled = false const headersMap = new Map([ ["aboutGame", "[align=center][b][u]About the game[/u][/b][/align]\n"], ["features", "\n[align=center][b][u]Features[/u][/b][/align]"], ["sysReqs", "\n\n[quote][align=center][b][u]System Requirements[/u][/b][/align]\n"], ["minimumReqs", "\n[b]Minimum[/b]"], ["recommendedReqs", "\n[b]Recommended[/b]"], ["os", "\n[*][b]OS[/b]: "], ["processor", "\n[*][b]Processor[/b]: "], ["memory", "\n[*][b]Memory[/b]: "], ["storage", "\n[*][b]Storage[/b]: "], ["graphics", "\n[*][b]Graphics[/b]: "], ["soundcard", "\n[*][b]Sound Card[/b]: "], ["directX", "\n[*][b]DirectX[/b]: "], ["additionalnotes", "\n[*][b]Additional Notes[/b]: "], ["other", "\n[*][b]Other[/b]: "], ["network", "\n[*][b]Network[/b]: "], ["drive", "\n[*][b]Drive[/b]: "], ["controllers", "\n[*][b]Controllers[/b]: "], ]) /** * @param {string} str * @param {string=} gameTitle * @returns {string} */ function formatAbout(str, gameTitle) { if (!str) return "" const aboutHeader = headersMap.get("aboutGame") const aboutGameRegex = /^(\[size=3])?\n?(\[(b|u|i|align=center)]\n?){0,4}(About\sthe\sGame|About\sThis\sGame|What\sis\sThis\sGame\?|About|Description)\s*:?(\n?\[\/(b|u|i|align|size)]){0,4}(\s*:|:|)/i str = str.replace(aboutGameRegex, aboutHeader) let [_, about, reqs] = new RegExp(`(?:${RegExp.escape(aboutHeader)})?(.*?)(\\[quote].*|$)`, 's').exec(str) about = about.trim() about = about.replace(/^\[align=.*?](.*)\[\/align]$/s, '$1') about = about.replace(/defence/g, 'defense') about = about.replace(/ *\/ */g, '/') // bold game title. replace [u] or [i] with bold if (gameTitle) { const boldTitleRegex = new RegExp(`(\\[[uib]\])?${RegExp.escape(gameTitle)}(?:\\[\\/[ui]])?`, 'i') about = about.replace(boldTitleRegex, (match, p1) => { if (p1 === '[b]') return match return `[b]${gameTitle}[/b]` }) } // If a line starts with [u], [i], or [b], there is no other text on that line, and it contains 'features', replace tags with [align=center][b][u] about = about.replace(/^(\[b]|\[u]|\[i])*(.*?)(\[\/b]|\[\/u]|\[\/i])*$/gm, (match, p1, p2, p3) => (p1 && p3 && /features/i.test(p2)) ? `[align=center][b][u]${p2}[/u][/b][/align]` : match) // Replace different list symbols with [*] about = about.replace(/^[-•◦■・★]\s*/gm, '[*]') // If a line starts with [u], [i], or [b], : and it is not the only text on that line, add [*] at the start and replace tags with [b] about = about.replace(/^(\[b]|\[u]|\[i])*(.*?)(\[\/b]|\[\/u]|\[\/i]):(.*$)/gm, (match, p1, p2, p3, p4) => { if (p4.trim() === '') { return match } return p1 && p3 ? `[*][b]${p2}[/b]${p4}` : match }) // If a line starts with [*] followed by a [u] or [i], replace them with [b] about = about.replace(/^\[\*]\[[ui]](.*?)\[\/[ui]]/gm, '[b]$1[/b]') // If a line starts with [*], have only one new line until the next [*] and after the last one, have a double newline about = fixMultiLinesInLists(about) /* // Remove double newlines between [*] lines about = about.replace(/(\[\*][^\n]*)(\n{2,})(?=\[\*])/g, '$1\n') // Add a newline when next line doesn't start with [*] about = about.replace(/(\[\*][^\n]*\n)([^\[*\]\n])/g, '$1\n$2') */ // If the line starts with [*] and the whole line until terminal punctuation is wrapped in [u], [i], or [b], remove the wrapping tags about = about.replace(/^\[\*]\[([bui])](.*?)\[\/([bui])]([.?!。?!])$/gm, "[*]$2$4") // If a line ends with [/align] replace double newlines with one newline about = about.replace(/(\[\/align])\n\n/g, '$1\n') // Remove colons in [align=center] about = about.replace(/\[align=center].*?(?:\[\/\w]:|:)\[\/align]/g, match => match.replace(/:/g, '')) if (destructiveEditsEnabled) { // If a line starts with [u], [i], or [b] and has only a new line after the closing tag, make it a list item about = about.replace(/\[[uib]](.*?)\[\/[uib]]:?\n(.*)/g, (_, p1, p2) => `[*][b]${p1}[/b]: ${p2}`) // Sentence case text inside list items about = about.replace(/\[\*]\[b](.*)\[\/b]/gm, (match, p1) => match.replace(p1, toSentenceCase(p1))) // Title case text inside [align=center][b][u] about = about.replace(/\[align=center]\[([bu])]\[([bu])]([\s\S]*?)\[\/\2]\[\/\1]\[\/align]/g, (match, p1, p2, p3) => `[align=center][b][u]${formatTitle(p3)}[/u][/b][/align]`) // Bold text before colon in list item about = about.replace(/\[\*](\w+): /g, '[*][b]$1[/b]: ') // If a line is all uppercase, title case it about = about.split('\n').map(line => line.toUpperCase() === line ? formatTitle(line) : line).join('\n') } about = about.replace(/\n*\s*\[\*]\s*/g, "\n[*]") const regFeatures = /\n(\[size=3])?\n?(\[(b|u|i|align=center|size=2)]\n?){0,4}(Key\sFeatures|Main\sFeatures|Game\sFeatures|Other\sFeatures|Features\sof\sthe\sGame|Features|Featuring|Feautures)\s*:?\s*(\n?\[\/(b|u|i|align|size)]){0,4}(\s*:|:|)/i const featuresHeader = headersMap.get("features") about = about.replace(regFeatures, featuresHeader) // Add features header about = about.replace(/(.*?)\n((?:\[\*].*\n?){3,})/g, (match, lineBefore, list) => { if (!lineBefore.includes('[/align]')) return (/^\s+$/.test(lineBefore) ? '': lineBefore + '\n') + (about.includes(featuresHeader) ? '' : featuresHeader) + '\n' + list return match }) // Add a newline before lines with [align=center] if there isn't already a double newline before it. Here after adding features header about = about.replace(/(?<!\n\n)(\[align=center])/g, '\n\$1') about = about.replace(/\[\/align]\n*/gi, "[/align]\n") about = about.replace(/\n{2,10}/g, "\n\n") return aboutHeader + about + (reqs ? `\n\n${reqs}` : '') function fixMultiLinesInLists() { const lines = about.split('\n') const result = [] let i = 0 while (i < lines.length) { const line = lines[i] // If line starts with [*], we're in a list section if (line.startsWith('[*]')) { const listItems = [] // Collect all consecutive [*] items (skipping empty lines between them) while (i < lines.length && (lines[i].startsWith('[*]') || lines[i].trim() === '')) { if (lines[i].startsWith('[*]')) { listItems.push(lines[i]) } i++ } // Add list items with single newlines between them result.push(...listItems) // Add single empty line after the list section if there's more content if (i < lines.length) { result.push('') // This creates one empty line, which with the next line creates a double newline } } else { // Regular line, add it result.push(line) i++ } } return result.join('\n') } } function toSentenceCase(str) { const pos = /]?\w/.exec(str)?.index // mainly to skip [*] if (pos === undefined) return let lowerStr = str.toLowerCase() const newStr = lowerStr.charAt(pos).toUpperCase() + lowerStr.slice(pos + 1) // Capitalise subsequent sentences return str.substring(0, pos) + newStr.replace(/([.?!\]]\s+)([a-z])/g, (match, p1, p2) => p1 + p2.toUpperCase()) } /** * @param {string} str * @returns {string} */ function formatSysReqs(str) { if (!str) return "" const sysReqsHeader = headersMap.get("sysReqs") str = str.replace(/\n?^(\[size=3])?\n*(\[(b|u|i|quote|align=center|align=left)]\n?){0,5}\s*(System\sRequirements|Game\sSystem\sRequirements|Requirements|GOG\sSystem\sRequirements|Minimum\sSystem\sRequirements|System\sRequierments)\s*([:.])?\s*(\n?\[\/(b|u|i|align|size)]){0,5}(\s*:|:|)\n*(\n\[align=(left|center)])?/i, sysReqsHeader) let reqs = new RegExp(`${RegExp.escape(sysReqsHeader)}(.*)\\[\\/quote]`, 's').exec(str)?.[1] if (!reqs) return str const original = reqs reqs = reqs.replace(/:\n/g, "\n") reqs = reqs.replace(/:\[\/b]\n/g, "[/b]\n") reqs = reqs.replace(/(\d)\+/g, '$1') const osHeader = headersMap.get("os") const mobileOsMatch = /^(?:android|ios)? *\d.*/i.exec(reqs)?.[0] if (mobileOsMatch) { return str.replace(original, `${osHeader.replace('\n', '')}${mobileOsMatch}`) } reqs = reqs.replace(/intel/gi, 'Intel') reqs = reqs.replace(/amd/gi, 'AMD') reqs = reqs.replace(/\(?64.?bit\)?/g, "(64-bit)") const minimumHeader = headersMap.get("minimumReqs") const recommendedHeader = headersMap.get("recommendedReqs") //region Section labels formatting (mostly from Description Broom) // Minimum reqs = reqs.replace(/\n*(\[\*]\[([bi])]|((\s*|)\[\*])|\[([bi])]|\*|)(\s*|)(Minimum\sSpecifications|Minimum\sSystem\sRequirements|Minimum\sRequirements|Minimum)(\s|)(:\s\[\/([bi])]|:\[\/([bi])]|\[\/([bi])]:|\[\/([bi])]|:)/gi, minimumHeader) // Recommended reqs = reqs.replace(/\n*(\[\*]\[([bi])]|((\s*|)\[\*])|\[([bi])]|\*|)(\s*|)(Recommended\sSpecifications|Recommended\sSystem\sRequirements|Re(c|cc)o(mm|m)ended)(\s|)(:\s\[\/([bi])]|:\[\/([bi])]|\[\/([bi])]:|\[\/([bi])]|:)/gi, recommendedHeader) formatSectionLabel("Supported\\sOS|OS|Operating\\sSystems|Operating\\sSystem|Mac\\sOS|System|Mac", osHeader) formatSectionLabel("CPU\\sType|CPU\\sProcessor|CPU|Processor", headersMap.get("processor")) formatSectionLabel("System\\sRAM|RAM|System\\sMemory|Memory", headersMap.get("memory")) formatSectionLabel("Free\\sHard\\sDisk\\sSpace|Hard\\sDrive\\sSpace|Hard\\sDisk\\sSpace|Hard\\sDisk|Free\\sSpace|Hard\\sDrive|HDD\\sSpace|HDD|Storage|Disk\\sSpace|Free\\sDisk\\sSpace|Drive\\sSpace|Available\\sHard\\sDisk\\sSpace", headersMap.get("storage")) formatSectionLabel("VGA|Graphics|Graphic\\sCard|GPU|Video\\sCard|Video|GFX", headersMap.get("graphics")) formatSectionLabel("Sound\\sCard|Sound", headersMap.get("soundcard")) formatSectionLabel("DirectX\\sVersion|DirectX|Direct\\sX|DX", headersMap.get("directX")) formatSectionLabel("Additional\\sNotes|Additional|Notice|Please\\snote|Notes", headersMap.get("additionalnotes")) formatSectionLabel("Other\\sRequirements|Other|Peripherals", headersMap.get("other")) formatSectionLabel("Network|Internet", headersMap.get("network")) formatSectionLabel("(CD\\sDrive\\sSpeed|Disc\\sDrive|CD-ROM|DVD\\sDrive)", headersMap.get("drive")) formatSectionLabel("Controllers|Supported\\sJoysticks|Input", headersMap.get("controllers")) //endregion reqs = reqs.replace(/\n(.*)\n?\[\*]Requires a 64-bit.*\n?(.*)/g, (_, header, nextLine) => { /* Remove the whole section when it's like [b]Recommended[/b] [*]Requires a 64-bit processor and operating system[/quote] */ if (!nextLine) return '' return `\n${header ? `${header}\n` : ''}${nextLine}` + (nextLine.includes('OS') && !nextLine.includes('64') ? ' (64-bit)' : '') }) // has minimum but no recommended, replace the minimum with new line if (reqs.includes(minimumHeader) && !reqs.includes(recommendedHeader)) { reqs = reqs.replace(minimumHeader + '\n', "") } reqs = reqs.replace(/OS \*/g, 'OS') reqs = reqs.replace(/(\d+)\s?(\w)b/gi, (match, p1, p2) => `${p1} ${p2.toUpperCase()}B`) reqs = reqs.replace(/([a-zA-Z]{2,})(\d)/g, '$1 $2') reqs = reqs.replace(/,? *\(?or\)? *(?:better|greater|higher|over|more|later|newer|faster|similar|equal|equivalent)\)?/gi, '') // convert to next unit if divisible by 1024 reqs = reqs.replace(/(\d+)\s*([KM]B)/gi, (match, num, unit) => { const intNum = parseInt(num) if (intNum % 1024 === 0) { return unit === 'KB' ? `${intNum / 1024} MB` : unit === 'MB' ? `${intNum / 1024} GB` : match } return match }) formatSection('OS', match => { match = match.replace(/\(?:?32.*64.?bit\)? ?/gi, '') // both bits written, remove all match = match.replace(/^ *\(?64-bit\)?\s?(.*)/g, "$1 (64-bit)") match = match.replace(/(?:Microsoft\s?)?Win(?:dows)?/gi, 'Windows') match = match.replace(/macos/gi, "macOS") match = match.replace(/, /g, '/') // Remove repeated OS names let firstSkipped = false match = match.replace(/[a-zA-Z]+ /g, match => { if (!firstSkipped) { firstSkipped = true return match } return '' }) return match }) formatSection('Processor', match => { match = match.replace(/ or |, /gi, ' / ') match = match.replace(/ryzen/gi, 'Ryzen') match = match.replace(/core/gi, 'Core') match = match.replace(/core with (.*)Hz/gi, 'Core $1Hz') match = match.replace(/(.*?)-core/gi, '$1 Core') match = match.replace(/(\d\.?\d?)\s?(\w)hz/gi, (match, p1, p2) => `${p1} ${p2.toUpperCase()}Hz`) if (destructiveEditsEnabled) { match = match.replace(/(\w+-? *)Core (?:intel )?i(\d)/gi, (m, p1, p2) => { if (p1.toLowerCase().trim() !== 'intel') return `${p1} Core Intel Core i${p2}` // keep the cores quantifier e.g. Quad Core/4-Core return m }) // if all Hz are the same, put it after a comma at the end const hzRegex = / \d\.?\d? \wHz/g const hzMatches = [...match.matchAll(hzRegex)] if (hzMatches.length > 1 && hzMatches.every(arr => arr[0] === hzMatches[0][0])) { for (const hzMatch of hzMatches) { match = match.replace(hzMatch[0], '') } match = match + `, ${hzMatches[0][0]}` } } // too complicated to combine with the 1st one above match = match.replace(/(?:intel )?(?:core )?i(\d)(?: *- *(\d+)(\w)?)?/gi, (_, gen, model, sfx) => `Intel Core i${gen}${model ? `-${model}` : ''}${sfx ? sfx.toUpperCase() : ''}`) return match }) formatSection('Graphics', match => { match = match.replace(/ or |, /gi, ' / ') if (destructiveEditsEnabled) { match = match.replace(/(?:nvidia )?(?:geforce )?([rg]tx) ?(\d+)/gi, (_, tx, num) => `Nvidia GeForce ${tx.toUpperCase()} ${num}`) match = match.replace(/(?:of )?(?:dedicated )?(?:(?<!intel hd )graphics |video )?/gi, '') } match = match.replace(/(?<!V)RAM|memory/, 'VRAM') match = match.replace(/graphics card with (.*?)\s?(?:of)? V?RAM/gi, '$1 VRAM') match = match.replace(/nvidia/gi, 'Nvidia') match = match.replace(/(?:amd )?radeon/gi, 'AMD Radeon') match = match.replace(/gpu/gi, 'GPU') match = match.replace(/(\d)Ti/gi, '$1 Ti') match = match.replace(/(?:series|video)?\s?card/gi, '') match = match.replace(/(\w)\/(\w)/g, '$1 / $2') return match }) formatSection('DirectX', match => { match = match.replace(/v(?:ersion)?\s?/i, '') match = match.replace('.0', '') return match }) reqs = reqs.replace(/(\d)\/([a-zA-Z])/g, '$1 / $2') reqs = reqs.replace(/(\S)\(/g, '$1 (') // remove duplicate Additional Notes let notes reqs = reqs.replace(/\n\[\*]\[b]Additional Notes\[\/b]:.*?$/gms, match => { if (!notes) { notes = match return match } return notes.toLowerCase() === match.toLowerCase() ? '' : match }) return str.replace(original, reqs) function formatSectionLabel(partialPattern, replacement) { reqs = reqs.replace( // colon only, colon preceded/followed by [/b], colon with space before [/b] new RegExp(`\\n(?:\\[\\*]\\[b]|\\s*\\[\\*]|\\[b]|\\*)\\s*(?:${partialPattern})\\s*(?::\\s\\[\\/b]|:\\[\\/b]|\\[\\/b]:|:)\\s`, 'gi'), replacement) } /** * @param {string} sectionName * @param {(match: string) => string} func */ function formatSection(sectionName, func) { const regExp = new RegExp(`^\\[\\*]\\[b]${sectionName}\\[\\/b]: (.*)`, 'gm') for (const match of reqs.matchAll(regExp)) { if (match?.[1]) { reqs = reqs.replace(match[1], func(match[1])) } } } } /** * @param {string} str * @returns {string} */ function formatDescCommon(str) { if (!str) return "" str = str.replace(/[™®©]/g, '') str = str.replace(/ ?\((?:[RC]|TM)\)/gi, '') str = str.replace(/([?!#$%;])(\w)/g, '$1 $2') str = str.replace(/([a-zA-Z]):(\w)/g, '$1: $2') // [a-zA-Z] to avoid aspect ratio str = str.replace(/(\w)[.,](\w)/g, (match, p1, p2) => { if (/\d/.test(p1) && /\d/.test(p2)) return match // dont add space for numbers, decimals return `${p1}. ${p2}` }) // Move : and . outside of closing tags str = str.replace(/((?:\[[bui]])+)(.*?)([:.])((?:\[\/[bui]])+)/g, '$1$2$4$3') // region Description Broom stuff if (str.includes("[quote]") && !str.includes("[/quote]")) { str = str.replace("\n[/align]", "[/quote]") if (!str.includes("[/quote]")) { str = str + "[/quote]" } } str = str.replace(/\n{2,10}\[align=center]/g, "\n\n[align=center]") str = str.replace(/\n*\[quote]/g, "\n\n[quote]") str = str.replace(/\n*\[\/quote].*/gi, "[/quote]") //endregion return str } /** * @param {string} str * @param {string=} gameTitle * @returns {string} */ function formatAll(str, gameTitle) { str = formatDescCommon(str) str = formatAbout(str, gameTitle) str = formatSysReqs(str) return str } /** * @param {HTMLTextAreaElement} textarea * @param {string} unformattedDesc */ function createUnformattedArea(textarea, unformattedDesc) { let div = document.getElementById('unformatted-desc') if (div) return textarea.insertAdjacentHTML('beforebegin', `<div id="unformatted-desc"> <button type="button" style="margin-bottom: 5px;"> Show unformatted description</button> <textarea cols="30" rows="15" style="filter: brightness(0.8); display:none; margin-bottom: 5px;margin-left: 0;width: 100%;" readonly>${unformattedDesc}</textarea> </div>`) div = document.getElementById('unformatted-desc') const ta = div.querySelector('textarea') const btn = div.querySelector('button') btn.onclick = () => { if (ta.style.display === 'none') { ta.style.display = 'block' btn.textContent = 'Hide unformatted description' } else if (ta.style.display === 'block') { ta.style.display = 'none' btn.textContent = 'Show unformatted description' } } }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址