GreasyFork Code: Syntax Highlight by CodeMirror

To syntax highlight GreasyFork Code by CodeMirror

As of 2024-01-24. See the latest version.

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