diff --git a/package.json b/package.json index e9971cf..9f9494d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "sideEffects": false, "type": "module", "workspaces": [ + "packages/html-enumerated-attributes", "packages/html-url-attributes", "packages/html-whitespace-sensitive-tag-names", "packages/hast-util-from-string", @@ -76,6 +77,7 @@ "node-fetch": "^2.0.0", "parse-author": "^2.0.0", "prettier": "^2.0.0", + "property-information": "^6.0.0", "rehype": "^12.0.0", "rehype-cli": "^11.0.0", "rehype-format": "^3.0.0", diff --git a/packages/html-enumerated-attributes/.npmrc b/packages/html-enumerated-attributes/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/packages/html-enumerated-attributes/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/html-enumerated-attributes/index.js b/packages/html-enumerated-attributes/index.js new file mode 100644 index 0000000..35cc48d --- /dev/null +++ b/packages/html-enumerated-attributes/index.js @@ -0,0 +1,503 @@ +/** + * @fileoverview + * Map of enumerated attributes in HTML + * @longdescription + * ## Use + * + * ```js + * import {enumeratedAttributes} from 'html-enumerated-attributes' + * + * enumeratedAttributes.loading + * //=> {selector: 'iframe, img', invalid: 'eager', missing: 'eager', states: ['eager', 'lazy']} + * ``` + * + * ## API + * + * ### `enumeratedAttributes` + * + * Map of enumerated attributes in HTML (`Record>`). + */ + +/** + * @typedef Definition + * @property {string} [selector] + * @property {string|null} [missing] + * @property {string|null} [invalid] + * @property {Array.} states + * @property {true} [allowUnknown] + * @property {true} [caseSensitive] + */ + +/** + * This map exposes a map of property names to one or more definitions. + * Each definition defines how that attribute is enumerated. + * + * @type {Record} + */ +export const enumeratedAttributes = { + autocomplete: { + selector: 'form', + missing: '', + invalid: '', + states: [['', 'on'], 'off'] + }, + behavior: { + selector: 'marquee', + missing: 'scroll', + states: ['alternate', 'scroll', 'slide'] + }, + charset: { + selector: 'meta, script', + // In HTML5, utf8 is implied. + // But we let it be here for older versions. + states: [ + ['utf8', 'utf-8', 'unicode-1-1-utf-8'], + ['866', 'cp866', 'ibm866', 'csibm866'], + [ + 'l1', + 'ascii', + 'cp819', + 'cp1252', + 'ibm819', + 'latin1', + 'us-ascii', + 'x-cp1252', + 'iso88591', + 'iso8859-1', + 'iso_8859-1', + 'iso-8859-1', + 'iso-ir-100', + 'csisolatin1', + 'windows-1252', + 'ansi_x3.4-1968', + 'iso_8859-1:1987' + ], + [ + 'l2', + 'csisolatin2', + 'iso-8859-2', + 'iso-ir-101', + 'iso8859-2', + 'iso88592', + 'iso_8859-2', + 'iso_8859-2:1987', + 'latin2' + ], + [ + 'l3', + 'csisolatin3', + 'iso-8859-3', + 'iso-ir-109', + 'iso8859-3', + 'iso88593', + 'iso_8859-3', + 'iso_8859-3:1988', + 'latin3' + ], + [ + 'l4', + 'csisolatin4', + 'iso-8859-4', + 'iso-ir-110', + 'iso8859-4', + 'iso88594', + 'iso_8859-4', + 'iso_8859-4:1988', + 'latin4' + ], + [ + 'l5', + 'latin5', + 'cp1254', + 'x-cp1254', + 'iso88599', + 'iso8859-9', + 'iso-8859-9', + 'iso_8859-9', + 'iso-ir-148', + 'csisolatin5', + 'windows-1254', + 'iso_8859-9:1989' + ], + [ + 'l6', + 'latin6', + 'iso885910', + 'iso-ir-157', + 'iso8859-10', + 'csisolatin6', + 'iso-8859-10' + ], + [ + 'l9', + 'iso885915', + 'iso8859-15', + 'iso-8859-15', + 'iso_8859-15', + 'csisolatin9' + ], + ['cp1250', 'x-cp1250', 'windows-1250'], + ['cp1251', 'x-cp1251', 'windows-1251'], + ['cp1253', 'x-cp1253', 'windows-1253'], + ['cp1255', 'x-cp1255', 'windows-1255'], + ['cp1256', 'x-cp1256', 'windows-1256'], + ['cp1257', 'x-cp1257', 'windows-1257'], + ['cp1258', 'x-cp1258', 'windows-1258'], + [ + 'cyrillic', + 'iso88595', + 'iso8859-5', + 'iso-8859-5', + 'iso_8859-5', + 'iso-ir-144', + 'iso_8859-5:1988', + 'csisolatincyrillic' + ], + [ + 'arabic', + 'iso88596', + 'ecma-114', + 'asmo-708', + 'iso8859-6', + 'iso-ir-127', + 'iso_8859-6', + 'iso-8859-6', + 'csiso88596e', + 'csiso88596i', + 'iso-8859-6-e', + 'iso-8859-6-i', + 'iso_8859-6:1987', + 'csisolatinarabic' + ], + [ + 'greek', + 'greek8', + 'iso88597', + 'ecma-118', + 'elot_928', + 'iso8859-7', + 'iso-8859-7', + 'iso_8859-7', + 'iso-ir-126', + 'sun_eu_greek', + 'iso_8859-7:1987', + 'csisolatingreek' + ], + [ + 'hebrew', + 'visual', + 'iso88598', + 'iso8859-8', + 'iso-8859-8', + 'iso_8859-8', + 'iso-ir-138', + 'csiso88598e', + 'iso-8859-8-e', + 'iso_8859-8:1988', + 'csisolatinhebrew' + ], + ['logical', 'csiso88598i', 'iso-8859-8-i'], + ['iso885913', 'iso8859-13', 'iso-8859-13'], + ['iso885914', 'iso8859-14', 'iso-8859-14'], + ['iso-8859-16'], + ['koi', 'koi8', 'koi8-r', 'koi8_r', 'cskoi8r'], + ['koi8-u', 'koi8-ru'], + ['mac', 'macintosh', 'csmacintosh', 'x-mac-roman'], + [ + 'dos-874', + 'tis-620', + 'iso885911', + 'iso8859-11', + 'iso-8859-11', + 'windows-874' + ], + ['x-mac-cyrillic', 'x-mac-ukrainian'], + [ + 'gbk', + 'x-gbk', + 'gb2312', + 'chinese', + 'gb_2312', + 'csgb2312', + 'iso-ir-58', + 'gb_2312-80', + 'csiso58gb231280' + ], + ['gb18030'], + ['big5', 'csbig5', 'cn-big5', 'x-x-big5', 'big5-hkscs'], + ['euc-jp', 'x-euc-jp', 'cseucpkdfmtjapanese'], + ['csiso2022jp', 'iso-2022-jp'], + [ + 'ms932', + 'sjis', + 'x-sjis', + 'ms_kanji', + 'shift-jis', + 'shift_jis', + 'csshiftjis', + 'windows-31j' + ], + [ + 'korean', + 'euc-kr', + 'cseuckr', + 'ksc5601', + 'ksc_5601', + 'iso-ir-149', + 'windows-949', + 'csksc56011987', + 'ks_c_5601-1987', + 'ks_c_5601-1989' + ], + [ + 'hz-gb-2312', + 'csiso2022kr', + 'iso-2022-kr', + 'iso-2022-cn', + 'iso-2022-cn-ext' + ], + ['utf-16be'], + ['utf-16', 'utf-16le'], + ['x-user-defined'] + ] + }, + contenteditable: { + missing: null, + invalid: null, + states: [null, ['', 'true'], 'false'] + }, + crossorigin: { + selector: 'link, img, audio, video, script', + missing: null, + invalid: '', + states: [['', 'anonymous'], 'use-credentials'] + }, + decoding: { + selector: 'img', + missing: '', + invalid: '', + states: ['sync', 'async', ['', 'auto']] + }, + dir: { + missing: '', + invalid: '', + states: ['', 'ltr', 'rtl', 'auto'] + }, + direction: { + selector: 'marquee', + missing: 'left', + states: ['left', 'right', 'up', 'down'] + }, + draggable: { + missing: null, + states: [null, 'true', 'false'] + }, + // When changing `encType`, please also change `formenctype`. + enctype: { + selector: 'form', + invalid: 'application/x-www-form-urlencoded', + missing: 'application/x-www-form-urlencoded', + states: [ + 'application/x-www-form-urlencoded', + 'multipart/form-data', + 'text/plain' + ] + }, + // When changing `formenctype`, please also change `encType`. + formenctype: { + selector: 'button, input', + invalid: 'application/x-www-form-urlencoded', + // Note that `missing: null` here is intentionally different from `encType`. + missing: null, + states: [ + 'application/x-www-form-urlencoded', + 'multipart/form-data', + 'text/plain' + ] + }, + // When changing `formmethod`, please also change `method`. + formmethod: { + selector: 'button, input', + invalid: 'get', + // Note that `missing: null` here is intentionally different from `formmethod`. + missing: null, + states: ['dialog', 'get', 'post'] + }, + // When changing `formtarget`, please also change `target`. + formtarget: { + selector: 'button, input', + // Note that `missing: null` here is intentionally different from `target`. + missing: null, + allowUnknown: true, + // Note that `formtarget` uses `_self` and `target` uses `['', '_self']`, + // which is intentional. + states: ['_blank', '_parent', '_self', '_top'] + }, + inputmode: { + // In fact only applies to `text`, `search`, and `password`. + selector: 'input', + invalid: '', + missing: '', + states: [ + '', + 'email', + 'full-width-latin', + 'kana', + 'kana-name', + 'katakana', + 'latin', + 'latin-name', + 'latin-prose', + 'numeric', + 'tel', + 'url', + 'verbatim' + ] + }, + keytype: { + selector: 'keygen', + missing: 'rsa', + states: ['', 'rsa'] + }, + kind: { + selector: 'track', + missing: 'subtitles', + invalid: 'metadata', + states: ['captions', 'chapters', 'descriptions', 'metadata', 'subtitles'] + }, + loading: { + selector: 'iframe, img', + invalid: 'eager', + missing: 'eager', + states: ['eager', 'lazy'] + }, + // When changing `method`, please also change `formmethod`. + method: { + selector: 'form', + invalid: 'get', + missing: 'get', + states: ['dialog', 'get', 'post'] + }, + preload: { + selector: 'audio, video', + // Note: https://html.spec.whatwg.org/#attr-media-preload + states: [['', 'auto'], 'metadata', 'none'] + }, + // Should also apply to `content` on `meta[name=referrer]`. + referrerpolicy: { + selector: 'a, area, iframe, img, link', + missing: '', + invalid: '', + states: [ + '', + 'no-referrer', + 'no-referrer-when-downgrade', + 'origin', + 'origin-when-cross-origin', + 'unsafe-url' + ] + }, + scope: { + selector: 'th', + missing: '', + states: ['', 'col', 'colgroup', 'row', 'rowgroup'] + }, + shape: { + selector: 'area', + missing: 'rect', + states: [ + // The latter are non-conforming. + ['rect', 'rectangle'], + ['poly', 'polygon'], + ['circle', 'circ'], + 'default' + ] + }, + spellcheck: { + missing: null, + invalid: null, + states: [null, ['', 'true'], 'false'] + }, + // When changing `target`, please also change `formtarget`. + target: { + selector: 'a, area, base, form', + missing: '', + allowUnknown: true, + states: ['_blank', '_parent', ['', '_self'], '_top'] + }, + translate: { + missing: null, + invalid: null, + states: [['', 'yes'], 'no'] + }, + type: [ + { + selector: 'button', + missing: 'submit', + states: ['button', 'menu', 'reset', 'submit'] + }, + { + selector: 'input', + missing: 'text', + states: [ + 'button', + 'checkbox', + 'color', + 'date', + 'datetime-local', + 'email', + 'file', + 'hidden', + 'image', + 'number', + 'month', + 'password', + 'radio', + 'range', + 'reset', + 'search', + 'submit', + 'tel', + 'text', + 'time', + 'url', + 'week' + ] + }, + { + caseSensitive: true, + selector: 'li', + missing: '', + invalid: '', + states: ['1', 'a', 'A', 'i', 'I', 'circle', 'disc', 'square'] + }, + { + selector: 'menu', + missing: '', + states: ['', 'context', 'toolbar'] + }, + { + selector: 'menuitem', + missing: 'command', + states: ['checkbox', 'command', 'radio'] + }, + { + caseSensitive: true, + selector: 'ol', + missing: '1', + invalid: '1', + states: ['1', 'a', 'A', 'i', 'I'] + }, + { + selector: 'ul', + missing: '', + invalid: '', + states: ['circle', 'disc', 'square'] + } + ], + wrap: { + selector: 'textarea', + missing: 'soft', + states: ['hard', 'soft'] + } +} diff --git a/packages/html-enumerated-attributes/package.json b/packages/html-enumerated-attributes/package.json new file mode 100644 index 0000000..bbf0615 --- /dev/null +++ b/packages/html-enumerated-attributes/package.json @@ -0,0 +1,42 @@ +{ + "name": "html-enumerated-attributes", + "version": "0.0.0", + "description": "Map of info on enumerated attributes in HTML", + "license": "MIT", + "keywords": [ + "html", + "attribute", + "property", + "enumerated", + "attribute" + ], + "repository": "https://github.com/rehypejs/rehype-minify/tree/main/packages/html-enumerated-attributes", + "bugs": "https://github.com/rehypejs/rehype-minify/issues", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "author": "Titus Wormer (https://wooorm.com)", + "contributors": [ + "Titus Wormer (https://wooorm.com)" + ], + "sideEffects": false, + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "files": [ + "index.d.ts", + "index.js" + ], + "scripts": { + "build": "rimraf \"*.d.ts\" && tsc && type-coverage", + "test": "node --conditions development test.js" + }, + "xo": false, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true + } +} diff --git a/packages/html-enumerated-attributes/readme.md b/packages/html-enumerated-attributes/readme.md new file mode 100644 index 0000000..64a3115 --- /dev/null +++ b/packages/html-enumerated-attributes/readme.md @@ -0,0 +1,99 @@ + + +# html-enumerated-attributes + +[![Build][build-badge]][build] +[![Coverage][coverage-badge]][coverage] +[![Downloads][downloads-badge]][downloads] +[![Size][size-badge]][size] +[![Sponsors][sponsors-badge]][collective] +[![Backers][backers-badge]][collective] +[![Chat][chat-badge]][chat] + +Map of enumerated attributes in HTML. + +## Install + +This package is [ESM only][esm]: +Node 12+ is needed to use it and it must be `imported`ed instead of `required`d. + +[npm][]: + +```sh +npm install html-enumerated-attributes +``` + +This package exports the following identifiers: +`enumeratedAttributes`. +There is no default export. + +## Use + +```js +import {enumeratedAttributes} from 'html-enumerated-attributes' + +enumeratedAttributes.loading +//=> {selector: 'iframe, img', invalid: 'eager', missing: 'eager', states: ['eager', 'lazy']} +``` + +## API + +### `enumeratedAttributes` + +Map of enumerated attributes in HTML (`Record>`). + +## Contribute + +See [`contributing.md`][contributing] in [`rehypejs/.github`][health] for ways +to get started. +See [`support.md`][support] for ways to get help. + +This project has a [code of conduct][coc]. +By interacting with this repository, organization, or community you agree to +abide by its terms. + +## License + +[MIT][license] © [Titus Wormer][author] + +[build-badge]: https://github.com/rehypejs/rehype-minify/workflows/main/badge.svg + +[build]: https://github.com/rehypejs/rehype-minify/actions + +[coverage-badge]: https://img.shields.io/codecov/c/github/rehypejs/rehype-minify.svg + +[coverage]: https://codecov.io/github/rehypejs/rehype-minify + +[downloads-badge]: https://img.shields.io/npm/dm/html-enumerated-attributes.svg + +[downloads]: https://www.npmjs.com/package/html-enumerated-attributes + +[size-badge]: https://img.shields.io/bundlephobia/minzip/html-enumerated-attributes.svg + +[size]: https://bundlephobia.com/result?p=html-enumerated-attributes + +[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg + +[backers-badge]: https://opencollective.com/unified/backers/badge.svg + +[collective]: https://opencollective.com/unified + +[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg + +[chat]: https://github.com/rehypejs/rehype/discussions + +[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c + +[npm]: https://docs.npmjs.com/cli/install + +[health]: https://github.com/rehypejs/.github + +[contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md + +[support]: https://github.com/rehypejs/.github/blob/main/support.md + +[coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md + +[license]: https://github.com/rehypejs/rehype-minify/blob/main/license + +[author]: https://wooorm.com diff --git a/packages/html-enumerated-attributes/test.js b/packages/html-enumerated-attributes/test.js new file mode 100644 index 0000000..202267c --- /dev/null +++ b/packages/html-enumerated-attributes/test.js @@ -0,0 +1,22 @@ +import test from 'tape' +import {html, find} from 'property-information' +import {enumeratedAttributes} from './index.js' + +const own = {}.hasOwnProperty + +test('html-enumerated-attributes', (t) => { + /** @type {string} */ + let key + + for (key in enumeratedAttributes) { + if (own.call(enumeratedAttributes, key)) { + t.equal( + find(html, key).attribute, + key, + 'should match html casing (`' + key + '`)' + ) + } + } + + t.end() +}) diff --git a/packages/html-enumerated-attributes/tsconfig.json b/packages/html-enumerated-attributes/tsconfig.json new file mode 100644 index 0000000..7e61871 --- /dev/null +++ b/packages/html-enumerated-attributes/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["*.js"] +} diff --git a/packages/rehype-minify-enumerated-attribute/index.js b/packages/rehype-minify-enumerated-attribute/index.js index dcf8414..89b4b0a 100644 --- a/packages/rehype-minify-enumerated-attribute/index.js +++ b/packages/rehype-minify-enumerated-attribute/index.js @@ -12,18 +12,19 @@ */ import {visit} from 'unist-util-visit' +import {html, find} from 'property-information' import {matches} from 'hast-util-select' import {hasProperty} from 'hast-util-has-property' import {stringify} from 'space-separated-tokens' -import {schema} from './schema.js' - -const own = {}.hasOwnProperty +import {enumeratedAttributes} from 'html-enumerated-attributes' /** * @typedef {import('hast').Root} Root - * @typedef {import('./schema.js').Info} Info + * @typedef {import('html-enumerated-attributes').Definition} Definition */ +const own = {}.hasOwnProperty + /** * Minify enumerated attributes. * @@ -40,26 +41,36 @@ export default function rehypeMinifyEnumeratedAttribute() { let prop for (prop in props) { - if (own.call(schema, prop) && hasProperty(node, prop)) { - let value = props[prop] + if (own.call(props, prop) && hasProperty(node, prop)) { + const attribute = find(html, prop).attribute - // Note: we don’t really handle enumerated as lists, so instead - // we cast them to a string (assuming they are space-separated). - if (Array.isArray(value)) { - value = stringify(value) - } + if (own.call(enumeratedAttributes, attribute)) { + let value = props[prop] - if (typeof value === 'string') { - const info = schema[prop] - const definitions = Array.isArray(info) ? info : [info] - let index = -1 + // Note: we don’t really handle enumerated as lists, so instead + // we cast them to a string (assuming they are space-separated). + if (Array.isArray(value)) { + value = stringify(value) + } - while (++index < definitions.length) { - const definition = definitions[index] + if (typeof value === 'string') { + const definition = enumeratedAttributes[attribute] + const definitions = Array.isArray(definition) + ? definition + : [definition] + let index = -1 // eslint-disable-next-line max-depth - if (!definition.selector || matches(definition.selector, node)) { - props[prop] = minify(value, definition) + while (++index < definitions.length) { + const definition = definitions[index] + + // eslint-disable-next-line max-depth + if ( + !definition.selector || + matches(definition.selector, node) + ) { + props[prop] = minify(value, definition) + } } } } @@ -71,7 +82,7 @@ export default function rehypeMinifyEnumeratedAttribute() { /** * @param {string} value - * @param {Info} info + * @param {Definition} info * @returns {string|null} */ function minify(value, info) { diff --git a/packages/rehype-minify-enumerated-attribute/package.json b/packages/rehype-minify-enumerated-attribute/package.json index 796a81a..28fb739 100644 --- a/packages/rehype-minify-enumerated-attribute/package.json +++ b/packages/rehype-minify-enumerated-attribute/package.json @@ -37,8 +37,10 @@ ], "dependencies": { "@types/hast": "^2.0.0", + "html-enumerated-attributes": "^0.0.0", "hast-util-select": "^5.0.0", "hast-util-has-property": "^2.0.0", + "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "unified": "^10.0.0", "unist-util-visit": "^4.0.0"