Greasy Fork 支持简体中文。

CSSAT

用于将网页改编为响应式设计的工具函数

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/439632/1103720/CSSAT.js

  1. // ==UserScript==
  2. // @name CSS Adaptation Toolkit
  3. // @name:zh CSS 适配工具包
  4. // @description CSS SDK for adapting pages to be responsive (responsive web design)
  5. // @description:zh 用于将网页改编为响应式设计的工具函数
  6. // @version 1.43.0
  7. // @match *://*/*
  8. // @license The Unlicense
  9. // ==/UserScript==
  10.  
  11.  
  12. // < APIs >
  13. const CSSA = unsafeWindow.CSSA = {
  14. inspect: {
  15. get whichHaveInlineStyles() { return elemsWithInlineStyles() },
  16. get overflowed() { return elemsOverflowed() }
  17. },
  18.  
  19. mod: {
  20. dump: cssTextModified,
  21. insertStyleSheet, apply: insertStyleSheet,
  22. unsetStyles,
  23. forceOverrideProps: new Set(['overflow']),
  24. doc: document
  25. },
  26.  
  27. selectFarthest,
  28. waitForSelector, wait: waitForSelector,
  29.  
  30. debug: {
  31. breakOnAttrsChange
  32. },
  33.  
  34. miscConfigs: {
  35. removeSelectorsThose: { tooBroad: true }
  36. },
  37.  
  38. toString() { return this.mod.dump().toString() }
  39. }
  40. // </ APIs >
  41.  
  42.  
  43.  
  44. unsafeWindow.addEventListener('load', () => {
  45. CSSA.mod.origWholeCssText = CSSA.mod.dump().modified
  46. })
  47.  
  48. function insertStyleSheet(styleText) {
  49. (CSSA.mod.doc || document).head.insertAdjacentHTML('afterbegin', `<style user-custom>${styleText}</style>`)
  50. }
  51.  
  52. const warnSelectorsThose = { tooBroad: '/*⚠*/' }
  53. const rxSelectorsThose = { tooBroad: /^\/\*⚠\*\/[^.#]+ {[^\n]+\n*/gm }
  54.  
  55. function elemsWithInlineStyles(doc = document, filterAttr) {
  56. const elems = []
  57. if (!doc) return elems
  58. elems.push(...[...doc.all].filter(el =>
  59. (!filterAttr || el.hasAttribute(filterAttr)) &&
  60. !/\b(a|img|span)\b/.test(el.localName) &&
  61. (el.localName === 'iframe'
  62. ? elems.push(...elemsWithInlineStyles(el.contentDocument, filterAttr)) && false
  63. : el.attributes.style?.value
  64. )
  65. ))
  66. return elems
  67. }
  68. function extractStyleToCssForm(elem) {
  69. let { localName, attributes: { id = '', class: className = '', style } } = elem
  70. if (specTags.has(localName)) id = className = ''
  71. else {
  72. localName = id || className ? '' : `${warnSelectorsThose.tooBroad}${localName}`
  73. if (className) className = className.value.replace(/ |^/g, '.')
  74. if (id) {
  75. id = /[-]|auto|\bid\b/.test(id.value) ? '' : `#${id.value}`
  76. if (id) className = ''
  77. }
  78. }
  79. return `${localName}${id}${className} { ${style.value
  80. .replace(/(:) | (!)/g, '$1$2')
  81. .replace(/;\b/g, '$& ')
  82. .replace(/;$/, '')} }`
  83. }
  84. const specTags = new Set('html body'.split(' '))
  85. function cssTextModified(rootNode = document, { filterAttr = '', existingCustomStyle = 'user-custom' } = {}) {
  86. rootNode = rootNode.getRootNode()
  87. // `rootNode` can be an arbitrarily selected leaf node without having to pay attention to selecting `HTMLDocument`
  88. let origCust = existingCustomStyle && rootNode instanceof Node && rootNode.querySelector(`style[${existingCustomStyle}]`)?.innerText || ''
  89. , curr = elemsWithInlineStyles(rootNode, filterAttr).map(extractStyleToCssForm).join('\n')
  90. , modified = modifiedCss(mergeCommonCss(origCust + curr))
  91. , merged = (origCust + modified).trim()
  92. if (!origCust) console.info(
  93. 'Note: If you have style rules located in a `<style>` element to merge,\n' +
  94. ' mark it like `<style user-custom>`.\n' +
  95. ' Then it will be `querySelector("style[user-custom]")`.'
  96. )
  97. return { modified, merged, pageOrig: CSSA.mod.origWholeCssText, toString() { return this.merged } }
  98. }
  99. function mergeCommonCss(css = '') {
  100. const re = {
  101. node: [/^(\s*)([^{}}]+)\s*\{([^}]+?)\s*\}(.*?)\2\{([^}]+?)\s*\}/ms, '$1$2{$3;$5 }$4'],
  102. nodes: [/([^{\n]+?)(\s*\{[^}]+\})(.*?)\s*([^{\n]+?)\2/s, '$1, $4$2$3']
  103. }
  104. let merged
  105. Object.values(re).forEach(([match, replace]) => {
  106. const merge = str => str.replace(match, replace)
  107. merged = merge(css)
  108. while (css !== merged) merged = merge(css = merged)
  109. })
  110. Object.keys(CSSA.miscConfigs.removeSelectorsThose).forEach(k =>
  111. CSSA.miscConfigs.removeSelectorsThose[k] && (
  112. merged = merged.replace(rxSelectorsThose[k], '')
  113. )
  114. )
  115. return merged.trim()
  116. }
  117. function modifiedCss(prevCss = '') {
  118. if (!CSSA.mod.origWholeCssText) return (CSSA.mod.origWholeCssText = prevCss)
  119. CSSA.mod.origWholeCssText.split('\n').forEach(line => prevCss = prevCss.replace(line.trim(), ''))
  120. return prevCss
  121. }
  122.  
  123. function elemsOverflowed(rootElem = document.body, { echo = false } = {}) {
  124. if (!(rootElem instanceof HTMLElement)) throw TypeError('An entry element is required to be specified.')
  125. if (echo) console.log(`The width of the rootElem`, rootElem, `is ${rootElem.clientWidth}px.`)
  126. return [...rootElem.querySelectorAll('*')].filter(el => el.clientWidth > rootElem.clientWidth)
  127. }
  128.  
  129. function unsetStyles(elem = CSSA.$0, props = [], { echo = false } = {}) {
  130. if (!(elem instanceof HTMLElement)) throw TypeError('An element is required to be specified.')
  131. if (typeof props === 'string') props = props.split(/[\s;]+/).filter(Boolean)
  132. elem.style.cssText += props.map(prop => `${prop}:unset${CSSA.mod.forceOverrideProps.has(prop) ? '!important' : ''}`).join('; ')
  133. if (echo) console.log('The style value of', elem, `has been set to: {\n ${elem.attributes.style.value}\n}`)
  134. }
  135. unsetStyles.for = {
  136. width: elem => unsetStyles(elem, 'min-width width')
  137. }
  138.  
  139. function selectFarthest(startElem, selectors = '*') {
  140. if (!selectors) throw TypeError('Please provide a non-empty selectors string.')
  141. if (startElem.contains(document.body)) throw TypeError('startElem should be a child node of <body>.')
  142. if (!startElem instanceof HTMLElement) throw TypeError('startElem should be an HTMLElement.')
  143. let { parentElement } = startElem
  144. while (parentElement && parentElement.localName !== 'body') {
  145. if (parentElement.matches(selectors)) startElem = parentElement;
  146. ({ parentElement } = startElem)
  147. }
  148. return startElem
  149. }
  150.  
  151. function waitForSelector(selectors, { timeout = 30000, optional } = {}) {
  152. return new Promise((resolve, reject) => {
  153. const immediateSelect = stage => {
  154. const elem = document.querySelector(selectors)
  155. if (elem) return (resolve(elem), stage || 1)
  156. // console.log(`${waitForSelector.name}: No matches were found in stage '${stage}'.`)
  157. }
  158. let iWait
  159. if (iWait = immediateSelect('ASAP')) return iWait
  160.  
  161. const observer = new MutationObserver(muts => {
  162. for (const mut of muts) {
  163. for (const node of mut.addedNodes) {
  164. if (node instanceof Element && node.querySelector(selectors)) {
  165. observer.disconnect()
  166. return resolve(node)
  167. }
  168. }
  169. }
  170. })
  171. observer.observe(document.body, { attributes: !true, childList: true, subtree: true })
  172.  
  173. setTimeout(() => {
  174. observer.disconnect()
  175. optional ? resolve(optional) : reject(`Timed out for selectors '${selectors}'`)
  176. }, timeout)
  177. })
  178. }
  179.  
  180. function breakOnAttrsChange(elem, attrsToObsvr) {
  181. if (!elem instanceof Element) throw TypeError('An element is required to be passed in.')
  182. if (typeof attrsToObsvr === 'string') attrsToObsvr = attrsToObsvr.split(/[,;\s]+/)
  183. if (!(Array.isArray(attrsToObsvr) && attrsToObsvr.length)) attrsToObsvr = ['class']
  184. const observer = new MutationObserver(muts => {
  185. muts.forEach(mut => console.log(
  186. mut.target, `: my attr '${mut.attributeName}' changed from` +
  187. `\n '${mut.oldValue}' to\n '${mut.target.getAttribute(mut.attributeName)}'`
  188. ))
  189. debugger
  190. })
  191. observer.observe(elem, { attributeFilter: attrsToObsvr, attributeOldValue: true })
  192. return unsafeWindow.__observers.push(observer)
  193. }
  194.  
  195. if (!Array.isArray(unsafeWindow.__observers)) unsafeWindow.__observers = []