// ==UserScript== // @name Universal Text Formatter // @namespace https://github.com/code-loyko/ // @version 1.2 // @description Apply Bold and Italic styles to any text field. // @author Loyko // @match *://*/* // @license GPL-3.0-or-later // @grant none // ==/UserScript== (function() { 'use strict'; // Unicode offsets for Sans-Serif Mathematical Alphanumeric Symbols // Style Index: [0: Normal, 1: Bold, 2: Italic, 3: Bold-Italic] const UNICODE_STYLE_OFFSETS = { UPPERCASE: [0, 120211, 120263, 120315], // A-Z LOWERCASE: [0, 120205, 120257, 120309], // a-z DIGIT: [0, 120764, 0, 120764] // 0-9 (No italics available for digits) }; /** * Identifies character base, category, and current style bitmask (0-3). */ function getCharacterMetadata(charCodePoint) { for (let styleBitmask = 3; styleBitmask >= 1; styleBitmask--) { for (const [charCategory, categoryOffsets] of Object.entries(UNICODE_STYLE_OFFSETS)) { const baseCodePoint = charCodePoint - categoryOffsets[styleBitmask]; const isMatchingCategory = (charCategory === 'UPPERCASE' && baseCodePoint >= 65 && baseCodePoint <= 90) || (charCategory === 'LOWERCASE' && baseCodePoint >= 97 && baseCodePoint <= 122) || (charCategory === 'DIGIT' && baseCodePoint >= 48 && baseCodePoint <= 57); if (isMatchingCategory) return { baseCodePoint, charCategory, currentStyle: styleBitmask }; } } // Standard ASCII Fallback if (charCodePoint >= 65 && charCodePoint <= 90) return { baseCodePoint: charCodePoint, charCategory: 'UPPERCASE', currentStyle: 0 }; if (charCodePoint >= 97 && charCodePoint <= 122) return { baseCodePoint: charCodePoint, charCategory: 'LOWERCASE', currentStyle: 0 }; if (charCodePoint >= 48 && charCodePoint <= 57) return { baseCodePoint: charCodePoint, charCategory: 'DIGIT', currentStyle: 0 }; return null; // Accents, punctuation, etc. } /** * Returns a formatter function based on the global toggle state of the selected text. * Behavior matches standard text editors: if any selected char lacks the style, apply to all. */ function prepareTextFormatter(concatenatedSelectedText, requestedStyleBitmask) { const characterCodePoints = [...concatenatedSelectedText]; const metadataListForSelection = characterCodePoints.map(char => getCharacterMetadata(char.codePointAt(0))); const isApplyingStyle = metadataListForSelection.some(metadata => metadata && (metadata.currentStyle & requestedStyleBitmask) === 0); return (textSegmentToFormat) => { return [...textSegmentToFormat].map(char => { const metadata = getCharacterMetadata(char.codePointAt(0)); if (!metadata || (metadata.charCategory === 'DIGIT' && requestedStyleBitmask === 2)) return char; const updatedStyleBitmask = isApplyingStyle ? (metadata.currentStyle | requestedStyleBitmask) : (metadata.currentStyle & ~requestedStyleBitmask); return String.fromCodePoint(metadata.baseCodePoint + UNICODE_STYLE_OFFSETS[metadata.charCategory][updatedStyleBitmask]); }).join(''); }; } window.addEventListener('keydown', (keyboardEvent) => { const isModifierKeyPressed = keyboardEvent.ctrlKey || keyboardEvent.metaKey; const requestedStyleBitmask = (keyboardEvent.key.toLowerCase() === 'b') ? 1 : (keyboardEvent.key.toLowerCase() === 'i') ? 2 : 0; if (!isModifierKeyPressed || !requestedStyleBitmask) return; const eventTargetElement = keyboardEvent.target; const isStandardInputOrTextarea = eventTargetElement.tagName === 'TEXTAREA' || eventTargetElement.tagName === 'INPUT'; // --- STANDARD INPUTS (Textareas) --- if (isStandardInputOrTextarea) { const selectionStartIndex = eventTargetElement.selectionStart; const selectionEndIndex = eventTargetElement.selectionEnd; if (selectionStartIndex === selectionEndIndex) return; keyboardEvent.preventDefault(); keyboardEvent.stopImmediatePropagation(); const selectedText = eventTargetElement.value.substring(selectionStartIndex, selectionEndIndex); const formatSegment = prepareTextFormatter(selectedText, requestedStyleBitmask); const finalFormattedText = formatSegment(selectedText); // Textareas support execCommand for Undo/Redo history document.execCommand('insertText', false, finalFormattedText); eventTargetElement.setSelectionRange(selectionStartIndex, selectionStartIndex + finalFormattedText.length); return; } // --- RICH TEXT EDITORS (LinkedIn, Facebook, etc.) --- const currentWindowSelection = window.getSelection(); if (!currentWindowSelection.rangeCount || currentWindowSelection.isCollapsed) return; const activeSelectionRange = currentWindowSelection.getRangeAt(0); const selectionAncestorContainer = activeSelectionRange.commonAncestorContainer.nodeType === Node.TEXT_NODE ? activeSelectionRange.commonAncestorContainer.parentNode : activeSelectionRange.commonAncestorContainer; // TreeWalker isolates strictly the text nodes to prevent destroying HTML structures like

