// ==UserScript==
// @name 代码片段语法高亮 + 不要翻译页面上的代码
// @namespace https://floatsyi.com/
// @version 0.3.1
// @description 使用 highlight.js 给代码片断添加语法高亮, 并设置更优雅的字体.查看codepen原型快速了解: https://codepen.io/FloatingShuYin/pen/GRRjmOE?editors=0010 不要翻译页面上的代码请参考:https://greasyfork.org/zh-CN/scripts/376658-%E4%B8%8D%E8%A6%81%E7%BF%BB%E8%AF%91github%E4%B8%8A%E7%9A%84%E4%BB%A3%E7%A0%81
// @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/[email protected]/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
// ==/UserScript==
// https://www.bootcdn.cn/highlight.js/
// 当前版本号
// FIXME
const currentVersion = '0.3.1'
// debug log
const isDev = false
const log = (...any) => {if (isDev) { console.log(...any) }}
log('GM_listValues', GM_listValues())
// 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 = ['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)
// 避免 google 网页翻译当前页面的代码
const hasCodeEleChild = ele => !!ele.querySelector('code')
function addCodeEle (ele) {
ele.innerHTML = '<code class="doNotTranslate">' + ele.innerHTML + '</code>'
}
function doNotTranslateCode () {
const pres = document.querySelectorAll('pre')
pres.forEach(function (pre) {
if (!hasCodeEleChild(pre)) addCodeEle(pre)
})
}
const _ = {}
_.debounce = function (func, wait) {
var lastCallTime
var lastThis
var lastArgs
var timerId
function startTimer (timerExpired, wait) {
return setTimeout(timerExpired, wait)
}
function remainingWait (time) {
const timeSinceLastCall = time - lastCallTime
const timeWaiting = wait - timeSinceLastCall
return timeWaiting
}
function shoudInvoking (time) {
return lastCallTime !== undefined && time - lastCallTime >= wait
}
function timerExpired () {
const time = Date.now()
if (shoudInvoking(time)) {
return invokeFunc()
}
timerId = startTimer(timerExpired, remainingWait(time))
}
function invokeFunc () {
timerId = undefined
const args = lastArgs
const thisArg = lastThis
let result = func.apply(thisArg, args)
lastArgs = lastThis = undefined
return result
}
function debounced (...args) {
let time = Date.now()
lastThis = this
lastArgs = args
lastCallTime = time
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
}
return debounced
}
const body = document.body
const option = {
childList: true,
subtree: true
}
let time = 0
function doNotTranslate (mutations, observer) {
// 处于过于频繁的 DOM 变更时, 暂停监听 50ms, 并放弃累积的未处理的变更事件
if (time >= 20) {
observer.disconnect()
observer.takeRecords()
time = 0
setTimeout(function () {
mo.observe(body, option)
}, 50)
}
doNotTranslateCode()
time++
log(`第${time}次执行: doNotTranslate`)
}
const MutationObserver =
window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver
const mo = new MutationObserver(_.debounce(doNotTranslate, 50))
const currentHostName = [window.location.host]
let hostNames = getCacheValue('hostNames')
if (!hostNames) {
setCacheValue('hostNames', ['www.npmjs.com'])
} else {
if (hostNames.includes(...currentHostName)){
doNotTranslateCode()
mo.observe(body, option)
}
}
// 注册设置页
GM_registerMenuCommand('不要翻译这个页面的代码', () => {
hostNames = getCacheValue('hostNames')
if (hostNames.includes(...currentHostName)) return false
doNotTranslateCode()
mo.observe(body, option)
const newhostNames= [...currentHostName, ...hostNames]
setCacheValue('hostNames', newhostNames)
}, 'D')
// 环境探测
const envDetection = [
unsafeWindow.Prism,
unsafeWindow.hljs,
unsafeWindow.prettyPrint
]
if (envDetection.some(item => !!item)) return false
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 && code) {
code.classList.remove('language-text')
window.hljs.highlightBlock(code)
}
if (isApplyFontChanges === 'Yes' && code) {
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 = `<body style="height: 100vh">
<div id="app-vue" class="container">
<div class="columns is-vcentered is-centered">
<div class="column is-3">
<section>
<b-field label="Themes">
<b-select placeholder="Select a name" @input="changeTheme" v-model="current.theme" rounded>
<option v-for="(item, index) in themes" :value="item" :key="index">
{{item}}
</option>
</b-select>
</b-field>
<b-field label="Monospace Fonts">
<b-select placeholder="Select a name" @input="changeFont" v-model="current.font" rounded>
<option v-for="(item, index) in fonts" :value="item" :key="index">
{{item}}
</option>
</b-select>
</b-field>
<b-field label="Font Size">
<b-slider v-model="fontSize" @input="changeFontSize"></b-slider>
</b-field>
<b-field label="Apply theme changes">
<b-switch v-model="isApplyThemeChanges" true-value="Yes" false-value="No">
{{ isApplyThemeChanges }}
</b-switch>
</b-field>
<b-field label="Apply font changes">
<b-switch v-model="isApplyFontChanges" true-value="Yes" false-value="No">
{{ isApplyFontChanges }}
</b-switch>
</b-field>
<b-field label="Force theme background colors">
<b-switch v-model="isForcePreBackground" true-value="Yes" false-value="No">
{{ isForcePreBackground }}
</b-switch>
</b-field>
<b-field label="铁幕重重困青年">
<b-switch v-model="isGFW" true-value="Fuck" false-value="No thank you">
{{ isGFW }}
</b-switch>
</b-field>
</section>
<section style="margin-top:30px;">
<b-button type="is-primary" rounded @click="apply">Apply</b-button>
<b-button type="is-warning" rounded @click="close">Close</b-button>
<b-button type="is-danger" rounded @click="reset">Reset</b-button>
</section>
</div>
<div class="column">
<section style="overflow:hidden;">
<h1 class="has-text-centered">Real-time preview</h1>
<pre class="has-text-left" :style="styles.pre" ref="pre">
<code :style="styles.code" ref="code">
import something from 'something'
// 获取并设置样式
const setStyle = () => {
// 获取主题样式并添加
const themeStyle = GM_getValue(hashString(currentTheme))
if (themeStyle) {
GM_addStyle(themeStyle)
} else {
const themeUrl = \`https://cdn.bootcss.com/highlight.js/9.15.10/styles/\${currentTheme}.min.css\`
fetchStyleText(themeUrl).then(style => {
GM_addStyle(style)
GM_setValue(hashString(currentTheme), style)
})
}
}
export default something
</code>
</pre>
</section>
</div>
</div>
</div>
</body>
`
const vm = new window.Vue({
el: vueRoot,
template: vueTemplate,
data () {
return {
current: {
theme: currentTheme,
font: currentFont
},
styles: {
pre: {
maxWidth: '952px',
maxHeight: '631px',
overflow: 'hidden',
},
code: {
overflow: 'hidden',
fontSize: `${fontSize}px`,
fontFamily: currentFont
}
},
fontSize: fontSize,
themes: themes,
fonts: fonts,
isApplyThemeChanges: isApplyThemeChanges,
isApplyFontChanges: isApplyFontChanges,
isForcePreBackground: isForcePreBackground,
isGFW: isGFW,
defaultBackground: ''
}
},
watch: {
isForcePreBackground (value) {
log(value)
if (value === 'No') {
this.$refs.pre.style.background = this.defaultBackground
} else {
poll({
condition: () => hasCacheValue(this.current.theme),
resolve: () => {
this.$refs.pre.style.background = getCurrentThemeBackground(
getCacheValue(this.current.theme)
)
},
millisecond: 1000,
retries: 5
})
}
}
},
methods: {
reset () {
this.fontSize = 16
this.isApplyThemeChanges = 'Yes'
this.isApplyFontChanges = 'Yes'
this.isForcePreBackground = 'No'
this.isGFW = 'Fuck'
this.current.theme = 'atom-one-dark'
this.current.font = 'Fira Code'
this.changeTheme(this.current.theme)
this.changeFont(this.current.font)
},
apply () {
setCacheValue('fontSize', this.fontSize)
setCacheValue('isApplyThemeChanges', this.isApplyThemeChanges)
setCacheValue('isApplyFontChanges', this.isApplyFontChanges)
setCacheValue(
'isForcePreBackground',
this.isForcePreBackground
)
setCacheValue('isGFW', this.isGFW)
setCacheValue('currentTheme', this.current.theme)
setCacheValue('currentFont', this.current.font)
},
close () {
parasitifer.hide()
},
changeTheme (value) {
if (this.isApplyThemeChanges === 'No') return false
const doChangeTheme = (thmemName, themeStyle) => {
themeStyleEle.innerText = themeStyle
if (this.isForcePreBackground === 'Yes') {
log(getCurrentThemeBackground(
themeStyle
))
this.$refs.pre.style.background = getCurrentThemeBackground(
themeStyle
)
log(getCurrentThemeBackground(themeStyle))
}
window.hljs.highlightBlock(this.$refs.code)
}
if (hasCacheValue(value)) {
doChangeTheme(value, getCacheValue(value))
log(`get theme: ${value} in cache`)
} else {
const url =
this.GFW === 'Fuck'
? `https://cdn.bootcss.com/highlight.js/9.15.10/styles/${value}.min.css`
: `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/${value}.min.css`
log(url)
fetchStyleText(url).then(style => {
setCacheValue(value, style)
doChangeTheme(value, style)
})
}
log('current theme', this.current.theme)
},
changeFont (value) {
if (this.isApplyFontChanges === 'No') return false
const doChangeFont = (fontName, fontStyle) => {
fontStyleEle.innerText = fontStyle
this.styles.code.fontFamily = fontName
}
if (hasCacheValue(value)) {
doChangeFont(value, getCacheValue(value))
log(`get font: ${value} in cache`)
} else {
const url =
this.GFW === 'Fuck'
? `https://fonts.loli.net/css?family=${value}&display=swap`
: `https://fonts.googleapis.com/css?family=${value}&display=swap`
fetchStyleText(url).then(style => {
log('font style:', style)
setCacheValue(value, style)
doChangeFont(value, style)
})
}
log(value)
},
changeFontSize (value) {
if (this.isApplyFontChanges === 'No') return false
log(value)
this.styles.code.fontSize = `${value}px`
}
},
mounted () {
this.defaultBackground = this.$refs.pre.style.background
this.changeTheme(this.current.theme)
this.changeFont(this.current.font)
}
})
log('Setting is mounted')
}
// 获取 bulmaStyle, 并挂载 设置页
const bulmaStyle = getCacheValue('bulmaStyle')
if (bulmaStyle) {
mount(bulmaStyle)
} else {
const bulmaUrl = 'https://unpkg.com/buefy/dist/buefy.min.css'
fetchStyleText(bulmaUrl).then(style => {
mount(style)
setCacheValue('bulmaStyle', style)
})
}
}
// 注册设置页
GM_registerMenuCommand('Open Setting', openSetting, 'SH')
beautify()
log('highlight runing')