GreasyFork Code: Syntax Highlight by PrismJS

To syntax highlight GreasyFork Code by PrismJS

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