首页
随机
登录
设置
关于中医百科
免责声明
中医百科
搜索
查看“︁MediaWiki:Gadget-TextSpacing.js”︁的源代码
←
MediaWiki:Gadget-TextSpacing.js
因为以下原因,您没有权限编辑该页面:
您请求的操作仅限属于该用户组的用户执行:
用户
此页面为本wiki上的软件提供界面文本,并受到保护以防止滥用。如欲修改所有wiki的翻译,请访问
translatewiki.net
上的MediaWiki本地化项目。
您无权编辑此JavaScript页面,因为编辑此页面可能会影响所有访问者。
您可以查看和复制此页面的源代码。
/** * This gadget automatically adjust spacing between Chinese with English, * with symbols and with numbers. * * @author [[User:Diskdance]] <https://w.wiki/5F6e> * @license BSD-3-Clause * @see https://github.com/diskdance/gadget-text-spacing */ // <nowiki> (function () { 'use strict'; const pendingActions = new WeakMap(); // Optimization: lazily execute pending actions once an element is visible const observer = new IntersectionObserver(onIntersection); function onIntersection(entries) { for (const entry of entries) { if (!entry.isIntersecting) { continue; } const element = entry.target; observer.unobserve(element); const callbacks = pendingActions.get(element); if (callbacks === undefined) { continue; } while (true) { const callback = callbacks.shift(); // FIFO if (callback === undefined) { break; } callback(element); } } } function queueDomMutation(element, callback) { var _a; if (!pendingActions.has(element)) { pendingActions.set(element, []); } (_a = pendingActions.get(element)) === null || _a === void 0 ? void 0 : _a.push(callback); observer.observe(element); } function isInlineHTMLElement(node) { return (node instanceof HTMLElement && window.getComputedStyle(node).display.includes('inline')); } function isTextNode(node) { return node.nodeType === Node.TEXT_NODE; } function isVisible(element) { const style = window.getComputedStyle(element); return (style.display !== 'none' && !['hidden', 'collapse'].includes(style.visibility) && Number.parseFloat(style.opacity) > 0); } function getNodeText(node) { return node instanceof HTMLElement ? node.innerText : node.data; } /** * Split a string before an array of indexes. * * For example, * ``` * splitAtIndexes('123456789', [3, 5, 7]); * ``` * results in * ``` * ['123', '45', '67', '89'] * ``` * * Note that empty string are included: * ``` * splitAtIndexes('123456789', [0, 9]); * ``` * results in * ``` * ['', '123456789', ''] * ``` * * Indexes that are negative or greater than the length of the string are ignored. * * @param str string to split * @param indexes indexes * @returns splitted string fragments */ function splitAtIndexes(str, indexes) { const result = []; const normalizedIndexes = [ // Remove duplications and sort in ascending order ...new Set(indexes .sort((a, b) => a - b) .filter((i) => i >= 0 && i <= str.length)), str.length, ]; for (let i = 0; i < normalizedIndexes.length; i++) { const slice = str.slice(normalizedIndexes[i - 1], normalizedIndexes[i]); result.push(slice); } return result; } const REGEX_RANGE_CHINESE = '(?:[\\u2E80-\\u2E99\\u2E9B-\\u2EF3\\u2F00-\\u2FD5\\u3005\\u3007\\u3021-\\u3029\\u3038-\\u303B\\u3400-\\u4DBF\\u4E00-\\u9FFF\\uF900-\\uFA6D\\uFA70-\\uFAD9]|\\uD81B[\\uDFE2\\uDFE3\\uDFF0\\uDFF1]|[\\uD840-\\uD868\\uD86A-\\uD86C\\uD86F-\\uD872\\uD874-\\uD879\\uD880-\\uD883][\\uDC00-\\uDFFF]|\\uD869[\\uDC00-\\uDEDF\\uDF00-\\uDFFF]|\\uD86D[\\uDC00-\\uDF38\\uDF40-\\uDFFF]|\\uD86E[\\uDC00-\\uDC1D\\uDC20-\\uDFFF]|\\uD873[\\uDC00-\\uDEA1\\uDEB0-\\uDFFF]|\\uD87A[\\uDC00-\\uDFE0]|\\uD87E[\\uDC00-\\uDE1D]|\\uD884[\\uDC00-\\uDF4A])'; const REGEX_RANGE_OTHER_LEFT = '[A-Za-z0-9@~%+=|±\\)}#$¥€£₤]'; const REGEX_RANGE_OTHER_RIGHT = '[A-Za-z0-9@~%+=|±\\({#$¥€£₤]'; const REGEX_STR_INTER_SCRIPT = `(?:(${REGEX_RANGE_CHINESE})(?=${REGEX_RANGE_OTHER_RIGHT})|(${REGEX_RANGE_OTHER_LEFT})(?=${REGEX_RANGE_CHINESE}))`; const SPACE = '\u200A'; const WRAPPER_CLASS = 'gadget-space'; const SELECTOR_ALLOWED = [ 'a', 'abbr', 'article', 'aside', 'b', 'bdi', 'big', 'blockquote', 'button', 'caption', 'center', 'cite', 'data', 'dd', 'del', 'details', 'dfn', 'div', 'dt', 'em', 'figcaption', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'header', 'i', 'ins', 'label', 'legend', 'li', 'main', 'mark', 'option', 'p', 'q', 'ruby', 's', 'section', 'small', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'td', 'th', 'time', 'u', ]; const SELECTOR_BLOCKED = [ 'code', 'kbd', 'pre', 'rp', 'rt', 'samp', 'textarea', 'var', // Elements with this class are excluded '.gadget-nospace', // Editable elements '[contenteditable="true"]', // ACE editor content '.ace_editor', // Visual Editor (and 2017 Wikitext Editor) content & diff '.ve-ui-surface', '.ve-init-mw-diffPage-diff', // Diff '.diff-context', '.diff-addedline', '.diff-deletedline', // Diff (inline mode) '.mw-diff-inline-added', '.mw-diff-inline-deleted', '.mw-diff-inline-moved', '.mw-diff-inline-changed', '.mw-diff-inline-context', ]; // FIXME: Use :is() in the future once it has better browser compatibility const SELECTOR = SELECTOR_ALLOWED .map((allowed) => `${allowed}:not(${SELECTOR_BLOCKED .flatMap((blocked) => // Not include itself if it is a tag selector blocked[0].match(/[a-z]/i) ? `${blocked} *` : [blocked, `${blocked} *`]) .join(',')})`) .join(','); function getLeafElements(parent) { const candidates = parent.querySelectorAll(SELECTOR); const result = []; if (parent.matches(SELECTOR)) { result.push(parent); } for (const candidate of candidates) { for (const childNode of candidate.childNodes) { if (isTextNode(childNode)) { result.push(candidate); break; } } } return result; } function getNextVisibleSibling(node) { let currentNode = node; // Use loops rather than recursion for better performance while (true) { const candidate = currentNode.nextSibling; if (candidate === null) { const parent = currentNode.parentElement; if (parent === null) { // Parent is Document, so no visible sibling return null; } // Bubble up to its parent and get its sibling currentNode = parent; continue; } if (!(candidate instanceof HTMLElement || candidate instanceof Text)) { // Comments, SVGs, etc.: get its sibling as result currentNode = candidate; continue; } if (candidate instanceof HTMLElement) { if (!isVisible(candidate)) { // Invisible: recursively get this element's next sibling currentNode = candidate; continue; } if (!isInlineHTMLElement(candidate)) { // Next sibling is not inline (at next line), so no siblings return null; } } if (candidate instanceof Text && candidate.data.trim() === '') { // Skip empty Text nodes (e.g. line breaks) currentNode = candidate; continue; } return candidate; } } function createSpacingWrapper(str) { const span = document.createElement('span'); span.className = WRAPPER_CLASS; span.textContent = str.slice(-1); return [str.slice(0, -1), span]; } function adjustSpacing(element) { var _a; // Freeze NodeList in advance const childNodes = [...element.childNodes]; const textSpacingPosMap = new Map(); for (const child of childNodes) { if (!(child instanceof Text)) { continue; } const nextSibling = getNextVisibleSibling(child); let testString = getNodeText(child); if (nextSibling !== null) { // Append first character to detect script intersection testString += (_a = getNodeText(nextSibling)[0]) !== null && _a !== void 0 ? _a : ''; } const indexes = []; // Global regexps are stateful so do initialization in each loop const regexTextNodeData = new RegExp(REGEX_STR_INTER_SCRIPT, 'g'); while (true) { const match = regexTextNodeData.exec(testString); if (match === null) { break; } indexes.push(match.index + 1); // +1 to match script boundary } if (indexes.length === 0) { // Optimization: skip further steps // Also prevent unnecessary mutation, which will be detected by MutationObserver, // resulting in infinite loops continue; } textSpacingPosMap.set(child, indexes); } // Schedule DOM mutation to prevent forced reflows queueDomMutation(element, () => { for (const [node, indexes] of textSpacingPosMap) { const text = node.data; const fragments = splitAtIndexes(text, indexes); const replacement = fragments .slice(0, -1) .flatMap((fragment) => createSpacingWrapper(fragment)); replacement.push(fragments.slice(-1)[0]); // Optimization: prevent forced reflows requestAnimationFrame(() => { node.replaceWith(...replacement); }); } }); } function addSpaceToString(str) { const regex = new RegExp(REGEX_STR_INTER_SCRIPT, 'g'); return str.replace(regex, `$1$2${SPACE}`); } function run(element) { const leaves = getLeafElements(element); for (const leaf of leaves) { adjustSpacing(leaf); } } const mutationObserver = new MutationObserver((records) => { for (const record of records) { if (record.type !== 'childList') { continue; } const nodes = [...record.addedNodes]; // Exclude mutations caused by adjustSpacing() to prevent infinite loops // Typically they will contain nodes with class WRAPPER_CLASS if (nodes.some((node) => node instanceof HTMLElement && node.classList.contains(WRAPPER_CLASS))) { continue; } for (const node of nodes) { if (node instanceof HTMLElement) { run(node); } else if (node instanceof Text) { const { parentElement } = node; if (parentElement !== null) { run(parentElement); } } } } }); function main() { document.title = addSpaceToString(document.title); // Watch for added nodes mutationObserver.observe(document.body, { subtree: true, childList: true }); run(document.body); } $(main); })(); // </nowiki>
该页面使用的模板:
Template:Gadget Header
(
查看源代码
)
返回
MediaWiki:Gadget-TextSpacing.js
。