GreasyFork Code: Syntax Highlight by CodeMirror

To syntax highlight GreasyFork Code by CodeMirror

// ==UserScript==
// @name         GreasyFork Code: Syntax Highlight by CodeMirror
// @namespace    Violentmonkey Scripts
// @grant        none
// @version      0.4.10
// @author       CY Fung
// @description  To syntax highlight GreasyFork Code by CodeMirror
// @run-at       document-start
// @inject-into  page
// @unwrap
// @license      MIT
// @match        https://greasyfork.org/*
// @match        https://sleazyfork.org/*
//
// ==/UserScript==


(() => {

    const cdn = 'https://cdn.jsdelivr.net/npm/[email protected]';
    const resoruces = {
        'codemirror.min.js': `${cdn}/lib/codemirror.min.js`,
        'javascript.min.js': `${cdn}/mode/javascript/javascript.min.js`,
        'css.min.js': `${cdn}/mode/css/css.min.js`,
        'stylus.min.js': `${cdn}/mode/stylus/stylus.min.js`,
        'active-line.min.js': `${cdn}/addon/selection/active-line.min.js`,
        'search.js': `${cdn}/addon/search/search.js`,
        'searchcursor.js': `${cdn}/addon/search/searchcursor.js`,
        'jump-to-line.js': `${cdn}/addon/search/jump-to-line.js`,
        'dialog.js': `${cdn}/addon/dialog/dialog.js`,
        'codemirror.min.css': `${cdn}/lib/codemirror.min.css`,
        'dialog.css': `${cdn}/addon/dialog/dialog.css`,
        'material.css': `${cdn}/theme/material.css`,
    }

    const doActionCSS = () => `

        .code-container{
            height:100vh;
        }
        .code-container .CodeMirror, .code-container textarea{
            height:100%;
        }
    `;


    const global_css = () => `

        html {
            line-height: 1.5;
            -webkit-text-size-adjust: 100%;
            -moz-tab-size: 4;
            -o-tab-size: 4;
            tab-size: 4;
            font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
            font-feature-settings: normal;
            font-variation-settings: normal
        }

        .code-container code, .code-container kbd, .code-container pre, .code-container samp {
            font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
            font-size: 1em
        }

        #script-content > .code-container[class] {
            width: 100%;
        }

        .code-container[class] {
            border-radius: 0;
        }

        .code-container[class] {
            border-radius: 0;
        }

        .code-container > pre:only-child{
            padding:0;
        }

        code.syntax-highlighted[class] {
            font-family: monospace;
            font-size: 13px;
            font-variant-ligatures: contextual;
            line-height: 1.15rem;
            text-shadow: none !important;
        }

        .hljs-comment[class], .hljs-quote[class] {
            font-style: inherit;
            color: #259789;
        }

        .hljs-add-marker-width .marker-fixed-width[class] {
            user-select: none !important;
            width: calc(var(--hljs-marker-width, 0em) + 16px);
            background: #f4f4f4;
            padding-right: 6px;
            margin-right: 4px;
            contain: paint style;
        }

        [dark] .hljs-add-marker-width .marker-fixed-width[class] {
            background: #242424;
            color: #b6b2b2;
        }

        .marker-fixed-width[marker-text]::before {
            content: attr(marker-text);
        }


        @keyframes cmLineNumberAppear {
            from {
                background-position-x: 3px;
            }
            to {
                background-position-x: 4px;
            }
        }

        .CodeMirror-linenumber:not(:empty){
            animation: cmLineNumberAppear 1ms linear 0s 1 normal forwards;
        }

        .CodeMirror-linenumber[marker-text]::before {
            content: attr(marker-text);
        }


    `;


    const cssForCodePage = () => /\/scripts\/\d+[^\s\/\\]*\/code(\/|$)/.test(location.href) ? `

        html:not([dkkfv]) div.code-container {
            visibility: collapse;
        }

        .code-container,
        .code-container pre:only-child,
        .code-container pre:only-child code:only-child {
            max-height: calc(100vh + 4px);
            max-width: calc(100vw + 4px);
        }
    ` : '';


    const cssAdd = () => `

        ${global_css()}

        ${cssForCodePage()}

        .code-container {
            max-width: 100%;
            display: inline-flex;
            flex-direction: column;
            overflow: auto;
            border-radius: 8px;
            max-height: 100%;
            overflow: visible;
        }
        .code-container > pre:only-child {
            max-width: 100%;
            display: inline-flex;
            flex-direction: column;
            flex-grow: 1;
            height: 0;
        }
        .code-container > pre:only-child > code:only-child {
            max-width: 100%;
            flex-grow: 1;
            height: 0;
        }
        .code-container pre code {
            padding: 0;
            font-family: Consolas;
            cursor: text;
            overflow: auto;
            box-sizing: border-box;
        }
        .code-container pre code .marker {
            display: inline-block;
            color: #636d83;
            text-align: right;
            padding-right: 20px;
            user-select: none;
            cursor: auto;
        }

        .code-container[contenteditable]{
          outline: 0 !important;
          contain: strict;
          box-sizing: border-box;
          overflow: hidden;
        }

        .code-container[contenteditable]>pre[contenteditable="false"]{
          contain: strict;
          width: initial;
          box-sizing: border-box;
          overflow: hidden;
        }

        html {
            --token-color-comment: #259789;
            --cm-dialog-background-color: #fefefe;
            --cm-linenumber-background-color: #f4f4f4;
            --cm-linenumber-text-color: #636d83;
            --cm-search-color: #ffeb3ab8;
        }

        [dark] {
            --token-color-comment:#59c6b9;
            --cm-dialog-background-color: #25262d;
            --cm-linenumber-background-color: #242424;
            --cm-linenumber-text-color: #b6b2b2;
            --cm-search-color: #6068bbb8;
        }


        .CodeMirror .cm-comment[class] {
            color: var(--token-color-comment);
        }


        html .CodeMirror .CodeMirror-linenumber[class] {
            background: var(--cm-linenumber-background-color);
            color: var(--cm-linenumber-text-color);
            padding-right: 6px;
            margin-right: 4px;
            contain: paint style;
            user-select: none !important;
        }

        html .CodeMirror[class] {
            background-color: inherit;
        }

        .CodeMirror[class] .cm-searching {
            background-color: var(--cm-search-color);
        }

        .CodeMirror[class] .CodeMirror-dialog[class] {
            background-color: var(--cm-dialog-background-color);
        }

        html .CodeMirror .CodeMirror-activeline-background[class] {
            background: inherit;
            background-color: #8fd9da17;
        }

        div.code-container .CodeMirror .CodeMirror-lines {
            padding: 0;
        }

        div.code-container .CodeMirror {
            font-family: monospace;
            font-size: 13px;
            font-variant-ligatures: contextual;
            line-height: 1.15rem;
            text-shadow: none !important;
        }


    `;

    const Promise = (async function () { })().constructor;

    const delayPn = delay => new Promise((fn => setTimeout(fn, delay)));

    const PromiseExternal = ((resolve_, reject_) => {
        const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
        return class PromiseExternal extends Promise {
            constructor(cb = h) {
                super(cb);
                if (cb === h) {
                    /** @type {(value: any) => void} */
                    this.resolve = resolve_;
                    /** @type {(reason?: any) => void} */
                    this.reject = reject_;
                }
            }
        };
    })();

    // -------- fix requestIdleCallback issue for long coding --------

    const pud = new PromiseExternal();
    if (typeof window.requestIdleCallback === 'function' && !window.requestIdleCallback842 && window.requestIdleCallback.length === 1) {
        window.requestIdleCallback842 = window.requestIdleCallback;
        window.requestIdleCallback = function (callback, ...args) {
            return (this || window).requestIdleCallback842(async function () {
                await pud.then();
                return callback.apply(this, arguments);
            }, ...args);
        }
    }

    // -------- fix requestIdleCallback issue for long coding --------

    const pScript = new PromiseExternal();
    const pElementQuery = new PromiseExternal();

    HTMLElement.prototype.getElementsByTagName331 = HTMLElement.prototype.getElementsByTagName;
    Document.prototype.getElementsByTagName331 = Document.prototype.getElementsByTagName;

    HTMLElement.prototype.getElementsByTagName = getElementsByTagName;
    Document.prototype.getElementsByTagName = getElementsByTagName;

    let byPass = true;

    const observablePromise = (proc, timeoutPromise) => {
        let promise = null;
        return {
            obtain() {
                if (!promise) {
                    promise = new Promise(resolve => {
                        let mo = null;
                        const f = () => {
                            let t = proc();
                            if (t) {
                                mo.disconnect();
                                mo.takeRecords();
                                mo = null;
                                resolve(t);
                            }
                        }
                        mo = new MutationObserver(f);
                        mo.observe(document, { subtree: true, childList: true })
                        f();
                        timeoutPromise && timeoutPromise.then(() => {
                            resolve(null)
                        });
                    });
                }
                return promise
            }
        }
    }

    const documentReady = new Promise(resolve => {
        Promise.resolve().then(() => {
            if (document.readyState !== 'loading') {
                resolve();
            } else {
                window.addEventListener("DOMContentLoaded", resolve, false);
            }
        });
    });

    documentReady.then(async () => {
        pud.resolve();
    });

    function getElementsByTagName(tag) {
        if (byPass) {
            if (tag === 'pre' || tag === 'code' || tag === 'xmp') {
                if (location.pathname.endsWith('/code')) {
                    pElementQuery.resolve();
                    return [];
                }
            }
        }
        return this.getElementsByTagName331(tag);
    }

    async function onBodyHeadReadyAsync() {
        await observablePromise(() => document.body && document.head).obtain();
    }


    // Load CSS
    function loadJS(href) {

        return new Promise(resolve => {

            const script = document.createElement('script');
            script.src = href;
            script.onload = () => {
                resolve(script);
            };
            document.head.appendChild(script);

        });

    }

    // Load CSS
    function loadCSS(href) {
        const link = document.createElement('link');
        link.rel = 'stylesheet';
        link.href = href;
        document.head.appendChild(link);
        return link;
    }






    /** @param {HTMLElement} pre */
    async function prepareCodeAreaAsync(pre) {

        if (pre.isConnected === false) return;

        for (const li of pre.querySelectorAll('li')) {
            li.append(document.createTextNode('\n'));
        }

        const codeElement = document.createElement('code');
        // codeElement.classList.add('language-javascript');
        codeElement.innerHTML = pre.innerHTML;

        // Clearing the original code container and appending the new one
        // pre.classList = '';
        pre.innerHTML = '';
        // pre.appendChild(codeElement);

        // if (pre.querySelector('code')) return;
        const code = codeElement;

        const codeContainer = pre.closest('.code-container');
        if (codeContainer && codeContainer.querySelector('.code-container>pre:only-child')) {
            // avoid selection to the outside by mouse dragging
            codeContainer.setAttribute('contenteditable', '');
            codeContainer.querySelector('.code-container>pre:only-child').setAttribute('contenteditable', 'false');
        }


        // let parentNode = code.parentNode;
        // let nextNode = code.nextSibling;

        // code.remove();
        let parentNode = pre;
        let nextNode = null;
        await Promise.resolve().then();

        // preset language
        /*
        const text = codeElement.textContent;
        if(/(^|\n)\s*\/\/\s+==UserScript==\s*\n/.test(text)){
            codeElement.classList.add('language-javascript');
        }else if(/(^|\n)\s*\/\*\s+==UserStyle==\s*\n/.test(text)){
            codeElement.classList.add('language-css');
        }
        */


        let preLang = '';

        if (pre.classList.contains('lang-js')) {
            preLang = 'lang-js';
        } else if (pre.classList.contains('lang-css')) {
            preLang = 'lang-css';
        } else if (pre.classList.contains('uglyprint')){
            let m =/\/\/\s*={2,9}(\w+)={2,9}\s*[\r\n]/.exec(codeElement.textContent);
            if(m){
                m = m[1];
                if(m === 'UserScript') preLang = 'lang-js';
                if(m === 'UserStyle') preLang = 'lang-css';
            }
        }

        let className = '';
        if (preLang === 'lang-js') {
            className = 'language-javascript';
        } else if (preLang === 'lang-css') {

            const text = codeElement.textContent;
            let m = /\n@preprocessor\s+([-_a-zA-Z]{3,8})\s*\n/.exec(text);
            className = 'language-css'
            if (m) {
                const preprocessor = m[1];
                if (preprocessor === 'stylus') {
                    className = 'language-stylus';
                } else if (preprocessor === 'uso') {
                    className = 'language-stylus';
                } else if (preprocessor === 'less') {
                    className = 'language-less';
                } else if (preprocessor === 'default') {
                    className = 'language-stylus';
                } else {
                    className = 'language-stylus';
                }
            }


        }


        if (!className) return;

        let mode = '';
        if (className === 'language-javascript') mode = 'javascript';
        if (className === 'language-stylus') mode = 'stylus';
        if (className === 'language-less') mode = 'less';
        if (className === 'language-css') mode = 'css';

        if (!mode) return;



        let textarea = document.createElement('textarea');
        textarea.value = `${code.textContent}`;
        textarea.readOnly = true;
        textarea.id = 'editor651';


        // textarea.classList.add('code-container')

        textarea.style.width = '100%';
        textarea.style.height = '100vh';

        parentNode.insertBefore(textarea, nextNode);


        const editor651 = CodeMirror.fromTextArea(document.querySelector('#editor651'), {

            mode: mode,
            theme: document.documentElement.hasAttribute('dark') ? 'material' : 'default',

            readOnly: true,
            styleActiveLine: true,
            lineNumbers: true,
            extraKeys: { "Alt-F": "findPersistent" }
        });
        editor651.save();
        function refresh() {
            try {
                editor651.display.input.cm.refresh();
            } catch (e) {
            }
        }
        document.documentElement.addEventListener('cm-highlight-refresh', function () {
            setTimeout(refresh, 100);
            requestAnimationFrame(refresh);
            refresh();
        });

    }

    const documentBodyHeadReady = onBodyHeadReadyAsync();

    documentBodyHeadReady.then(async () => {

        if (!location.pathname.endsWith('/code')) {
            return;
        }

        document.head.appendChild(document.createElement('style')).textContent = `${cssAdd()}`;

        await loadJS(resoruces['codemirror.min.js']);

        await Promise.all([
            loadJS(resoruces['javascript.min.js']),
            loadJS(resoruces['css.min.js']),
            loadJS(resoruces['stylus.min.js']),
            loadJS(resoruces['active-line.min.js']),
            loadJS(resoruces['search.js']),
            loadJS(resoruces['searchcursor.js']),
            loadJS(resoruces['jump-to-line.js']),
            loadJS(resoruces['dialog.js'])
        ]);

        if (document.documentElement.hasAttribute('dark')) {

            // TBC
            loadCSS(resoruces['codemirror.min.css']);
            loadCSS(resoruces['dialog.css']);
            loadCSS(resoruces['material.css']);

        } else {

            loadCSS(resoruces['codemirror.min.css']);
            loadCSS(resoruces['dialog.css']);
        }


        pScript.resolve();




    });

    let keydownActive = false;

    documentReady.then(async () => {

        if (!location.pathname.endsWith('/code')) {
            byPass = false;
            return;
        }

        await pScript.then();

        await Promise.race([pElementQuery, delayPn(800)]);

        const targets = document.querySelectorAll('.code-container pre.lang-js, .code-container pre.lang-css, .code-container pre.uglyprint');

        if (targets.length === 0) return;

        await delayPn(40);

        document.head.appendChild(document.createElement('style')).textContent = doActionCSS();

        await delayPn(40);

        byPass = false;

        // Code highlighting
        const promises = [...targets].map(prepareCodeAreaAsync)
        await Promise.all(promises);

        await delayPn(40);
        document.documentElement.setAttribute('dkkfv', '');
        keydownActive = true;
        document.documentElement.dispatchEvent(new CustomEvent('cm-highlight-refresh'));

    });

    function selectAllWithinElement(element) {
        window.getSelection().removeAllRanges();
        let range = document.createRange();
        if (element) {
            range.selectNodeContents(element);
            window.getSelection().addRange(range);
        } else {
            console.error('Element not found with ID:', element);
        }
    }
    document.addEventListener('keydown', (e) => {
        if (keydownActive && e && e.code === 'KeyA' && e.isTrusted && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {

            const target = e.target;
            const container = target ? target.closest('div.code-container') : null;
            const code = container ? container.querySelector('code') : null;

            if (container && code) {

                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();

                setTimeout(() => {
                    selectAllWithinElement(code);
                }, 1)

            }

        }
    }, true);


    const cmLineNumberAppearFn = (evt) => {
        const elm = evt.target;
        if (!(elm instanceof HTMLElement)) return;

        elm.setAttribute('marker-text', elm.textContent.trim());
        elm.textContent = '';
    }

    document.addEventListener('animationstart', (evt) => {
        const animationName = evt.animationName;
        if (!animationName) return;
        if (animationName === 'cmLineNumberAppear') cmLineNumberAppearFn(evt);
    }, { capture: true, passive: true });


})();