GreasyFork Code: Syntax Highlight by highlight.js

To syntax highlight GreasyFork Code by highlight.js

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