From 81bd2febe4f47e09f296836f28397dd3363d8a8c Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 1 Nov 2021 19:17:03 +0100 Subject: [PATCH] Refactor --- .gitignore | 6 +- build.js | 485 +++++++++++++++++++++------------------------------ index.js | 9 + package.json | 1 - test.js | 47 +++-- 5 files changed, 250 insertions(+), 298 deletions(-) diff --git a/.gitignore b/.gitignore index c977c85..15c4d8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -.DS_Store -*.d.ts -*.log coverage/ node_modules/ +*.d.ts +*.log +.DS_Store yarn.lock diff --git a/build.js b/build.js index d4e0713..bea32d2 100644 --- a/build.js +++ b/build.js @@ -2,323 +2,242 @@ import fs from 'node:fs' import https from 'node:https' import {bail} from 'bail' import concat from 'concat-stream' -import alphaSort from 'alpha-sort' import {unified} from 'unified' import parse from 'rehype-parse' import {select, selectAll} from 'hast-util-select' import {toString} from 'hast-util-to-string' import {isEventHandler} from 'hast-util-is-event-handler' -/** - * @typedef {import('http').IncomingMessage} IncomingMessage - * - * @typedef {import('hast').Element} Element - * - * @typedef {Object.>} Map - */ +const processor = unified().use(parse) -const proc = unified().use(parse) +const own = {}.hasOwnProperty -let actual = 0 const expected = 3 -/** @type {Map} */ -const all = {} - -https.get('https://www.w3.org/TR/SVG11/attindex.html', onsvg1) -https.get('https://www.w3.org/TR/SVGTiny12/attributeTable.html', ontiny) -https.get('https://www.w3.org/TR/SVG2/attindex.html', onsvg2) - -/** - * @param {IncomingMessage} response - */ -function onsvg1(response) { - response.pipe(concat(onconcat)).on('error', bail) - - /** - * @param {Buffer} buf - */ - function onconcat(buf) { - const tree = proc.parse(buf) - /** @type {Map} */ - const map = {} - /** @type {Element[]} */ - const nodes = selectAll('.property-table tr', tree) - - if (nodes.length === 0) { - throw new Error('Couldn’t find rows in SVG 1') - } - - nodes.forEach(each) - - done(map) - - /** - * @param {Element} node - */ - function each(node) { - /** @type {Element[]} */ - const elements = selectAll('.element-name', node) - - selectAll('.attr-name', node).forEach((name) => { - elements - .map(toString) - .map(clean) - .forEach(add(map, clean(toString(name)))) +/** @type {Array>>} */ +const maps = [] + +https.get('https://www.w3.org/TR/SVG11/attindex.html', (response) => { + response + .pipe( + concat((buf) => { + /** @type {Record>} */ + const map = {} + const tree = processor.parse(buf) + const rows = selectAll('.property-table tr', tree) + let index = -1 + + if (rows.length === 0) { + throw new Error('Couldn’t find rows in SVG 1') + } + + while (++index < rows.length) { + const row = rows[index] + const attributes = selectAll('.attr-name', row) + const elements = selectAll('.element-name', row) + let elementIndex = -1 + + while (++elementIndex < elements.length) { + const element = toString(elements[elementIndex]).replace( + /[‘’]/g, + '' + ) + let attributeIndex = -1 + + if (!own.call(map, element)) { + map[element] = new Set() + } + + while (++attributeIndex < attributes.length) { + const attribute = toString(attributes[attributeIndex]).replace( + /[‘’]/g, + '' + ) + map[element].add(attribute) + } + } + } + + maps.push(map) + done() }) - } - - /** - * @param {string} value - * @returns {string} - */ - function clean(value) { - return value.replace(/[‘’]/g, '') - } - } -} - -/** - * @param {IncomingMessage} response - */ -function ontiny(response) { - response.pipe(concat(onconcat)).on('error', bail) - - /** - * @param {Buffer} buf - */ - function onconcat(buf) { - const tree = proc.parse(buf) - /** @type {Map} */ - const map = {} - /** @type {Element[]} */ - const nodes = selectAll('#attributes .attribute', tree) - - if (nodes.length === 0) { - throw new Error('Couldn’t find nodes in SVG Tiny') - } - - nodes.forEach(each) - - done(map) - - /** - * @param {Element} node - */ - function each(node) { - /** @type {Element[]} */ - const all = selectAll('.element', node) - - all - .map(toString) - .forEach(add(map, toString(select('.attribute-name', node)))) - } - } -} - -/** - * @param {IncomingMessage} response - */ -function onsvg2(response) { - response.pipe(concat(onconcat)).on('error', bail) - - /** - * @param {Buffer} buf - */ - function onconcat(buf) { - const tree = proc.parse(buf) - /** @type {Map} */ - const map = {} - /** @type {Element[]} */ - const nodes = selectAll('tbody tr', tree) - - if (nodes.length === 0) { - throw new Error('Couldn’t find nodes in SVG 2') - } - - nodes.forEach(each) - - done(map) - - /** - * @param {Element} node - */ - function each(node) { - /** @type {Element[]} */ - const all = selectAll('.element-name span', node) - - all - .map(toString) - .forEach(add(map, toString(select('.attr-name span', node)))) - } - } -} - -/** - * Add a map. - * - * @param {Map} map - */ -function done(map) { - merge(all, clean(map)) - cleanAll(all) - - actual++ - - if (actual === expected) { - fs.writeFile( - 'index.js', - 'export const svgElementAttributes = ' + - JSON.stringify(sort(all), null, 2) + - '\n', - bail ) - } -} + .on('error', bail) +}) + +https.get('https://www.w3.org/TR/SVGTiny12/attributeTable.html', (response) => { + response + .pipe( + concat((buf) => { + /** @type {Record>} */ + const map = {} + const tree = processor.parse(buf) + const rows = selectAll('#attributes .attribute', tree) + let index = -1 + + if (rows.length === 0) { + throw new Error('Couldn’t find nodes in SVG Tiny') + } + + while (++index < rows.length) { + const row = rows[index] + const attribute = toString(select('.attribute-name', row)) + const elements = selectAll('.element', row) + let elementIndex = -1 + + while (++elementIndex < elements.length) { + const element = toString(elements[elementIndex]) + + if (!own.call(map, element)) { + map[element] = new Set() + } + + map[element].add(attribute) + } + } + + maps.push(map) + done() + }) + ) + .on('error', bail) +}) + +https.get('https://www.w3.org/TR/SVG2/attindex.html', (response) => { + response + .pipe( + concat((buf) => { + /** @type {Record>} */ + const map = {} + const tree = processor.parse(buf) + const rows = selectAll('tbody tr', tree) + let index = -1 + + if (rows.length === 0) { + throw new Error('Couldn’t find rows in SVG 2') + } + + while (++index < rows.length) { + const row = rows[index] + const attribute = toString(select('.attr-name span', row)) + const elements = selectAll('.element-name span', row) + let elementIndex = -1 + + while (++elementIndex < elements.length) { + const element = toString(elements[elementIndex]) + + if (!own.call(map, element)) { + map[element] = new Set() + } + + map[element].add(attribute) + } + } + + maps.push(map) + done() + }) + ) + .on('error', bail) +}) /** - * Add to a map. - * - * @param {Map} map - * @param {string} name Attribute name + * Add a map. */ -function add(map, name) { - if ( - isEventHandler(name) || - name === 'role' || - name.slice(0, 5) === 'aria-' || - name.slice(0, 3) === 'ev:' || - name.slice(0, 4) === 'xml:' || - name.slice(0, 6) === 'xlink:' - ) { - return noop +function done() { + if (maps.length !== expected) { + return } - return fn - - /** - * @param {string} tagName Element name - */ - function fn(tagName) { - const attributes = map[tagName] || (map[tagName] = []) - - if (!attributes.includes(name)) { - attributes.push(name) - } - } + /** @type {Set} */ + const globals = new Set() - function noop() {} -} + let index = -1 + while (++index < maps.length) { + const map = maps[index] + /** @type {Set} */ + let all = new Set() + /** @type {string} */ + let tagName -/** - * @param {Map} map - * @returns {Map} - */ -function clean(map) { - /** @type {Map} */ - let result = {} - /** @type {Array.} */ - const list = [] - /** @type {Array.} */ - const globals = [] - - // Find all used attributes. - Object.keys(map).forEach(function (tagName) { - map[tagName].forEach(function (attribute) { - if (!list.includes(attribute)) { - list.push(attribute) + // Ttrack all attributes. + for (tagName in map) { + if (own.call(map, tagName)) { + all = new Set([...all, ...map[tagName]]) } - }) - }) + } - // Find global attributes. - list.forEach(function (attribute) { - let global = true + // Figure out globals. + // We have to do this per map. /** @type {string} */ - let key + let attribute + + for (attribute of all) { + let global = true - for (key in map) { - if (!map[key].includes(attribute)) { - global = false - break + for (tagName in map) { + if (own.call(map, tagName) && !map[tagName].has(attribute)) { + global = false + break + } } - } - if (global) { - globals.push(attribute) + if (global) { + globals.add(attribute) + } } - }) - - // Remove globals. - result = { - '*': globals } - Object.keys(map) - .sort() - .forEach(function (tagName) { - const attributes = map[tagName] - .filter(function (attribute) { - return !globals.includes(attribute) - }) - .sort() - - if (attributes.length > 0) { - result[tagName] = attributes - } - }) + /** @type {Record>} */ + const merged = {'*': globals} - return result -} - -/** - * @param {Map} left - * @param {Map} right - */ -function merge(left, right) { - Object.keys(right).forEach(each) - - /** - * @param {string} tagName - */ - function each(tagName) { - left[tagName] = (left[tagName] || []) - .concat(right[tagName]) - .filter(function (attribute, index, list) { - return list.indexOf(attribute) === index - }) - .sort() - } -} + index = -1 + while (++index < maps.length) { + const map = maps[index] + /** @type {string} */ + let tagName -/** - * @param {Map} map - */ -function cleanAll(map) { - const globals = map['*'] + for (tagName in map) { + if (own.call(map, tagName)) { + const existing = own.call(merged, tagName) ? [...merged[tagName]] : [] + const list = [...map[tagName]].filter((d) => !globals.has(d)) - Object.keys(map).forEach(function (tagName) { - if (tagName !== '*') { - map[tagName] = map[tagName].filter(function (attribute) { - return !globals.includes(attribute) - }) + merged[tagName] = new Set([...existing, ...list]) + } } - }) -} + } -/** - * @param {Map} map - * @returns {Map} - */ -function sort(map) { - /** @type {Map} */ + /** @type {Record>} */ const result = {} - - Object.keys(map) - .sort(alphaSort()) - .forEach(function (key) { - result[key] = map[key] + const keys = Object.keys(merged).sort() + /** @type {string} */ + let tagName + + for (tagName of keys) { + const list = [...merged[tagName]] + result[tagName] = list.sort().filter((d) => { + return !( + isEventHandler(d) || + d === 'role' || + d.slice(0, 5) === 'aria-' || + d.slice(0, 3) === 'ev:' || + d.slice(0, 4) === 'xml:' || + d.slice(0, 6) === 'xlink:' + ) }) + } - return result + fs.writeFile( + 'index.js', + [ + '/**', + ' * Map of SVG elements to allowed attributes.', + ' *', + ' * @type {Record>}', + ' */', + 'export const svgElementAttributes = ' + JSON.stringify(result, null, 2), + '' + ].join('\n'), + bail + ) } diff --git a/index.js b/index.js index fd36083..7b5722f 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,8 @@ +/** + * Map of SVG elements to allowed attributes. + * + * @type {Record>} + */ export const svgElementAttributes = { '*': [ 'about', @@ -173,6 +178,8 @@ export const svgElementAttributes = { 'x', 'y' ], + altGlyphDef: [], + altGlyphItem: [], animate: [ 'accumulate', 'additive', @@ -2143,6 +2150,7 @@ export const svgElementAttributes = { ], 'font-face-format': ['string'], 'font-face-name': ['name'], + 'font-face-src': [], 'font-face-uri': ['externalResourcesRequired'], foreignObject: [ 'alignment-baseline', @@ -3424,6 +3432,7 @@ export const svgElementAttributes = { 'systemLanguage', 'to' ], + solidColor: [], stop: [ 'alignment-baseline', 'baseline-shift', diff --git a/package.json b/package.json index ba9cdce..d7def74 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@types/hast": "^2.0.0", "@types/node": "^16.0.0", "@types/tape": "^4.0.0", - "alpha-sort": "^5.0.0", "bail": "^2.0.0", "c8": "^7.0.0", "concat-stream": "^2.0.0", diff --git a/test.js b/test.js index f07f2c0..07a3d4c 100644 --- a/test.js +++ b/test.js @@ -2,27 +2,52 @@ import assert from 'node:assert' import test from 'tape' import {svgElementAttributes} from './index.js' +const own = {}.hasOwnProperty + test('svgElementAttributes', function (t) { t.equal(typeof svgElementAttributes, 'object', 'should be an `object`') t.doesNotThrow(function () { - for (const name of Object.keys(svgElementAttributes)) { - assert.ok(Array.isArray(svgElementAttributes[name]), name) + /** @type {string} */ + let tagName + + for (tagName in svgElementAttributes) { + if (own.call(svgElementAttributes, tagName)) { + assert.ok(Array.isArray(svgElementAttributes[tagName]), tagName) + } } }, 'values should be array') t.doesNotThrow(function () { - for (const name of Object.keys(svgElementAttributes)) { - const props = svgElementAttributes[name] - - for (const prop of props) { - const label = prop + ' in ' + name - assert.strictEqual(typeof prop, 'string', label + ' should be string') - assert.strictEqual(prop, prop.trim(), label + ' should be trimmed') - assert.ok(/^[a-z][a-z\d-]*$/i.test(prop), label + ' should be `a-z-`') + /** @type {string} */ + let tagName + + for (tagName in svgElementAttributes) { + if (own.call(svgElementAttributes, tagName)) { + const attributes = svgElementAttributes[tagName] + let index = -1 + + while (++index < attributes.length) { + const attribute = attributes[index] + const label = attribute + ' in ' + tagName + assert.strictEqual( + typeof attribute, + 'string', + label + ' should be string' + ) + assert.strictEqual( + attribute, + attribute.trim(), + label + ' should be trimmed' + ) + assert.ok( + /^[a-z][a-z\d-]*$/i.test(attribute), + label + ' should be `a-z-`' + ) + } } } - }, 'name should be lower-case, alphabetical strings') + }, 'name should be lowercase, alphabetical strings') t.end() })