// ==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/codemirror@5.65.16';
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 });
})();