您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a live side-panel preview when editing a character profile on F-List using a native parser.
当前为
// ==UserScript== // @name F-List Live Profile Preview (Parser Version) // @namespace http://tampermonkey.net/ // @version 3.4 // @description Adds a live side-panel preview when editing a character profile on F-List using a native parser. // @author Gemini // @match *://*.f-list.net/character_edit.php* // @grant GM_addStyle // @grant unsafeWindow // @require https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js // ==/UserScript== (function() { 'use strict'; /* global $, unsafeWindow, GM_addStyle */ // ------------------------------------------------- // START: BBCode Parser // ------------------------------------------------- const appendTextWithLineBreaks = (parent, text) => { if (!parent || typeof text !== 'string') return; const lines = text.split('\n'); lines.forEach((line, index) => { if (line.length > 0) { parent.appendChild(document.createTextNode(line)); } if (index < lines.length - 1) { parent.appendChild(document.createElement('br')); } }); }; class BBCodeTag { noClosingTag = false; allowedTags = undefined; constructor(tag, tagList) { this.tag = tag; if (tagList !== undefined) this.setAllowedTags(tagList); } isAllowed(tag) { return this.allowedTags === undefined || this.allowedTags[tag] !== undefined; } setAllowedTags(allowed) { this.allowedTags = {}; for (const tag of allowed) this.allowedTags[tag] = true; } } class BBCodeSimpleTag extends BBCodeTag { constructor(tag, elementName, classes, tagList) { super(tag, tagList); this.elementName = elementName; this.classes = classes; } createElement(parser, parent, param) { const el = parser.createElement(this.elementName); if (this.classes !== undefined && this.classes.length > 0) { el.className = this.classes.join(' '); } parent.appendChild(el); return el; } } class BBCodeCustomTag extends BBCodeTag { constructor(tag, customCreator, tagList) { super(tag, tagList); this.customCreator = customCreator; } createElement(parser, parent, param) { return this.customCreator(parser, parent, param); } } class BBCodeTextTag extends BBCodeTag { constructor(tag, customCreator) { super(tag, []); // Text tags cannot have child tags this.customCreator = customCreator; } createElement(parser, parent, param, content) { return this.customCreator(parser, parent, param, content); } } class BBCodeParser { _tags = {}; _line = 1; _column = 1; _currentTag = { tag: '<root>', line: 1, column: 1 }; addTag(impl) { this._tags[impl.tag] = impl; } createElement(tag) { return document.createElement(tag); } parseEverything(input) { const parent = this.createElement('span'); parent.className = 'bbcode'; this.parse(input, 0, undefined, parent, () => true, 0); return parent; } parse(input, start, self, parent, isAllowed, depth) { let currentTag = this._currentTag; const selfAllowed = self !== undefined ? isAllowed(self.tag) : true; if (self !== undefined) { const parentAllowed = isAllowed; isAllowed = name => self.isAllowed(name) && parentAllowed(name); currentTag = this._currentTag = { tag: self.tag, line: this._line, column: this._column }; } let tagStart = -1, paramStart = -1, mark = start; for (let i = start; i < input.length; ++i) { const c = input[i]; if (c === '\n') { this._line++; this._column = 1; } else { this._column++; } if (c === '[') { tagStart = i; paramStart = -1; } else if (c === '=' && tagStart !== -1 && paramStart === -1) { paramStart = i; } else if (c === ']' && tagStart !== -1) { const paramIndex = paramStart === -1 ? i : paramStart; let tagKey = input.substring(tagStart + 1, paramIndex).trim().toLowerCase(); if (tagKey.length === 0) { tagStart = -1; continue; } const param = paramStart > tagStart ? input.substring(paramStart + 1, i) : ''; const close = tagKey[0] === '/'; if (close) tagKey = tagKey.substr(1).trim(); if (this._tags[tagKey] === undefined) { tagStart = -1; continue; } const tag = this._tags[tagKey]; const allowed = isAllowed(tagKey); if (!close) { if (parent !== undefined) { appendTextWithLineBreaks(parent, input.substring(mark, tagStart)); } mark = i + 1; if (!allowed || parent === undefined || depth > 100) { i = this.parse(input, i + 1, tag, undefined, isAllowed, depth + 1); mark = i + 1; continue; } if (tag instanceof BBCodeTextTag) { const endPos = this.parse(input, i + 1, tag, undefined, isAllowed, depth + 1); const contentEnd = input.lastIndexOf('[', endPos); const content = input.substring(mark, contentEnd > mark ? contentEnd : mark); tag.createElement(this, parent, param.trim(), content); i = endPos; } else { const element = tag.createElement(this, parent, param.trim()); if (element !== undefined && !tag.noClosingTag) { i = this.parse(input, i + 1, tag, element, isAllowed, depth + 1); } } mark = i + 1; this._currentTag = currentTag; } else if (self !== undefined && self.tag === tagKey) { if (parent !== undefined) { appendTextWithLineBreaks(parent, input.substring(mark, tagStart)); } return i; } tagStart = -1; } } if (mark < input.length && parent !== undefined) { appendTextWithLineBreaks(parent, input.substring(mark)); } return input.length; } } // ------------------------------------------------- // END: BBCode Parser // ------------------------------------------------- // ------------------------------------------------- // START: F-List Parser Configuration // ------------------------------------------------- function createFListParser() { const parser = new BBCodeParser(); // Simple formatting tags parser.addTag(new BBCodeSimpleTag('b', 'b')); parser.addTag(new BBCodeSimpleTag('i', 'i')); parser.addTag(new BBCodeSimpleTag('u', 'u')); parser.addTag(new BBCodeSimpleTag('s', 's')); parser.addTag(new BBCodeSimpleTag('sup', 'sup')); parser.addTag(new BBCodeSimpleTag('sub', 'sub')); parser.addTag(new BBCodeSimpleTag('quote', 'blockquote')); // Horizontal Rule const hrTag = new BBCodeSimpleTag('hr', 'hr'); hrTag.noClosingTag = true; parser.addTag(hrTag); // Alignment tags parser.addTag(new BBCodeCustomTag('center', (p, parent) => { const el = p.createElement('div'); el.style.textAlign = 'center'; parent.appendChild(el); return el; })); parser.addTag(new BBCodeCustomTag('right', (p, parent) => { const el = p.createElement('div'); el.style.textAlign = 'right'; parent.appendChild(el); return el; })); parser.addTag(new BBCodeCustomTag('justify', (p, parent) => { const el = p.createElement('div'); el.style.textAlign = 'justify'; parent.appendChild(el); return el; })); // Color tag parser.addTag(new BBCodeCustomTag('color', (p, parent, param) => { const el = p.createElement('span'); if (/^(#([0-9a-f]{3}){1,2}|[a-z]+)$/i.test(param)) { el.style.color = param; } parent.appendChild(el); return el; })); // URL tag parser.addTag(new BBCodeTextTag('url', (p, parent, param, content) => { const a = p.createElement('a'); const url = (param || content).trim(); const text = content.trim(); if (url && (url.startsWith('http://') || url.startsWith('https://'))) { a.href = url; a.target = '_blank'; a.rel = 'noopener noreferrer nofollow'; appendTextWithLineBreaks(a, text); parent.appendChild(a); } else { appendTextWithLineBreaks(parent, `[url${param ? '=' + param : ''}]${content}[/url]`); } })); // Image tag parser.addTag(new BBCodeTextTag('img', (p, parent, param, content) => { const img = p.createElement('img'); img.style.maxWidth = '100%'; if (param) { // [img=ID]...[/img] case const inlines = unsafeWindow.FList.Inlines.inlines; const inlineData = inlines ? inlines[param] : null; if (inlineData) { const { hash, extension } = inlineData; // THE FIX IS HERE: Use substring(2, 4) instead of substring(2, 2) img.src = `https://static.f-list.net/images/charinline/${hash.substring(0, 2)}/${hash.substring(2, 4)}/${hash}.${extension}`; img.alt = content.trim(); } else { appendTextWithLineBreaks(parent, `[img=${param}]${content}[/img]`); return; } } else { // [img]URL[/img] case const url = content.trim(); if (url.startsWith('http://') || url.startsWith('https://')) { img.src = url; } else { appendTextWithLineBreaks(parent, `[img]${content}[/img]`); return; } } parent.appendChild(img); })); // Icon / User tags const createUserTag = (p, parent, param, content) => { const name = content.trim(); if (!name) return; const a = p.createElement('a'); a.href = `https://www.f-list.net/c/${encodeURIComponent(name)}`; a.target = '_blank'; a.className = 'character-icon'; const img = p.createElement('img'); img.src = `https://static.f-list.net/images/avatar/${name.toLowerCase()}.png`; img.style.width = '50px'; img.style.height = '50px'; img.style.verticalAlign = 'middle'; img.style.marginRight = '5px'; a.appendChild(img); appendTextWithLineBreaks(a, name); parent.appendChild(a); }; parser.addTag(new BBCodeTextTag('icon', createUserTag)); parser.addTag(new BBCodeTextTag('user', createUserTag)); // Collapse tag parser.addTag(new BBCodeCustomTag('collapse', (p, parent, param) => { const header = p.createElement('div'); header.className = 'CollapseHeader'; const headerText = p.createElement('div'); headerText.className = 'CollapseHeaderText'; const headerSpan = p.createElement('span'); // Use a non-breaking space for blank titles to ensure the header has height appendTextWithLineBreaks(headerSpan, param || '\u00A0'); headerText.appendChild(headerSpan); header.appendChild(headerText); const block = p.createElement('div'); block.className = 'CollapseBlock'; block.style.display = 'none'; parent.appendChild(header); parent.appendChild(block); $(header).on('click', function() { $(block).slideToggle(200); }); return block; // Children will be parsed into this block })); return parser; } // ------------------------------------------------- // END: F-List Parser Configuration // ------------------------------------------------- function waitForElementAndRun() { console.log("F-List Live Preview: Waiting for editor and inline data..."); const interval = setInterval(function() { if (document.getElementById('CharacterEditDescription') && typeof unsafeWindow.FList !== 'undefined' && typeof unsafeWindow.FList.Inlines !== 'undefined') { clearInterval(interval); console.log("F-List Live Preview: Editor and all data ready! Initializing."); main(); } }, 100); } function main() { GM_addStyle(` #Page > tbody > tr { display: flex; width: 100%; } #Content { flex: 1 1 50%; min-width: 600px; } #live-preview-container { flex: 1 1 50%; padding: 0 10px 10px 10px; vertical-align: top; } #live-preview-content { background: #1c1c1c; border: 1px solid #444; min-height: 500px; height: 80vh; overflow-y: auto; padding: 10px; } #live-preview-content .character-description { line-height: 1.4; color: #ccc; word-wrap: break-word; } /* F-list specific styles for parser output */ #live-preview-content blockquote { border-left: 3px solid #555; background: #2e2e2e; padding: 5px 10px; margin: 8px 0; } #live-preview-content .CollapseHeader { background: #333; border: 1px solid #555; padding: 4px 8px; margin-top: 5px; cursor: pointer; user-select: none; } #live-preview-content .CollapseHeader:hover { background: #444; } #live-preview-content .CollapseHeaderText { font-weight: bold; } #live-preview-content .CollapseBlock { border: 1px solid #555; border-top: 0; padding: 8px; background: #292929; } `); const contentCell = document.getElementById('Content'); if (!contentCell) return; const previewContainer = document.createElement('td'); previewContainer.id = 'live-preview-container'; previewContainer.innerHTML = `<h2 style="margin:10px 0px 20px 0px;">Live Preview</h2><div id="live-preview-content"><div class="character-description"></div></div>`; contentCell.parentNode.insertBefore(previewContainer, contentCell.nextSibling); const descriptionTextarea = document.getElementById('CharacterEditDescription'); const previewContentDiv = document.querySelector('#live-preview-content .character-description'); const parser = createFListParser(); let previewTimeout; const updatePreview = () => { const bbcode = descriptionTextarea.value; if (!bbcode.trim()) { previewContentDiv.innerHTML = '<em>Start typing to see a preview...</em>'; return; } try { const parsedElement = parser.parseEverything(bbcode); previewContentDiv.innerHTML = ''; // Clear previous content previewContentDiv.appendChild(parsedElement); } catch (e) { console.error("F-List Live Preview: Error during parsing.", e); previewContentDiv.innerHTML = `<em style="color: red;">Error parsing BBCode. See console for details.</em>`; } }; const debouncedUpdatePreview = () => { clearTimeout(previewTimeout); previewTimeout = setTimeout(updatePreview, 300); }; descriptionTextarea.addEventListener('input', debouncedUpdatePreview); updatePreview(); // Initial preview on page load console.log("F-List Live Preview: Native parser setup complete and active."); } waitForElementAndRun(); })();