GreasyFork Code: Syntax Highlight by CodeMirror

To syntax highlight GreasyFork Code by CodeMirror

  1. // ==UserScript==
  2. // @name GreasyFork Code: Syntax Highlight by CodeMirror
  3. // @namespace Violentmonkey Scripts
  4. // @grant none
  5. // @version 0.4.10
  6. // @author CY Fung
  7. // @description To syntax highlight GreasyFork Code by CodeMirror
  8. // @run-at document-start
  9. // @inject-into page
  10. // @unwrap
  11. // @license MIT
  12. // @match https://greatest.deepsurf.us/*
  13. // @match https://sleazyfork.org/*
  14. //
  15. // ==/UserScript==
  16.  
  17.  
  18. (() => {
  19.  
  20. const cdn = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16';
  21. const resoruces = {
  22. 'codemirror.min.js': `${cdn}/lib/codemirror.min.js`,
  23. 'javascript.min.js': `${cdn}/mode/javascript/javascript.min.js`,
  24. 'css.min.js': `${cdn}/mode/css/css.min.js`,
  25. 'stylus.min.js': `${cdn}/mode/stylus/stylus.min.js`,
  26. 'active-line.min.js': `${cdn}/addon/selection/active-line.min.js`,
  27. 'search.js': `${cdn}/addon/search/search.js`,
  28. 'searchcursor.js': `${cdn}/addon/search/searchcursor.js`,
  29. 'jump-to-line.js': `${cdn}/addon/search/jump-to-line.js`,
  30. 'dialog.js': `${cdn}/addon/dialog/dialog.js`,
  31. 'codemirror.min.css': `${cdn}/lib/codemirror.min.css`,
  32. 'dialog.css': `${cdn}/addon/dialog/dialog.css`,
  33. 'material.css': `${cdn}/theme/material.css`,
  34. }
  35.  
  36. const doActionCSS = () => `
  37.  
  38. .code-container{
  39. height:100vh;
  40. }
  41. .code-container .CodeMirror, .code-container textarea{
  42. height:100%;
  43. }
  44. `;
  45.  
  46.  
  47. const global_css = () => `
  48.  
  49. html {
  50. line-height: 1.5;
  51. -webkit-text-size-adjust: 100%;
  52. -moz-tab-size: 4;
  53. -o-tab-size: 4;
  54. tab-size: 4;
  55. 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;
  56. font-feature-settings: normal;
  57. font-variation-settings: normal
  58. }
  59.  
  60. .code-container code, .code-container kbd, .code-container pre, .code-container samp {
  61. font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
  62. font-size: 1em
  63. }
  64.  
  65. #script-content > .code-container[class] {
  66. width: 100%;
  67. }
  68.  
  69. .code-container[class] {
  70. border-radius: 0;
  71. }
  72.  
  73. .code-container[class] {
  74. border-radius: 0;
  75. }
  76.  
  77. .code-container > pre:only-child{
  78. padding:0;
  79. }
  80.  
  81. code.syntax-highlighted[class] {
  82. font-family: monospace;
  83. font-size: 13px;
  84. font-variant-ligatures: contextual;
  85. line-height: 1.15rem;
  86. text-shadow: none !important;
  87. }
  88.  
  89. .hljs-comment[class], .hljs-quote[class] {
  90. font-style: inherit;
  91. color: #259789;
  92. }
  93.  
  94. .hljs-add-marker-width .marker-fixed-width[class] {
  95. user-select: none !important;
  96. width: calc(var(--hljs-marker-width, 0em) + 16px);
  97. background: #f4f4f4;
  98. padding-right: 6px;
  99. margin-right: 4px;
  100. contain: paint style;
  101. }
  102.  
  103. [dark] .hljs-add-marker-width .marker-fixed-width[class] {
  104. background: #242424;
  105. color: #b6b2b2;
  106. }
  107.  
  108. .marker-fixed-width[marker-text]::before {
  109. content: attr(marker-text);
  110. }
  111.  
  112.  
  113. @keyframes cmLineNumberAppear {
  114. from {
  115. background-position-x: 3px;
  116. }
  117. to {
  118. background-position-x: 4px;
  119. }
  120. }
  121.  
  122. .CodeMirror-linenumber:not(:empty){
  123. animation: cmLineNumberAppear 1ms linear 0s 1 normal forwards;
  124. }
  125.  
  126. .CodeMirror-linenumber[marker-text]::before {
  127. content: attr(marker-text);
  128. }
  129.  
  130.  
  131. `;
  132.  
  133.  
  134. const cssForCodePage = () => /\/scripts\/\d+[^\s\/\\]*\/code(\/|$)/.test(location.href) ? `
  135.  
  136. html:not([dkkfv]) div.code-container {
  137. visibility: collapse;
  138. }
  139.  
  140. .code-container,
  141. .code-container pre:only-child,
  142. .code-container pre:only-child code:only-child {
  143. max-height: calc(100vh + 4px);
  144. max-width: calc(100vw + 4px);
  145. }
  146. ` : '';
  147.  
  148.  
  149. const cssAdd = () => `
  150.  
  151. ${global_css()}
  152.  
  153. ${cssForCodePage()}
  154.  
  155. .code-container {
  156. max-width: 100%;
  157. display: inline-flex;
  158. flex-direction: column;
  159. overflow: auto;
  160. border-radius: 8px;
  161. max-height: 100%;
  162. overflow: visible;
  163. }
  164. .code-container > pre:only-child {
  165. max-width: 100%;
  166. display: inline-flex;
  167. flex-direction: column;
  168. flex-grow: 1;
  169. height: 0;
  170. }
  171. .code-container > pre:only-child > code:only-child {
  172. max-width: 100%;
  173. flex-grow: 1;
  174. height: 0;
  175. }
  176. .code-container pre code {
  177. padding: 0;
  178. font-family: Consolas;
  179. cursor: text;
  180. overflow: auto;
  181. box-sizing: border-box;
  182. }
  183. .code-container pre code .marker {
  184. display: inline-block;
  185. color: #636d83;
  186. text-align: right;
  187. padding-right: 20px;
  188. user-select: none;
  189. cursor: auto;
  190. }
  191.  
  192. .code-container[contenteditable]{
  193. outline: 0 !important;
  194. contain: strict;
  195. box-sizing: border-box;
  196. overflow: hidden;
  197. }
  198.  
  199. .code-container[contenteditable]>pre[contenteditable="false"]{
  200. contain: strict;
  201. width: initial;
  202. box-sizing: border-box;
  203. overflow: hidden;
  204. }
  205.  
  206. html {
  207. --token-color-comment: #259789;
  208. --cm-dialog-background-color: #fefefe;
  209. --cm-linenumber-background-color: #f4f4f4;
  210. --cm-linenumber-text-color: #636d83;
  211. --cm-search-color: #ffeb3ab8;
  212. }
  213.  
  214. [dark] {
  215. --token-color-comment:#59c6b9;
  216. --cm-dialog-background-color: #25262d;
  217. --cm-linenumber-background-color: #242424;
  218. --cm-linenumber-text-color: #b6b2b2;
  219. --cm-search-color: #6068bbb8;
  220. }
  221.  
  222.  
  223. .CodeMirror .cm-comment[class] {
  224. color: var(--token-color-comment);
  225. }
  226.  
  227.  
  228. html .CodeMirror .CodeMirror-linenumber[class] {
  229. background: var(--cm-linenumber-background-color);
  230. color: var(--cm-linenumber-text-color);
  231. padding-right: 6px;
  232. margin-right: 4px;
  233. contain: paint style;
  234. user-select: none !important;
  235. }
  236.  
  237. html .CodeMirror[class] {
  238. background-color: inherit;
  239. }
  240.  
  241. .CodeMirror[class] .cm-searching {
  242. background-color: var(--cm-search-color);
  243. }
  244.  
  245. .CodeMirror[class] .CodeMirror-dialog[class] {
  246. background-color: var(--cm-dialog-background-color);
  247. }
  248.  
  249. html .CodeMirror .CodeMirror-activeline-background[class] {
  250. background: inherit;
  251. background-color: #8fd9da17;
  252. }
  253.  
  254. div.code-container .CodeMirror .CodeMirror-lines {
  255. padding: 0;
  256. }
  257.  
  258. div.code-container .CodeMirror {
  259. font-family: monospace;
  260. font-size: 13px;
  261. font-variant-ligatures: contextual;
  262. line-height: 1.15rem;
  263. text-shadow: none !important;
  264. }
  265.  
  266.  
  267. `;
  268.  
  269. const Promise = (async function () { })().constructor;
  270.  
  271. const delayPn = delay => new Promise((fn => setTimeout(fn, delay)));
  272.  
  273. const PromiseExternal = ((resolve_, reject_) => {
  274. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  275. return class PromiseExternal extends Promise {
  276. constructor(cb = h) {
  277. super(cb);
  278. if (cb === h) {
  279. /** @type {(value: any) => void} */
  280. this.resolve = resolve_;
  281. /** @type {(reason?: any) => void} */
  282. this.reject = reject_;
  283. }
  284. }
  285. };
  286. })();
  287.  
  288. // -------- fix requestIdleCallback issue for long coding --------
  289.  
  290. const pud = new PromiseExternal();
  291. if (typeof window.requestIdleCallback === 'function' && !window.requestIdleCallback842 && window.requestIdleCallback.length === 1) {
  292. window.requestIdleCallback842 = window.requestIdleCallback;
  293. window.requestIdleCallback = function (callback, ...args) {
  294. return (this || window).requestIdleCallback842(async function () {
  295. await pud.then();
  296. return callback.apply(this, arguments);
  297. }, ...args);
  298. }
  299. }
  300.  
  301. // -------- fix requestIdleCallback issue for long coding --------
  302.  
  303. const pScript = new PromiseExternal();
  304. const pElementQuery = new PromiseExternal();
  305.  
  306. HTMLElement.prototype.getElementsByTagName331 = HTMLElement.prototype.getElementsByTagName;
  307. Document.prototype.getElementsByTagName331 = Document.prototype.getElementsByTagName;
  308.  
  309. HTMLElement.prototype.getElementsByTagName = getElementsByTagName;
  310. Document.prototype.getElementsByTagName = getElementsByTagName;
  311.  
  312. let byPass = true;
  313.  
  314. const observablePromise = (proc, timeoutPromise) => {
  315. let promise = null;
  316. return {
  317. obtain() {
  318. if (!promise) {
  319. promise = new Promise(resolve => {
  320. let mo = null;
  321. const f = () => {
  322. let t = proc();
  323. if (t) {
  324. mo.disconnect();
  325. mo.takeRecords();
  326. mo = null;
  327. resolve(t);
  328. }
  329. }
  330. mo = new MutationObserver(f);
  331. mo.observe(document, { subtree: true, childList: true })
  332. f();
  333. timeoutPromise && timeoutPromise.then(() => {
  334. resolve(null)
  335. });
  336. });
  337. }
  338. return promise
  339. }
  340. }
  341. }
  342.  
  343. const documentReady = new Promise(resolve => {
  344. Promise.resolve().then(() => {
  345. if (document.readyState !== 'loading') {
  346. resolve();
  347. } else {
  348. window.addEventListener("DOMContentLoaded", resolve, false);
  349. }
  350. });
  351. });
  352.  
  353. documentReady.then(async () => {
  354. pud.resolve();
  355. });
  356.  
  357. function getElementsByTagName(tag) {
  358. if (byPass) {
  359. if (tag === 'pre' || tag === 'code' || tag === 'xmp') {
  360. if (location.pathname.endsWith('/code')) {
  361. pElementQuery.resolve();
  362. return [];
  363. }
  364. }
  365. }
  366. return this.getElementsByTagName331(tag);
  367. }
  368.  
  369. async function onBodyHeadReadyAsync() {
  370. await observablePromise(() => document.body && document.head).obtain();
  371. }
  372.  
  373.  
  374. // Load CSS
  375. function loadJS(href) {
  376.  
  377. return new Promise(resolve => {
  378.  
  379. const script = document.createElement('script');
  380. script.src = href;
  381. script.onload = () => {
  382. resolve(script);
  383. };
  384. document.head.appendChild(script);
  385.  
  386. });
  387.  
  388. }
  389.  
  390. // Load CSS
  391. function loadCSS(href) {
  392. const link = document.createElement('link');
  393. link.rel = 'stylesheet';
  394. link.href = href;
  395. document.head.appendChild(link);
  396. return link;
  397. }
  398.  
  399.  
  400.  
  401.  
  402.  
  403.  
  404. /** @param {HTMLElement} pre */
  405. async function prepareCodeAreaAsync(pre) {
  406.  
  407. if (pre.isConnected === false) return;
  408.  
  409. for (const li of pre.querySelectorAll('li')) {
  410. li.append(document.createTextNode('\n'));
  411. }
  412.  
  413. const codeElement = document.createElement('code');
  414. // codeElement.classList.add('language-javascript');
  415. codeElement.innerHTML = pre.innerHTML;
  416.  
  417. // Clearing the original code container and appending the new one
  418. // pre.classList = '';
  419. pre.innerHTML = '';
  420. // pre.appendChild(codeElement);
  421.  
  422. // if (pre.querySelector('code')) return;
  423. const code = codeElement;
  424.  
  425. const codeContainer = pre.closest('.code-container');
  426. if (codeContainer && codeContainer.querySelector('.code-container>pre:only-child')) {
  427. // avoid selection to the outside by mouse dragging
  428. codeContainer.setAttribute('contenteditable', '');
  429. codeContainer.querySelector('.code-container>pre:only-child').setAttribute('contenteditable', 'false');
  430. }
  431.  
  432.  
  433. // let parentNode = code.parentNode;
  434. // let nextNode = code.nextSibling;
  435.  
  436. // code.remove();
  437. let parentNode = pre;
  438. let nextNode = null;
  439. await Promise.resolve().then();
  440.  
  441. // preset language
  442. /*
  443. const text = codeElement.textContent;
  444. if(/(^|\n)\s*\/\/\s+==UserScript==\s*\n/.test(text)){
  445. codeElement.classList.add('language-javascript');
  446. }else if(/(^|\n)\s*\/\*\s+==UserStyle==\s*\n/.test(text)){
  447. codeElement.classList.add('language-css');
  448. }
  449. */
  450.  
  451.  
  452. let preLang = '';
  453.  
  454. if (pre.classList.contains('lang-js')) {
  455. preLang = 'lang-js';
  456. } else if (pre.classList.contains('lang-css')) {
  457. preLang = 'lang-css';
  458. } else if (pre.classList.contains('uglyprint')){
  459. let m =/\/\/\s*={2,9}(\w+)={2,9}\s*[\r\n]/.exec(codeElement.textContent);
  460. if(m){
  461. m = m[1];
  462. if(m === 'UserScript') preLang = 'lang-js';
  463. if(m === 'UserStyle') preLang = 'lang-css';
  464. }
  465. }
  466.  
  467. let className = '';
  468. if (preLang === 'lang-js') {
  469. className = 'language-javascript';
  470. } else if (preLang === 'lang-css') {
  471.  
  472. const text = codeElement.textContent;
  473. let m = /\n@preprocessor\s+([-_a-zA-Z]{3,8})\s*\n/.exec(text);
  474. className = 'language-css'
  475. if (m) {
  476. const preprocessor = m[1];
  477. if (preprocessor === 'stylus') {
  478. className = 'language-stylus';
  479. } else if (preprocessor === 'uso') {
  480. className = 'language-stylus';
  481. } else if (preprocessor === 'less') {
  482. className = 'language-less';
  483. } else if (preprocessor === 'default') {
  484. className = 'language-stylus';
  485. } else {
  486. className = 'language-stylus';
  487. }
  488. }
  489.  
  490.  
  491. }
  492.  
  493.  
  494. if (!className) return;
  495.  
  496. let mode = '';
  497. if (className === 'language-javascript') mode = 'javascript';
  498. if (className === 'language-stylus') mode = 'stylus';
  499. if (className === 'language-less') mode = 'less';
  500. if (className === 'language-css') mode = 'css';
  501.  
  502. if (!mode) return;
  503.  
  504.  
  505.  
  506. let textarea = document.createElement('textarea');
  507. textarea.value = `${code.textContent}`;
  508. textarea.readOnly = true;
  509. textarea.id = 'editor651';
  510.  
  511.  
  512. // textarea.classList.add('code-container')
  513.  
  514. textarea.style.width = '100%';
  515. textarea.style.height = '100vh';
  516.  
  517. parentNode.insertBefore(textarea, nextNode);
  518.  
  519.  
  520. const editor651 = CodeMirror.fromTextArea(document.querySelector('#editor651'), {
  521.  
  522. mode: mode,
  523. theme: document.documentElement.hasAttribute('dark') ? 'material' : 'default',
  524.  
  525. readOnly: true,
  526. styleActiveLine: true,
  527. lineNumbers: true,
  528. extraKeys: { "Alt-F": "findPersistent" }
  529. });
  530. editor651.save();
  531. function refresh() {
  532. try {
  533. editor651.display.input.cm.refresh();
  534. } catch (e) {
  535. }
  536. }
  537. document.documentElement.addEventListener('cm-highlight-refresh', function () {
  538. setTimeout(refresh, 100);
  539. requestAnimationFrame(refresh);
  540. refresh();
  541. });
  542.  
  543. }
  544.  
  545. const documentBodyHeadReady = onBodyHeadReadyAsync();
  546.  
  547. documentBodyHeadReady.then(async () => {
  548.  
  549. if (!location.pathname.endsWith('/code')) {
  550. return;
  551. }
  552.  
  553. document.head.appendChild(document.createElement('style')).textContent = `${cssAdd()}`;
  554.  
  555. await loadJS(resoruces['codemirror.min.js']);
  556.  
  557. await Promise.all([
  558. loadJS(resoruces['javascript.min.js']),
  559. loadJS(resoruces['css.min.js']),
  560. loadJS(resoruces['stylus.min.js']),
  561. loadJS(resoruces['active-line.min.js']),
  562. loadJS(resoruces['search.js']),
  563. loadJS(resoruces['searchcursor.js']),
  564. loadJS(resoruces['jump-to-line.js']),
  565. loadJS(resoruces['dialog.js'])
  566. ]);
  567.  
  568. if (document.documentElement.hasAttribute('dark')) {
  569.  
  570. // TBC
  571. loadCSS(resoruces['codemirror.min.css']);
  572. loadCSS(resoruces['dialog.css']);
  573. loadCSS(resoruces['material.css']);
  574.  
  575. } else {
  576.  
  577. loadCSS(resoruces['codemirror.min.css']);
  578. loadCSS(resoruces['dialog.css']);
  579. }
  580.  
  581.  
  582. pScript.resolve();
  583.  
  584.  
  585.  
  586.  
  587. });
  588.  
  589. let keydownActive = false;
  590.  
  591. documentReady.then(async () => {
  592.  
  593. if (!location.pathname.endsWith('/code')) {
  594. byPass = false;
  595. return;
  596. }
  597.  
  598. await pScript.then();
  599.  
  600. await Promise.race([pElementQuery, delayPn(800)]);
  601.  
  602. const targets = document.querySelectorAll('.code-container pre.lang-js, .code-container pre.lang-css, .code-container pre.uglyprint');
  603.  
  604. if (targets.length === 0) return;
  605.  
  606. await delayPn(40);
  607.  
  608. document.head.appendChild(document.createElement('style')).textContent = doActionCSS();
  609.  
  610. await delayPn(40);
  611.  
  612. byPass = false;
  613.  
  614. // Code highlighting
  615. const promises = [...targets].map(prepareCodeAreaAsync)
  616. await Promise.all(promises);
  617.  
  618. await delayPn(40);
  619. document.documentElement.setAttribute('dkkfv', '');
  620. keydownActive = true;
  621. document.documentElement.dispatchEvent(new CustomEvent('cm-highlight-refresh'));
  622.  
  623. });
  624.  
  625. function selectAllWithinElement(element) {
  626. window.getSelection().removeAllRanges();
  627. let range = document.createRange();
  628. if (element) {
  629. range.selectNodeContents(element);
  630. window.getSelection().addRange(range);
  631. } else {
  632. console.error('Element not found with ID:', element);
  633. }
  634. }
  635. document.addEventListener('keydown', (e) => {
  636. if (keydownActive && e && e.code === 'KeyA' && e.isTrusted && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
  637.  
  638. const target = e.target;
  639. const container = target ? target.closest('div.code-container') : null;
  640. const code = container ? container.querySelector('code') : null;
  641.  
  642. if (container && code) {
  643.  
  644. e.preventDefault();
  645. e.stopPropagation();
  646. e.stopImmediatePropagation();
  647.  
  648. setTimeout(() => {
  649. selectAllWithinElement(code);
  650. }, 1)
  651.  
  652. }
  653.  
  654. }
  655. }, true);
  656.  
  657.  
  658. const cmLineNumberAppearFn = (evt) => {
  659. const elm = evt.target;
  660. if (!(elm instanceof HTMLElement)) return;
  661.  
  662. elm.setAttribute('marker-text', elm.textContent.trim());
  663. elm.textContent = '';
  664. }
  665.  
  666. document.addEventListener('animationstart', (evt) => {
  667. const animationName = evt.animationName;
  668. if (!animationName) return;
  669. if (animationName === 'cmLineNumberAppear') cmLineNumberAppearFn(evt);
  670. }, { capture: true, passive: true });
  671.  
  672.  
  673. })();