给CSS适配人员用的辅助工具函数
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/439632/1015907/CSS%E6%94%B9%E7%BC%96%E7%BD%91%E9%A1%B5%E5%93%8D%E5%BA%94%E5%BC%8F%20%E9%80%82%E9%85%8D%E5%B7%A5%E4%BD%9C%E8%80%85.js
// ==UserScript==
// @name CSS改编/网页响应式 适配工作者
// @version 0.89
// @description 给CSS适配人员用的辅助工具函数
// @description:en Helper functions for adaptation developers
// @match *://*/*
// @license The Unlicense
// @namespace https://greasyfork.org/users/871942
// ==/UserScript==
const CSSA = unsafeWindow.CSSA = {
elemsWithInlineStyles,
extractCssOfModifiedInPage,
findOverflowedElems,
unsetStyles,
removeSelectorsThose: { tooBroad: true }
}
const warnSelectorsThose = { tooBroad: '/*⚠*/' }
const rxSelectorsThose = { tooBroad: /^\/\*⚠\*\/[^.#]+ {[^\n]+\n*/gm }
function elemsWithInlineStyles(doc = document, filterAttr) {
const elems = []
if (!doc) return elems
elems.push(...[...doc.all].filter(el =>
(!filterAttr || el.hasAttribute(filterAttr)) &&
!/\b(a|img|span)\b/.test(el.localName) &&
(el.localName === 'iframe'
? elems.push(...elemsWithInlineStyles(el.contentDocument, filterAttr)) && false
: el.attributes.style?.value
)
))
return elems
}
function extractStyleToCssForm(elem) {
let { localName, attributes: { id = '', class: className = '', style } } = elem
if (specTags.has(localName)) id = className = ''
else {
localName = id || className ? '' : `${warnSelectorsThose.tooBroad}${localName}`
if (className) className = className.value.replace(/ |^/g, '.')
if (id) {
id = /[-]|auto|\bid\b/.test(id.value) ? '' : `#${id.value}`
if (id) className = ''
}
}
return `${localName}${id}${className} { ${style.value
.replace(/(:) /g, '$1')
.replace(/;\b/g, '$& ')
.replace(/;$/, '')} }`
}
const specTags = new Set('html body'.split(' '))
function extractCssOfModifiedInPage(rootNode = document, { filterAttr = '', existing = 'user-custom' } = {}) {
rootNode = rootNode.getRootNode()
// `rootNode` can be an arbitrarily selected leaf node without having to pay attention to selecting `HTMLDocument`
let orig = existing && rootNode instanceof Node && rootNode.querySelector(`style[${existing}]`)?.innerText || ''
, new_ = elemsWithInlineStyles(rootNode, filterAttr).map(extractStyleToCssForm).join('\n')
return { modified: mergeCommonCss(new_), merged: mergeCommonCss(orig + new_) }
}
function mergeCommonCss(css = '') {
const re = {
node: [/^(\s*)([^{}}]+)\s*\{([^}]+?)\s*\}(.*?)\2\{([^}]+?)\s*\}/ms, '$1$2{$3;$5 }$4'],
nodes: [/([^{\n]+?)(\s*\{[^}]+\})(.*?)\s*([^{\n]+?)\2/s, '$1, $4$2$3']
}
let merged
Object.values(re).forEach(([mat, pla]) => {
const merge = str => str.replace(mat, pla)
merged = merge(css)
while (css !== merged) merged = merge(css = merged)
})
Object.keys(CSSA.removeSelectorsThose).forEach(k =>
CSSA.removeSelectorsThose[k] && (
merged = merged.replace(rxSelectorsThose[k], '')
)
)
return merged.trim()
}
function findOverflowedElems(rootElem = document.body, { echo = false } = {}) {
if (!(rootElem instanceof HTMLElement)) throw TypeError('An entry element is required to be specified.')
if (echo) console.log(`The width of the rootElem`, rootElem, `is ${rootElem.clientWidth}px.`)
return [...rootElem.querySelectorAll('*')].filter(el => el.clientWidth > rootElem.clientWidth)
}
function unsetStyles(elem = CSSA.$0, props = [], { echo = true } = {}) {
if (!(elem instanceof HTMLElement)) throw TypeError('An element is required to be specified.')
if (typeof props === 'string') props = props.split(/[\s;]+/).filter(Boolean)
elem.style.cssText += props.map(prop => `${prop}:unset`).join('; ')
if (echo) console.log('The style value of', elem, `has been set to: {\n ${elem.attributes.style.value}\n}`)
}