GreasyFork Code: Syntax Highlight by highlight.js

To syntax highlight GreasyFork Code by highlight.js

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


(() => {

    const cdn = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0';
    const resoruces = {
        'highlight.min.js': `${cdn}/highlight.min.js`,
        'javascript.min.js': `${cdn}/languages/javascript.min.js`,
        'css.min.js': `${cdn}/languages/css.min.js`,
        'stylus.min.js': `${cdn}/languages/stylus.min.js`,
        'nnfx-light.css': `${cdn}/styles/nnfx-light.min.css`,
        'nnfx-dark.css': `${cdn}/styles/nnfx-dark.min.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);
        }

    `;


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

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

        .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;
        }

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

    `;

    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;




        codeElement.classList.add(className);
        codeElement.classList.add('syntax-highlighted')

        // Highlighting
        // hljs.highlightElement(code);

        let htmlCode = '';

        if (className === 'language-javascript') {

            const result = hljs.highlight(code.textContent, {language: 'javascript', ignoreIllegals: true});
            htmlCode = result.value;

        } else if (className === 'language-stylus') {

            const result = hljs.highlight(code.textContent, {language: 'stylus', ignoreIllegals: true});
            htmlCode = result.value;

        } else if (className === 'language-less') {

            const result = hljs.highlight(code.textContent, {language: 'less', ignoreIllegals: true});
            htmlCode = result.value;

        } else {

            const result = hljs.highlight(code.textContent, {language: 'css', ignoreIllegals: true});
            htmlCode = result.value;

        }



        // Adding line numbers
        htmlCode = htmlCode || '';
        const htmlSplit = htmlCode ? htmlCode.split('\n') : [];
        const totalLines = htmlSplit.length;

        let mwStyle = '';

        if (totalLines >= 1) {
            code.classList.add('hljs-add-marker-width');
            const len = `${totalLines}`.length * 0.5;
            mwStyle = `${len}em`;
            htmlCode = htmlSplit.map((n, i) => `<span class="marker marker-fixed-width" marker-text="${i + 1}"></span>${n}`).join('\n');

        } else {

            code.classList.remove('hljs-add-marker-width');
        }

        let targetCode = code;

        if (htmlCode) {

            const template = document.createElement('template');

            template.innerHTML = `<code>${htmlCode}</code>`;
            const code2 = template.content.firstChild;
            for (const className of code.classList) {
                code2.classList.add(className);
            }

            targetCode = code2;
        } else {
            code.innerHTML = "";
            targetCode = code;
        }

        targetCode.style.setProperty('--hljs-marker-width', mwStyle);

        parentNode.insertBefore(targetCode, nextNode);



    }

    const documentBodyHeadReady = onBodyHeadReadyAsync();

    documentBodyHeadReady.then(async () => {

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

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


        await loadJS(resoruces['highlight.min.js']);
        await loadJS(resoruces['javascript.min.js']);
        await loadJS(resoruces['css.min.js']);
        await loadJS(resoruces['stylus.min.js']);

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

            loadCSS(resoruces['nnfx-dark.css']);
        } else {

            loadCSS(resoruces['nnfx-light.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;

    });

    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);


})();