├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── package.json ├── src ├── cssUtils.ts ├── csspaint.ts ├── cssvar.ts ├── domUtils.ts ├── engine.ts ├── index.ts └── rawss.ts ├── test ├── cssUtils.spec.ts ├── cssvars.spec.ts ├── domUtils.spec.ts ├── engine.spec.ts ├── fixtures │ ├── index.html │ ├── test1.css │ └── test2.css ├── manual │ ├── checkerboard.js │ ├── houdini.html │ ├── index.html │ ├── server.js │ └── style.css ├── mocha.opts └── rawss.spec.ts ├── tsconfig.json ├── typings.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | yarn.lock 62 | .DS_Store 63 | dist/ 64 | package-lock.json 65 | docs 66 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8.7.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Wix.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rawss 2 | A generic framework for polyfilling CSS 3 | Aiming for a fully functional polyfill for CSS variable 4 | 5 | ## What it lets you do 6 | * Write any CSS in any browser that supports mutation observers, and process it. 7 | * Use CSS variables in any browser 8 | 9 | Open the demo in IE: 10 | [Demo](https://wix.github.io/rawss/test/manual/index.html) 11 | 12 | ## API 13 | const {Rawss, StyleProcessor, cssVariables} = require('rawss') 14 | 15 | ### StyleProcessor: A generic processor interface to handle a raw CSS rule 16 | * match({name, value, selector}): boolean 17 | Implement to specify which style declarations apply to your processor 18 | 19 | * process(element, getAtomicStyle): CSSStyleDeclaration 20 | Implement to apply raw style to real, valid style 21 | getAtomicStyle can return the raw, unfiltered style of any element 22 | 23 | ### Rawss: generic API for responding to any style change 24 | ### cssVariables: specific implementation for CSS variables polyfill 25 | [API](https://wix.github.io/rawss/docs/) 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rawss", 3 | "version": "1.0.0", 4 | "description": "Framework for CSS polyfills", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "dev": "webpack-dev-server" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+ssh://git@github.com/wix/rawss.git" 13 | }, 14 | "keywords": [ 15 | "polyfill", 16 | "css" 17 | ], 18 | "author": "noamr@wix.com", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wix/rawss/issues" 22 | }, 23 | "homepage": "https://github.com/wix/rawss#readme", 24 | "devDependencies": { 25 | "@types/es6-shim": "^0.31.35", 26 | "@types/mocha": "^2.2.46", 27 | "@types/node": "^9.3.0", 28 | "@types/puppeteer": "^0.13.9", 29 | "chai": "^4.1.2", 30 | "express": "^4.16.2", 31 | "mocha": "^5.0.0", 32 | "mocha-typescript": "^1.1.12", 33 | "puppeteer": "^1.0.0", 34 | "ts-loader": "^3.2.0", 35 | "ts-node": "^4.1.0", 36 | "tsconfig-paths": "^3.1.1", 37 | "typedoc": "^0.9.0", 38 | "typedoc-webpack-plugin": "^1.1.4", 39 | "typescript": "^2.6.2", 40 | "webpack": "^3.10.0", 41 | "webpack-dev-server": "^2.11.0", 42 | "webpack-watch-server": "^1.2.1" 43 | }, 44 | "dependencies": { 45 | "css-shorthand-expand": "^1.1.0", 46 | "css-specificity": "^0.1.0", 47 | "shortid": "^2.2.8" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/cssUtils.ts: -------------------------------------------------------------------------------- 1 | import * as specificity from 'css-specificity' 2 | import * as expand from 'css-shorthand-expand' 3 | export interface AtomicStyleEntry { 4 | name: string; 5 | value: string; 6 | important?: boolean; 7 | } 8 | 9 | export type AtomicStyle = { [prop: string]: string } 10 | export type AtomicStyleDeclaration = AtomicStyleEntry[] 11 | export interface AtomicStyleRule extends AtomicStyleEntry { 12 | selector: string | HTMLElement; 13 | } 14 | 15 | interface RuleSorter { 16 | rule: AtomicStyleRule 17 | index: number 18 | } 19 | const specificityComparator = (a: RuleSorter, b: RuleSorter) => { 20 | if (a.rule.important !== b.rule.important) { 21 | return b.rule.important ? 1 : -1 22 | } 23 | const selectors = [a.rule.selector, b.rule.selector] 24 | const isString = selectors.map(s => typeof(s) === 'string') 25 | if (!isString[0]) { 26 | return -1; 27 | } 28 | 29 | if (isString[0] && !isString[1]) { 30 | return 1; 31 | } 32 | const res = selectors.map(s => specificity.calc(s)[0]) 33 | for (var i = 1; i < 4; i++) { 34 | if (res[0].specificity[i] !== res[1].specificity[i]) { 35 | return res[1].specificity[i] - res[0].specificity[i] 36 | } 37 | } 38 | 39 | return a.index - b.index 40 | } 41 | 42 | // yeah I know this doesn't cover strings. tough luck for now, not willing to integrate a full-blown parser, as it will reduce freedom 43 | function clearComments(css) { 44 | return css.replace(/\/(\*(.|[\r\n])*?\*\/)|(\/[^\n]*)/ig, '') 45 | } 46 | 47 | export function parseDeclaration(rawCss: string) : AtomicStyleDeclaration { 48 | const css = clearComments(rawCss) 49 | const declarationRegex = /\s*([^;:\s]+)\s*:\s*([^;:!]+)\s*(\!important)?;?\s*/ 50 | let index = 0 51 | let parsed = null 52 | let declaration : AtomicStyleDeclaration = [] 53 | while (parsed = css && declarationRegex.exec(css.substr(index))) { 54 | const name = parsed[1].trim() 55 | const value = parsed[2].trim() 56 | const important = !!parsed[3] 57 | const expanded = expand(name, value) || {[name]: value} 58 | const entries = Object.keys(expanded).map(name => (Object.assign({name, value: expanded[name]}, important && {important}))) 59 | declaration = [...declaration, ...entries] 60 | index += parsed.index + parsed[0].length 61 | } 62 | 63 | return declaration 64 | } 65 | 66 | export function parseStylesheet(rawCss: string) : AtomicStyleRule[] { 67 | if (!rawCss) { 68 | return [] 69 | } 70 | const css = clearComments(rawCss) 71 | const ruleRegex = /([^{}]+){([^{}]+)}/ 72 | let index = 0 73 | let parsed = null 74 | let rules : AtomicStyleRule[] = [] 75 | while (parsed = css && ruleRegex.exec(css.substr(index))) { 76 | const selector = parsed[1].trim() 77 | const style = parseDeclaration(parsed[2]) 78 | rules = [...rules, ...style.map(entry => ({selector, ...entry}))] 79 | index += parsed.index + parsed[0].length 80 | } 81 | 82 | return rules 83 | } 84 | 85 | export function sortRulesBySpecificFirst(rules: AtomicStyleRule[]) : AtomicStyleRule[] { 86 | return rules 87 | .map((rule, index) => ({rule, index})) 88 | .sort(specificityComparator) 89 | .map(e => e.rule) 90 | } 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/csspaint.ts: -------------------------------------------------------------------------------- 1 | import {Rawss, StyleResolver, createRawss} from './rawss' 2 | import { AtomicStyle, AtomicStyleRule } from './cssUtils'; 3 | import { escape } from 'querystring'; 4 | import { getRawComputedStyle } from './domUtils'; 5 | 6 | export interface CSSPaintPolyfill { 7 | /*** 8 | * Observe changes in the document, and apply CSS variable resolving as they happen 9 | */ 10 | start() : void 11 | 12 | /*** 13 | * Stop observing 14 | */ 15 | stop() : void 16 | 17 | /*** 18 | * Resolve based on currently loaded styles 19 | */ 20 | once() : void 21 | } 22 | /** 23 | * 24 | * @param options 25 | */ 26 | 27 | export function cssPaint() { 28 | const rawss = createRawss(window.document.documentElement) 29 | const regex = /paint\(([^)]+)\)/ 30 | const customPaints = {} 31 | const elementsWithCustomPaint = new WeakSet() 32 | window['registerPaint'] = (name, cls) => customPaints[name] = cls 33 | CSS['paintWorklet'] = { 34 | addModule: uri => { 35 | const script = document.createElement('script') 36 | script.src = uri 37 | script.onload = () => rawss.once() 38 | document.head.appendChild(script) 39 | } 40 | } 41 | 42 | function resolve(element: HTMLElement, getAtomicStyle: (e: HTMLElement) => AtomicStyle) : Partial { 43 | const atomicStyle = getAtomicStyle(element) 44 | const computedStyle = getComputedStyle(element) 45 | 46 | function doPaint({paint}, props, geom) { 47 | // TODO: cache 48 | const canvas = document.createElement('canvas') 49 | canvas.width = geom.width 50 | canvas.height = geom.height 51 | const ctx = canvas.getContext('2d') 52 | paint(ctx, geom, props) 53 | return `url(${canvas.toDataURL()})` 54 | } 55 | 56 | function listenToElementChanges(element) { 57 | function step() { 58 | const {clientWidth, clientHeight} = element 59 | const {width, height} = element.dataset 60 | if (width !== clientWidth) { 61 | element.dataset.width = clientWidth 62 | } 63 | if (height !== clientHeight) { 64 | element.dataset.height = clientHeight 65 | } 66 | window.requestAnimationFrame(step) 67 | } 68 | 69 | step() 70 | } 71 | 72 | function process(value) { 73 | const matched = regex.exec(value) 74 | if (!matched) { 75 | return value 76 | } 77 | 78 | const paintName = matched[1] 79 | const painter = customPaints[paintName] 80 | if (!painter) { 81 | console.warn(`No paint registered name ${paintName}`) 82 | return value 83 | } 84 | 85 | const props = (painter.inputProperties || []) 86 | .reduce((props, propName) => Object.assign({[propName]: computedStyle.getPropertyValue(propName).trim()}, props), {}) 87 | 88 | // Careful with layout thrashing. 89 | const geom = {width: element.offsetWidth, height: element.offsetHeight} 90 | if (!elementsWithCustomPaint.has(element)) { 91 | listenToElementChanges(element) 92 | elementsWithCustomPaint.add(element) 93 | } 94 | return doPaint(new painter(), {'get': key => props[key] }, geom) 95 | } 96 | 97 | return Object.keys(atomicStyle).reduce((style, key) => { 98 | const value = atomicStyle[key] 99 | return value.match(regex) ? {...style, [key]: process(value)} : style 100 | }, {}); 101 | } 102 | 103 | function match(rule: AtomicStyleRule) : boolean { 104 | return !!rule.value.match(regex) 105 | } 106 | 107 | rawss.add({ 108 | resolve, match 109 | }) 110 | 111 | return { 112 | start() { 113 | rawss.start() 114 | }, 115 | 116 | stop() { 117 | rawss.pause() 118 | }, 119 | 120 | once() { 121 | rawss.once() 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/cssvar.ts: -------------------------------------------------------------------------------- 1 | import {Rawss, StyleResolver, createRawss} from './rawss' 2 | import { AtomicStyle, AtomicStyleRule } from './cssUtils'; 3 | import { escape } from 'querystring'; 4 | import { getRawComputedStyle } from './domUtils'; 5 | 6 | export interface CssVariablesOptions { 7 | /** 8 | * An existing Rawss observer. Better reuse if you have several on the page 9 | */ 10 | rawss?: Rawss 11 | 12 | /** 13 | * Root element for scoping style resolving 14 | */ 15 | rootElement?: HTMLElement 16 | 17 | /** 18 | * Prefix for variables. Default is (--), as per spec 19 | */ 20 | prefix: string 21 | } 22 | 23 | export interface CssVariablesPolyfill { 24 | /*** 25 | * Observe changes in the document, and apply CSS variable resolving as they happen 26 | */ 27 | start() : void 28 | 29 | /*** 30 | * Stop observing 31 | */ 32 | stop() : void 33 | 34 | /*** 35 | * Resolve based on currently loaded styles 36 | */ 37 | once() : void 38 | } 39 | /** 40 | * @hidden 41 | */ 42 | function escapeRegExp(str) { 43 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 44 | } 45 | 46 | /** 47 | * 48 | * @param options 49 | */ 50 | export function cssVariables(options: CssVariablesOptions = {rootElement: window.document.documentElement, rawss: null, prefix: '--'}) : CssVariablesPolyfill { 51 | let rawss = options.rawss || createRawss(options.rootElement || window.document.documentElement) 52 | const prefix = escapeRegExp(options.prefix) 53 | const DECLARE_REGEX = new RegExp(`${prefix}\\w+$`) 54 | const USE_REGEX = new RegExp(`var\\(\\s*${prefix}(\\w+)\\s*\\)`) 55 | 56 | function resolve(element: HTMLElement, getAtomicStyle: (e: HTMLElement) => AtomicStyle) : Partial { 57 | const style = getAtomicStyle(element) 58 | return Object.keys(style).reduce((newStyle, key) => { 59 | let value = style[key] 60 | let index = 0; 61 | let accumStyle = newStyle 62 | let match = null 63 | while (match = USE_REGEX.exec(value.substr(index))) { 64 | const varName = match[1] 65 | const attrName = options.prefix + varName 66 | let resolvedValue : string = style[attrName] 67 | if (!resolvedValue) { 68 | for (let e = element.parentElement; e && !resolvedValue; e = e.parentElement) { 69 | resolvedValue = getAtomicStyle(e)[attrName] 70 | } 71 | } 72 | 73 | if (!resolvedValue) { 74 | return newStyle 75 | } 76 | 77 | value = value.replace(new RegExp(`var\\(\\s*${prefix}${varName}\\s*\\)`), resolvedValue) 78 | accumStyle = {...accumStyle, ...{[key]: value}} 79 | index += match.index + match[0].length 80 | } 81 | 82 | return accumStyle 83 | }, {}) 84 | } 85 | 86 | function match(rule: AtomicStyleRule) : boolean { 87 | return !!rule.value.match(USE_REGEX) 88 | } 89 | 90 | rawss.add({ 91 | resolve, match 92 | }) 93 | 94 | return { 95 | start() { 96 | rawss.start() 97 | }, 98 | 99 | stop() { 100 | rawss.pause() 101 | }, 102 | 103 | once() { 104 | rawss.once() 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/domUtils.ts: -------------------------------------------------------------------------------- 1 | import {AtomicStyleRule, AtomicStyleDeclaration, parseDeclaration, sortRulesBySpecificFirst, parseStylesheet} from './cssUtils' 2 | export function matches(e: HTMLElement, selector: string) { 3 | return (e.matches || e.msMatchesSelector).call(e, selector) 4 | } 5 | 6 | export function doesRuleApply(element: HTMLElement, rule: AtomicStyleRule) { 7 | if (rule.selector instanceof HTMLElement) { 8 | return element === rule.selector 9 | } 10 | 11 | return matches(element, rule.selector) 12 | } 13 | 14 | export function parseInlineStyle(element: HTMLElement) : AtomicStyleRule[] { 15 | return parseDeclaration(element.hasAttribute('data-style') ? element.getAttribute('data-style') : element.getAttribute('style')).map(({name, value}) => ({name, value, selector: element})) 16 | } 17 | 18 | const loadedExternalStyles = new WeakMap() 19 | 20 | function getStylesheetText(e: HTMLElement) { 21 | switch (e.tagName) { 22 | case 'STYLE': return e.innerHTML; 23 | case 'LINK': return loadedExternalStyles.get(e) 24 | } 25 | } 26 | 27 | export function getAllRules(rootElement: HTMLElement, filter: ((e: HTMLElement) => boolean) = () => true) : AtomicStyleRule[] { 28 | const elementsWithStyleAttributes = rootElement.querySelectorAll('[style],[data-style]') 29 | const inlineStyleRules = [].filter.call(elementsWithStyleAttributes, filter).reduce((agg, element) => [...agg, ...parseInlineStyle(element)], []).reverse() 30 | const styleTags = [].map.call(document.styleSheets, s => s.ownerNode).filter((n : HTMLElement) => 31 | n && 32 | (rootElement.compareDocumentPosition(rootElement) | Node.DOCUMENT_POSITION_CONTAINS) 33 | && filter(n)) 34 | .reverse() 35 | .reduce((agg, e) => [...agg, ...parseStylesheet(getStylesheetText(e))], []) 36 | return [...inlineStyleRules, ...sortRulesBySpecificFirst(styleTags)] 37 | } 38 | 39 | export function waitForStylesToBeLoaded(rootElement: HTMLElement) { 40 | const links = rootElement.querySelectorAll('link[rel="stylesheet"]') 41 | const linksWithoutStyles = [].filter.call(links, (link: HTMLLinkElement) => !loadedExternalStyles.has(link)) 42 | return Promise.all(linksWithoutStyles.map(async (link: HTMLLinkElement) => { 43 | const response = await fetch(link.href, {headers: {'Content-Type': link.type}}) 44 | loadedExternalStyles.set(link, await response.text()) 45 | })) 46 | } 47 | 48 | export function getRawComputedStyle(rules: AtomicStyleRule[], element: HTMLElement) : {[name: string]: string} { 49 | return rules.reduce((style, rule: AtomicStyleRule) => style[rule.name] || !doesRuleApply(element, rule) ? style : {...style, [rule.name]: rule.value}, {}) 50 | } 51 | 52 | export function getMatchingElements(rootElement: HTMLElement, rule: AtomicStyleRule) : HTMLElement[] { 53 | return typeof(rule.selector) === 'string' ? [].slice.call(rootElement.querySelectorAll(rule.selector)) : [rule.selector] 54 | } -------------------------------------------------------------------------------- /src/engine.ts: -------------------------------------------------------------------------------- 1 | import { getAllRules, getMatchingElements, getRawComputedStyle, matches, waitForStylesToBeLoaded } from './domUtils'; 2 | import { AtomicStyleRule, AtomicStyleDeclaration, AtomicStyle } from './cssUtils' 3 | import * as shortid from 'shortid' 4 | 5 | /** 6 | * @ 7 | */ 8 | export interface StyleResolver { 9 | /** 10 | * @param element: Element to resolve style for 11 | * @param getAtomicStyle: receive any raw style for an element 12 | */ 13 | resolve: (element: HTMLElement, getAtomicStyle: (HTMLElement) => AtomicStyle) => Partial 14 | match: (rule: AtomicStyleRule) => boolean 15 | } 16 | 17 | /** 18 | * @hidden 19 | * @param resolver 20 | */ 21 | export function createStyleResolver({resolve, match}) : StyleResolver { 22 | return { 23 | resolve: (element, getAtomicStyle) => resolve(getAtomicStyle(element), element), 24 | match 25 | } 26 | } 27 | 28 | /** 29 | * @hidden 30 | */ 31 | export {getAllRules, getRawComputedStyle} from './domUtils' 32 | 33 | /** 34 | * @hidden 35 | */ 36 | function issueID(element: HTMLElement, attrName) { 37 | if (element.hasAttribute(attrName)) { 38 | return element.getAttribute(attrName) 39 | } 40 | 41 | const id = shortid.generate().toLowerCase() 42 | element.setAttribute(attrName, id) 43 | return id 44 | } 45 | 46 | /** 47 | * @hidden 48 | */ 49 | export type Engine = { 50 | run(resolver: StyleResolver[]) 51 | isManaging(m: MutationRecord) 52 | waitForStylesToBeLoaded() : Promise<{}> 53 | cleanup() 54 | } 55 | 56 | /** 57 | * 58 | * @hidden 59 | */ 60 | export function create(rootElement: HTMLElement) { 61 | const document = rootElement.ownerDocument 62 | const headElement = rootElement === document.documentElement ? document.head : rootElement; 63 | const styleTag = document.createElement('style') 64 | const managerID = issueID(styleTag, 'rawss') 65 | headElement.appendChild(styleTag) 66 | return { 67 | cleanup: () => { 68 | headElement.removeChild(styleTag) 69 | }, 70 | 71 | isManaging(m: MutationRecord) { 72 | return m.target === styleTag || m.target.parentElement === styleTag || m.attributeName === managerID 73 | }, 74 | 75 | waitForStylesToBeLoaded() { 76 | return new Promise(r => waitForStylesToBeLoaded(rootElement).then(() => r())) 77 | }, 78 | 79 | run: (resolvers: StyleResolver[]) => { 80 | const allRules = getAllRules(rootElement, element => element !== styleTag) 81 | const cache = new WeakMap() 82 | function issueAtomicStyle(element: HTMLElement) { 83 | if (cache.has(element)) { 84 | return cache.get(element); 85 | } 86 | 87 | const style = getRawComputedStyle(allRules, element) 88 | cache.set(element, style) 89 | return style 90 | } 91 | 92 | const styleChanges = new Map>() 93 | 94 | resolvers.forEach(resolver => { 95 | new Set( 96 | allRules.filter(resolver.match) 97 | .reduce((els, rule) => [...els, ...getMatchingElements(rootElement, rule)], [])) 98 | 99 | .forEach(element => { 100 | const newStyle = resolver.resolve(element, issueAtomicStyle); 101 | styleChanges.set(element, Object.assign({}, styleChanges.get(element), newStyle)) 102 | }) 103 | }) 104 | 105 | const styleEntries = [] 106 | styleChanges.forEach((value, key) => styleEntries.push([key, value])) 107 | 108 | const kebabCase = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase() 109 | let cssText = ` 110 | ${styleEntries.map(([element, style]) => `[${managerID}='${issueID(element, managerID)}'] {${Object.keys(style).map(name => ` 111 | ${kebabCase(name)}: ${style[name]} !important; 112 | `)}}`).join('\n')} 113 | ` 114 | styleTag.innerHTML = cssText 115 | } 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {cssVariables} from './cssvar' 2 | import {Rawss, StyleResolver} from './rawss' 3 | export { 4 | Rawss, 5 | StyleResolver, 6 | cssVariables 7 | } 8 | -------------------------------------------------------------------------------- /src/rawss.ts: -------------------------------------------------------------------------------- 1 | import {create, Engine, StyleResolver} from './engine' 2 | 3 | /** 4 | * Interface for managing CSS resolve lifecycle 5 | */ 6 | export interface Rawss { 7 | /** 8 | * Add a style resolver 9 | * @param resolver 10 | */ 11 | add(resolver: StyleResolver) 12 | 13 | /*** 14 | * Process all resolvers once 15 | */ 16 | once() 17 | 18 | /** 19 | * Observe the document for changes, and resolve when needed 20 | */ 21 | start() 22 | 23 | /** 24 | * Stop observing the docment for changes 25 | */ 26 | pause() 27 | 28 | /** 29 | * Remove attributes/elements created by Rawss 30 | */ 31 | cleanup() 32 | 33 | /** 34 | * Returns a promise that gets resolved when all styles are loaded 35 | */ 36 | settle() : Promise<{}> 37 | } 38 | 39 | export function createRawss(rootElement: HTMLElement) : Rawss { 40 | const resolvers : StyleResolver[] = [] 41 | const engine = create(rootElement) 42 | function once() { 43 | engine.run(resolvers) 44 | } 45 | 46 | function resolve(mutations: MutationRecord[]) { 47 | const relevantMutations = mutations.filter(m => !engine.isManaging(m)) 48 | if (!relevantMutations.length) { 49 | return 50 | } 51 | 52 | once() 53 | reapplyOnStylesLoaded() 54 | } 55 | 56 | function pause() { 57 | observer.disconnect() 58 | } 59 | 60 | function reapplyOnStylesLoaded() { 61 | engine.waitForStylesToBeLoaded().then(once); 62 | } 63 | 64 | const observer = new MutationObserver((mutations: MutationRecord[]) => { 65 | resolve(mutations) 66 | }) 67 | 68 | function start() { 69 | observer.observe(rootElement, { 70 | attributes: true, 71 | childList: true, 72 | subtree: true, 73 | characterData: true 74 | }) 75 | } 76 | 77 | reapplyOnStylesLoaded() 78 | 79 | return { 80 | add(resolver: StyleResolver) { 81 | resolvers.push(resolver) 82 | }, 83 | 84 | cleanup() { 85 | engine.cleanup(); 86 | }, 87 | 88 | once, start, pause, 89 | 90 | settle() { 91 | return engine.waitForStylesToBeLoaded() 92 | } 93 | } 94 | } 95 | 96 | export type StyleResolver = StyleResolver 97 | -------------------------------------------------------------------------------- /test/cssUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseDeclaration, parseStylesheet, sortRulesBySpecificFirst } from 'src/cssUtils'; 2 | import {expect} from 'chai' 3 | 4 | describe('cssUtils', () => { 5 | describe('declaration', () => { 6 | it('should parse a single entry declaration', () => { 7 | expect(parseDeclaration('nothing: something')).to.deep.equal([{name: 'nothing', value: 'something'}]) 8 | }) 9 | it('should parse a single entry declaration with trailing whitespace', () => { 10 | expect(parseDeclaration('nothing: something ')).to.deep.equal([{name: 'nothing', value: 'something'}]) 11 | }) 12 | it('should parse a single entry declaration with trailing semicolon', () => { 13 | expect(parseDeclaration('nothing: something;')).to.deep.equal([{name: 'nothing', value: 'something'}]) 14 | }) 15 | it('should parse multiple entries', () => { 16 | expect(parseDeclaration('foo:1; bar:2')).to.deep.equal([{name: 'foo', value: '1'}, {name: 'bar', value: '2'}]) 17 | }) 18 | it('should parse multiple entries with the same name in the right order', () => { 19 | expect(parseDeclaration('foo:2; foo:1')).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1'}]) 20 | }) 21 | it('should parse an !important', () => { 22 | expect(parseDeclaration('foo:2; foo:1 !important')).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1', important: true}]) 23 | }) 24 | it('should parse several !important markers', () => { 25 | expect(parseDeclaration('foo:2 !important ; foo:1 !important')).to.deep.equal([{name: 'foo', value: '2', important: true}, {name: 'foo', value: '1', important: true}]) 26 | }) 27 | it('should expand shorthand properties', () => { 28 | expect(parseDeclaration(`border: 1px solid black`)).to.deep.equal([ 29 | {name: 'border-top-width', value: '1px'}, 30 | {name: 'border-right-width', value: '1px'}, 31 | {name: 'border-bottom-width', value: '1px'}, 32 | {name: 'border-left-width', value: '1px'}, 33 | {name: 'border-top-style', value: 'solid'}, 34 | {name: 'border-right-style', value: 'solid'}, 35 | {name: 'border-bottom-style', value: 'solid'}, 36 | {name: 'border-left-style', value: 'solid'}, 37 | {name: 'border-top-color', value: 'black'}, 38 | {name: 'border-right-color', value: 'black'}, 39 | {name: 'border-bottom-color', value: 'black'}, 40 | {name: 'border-left-color', value: 'black'} 41 | ]) 42 | }) 43 | it('should treat whitespace correctly', () => { 44 | expect(parseDeclaration(` 45 | foo:2; 46 | a foo:1; 47 | `)).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1'}]) 48 | }) 49 | it('should ignore single-line comments', () => { 50 | expect(parseDeclaration(` 51 | foo:2; 52 | // comment 53 | foo:1 ; 54 | `)).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1'}]) 55 | }) 56 | it('should ignore single-line comments at the end of an acceptable line', () => { 57 | expect(parseDeclaration(` 58 | foo:2// comment 59 | ;foo:1 ; 60 | `)).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1'}]) 61 | }) 62 | it('should ignore multiple line comments', () => { 63 | expect(parseDeclaration(` 64 | foo:2/* comment 65 | ; a:b;*dfgradf */;foo:1 ; 66 | `)).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1'}]) 67 | }) 68 | it('should ignore several single and multiple line comments', () => { 69 | expect(parseDeclaration(` 70 | foo:2/* comment 71 | // abc 1241421 2345w4treddfcg a:1 /*123* 72 | ; a:b;*dfgradf */;foo:1 ; /*123 */ // bla 73 | 74 | 75 | 76 | 77 | /* * // * 78 | 79 | a a adfea 80 | // 81 | `)).to.deep.equal([{name: 'foo', value: '2'}, {name: 'foo', value: '1'}]) 82 | }) 83 | }) 84 | 85 | describe('sheet', () => { 86 | it ('should parse a single rule', () => { 87 | expect(parseStylesheet('* {foo: 1}')).to.deep.equal([{name: 'foo', value: '1', selector: '*'}]) 88 | }) 89 | it ('should parse several rules', () => { 90 | expect(parseStylesheet('* {foo: 1} abc {123: true}')).to.deep.equal([{name: 'foo', value: '1', selector: '*'}, {name: '123', value: 'true', selector: 'abc'}]) 91 | }) 92 | it ('should ignore single line comments', () => { 93 | expect(parseStylesheet(` 94 | * {foo: 1} // bla 95 | abc {123: true}`)).to.deep.equal([{name: 'foo', value: '1', selector: '*'}, {name: '123', value: 'true', selector: 'abc'}]) 96 | }) 97 | }) 98 | 99 | describe('specificity', () => { 100 | it('should sort by specificity', () => { 101 | expect(sortRulesBySpecificFirst([ 102 | {selector: '#id', name: '', value: ''}, 103 | {selector: '.class #id', name: '', value: ''} 104 | ])).to.deep.equal([ 105 | {selector: '.class #id', name: '', value: ''}, 106 | {selector: '#id', name: '', value: ''} 107 | ]) 108 | }) 109 | 110 | it('should put element specificity before selector', () => { 111 | expect(sortRulesBySpecificFirst([ 112 | {selector: '#id', name: '', value: ''}, 113 | {selector: null, name: '', value: ''} 114 | ])).to.deep.equal([ 115 | {selector: null, name: '', value: ''}, 116 | {selector: '#id', name: '', value: ''} 117 | ]) 118 | }) 119 | 120 | it('should put element specificity after selector with !important', () => { 121 | expect(sortRulesBySpecificFirst([ 122 | {selector: '#id', name: '', value: '', important: true}, 123 | {selector: null, name: '', value: ''} 124 | ])).to.deep.equal([ 125 | {selector: '#id', name: '', value: '', important: true}, 126 | {selector: null, name: '', value: ''} 127 | ]) 128 | }) 129 | 130 | it('should put element with !important before selector with !important', () => { 131 | expect(sortRulesBySpecificFirst([ 132 | {selector: '#id', name: '', value: '', important: true}, 133 | {selector: null, name: '', value: '', important: true} 134 | ])).to.deep.equal([ 135 | {selector: null, name: '', value: '', important: true}, 136 | {selector: '#id', name: '', value: '', important: true} 137 | ]) 138 | }) 139 | 140 | it('should keep order for items with same specificity', () => { 141 | expect(sortRulesBySpecificFirst([ 142 | {selector: '#id', name: '', value: ''}, 143 | {selector: '#abc', name: '', value: ''} 144 | ])).to.deep.equal([ 145 | {selector: '#id', name: '', value: ''}, 146 | {selector: '#abc', name: '', value: ''} 147 | ]) 148 | }) 149 | 150 | it('should keep order for items with same specificity and priority', () => { 151 | expect(sortRulesBySpecificFirst([ 152 | {selector: '#id', name: '', value: '', important: true}, 153 | {selector: '#abc', name: '', value: '', important: true} 154 | ])).to.deep.equal([ 155 | {selector: '#id', name: '', value: '', important: true}, 156 | {selector: '#abc', name: '', value: '', important: true} 157 | ]) 158 | }) 159 | 160 | it('should keep order for items with inline specificity', () => { 161 | expect(sortRulesBySpecificFirst([ 162 | {selector: null, name: 'abc', value: ''}, 163 | {selector: null, name: '', value: ''} 164 | ])).to.deep.equal([ 165 | {selector: null, name: 'abc', value: ''}, 166 | {selector: null, name: '', value: ''} 167 | ]) 168 | }) 169 | }) 170 | }) -------------------------------------------------------------------------------- /test/cssvars.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {launch, Page} from 'puppeteer' 3 | import * as cssvar from '../src/cssvar'; 4 | import {AtomicStyleRule, AtomicStyle} from 'src/cssUtils' 5 | 6 | const cssVariables = cssvar.cssVariables 7 | 8 | describe('CSS Variables', () => { 9 | let browser = null; 10 | let page : Page = null; 11 | before(async() => browser = await launch()) 12 | beforeEach(async () => { 13 | page = await browser.newPage(); 14 | page.on('console', (e, args) => console[e['_type']](e['_text'])) 15 | await page.addScriptTag({path: 'dist/cssvar.js'}) 16 | }); 17 | 18 | afterEach(() => page.close()) 19 | after(() => browser.close()) 20 | 21 | it('should support simple CSS variables with $$ syntax', async() => { 22 | await page.setContent(` 23 | 24 | 30 | 31 | 32 |
33 |
34 |
35 | 36 | `) 37 | const height = await page.evaluate(() => { 38 | const cssVars = cssVariables({prefix: '$$'}) 39 | cssVars.once() 40 | return (document.querySelector('#test')).offsetHeight 41 | }); 42 | 43 | expect(height).to.equal(7) 44 | }) 45 | 46 | it('should support several CSS variables with $$ syntax', async() => { 47 | await page.setContent(` 48 | 49 | 55 | 56 | 57 |
58 |
59 |
60 | 61 | `) 62 | const height = await page.evaluate(() => { 63 | const cssVars = cssVariables({prefix: '$$'}) 64 | cssVars.once() 65 | return (document.querySelector('#test')).offsetHeight 66 | }); 67 | 68 | expect(height).to.equal(10) 69 | }) 70 | 71 | it('should support whitespace inside variable usage', async() => { 72 | await page.setContent(` 73 | 74 | 80 | 81 | 82 |
83 |
84 |
85 | 86 | `) 87 | const height = await page.evaluate(() => { 88 | const cssVars = cssVariables({prefix: '$$'}) 89 | cssVars.once() 90 | return (document.querySelector('#test')).offsetHeight 91 | }); 92 | 93 | expect(height).to.equal(10) 94 | }) 95 | 96 | }) -------------------------------------------------------------------------------- /test/domUtils.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {launch, Page} from 'puppeteer' 3 | import {readFileSync} from 'fs' 4 | import * as domUtils from '../src/domUtils'; 5 | import { AtomicStyleRule } from 'src/cssUtils'; 6 | import * as express from 'express' 7 | 8 | const doesRuleApply = domUtils.doesRuleApply 9 | const parseInlineStyle = domUtils.parseInlineStyle 10 | const getAllRules = domUtils.getAllRules 11 | const getRawComputedStyle = domUtils.getRawComputedStyle 12 | const waitForStylesToBeLoaded = domUtils.waitForStylesToBeLoaded 13 | 14 | describe('domUtils', () => { 15 | let browser = null; 16 | let page : Page = null; 17 | let app = null 18 | let server = null 19 | const port = 9987 20 | 21 | before(async() => { 22 | browser = await launch({devtools: true}) 23 | app = express() 24 | app.use(express.static('test/fixtures')) 25 | server = app.listen(port) 26 | }) 27 | beforeEach(async () => { 28 | page = await browser.newPage(); 29 | await page.goto(`http://localhost:${port}/index.html`) 30 | page.on('console', (e, args) => console[e['_type']](e['_text'])) 31 | await page.addScriptTag({path: 'dist/domUtils.js'}) 32 | }); 33 | 34 | afterEach(() => page.close()) 35 | after(async () => { 36 | await browser.close(); 37 | await new Promise(r => server.close(r)) 38 | }) 39 | 40 | describe('doesRuleApply', () => { 41 | it('should return true for a simple rule', async () => { 42 | await page.setContent(` 43 | 44 |
45 | 46 | `) 47 | const applies = await page.$eval('#test', (e : HTMLElement) => doesRuleApply(e, {selector: '#test', name: '', value: ''})) 48 | expect(applies).to.equal(true) 49 | }) 50 | it('should return true for a wrong simple rule', async () => { 51 | await page.setContent(` 52 | 53 |
54 | 55 | `) 56 | const applies = await page.$eval('#test', (e : HTMLElement) => doesRuleApply(e, {selector: '#wrong', name: '', value: ''})) 57 | expect(applies).to.equal(false) 58 | }) 59 | 60 | it('should return true for a class rule', async () => { 61 | await page.setContent(` 62 | 63 |
64 | 65 | `) 66 | const applies = await page.$eval('#test', (e : HTMLElement) => doesRuleApply(e, {selector: '.bla', name: '', value: ''})) 67 | expect(applies).to.equal(true) 68 | 69 | }) 70 | 71 | it('should return false for a wrong class rule', async () => { 72 | await page.setContent(` 73 | 74 |
75 | 76 | `) 77 | const applies = await page.$eval('#test', (e : HTMLElement) => doesRuleApply(e, {selector: '.bla2', name: '', value: ''})) 78 | expect(applies).to.equal(false) 79 | 80 | }) 81 | 82 | it('should return true for self inline-style rule', async () => { 83 | await page.setContent(` 84 | 85 |
86 | 87 | `) 88 | const applies = await page.$eval('#test', (e : HTMLElement) => doesRuleApply(e, {selector: e, name: 'baz', value: 'bar'})) 89 | expect(applies).to.equal(true) 90 | }) 91 | }) 92 | 93 | describe('parse inline style', () => { 94 | it('should parse the correct rule', async() => { 95 | await page.setContent(` 96 | 97 |
98 | 99 | `) 100 | const inlineStyle: AtomicStyleRule[] = await page.$eval('#test', (e : HTMLElement) => { 101 | const rules = parseInlineStyle(e) 102 | return rules.map(r => [(r.selector).id, r.name, r.value]) 103 | }) 104 | expect(inlineStyle).to.deep.equal([['test', 'height', 'three-pixels']]) 105 | }) 106 | }) 107 | 108 | describe('get all rules', () => { 109 | it('should get rules from inline styles', async() => { 110 | await page.setContent(` 111 | 112 |
113 | 114 | `) 115 | const rules = await page.evaluate(() => getAllRules(document.documentElement).map(r => [(r.selector).id, r.name, r.value])) 116 | expect(rules).to.deep.equal([['test', 'height', 'three-pixels']]) 117 | }) 118 | it('should get rules from style tags', async() => { 119 | await page.setContent(` 120 | 121 | 122 | 123 | `) 124 | const rules = await page.evaluate(() => getAllRules(document.documentElement)) 125 | expect(rules).to.deep.equal([{selector: '*', name: 'bla', value: '--123'}]) 126 | }) 127 | 128 | it('should get rules from external style tags', async() => { 129 | await page.setContent(` 130 | 131 | 132 | 133 | `) 134 | await page.evaluate(() => waitForStylesToBeLoaded(document.documentElement)) 135 | const rules = await page.evaluate(() => getAllRules(document.documentElement)) 136 | expect(rules).to.deep.equal([{selector: '*', name: 'bla', value: '--676'}]) 137 | }).timeout(1000000) 138 | 139 | it('should filter out rules from managed style tags', async() => { 140 | await page.setContent(` 141 | 142 | 143 | 144 | 145 | `) 146 | const rules = await page.evaluate(() => getAllRules(document.documentElement, e => e.getAttribute('id') !== 'something')) 147 | expect(rules).to.deep.equal([{selector: '*', name: 'bla', value: '--123'}]) 148 | }) 149 | 150 | it('should get rules from different origins in the correct order', async() => { 151 | await page.setContent(` 152 | 153 | 154 | 155 | 156 | 157 |
158 | 159 | 160 | `) 161 | const rules = await page.evaluate(() => getAllRules(document.documentElement)) 162 | expect(rules.map(r => r.value)).to.deep.equal(['sheker', 'bar', 'abc', '--123']) 163 | }) 164 | }) 165 | 166 | describe('get raw computed style', () => { 167 | it('should return the first rule for each prop', async () => { 168 | await page.setContent(` 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 |
177 | 178 | 179 | `) 180 | 181 | const style = await page.$eval('#test', element => { 182 | const rules = getAllRules(document.documentElement) 183 | return getRawComputedStyle(rules, element) 184 | }) 185 | 186 | expect(style).to.deep.equal({height: 'sheker', foo: 'bar', width: '100px', bla: '--123'}) 187 | }) 188 | }) 189 | }) -------------------------------------------------------------------------------- /test/engine.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {launch, Page} from 'puppeteer' 3 | import * as engine from '../src/engine'; 4 | import {readFileSync} from 'fs' 5 | import {AtomicStyleRule, AtomicStyle} from 'src/cssUtils' 6 | 7 | const create = engine.create 8 | const createStyleResolver = engine.createStyleResolver 9 | 10 | describe('Engine', () => { 11 | let browser = null; 12 | let page : Page = null; 13 | before(async() => browser = await launch()) 14 | beforeEach(async () => { 15 | page = await browser.newPage(); 16 | page.on('console', (e, args) => console[e['_type']](e['_text'])) 17 | await page.addScriptTag({path: 'dist/engine.js'}) 18 | }); 19 | 20 | afterEach(() => page.close()) 21 | after(() => browser.close()) 22 | 23 | it('should register and resolve a simple rule', async() => { 24 | await page.setContent(` 25 | 26 |
27 | 28 | `) 29 | const height = await page.evaluate(() => { 30 | const proc = createStyleResolver({ 31 | match: (styleRule : AtomicStyleRule) => styleRule.value === 'three-pixels', 32 | resolve: (AtomicStyle : AtomicStyle, element: HTMLElement) => (Object.keys(AtomicStyle).reduce((style, key) => ({[key] : AtomicStyle[key] === 'three-pixels' ? '3px' : AtomicStyle[key], ...style}), {})) 33 | }) 34 | 35 | create(document.documentElement).run([proc]) 36 | return (document.querySelector('#test')).offsetHeight 37 | }); 38 | 39 | expect(height).to.equal(3) 40 | }) 41 | 42 | }) -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wix-incubator/rawss/8303093acec7f7003e5daf7c7a9f841722bd11fc/test/fixtures/index.html -------------------------------------------------------------------------------- /test/fixtures/test1.css: -------------------------------------------------------------------------------- 1 | * {bla: --676} -------------------------------------------------------------------------------- /test/fixtures/test2.css: -------------------------------------------------------------------------------- 1 | #test {height: four-pixels} -------------------------------------------------------------------------------- /test/manual/checkerboard.js: -------------------------------------------------------------------------------- 1 | // checkerboard.js 2 | class CheckerboardPainter { 3 | // inputProperties returns a list of CSS properties that this paint function gets access to 4 | static get inputProperties() { return ['--checkerboard-spacing', '--checkerboard-size']; } 5 | 6 | paint(ctx, geom, properties) { 7 | // Paint worklet uses CSS Typed OM to model the input values. 8 | // As of now, they are mostly wrappers around strings, 9 | // but will be augmented to hold more accessible data over time. 10 | const size = parseInt(properties.get('--checkerboard-size').toString()); 11 | const spacing = parseInt(properties.get('--checkerboard-spacing').toString()); 12 | const colors = ['red', 'green', 'blue']; 13 | for(let y = 0; y < geom.height/size; y++) { 14 | for(let x = 0; x < geom.width/size; x++) { 15 | ctx.fillStyle = colors[(x + y) % colors.length]; 16 | ctx.beginPath(); 17 | ctx.rect(x*(size + spacing), y*(size + spacing), size, size); 18 | ctx.fill(); 19 | } 20 | } 21 | } 22 | } 23 | 24 | registerPaint('checkerboard', CheckerboardPainter); -------------------------------------------------------------------------------- /test/manual/houdini.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 17 | 20 | 21 | 22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /test/manual/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 24 | 25 | 65 | 66 | 71 | 72 | 73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | 94 | -------------------------------------------------------------------------------- /test/manual/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const path = require('path') 4 | app.use(express.static(__dirname)) 5 | app.use('/dist', express.static(path.resolve(__dirname, '../../dist'))) 6 | app.listen(3000) -------------------------------------------------------------------------------- /test/manual/style.css: -------------------------------------------------------------------------------- 1 | [bg="green"] { 2 | --bg: green; 3 | } 4 | 5 | [bg="something"] { 6 | --bg: red; 7 | background: var(--bg); 8 | } -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register -r tsconfig-paths/register 2 | test/**/*.spec.ts -------------------------------------------------------------------------------- /test/rawss.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {launch, Page} from 'puppeteer' 3 | import * as rawss from '../src/rawss'; 4 | import * as engine from '../src/engine'; 5 | import {AtomicStyleRule, AtomicStyle} from 'src/cssUtils' 6 | import * as express from 'express' 7 | const createStyleResolver = engine.createStyleResolver 8 | const createRawss = rawss.createRawss 9 | describe('Rawcss', () => { 10 | let browser = null; 11 | let page : Page = null; 12 | let app = null 13 | let server = null 14 | const port = 9987 15 | before(async() => { 16 | browser = await launch() 17 | app = express() 18 | app.use(express.static('test/fixtures')) 19 | server = app.listen(port) 20 | }) 21 | 22 | beforeEach(async () => { 23 | page = await browser.newPage(); 24 | await page.goto(`http://localhost:${port}/index.html`) 25 | page.on('console', (e, args) => console[e['_type']](e['_text'])) 26 | await page.addScriptTag({path: 'dist/rawss.js'}) 27 | await page.addScriptTag({path: 'dist/engine.js'}) 28 | }); 29 | 30 | afterEach(() => page.close()) 31 | after(async () => { 32 | await browser.close(); 33 | await new Promise(r => server.close(r)) 34 | }) 35 | 36 | 37 | it('should register and resolve a rule using once()', async() => { 38 | await page.setContent(` 39 | 40 |
41 | 42 | `) 43 | const height = await page.evaluate(() => { 44 | const proc = createStyleResolver({ 45 | match: (styleRule : AtomicStyleRule) => styleRule.value === 'three-pixels', 46 | resolve: (AtomicStyle : AtomicStyle, element: HTMLElement) => (Object.keys(AtomicStyle).reduce((style, key) => ({[key] : AtomicStyle[key] === 'three-pixels' ? '3px' : AtomicStyle[key], ...style}), {})) 47 | }) 48 | 49 | const rawss = createRawss(document.documentElement) 50 | rawss.add(proc) 51 | rawss.once() 52 | return (document.querySelector('#test')).offsetHeight 53 | }); 54 | 55 | expect(height).to.equal(3) 56 | }) 57 | 58 | it('should register and resolve a rule using start()', async() => { 59 | const height = await page.evaluate(() => { 60 | const proc = createStyleResolver({ 61 | match: (styleRule : AtomicStyleRule) => styleRule.value === 'four-pixels', 62 | resolve: (AtomicStyle : AtomicStyle, element: HTMLElement) => (Object.keys(AtomicStyle).reduce((style, key) => ({[key] : AtomicStyle[key] === 'four-pixels' ? '4px' : AtomicStyle[key], ...style}), {})) 63 | }) 64 | 65 | const rawss = createRawss(document.documentElement) 66 | rawss.add(proc) 67 | rawss.start() 68 | document.body.innerHTML = '
' 69 | return new Promise(r => { 70 | requestAnimationFrame(() => { 71 | r((document.querySelector('#test')).offsetHeight) 72 | }) 73 | }) 74 | }); 75 | 76 | expect(height).to.equal(4) 77 | }) 78 | 79 | it('should resolve several changes in a row', async() => { 80 | const height = await page.evaluate(() => { 81 | const proc = createStyleResolver({ 82 | match: (styleRule : AtomicStyleRule) => styleRule.value === 'four-pixels', 83 | resolve: (AtomicStyle : AtomicStyle, element: HTMLElement) => (Object.keys(AtomicStyle).reduce((style, key) => ({[key] : AtomicStyle[key] === 'four-pixels' ? '4px' : AtomicStyle[key], ...style}), {})) 84 | }) 85 | 86 | const rawss = createRawss(document.documentElement) 87 | rawss.add(proc) 88 | rawss.start() 89 | document.body.innerHTML = '
' 90 | 91 | return new Promise(r => { 92 | requestAnimationFrame(() => { 93 | document.getElementById('test').setAttribute('data-style', 'height: 100px; height: four-pixels') 94 | requestAnimationFrame(() => { 95 | r((document.querySelector('#test')).offsetHeight) 96 | }) 97 | }) 98 | }) 99 | }); 100 | 101 | expect(height).to.equal(4) 102 | }) 103 | 104 | it('should resolve styles from external stylesheets', async() => { 105 | await page.setContent('') 106 | const height = await page.evaluate(() => { 107 | const proc = createStyleResolver({ 108 | match: (styleRule : AtomicStyleRule) => styleRule.value === 'four-pixels', 109 | resolve: (AtomicStyle : AtomicStyle, element: HTMLElement) => (Object.keys(AtomicStyle).reduce((style, key) => ({[key] : AtomicStyle[key] === 'four-pixels' ? '4px' : AtomicStyle[key], ...style}), {})) 110 | }) 111 | 112 | const rawss = createRawss(document.documentElement) 113 | rawss.add(proc) 114 | rawss.start() 115 | document.body.innerHTML = '
' 116 | 117 | return rawss.settle().then(() => new Promise(r => { 118 | requestAnimationFrame(() => { 119 | r((document.querySelector('#test')).offsetHeight) 120 | }) 121 | })) 122 | }); 123 | 124 | expect(height).to.equal(4) 125 | }) 126 | 127 | it('should not resolve changes once pause() is called', async() => { 128 | const height = await page.evaluate(() => { 129 | const proc = createStyleResolver({ 130 | match: (styleRule : AtomicStyleRule) => styleRule.value === 'four-pixels', 131 | resolve: (AtomicStyle : AtomicStyle, element: HTMLElement) => (Object.keys(AtomicStyle).reduce((style, key) => ({[key] : AtomicStyle[key] === 'four-pixels' ? '4px' : AtomicStyle[key], ...style}), {})) 132 | }) 133 | 134 | const rawss = createRawss(document.documentElement) 135 | rawss.add(proc) 136 | rawss.start() 137 | document.body.innerHTML = '
' 138 | 139 | return new Promise(r => { 140 | requestAnimationFrame(() => { 141 | rawss.pause() 142 | document.getElementById('test').setAttribute('style', 'height: 100px; height: four-pixels') 143 | requestAnimationFrame(() => { 144 | r((document.querySelector('#test')).offsetHeight) 145 | }) 146 | }) 147 | }) 148 | }); 149 | 150 | expect(height).to.equal(100) 151 | }) 152 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": ["src/**/*.ts", "ts/refs.d.ts"], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es6", 6 | "baseUrl": ".", 7 | "declaration": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chai": "registry:npm/chai#3.5.0+20160723033700", 4 | "lodash": "registry:npm/lodash#4.0.0+20161015015725" 5 | }, 6 | "globalDependencies": { 7 | "mocha": "registry:dt/mocha#2.2.5+20170311011848" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const TypedocWebpackPlugin = require('typedoc-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | index: path.resolve('src/index.ts'), 9 | engine: path.resolve('src/engine.ts'), 10 | rawss: path.resolve('src/rawss.ts'), 11 | cssvar: path.resolve('src/cssvar.ts'), 12 | csspaint: path.resolve('src/csspaint.ts'), 13 | domUtils: path.resolve('src/domUtils.ts') 14 | }, 15 | output: { 16 | filename: '[name].js', 17 | path: path.join(__dirname, 'dist'), 18 | libraryTarget: 'umd' 19 | }, 20 | devtool: 'source-map', 21 | resolve: { 22 | // Add '.ts' and '.tsx' as a resolvable extension. 23 | extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'] 24 | }, 25 | plugins: [ 26 | new TypedocWebpackPlugin({ 27 | readme: 'none', out: '../docs' 28 | }, ['./src/cssvar.ts', './src/rawss.ts', './src/index.ts']) 29 | ], 30 | module: { 31 | loaders: [ 32 | // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' 33 | { test: /src\/.*\.ts$/, loader: 'ts-loader?'+JSON.stringify({ 34 | compilerOptions: { 35 | lib: ['es6', 'ES2015.Promise', 'DOM'], 36 | types: ['node', 'mocha'], 37 | target: 'es5' 38 | } 39 | })} 40 | ] 41 | }, 42 | externals: { 43 | lodash : 'lodash' 44 | } 45 | }; 46 | --------------------------------------------------------------------------------