├── .prettierignore ├── website ├── .gitignore ├── .env ├── public │ ├── favicon.ico │ ├── StyleEditor.png │ ├── manifest.json │ └── index.html ├── src │ ├── index.js │ └── App.js └── package.json ├── .gitignore ├── docs ├── favicon.ico ├── StyleEditor.png ├── manifest.json ├── asset-manifest.json ├── precache-manifest.77b6388920be8bff4320f0bd14c1987b.js ├── service-worker.js ├── static │ └── js │ │ └── runtime~main.4292b16b.js └── index.html ├── src ├── utils │ ├── shorten.js │ ├── clean.js │ ├── hasSelection.js │ ├── cls.js │ ├── COMMON.js │ ├── analyze.js │ ├── stringify.js │ ├── identify.js │ ├── unignore.js │ ├── prettify.js │ ├── modify.js │ ├── stylize.js │ ├── ignore.js │ └── validate.js ├── index.js └── components │ ├── Alert.js │ ├── Checkbox.js │ ├── StyleEditor.stories.js │ ├── Comment.js │ ├── Area.js │ ├── Declaration.js │ ├── Rule.js │ └── StyleEditor.js ├── .storybook └── main.js ├── lib ├── utils │ ├── shorten.js │ ├── clean.js │ ├── hasSelection.js │ ├── cls.js │ ├── analyze.js │ ├── COMMON.js │ ├── modify.js │ ├── stylize.js │ ├── stringify.js │ ├── identify.js │ ├── unignore.js │ ├── prettify.js │ ├── ignore.js │ └── validate.js ├── components │ ├── Alert.js │ ├── Checkbox.js │ ├── Comment.js │ ├── Area.js │ ├── Declaration.js │ └── Rule.js └── index.js ├── LICENSE ├── tests ├── ignore.test.js ├── ignore.test.html ├── validate.test.html ├── parse.test.html ├── database.test.html └── validate.test.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── scripts └── collect-database.js ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | lib 2 | docs -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | src/tmp -------------------------------------------------------------------------------- /website/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | GENERATE_SOURCEMAP=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .idea 3 | .vscode 4 | .DS_Store 5 | node_modules 6 | package-lock.json -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aurelain/react-style-editor/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/StyleEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aurelain/react-style-editor/HEAD/docs/StyleEditor.png -------------------------------------------------------------------------------- /src/utils/shorten.js: -------------------------------------------------------------------------------- 1 | export default (blob, count) => { 2 | return blob.substr(0, count) + '…'; 3 | }; 4 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aurelain/react-style-editor/HEAD/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/StyleEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aurelain/react-style-editor/HEAD/website/public/StyleEditor.png -------------------------------------------------------------------------------- /src/utils/clean.js: -------------------------------------------------------------------------------- 1 | const clean = (blob) => { 2 | return blob.trim().replace(/\s+/g, ' '); 3 | }; 4 | 5 | export default clean; 6 | -------------------------------------------------------------------------------- /src/utils/hasSelection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | const hasSelection = () => !window.getSelection().isCollapsed; 5 | 6 | export default hasSelection; 7 | -------------------------------------------------------------------------------- /website/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /src/utils/cls.js: -------------------------------------------------------------------------------- 1 | /* 2 | A tiny alternative to `classnames`, `clsx` and `obj-str`. 3 | */ 4 | const cls = (...args) => { 5 | const o = []; 6 | for (const k of args) k && o.push(k); 7 | return o.join(' ') || null; 8 | }; 9 | 10 | export default cls; 11 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'], 3 | framework: { 4 | name: '@storybook/react-webpack5', 5 | options: {}, 6 | }, 7 | docs: { 8 | autodocs: true, 9 | }, 10 | }; 11 | export default config; -------------------------------------------------------------------------------- /lib/utils/shorten.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _default = function _default(blob, count) { 9 | return blob.substr(0, count) + '…'; 10 | }; 11 | 12 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/utils/clean.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var clean = function clean(blob) { 9 | return blob.trim().replace(/\s+/g, ' '); 10 | }; 11 | 12 | var _default = clean; 13 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/utils/COMMON.js: -------------------------------------------------------------------------------- 1 | export const RULE = 'rule'; 2 | export const ATRULE = 'atrule'; 3 | export const DECLARATION = 'declaration'; 4 | export const COMMENT = 'comment'; 5 | export const SLASH_SUBSTITUTE = '!'; 6 | export const AFTER_BEGIN = 'afterBegin'; 7 | export const BEFORE = 'before'; 8 | export const AFTER = 'after'; 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import StyleEditor from './components/StyleEditor'; 2 | import analyze from './utils/analyze'; 3 | import parse from './utils/parse'; 4 | import stringify from './utils/stringify'; 5 | import prettify from './utils/prettify'; 6 | 7 | export default StyleEditor; 8 | export {analyze, parse, stringify, prettify}; 9 | -------------------------------------------------------------------------------- /src/utils/analyze.js: -------------------------------------------------------------------------------- 1 | import parse from './parse.js'; 2 | import validate from './validate.js'; 3 | import identify from './identify.js'; 4 | 5 | /** 6 | * 7 | */ 8 | const analyze = (css) => { 9 | const rules = parse(css); 10 | validate(rules); 11 | identify(rules); 12 | return rules; 13 | }; 14 | 15 | export default analyze; 16 | -------------------------------------------------------------------------------- /lib/utils/hasSelection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | /** 9 | * 10 | */ 11 | var hasSelection = function hasSelection() { 12 | return !window.getSelection().isCollapsed; 13 | }; 14 | 15 | var _default = hasSelection; 16 | exports["default"] = _default; -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /website/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.js": "/react-style-editor/static/js/main.c4875821.chunk.js", 4 | "runtime~main.js": "/react-style-editor/static/js/runtime~main.4292b16b.js", 5 | "static/js/2.a7160d20.chunk.js": "/react-style-editor/static/js/2.a7160d20.chunk.js", 6 | "index.html": "/react-style-editor/index.html", 7 | "precache-manifest.77b6388920be8bff4320f0bd14c1987b.js": "/react-style-editor/precache-manifest.77b6388920be8bff4320f0bd14c1987b.js", 8 | "service-worker.js": "/react-style-editor/service-worker.js" 9 | } 10 | } -------------------------------------------------------------------------------- /docs/precache-manifest.77b6388920be8bff4320f0bd14c1987b.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "1e96af68ab12e8d20eb35844516f4acb", 4 | "url": "/react-style-editor/index.html" 5 | }, 6 | { 7 | "revision": "98618ec1407e7b47ffab", 8 | "url": "/react-style-editor/static/js/2.a7160d20.chunk.js" 9 | }, 10 | { 11 | "revision": "234c9c302a588951ece7", 12 | "url": "/react-style-editor/static/js/main.c4875821.chunk.js" 13 | }, 14 | { 15 | "revision": "ee5b4c8642606efc13c1", 16 | "url": "/react-style-editor/static/js/runtime~main.4292b16b.js" 17 | } 18 | ]); -------------------------------------------------------------------------------- /lib/utils/cls.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | /* 9 | A tiny alternative to `classnames`, `clsx` and `obj-str`. 10 | */ 11 | var cls = function cls() { 12 | var o = []; 13 | 14 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 15 | args[_key] = arguments[_key]; 16 | } 17 | 18 | for (var _i = 0, _args = args; _i < _args.length; _i++) { 19 | var k = _args[_i]; 20 | k && o.push(k); 21 | } 22 | 23 | return o.join(' ') || null; 24 | }; 25 | 26 | var _default = cls; 27 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/utils/analyze.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _parse = _interopRequireDefault(require("./parse.js")); 9 | 10 | var _validate = _interopRequireDefault(require("./validate.js")); 11 | 12 | var _identify = _interopRequireDefault(require("./identify.js")); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 15 | 16 | /** 17 | * 18 | */ 19 | var analyze = function analyze(css) { 20 | var rules = (0, _parse["default"])(css); 21 | (0, _validate["default"])(rules); 22 | (0, _identify["default"])(rules); 23 | return rules; 24 | }; 25 | 26 | var _default = analyze; 27 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/utils/COMMON.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.SLASH_SUBSTITUTE = exports.RULE = exports.DECLARATION = exports.COMMENT = exports.BEFORE = exports.ATRULE = exports.AFTER_BEGIN = exports.AFTER = void 0; 7 | var RULE = 'rule'; 8 | exports.RULE = RULE; 9 | var ATRULE = 'atrule'; 10 | exports.ATRULE = ATRULE; 11 | var DECLARATION = 'declaration'; 12 | exports.DECLARATION = DECLARATION; 13 | var COMMENT = 'comment'; 14 | exports.COMMENT = COMMENT; 15 | var SLASH_SUBSTITUTE = '!'; 16 | exports.SLASH_SUBSTITUTE = SLASH_SUBSTITUTE; 17 | var AFTER_BEGIN = 'afterBegin'; 18 | exports.AFTER_BEGIN = AFTER_BEGIN; 19 | var BEFORE = 'before'; 20 | exports.BEFORE = BEFORE; 21 | var AFTER = 'after'; 22 | exports.AFTER = AFTER; -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import stylize from '../utils/stylize'; 4 | 5 | const classes = stylize('Alert', { 6 | root: { 7 | width: 12, 8 | height: 12, 9 | fill: '#d7b600', 10 | verticalAlign: -2, 11 | marginLeft: 4, 12 | }, 13 | }); 14 | 15 | const stopPropagation = (event) => event.stopPropagation(); 16 | 17 | export default () => ( 18 | 19 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aurelain 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 | -------------------------------------------------------------------------------- /lib/components/Alert.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _stylize = _interopRequireDefault(require("../utils/stylize")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | var classes = (0, _stylize["default"])('Alert', { 15 | root: { 16 | width: 12, 17 | height: 12, 18 | fill: '#d7b600', 19 | verticalAlign: -2, 20 | marginLeft: 4 21 | } 22 | }); 23 | 24 | var stopPropagation = function stopPropagation(event) { 25 | return event.stopPropagation(); 26 | }; 27 | 28 | var _default = function _default() { 29 | return /*#__PURE__*/_react["default"].createElement("svg", { 30 | viewBox: "0 0 12 12", 31 | className: classes.root, 32 | onClick: stopPropagation 33 | }, /*#__PURE__*/_react["default"].createElement("path", { 34 | d: "M6 0a1 1 0 0 1 .89.54l5 9.6A1 1 0 0 1 11 11.6H1a1 1 0 0 1-.89-1.46l5-9.6A1 1 0 0 1 6 0z m-.25 8a.75.75 0 0 0-.75.75v.5c0 .41.34.75.75.75h.5c.41 0 .75-.34.75-.75v-.5A.75.75 0 0 0 6.25 8h-.5z M7 3.7a1 1 0 1 0-2 0v2.6a1 1 0 1 0 2 0V3.7z" 35 | })); 36 | }; 37 | 38 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/utils/stringify.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | */ 4 | 5 | import {ATRULE, COMMENT, DECLARATION, RULE} from './COMMON.js'; 6 | 7 | /** 8 | * 9 | */ 10 | const stringify = (kids) => { 11 | return flatten(kids).join(''); 12 | }; 13 | 14 | /** 15 | * 16 | */ 17 | const flatten = (kids, accumulator = []) => { 18 | for (const item of kids) { 19 | switch (item.type) { 20 | case ATRULE: 21 | case RULE: 22 | accumulator.push(item.selector + (item.hasBraceBegin ? '{' : '')); 23 | if (item.kids && item.kids.length) { 24 | flatten(item.kids, accumulator); 25 | } 26 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : '')); 27 | break; 28 | case DECLARATION: 29 | accumulator.push( 30 | item.property + (item.hasColon ? ':' : '') + item.value + (item.hasSemicolon ? ';' : '') 31 | ); 32 | break; 33 | case COMMENT: 34 | accumulator.push(item.prefix + '/*' + item.content + (item.hasSlashEnd ? '*/' : '')); 35 | break; 36 | default: 37 | // nothing 38 | } 39 | } 40 | return accumulator; 41 | }; 42 | 43 | export default stringify; 44 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | Object.defineProperty(exports, "analyze", { 7 | enumerable: true, 8 | get: function get() { 9 | return _analyze["default"]; 10 | } 11 | }); 12 | exports["default"] = void 0; 13 | Object.defineProperty(exports, "parse", { 14 | enumerable: true, 15 | get: function get() { 16 | return _parse["default"]; 17 | } 18 | }); 19 | Object.defineProperty(exports, "prettify", { 20 | enumerable: true, 21 | get: function get() { 22 | return _prettify["default"]; 23 | } 24 | }); 25 | Object.defineProperty(exports, "stringify", { 26 | enumerable: true, 27 | get: function get() { 28 | return _stringify["default"]; 29 | } 30 | }); 31 | 32 | var _StyleEditor = _interopRequireDefault(require("./components/StyleEditor")); 33 | 34 | var _analyze = _interopRequireDefault(require("./utils/analyze")); 35 | 36 | var _parse = _interopRequireDefault(require("./utils/parse")); 37 | 38 | var _stringify = _interopRequireDefault(require("./utils/stringify")); 39 | 40 | var _prettify = _interopRequireDefault(require("./utils/prettify")); 41 | 42 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 43 | 44 | var _default = _StyleEditor["default"]; 45 | exports["default"] = _default; -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "homepage": "http://aurelain.github.io/react-style-editor", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@material-ui/core": "^3.9.3", 8 | "@material-ui/styles": "^3.0.0-alpha.10", 9 | "clsx": "^1.0.4", 10 | "react": "^16.8.6", 11 | "react-dom": "^16.8.6", 12 | "react-style-editor": "^0.4.0" 13 | }, 14 | "devDependencies": { 15 | "prettier": "^2.4.1", 16 | "react-scripts": "3.0.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "prettier": { 28 | "tabWidth": 4, 29 | "singleQuote": true, 30 | "printWidth": 120, 31 | "bracketSpacing": false 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/ignore.test.js: -------------------------------------------------------------------------------- 1 | import parse from '../src/utils/parse.js'; 2 | import ignore from '../src/utils/ignore.js'; 3 | import stringify from '../src/utils/stringify.js'; 4 | import identify from '../src/utils/identify.js'; 5 | 6 | const tests = [ 7 | // ----------------------------------------------------------------------------------------------------------------- 8 | [ 9 | 1, 10 | { 11 | rulesBlob: ` div { color : red ; /* hello */ background : blue ; } `, 12 | idToIgnore: `div{`, 13 | }, 14 | `/* div { color : red ; !* hello *! background : blue ; }*/ `, 15 | ], 16 | ]; 17 | 18 | const normalTests = []; 19 | const importantTests = []; 20 | tests.forEach((item) => { 21 | if (item[0] === 1) { 22 | normalTests.push(item); 23 | } else if (item[0] === 2) { 24 | importantTests.push(item); 25 | } 26 | }); 27 | 28 | const usedTests = importantTests.length ? importantTests : normalTests; 29 | usedTests.forEach((item) => { 30 | it(item[1].rulesBlob, () => { 31 | const {rulesBlob, idToIgnore} = item[1]; 32 | const rules = parse(rulesBlob); 33 | identify(rules); 34 | const actual = ignore(rules, idToIgnore); 35 | const actualBlob = stringify(actual); 36 | return expect(actualBlob).toEqual(item[2]); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /docs/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/react-style-editor/precache-manifest.77b6388920be8bff4320f0bd14c1987b.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/react-style-editor/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^/]+\.[^/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.4.0] - 2021-12-28 8 | ### Fixed 9 | - The one-time temporary iframe used for internal CSS validation is now removed from the DOM. 10 | - Selecting texts was sometimes triggering edit-mode. 11 | 12 | ### Changed 13 | - All comments can now be toggled ([#2](https://github.com/Aurelain/react-style-editor/issues/2)). 14 | - The copy command is no longer intercepted when a selection exists ([#3](https://github.com/Aurelain/react-style-editor/issues/3)). 15 | 16 | ## [0.3.0] - 2021-11-25 17 | ### Added 18 | - Protection against large base64. 19 | 20 | ### Fixed 21 | - Since Chrome 80, the CSS browser validation was wrong. 22 | - Writing inside `#foo{}` was producing `#foo{b;} #foo{ba;b;} #foo{bar;ba;b;}` ([#1](https://github.com/Aurelain/react-style-editor/issues/1)). 23 | 24 | ### Changed 25 | - `onChange` no longer gets called automatically by mount. 26 | - Implemented some minor visual changes. 27 | - Improved textarea autosize. 28 | 29 | 30 | [0.4.0]: https://github.com/Aurelain/react-style-editor/releases/tag/v0.4.0 31 | [0.3.0]: https://github.com/Aurelain/react-style-editor/releases/tag/v0.3.0 32 | [0.2.0]: https://github.com/Aurelain/react-style-editor/releases/tag/v0.2.0 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Thank you for your help in making RSE better! 4 | 5 | Please note the project is currently in slow-maintenance mode until the end of 2023. 6 | 7 | 8 | ### Development 9 | 10 | Unfortunately, there is no proper development environment setup. 11 | 12 | Back in v0.1, the setup involved using storybook: 13 | 14 | ```bash 15 | npm install 16 | npm run storybook 17 | ``` 18 | Nowadays, this will most likely throw some errors. 19 | You're better off creating an environment of your own. 20 | 21 | ### Pull requests 22 | 23 | 1. Please avoid unnecessary changes, refactoring or changing coding styles as part of your change. 24 | 2. Do not create dependencies with other packages, unless you have pre-approved this intention. 25 | 3. Ensure that **prettier** has formatted the code. 26 | 4. Follow the coding conventions from the surrounding code. 27 | 28 | ### Contributions license 29 | 30 | When contributing the code you confirm that: 31 | 32 | 1. Your contribution is created by you. 33 | 2. You have the right to submit it under the MIT license. 34 | 3. You understand and agree that your contribution is public, will be stored indefinitely, can be redistributed as the part of RSE under MIT license, modified or completely removed from RSE. 35 | 4. You grant irrevocable MIT license to use your contribution as part of RSE. 36 | 6. Unless you request otherwise, you can be mentioned as the author of the contribution in the RSE change log. 37 | -------------------------------------------------------------------------------- /docs/static/js/runtime~main.4292b16b.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,i,l=r[0],f=r[1],a=r[2],p=0,s=[];p { 13 | for (const item of list) { 14 | let id; 15 | switch (item.type) { 16 | case ATRULE: 17 | case RULE: 18 | id = item.selector.trim() + (item.hasBraceBegin ? '{' : '') + (item.hasSemicolon ? ';' : ''); 19 | break; 20 | case DECLARATION: 21 | id = 22 | item.property.trim() + 23 | (item.hasColon ? ':' : '') + 24 | item.value.substr(0, MAX_CHARS) + 25 | (item.hasSemicolon ? ';' : ''); 26 | break; 27 | case COMMENT: 28 | id = '/*' + item.content.substr(0, MAX_CHARS) + '*/'; 29 | break; 30 | default: 31 | // nothing 32 | } 33 | if (id in usedIds) { 34 | usedIds[id]++; 35 | item.id = id + usedIds[id]; 36 | } else { 37 | usedIds[id] = 1; 38 | item.id = id; 39 | } 40 | if (item.kids && item.kids.length) { 41 | identify(item.kids, usedIds); 42 | } 43 | } 44 | }; 45 | 46 | export default identify; 47 | -------------------------------------------------------------------------------- /src/utils/unignore.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | */ 4 | 5 | import {RULE, COMMENT, SLASH_SUBSTITUTE} from './COMMON.js'; 6 | import modify from './modify.js'; 7 | import stringify from './stringify.js'; 8 | 9 | /** 10 | * 11 | */ 12 | const unignore = (rules, id) => { 13 | const {freshRules, freshNode} = modify(rules, id, {}); // blank change to get the `freshNode` 14 | if (freshNode.type === COMMENT) { 15 | unignoreComment(freshNode); 16 | } else { 17 | unignoreKids(freshNode.kids); 18 | } 19 | return stringify(freshRules); 20 | }; 21 | 22 | /** 23 | * 24 | */ 25 | const unignoreComment = (node) => { 26 | const prefix = node.prefix; // backup 27 | const content = node.content 28 | .split(SLASH_SUBSTITUTE + '*') 29 | .join('/*') 30 | .split('*' + SLASH_SUBSTITUTE) 31 | .join('*/'); 32 | for (const key in node) { 33 | delete node[key]; 34 | } 35 | Object.assign(node, { 36 | type: RULE, // could also be ATRULE or DECLARATION, because it's just temporary 37 | selector: prefix + content, 38 | }); 39 | }; 40 | 41 | /** 42 | * 43 | */ 44 | const unignoreKids = (kids) => { 45 | for (const item of kids) { 46 | if (item.type === COMMENT) { 47 | unignoreComment(item); 48 | } else { 49 | if (item.kids && item.kids.length) { 50 | unignoreKids(item.kids); 51 | } 52 | } 53 | } 54 | }; 55 | 56 | export default unignore; 57 | -------------------------------------------------------------------------------- /src/utils/prettify.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | */ 4 | 5 | import {ATRULE, COMMENT, DECLARATION, RULE} from './COMMON'; 6 | import clean from './clean'; 7 | 8 | /** 9 | * 10 | */ 11 | const prettify = (kids) => { 12 | return flatten(kids).join(''); 13 | }; 14 | 15 | /** 16 | * 17 | */ 18 | const flatten = (kids, accumulator = [], indent = '') => { 19 | for (const item of kids) { 20 | switch (item.type) { 21 | case ATRULE: 22 | case RULE: 23 | const {type, kids, selector, hasBraceBegin, hasBraceEnd, hasSemicolon} = item; 24 | if (!kids.length && !selector.trim() && !hasBraceBegin && !hasBraceEnd && !hasSemicolon) { 25 | continue; 26 | } 27 | accumulator.push(indent + clean(selector) + ' {\r\n'); 28 | if (kids && kids.length) { 29 | flatten(kids, accumulator, indent + ' '); 30 | } 31 | if (type === ATRULE && !hasBraceBegin) { 32 | accumulator.push(';\r\n'); 33 | } else { 34 | accumulator.push(indent + '}\r\n'); 35 | } 36 | break; 37 | case DECLARATION: 38 | if (!item.hasColon && !item.property.trim()) { 39 | continue; 40 | } 41 | accumulator.push(indent + clean(item.property) + ': ' + clean(item.value) + ';\r\n'); 42 | break; 43 | case COMMENT: 44 | accumulator.push(indent + '/*' + item.content + '*/\r\n'); 45 | break; 46 | default: 47 | // nothing 48 | } 49 | } 50 | return accumulator; 51 | }; 52 | 53 | export default prettify; 54 | -------------------------------------------------------------------------------- /scripts/collect-database.js: -------------------------------------------------------------------------------- 1 | /* 2 | This script merges multiple "*.css" files into a single js file to serve as database for tests. 3 | */ 4 | 5 | /** 6 | * You can place any css files in the input folder. 7 | * A good sample can be found here: https://github.com/w3c/css-validator-testsuite 8 | * 9 | * IMPORTANT: The above repository contains 3 suites that are obscenely large (>200MB), 10 | * so you might want to remove them before running the script. They are: 11 | * - properties/positive/background 12 | * - properties/positive/background-position 13 | * - properties/positive/border-color 14 | */ 15 | const INPUT_DIR = '../css-validator-testsuite-master/'; 16 | const OUTPUT_FILE = 'tests/database.js'; 17 | 18 | const path = require('path'); 19 | const fs = require('fs'); 20 | 21 | const output = []; 22 | 23 | const fromDir = (startPath, filter) => { 24 | if (fs.existsSync(startPath)) { 25 | const fileNames = fs.readdirSync(startPath); 26 | for (const fileName of fileNames) { 27 | const filePath = path.join(startPath, fileName); 28 | const stat = fs.lstatSync(filePath); 29 | if (stat.isDirectory()) { 30 | fromDir(filePath, filter); // recursion 31 | } else if (fileName.toLowerCase().indexOf(filter) >= 0) { 32 | //------------------------------------------------------------- 33 | output.push(JSON.stringify(fs.readFileSync(filePath, 'utf8'))); 34 | //------------------------------------------------------------- 35 | } 36 | } 37 | } 38 | }; 39 | 40 | fromDir(INPUT_DIR, '.css'); 41 | if (fs.existsSync(OUTPUT_FILE)) { 42 | fs.unlinkSync(OUTPUT_FILE); 43 | } 44 | fs.writeFileSync(OUTPUT_FILE, `export default [\r\n${output.join(',\r\n')}];`); 45 | -------------------------------------------------------------------------------- /src/utils/modify.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | */ 4 | 5 | /** 6 | * 7 | */ 8 | const modify = (nodeList, nodeId, payload) => { 9 | const ancestors = findAncestors(nodeList, nodeId); 10 | let oldNode = ancestors.pop(); 11 | let node = Object.assign({}, oldNode, payload); 12 | const originalNode = oldNode; 13 | const freshNode = node; 14 | for (let i = ancestors.length - 1; i >= 0; i--) { 15 | const oldParent = ancestors[i]; 16 | const parent = (ancestors[i] = Object.assign({}, oldParent)); 17 | const kids = (parent.kids = parent.kids.slice()); 18 | const index = kids.indexOf(oldNode); 19 | kids[index] = node; 20 | oldNode = oldParent; 21 | node = parent; 22 | } 23 | return { 24 | freshRules: node.kids, 25 | originalNode, 26 | freshNode, 27 | parentNode: ancestors[ancestors.length - 1], 28 | }; 29 | }; 30 | 31 | const findAncestors = (nodeList, nodeId) => { 32 | const path = [{kids: nodeList}]; 33 | const indexes = []; 34 | let level = 0; 35 | let i = 0; 36 | let kids = nodeList; 37 | while (true) { 38 | const node = kids[i]; 39 | if (!node) { 40 | level--; 41 | path.pop(); 42 | if (level < 0) { 43 | break; 44 | } 45 | i = indexes[level] + 1; 46 | kids = path[level].kids; 47 | } else { 48 | if (node.id === nodeId) { 49 | path.push(node); 50 | return path; 51 | } 52 | if (node.kids) { 53 | path.push(node); 54 | indexes[level] = i; 55 | level++; 56 | i = 0; 57 | kids = node.kids; 58 | } else { 59 | i++; 60 | } 61 | } 62 | } 63 | return null; 64 | }; 65 | 66 | export default modify; 67 | -------------------------------------------------------------------------------- /lib/utils/modify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | /* 9 | 10 | */ 11 | 12 | /** 13 | * 14 | */ 15 | var modify = function modify(nodeList, nodeId, payload) { 16 | var ancestors = findAncestors(nodeList, nodeId); 17 | var oldNode = ancestors.pop(); 18 | var node = Object.assign({}, oldNode, payload); 19 | var originalNode = oldNode; 20 | var freshNode = node; 21 | 22 | for (var i = ancestors.length - 1; i >= 0; i--) { 23 | var oldParent = ancestors[i]; 24 | var parent = ancestors[i] = Object.assign({}, oldParent); 25 | var kids = parent.kids = parent.kids.slice(); 26 | var index = kids.indexOf(oldNode); 27 | kids[index] = node; 28 | oldNode = oldParent; 29 | node = parent; 30 | } 31 | 32 | return { 33 | freshRules: node.kids, 34 | originalNode: originalNode, 35 | freshNode: freshNode, 36 | parentNode: ancestors[ancestors.length - 1] 37 | }; 38 | }; 39 | 40 | var findAncestors = function findAncestors(nodeList, nodeId) { 41 | var path = [{ 42 | kids: nodeList 43 | }]; 44 | var indexes = []; 45 | var level = 0; 46 | var i = 0; 47 | var kids = nodeList; 48 | 49 | while (true) { 50 | var node = kids[i]; 51 | 52 | if (!node) { 53 | level--; 54 | path.pop(); 55 | 56 | if (level < 0) { 57 | break; 58 | } 59 | 60 | i = indexes[level] + 1; 61 | kids = path[level].kids; 62 | } else { 63 | if (node.id === nodeId) { 64 | path.push(node); 65 | return path; 66 | } 67 | 68 | if (node.kids) { 69 | path.push(node); 70 | indexes[level] = i; 71 | level++; 72 | i = 0; 73 | kids = node.kids; 74 | } else { 75 | i++; 76 | } 77 | } 78 | } 79 | 80 | return null; 81 | }; 82 | 83 | var _default = modify; 84 | exports["default"] = _default; -------------------------------------------------------------------------------- /website/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React Style Editor 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | React Style Editor
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-style-editor", 3 | "version": "0.4.0", 4 | "description": "A React component that displays and edits CSS, similar to the browser's DevTools.", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/Aurelain/react-style-editor.git" 9 | }, 10 | "author": "Aurelain ", 11 | "license": "MIT", 12 | "homepage": "https://github.com/Aurelain/react-style-editor/", 13 | "keywords": [ 14 | "react", 15 | "style", 16 | "editor", 17 | "CSS", 18 | "rules", 19 | "declarations", 20 | "comment", 21 | "checkbox", 22 | "validator" 23 | ], 24 | "scripts": { 25 | "storybook": "storybook dev -c .storybook", 26 | "lib": "rm -rf lib && mkdir lib && babel src -d lib", 27 | "collect-database": "node scripts/collect-database.js" 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.4.4", 31 | "@babel/core": "^7.4.4", 32 | "@babel/plugin-proposal-class-properties": "^7.4.4", 33 | "@babel/preset-env": "^7.4.4", 34 | "@babel/preset-react": "^7.0.0", 35 | "@storybook/react": "^7.0.22", 36 | "@storybook/react-webpack5": "^7.0.22", 37 | "babel-loader": "^8.0.5", 38 | "prettier": "^2.4.1", 39 | "react": "^16.8.6", 40 | "react-dom": "^16.8.6", 41 | "storybook": "^7.0.22" 42 | }, 43 | "files": [ 44 | "lib", 45 | "src" 46 | ], 47 | "babel": { 48 | "presets": [ 49 | "@babel/preset-env", 50 | "@babel/preset-react" 51 | ], 52 | "plugins": [ 53 | "@babel/plugin-proposal-class-properties" 54 | ] 55 | }, 56 | "prettier": { 57 | "tabWidth": 4, 58 | "singleQuote": true, 59 | "printWidth": 120, 60 | "bracketSpacing": false 61 | }, 62 | "bugs": { 63 | "url": "https://github.com/Aurelain/react-style-editor/issues" 64 | }, 65 | "directories": { 66 | "lib": "lib", 67 | "test": "tests" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/stylize.js: -------------------------------------------------------------------------------- 1 | /* 2 | A quick-and-dirty simulation of JSS. 3 | */ 4 | 5 | const PREFIX = 'rse'; 6 | const SEPARATOR = '-'; 7 | const dashConverter = (match) => '-' + match.toLowerCase(); 8 | 9 | let registry = {}; 10 | let cssCollection = []; 11 | let style = document.createElement('style'); 12 | let count = 0; 13 | 14 | /** 15 | * 16 | */ 17 | const stylize = (name, classes) => { 18 | const output = {}; 19 | const css = collect(name, classes, output); 20 | const index = registry[name]; 21 | if (index === undefined) { 22 | registry[name] = cssCollection.push(css) - 1; 23 | } else { 24 | cssCollection[index] = css; 25 | } 26 | return output; 27 | }; 28 | 29 | /** 30 | * 31 | */ 32 | const collect = (name, classes, accumulator = {}) => { 33 | let css = ''; 34 | for (const selector in classes) { 35 | const block = classes[selector]; 36 | const className = PREFIX + SEPARATOR + name + SEPARATOR + selector; 37 | css += '.' + className + '{\r\n'; 38 | const nested = {}; 39 | for (const property in block) { 40 | const value = block[property]; 41 | if (property.indexOf('&') >= 0) { 42 | // this is in fact a nested selector, not a declaration 43 | const resolved = property.replace(/&/g, selector); 44 | nested[resolved] = value; 45 | continue; 46 | } 47 | const cssProperty = property.replace(/([A-Z])/g, dashConverter); 48 | const cssValue = value + (typeof value === 'number' ? 'px' : ''); 49 | css += ' ' + cssProperty + ':' + cssValue + ';\r\n'; 50 | } 51 | css += '}\r\n'; 52 | if (Object.keys(nested).length) { 53 | css += collect(name, nested); 54 | } 55 | accumulator[selector] = className; 56 | } 57 | return css; 58 | }; 59 | 60 | /** 61 | * 62 | */ 63 | const prepareStyling = () => { 64 | count++; 65 | if (count === 1) { 66 | // TODO: study impact on hot loading 67 | style.innerHTML = cssCollection.join(''); 68 | document.head.appendChild(style); 69 | } 70 | }; 71 | 72 | /** 73 | * 74 | */ 75 | const releaseStyling = () => { 76 | count--; 77 | if (count === 0) { 78 | document.head.removeChild(style); 79 | style.innerHTML = ''; 80 | } 81 | }; 82 | 83 | export default stylize; 84 | export {prepareStyling, releaseStyling}; 85 | -------------------------------------------------------------------------------- /src/utils/ignore.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | */ 4 | 5 | import modify from './modify.js'; 6 | import stringify from './stringify.js'; 7 | import {ATRULE, DECLARATION, RULE, COMMENT, SLASH_SUBSTITUTE} from './COMMON.js'; 8 | 9 | /** 10 | * 11 | */ 12 | const ignore = (oldRules, id) => { 13 | const {freshRules, freshNode} = modify(oldRules, id, {}); // blank change to get the `freshNode` 14 | const content = stringifyAndHandleComments([freshNode]); 15 | for (const key in freshNode) { 16 | delete freshNode[key]; 17 | } 18 | Object.assign(freshNode, { 19 | type: COMMENT, 20 | prefix: '', 21 | hasSlashEnd: true, 22 | content: content, 23 | }); 24 | return stringify(freshRules); 25 | }; 26 | 27 | /** 28 | * 29 | */ 30 | const stringifyAndHandleComments = (kids) => { 31 | return flatten(kids).join(''); 32 | }; 33 | 34 | /** 35 | * 36 | */ 37 | const flatten = (kids, accumulator = []) => { 38 | for (const item of kids) { 39 | switch (item.type) { 40 | case ATRULE: 41 | case RULE: 42 | accumulator.push(handleInlineComments(item.selector) + (item.hasBraceBegin ? '{' : '')); 43 | if (item.kids && item.kids.length) { 44 | flatten(item.kids, accumulator); 45 | } 46 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : '')); 47 | break; 48 | case DECLARATION: 49 | accumulator.push( 50 | handleInlineComments(item.property) + 51 | (item.hasColon ? ':' : '') + 52 | handleInlineComments(item.value) + 53 | (item.hasSemicolon ? ';' : '') 54 | ); 55 | break; 56 | case COMMENT: 57 | accumulator.push( 58 | item.prefix + 59 | SLASH_SUBSTITUTE + 60 | '*' + 61 | item.content + 62 | (item.hasSlashEnd ? '*' + SLASH_SUBSTITUTE : '') 63 | ); 64 | break; 65 | default: 66 | // nothing 67 | } 68 | } 69 | return accumulator; 70 | }; 71 | 72 | /** 73 | * 74 | */ 75 | const handleInlineComments = (blob) => { 76 | return blob 77 | .split('/*') 78 | .join(SLASH_SUBSTITUTE + '*') 79 | .split('*/') 80 | .join('*' + SLASH_SUBSTITUTE); 81 | }; 82 | 83 | export default ignore; 84 | -------------------------------------------------------------------------------- /src/components/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import cls from '../utils/cls'; 4 | import stylize from '../utils/stylize'; 5 | 6 | // ===================================================================================================================== 7 | // D E C L A R A T I O N S 8 | // ===================================================================================================================== 9 | const classes = stylize('Checkbox', { 10 | root: { 11 | position: 'relative', 12 | display: 'inline-block', 13 | verticalAlign: 'middle', 14 | marginTop: -2, 15 | marginRight: 4, 16 | width: 12, 17 | height: 12, 18 | border: 'solid 1px #333333', 19 | userSelect: 'none', 20 | }, 21 | checked: { 22 | '&:after': { 23 | position: 'absolute', 24 | content: '""', 25 | left: 3, 26 | top: 0, 27 | width: 3, 28 | height: 7, 29 | border: 'solid 1px #000', 30 | borderTop: 'none', 31 | borderLeft: 'none', 32 | transform: 'rotate(45deg)', 33 | }, 34 | }, 35 | mixed: { 36 | // currently unused 37 | '&:after': { 38 | position: 'absolute', 39 | content: '""', 40 | left: 2, 41 | top: 2, 42 | width: 6, 43 | height: 6, 44 | background: '#333', 45 | }, 46 | }, 47 | }); 48 | 49 | // ===================================================================================================================== 50 | // C O M P O N E N T 51 | // ===================================================================================================================== 52 | class Checkbox extends React.PureComponent { 53 | /** 54 | * 55 | */ 56 | render() { 57 | const {tick} = this.props; 58 | return ( 59 |
63 | ); 64 | } 65 | 66 | /** 67 | * 68 | */ 69 | onClick = (event) => { 70 | event.stopPropagation(); 71 | const {onTick, id, tick} = this.props; 72 | onTick(id, [true, false, true][tick]); // 0 => true, 1 => false, 2 => true 73 | }; 74 | } 75 | 76 | // ===================================================================================================================== 77 | // D E F I N I T I O N 78 | // ===================================================================================================================== 79 | Checkbox.defaultProps = { 80 | tick: 0, 81 | }; 82 | export default Checkbox; 83 | -------------------------------------------------------------------------------- /lib/utils/stylize.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.releaseStyling = exports.prepareStyling = exports["default"] = void 0; 7 | 8 | /* 9 | A quick-and-dirty simulation of JSS. 10 | */ 11 | var PREFIX = 'rse'; 12 | var SEPARATOR = '-'; 13 | 14 | var dashConverter = function dashConverter(match) { 15 | return '-' + match.toLowerCase(); 16 | }; 17 | 18 | var registry = {}; 19 | var cssCollection = []; 20 | var style = document.createElement('style'); 21 | var count = 0; 22 | /** 23 | * 24 | */ 25 | 26 | var stylize = function stylize(name, classes) { 27 | var output = {}; 28 | var css = collect(name, classes, output); 29 | var index = registry[name]; 30 | 31 | if (index === undefined) { 32 | registry[name] = cssCollection.push(css) - 1; 33 | } else { 34 | cssCollection[index] = css; 35 | } 36 | 37 | return output; 38 | }; 39 | /** 40 | * 41 | */ 42 | 43 | 44 | var collect = function collect(name, classes) { 45 | var accumulator = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 46 | var css = ''; 47 | 48 | for (var selector in classes) { 49 | var block = classes[selector]; 50 | var className = PREFIX + SEPARATOR + name + SEPARATOR + selector; 51 | css += '.' + className + '{\r\n'; 52 | var nested = {}; 53 | 54 | for (var property in block) { 55 | var value = block[property]; 56 | 57 | if (property.indexOf('&') >= 0) { 58 | // this is in fact a nested selector, not a declaration 59 | var resolved = property.replace(/&/g, selector); 60 | nested[resolved] = value; 61 | continue; 62 | } 63 | 64 | var cssProperty = property.replace(/([A-Z])/g, dashConverter); 65 | var cssValue = value + (typeof value === 'number' ? 'px' : ''); 66 | css += ' ' + cssProperty + ':' + cssValue + ';\r\n'; 67 | } 68 | 69 | css += '}\r\n'; 70 | 71 | if (Object.keys(nested).length) { 72 | css += collect(name, nested); 73 | } 74 | 75 | accumulator[selector] = className; 76 | } 77 | 78 | return css; 79 | }; 80 | /** 81 | * 82 | */ 83 | 84 | 85 | var prepareStyling = function prepareStyling() { 86 | count++; 87 | 88 | if (count === 1) { 89 | // TODO: study impact on hot loading 90 | style.innerHTML = cssCollection.join(''); 91 | document.head.appendChild(style); 92 | } 93 | }; 94 | /** 95 | * 96 | */ 97 | 98 | 99 | exports.prepareStyling = prepareStyling; 100 | 101 | var releaseStyling = function releaseStyling() { 102 | count--; 103 | 104 | if (count === 0) { 105 | document.head.removeChild(style); 106 | style.innerHTML = ''; 107 | } 108 | }; 109 | 110 | exports.releaseStyling = releaseStyling; 111 | var _default = stylize; 112 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/components/StyleEditor.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StyleEditor from './StyleEditor'; 3 | 4 | const large = ` 5 | div { 6 | background-color:red; 7 | back/*foo*/ground:blue; 8 | color:red; /* short comment*/ 9 | overflow:hidden; 10 | /*border:none;*/ 11 | foo: bar; 12 | font-weight: 13 | } 14 | /*span {color:red}*/ 15 | @supports (display: flex) { 16 | @media screen and (min-width: 900px) { 17 | div { 18 | display: flex; 19 | } 20 | /* 21 | GIANT COMMENT: 22 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus aliquet est ut quam rutrum fringilla. Suspendisse et odio. Sed fringilla risus vel est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam auctor mi quis eros. Morbi justo nulla, lobortis imperdiet, consequat at, auctor ut, tortor. Sed est ipsum, posuere dictum, egestas ac, aliquam eu, sapien. Aenean ornare enim at mi. Phasellus id libero viverra felis elementum lobortis. Curabitur nec sapien gravida lacus lobortis tempor. Quisque eget mi a turpis rutrum venenatis. Nam tempus luctus nunc. Nulla ut orci ac est laoreet malesuada. 23 | */ 24 | @media screen { 25 | div { 26 | color:lime; 27 | } 28 | } 29 | } 30 | } 31 | `; 32 | 33 | export default { 34 | component: StyleEditor, 35 | }; 36 | 37 | export const Empty = { 38 | name: 'empty', 39 | }; 40 | export const Large = { 41 | name: 'large', 42 | args: { 43 | defaultValue: large, 44 | }, 45 | }; 46 | export const Warning = { 47 | name: 'warning', 48 | args: { 49 | defaultValue: `@import 'custom.css';`, 50 | }, 51 | }; 52 | export const Height = { 53 | name: 'height forced', 54 | args: { 55 | defaultValue: 'div{color:red}', 56 | style: {height: 100}, 57 | }, 58 | }; 59 | export const Invalid = { 60 | name: 'invalidRule', 61 | args: { 62 | defaultValue: '0div{mother:father;font-weight:bold}', 63 | }, 64 | }; 65 | export const Overwrite = { 66 | name: 'overwrite declarations', 67 | args: { 68 | defaultValue: 'div{background-color:red;background:blue;}', 69 | }, 70 | }; 71 | export const EmptySlots = { 72 | name: 'empty slots', 73 | args: { 74 | defaultValue: ' {mother:;: bold}', 75 | }, 76 | }; 77 | export const ReadOnly = { 78 | name: 'readOnly', 79 | args: { 80 | defaultValue: 'div{color:red}', 81 | readOnly: true, 82 | }, 83 | }; 84 | export const CommentsOutside = { 85 | name: 'comments outside', 86 | args: { 87 | defaultValue: '/*x*/h1{color:red} h2/*x*/{color:red} h3{color:red}/*x*/', 88 | }, 89 | }; 90 | export const CommentsInside = { 91 | name: 'comments inside', 92 | args: { 93 | defaultValue: 94 | 'h1{/*x*/color:red} h2{c/*x*/olor:red} h3{color:/*x*/red} h4{color:red/*x*/} h5{color:red;/*x*/} h6{color:red;/*x*//*x*/} .empty{color:red;/**//**//**//**/}', 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /tests/ignore.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 38 | 39 | 40 | 41 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /lib/utils/stringify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _COMMON = require("./COMMON.js"); 9 | 10 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } 11 | 12 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 13 | 14 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 15 | 16 | /** 17 | * 18 | */ 19 | var stringify = function stringify(kids) { 20 | return flatten(kids).join(''); 21 | }; 22 | /** 23 | * 24 | */ 25 | 26 | 27 | var flatten = function flatten(kids) { 28 | var accumulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 29 | 30 | var _iterator = _createForOfIteratorHelper(kids), 31 | _step; 32 | 33 | try { 34 | for (_iterator.s(); !(_step = _iterator.n()).done;) { 35 | var item = _step.value; 36 | 37 | switch (item.type) { 38 | case _COMMON.ATRULE: 39 | case _COMMON.RULE: 40 | accumulator.push(item.selector + (item.hasBraceBegin ? '{' : '')); 41 | 42 | if (item.kids && item.kids.length) { 43 | flatten(item.kids, accumulator); 44 | } 45 | 46 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : '')); 47 | break; 48 | 49 | case _COMMON.DECLARATION: 50 | accumulator.push(item.property + (item.hasColon ? ':' : '') + item.value + (item.hasSemicolon ? ';' : '')); 51 | break; 52 | 53 | case _COMMON.COMMENT: 54 | accumulator.push(item.prefix + '/*' + item.content + (item.hasSlashEnd ? '*/' : '')); 55 | break; 56 | 57 | default: // nothing 58 | 59 | } 60 | } 61 | } catch (err) { 62 | _iterator.e(err); 63 | } finally { 64 | _iterator.f(); 65 | } 66 | 67 | return accumulator; 68 | }; 69 | 70 | var _default = stringify; 71 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/utils/identify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _COMMON = require("./COMMON.js"); 9 | 10 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } 11 | 12 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 13 | 14 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 15 | 16 | var MAX_CHARS = 32; // how many characters to use as identifier. Protects against giant base64. 17 | 18 | /** 19 | * 20 | */ 21 | 22 | var identify = function identify(list) { 23 | var usedIds = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 24 | 25 | var _iterator = _createForOfIteratorHelper(list), 26 | _step; 27 | 28 | try { 29 | for (_iterator.s(); !(_step = _iterator.n()).done;) { 30 | var item = _step.value; 31 | var id = void 0; 32 | 33 | switch (item.type) { 34 | case _COMMON.ATRULE: 35 | case _COMMON.RULE: 36 | id = item.selector.trim() + (item.hasBraceBegin ? '{' : '') + (item.hasSemicolon ? ';' : ''); 37 | break; 38 | 39 | case _COMMON.DECLARATION: 40 | id = item.property.trim() + (item.hasColon ? ':' : '') + item.value.substr(0, MAX_CHARS) + (item.hasSemicolon ? ';' : ''); 41 | break; 42 | 43 | case _COMMON.COMMENT: 44 | id = '/*' + item.content.substr(0, MAX_CHARS) + '*/'; 45 | break; 46 | 47 | default: // nothing 48 | 49 | } 50 | 51 | if (id in usedIds) { 52 | usedIds[id]++; 53 | item.id = id + usedIds[id]; 54 | } else { 55 | usedIds[id] = 1; 56 | item.id = id; 57 | } 58 | 59 | if (item.kids && item.kids.length) { 60 | identify(item.kids, usedIds); 61 | } 62 | } 63 | } catch (err) { 64 | _iterator.e(err); 65 | } finally { 66 | _iterator.f(); 67 | } 68 | }; 69 | 70 | var _default = identify; 71 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/utils/unignore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _COMMON = require("./COMMON.js"); 9 | 10 | var _modify2 = _interopRequireDefault(require("./modify.js")); 11 | 12 | var _stringify = _interopRequireDefault(require("./stringify.js")); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 15 | 16 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } 17 | 18 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 19 | 20 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 21 | 22 | /** 23 | * 24 | */ 25 | var unignore = function unignore(rules, id) { 26 | var _modify = (0, _modify2["default"])(rules, id, {}), 27 | freshRules = _modify.freshRules, 28 | freshNode = _modify.freshNode; // blank change to get the `freshNode` 29 | 30 | 31 | if (freshNode.type === _COMMON.COMMENT) { 32 | unignoreComment(freshNode); 33 | } else { 34 | unignoreKids(freshNode.kids); 35 | } 36 | 37 | return (0, _stringify["default"])(freshRules); 38 | }; 39 | /** 40 | * 41 | */ 42 | 43 | 44 | var unignoreComment = function unignoreComment(node) { 45 | var prefix = node.prefix; // backup 46 | 47 | var content = node.content.split(_COMMON.SLASH_SUBSTITUTE + '*').join('/*').split('*' + _COMMON.SLASH_SUBSTITUTE).join('*/'); 48 | 49 | for (var key in node) { 50 | delete node[key]; 51 | } 52 | 53 | Object.assign(node, { 54 | type: _COMMON.RULE, 55 | // could also be ATRULE or DECLARATION, because it's just temporary 56 | selector: prefix + content 57 | }); 58 | }; 59 | /** 60 | * 61 | */ 62 | 63 | 64 | var unignoreKids = function unignoreKids(kids) { 65 | var _iterator = _createForOfIteratorHelper(kids), 66 | _step; 67 | 68 | try { 69 | for (_iterator.s(); !(_step = _iterator.n()).done;) { 70 | var item = _step.value; 71 | 72 | if (item.type === _COMMON.COMMENT) { 73 | unignoreComment(item); 74 | } else { 75 | if (item.kids && item.kids.length) { 76 | unignoreKids(item.kids); 77 | } 78 | } 79 | } 80 | } catch (err) { 81 | _iterator.e(err); 82 | } finally { 83 | _iterator.f(); 84 | } 85 | }; 86 | 87 | var _default = unignore; 88 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/utils/prettify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _COMMON = require("./COMMON"); 9 | 10 | var _clean = _interopRequireDefault(require("./clean")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } 15 | 16 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 17 | 18 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 19 | 20 | /** 21 | * 22 | */ 23 | var prettify = function prettify(kids) { 24 | return flatten(kids).join(''); 25 | }; 26 | /** 27 | * 28 | */ 29 | 30 | 31 | var flatten = function flatten(kids) { 32 | var accumulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 33 | var indent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; 34 | 35 | var _iterator = _createForOfIteratorHelper(kids), 36 | _step; 37 | 38 | try { 39 | for (_iterator.s(); !(_step = _iterator.n()).done;) { 40 | var item = _step.value; 41 | 42 | switch (item.type) { 43 | case _COMMON.ATRULE: 44 | case _COMMON.RULE: 45 | var type = item.type, 46 | _kids = item.kids, 47 | selector = item.selector, 48 | hasBraceBegin = item.hasBraceBegin, 49 | hasBraceEnd = item.hasBraceEnd, 50 | hasSemicolon = item.hasSemicolon; 51 | 52 | if (!_kids.length && !selector.trim() && !hasBraceBegin && !hasBraceEnd && !hasSemicolon) { 53 | continue; 54 | } 55 | 56 | accumulator.push(indent + (0, _clean["default"])(selector) + ' {\r\n'); 57 | 58 | if (_kids && _kids.length) { 59 | flatten(_kids, accumulator, indent + ' '); 60 | } 61 | 62 | if (type === _COMMON.ATRULE && !hasBraceBegin) { 63 | accumulator.push(';\r\n'); 64 | } else { 65 | accumulator.push(indent + '}\r\n'); 66 | } 67 | 68 | break; 69 | 70 | case _COMMON.DECLARATION: 71 | if (!item.hasColon && !item.property.trim()) { 72 | continue; 73 | } 74 | 75 | accumulator.push(indent + (0, _clean["default"])(item.property) + ': ' + (0, _clean["default"])(item.value) + ';\r\n'); 76 | break; 77 | 78 | case _COMMON.COMMENT: 79 | accumulator.push(indent + '/*' + item.content + '*/\r\n'); 80 | break; 81 | 82 | default: // nothing 83 | 84 | } 85 | } 86 | } catch (err) { 87 | _iterator.e(err); 88 | } finally { 89 | _iterator.f(); 90 | } 91 | 92 | return accumulator; 93 | }; 94 | 95 | var _default = prettify; 96 | exports["default"] = _default; -------------------------------------------------------------------------------- /tests/validate.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 36 | 37 | 38 | 39 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /tests/parse.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 36 | 37 | 38 | 39 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | import {ATRULE, COMMENT} from './COMMON'; 5 | 6 | let sheet; 7 | const BASE64_TEMP = ';base64,0'; 8 | const base64Pattern = /;base64,[a-zA-Z/0-9+=]*/g; 9 | 10 | /** 11 | * 12 | */ 13 | const validate = (list) => { 14 | sheet = sheet || createPlayground(); // lazy initialization 15 | validateRules(list, '', '', ''); 16 | return list; 17 | }; 18 | 19 | /** 20 | * 21 | */ 22 | const validateRules = (list, parentPrefix, parentSuffix, parentFingerprint) => { 23 | for (const rule of list) { 24 | if (rule.type === COMMENT) { 25 | continue; 26 | } 27 | const adaptedSelector = rule.selector.split('&').join('#x'); // act as if `&` is valid 28 | const rulePrefix = parentPrefix + adaptedSelector + (rule.hasBraceBegin ? '{' : ''); 29 | const ruleSuffix = (rule.hasBraceEnd ? '}' : '') + (rule.hasSemicolon ? ';' : '') + parentSuffix; 30 | const fingerprint = inAndOut(rulePrefix + ruleSuffix); 31 | if (fingerprint !== parentFingerprint) { 32 | // the browser accepted our rule 33 | rule.isValid = true; 34 | if (rule.kids.length) { 35 | if (rule.type === ATRULE) { 36 | validateRules(rule.kids, rulePrefix, ruleSuffix, fingerprint); 37 | } else { 38 | // RULE 39 | validateDeclarations(rule.kids, rulePrefix, ruleSuffix, fingerprint); 40 | } 41 | } 42 | } else { 43 | rule.isValid = false; 44 | if (rule.kids.length) { 45 | invalidateChildren(rule.kids); 46 | } 47 | } 48 | } 49 | }; 50 | 51 | /** 52 | * 53 | */ 54 | const validateDeclarations = (list, parentPrefix, parentSuffix, parentFingerprint) => { 55 | let fingerprint = parentFingerprint; 56 | let block = ''; 57 | for (let i = list.length - 1; i >= 0; i--) { 58 | // we traverse backwards to detect overruled declarations 59 | const declaration = list[i]; 60 | if (declaration.type === COMMENT) { 61 | continue; 62 | } 63 | block = (declaration.hasSemicolon ? ';' : '') + block; 64 | const safeDeclarationValue = declaration.value.replace(base64Pattern, BASE64_TEMP); 65 | block = declaration.property + (declaration.hasColon ? ':' : '') + safeDeclarationValue + block; 66 | const freshFingerprint = inAndOut(parentPrefix + block + parentSuffix); 67 | if (fingerprint !== freshFingerprint) { 68 | // the browser accepted our declaration 69 | declaration.isValid = true; 70 | fingerprint = freshFingerprint; 71 | } else { 72 | declaration.isValid = false; 73 | } 74 | } 75 | }; 76 | 77 | /** 78 | * 79 | */ 80 | const invalidateChildren = (list) => { 81 | for (const item of list) { 82 | if (item.type === COMMENT) { 83 | continue; 84 | } 85 | item.isValid = false; 86 | const kids = item.kids; 87 | if (kids && kids.length) { 88 | invalidateChildren(kids); 89 | } 90 | } 91 | }; 92 | 93 | /** 94 | * 95 | */ 96 | const inAndOut = (blob) => { 97 | let index; 98 | try { 99 | index = sheet.insertRule(blob); 100 | } catch (e) { 101 | // console.log(e); 102 | } 103 | if (index >= 0) { 104 | const fingerprint = sheet.cssRules[index].cssText; 105 | sheet.deleteRule(index); 106 | return fingerprint; 107 | } 108 | return ''; 109 | }; 110 | 111 | /** 112 | * 113 | * Note: DocumentFragment doesn't work because it doesn't compute styles. 114 | */ 115 | const createPlayground = () => { 116 | const iframe = document.createElement('iframe'); 117 | iframe.style.display = 'none'; 118 | document.head.appendChild(iframe); 119 | const iframeDocument = iframe.contentWindow.document; 120 | const style = iframeDocument.createElement('style'); 121 | iframeDocument.head.appendChild(style); 122 | 123 | // Important: Since Chrome 80 (or so), we need to remove the iframe AFTER we added the style. 124 | document.head.removeChild(iframe); 125 | 126 | return style.sheet; 127 | }; 128 | 129 | /** 130 | * 131 | */ 132 | const destroyPlayground = () => { 133 | sheet = null; 134 | }; 135 | 136 | export default validate; 137 | export {destroyPlayground}; 138 | -------------------------------------------------------------------------------- /lib/utils/ignore.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _modify2 = _interopRequireDefault(require("./modify.js")); 9 | 10 | var _stringify = _interopRequireDefault(require("./stringify.js")); 11 | 12 | var _COMMON = require("./COMMON.js"); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 15 | 16 | function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } 17 | 18 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } 19 | 20 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } 21 | 22 | /** 23 | * 24 | */ 25 | var ignore = function ignore(oldRules, id) { 26 | var _modify = (0, _modify2["default"])(oldRules, id, {}), 27 | freshRules = _modify.freshRules, 28 | freshNode = _modify.freshNode; // blank change to get the `freshNode` 29 | 30 | 31 | var content = stringifyAndHandleComments([freshNode]); 32 | 33 | for (var key in freshNode) { 34 | delete freshNode[key]; 35 | } 36 | 37 | Object.assign(freshNode, { 38 | type: _COMMON.COMMENT, 39 | prefix: '', 40 | hasSlashEnd: true, 41 | content: content 42 | }); 43 | return (0, _stringify["default"])(freshRules); 44 | }; 45 | /** 46 | * 47 | */ 48 | 49 | 50 | var stringifyAndHandleComments = function stringifyAndHandleComments(kids) { 51 | return flatten(kids).join(''); 52 | }; 53 | /** 54 | * 55 | */ 56 | 57 | 58 | var flatten = function flatten(kids) { 59 | var accumulator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 60 | 61 | var _iterator = _createForOfIteratorHelper(kids), 62 | _step; 63 | 64 | try { 65 | for (_iterator.s(); !(_step = _iterator.n()).done;) { 66 | var item = _step.value; 67 | 68 | switch (item.type) { 69 | case _COMMON.ATRULE: 70 | case _COMMON.RULE: 71 | accumulator.push(handleInlineComments(item.selector) + (item.hasBraceBegin ? '{' : '')); 72 | 73 | if (item.kids && item.kids.length) { 74 | flatten(item.kids, accumulator); 75 | } 76 | 77 | accumulator.push((item.hasBraceEnd ? '}' : '') + (item.hasSemicolon ? ';' : '')); 78 | break; 79 | 80 | case _COMMON.DECLARATION: 81 | accumulator.push(handleInlineComments(item.property) + (item.hasColon ? ':' : '') + handleInlineComments(item.value) + (item.hasSemicolon ? ';' : '')); 82 | break; 83 | 84 | case _COMMON.COMMENT: 85 | accumulator.push(item.prefix + _COMMON.SLASH_SUBSTITUTE + '*' + item.content + (item.hasSlashEnd ? '*' + _COMMON.SLASH_SUBSTITUTE : '')); 86 | break; 87 | 88 | default: // nothing 89 | 90 | } 91 | } 92 | } catch (err) { 93 | _iterator.e(err); 94 | } finally { 95 | _iterator.f(); 96 | } 97 | 98 | return accumulator; 99 | }; 100 | /** 101 | * 102 | */ 103 | 104 | 105 | var handleInlineComments = function handleInlineComments(blob) { 106 | return blob.split('/*').join(_COMMON.SLASH_SUBSTITUTE + '*').split('*/').join('*' + _COMMON.SLASH_SUBSTITUTE); 107 | }; 108 | 109 | var _default = ignore; 110 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/components/Comment.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import stylize from '../utils/stylize'; 4 | import clean from '../utils/clean'; 5 | import Checkbox from './Checkbox'; 6 | import Area from './Area'; 7 | import {AFTER} from '../utils/COMMON'; 8 | import shorten from '../utils/shorten'; 9 | import hasSelection from '../utils/hasSelection'; 10 | 11 | // ===================================================================================================================== 12 | // D E C L A R A T I O N S 13 | // ===================================================================================================================== 14 | const classes = stylize('Comment', { 15 | root: { 16 | color: 'silver', 17 | padding: '2px 0', 18 | whiteSpace: 'nowrap', 19 | overflow: 'hidden', 20 | textOverflow: 'ellipsis', 21 | }, 22 | content: { 23 | cursor: 'text', 24 | borderBottom: '1px dashed transparent', 25 | '&:hover': { 26 | borderBottomColor: 'currentColor', 27 | }, 28 | }, 29 | after: { 30 | marginTop: 4, 31 | }, 32 | }); 33 | 34 | const MAX_CHARS_VALUE = 32; // how many characters to display in the value. Protects against giant base64. 35 | const MAX_CHARS_TITLE = 512; // how many characters to display in the tooltip. Protects against giant base64. 36 | 37 | // ===================================================================================================================== 38 | // C O M P O N E N T 39 | // ===================================================================================================================== 40 | class Comment extends React.PureComponent { 41 | state = { 42 | isEditingContent: false, 43 | isEditingAfter: false, 44 | }; 45 | 46 | /** 47 | * 48 | */ 49 | render() { 50 | const {id, content, onTick} = this.props; 51 | const {isEditingContent, isEditingAfter} = this.state; 52 | 53 | const cleanContent = clean(content); 54 | 55 | let shortContent = cleanContent; 56 | let shortTitle = ''; 57 | if (cleanContent.length > MAX_CHARS_VALUE) { 58 | shortContent = shorten(cleanContent, MAX_CHARS_VALUE); 59 | shortTitle = shorten(cleanContent, MAX_CHARS_TITLE); 60 | } 61 | 62 | return ( 63 |
64 | 65 | 66 | {isEditingContent ? ( 67 | this.renderArea('content', content) 68 | ) : ( 69 | 70 | {'/*' + shortContent + '*/'} 71 | 72 | )} 73 | {isEditingAfter && ( 74 |
75 | 76 | {this.renderArea(AFTER, '')} 77 |
78 | )} 79 |
80 | ); 81 | } 82 | 83 | /** 84 | * 85 | */ 86 | renderArea = (payloadProperty, defaultValue) => { 87 | const {id, onEditChange} = this.props; 88 | return ( 89 | 96 | ); 97 | }; 98 | 99 | /** 100 | * 101 | */ 102 | onContentClick = (event) => { 103 | if (hasSelection()) return; 104 | event.stopPropagation(); 105 | this.setState({isEditingContent: true}); 106 | this.props.onEditBegin(); 107 | }; 108 | 109 | /** 110 | * 111 | */ 112 | onCommentClick = (event) => { 113 | if (hasSelection()) return; 114 | event.stopPropagation(); 115 | this.setState({isEditingAfter: true}); 116 | this.props.onEditBegin(); 117 | }; 118 | 119 | /** 120 | * 121 | */ 122 | onAreaBlur = (id, payload) => { 123 | this.setState({ 124 | isEditingContent: false, 125 | isEditingAfter: false, 126 | }); 127 | this.props.onEditEnd(id, payload); 128 | }; 129 | } 130 | 131 | // ===================================================================================================================== 132 | // D E F I N I T I O N 133 | // ===================================================================================================================== 134 | export default Comment; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Style Editor 2 | 3 | [![Npm Version][npm-version-image]][npm-version-url]   [![Size][bundlephobia-image]][bundlephobia-url] 4 | 5 | A React component that displays and edits CSS, similar to the browser's DevTools. 6 | 7 | [![Live demo](https://aurelain.github.io/react-style-editor/StyleEditor.png)](https://aurelain.github.io/react-style-editor/) 8 | 9 | ## [Live demo](https://aurelain.github.io/react-style-editor/) 10 | 11 | ## Features 12 | 13 | - Parses any CSS string and formats it in a familiar fashion 14 | - Validates each rule and each declaration using the browsers's own engine 15 | - Facilitates commenting the CSS code through checkbox toggling 16 | - Allows easy additions by clicking next to the desired location 17 | - Has no dependencies (other than React) 18 | - Is tiny (< 10 KB minified) 19 | - Is customizable through classes 20 | - Offers 3 output formats: 21 | - the code with preserved formatting 22 | - a machine-friendly model of the code (recursive array of objects) 23 | - the prettified code 24 | 25 | ## Installation 26 | 27 | ```sh 28 | npm i react-style-editor 29 | ``` 30 | 31 | ## Usage 32 | 33 | ```js 34 | import React from 'react'; 35 | import StyleEditor from 'react-style-editor'; 36 | 37 | class Component extends React.Component { 38 | render() { 39 | return ( 40 | 51 | ); 52 | } 53 | } 54 | ``` 55 | 56 | ## Props 57 | 58 | | prop | type | default | description | 59 | | --------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------------- | 60 | | `defaultValue` | string | `''` | The initial CSS code | 61 | | `value` | string | `undefined` | The controlled CSS code | 62 | | `onChange` | function | `null` | A closure that receives a single argument, `string` or `array`, depending on the value of `outputFormats` | 63 | | `outputFormats` | string | `'pretty'` | Comma-separated values of: `'preserved'`, `'machine'`, `'pretty'` | 64 | | `readOnly` | boolean | `false` | All interactions with the component are blocked | 65 | 66 | All parameters are optional, but some are inter-related. For example, due to the nature of React, you should use `StyleEditor` either fully controlled or fully uncontrolled (see [this article](https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#preferred-solutions)). 67 | A short summary: 68 | 69 | - `defaultValue` => uncontrolled, the component is on its own 70 | - `value` => controlled => you must also use the `onChange` or `readOnly` properties. 71 | 72 | The above behavior is identical to that of normal React form elements, e.g. `