สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/540511/1635852/GGn%20Formatters.js
// ==UserScript==
// @name GGn Formatters
// @version 26
// @description Formatters
// @author ingts (some by ZeDoCaixao and letsclay)
// @match https://gazellegames.net/
// ==/UserScript==
if (typeof RegExp.escape === 'undefined') { // temporary
RegExp.escape = function (s) {
return s.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
}
}
let destructiveEditsEnabled = false
const isEditPage = location.href.includes("action=editgroup")
/**
* @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", ["と", "ト"]],
["yo", ["よ", "ヨ"]],
["ka", ["か", "カ"]],
["ya", ["や", "ヤ"]],
["de", ["で", "デ"]],
["ni", ["に", "ニ"]],
["so", ["そ", "ソ"]],
["na", ["な", "ナ"]],
["i", ["い", "イ"]],
["u", ["う", "ウ"]],
["e", ["え", "エ"]],
["o", ["お", "オ"]],
["wo", ["を", "ヲ"]],
["san", ["さん"]],
["sama", ["さま"]],
["kun", ["くん"]],
["chan", ["ちゃん"]],
["de", ["で", "デ"]],
["ne", ["ね", "ネ"]],
["sa", ["さ", "サ"]],
["ba", ["ば", "バ"]],
["demo", ["でも", "デモ"]],
])
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 allUppercaseWords = ['RPG', 'FPS', 'TPS', 'RTS', 'TBS', 'MMO', 'MMORPG', 'ARPG', 'JRPG', 'PVP', 'PVE', 'NTR', 'NPC', 'OST']
const uppercaseRegex = new RegExp(`^[A-Z]{2}$|${allUppercaseWords.join('|')}`)
return str
.replace(/\s/g, ' ')
.replace(/ -(.*)- /, ': $1 ')
.replace('—', ' - ')
.replace(/ ?~$/, '')
.replace(/-$/, '')
.replace(/^-/, '')
.replace(/ ~ ?/, ': ')
.replace(/ - ?/, ': ')
.replace(/[™®©]/g, '')
.replace(' : ', ': ')
.trim()
.split(wordSeparators)
.map((current, index, array) => {
const isSmallWord = smallWords.test(current)
if ((uppercaseRegex.test(current) && !isSmallWord) || /\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 the current word has an uppercase letter after 2 lowercase letters, assume it should just be like that
if (!/\w[a-z]{2}[A-Z]/.test(current)) current = current.toLowerCase()
const firstOrLastWord = index === 0 || index === array.length - 1
if (alias && !firstOrLastWord) {
const jpWords = japaneseLowercase.get(current)
if (jpWords?.some(w => alias.includes(w))) return current
}
if (
/* Check for small words */
isSmallWord &&
/* Ignore first and last word */
!firstOrLastWord &&
/* 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('')
}
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(/\bdefence\b/g, 'defense')
about = about.replace(/ *\/ */g, '/')
// Replace different list symbols with [*]
about = about.replace(/^\s*[-•◦■・★*]\s*/gm, '[*]')
// 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 (destructiveEditsEnabled) {
// If a line is all uppercase, title case it
about = about.split('\n').map(line => line.toUpperCase() === line ? formatTitle(line) : line).join('\n')
// Bold text before colon in list item, change -/— to :
about = about.replace(/\[\*](?:\[b])?(.*?)(?:\[\/b])?(?:: *| +- *| *— *)(\w)/g,
(_, p1, p2) => `[*][b]${p1}[/b]: ${p2.toUpperCase()}`)
}
// bold game title. replace [u] or [i] with bold
if (gameTitle) {
const boldTitleRegex = new RegExp(`((?:\\[[uib]])+)?(?:${RegExp.escape(gameTitle)}|${RegExp.escape(formatTitle(gameTitle))})(?:(?:\\[\\/[uib]])+)?`, '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)
*/
// If a line starts with [b]text[/b]: and it is not the only text on that line, add [*] at the start and replace tags with [b]
about = about.replace(/^\[b](.*?]:)(.*)/gm, (match, p1, p2) => {
if (p2.trim() === '') {
return match
}
return p1 ? `[*][b]${removeBbcode(p1)}${p2}` : match
})
// 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}`)
// Title case text inside [align=center][b][u] and remove extra tags
about = about.replace(/\[align=center]\[([bu])]\[([bu])]([\s\S]*?)\[\/\2]\[\/\1]\[\/align]/g,
(match, p1, p2, p3) => `[align=center][b][u]${formatTitle(removeBbcode(p3))}[/u][/b][/align]`)
}
about = about.replace(/\n*\s*\[\*]\s*/g, "\n[*]")
const regFeatures = /\n(\[size=3])?\n?(?:\[\*])?(\[(b|u|i|align=center|size=2)]\n?){0,4}(the features|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(str) {
const lines = str.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')
}
/**
* @param {string} str
* @returns {string}
*/
function formatSysReqs(str) {
if (!str) return ""
const sysReqsHeader = headersMap.get("sysReqs")
let reqs = new RegExp(`${RegExp.escape(sysReqsHeader)}(.*)\\[\\/quote]`, 's').exec(str)?.[1]
if (!reqs) return str
const original = reqs
const osHeader = headersMap.get("os")
reqs = reqs.replace(/:\n/g, "\n")
reqs = reqs.replace(/:\[\/b]\n/g, "[/b]\n")
if (destructiveEditsEnabled) {
// remove + after a number
reqs = reqs.replace(/(\d)\+/g, '$1')
reqs = reqs.replace(/\.x/g, '')
}
const platform = (isEditPage &&
document.getElementById('nexusmodsuri') ? 'Windows'
: (document.getElementById('itunesuri') && document.getElementById('epicgamesuri')) ? 'Mac'
: document.getElementById('googleplayuri') ? 'Android'
: (document.getElementById('itunesuri') && !document.getElementById('epicgamesuri')) ? 'iOS'
: '')
|| document.getElementById('platform')?.value
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])]|:)(?:\n*(?!\S))?/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])]|:)(?:\n*(?!\S))?/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("audio\\scard|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
// remove duplicate Additional Notes
let notes
reqs = reqs.replace(new RegExp(RegExp.escape(headersMap.get("additionalnotes")) + '.*?$', 'gms'), match => {
if (!notes) {
notes = match
return match
}
return notes.toLowerCase() === match.toLowerCase() ? '' : match
})
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)' : '')
})
reqs = reqs.replace(/intel/gi, 'Intel')
reqs = reqs.replace(/amd/gi, 'AMD')
if (destructiveEditsEnabled) {
reqs = reqs.replace(/x?\(?32.?(?:bit)?\)?/gi, "")
reqs = reqs.replace(/\s*x64/gi, " (64-bit)")
reqs = reqs.replace(/\s*\(?64.?bit\)?/gi, " (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')
// uppercase unit
reqs = reqs.replace(/(\d+)\s?(\w)b/gi, (match, p1, p2) => `${p1} ${p2.toUpperCase()}B`)
// add space between at least 2 letters then a number
reqs = reqs.replace(/([a-zA-Z]{2,})(\d)/g, '$1 $2')
reqs = reqs.replace(/,? *(?:or|and) +(?:up|better|greater|higher|over|more|later|newer|faster|similar|equal|equivalent|comparable)/gi, '')
reqs = reqs.replace(/ *\(\)/g, '') // leftover empty brackets from the above because it needs to handle (or better) or (3 GHz or greater / 6 cores or greater)
reqs = reqs.replace(/ *以上/g, '')
// 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
})
if (platform === 'Android' || platform === 'iOS') {
// add the OS label if missing
reqs = reqs.replace(/^(?:android|ios)? *(\d.*)/i,
osHeader.replace('\n', '') + platform + ' ' + '$1')
}
formatSection('OS', match => {
match = match.replace(/ *home/gi, '')
match = match.replace(/^(\d)/, `${platform ? platform + ' ' : ''}$1`)
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 = replaceOrAndCommaWithSlash(match)
match = match.replace(/ *\/ */g, '/')
// Remove repeated OS names
let name = ''
match = match.replace(/[a-zA-Z]+ /g, match => {
if (!name) {
name = match
return match
}
return match.toLowerCase() === name.toLowerCase() ? '' : match
})
return match
})
formatSection('Processor', match => {
match = match.replace(/ryzen/gi, 'Ryzen')
match = match.replace(/core/gi, 'Core')
match = match.replace(/(?<!intel )pentium/gi, 'Intel Pentium')
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(/ *processor */gi, '')
match = match.replace(/(?:intel )?core 2 (Solo|Duo|Quad|Extreme)/gi, (match, p1) =>
`Intel Core 2 ${toSentenceCase(p1)}`)
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
})
match = match.replace(/^(\d\.?\d? .Hz),? (.+ core)$/gi, (match, hz, cores) => cores + ' ' + hz)
match = match.replace(/(?:intel )?(?:core *)?i(\d)(?: *- *(\d+)(\w)?)?/gi, // no space i(\d) so that Pentium II+ won't change
(_, gen, model, suffix) => `Intel Core i${gen}${model ? `-${model}` : ''}${suffix ? suffix.toUpperCase() : ''}`)
match = replaceOrAndCommaWithSlash(match)
// 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]}`
}
}
match = match.replace(/(\d)\/([a-zA-Z])/g, '$1 / $2')
return match
})
formatSection('Memory', match => {
return match.replace(/([kmg])B(?: ?ram)?/i, '$1B RAM')
})
formatSection('Storage', match => {
return match.replace(/hd(?!d)/i, 'HDD')
})
formatSection('Graphics', match => {
match = match.replace(/(?<!V)RAM|memory/gi, 'VRAM')
if (destructiveEditsEnabled) {
match = match.replace(/(?:nvidia )?(?:geforce )?([rg]tx) ?(\d+)/gi,
(_, tx, num) => `Nvidia GeForce ${tx.toUpperCase()} ${num}`)
match = match.replace(/graphics card with (.*?)\s?(?:of)? V?RAM/gi, '$1 VRAM')
match = match.replace(/(?:of )?(?:dedicated )?(?:(?<!intel hd )graphics |video )?/gi, '')
match = match.replace(/(at least )?(\d [MG]B)(?! ?vram)/gi, (match, p1, p2) => {
if (!p1 && p2.includes('GB')) return match
return `${p2} VRAM`
})
match = replaceOrAndCommaWithSlash(match)
match = match.replace(/(?:series|video)?\s?cards? ?/gi, '')
}
match = match.replace(/d(?:irect)?x (\d)(?:\.0(?!\w))?/gi, 'DirectX $1')
match = match.replace(/nvidia/gi, 'Nvidia')
match = match.replace(/open ?gl/gi, 'OpenGL')
match = match.replace(/(?:amd )?radeon/gi, 'AMD Radeon')
match = match.replace(/(?:intel )?arc/gi, 'Intel Arc')
match = match.replace(/gpu/gi, 'GPU')
match = match.replace(/(\d) ?Ti/gi, '$1 Ti')
match = match.replace(/(\w)\/(\w)/g, '$1 / $2')
match = match.replace(
/(OpenGL .*) hardware driver support required for WebGL acceleration.*\((AMD Catalyst .*? \/ Nvidia \d.*)\)/i, '$2; $1')
return match
})
formatSection('DirectX', match => {
match = match.replace(/v(?:ersion)?\s?/i, '')
match = match.replace(/\.0(?!\w)/, '')
return match
})
reqs = reqs.replace(/(\S)\(/g, '$1 (')
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')
reqs = reqs.replace(regExp, (_, p1, p2) => `${p1}${func(p2)}`)
}
function replaceOrAndCommaWithSlash(str) {
return str.replace(/,? or |, /gi, ' / ')
}
}
/**
* @param {string} str
* @returns {string}
*/
function formatDescCommon(str) {
if (!str) return ""
if (destructiveEditsEnabled) {
// Convert full-width ASCII characters (!~) to half-width
str = str.replace(/[\uFF01-\uFF5E]/g,
match => String.fromCharCode(match.charCodeAt(0) - 0xFEE0)).replace(/\u3000/g, ' ');
}
str = str.replace(/\s*([.,?!:])[^\S\r\n]+/g, '$1 ')
str = str.replace(/[™®©]/g, '')
str = str.replace(/。/g, '.')
str = str.replace(/?/g, '?')
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(/([a-z0-9])([.,])(?![A-Z]\.)(\w)/g, (match, p1, p2, p3) => { // try to avoid initialisms
if (/\d/.test(p1) && /\d/.test(p3)) return match // dont add space for numbers, decimals
return `${p1}${p2} ${p3}`
})
// Move punctuation outside closing tags
str = str.replace(/((?:\[[bui]])+)(.*?)([,.?!:])[^\S\r\n]*((?:\[\/[bui]])+)[^\S\r\n]*/g, '$1$2$4$3 ')
str = str.replace(/\[u]\[b](.*?)\[\/b]\[\/u]/g, '[b][u]$1[/u][/b]')
// 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]")
// here so that formatAbout can find it
const sysReqsHeader = headersMap.get("sysReqs")
if (!str.includes(sysReqsHeader)) {
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)
}
//endregion
str = str.replace(/[^\S\r\n]*\n/g, '\n')
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
}
function removeBbcode(str) {
return str.replace(/\[(\/?[^\]]+)]/g, '')
}
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 {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'
}
}
}