GreasyFork Code: Syntax Highlight by CodeMirror

To syntax highlight GreasyFork Code by CodeMirror

2024-01-28 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

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