Universal Text Formatter

Apply Bold and Italic styles to any text field.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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 <p> or <br>
        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);
})();