From 66283a6d5dee515fc1355065e8d01765832cba6d Mon Sep 17 00:00:00 2001 From: ExE Boss <3889017+ExE-Boss@users.noreply.github.com> Date: Wed, 1 Apr 2020 18:30:00 +0200 Subject: [PATCH] =?UTF-8?q?Migrate=C2=A0`CSSStyleDeclaration`=20to=C2=A0We?= =?UTF-8?q?bIDL2JS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Timothy Gu --- .eslintignore | 4 + .gitignore | 5 + .npmignore | 2 + .travis.yml | 1 - README.md | 45 ++++- index.js | 34 ++++ jest.config.js | 2 + ...aration.js => CSSStyleDeclaration-impl.js} | 155 +++++++++--------- lib/CSSStyleDeclaration.test.js | 14 +- lib/allExtraProperties.js | 1 - lib/allWebkitProperties.js | 2 +- lib/parsers.js | 63 ++++--- lib/parsers.test.js | 4 +- lib/properties/borderSpacing.js | 2 +- package-lock.json | 80 ++++++++- package.json | 16 +- scripts/convert-idl.js | 88 ++++++++++ scripts/download_latest_properties.js | 4 +- scripts/generate_implemented_properties.js | 12 +- scripts/generate_properties.js | 17 +- src/CSSStyleDeclaration.webidl | 18 ++ webidl2js-wrapper.js | 2 + 22 files changed, 431 insertions(+), 140 deletions(-) create mode 100644 index.js rename lib/{CSSStyleDeclaration.js => CSSStyleDeclaration-impl.js} (67%) create mode 100644 scripts/convert-idl.js create mode 100644 src/CSSStyleDeclaration.webidl create mode 100644 webidl2js-wrapper.js diff --git a/.eslintignore b/.eslintignore index 9f55fbbb..de78dd4f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,8 @@ node_modules +lib/CSSStyleDeclaration.js +lib/Function.js +lib/VoidFunction.js lib/implementedProperties.js lib/properties.js +lib/utils.js jest.config.js diff --git a/.gitignore b/.gitignore index 558dbbb4..4cd4b401 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ node_modules npm-debug.log +lib/CSSStyleDeclaration.js +lib/Function.js +lib/VoidFunction.js lib/implementedProperties.js lib/properties.js +lib/utils.js coverage +src/CSSStyleDeclaration-properties.webidl diff --git a/.npmignore b/.npmignore index 4c9a487d..d9a5f721 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,5 @@ /* !lib/ +lib/Function.js +lib/VoidFunction.js !LICENSE diff --git a/.travis.yml b/.travis.yml index 3e4789bd..f605a283 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ install: - npm install - npm install -g codecov node_js: - - "8" - "10" - "12" diff --git a/README.md b/README.md index e0ce185d..aec91bd9 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,47 @@ A Node JS implementation of the CSS Object Model [CSSStyleDeclaration interface] [![NpmVersion](https://img.shields.io/npm/v/cssstyle.svg)](https://www.npmjs.com/package/cssstyle) [![Build Status](https://travis-ci.org/jsdom/cssstyle.svg?branch=master)](https://travis-ci.org/jsdom/cssstyle) [![codecov](https://codecov.io/gh/jsdom/cssstyle/branch/master/graph/badge.svg)](https://codecov.io/gh/jsdom/cssstyle) ---- +## Background -#### Background +This package is an extension of the CSSStyleDeclaration class in Nikita Vasilyev's [CSSOM](https://github.com/NV/CSSOM) with added support for CSS 2 & 3 properties. The primary use case is for testing browser code in a Node environment. -This package is an extension of the CSSStyleDeclaration class in Nikita Vasilyev's [CSSOM](https://github.com/NV/CSSOM) with added support for CSS 2 & 3 properties. The primary use case is for testing browser code in a Node environment. - -It was originally created by Chad Walker, it is now maintained by the jsdom community. +It was originally created by Chad Walker, it is now maintained by Jon Sakas and other open source contributors. Bug reports and pull requests are welcome. + +## APIs + +This package exposes two flavors of the `CSSStyleDeclaration` interface depending on the imported module. + +### `cssstyle` module + +This module default-exports the `CSSStyleDeclaration` interface constructor, with the change that it can be constructed with an optional `onChangeCallback` parameter. Whenever any CSS property is modified through an instance of this class, the callback (if provided) will be called with a string that represents all CSS properties of this element, serialized. This allows the embedding environment to properly reflect the style changes to an element's `style` attribute. + +Here is a crude example of using the `onChangeCallback` to implement the `style` property of `HTMLElement`: +```js +const CSSStyleDeclaration = require('cssstyle'); + +class HTMLElement extends Element { + constructor() { + this._style = new CSSStyleDeclaration(newCSSText => { + this.setAttributeNS(null, "style", newCSSText); + }); + } + + get style() { + return this._style; + } + + set style(text) { + this._style.cssText = text; + } +} +``` + +### `cssstyle/webidl2js-wrapper` module + +This module exports the `CSSStyleDeclaration` [interface wrapper API](https://github.com/jsdom/webidl2js#for-interfaces) generated by [webidl2js](https://github.com/jsdom/webidl2js). Unlike the default export, `CSSStyleDeclaration` constructors installed by the webidl2js wrapper do _not_ support construction, just like how they actually are in browsers. Creating new `CSSStyleDeclaration` objects can be done with the [`create`](https://github.com/jsdom/webidl2js#createglobalobject-constructorargs-privatedata) method of the wrapper. + +#### `privateData` + +The `privateData` parameter of `create` and `createImpl` provides a way to specify the `onChangeCallback` that is a constructor parameter in the default export. Only the `onChangeCallback` property is supported on `privateData` currently, with the same semantics as the constructor parameter documented above. diff --git a/index.js b/index.js new file mode 100644 index 00000000..aa2fd632 --- /dev/null +++ b/index.js @@ -0,0 +1,34 @@ +'use strict'; +const webidlWrapper = require('./webidl2js-wrapper.js'); + +const sharedGlobalObject = {}; +webidlWrapper.install(sharedGlobalObject, ['Window']); + +const origCSSStyleDeclaration = sharedGlobalObject.CSSStyleDeclaration; + +/** + * @constructor + * @param {((cssText: string) => void) | null} [onChangeCallback] + * The callback that is invoked whenever a property changes. + */ +function CSSStyleDeclaration(onChangeCallback = null) { + if (new.target === undefined) { + throw new TypeError("Class constructor CSSStyleDeclaration cannot be invoked without 'new'"); + } + + if (onChangeCallback !== null && typeof onChangeCallback !== 'function') { + throw new TypeError('Failed to construct CSSStyleDeclaration: parameter 1 is not a function'); + } + + return webidlWrapper.create(sharedGlobalObject, undefined, { onChangeCallback }); +} + +sharedGlobalObject.CSSStyleDeclaration = CSSStyleDeclaration; +Object.defineProperty(CSSStyleDeclaration, 'prototype', { + value: origCSSStyleDeclaration.prototype, + writable: false, +}); +CSSStyleDeclaration.prototype.constructor = CSSStyleDeclaration; +Object.setPrototypeOf(CSSStyleDeclaration, Object.getPrototypeOf(origCSSStyleDeclaration)); + +module.exports = CSSStyleDeclaration; diff --git a/jest.config.js b/jest.config.js index 565eada6..fa357eef 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,8 +3,10 @@ module.exports = { "collectCoverage": true, "collectCoverageFrom": [ "lib/**/*.js", + "!lib/CSSStyleDeclaration.js", "!lib/implementedProperties.js", "!lib/properties.js", + "!lib/utils.js", ], "coverageDirectory": "coverage", }; diff --git a/lib/CSSStyleDeclaration.js b/lib/CSSStyleDeclaration-impl.js similarity index 67% rename from lib/CSSStyleDeclaration.js rename to lib/CSSStyleDeclaration-impl.js index bded9a44..9ce8567a 100644 --- a/lib/CSSStyleDeclaration.js +++ b/lib/CSSStyleDeclaration-impl.js @@ -7,25 +7,28 @@ var CSSOM = require('cssom'); var allProperties = require('./allProperties'); var allExtraProperties = require('./allExtraProperties'); var implementedProperties = require('./implementedProperties'); -var { dashedToCamelCase } = require('./parsers'); +var { cssPropertyToIDLAttribute } = require('./parsers'); var getBasicPropertyDescriptor = require('./utils/getBasicPropertyDescriptor'); +const idlUtils = require('./utils.js'); -/** - * @constructor - * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration - */ -var CSSStyleDeclaration = function CSSStyleDeclaration(onChangeCallback) { - this._values = {}; - this._importants = {}; - this._length = 0; - this._onChange = - onChangeCallback || - function() { - return; - }; -}; -CSSStyleDeclaration.prototype = { - constructor: CSSStyleDeclaration, +class CSSStyleDeclarationImpl { + /** + * @constructor + * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration + * + * @param {object} globalObject + * @param {*[]} args + * @param {object} privateData + * @param {((cssText: string) => void) | null} [privateData.onChangeCallback] + */ + constructor(globalObject, args, { onChangeCallback }) { + this._globalObject = globalObject; + this._values = Object.create(null); + this._importants = Object.create(null); + this._length = 0; + this._onChange = onChangeCallback || (() => {}); + this.parentRule = null; + } /** * @@ -34,25 +37,19 @@ CSSStyleDeclaration.prototype = { * @return {string} the value of the property if it has been explicitly set for this declaration block. * Returns the empty string if the property has not been set. */ - getPropertyValue: function(name) { - if (!this._values.hasOwnProperty(name)) { - return ''; - } - return this._values[name].toString(); - }, + getPropertyValue(name) { + return this._values[name] || ''; + } /** * * @param {string} name * @param {string} value - * @param {string} [priority=null] "important" or null + * @param {string} [priority=""] "important" or "" * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty */ - setProperty: function(name, value, priority) { - if (value === undefined) { - return; - } - if (value === null || value === '') { + setProperty(name, value, priority = '') { + if (value === '') { this.removeProperty(name); return; } @@ -68,8 +65,16 @@ CSSStyleDeclaration.prototype = { this[lowercaseName] = value; this._importants[lowercaseName] = priority; - }, - _setProperty: function(name, value, priority) { + } + + /** + * @param {string} name + * @param {string | null} value + * @param {string} [priority=""] + */ + _setProperty(name, value, priority = '') { + // FIXME: A good chunk of the implemented properties call this method + // with `value = undefined`, expecting it to do nothing: if (value === undefined) { return; } @@ -92,7 +97,7 @@ CSSStyleDeclaration.prototype = { this._values[name] = value; this._importants[name] = priority; this._onChange(this.cssText); - }, + } /** * @@ -101,8 +106,8 @@ CSSStyleDeclaration.prototype = { * @return {string} the value of the property if it has been explicitly set for this declaration block. * Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property. */ - removeProperty: function(name) { - if (!this._values.hasOwnProperty(name)) { + removeProperty(name) { + if (!idlUtils.hasOwn(this._values, name)) { return ''; } @@ -123,49 +128,36 @@ CSSStyleDeclaration.prototype = { this._onChange(this.cssText); return prevValue; - }, + } /** * * @param {String} name */ - getPropertyPriority: function(name) { + getPropertyPriority(name) { return this._importants[name] || ''; - }, - - getPropertyCSSValue: function() { - //FIXME - return; - }, - - /** - * element.style.overflow = "auto" - * element.style.getPropertyShorthand("overflow-x") - * -> "overflow" - */ - getPropertyShorthand: function() { - //FIXME - return; - }, - - isPropertyImplicit: function() { - //FIXME - return; - }, + } /** * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-item */ - item: function(index) { - index = parseInt(index, 10); + item(index) { if (index < 0 || index >= this._length) { return ''; } return this[index]; - }, -}; + } + + [idlUtils.supportsPropertyIndex](index) { + return index >= 0 && index < this._length; + } -Object.defineProperties(CSSStyleDeclaration.prototype, { + [idlUtils.supportedPropertyIndices]() { + return Array.prototype.keys.call(this); + } +} + +Object.defineProperties(CSSStyleDeclarationImpl.prototype, { cssText: { get: function() { var properties = []; @@ -178,9 +170,9 @@ Object.defineProperties(CSSStyleDeclaration.prototype, { value = this.getPropertyValue(name); priority = this.getPropertyPriority(name); if (priority !== '') { - priority = ' !' + priority; + priority = ` !${priority}`; } - properties.push([name, ': ', value, priority, ';'].join('')); + properties.push(`${name}: ${value}${priority};`); } return properties.join(' '); }, @@ -211,13 +203,6 @@ Object.defineProperties(CSSStyleDeclaration.prototype, { enumerable: true, configurable: true, }, - parentRule: { - get: function() { - return null; - }, - enumerable: true, - configurable: true, - }, length: { get: function() { return this._length; @@ -239,22 +224,38 @@ Object.defineProperties(CSSStyleDeclaration.prototype, { }, }); -require('./properties')(CSSStyleDeclaration.prototype); +require('./properties')(CSSStyleDeclarationImpl.prototype); +// TODO: Consider using `[Reflect]` for basic properties allProperties.forEach(function(property) { if (!implementedProperties.has(property)) { var declaration = getBasicPropertyDescriptor(property); - Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration); - Object.defineProperty(CSSStyleDeclaration.prototype, dashedToCamelCase(property), declaration); + Object.defineProperty(CSSStyleDeclarationImpl.prototype, property, declaration); + Object.defineProperty( + CSSStyleDeclarationImpl.prototype, + cssPropertyToIDLAttribute(property), + declaration + ); } }); allExtraProperties.forEach(function(property) { if (!implementedProperties.has(property)) { var declaration = getBasicPropertyDescriptor(property); - Object.defineProperty(CSSStyleDeclaration.prototype, property, declaration); - Object.defineProperty(CSSStyleDeclaration.prototype, dashedToCamelCase(property), declaration); + Object.defineProperty(CSSStyleDeclarationImpl.prototype, property, declaration); + Object.defineProperty( + CSSStyleDeclarationImpl.prototype, + cssPropertyToIDLAttribute(property), + declaration + ); + if (property.startsWith('-webkit-')) { + Object.defineProperty( + CSSStyleDeclarationImpl.prototype, + cssPropertyToIDLAttribute(property, /* lowercaseFirst = */ true), + declaration + ); + } } }); -exports.CSSStyleDeclaration = CSSStyleDeclaration; +exports.implementation = CSSStyleDeclarationImpl; diff --git a/lib/CSSStyleDeclaration.test.js b/lib/CSSStyleDeclaration.test.js index 9fa8ed3b..6b6a07ac 100644 --- a/lib/CSSStyleDeclaration.test.js +++ b/lib/CSSStyleDeclaration.test.js @@ -1,20 +1,20 @@ 'use strict'; -var { CSSStyleDeclaration } = require('./CSSStyleDeclaration'); +var CSSStyleDeclaration = require('../index.js'); var allProperties = require('./allProperties'); var allExtraProperties = require('./allExtraProperties'); var implementedProperties = require('./implementedProperties'); -var parsers = require('./parsers'); +var { cssPropertyToIDLAttribute } = require('./parsers'); var dashedProperties = [...allProperties, ...allExtraProperties]; -var allowedProperties = dashedProperties.map(parsers.dashedToCamelCase); -implementedProperties = Array.from(implementedProperties).map(parsers.dashedToCamelCase); +var allowedProperties = ['cssFloat', ...dashedProperties.map(p => cssPropertyToIDLAttribute(p))]; +implementedProperties = Array.from(implementedProperties, p => cssPropertyToIDLAttribute(p)); var invalidProperties = implementedProperties.filter(prop => !allowedProperties.includes(prop)); describe('CSSStyleDeclaration', () => { test('has only valid properties implemented', () => { - expect(invalidProperties.length).toEqual(0); + expect(invalidProperties).toEqual([]); }); test('has all properties', () => { @@ -41,9 +41,6 @@ describe('CSSStyleDeclaration', () => { expect(typeof style.setProperty).toEqual('function'); expect(typeof style.getPropertyPriority).toEqual('function'); expect(typeof style.removeProperty).toEqual('function'); - - // TODO - deprecated according to MDN and not implemented at all, can we remove? - expect(typeof style.getPropertyCSSValue).toEqual('function'); }); test('has special properties', () => { @@ -52,7 +49,6 @@ describe('CSSStyleDeclaration', () => { expect(style.__lookupGetter__('cssText')).toBeTruthy(); expect(style.__lookupSetter__('cssText')).toBeTruthy(); expect(style.__lookupGetter__('length')).toBeTruthy(); - expect(style.__lookupSetter__('length')).toBeTruthy(); expect(style.__lookupGetter__('parentRule')).toBeTruthy(); }); diff --git a/lib/allExtraProperties.js b/lib/allExtraProperties.js index 44b9c296..1a4b8e0b 100644 --- a/lib/allExtraProperties.js +++ b/lib/allExtraProperties.js @@ -16,7 +16,6 @@ module.exports = new Set( 'color-interpolation', 'color-profile', 'color-rendering', - 'css-float', 'enable-background', 'fill', 'fill-opacity', diff --git a/lib/allWebkitProperties.js b/lib/allWebkitProperties.js index d6e71df6..57b2799b 100644 --- a/lib/allWebkitProperties.js +++ b/lib/allWebkitProperties.js @@ -191,4 +191,4 @@ module.exports = [ 'wrap-through', 'writing-mode', 'zoom', -].map(prop => 'webkit-' + prop); +].map(prop => '-webkit-' + prop); diff --git a/lib/parsers.js b/lib/parsers.js index 8ecdf5e3..544996f6 100644 --- a/lib/parsers.js +++ b/lib/parsers.js @@ -447,22 +447,35 @@ exports.parseKeyword = function parseKeyword(val, valid_keywords) { return undefined; }; -// utility to translate from border-width to borderWidth -var dashedToCamelCase = function(dashed) { - var i; - var camel = ''; - var nextCap = false; - for (i = 0; i < dashed.length; i++) { - if (dashed[i] !== '-') { - camel += nextCap ? dashed[i].toUpperCase() : dashed[i]; - nextCap = false; +/** + * utility to translate from border-width to borderWidth + * + * @param {string} property + * @param {boolean} [lowercaseFirst] + * @see https://drafts.csswg.org/cssom/#css-property-to-idl-attribute + */ +function cssPropertyToIDLAttribute(property, lowercaseFirst = false) { + let output = ''; + let uppercaseNext = false; + + if (lowercaseFirst) { + property = property.substring(1); + } + + for (const c of property) { + if (c === '-') { + uppercaseNext = true; + } else if (uppercaseNext) { + uppercaseNext = false; + output += c.toUpperCase(); } else { - nextCap = true; + output += c; } } - return camel; -}; -exports.dashedToCamelCase = dashedToCamelCase; + + return output; +} +exports.cssPropertyToIDLAttribute = cssPropertyToIDLAttribute; var is_space = /\s/; var opening_deliminators = ['"', "'", '(']; @@ -566,7 +579,7 @@ exports.shorthandSetter = function(property, shorthand_for) { Object.keys(obj).forEach(function(subprop) { // in case subprop is an implicit property, this will clear // *its* subpropertiesX - var camel = dashedToCamelCase(subprop); + var camel = cssPropertyToIDLAttribute(subprop); this[camel] = obj[subprop]; // in case it gets translated into something else (0 -> 0px) obj[subprop] = this[camel]; @@ -708,15 +721,15 @@ exports.subImplicitSetter = function(prefix, part, isValid, parser) { }; }; -var camel_to_dashed = /[A-Z]/g; -var first_segment = /^\([^-]\)-/; -var vendor_prefixes = ['o', 'moz', 'ms', 'webkit']; -exports.camelToDashed = function(camel_case) { - var match; - var dashed = camel_case.replace(camel_to_dashed, '-$&').toLowerCase(); - match = dashed.match(first_segment); - if (match && vendor_prefixes.indexOf(match[1]) !== -1) { - dashed = '-' + dashed; - } - return dashed; +const camel_to_dashed = /[A-Z]/g; + +/** + * @param {string} attribute + * @param {boolean} [dashPrefix] + * @see https://drafts.csswg.org/cssom/#idl-attribute-to-css-property + */ +exports.idlAttributeToCSSProperty = (attribute, dashPrefix = false) => { + let output = dashPrefix ? '-' : ''; + output += attribute.replace(camel_to_dashed, '-$&').toLowerCase(); + return output; }; diff --git a/lib/parsers.test.js b/lib/parsers.test.js index 926f7e74..792b4440 100644 --- a/lib/parsers.test.js +++ b/lib/parsers.test.js @@ -116,7 +116,7 @@ describe('parseAngle', () => { describe('parseKeyword', () => { it.todo('test'); }); -describe('dashedToCamelCase', () => { +describe('cssPropertyToIDLAttribute', () => { it.todo('test'); }); describe('shorthandParser', () => { @@ -134,6 +134,6 @@ describe('implicitSetter', () => { describe('subImplicitSetter', () => { it.todo('test'); }); -describe('camelToDashed', () => { +describe('idlAttributeToCSSProperty', () => { it.todo('test'); }); diff --git a/lib/properties/borderSpacing.js b/lib/properties/borderSpacing.js index ff1ce882..d603e21b 100644 --- a/lib/properties/borderSpacing.js +++ b/lib/properties/borderSpacing.js @@ -10,7 +10,7 @@ var parse = function parse(v) { if (v === '' || v === null) { return undefined; } - if (v === 0) { + if (v === '0' || v === 0) { return '0px'; } if (v.toLowerCase() === 'inherit') { diff --git a/package-lock.json b/package-lock.json index 1e7f6ffb..a78d88eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1387,9 +1387,9 @@ "dev": true }, "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==" + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" }, "cssstyle": { "version": "1.2.2", @@ -1398,6 +1398,14 @@ "dev": true, "requires": { "cssom": "0.3.x" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } } }, "dashdash": { @@ -1429,6 +1437,14 @@ "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" + }, + "dependencies": { + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + } } } } @@ -1553,6 +1569,14 @@ "dev": true, "requires": { "webidl-conversions": "^4.0.2" + }, + "dependencies": { + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + } } }, "ecc-jsbn": { @@ -4674,6 +4698,18 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", "dev": true + }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true } } }, @@ -6760,11 +6796,35 @@ } }, "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" + }, + "webidl2": { + "version": "23.12.1", + "resolved": "https://registry.npmjs.org/webidl2/-/webidl2-23.12.1.tgz", + "integrity": "sha512-CbBwugDZl4vO2hH6oY5ZxCZM+6OB3+k8tXzkKTk1y1w4eLExHKMXS/mtIMFBj2vA7JanD8toNcDCp4EH7J3gLg==", "dev": true }, + "webidl2js": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/webidl2js/-/webidl2js-16.2.0.tgz", + "integrity": "sha512-+ujeVqKYxe1efW1xmhS0rM4O1rqtoXOqBM3J96v/8SqID65Chwc9QotVaqCJtaIYqGa0B7lBn1kqUIXk52mgPw==", + "dev": true, + "requires": { + "prettier": "^2.0.4", + "webidl-conversions": "^6.1.0", + "webidl2": "^23.12.1" + }, + "dependencies": { + "prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + } + } + }, "whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -6789,6 +6849,14 @@ "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" + }, + "dependencies": { + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + } } }, "which": { diff --git a/package.json b/package.json index 7ded3070..4d278677 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,14 @@ "lib": "./lib" }, "files": [ + "index.js", + "webidl2js-wrapper.js", "lib/" ], - "main": "./lib/CSSStyleDeclaration.js", + "main": "./index.js", "dependencies": { - "cssom": "~0.3.6" + "cssom": "^0.4.4", + "webidl-conversions": "^6.1.0" }, "devDependencies": { "babel-generator": "~6.26.1", @@ -51,13 +54,16 @@ "npm-run-all": "^4.1.5", "prettier": "~1.18.0", "request": "^2.88.0", - "resolve": "~1.11.1" + "resolve": "~1.11.1", + "webidl2js": "^16.2.0" }, "scripts": { + "prepare": "npm run generate", "download": "node ./scripts/download_latest_properties.js && eslint lib/allProperties.js --fix", - "generate": "run-p generate:*", + "generate": "run-s generate:*", "generate:implemented_properties": "node ./scripts/generate_implemented_properties.js", "generate:properties": "node ./scripts/generate_properties.js", + "generate:webidl2js": "node ./scripts/convert-idl.js", "lint": "npm run generate && eslint . --max-warnings 0", "lint:fix": "eslint . --fix --max-warnings 0", "prepublishOnly": "npm run lint && npm run test", @@ -67,6 +73,6 @@ }, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10.4" } } diff --git a/scripts/convert-idl.js b/scripts/convert-idl.js new file mode 100644 index 00000000..6c32a306 --- /dev/null +++ b/scripts/convert-idl.js @@ -0,0 +1,88 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Transformer = require('webidl2js'); + +const allProperties = require('../lib/allProperties.js'); +const allExtraProperties = require('../lib/allExtraProperties.js'); +const { cssPropertyToIDLAttribute } = require('../lib/parsers.js'); + +const srcDir = path.resolve(__dirname, '../src'); +const implDir = path.resolve(__dirname, '../lib'); +const outputDir = implDir; + +const propertyNames = [ + ...allProperties, + ...Array.from(allExtraProperties).filter(prop => { + return !allProperties.has(prop); + }), +].sort(); + +// TODO: This should be natively supported by WebIDL2JS's Transformer +// https://github.com/jsdom/webidl2js/issues/188 +const genIDL = fs.createWriteStream( + path.resolve(__dirname, '../src/CSSStyleDeclaration-properties.webidl'), + { + encoding: 'utf-8', + } +); + +{ + genIDL.write( + `// autogenerated by scripts/convert-idl.js. do not edit! ${new Date().toISOString()} + +partial interface CSSStyleDeclaration { + // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-camel_cased_attribute +` + ); + + for (const property of propertyNames) { + const camelCasedAttribute = cssPropertyToIDLAttribute(property); + genIDL.write(` [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString _${camelCasedAttribute}; +`); + } + + genIDL.write(` + // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-webkit_cased_attribute +`); + + for (const property of propertyNames) { + if (!property.startsWith('-webkit-')) continue; + const webkitCasedAttribute = cssPropertyToIDLAttribute(property, /* lowercaseFirst = */ true); + genIDL.write(` [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString _${webkitCasedAttribute}; +`); + } + + genIDL.write(` + // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-dashed_attribute +`); + + for (const property of propertyNames) { + if (!property.includes('-')) continue; + genIDL.write(` [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString ${property}; +`); + } + + genIDL.write(`}; +`); + + genIDL.end(err => { + if (err) { + throw err; + } + }); +} + +const transformer = new Transformer({ + implSuffix: '-impl', + // TODO: Add support for `[CEReactions]` +}); + +transformer.addSource(srcDir, implDir); +new Promise(resolve => genIDL.on('finish', resolve)) + .then(() => transformer.generate(outputDir)) + .catch(err => { + console.error(err.stack); + process.exit(1); + }); diff --git a/scripts/download_latest_properties.js b/scripts/download_latest_properties.js index 15f85123..177651d0 100644 --- a/scripts/download_latest_properties.js +++ b/scripts/download_latest_properties.js @@ -23,7 +23,7 @@ var path = require('path'); var request = require('request'); -const { camelToDashed } = require('../lib/parsers'); +const { idlAttributeToCSSProperty } = require('../lib/parsers'); var url = 'https://www.w3.org/Style/CSS/all-properties.en.json'; @@ -69,7 +69,7 @@ request(url, function(error, response, body) { out_file.write('/*\n *\n * https://www.w3.org/Style/CSS/all-properties.en.html\n */\n\n'); out_file.write( 'module.exports = new Set(' + - JSON.stringify(CSSpropertyNames.map(camelToDashed), null, 2) + + JSON.stringify(CSSpropertyNames.map(p => idlAttributeToCSSProperty(p)), null, 2) + ');\n' ); diff --git a/scripts/generate_implemented_properties.js b/scripts/generate_implemented_properties.js index caa88f12..30e255d2 100644 --- a/scripts/generate_implemented_properties.js +++ b/scripts/generate_implemented_properties.js @@ -4,12 +4,18 @@ const fs = require('fs'); const path = require('path'); const t = require('babel-types'); const generate = require('babel-generator').default; -const camelToDashed = require('../lib/parsers').camelToDashed; +const { idlAttributeToCSSProperty } = require('../lib/parsers'); +const webkitPropertyName = /^webkit[A-Z]/; const dashedProperties = fs .readdirSync(path.resolve(__dirname, '../lib/properties')) - .filter(propertyFile => propertyFile.substr(-3) === '.js') - .map(propertyFile => camelToDashed(propertyFile.replace('.js', ''))); + .filter(propertyFile => path.extname(propertyFile) === '.js') + .map(propertyFile => { + return idlAttributeToCSSProperty( + path.basename(propertyFile, '.js'), + /* dashPrefix = */ webkitPropertyName.test(propertyFile) + ); + }); const out_file = fs.createWriteStream(path.resolve(__dirname, '../lib/implementedProperties.js'), { encoding: 'utf-8', diff --git a/scripts/generate_properties.js b/scripts/generate_properties.js index 33a42728..917db424 100644 --- a/scripts/generate_properties.js +++ b/scripts/generate_properties.js @@ -8,7 +8,7 @@ var generate = require('babel-generator').default; var traverse = require('babel-traverse').default; var resolve = require('resolve'); -var camelToDashed = require('../lib/parsers').camelToDashed; +var { idlAttributeToCSSProperty, cssPropertyToIDLAttribute } = require('../lib/parsers'); var basename = path.basename; var dirname = path.dirname; @@ -56,12 +56,17 @@ function isRequire(node, filename) { } } +const webkitPropertyName = /^webkit[A-Z]/; + // step 1: parse all files and figure out their dependencies var parsedFilesByPath = {}; property_files.map(function(property) { var filename = path.resolve(__dirname, '../lib/properties/' + property); var src = fs.readFileSync(filename, 'utf8'); property = basename(property, '.js'); + if (webkitPropertyName.test(property)) { + property = property[0].toUpperCase() + property.substring(1); + } var ast = babylon.parse(src); var dependencies = []; traverse(ast, { @@ -254,13 +259,21 @@ parsedFiles.forEach(function(file) { }); var propertyDefinitions = []; parsedFiles.forEach(function(file) { - var dashed = camelToDashed(file.property); + var dashed = idlAttributeToCSSProperty(file.property); propertyDefinitions.push( t.objectProperty( t.identifier(file.property), t.identifier(file.property + '_export_definition') ) ); + if (dashed.startsWith('-webkit-')) { + propertyDefinitions.push( + t.objectProperty( + t.identifier(cssPropertyToIDLAttribute(dashed, /* lowercaseFirst = */ true)), + t.identifier(file.property + '_export_definition') + ) + ); + } if (file.property !== dashed) { propertyDefinitions.push( t.objectProperty(t.stringLiteral(dashed), t.identifier(file.property + '_export_definition')) diff --git a/src/CSSStyleDeclaration.webidl b/src/CSSStyleDeclaration.webidl new file mode 100644 index 00000000..6c3d3357 --- /dev/null +++ b/src/CSSStyleDeclaration.webidl @@ -0,0 +1,18 @@ +// https://drafts.csswg.org/cssom/#cssomstring +typedef DOMString CSSOMString; + +// https://drafts.csswg.org/cssom/#cssstyledeclaration +[Exposed=Window] +interface CSSStyleDeclaration { + [CEReactions] attribute CSSOMString cssText; + readonly attribute unsigned long length; + getter CSSOMString item(unsigned long index); + CSSOMString getPropertyValue(CSSOMString property); + CSSOMString getPropertyPriority(CSSOMString property); + [CEReactions] void setProperty(CSSOMString property, [LegacyNullToEmptyString] CSSOMString value, optional [LegacyNullToEmptyString] CSSOMString priority = ""); + [CEReactions] CSSOMString removeProperty(CSSOMString property); + readonly attribute CSSRule? parentRule; + [CEReactions] attribute [LegacyNullToEmptyString] CSSOMString cssFloat; +}; + +// Additional partial interfaces are generated by `scripts/convert-idl.js`. diff --git a/webidl2js-wrapper.js b/webidl2js-wrapper.js new file mode 100644 index 00000000..0c6f5215 --- /dev/null +++ b/webidl2js-wrapper.js @@ -0,0 +1,2 @@ +'use strict'; +module.exports = require('./lib/CSSStyleDeclaration.js');