// ==UserScript== // @name Syntax highlighting // @namespace https://floatsyi.com/ // @version 0.2.3 // @description 使用 highlight.js 给代码片断添加语法高亮, 并设置更优雅的字体.查看codepen原型快速了解: https://codepen.io/FloatingShuYin/pen/GRRjmOE?editors=0010 // @author floatsyi // @license MIT // @require https://cdn.bootcss.com/highlight.js/9.15.10/highlight.min.js // @require https://cdn.bootcss.com/fontfaceobserver/2.1.0/fontfaceobserver.js // @require https://unpkg.com/vue@2.6.10/dist/vue.min.js // @require https://unpkg.com/buefy/dist/buefy.min.js // @match *://*/* // @grant GM_addStyle // @grant GM_deleteValue // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_addValueChangeListener // @grant unsafeWindow // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== // https://www.bootcdn.cn/highlight.js/ // 当前版本号 // FIXME const currentVersion = '0.2.0' // debug log const isDev = false const log = (...any) => {if (isDev) { console.log(...any) }} log('GM_listValues', GM_listValues()) // 环境探测 const envDetection = [ unsafeWindow.Prism, unsafeWindow.hljs, unsafeWindow.prettyPrint ] if (envDetection.some(item => !!item)) return false // thmem 和 font 列表 // https://highlightjs.org/static/demo/ // ;[...document.querySelectorAll('#styles > li')].map(item =>item.innerText.toLocaleLowerCase().replace(/\s/g, '-').replace(/(-)?(\d+)(-)?/g,'$2').replace(/(qtcreator)(-)(dark|light)/, '$1_$3').replace(/(kimbie)(-)(dark|light)/,'$1.$2')) const themes = [ 'default', 'a11y-dark', 'a11y-light', 'agate', 'an-old-hope', 'androidstudio', 'arduino-light', 'arta', 'ascetic', 'atelier-cave-dark', 'atelier-cave-light', 'atelier-dune-dark', 'atelier-dune-light', 'atelier-estuary-dark', 'atelier-estuary-light', 'atelier-forest-dark', 'atelier-forest-light', 'atelier-heath-dark', 'atelier-heath-light', 'atelier-lakeside-dark', 'atelier-lakeside-light', 'atelier-plateau-dark', 'atelier-plateau-light', 'atelier-savanna-dark', 'atelier-savanna-light', 'atelier-seaside-dark', 'atelier-seaside-light', 'atelier-sulphurpool-dark', 'atelier-sulphurpool-light', 'atom-one-dark-reasonable', 'atom-one-dark', 'atom-one-light', 'brown-paper', 'codepen-embed', 'color-brewer', 'darcula', 'dark', 'darkula', 'docco', 'dracula', 'far', 'foundation', 'github-gist', 'github', 'gml', 'googlecode', 'grayscale', 'gruvbox-dark', 'gruvbox-light', 'hopscotch', 'hybrid', 'idea', 'ir-black', 'isbl-editor-dark', 'isbl-editor-light', 'kimbie.dark', 'kimbie.light', 'lightfair', 'magula', 'mono-blue', 'monokai-sublime', 'monokai', 'nord', 'obsidian', 'ocean', 'paraiso-dark', 'paraiso-light', 'pojoaque', 'purebasic', 'qtcreator_dark', 'qtcreator_light', 'railscasts', 'rainbow', 'routeros', 'school-book', 'shades-of-purple', 'solarized-dark', 'solarized-light', 'sunburst', 'tomorrow-night-blue', 'tomorrow-night-bright', 'tomorrow-night-eighties', 'tomorrow-night', 'tomorrow', 'vs', 'vs2015', 'xcode', 'xt256', 'zenburn' ] // google Monospace fonts: https://fonts.google.com/?sort=date&category=Monospace // ;[...document.querySelectorAll('.fonts-module-title')].map(item => item.innerText) const fonts = [ 'Fira Code', 'B612 Mono', 'Major Mono Display', 'IBM Plex Mono', 'Nanum Gothic Coding', 'Overpass Mono', 'Space Mono', 'Roboto Mono', 'Fira Mono', 'Share Tech Mono', 'Cutive Mono', 'Source Code Pro' ] const shouldClearCacheKeys = ['currentTheme', 'currentFont', 'bulmaStyle'] const hash = '662eb72f' // fnv132('Syntax_highlighting') const hashString = str => `${str}-${hash}` const getCacheValue = key => GM_getValue(hashString(key)) const setCacheValue = (key, value) => GM_setValue(hashString(key), value) const deleteCacheValue = key => GM_deleteValue(hashString(key)) const hasCacheValue = key => !!GM_getValue(hashString(key)) // 默认字体与主题 const defaultTheme = 'atom-one-dark' const defaultFont = 'Fira Code' let currentTheme = getCacheValue('currentTheme') || defaultTheme let currentFont = getCacheValue('currentFont') || defaultFont // hashString const hashVersion = hashString('version') const clearCache = () => { for (const key of [...themes, ...fonts, ...shouldClearCacheKeys]) { deleteCacheValue(key) } } // 如果是新版本就清除缓存 GM_addValueChangeListener(hashVersion, function ( name, old_value, new_value, remote ) { if (old_value !== new_value) { clearCache() // TODO 清除之前 0.1.2 版本的废弃缓存 ;['Fira Code', 'atom-one-dark', 'bulmaStyle'].forEach(key => {GM_deleteValue(key)}) // TODO 清除 0.2.2 版本的废弃缓存 deleteCacheValue('isForcePreBackgroundColors') } }) // 保存当前版本号, 触发监听 GM_setValue(hashVersion, currentVersion) const fontSize = getCacheValue('fontSize') || 16 const isApplyThemeChanges = getCacheValue('isApplyThemeChanges') || 'Yes' const isApplyFontChanges = getCacheValue('isApplyFontChanges') || 'Yes' const isGFW = getCacheValue('isGFW') || 'Fuck' const isForcePreBackground = getCacheValue('isForcePreBackground') || 'Yes' const getCurrentThemeBackground = styleText => styleText.match(/background:(.*?)[;}]/)[1] // 轮询 const poll = ({ condition, resolve, reject = () => {}, millisecond = 1000, retries = 1 }) => { if (condition()) return resolve() let time = 0 const int = setInterval(() => { time++ if (condition()) { clearInterval(int) return resolve() } else if (time > retries) { clearInterval(int) return reject() } }, millisecond) const stop = () => { clearInterval(int) } return stop } const fetchStyleText = url => fetch(url, { headers: { 'Content-Type': 'text/plain' } }).then(response => { return response.text() }) // 获取并设置样式 const setStyle = () => { // 获取主题样式并添加 const themeStyle = getCacheValue(currentTheme) if (themeStyle) { GM_addStyle(themeStyle) } else { const themeUrl = this.GFW === 'Fuck' ? `https://cdn.bootcss.com/highlight.js/9.15.10/styles/${currentTheme}.min.css` : `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/${currentTheme}.min.css` fetchStyleText(themeUrl).then(style => { GM_addStyle(style) setCacheValue(currentTheme, style) }) } // 获取字体样式并添加 const fontStyle = getCacheValue(currentFont) if (fontStyle) { GM_addStyle(fontStyle) } else { const fontUrl = isGFW ? `https://fonts.loli.net/css?family=${currentFont}&display=swap` : `https://fonts.googleapis.com/css?family=${currentFont}&display=swap` fetchStyleText(fontUrl).then(style => { GM_addStyle(style) setCacheValue(currentFont, style) }) } } // 为 code 片断应用 highlightBlock 并设置字体样式 const beautify = () => { setStyle() const font = new window.FontFaceObserver(currentFont) font.load().then( () => { log('Font is available') for (const pre of Array.from(document.querySelectorAll('pre'))) { const code = pre.querySelector('code') if (isApplyThemeChanges) { window.hljs.highlightBlock(code) } if (isApplyFontChanges === 'Yes') { code.style.fontFamily = currentFont code.style.fontSize = `${fontSize}px` } if (isForcePreBackground === 'Yes') { pre.style.background = getCurrentThemeBackground( getCacheValue(currentTheme) ) } } }, () => { log('Font is not available') } ) } // 设置页 let parasitifer = null const openSetting = () => { // 非首次调用 if (parasitifer) { parasitifer.show() return true } parasitifer = document.createElement('div') // 此 DOM 节点将用作 shadowDOM 的载体被插入宿主的 DOM 节点中. parasitifer.id = 'host-element' parasitifer.style = `position: fixed;top:0;bottom:0;z-index:9999;width:100vw;height:100vh;font-size:16px;background-color:#fff;` parasitifer.show = () => { parasitifer.style.display = 'block' } parasitifer.hide = () => { parasitifer.style.display = 'none' } const shadowRoot = parasitifer.attachShadow({ mode: 'open' }) // 此节点将成为 shadowDOM 的直接子元素, 包裹一切, 所以用 HTML 元素很合适. // 不仅仅是语义上的合适, 大多数的 UI 库都需要一个结构完整的 DOM 树用来做自适应布局. const shadowContent = document.createElement('HTML') const shadowStyleEle = document.createElement('style') const bulmaStyleEle = document.createElement('style') const fontStyleEle = document.createElement('style') const themeStyleEle = document.createElement('style') const vueContainer = document.createElement('div') // 这个 DOM 节点不会显示在 DOM 树中, 而是作为 vue 的挂载点,同来渲染 vue 的模板. vueContainer.id = 'vue-root' // shadow DOM 的样式作用域隔离是非常实用的特性, 完全不受宿主环境影响的样式, 轻盈的开始 shadowStyleEle.innerText = `` shadowContent.appendChild(shadowStyleEle) shadowContent.appendChild(bulmaStyleEle) shadowContent.appendChild(fontStyleEle) shadowContent.appendChild(themeStyleEle) shadowContent.appendChild(vueContainer) shadowRoot.appendChild(shadowContent) document.body.appendChild(parasitifer) const mount = style => { bulmaStyleEle.innerText = style const vueRoot = document .querySelector('#host-element') .shadowRoot.querySelector('#vue-root') // 这里使用 body 元素 作为父节点, 结合上面创造的 HTML 元素是为了给 UI 组件一个完整的上下文环境, 就像在一个新的 HTML 页面中一样. const vueTemplate = `