Repository: preactjs/preact-render-to-string
Pull request #311 (2023-08-01 15:36:51)

Apply attribute name handling in pretty mode
Follow up to #308 & #310 to apply those updates to the pretty renderer as well.


Patch:
diff --git a/.changeset/plenty-cycles-work.md b/.changeset/plenty-cycles-work.md
new file mode 100644
index 0000000..477347e
--- /dev/null
+++ b/.changeset/plenty-cycles-work.md
@@ -0,0 +1,5 @@
+---
+"preact-render-to-string": patch
+---
+
+Apply attribute name handling in pretty mode
diff --git a/src/index.js b/src/index.js
index 781ccc4..4ceda2a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,11 @@
-import { encodeEntities, styleObjToCss, UNSAFE_NAME } from './util.js';
+import {
+	encodeEntities,
+	styleObjToCss,
+	UNSAFE_NAME,
+	NAMESPACE_REPLACE_REGEX,
+	HTML_LOWER_CASE,
+	SVG_CAMEL_CASE
+} from './util.js';
 import { options, h, Fragment } from 'preact';
 import {
 	CHILDREN,
@@ -454,9 +461,6 @@ function _renderToString(vnode, context, isSvgMode, selectValue, parent) {
 	return s + '>' + html + '</' + type + '>';
 }
 
-const HTML_LOWER_CASE = /^accessK|^auto[A-Z]|^ch|^col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|readO|rowS|spellC|src[A-Z]|tabI|item[A-Z]/;
-const SVG_CAMEL_CASE = /^ac|^ali|arabic|basel|cap|clipPath$|clipRule$|color|dominant|enable|fill|flood|font|glyph[^R]|horiz|image|letter|lighting|marker[^WUH]|overline|panose|pointe|paint|rendering|shape|stop|strikethrough|stroke|text[^L]|transform|underline|unicode|units|^v[^i]|^w|^xH/;
-const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)(:|[A-Z])/;
 const SELF_CLOSING = new Set([
 	'area',
 	'base',
diff --git a/src/pretty.js b/src/pretty.js
index 4e37d92..08f1c69 100644
--- a/src/pretty.js
+++ b/src/pretty.js
@@ -6,8 +6,10 @@ import {
 	getChildren,
 	createComponent,
 	UNSAFE_NAME,
-	XLINK,
-	VOID_ELEMENTS
+	VOID_ELEMENTS,
+	NAMESPACE_REPLACE_REGEX,
+	HTML_LOWER_CASE,
+	SVG_CAMEL_CASE
 } from './util.js';
 import { COMMIT, DIFF, DIFFED, RENDER, SKIP_EFFECTS } from './constants.js';
 import { options, Fragment } from 'preact';
@@ -243,8 +245,21 @@ function _renderToStringPretty(
 			} else if (name === 'className') {
 				if (typeof props.class !== 'undefined') continue;
 				name = 'class';
-			} else if (isSvgMode && XLINK.test(name)) {
-				name = name.toLowerCase().replace(/^xlink:?/, 'xlink:');
+			} else if (name === 'acceptCharset') {
+				name = 'accept-charset';
+			} else if (name === 'httpEquiv') {
+				name = 'http-equiv';
+			} else if (NAMESPACE_REPLACE_REGEX.test(name)) {
+				name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
+			} else if (isSvgMode) {
+				if (SVG_CAMEL_CASE.test(name)) {
+					name =
+						name === 'panose1'
+							? 'panose-1'
+							: name.replace(/([A-Z])/g, '-$1').toLowerCase();
+				}
+			} else if (HTML_LOWER_CASE.test(name)) {
+				name = name.toLowerCase();
 			}
 
 			if (name === 'htmlFor') {
diff --git a/src/util.js b/src/util.js
index c3a598e..bc80ea4 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,6 +1,8 @@
 export const VOID_ELEMENTS = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/;
 export const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
-export const XLINK = /^xlink:?./;
+export const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)(:|[A-Z])/;
+export const HTML_LOWER_CASE = /^accessK|^auto[A-Z]|^ch|^col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|readO|rowS|spellC|src[A-Z]|tabI|item[A-Z]/;
+export const SVG_CAMEL_CASE = /^ac|^ali|arabic|basel|cap|clipPath$|clipRule$|color|dominant|enable|fill|flood|font|glyph[^R]|horiz|image|letter|lighting|marker[^WUH]|overline|panose|pointe|paint|rendering|shape|stop|strikethrough|stroke|text[^L]|transform|underline|unicode|units|^v[^i]|^w|^xH/;
 
 // DOM properties that should NOT have "px" added when numeric
 const ENCODED_ENTITIES = /["&<]/;


Test patch:
diff --git a/test/pretty.test.js b/test/pretty.test.js
index 7d6ecd1..a5812dc 100644
--- a/test/pretty.test.js
+++ b/test/pretty.test.js
@@ -2,10 +2,10 @@ import basicRender from '../src/index.js';
 import { render } from '../src/jsx.js';
 import { h, Fragment } from 'preact';
 import { expect } from 'chai';
-import { dedent } from './utils.js';
+import { dedent, svgAttributes, htmlAttributes } from './utils.js';
 
 describe('pretty', () => {
-	let prettyRender = (jsx) => render(jsx, {}, { pretty: true });
+	let prettyRender = (jsx, opts) => render(jsx, {}, { pretty: true, ...opts });
 
 	it('should render no whitespace by default', () => {
 		let rendered = basicRender(
@@ -196,4 +196,56 @@ describe('pretty', () => {
 	it('should not render function children', () => {
 		expect(prettyRender(<div>{() => {}}</div>)).to.equal('<div></div>');
 	});
+
+	it('should render SVG elements', () => {
+		let rendered = prettyRender(
+			<svg>
+				<image xlinkHref="#" />
+				<foreignObject>
+					<div xlinkHref="#" />
+				</foreignObject>
+				<g>
+					<image xlinkHref="#" />
+				</g>
+			</svg>
+		);
+
+		expect(rendered).to.equal(
+			`<svg>\n\t<image xlink:href="#"></image>\n\t<foreignObject>\n\t\t<div xlink:href="#"></div>\n\t</foreignObject>\n\t<g>\n\t\t<image xlink:href="#"></image>\n\t</g>\n</svg>`
+		);
+	});
+
+	describe('Attribute casing', () => {
+		it('should have correct SVG casing', () => {
+			for (let name in svgAttributes) {
+				let value = svgAttributes[name];
+
+				let rendered = prettyRender(
+					<svg>
+						<path {...{ [name]: 'foo' }} />
+					</svg>
+				);
+				expect(rendered).to.equal(
+					`<svg>\n\t<path ${value}="foo"></path>\n</svg>`
+				);
+			}
+		});
+
+		it('should have correct HTML casing', () => {
+			for (let name in htmlAttributes) {
+				let value = htmlAttributes[name];
+
+				if (name === 'checked') {
+					let rendered = prettyRender(<input type="checkbox" checked />, {
+						jsx: false
+					});
+					expect(rendered).to.equal(`<input type="checkbox" checked />`);
+					continue;
+				} else {
+					let rendered = prettyRender(<div {...{ [name]: 'foo' }} />);
+					expect(rendered).to.equal(`<div ${value}="foo"></div>`);
+				}
+			}
+		});
+	});
 });
diff --git a/test/render.test.js b/test/render.test.js
index 9d00a0c..e6b6fe1 100644
--- a/test/render.test.js
+++ b/test/render.test.js
@@ -19,6 +19,7 @@ import {
 } from 'preact/hooks';
 import { expect } from 'chai';
 import { spy, stub, match } from 'sinon';
+import { svgAttributes, htmlAttributes } from './utils.js';
 
 function shallowRender(vnode) {
 	return renderToStringJSX(vnode, context, {
@@ -1601,248 +1602,6 @@ describe('render', () => {
 
 	describe('Attribute casing', () => {
 		it('should have correct SVG casing', () => {
-			const svgAttributes = {
-				accentHeight: 'accent-height',
-				accumulate: 'accumulate',
-				additive: 'additive',
-				alignmentBaseline: 'alignment-baseline',
-				allowReorder: 'allowReorder', // unsure
-				alphabetic: 'alphabetic',
-				amplitude: 'amplitude',
-				arabicForm: 'arabic-form',
-				ascent: 'ascent',
-				attributeName: 'attributeName',
-				attributeType: 'attributeType',
-				autoReverse: 'autoReverse', // unsure
-				azimuth: 'azimuth',
-				baseFrequency: 'baseFrequency',
-				baselineShift: 'baseline-shift',
-				baseProfile: 'baseProfile',
-				bbox: 'bbox',
-				begin: 'begin',
-				bias: 'bias',
-				by: 'by',
-				calcMode: 'calcMode',
-				capHeight: 'cap-height',
-				class: 'class',
-				clip: 'clip',
-				clipPathUnits: 'clipPathUnits',
-				clipPath: 'clip-path',
-				clipRule: 'clip-rule',
-				color: 'color',
-				colorInterpolation: 'color-interpolation',
-				colorInterpolationFilters: 'color-interpolation-filters',
-				colorProfile: 'color-profile',
-				colorRendering: 'color-rendering',
-				contentScriptType: 'contentScriptType',
-				contentStyleType: 'contentStyleType',
-				crossorigin: 'crossorigin',
-				cursor: 'cursor',
-				cx: 'cx',
-				cy: 'cy',
-				d: 'd',
-				decelerate: 'decelerate',
-				descent: 'descent',
-				diffuseConstant: 'diffuseConstant',
-				direction: 'direction',
-				display: 'display',
-				divisor: 'divisor',
-				dominantBaseline: 'dominant-baseline',
-				dur: 'dur',
-				dx: 'dx',
-				dy: 'dy',
-				edgeMode: 'edgeMode',
-				elevation: 'elevation',
-				enableBackground: 'enable-background',
-				end: 'end',
-				exponent: 'exponent',
-				fill: 'fill',
-				fillOpacity: 'fill-opacity',
-				fillRule: 'fill-rule',
-				filter: 'filter',
-				filterRes: 'filterRes',
-				filterUnits: 'filterUnits',
-				floodColor: 'flood-color',
-				floodOpacity: 'flood-opacity',
-				fontFamily: 'font-family',
-				fontSize: 'font-size',
-				fontSizeAdjust: 'font-size-adjust',
-				fontStretch: 'font-stretch',
-				fontStyle: 'font-style',
-				fontVariant: 'font-variant',
-				fontWeight: 'font-weight',
-				format: 'format',
-				from: 'from',
-				fx: 'fx',
-				fy: 'fy',
-				g1: 'g1',
-				g2: 'g2',
-				glyphName: 'glyph-name',
-				glyphOrientationHorizontal: 'glyph-orientation-horizontal',
-				glyphOrientationVertical: 'glyph-orientation-vertical',
-				glyphRef: 'glyphRef',
-				gradientTransform: 'gradientTransform',
-				gradientUnits: 'gradientUnits',
-				hanging: 'hanging',
-				horizAdvX: 'horiz-adv-x',
-				horizOriginX: 'horiz-origin-x',
-				ideographic: 'ideographic',
-				imageRendering: 'image-rendering',
-				in: 'in',
-				in2: 'in2',
-				intercept: 'intercept',
-				k: 'k',
-				k1: 'k1',
-				k2: 'k2',
-				k3: 'k3',
-				k4: 'k4',
-				kernelMatrix: 'kernelMatrix',
-				kernelUnitLength: 'kernelUnitLength',
-				kerning: 'kerning',
-				keyPoints: 'keyPoints',
-				keySplines: 'keySplines',
-				keyTimes: 'keyTimes',
-				lengthAdjust: 'lengthAdjust',
-				letterSpacing: 'letter-spacing',
-				lightingColor: 'lighting-color',
-				limitingConeAngle: 'limitingConeAngle',
-				local: 'local',
-				markerEnd: 'marker-end',
-				markerMid: 'marker-mid',
-				markerStart: 'marker-start',
-				markerHeight: 'markerHeight',
-				markerUnits: 'markerUnits',
-				markerWidth: 'markerWidth',
-				mask: 'mask',
-				maskContentUnits: 'maskContentUnits',
-				maskUnits: 'maskUnits',
-				mathematical: 'mathematical',
-				numOctaves: 'numOctaves',
-				offset: 'offset',
-				opacity: 'opacity',
-				operator: 'operator',
-				order: 'order',
-				orient: 'orient',
-				orientation: 'orientation',
-				origin: 'origin',
-				overflow: 'overflow',
-				overlinePosition: 'overline-position',
-				overlineThickness: 'overline-thickness',
-				panose1: 'panose-1',
-				paintOrder: 'paint-order',
-				pathLength: 'pathLength',
-				patternContentUnits: 'patternContentUnits',
-				patternTransform: 'patternTransform',
-				patternUnits: 'patternUnits',
-				pointerEvents: 'pointer-events',
-				points: 'points',
-				pointsAtX: 'pointsAtX',
-				pointsAtY: 'pointsAtY',
-				pointsAtZ: 'pointsAtZ',
-				preserveAlpha: 'preserveAlpha',
-				preserveAspectRatio: 'preserveAspectRatio',
-				primitiveUnits: 'primitiveUnits',
-				r: 'r',
-				radius: 'radius',
-				refX: 'refX',
-				refY: 'refY',
-				rel: 'rel',
-				renderingIntent: 'rendering-intent',
-				repeatCount: 'repeatCount',
-				repeatDur: 'repeatDur',
-				requiredExtensions: 'requiredExtensions',
-				requiredFeatures: 'requiredFeatures',
-				restart: 'restart',
-				result: 'result',
-				rotate: 'rotate',
-				rx: 'rx',
-				ry: 'ry',
-				scale: 'scale',
-				seed: 'seed',
-				shapeRendering: 'shape-rendering',
-				slope: 'slope',
-				spacing: 'spacing',
-				specularConstant: 'specularConstant',
-				specularExponent: 'specularExponent',
-				speed: 'speed',
-				spreadMethod: 'spreadMethod',
-				startOffset: 'startOffset',
-				stdDeviation: 'stdDeviation',
-				stemh: 'stemh',
-				stemv: 'stemv',
-				stitchTiles: 'stitchTiles',
-				stopColor: 'stop-color',
-				stopOpacity: 'stop-opacity',
-				strikethroughPosition: 'strikethrough-position',
-				strikethroughThickness: 'strikethrough-thickness',
-				string: 'string',
-				stroke: 'stroke',
-				strokeDasharray: 'stroke-dasharray',
-				strokeDashoffset: 'stroke-dashoffset',
-				strokeLinecap: 'stroke-linecap',
-				strokeLinejoin: 'stroke-linejoin',
-				strokeMiterlimit: 'stroke-miterlimit',
-				strokeOpacity: 'stroke-opacity',
-				strokeWidth: 'stroke-width',
-				surfaceScale: 'surfaceScale',
-				systemLanguage: 'systemLanguage',
-				tableValues: 'tableValues',
-				targetX: 'targetX',
-				targetY: 'targetY',
-				textAnchor: 'text-anchor',
-				textDecoration: 'text-decoration',
-				textRendering: 'text-rendering',
-				textLength: 'textLength',
-				to: 'to',
-				transform: 'transform',
-				transformOrigin: 'transform-origin',
-				u1: 'u1',
-				u2: 'u2',
-				underlinePosition: 'underline-position',
-				underlineThickness: 'underline-thickness',
-				unicode: 'unicode',
-				unicodeBidi: 'unicode-bidi',
-				unicodeRange: 'unicode-range',
-				unitsPerEm: 'units-per-em',
-				vAlphabetic: 'v-alphabetic',
-				vHanging: 'v-hanging',
-				vIdeographic: 'v-ideographic',
-				vMathematical: 'v-mathematical',
-				values: 'values',
-				vectorEffect: 'vector-effect',
-				version: 'version',
-				vertAdvY: 'vert-adv-y',
-				vertOriginX: 'vert-origin-x',
-				vertOriginY: 'vert-origin-y',
-				viewBox: 'viewBox',
-				viewTarget: 'viewTarget',
-				visibility: 'visibility',
-				widths: 'widths',
-				wordSpacing: 'word-spacing',
-				writingMode: 'writing-mode',
-				x: 'x',
-				xHeight: 'x-height',
-				x1: 'x1',
-				x2: 'x2',
-				xChannelSelector: 'xChannelSelector',
-				xlinkActuate: 'xlink:actuate',
-				xlinkArcrole: 'xlink:arcrole',
-				xlinkHref: 'xlink:href',
-				xlinkRole: 'xlink:role',
-				xlinkShow: 'xlink:show',
-				xlinkTitle: 'xlink:title',
-				xlinkType: 'xlink:type',
-				xmlBase: 'xml:base',
-				xmlLang: 'xml:lang',
-				xmlSpace: 'xml:space',
-				y: 'y',
-				y1: 'y1',
-				y2: 'y2',
-				yChannelSelector: 'yChannelSelector',
-				z: 'z',
-				zoomAndPan: 'zoomAndPan'
-			};
-
 			for (let name in svgAttributes) {
 				let value = svgAttributes[name];
 
@@ -1871,171 +1630,6 @@ describe('render', () => {
 		});
 
 		it('should have correct HTML casing', () => {
-			let htmlAttributes = {
-				accept: 'accept',
-				acceptCharset: 'accept-charset',
-				accessKey: 'accesskey',
-				action: 'action',
-				allow: 'allow',
-				// allowFullScreen: '', // unsure?
-				// allowTransparency: '', // unsure?
-				alt: 'alt',
-				as: 'as',
-				async: 'async',
-				autocomplete: 'autocomplete',
-				autoComplete: 'autocomplete',
-				autocorrect: 'autocorrect',
-				autoCorrect: 'autocorrect',
-				autofocus: 'autofocus',
-				autoFocus: 'autofocus',
-				autoPlay: 'autoplay',
-				capture: 'capture',
-				cellPadding: 'cellPadding',
-				cellSpacing: 'cellSpacing',
-				charSet: 'charset',
-				challenge: 'challenge',
-				checked: 'checked',
-				cite: 'cite',
-				class: 'class',
-				className: 'class',
-				cols: 'cols',
-				colSpan: 'colspan',
-				content: 'content',
-				contentEditable: 'contenteditable',
-				contextMenu: 'contextmenu',
-				controls: 'controls',
-				coords: 'coords',
-				crossOrigin: 'crossorigin',
-				data: 'data',
-				dateTime: 'datetime',
-				default: 'default',
-				defaultChecked: 'checked',
-				defaultValue: 'value',
-				defer: 'defer',
-				dir: 'dir',
-				disabled: 'disabled',
-				download: 'download',
-				decoding: 'decoding',
-				draggable: 'draggable',
-				encType: 'enctype',
-				enterkeyhint: 'enterkeyhint',
-				for: 'for',
-				form: 'form',
-				formAction: 'formaction',
-				formEncType: 'formenctype',
-				formMethod: 'formmethod',
-				formNoValidate: 'formnovalidate',
-				formTarget: 'formtarget',
-				frameBorder: 'frameborder',
-				headers: 'headers',
-				height: 'height',
-				hidden: 'hidden',
-				high: 'high',
-				href: 'href',
-				hrefLang: 'hreflang',
-				htmlFor: 'for',
-				httpEquiv: 'http-equiv',
-				icon: 'icon',
-				id: 'id',
-				indeterminate: 'indeterminate',
-				inputMode: 'inputmode',
-				integrity: 'integrity',
-				is: 'is',
-				kind: 'kind',
-				label: 'label',
-				lang: 'lang',
-				list: 'list',
-				loading: 'loading',
-				loop: 'loop',
-				low: 'low',
-				manifest: 'manifest',
-				max: 'max',
-				maxLength: 'maxlength',
-				media: 'media',
-				method: 'method',
-				min: 'min',
-				minLength: 'minlength',
-				multiple: 'multiple',
-				muted: 'muted',
-				name: 'name',
-				nomodule: 'nomodule',
-				nonce: 'nonce',
-				noValidate: 'novalidate',
-				open: 'open',
-				optimum: 'optimum',
-				part: 'part',
-				pattern: 'pattern',
-				ping: 'ping',
-				placeholder: 'placeholder',
-				playsInline: 'playsinline',
-				poster: 'poster',
-				preload: 'preload',
-				readonly: 'readonly',
-				readOnly: 'readonly',
-				referrerpolicy: 'referrerpolicy',
-				rel: 'rel',
-				required: 'required',
-				reversed: 'reversed',
-				role: 'role',
-				rows: 'rows',
-				rowSpan: 'rowspan',
-				sandbox: 'sandbox',
-				scope: 'scope',
-				scoped: 'scoped',
-				scrolling: 'scrolling',
-				seamless: 'seamless',
-				selected: 'selected',
-				shape: 'shape',
-				size: 'size',
-				sizes: 'sizes',
-				slot: 'slot',
-				span: 'span',
-				spellcheck: 'spellcheck',
-				spellCheck: 'spellcheck',
-				src: 'src',
-				srcset: 'srcset',
-				srcDoc: 'srcdoc',
-				srcLang: 'srclang',
-				srcSet: 'srcset',
-				start: 'start',
-				step: 'step',
-				style: 'style',
-				summary: 'summary',
-				tabIndex: 'tabindex',
-				target: 'target',
-				title: 'title',
-				type: 'type',
-				useMap: 'useMap',
-				value: 'value',
-				volume: 'volume',
-				width: 'width',
-				wmode: 'wmode',
-				wrap: 'wrap',
-
-				// Non-standard Attributes
-				autocapitalize: 'autocapitalize',
-				autoCapitalize: 'autocapitalize',
-				results: 'results',
-				translate: 'translate',
-
-				// RDFa Attributes
-				about: 'about',
-				datatype: 'datatype',
-				inlist: 'inlist',
-				prefix: 'prefix',
-				property: 'property',
-				resource: 'resource',
-				typeof: 'typeof',
-				vocab: 'vocab',
-
-				// Microdata Attributes
-				itemProp: 'itemprop',
-				itemScope: 'itemscope',
-				itemType: 'itemtype',
-				itemID: 'itemid',
-				itemRef: 'itemref'
-			};
-
 			for (let name in htmlAttributes) {
 				let value = htmlAttributes[name];
 
diff --git a/test/utils.js b/test/utils.js
index abb3418..4c2b5d2 100644
--- a/test/utils.js
+++ b/test/utils.js
@@ -10,3 +10,410 @@ export function dedent([str]) {
 		.join('\n')
 		.replace(/(^\n+|\n+\s*$)/g, '');
 }
+
+export const svgAttributes = {
+	accentHeight: 'accent-height',
+	accumulate: 'accumulate',
+	additive: 'additive',
+	alignmentBaseline: 'alignment-baseline',
+	allowReorder: 'allowReorder', // unsure
+	alphabetic: 'alphabetic',
+	amplitude: 'amplitude',
+	arabicForm: 'arabic-form',
+	ascent: 'ascent',
+	attributeName: 'attributeName',
+	attributeType: 'attributeType',
+	autoReverse: 'autoReverse', // unsure
+	azimuth: 'azimuth',
+	baseFrequency: 'baseFrequency',
+	baselineShift: 'baseline-shift',
+	baseProfile: 'baseProfile',
+	bbox: 'bbox',
+	begin: 'begin',
+	bias: 'bias',
+	by: 'by',
+	calcMode: 'calcMode',
+	capHeight: 'cap-height',
+	class: 'class',
+	clip: 'clip',
+	clipPathUnits: 'clipPathUnits',
+	clipPath: 'clip-path',
+	clipRule: 'clip-rule',
+	color: 'color',
+	colorInterpolation: 'color-interpolation',
+	colorInterpolationFilters: 'color-interpolation-filters',
+	colorProfile: 'color-profile',
+	colorRendering: 'color-rendering',
+	contentScriptType: 'contentScriptType',
+	contentStyleType: 'contentStyleType',
+	crossorigin: 'crossorigin',
+	cursor: 'cursor',
+	cx: 'cx',
+	cy: 'cy',
+	d: 'd',
+	decelerate: 'decelerate',
+	descent: 'descent',
+	diffuseConstant: 'diffuseConstant',
+	direction: 'direction',
+	display: 'display',
+	divisor: 'divisor',
+	dominantBaseline: 'dominant-baseline',
+	dur: 'dur',
+	dx: 'dx',
+	dy: 'dy',
+	edgeMode: 'edgeMode',
+	elevation: 'elevation',
+	enableBackground: 'enable-background',
+	end: 'end',
+	exponent: 'exponent',
+	fill: 'fill',
+	fillOpacity: 'fill-opacity',
+	fillRule: 'fill-rule',
+	filter: 'filter',
+	filterRes: 'filterRes',
+	filterUnits: 'filterUnits',
+	floodColor: 'flood-color',
+	floodOpacity: 'flood-opacity',
+	fontFamily: 'font-family',
+	fontSize: 'font-size',
+	fontSizeAdjust: 'font-size-adjust',
+	fontStretch: 'font-stretch',
+	fontStyle: 'font-style',
+	fontVariant: 'font-variant',
+	fontWeight: 'font-weight',
+	format: 'format',
+	from: 'from',
+	fx: 'fx',
+	fy: 'fy',
+	g1: 'g1',
+	g2: 'g2',
+	glyphName: 'glyph-name',
+	glyphOrientationHorizontal: 'glyph-orientation-horizontal',
+	glyphOrientationVertical: 'glyph-orientation-vertical',
+	glyphRef: 'glyphRef',
+	gradientTransform: 'gradientTransform',
+	gradientUnits: 'gradientUnits',
+	hanging: 'hanging',
+	horizAdvX: 'horiz-adv-x',
+	horizOriginX: 'horiz-origin-x',
+	ideographic: 'ideographic',
+	imageRendering: 'image-rendering',
+	in: 'in',
+	in2: 'in2',
+	intercept: 'intercept',
+	k: 'k',
+	k1: 'k1',
+	k2: 'k2',
+	k3: 'k3',
+	k4: 'k4',
+	kernelMatrix: 'kernelMatrix',
+	kernelUnitLength: 'kernelUnitLength',
+	kerning: 'kerning',
+	keyPoints: 'keyPoints',
+	keySplines: 'keySplines',
+	keyTimes: 'keyTimes',
+	lengthAdjust: 'lengthAdjust',
+	letterSpacing: 'letter-spacing',
+	lightingColor: 'lighting-color',
+	limitingConeAngle: 'limitingConeAngle',
+	local: 'local',
+	markerEnd: 'marker-end',
+	markerMid: 'marker-mid',
+	markerStart: 'marker-start',
+	markerHeight: 'markerHeight',
+	markerUnits: 'markerUnits',
+	markerWidth: 'markerWidth',
+	mask: 'mask',
+	maskContentUnits: 'maskContentUnits',
+	maskUnits: 'maskUnits',
+	mathematical: 'mathematical',
+	numOctaves: 'numOctaves',
+	offset: 'offset',
+	opacity: 'opacity',
+	operator: 'operator',
+	order: 'order',
+	orient: 'orient',
+	orientation: 'orientation',
+	origin: 'origin',
+	overflow: 'overflow',
+	overlinePosition: 'overline-position',
+	overlineThickness: 'overline-thickness',
+	panose1: 'panose-1',
+	paintOrder: 'paint-order',
+	pathLength: 'pathLength',
+	patternContentUnits: 'patternContentUnits',
+	patternTransform: 'patternTransform',
+	patternUnits: 'patternUnits',
+	pointerEvents: 'pointer-events',
+	points: 'points',
+	pointsAtX: 'pointsAtX',
+	pointsAtY: 'pointsAtY',
+	pointsAtZ: 'pointsAtZ',
+	preserveAlpha: 'preserveAlpha',
+	preserveAspectRatio: 'preserveAspectRatio',
+	primitiveUnits: 'primitiveUnits',
+	r: 'r',
+	radius: 'radius',
+	refX: 'refX',
+	refY: 'refY',
+	rel: 'rel',
+	renderingIntent: 'rendering-intent',
+	repeatCount: 'repeatCount',
+	repeatDur: 'repeatDur',
+	requiredExtensions: 'requiredExtensions',
+	requiredFeatures: 'requiredFeatures',
+	restart: 'restart',
+	result: 'result',
+	rotate: 'rotate',
+	rx: 'rx',
+	ry: 'ry',
+	scale: 'scale',
+	seed: 'seed',
+	shapeRendering: 'shape-rendering',
+	slope: 'slope',
+	spacing: 'spacing',
+	specularConstant: 'specularConstant',
+	specularExponent: 'specularExponent',
+	speed: 'speed',
+	spreadMethod: 'spreadMethod',
+	startOffset: 'startOffset',
+	stdDeviation: 'stdDeviation',
+	stemh: 'stemh',
+	stemv: 'stemv',
+	stitchTiles: 'stitchTiles',
+	stopColor: 'stop-color',
+	stopOpacity: 'stop-opacity',
+	strikethroughPosition: 'strikethrough-position',
+	strikethroughThickness: 'strikethrough-thickness',
+	string: 'string',
+	stroke: 'stroke',
+	strokeDasharray: 'stroke-dasharray',
+	strokeDashoffset: 'stroke-dashoffset',
+	strokeLinecap: 'stroke-linecap',
+	strokeLinejoin: 'stroke-linejoin',
+	strokeMiterlimit: 'stroke-miterlimit',
+	strokeOpacity: 'stroke-opacity',
+	strokeWidth: 'stroke-width',
+	surfaceScale: 'surfaceScale',
+	systemLanguage: 'systemLanguage',
+	tableValues: 'tableValues',
+	targetX: 'targetX',
+	targetY: 'targetY',
+	textAnchor: 'text-anchor',
+	textDecoration: 'text-decoration',
+	textRendering: 'text-rendering',
+	textLength: 'textLength',
+	to: 'to',
+	transform: 'transform',
+	transformOrigin: 'transform-origin',
+	u1: 'u1',
+	u2: 'u2',
+	underlinePosition: 'underline-position',
+	underlineThickness: 'underline-thickness',
+	unicode: 'unicode',
+	unicodeBidi: 'unicode-bidi',
+	unicodeRange: 'unicode-range',
+	unitsPerEm: 'units-per-em',
+	vAlphabetic: 'v-alphabetic',
+	vHanging: 'v-hanging',
+	vIdeographic: 'v-ideographic',
+	vMathematical: 'v-mathematical',
+	values: 'values',
+	vectorEffect: 'vector-effect',
+	version: 'version',
+	vertAdvY: 'vert-adv-y',
+	vertOriginX: 'vert-origin-x',
+	vertOriginY: 'vert-origin-y',
+	viewBox: 'viewBox',
+	viewTarget: 'viewTarget',
+	visibility: 'visibility',
+	widths: 'widths',
+	wordSpacing: 'word-spacing',
+	writingMode: 'writing-mode',
+	x: 'x',
+	xHeight: 'x-height',
+	x1: 'x1',
+	x2: 'x2',
+	xChannelSelector: 'xChannelSelector',
+	xlinkActuate: 'xlink:actuate',
+	xlinkArcrole: 'xlink:arcrole',
+	xlinkHref: 'xlink:href',
+	xlinkRole: 'xlink:role',
+	xlinkShow: 'xlink:show',
+	xlinkTitle: 'xlink:title',
+	xlinkType: 'xlink:type',
+	xmlBase: 'xml:base',
+	xmlLang: 'xml:lang',
+	xmlSpace: 'xml:space',
+	y: 'y',
+	y1: 'y1',
+	y2: 'y2',
+	yChannelSelector: 'yChannelSelector',
+	z: 'z',
+	zoomAndPan: 'zoomAndPan'
+};
+
+export const htmlAttributes = {
+	accept: 'accept',
+	acceptCharset: 'accept-charset',
+	accessKey: 'accesskey',
+	action: 'action',
+	allow: 'allow',
+	// allowFullScreen: '', // unsure?
+	// allowTransparency: '', // unsure?
+	alt: 'alt',
+	as: 'as',
+	async: 'async',
+	autocomplete: 'autocomplete',
+	autoComplete: 'autocomplete',
+	autocorrect: 'autocorrect',
+	autoCorrect: 'autocorrect',
+	autofocus: 'autofocus',
+	autoFocus: 'autofocus',
+	autoPlay: 'autoplay',
+	capture: 'capture',
+	cellPadding: 'cellPadding',
+	cellSpacing: 'cellSpacing',
+	charSet: 'charset',
+	challenge: 'challenge',
+	checked: 'checked',
+	cite: 'cite',
+	class: 'class',
+	className: 'class',
+	cols: 'cols',
+	colSpan: 'colspan',
+	content: 'content',
+	contentEditable: 'contenteditable',
+	contextMenu: 'contextmenu',
+	controls: 'controls',
+	coords: 'coords',
+	crossOrigin: 'crossorigin',
+	data: 'data',
+	dateTime: 'datetime',
+	default: 'default',
+	defaultChecked: 'checked',
+	defaultValue: 'value',
+	defer: 'defer',
+	dir: 'dir',
+	disabled: 'disabled',
+	download: 'download',
+	decoding: 'decoding',
+	draggable: 'draggable',
+	encType: 'enctype',
+	enterkeyhint: 'enterkeyhint',
+	for: 'for',
+	form: 'form',
+	formAction: 'formaction',
+	formEncType: 'formenctype',
+	formMethod: 'formmethod',
+	formNoValidate: 'formnovalidate',
+	formTarget: 'formtarget',
+	frameBorder: 'frameborder',
+	headers: 'headers',
+	height: 'height',
+	hidden: 'hidden',
+	high: 'high',
+	href: 'href',
+	hrefLang: 'hreflang',
+	htmlFor: 'for',
+	httpEquiv: 'http-equiv',
+	icon: 'icon',
+	id: 'id',
+	indeterminate: 'indeterminate',
+	inputMode: 'inputmode',
+	integrity: 'integrity',
+	is: 'is',
+	kind: 'kind',
+	label: 'label',
+	lang: 'lang',
+	list: 'list',
+	loading: 'loading',
+	loop: 'loop',
+	low: 'low',
+	manifest: 'manifest',
+	max: 'max',
+	maxLength: 'maxlength',
+	media: 'media',
+	method: 'method',
+	min: 'min',
+	minLength: 'minlength',
+	multiple: 'multiple',
+	muted: 'muted',
+	name: 'name',
+	nomodule: 'nomodule',
+	nonce: 'nonce',
+	noValidate: 'novalidate',
+	open: 'open',
+	optimum: 'optimum',
+	part: 'part',
+	pattern: 'pattern',
+	ping: 'ping',
+	placeholder: 'placeholder',
+	playsInline: 'playsinline',
+	poster: 'poster',
+	preload: 'preload',
+	readonly: 'readonly',
+	readOnly: 'readonly',
+	referrerpolicy: 'referrerpolicy',
+	rel: 'rel',
+	required: 'required',
+	reversed: 'reversed',
+	role: 'role',
+	rows: 'rows',
+	rowSpan: 'rowspan',
+	sandbox: 'sandbox',
+	scope: 'scope',
+	scoped: 'scoped',
+	scrolling: 'scrolling',
+	seamless: 'seamless',
+	selected: 'selected',
+	shape: 'shape',
+	size: 'size',
+	sizes: 'sizes',
+	slot: 'slot',
+	span: 'span',
+	spellcheck: 'spellcheck',
+	spellCheck: 'spellcheck',
+	src: 'src',
+	srcset: 'srcset',
+	srcDoc: 'srcdoc',
+	srcLang: 'srclang',
+	srcSet: 'srcset',
+	start: 'start',
+	step: 'step',
+	style: 'style',
+	summary: 'summary',
+	tabIndex: 'tabindex',
+	target: 'target',
+	title: 'title',
+	type: 'type',
+	useMap: 'useMap',
+	value: 'value',
+	volume: 'volume',
+	width: 'width',
+	wmode: 'wmode',
+	wrap: 'wrap',
+
+	// Non-standard Attributes
+	autocapitalize: 'autocapitalize',
+	autoCapitalize: 'autocapitalize',
+	results: 'results',
+	translate: 'translate',
+
+	// RDFa Attributes
+	about: 'about',
+	datatype: 'datatype',
+	inlist: 'inlist',
+	prefix: 'prefix',
+	property: 'property',
+	resource: 'resource',
+	typeof: 'typeof',
+	vocab: 'vocab',
+
+	// Microdata Attributes
+	itemProp: 'itemprop',
+	itemScope: 'itemscope',
+	itemType: 'itemtype',
+	itemID: 'itemid',
+	itemRef: 'itemref'
+};