or
const textNodeWalker = document.createTreeWalker(selectionAncestorContainer, NodeFilter.SHOW_TEXT); const textNodesInSelection = []; let traversedTextNode; while ((traversedTextNode = textNodeWalker.nextNode())) { if (activeSelectionRange.intersectsNode(traversedTextNode)) { const traversedNodeRange = document.createRange(); traversedNodeRange.selectNodeContents(traversedTextNode); const clampedSelectionRange = activeSelectionRange.cloneRange(); // Clamp the intersection specifically to the boundaries of the current node if (clampedSelectionRange.compareBoundaryPoints(Range.START_TO_START, traversedNodeRange) < 0) { clampedSelectionRange.setStart(traversedNodeRange.startContainer, traversedNodeRange.startOffset); } if (clampedSelectionRange.compareBoundaryPoints(Range.END_TO_END, traversedNodeRange) > 0) { clampedSelectionRange.setEnd(traversedNodeRange.endContainer, traversedNodeRange.endOffset); } if (!clampedSelectionRange.collapsed) { textNodesInSelection.push({ targetTextNode: traversedTextNode, nodeSelectionStart: clampedSelectionRange.startOffset, nodeSelectionEnd: clampedSelectionRange.endOffset }); } } } if (!textNodesInSelection.length) return; keyboardEvent.preventDefault(); keyboardEvent.stopImmediatePropagation(); // Evaluate global toggle format based on all text nodes involved const concatenatedSelectedText = textNodesInSelection.map(info => info.targetTextNode.nodeValue.substring(info.nodeSelectionStart, info.nodeSelectionEnd)).join(''); const formatSegment = prepareTextFormatter(concatenatedSelectedText, requestedStyleBitmask); let rangeStartNode = textNodesInSelection[0].targetTextNode; let rangeStartOffset = textNodesInSelection[0].nodeSelectionStart; let rangeEndNode = textNodesInSelection[textNodesInSelection.length - 1].targetTextNode; let rangeEndOffset = textNodesInSelection[textNodesInSelection.length - 1].nodeSelectionEnd; // DOM mutation textNodesInSelection.forEach((nodeSelectionInfo, index) => { const { targetTextNode, nodeSelectionStart, nodeSelectionEnd } = nodeSelectionInfo; const originalNodeContent = targetTextNode.nodeValue; const textToFormat = originalNodeContent.substring(nodeSelectionStart, nodeSelectionEnd); const formattedTextResult = formatSegment(textToFormat); targetTextNode.nodeValue = originalNodeContent.substring(0, nodeSelectionStart) + formattedTextResult + originalNodeContent.substring(nodeSelectionEnd); // Sync the cursor position if multi-unit characters (like emojis) if (index === textNodesInSelection.length - 1) { rangeEndOffset += (formattedTextResult.length - textToFormat.length); } }); // Maintain partial history compatibility eventTargetElement.dispatchEvent(new InputEvent('input', { inputType: 'insertReplacementText', bubbles: true, cancelable: true })); // Restore exact selection currentWindowSelection.removeAllRanges(); const newSelectionRange = document.createRange(); newSelectionRange.setStart(rangeStartNode, rangeStartOffset); newSelectionRange.setEnd(rangeEndNode, rangeEndOffset); currentWindowSelection.addRange(newSelectionRange); }, true); })();