您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Buttons to format description
// ==UserScript== // @name GGn Game Description Formatter // @namespace none // @version 1.1 // @description Buttons to format description // @author ingts // @grant GM_addStyle // @match https://gazellegames.net/upload.php* // @match https://gazellegames.net/torrents.php?action=editgroup&groupid=* // @require https://update.greasyfork.org/scripts/540511/1627900/GGn%20Formatters.js // ==/UserScript== // noinspection CssUnusedSymbol destructiveEditsEnabled = true //language=css GM_addStyle(` #description-formatter { display: flex; gap: 3%; align-items: center; margin: 5px 0; padding: 0 10px; > div > button { height: auto; white-space: nowrap; padding: 5px; } button { transition-property: background-color; transition-duration: 1s; } button:active { background-color: gainsboro; transition-duration: 0s; } section { display: grid; grid-template-columns: 1fr 3fr; column-gap: 15px; row-gap: 8px; } .formatter-buttons-row { display: flex; flex-wrap: wrap; gap: 3px; } } `) const isEditPage = location.href.includes("action=editgroup") /** @type {HTMLTextAreaElement} */ const descInput = isEditPage ? document.querySelector("textarea[name='body']") : document.getElementById('album_desc') // language=HTML descInput.insertAdjacentHTML('afterend', ` <section id="description-formatter"> <div style="display:flex;flex-direction:column;gap: 8px;width: fit-content;"> <button type="button">Format All</button> <button type="button">Format About</button> <button type="button">Format SR</button> </div> <section> <div> <strong>Casing</strong> <div class="formatter-buttons-row"></div> </div> <div> <strong>BBCode</strong> <div class="formatter-buttons-row"></div> </div> <div> <strong>Headers</strong> <div class="formatter-buttons-row"></div> </div> <div> <strong>System Requirements</strong> <div class="formatter-buttons-row"></div> <div class="formatter-buttons-row" style="margin-top: 3px;"></div> </div> </section> </section>`) const main = document.getElementById('description-formatter') const mainButtons = main.querySelectorAll(':scope > div button') const title = isEditPage ? document.querySelector("#content > div > h2 > a").textContent : document.getElementById('title').value mainButtons[0].onclick = () => mainButtonClick(desc => formatAll(desc, title)) mainButtons[1].onclick = () => mainButtonClick(desc => { const common = formatDescCommon(desc) return formatAbout(common, title) }) mainButtons[2].onclick = () => mainButtonClick(desc => { const common = formatDescCommon(desc) return formatSysReqs(common) }) function mainButtonClick(func) { const desc = descInput.value descInput.value = func(desc) createUnformattedArea(descInput, desc) } /** @typedef {{ label: string, tooltip?: string, func?: (str: string) => string, text?: string, insertPosition?: 'start' | 'end', includeNewLine?: boolean, returnTextOnly?: boolean, useSelection?: boolean, }} ButtonProp */ /** @type {ButtonProp[]} */ const casingButtons = [ { label: "Sentence case", func: toSentenceCase, returnTextOnly: true }, { label: "Title Case", func: formatTitle, returnTextOnly: true }, ] /** @type {ButtonProp[]} */ const bbCodeButtons = [ { func: (str) => str, label: "Unwrap", returnTextOnly: true, tooltip: "Unwraps all BBCode" }, { func: removeBbcode, label: "Unwrap All", tooltip: `Remove all BBCode in the selection`, useSelection: true, }, { func: (str) => `[*][b]${str.replaceAll(':', '')}[/b]: `, label: "Header to List Item", includeNewLine: true, returnTextOnly: true, tooltip: `Unwraps then convert to "[*][b]{text}[b]: ", removing colons and the new line` }, { func: (str) => `[*]${str}`, label: "To List Item", tooltip: `Unwraps then convert to "[*]{text}"` }, { func: (str) => `[b]${str}[/b]`, label: "Bold", returnTextOnly: true, tooltip: `Unwraps then wraps with [b]`, }, ] /** @type {ButtonProp[]} */ const headerButtons = [ { label: "About the game", text: headersMap.get("aboutGame"), insertPosition: 'start' }, { label: "Features", text: headersMap.get("features"), }, { label: "Gameplay", text: "\n[align=center][b][u]Gameplay[/u][/b][/align]", }, { label: "Story", text: "\n[align=center][b][u]Story[/u][/b][/align]", }, { label: "Characters", text: "\n[align=center][b][u]Characters[/u][/b][/align]", }, ] /** @type {ButtonProp[]} */ const sysReqsButtons = [ { label: "Regular", text: `${headersMap.get("sysReqs")} [b]Minimum[/b]${headersMap.get("os")}${headersMap.get("processor")}${headersMap.get("memory")}${headersMap.get("graphics")}${headersMap.get("storage")} [b]Recommended[/b]${headersMap.get("os")}${headersMap.get("processor")}${headersMap.get("memory")}${headersMap.get("graphics")}${headersMap.get("storage")}[/quote]`, insertPosition: 'end', }, { label: "None", text: `${headersMap.get("sysReqs")}None provided[/quote]`, insertPosition: 'end', }, { label: "Empty", text: `${headersMap.get("sysReqs")}[/quote]`, insertPosition: 'end', }, { label: "Minimum", text: `${headersMap.get("minimumReqs")}` }, { label: "Recommended", text: `${headersMap.get("recommendedReqs")}` }, ] /** @type {ButtonProp[]} */ const sysReqsLineButtons = [ { label: "OS", text: `${headersMap.get("os")}` }, { label: "Processor", text: `${headersMap.get("processor")}` }, { label: "Memory", text: `${headersMap.get("memory")}` }, { label: "Storage", text: `${headersMap.get("storage")}` }, { label: "Graphics", text: `${headersMap.get("graphics")}` }, { label: "Sound Card", text: `${headersMap.get("soundcard")}` }, { label: "DirectX", text: `${headersMap.get("directX")}` }, { label: "Additional Notes", text: `${headersMap.get("additionalnotes")}` }, { label: "Other", text: `${headersMap.get("other")}` }, { label: "Network", text: `${headersMap.get("network")}` }, { label: "Drive", text: `${headersMap.get("drive")}` }, { label: "Controllers", text: `${headersMap.get("controllers")}` }, ] const buttonDivs = main.querySelectorAll('.formatter-buttons-row') addButtons({ buttonProps: casingButtons, appendTo: buttonDivs[0], replaceTextOnly: true, }) addButtons({ buttonProps: bbCodeButtons, appendTo: buttonDivs[1], }) addButtons({ buttonProps: headerButtons, appendTo: buttonDivs[2], insertText: true, textAsTooltip: true, }) addButtons({ buttonProps: sysReqsButtons, appendTo: buttonDivs[3], insertText: true, textAsTooltip: true, }) addButtons({ buttonProps: sysReqsLineButtons, appendTo: buttonDivs[4], insertText: true, textAsTooltip: true, }) /** * @param {ButtonProp[]} buttonProps * @param {Element} appendTo * @param {boolean?} replaceTextOnly * @param {boolean?} insertText * @param {boolean?} textAsTooltip */ function addButtons({ buttonProps, appendTo, replaceTextOnly, insertText, textAsTooltip }) { for (const buttonProp of buttonProps) { const button = document.createElement('button') button.textContent = buttonProp.label button.type = 'button' const tooltip = textAsTooltip ? buttonProp.text : buttonProp.tooltip if (tooltip) { $(button).tooltipster({ // content: `<pre style="width: 100%;margin: 0;">${tooltip}</pre>`, content: tooltip.replace(/\n/g, '<br>'), contentAsHTML: true, }) } appendTo.append(button) button.onclick = () => { if (buttonProp.useSelection) { const selectionStart = descInput.selectionStart const selectedText = descInput.value.substring(selectionStart, descInput.selectionEnd) descInput.value = descInput.value.replace(selectedText, buttonProp.func(selectedText)) descInput.selectionStart = selectionStart descInput.selectionEnd = selectionStart } else { if (insertText) { const textToInsert = buttonProp.text const insertAtStart = buttonProp?.insertPosition === 'start' const insertAtEnd = buttonProp?.insertPosition === 'end' const currentText = descInput.value let newValue, newCaretPos if (insertAtStart || insertAtEnd) { newValue = insertAtStart ? textToInsert + currentText : currentText + textToInsert newCaretPos = insertAtStart ? textToInsert.length : newValue.length } else { const selectionStart = descInput.selectionStart const selectionEnd = descInput.selectionEnd const before = currentText.substring(0, selectionStart) const after = currentText.substring(selectionEnd, currentText.length) newValue = before + textToInsert + after newCaretPos = selectionStart + textToInsert.length } descInput.value = newValue descInput.selectionStart = newCaretPos descInput.selectionEnd = newCaretPos } else { const result = getBBCodeOrLineAtCaret(replaceTextOnly, buttonProp.includeNewLine) const replacement = buttonProp.func(buttonProp.returnTextOnly ? result.textOnly : result.text) const caretOffset = replacement.length - (result.end - result.start) const newCaretPos = descInput.selectionStart + caretOffset + ((replaceTextOnly ? result.textOnly.length : result.text.length - result.openingTagsLength) - replacement.length) debugger descInput.value = result.replace(replacement) descInput.selectionStart = newCaretPos descInput.selectionEnd = newCaretPos } } descInput.focus() } } } // button to convert align to [*][b] /** * @param {boolean} textOnly * @param {boolean?} includeNewLine * @returns {{text: string, start: number, end: number, textOnly: string, openingTagsLength: number, replace: (str: string) => string}} */ function getBBCodeOrLineAtCaret(textOnly, includeNewLine) { const text = descInput.value const caretPos = descInput.selectionStart // Find the current line boundaries const beforeCaret = text.substring(0, caretPos) const afterCaret = text.substring(caretPos) const lastNewlineBefore = beforeCaret.lastIndexOf('\n') const nextNewlineAfter = afterCaret.indexOf('\n') const lineStart = lastNewlineBefore === -1 ? 0 : lastNewlineBefore + 1 const lineEnd = nextNewlineAfter === -1 ? text.length : caretPos + nextNewlineAfter const currentLine = text.substring(lineStart, lineEnd) const caretPosInLine = caretPos - lineStart // BBCode tag pattern - matches opening and closing tags const tagPattern = /\[(\/?[^\]]+)]/g const tags = [] // Find all tags in the current line for (const match of currentLine.matchAll(tagPattern)) { const match1 = match[1] if (match1 === '*' || match1 === '#') { continue } tags.push({ tag: match1, fullTag: match[0], start: match.index, end: match.index + match[0].length, isClosing: match1.startsWith('/') }) } // Build a stack to track nested tags const tagStack = [] const tagPairs = [] for (const tag of tags) { if (tag.isClosing) { // Find matching opening tag const tagName = tag.tag.substring(1) // Remove the '/' prefix for (let i = tagStack.length - 1; i >= 0; i--) { // Extract base tag name (before any = or space) const openingTagName = tagStack[i].tag.split(/[=\s]/)[0] if (openingTagName === tagName) { tagPairs.push({ opening: tagStack[i], closing: tag }) tagStack.splice(i, 1) break } } } else { tagStack.push(tag) } } // Find all tag pairs that contain the caret, sorted by nesting level const containingPairs = [] for (const pair of tagPairs) { const contentStart = pair.opening.end const contentEnd = pair.closing.start // Check if caret is within the content of this tag pair if (caretPosInLine >= contentStart && caretPosInLine <= contentEnd) { containingPairs.push({ ...pair, range: contentEnd - contentStart }) } } if (containingPairs.length === 0) { // No BBCode found, return the whole line return { text: currentLine, textOnly: currentLine, start: lineStart, end: lineEnd, openingTagsLength: 0, replace: function (newText) { const before = text.substring(0, lineStart) const after = text.substring(lineEnd + (includeNewLine ? 1 : 0)) return before + newText + after } } } // Sort by range (innermost first) containingPairs.sort((a, b) => a.range - b.range) // Find the outermost pair (contains all others) const outermostPair = containingPairs[containingPairs.length - 1] const innermostPair = containingPairs[0] const targetPair = textOnly ? containingPairs[0] : outermostPair const absoluteStart = textOnly ? lineStart + targetPair.opening.end : lineStart + outermostPair.opening.start const onlyHasBbCode = /^\[(?!\*]).*\[\/.*]$/.test(currentLine) const absoluteEnd = (textOnly ? lineStart + targetPair.closing.start : lineStart + outermostPair.closing.end) + (includeNewLine && onlyHasBbCode ? 1 : 0) const fullBBCode = currentLine.substring(outermostPair.opening.start, outermostPair.closing.end) // Check if the line contains only BBCode tags const textContent = currentLine.substring(innermostPair.opening.end, innermostPair.closing.start) return { text: fullBBCode, textOnly: textContent, start: absoluteStart, end: absoluteEnd, openingTagsLength: containingPairs.reduce((length, pair) => length + pair.opening.fullTag.length, 0), replace: function (newText) { const before = text.substring(0, absoluteStart) const after = text.substring(absoluteEnd) return before + newText + after } } }