From 71f18a253e3413b47f8e4bf9177a77d4d7732cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:20:13 +0100 Subject: [PATCH 1/9] refactor(attributes): Split, simplify `readData` --- src/api/attributes.ts | 98 +++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index de945b2a83..12a8923814 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -495,61 +495,77 @@ function setData( } /** - * Read the specified attribute from the equivalent HTML5 `data-*` attribute, - * and (if present) cache the value in the node's internal data store. If no - * attribute name is specified, read _all_ HTML5 `data-*` attributes in this - * manner. + * Read _all_ HTML5 `data-*` attributes from the equivalent HTML5 `data-*` + * attribute, and (if present) cache the value in the node's internal data + * store. * * @private * @category Attributes * @param el - Element to get the data attribute of. - * @param name - Name of the data attribute. - * @returns The data attribute's value, or a map with all of the data - * attributes. + * @returns A map with all of the data attributes. */ -function readData(el: DataElement, name?: string): unknown { - let domNames; - let jsNames; - let value; - - if (name == null) { - domNames = Object.keys(el.attribs).filter((attrName) => - attrName.startsWith(dataAttrPrefix) - ); - jsNames = domNames.map((domName) => - camelCase(domName.slice(dataAttrPrefix.length)) - ); - } else { - domNames = [dataAttrPrefix + cssCase(name)]; - jsNames = [name]; - } +function readAllData(el: DataElement): unknown { + const domNames = Object.keys(el.attribs).filter((attrName) => + attrName.startsWith(dataAttrPrefix) + ); for (let idx = 0; idx < domNames.length; ++idx) { const domName = domNames[idx]; - const jsName = jsNames[idx]; + const jsName = camelCase(domName.slice(dataAttrPrefix.length)); + if ( hasOwn.call(el.attribs, domName) && !hasOwn.call((el as DataElement).data, jsName) ) { - value = el.attribs[domName]; - - if (hasOwn.call(primitives, value)) { - value = primitives[value]; - } else if (value === String(Number(value))) { - value = Number(value); - } else if (rbrace.test(value)) { - try { - value = JSON.parse(value); - } catch (e) { - /* Ignore */ - } - } - - (el.data as Record)[jsName] = value; + (el.data as Record)[jsName] = parseDataValue( + el.attribs[domName] + ); } } - return name == null ? el.data : value; + return el.data; +} + +/** + * Read the specified attribute from the equivalent HTML5 `data-*` attribute, + * and (if present) cache the value in the node's internal data store. + * + * @private + * @category Attributes + * @param el - Element to get the data attribute of. + * @param name - Name of the data attribute. + * @returns The data attribute's value. + */ +function readData(el: DataElement, name: string): unknown { + const domName = dataAttrPrefix + cssCase(name); + const data = el.data!; + + if (hasOwn.call(data, name)) { + return data[name]; + } + + if (hasOwn.call(el.attribs, domName)) { + return (data[name] = parseDataValue(el.attribs[domName])); + } + + return undefined; +} + +function parseDataValue(value: string) { + if (hasOwn.call(primitives, value)) { + return primitives[value]; + } + if (value === String(Number(value))) { + return Number(value); + } + if (rbrace.test(value)) { + try { + return JSON.parse(value); + } catch (e) { + /* Ignore */ + } + } + return value; } /** @@ -651,7 +667,7 @@ export function data( // Return the entire data object if no data specified if (!name) { - return readData(dataEl); + return readAllData(dataEl); } // Set the value (with attr map support) From c2dd56c7dd35e0623375e5777f7a7cd9894e9150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:27:13 +0100 Subject: [PATCH 2/9] Update attributes.ts --- src/api/attributes.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 12a8923814..a454087408 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -517,9 +517,7 @@ function readAllData(el: DataElement): unknown { hasOwn.call(el.attribs, domName) && !hasOwn.call((el as DataElement).data, jsName) ) { - (el.data as Record)[jsName] = parseDataValue( - el.attribs[domName] - ); + el.data![jsName] = parseDataValue(el.attribs[domName]); } } From 720341321a8c60d681ab6a3bea67736a4b3d6ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:37:20 +0100 Subject: [PATCH 3/9] Update attributes.ts --- src/api/attributes.ts | 49 ++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index a454087408..094f910c0b 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -12,15 +12,7 @@ import { innerText, textContent } from 'domutils'; const hasOwn = Object.prototype.hasOwnProperty; const rspace = /\s+/; const dataAttrPrefix = 'data-'; -/* - * Lookup table for coercing string data-* attributes to their corresponding - * JavaScript primitives - */ -const primitives: Record = { - null: null, - true: true, - false: false, -}; + // Attributes that are booleans const rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i; @@ -505,19 +497,15 @@ function setData( * @returns A map with all of the data attributes. */ function readAllData(el: DataElement): unknown { - const domNames = Object.keys(el.attribs).filter((attrName) => - attrName.startsWith(dataAttrPrefix) - ); + for (const domName of Object.keys(el.attribs)) { + if (!domName.startsWith(dataAttrPrefix)) { + continue; + } - for (let idx = 0; idx < domNames.length; ++idx) { - const domName = domNames[idx]; const jsName = camelCase(domName.slice(dataAttrPrefix.length)); - if ( - hasOwn.call(el.attribs, domName) && - !hasOwn.call((el as DataElement).data, jsName) - ) { - el.data![jsName] = parseDataValue(el.attribs[domName]); + if (!hasOwn.call(el.data, jsName)) { + el.data![jsName] ??= parseDataValue(el.attribs[domName]); } } @@ -549,13 +537,19 @@ function readData(el: DataElement, name: string): unknown { return undefined; } -function parseDataValue(value: string) { - if (hasOwn.call(primitives, value)) { - return primitives[value]; - } - if (value === String(Number(value))) { - return Number(value); - } +/** + * Coerce string data-* attributes to their corresponding JavaScript primitives. + * + * @private + * @category Attributes + * @returns The parsed value. + */ +function parseDataValue(value: string): unknown { + if (value === 'null') return null; + if (value === 'true') return true; + if (value === 'false') return false; + const num = Number(value); + if (value === String(num)) return num; if (rbrace.test(value)) { try { return JSON.parse(value); @@ -678,9 +672,6 @@ export function data( }); return this; } - if (hasOwn.call(dataEl.data, name)) { - return dataEl.data[name]; - } return readData(dataEl, name); } From ecc634a869fe103d2115107b470a084f17c09802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:40:13 +0100 Subject: [PATCH 4/9] fix: Allow empty string for `data` --- src/api/attributes.spec.ts | 6 ++++++ src/api/attributes.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/attributes.spec.ts b/src/api/attributes.spec.ts index d144525056..c026e3511e 100644 --- a/src/api/attributes.spec.ts +++ b/src/api/attributes.spec.ts @@ -530,6 +530,12 @@ describe('$(...)', () => { expect($el.data('custom')).toBe('{{templatevar}}'); }); + it('("") : should accept the empty string as a name', () => { + const $el = cheerio('
'); + + expect($el.data('')).toBe('a'); + }); + it('(hyphen key) : data addribute with hyphen should be camelized ;-)', () => { const data = $('.frey').data(); expect(data).toStrictEqual({ diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 094f910c0b..33c98e6966 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -658,7 +658,7 @@ export function data( dataEl.data ??= {}; // Return the entire data object if no data specified - if (!name) { + if (name == null) { return readAllData(dataEl); } From 5b2849c906c9da941457ad30fb7835437ae53a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:42:23 +0100 Subject: [PATCH 5/9] Update attributes.ts --- src/api/attributes.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 33c98e6966..6eb6001753 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -488,8 +488,7 @@ function setData( /** * Read _all_ HTML5 `data-*` attributes from the equivalent HTML5 `data-*` - * attribute, and (if present) cache the value in the node's internal data - * store. + * attribute, and cache the value in the node's internal data store. * * @private * @category Attributes From 5b75fdd8955c1fb5160336ce1b29b51c2a7cd8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 12:43:33 +0100 Subject: [PATCH 6/9] Update attributes.ts --- src/api/attributes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 6eb6001753..c6df58ec64 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -504,7 +504,7 @@ function readAllData(el: DataElement): unknown { const jsName = camelCase(domName.slice(dataAttrPrefix.length)); if (!hasOwn.call(el.data, jsName)) { - el.data![jsName] ??= parseDataValue(el.attribs[domName]); + el.data![jsName] = parseDataValue(el.attribs[domName]); } } From 05e61f5f6ba145adc7913eed93c034308c0ff9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:43:08 +0100 Subject: [PATCH 7/9] Update attributes.ts --- src/api/attributes.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index c6df58ec64..d697f67e9d 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -472,17 +472,15 @@ interface DataElement extends Element { * @param value - The data attribute's value. */ function setData( - el: Element, + el: DataElement, name: string | Record, value?: unknown ) { - const elem: DataElement = el; - - elem.data ??= {}; + const data = el.data!; - if (typeof name === 'object') Object.assign(elem.data, name); + if (typeof name === 'object') Object.assign(data, name); else if (typeof name === 'string' && value !== undefined) { - elem.data[name] = value; + data[name] = value; } } From 6a69f9df7d60f73233d1fd41a7b82f8552a09fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:47:54 +0100 Subject: [PATCH 8/9] Update attributes.ts --- src/api/attributes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index d697f67e9d..dfc3d4976e 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -476,11 +476,11 @@ function setData( name: string | Record, value?: unknown ) { - const data = el.data!; + el.data ??= {}; - if (typeof name === 'object') Object.assign(data, name); + if (typeof name === 'object') Object.assign(el.data, name); else if (typeof name === 'string' && value !== undefined) { - data[name] = value; + el.data[name] = value; } } From bf1764e3d9fde3d7fdb3365e90d3c69e2215c103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Mon, 24 Oct 2022 14:48:32 +0100 Subject: [PATCH 9/9] Update attributes.ts --- src/api/attributes.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/api/attributes.ts b/src/api/attributes.ts index dfc3d4976e..20305fe04f 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -467,20 +467,20 @@ interface DataElement extends Element { * Sets the value of a data attribute. * * @private - * @param el - The element to set the data attribute on. + * @param elem - The element to set the data attribute on. * @param name - The data attribute's name. * @param value - The data attribute's value. */ function setData( - el: DataElement, + elem: DataElement, name: string | Record, value?: unknown ) { - el.data ??= {}; + elem.data ??= {}; - if (typeof name === 'object') Object.assign(el.data, name); + if (typeof name === 'object') Object.assign(elem.data, name); else if (typeof name === 'string' && value !== undefined) { - el.data[name] = value; + elem.data[name] = value; } }