attributes.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. "use strict";
  2. /**
  3. * Methods for getting and modifying attributes.
  4. *
  5. * @module cheerio/attributes
  6. */
  7. Object.defineProperty(exports, "__esModule", { value: true });
  8. exports.attr = attr;
  9. exports.prop = prop;
  10. exports.data = data;
  11. exports.val = val;
  12. exports.removeAttr = removeAttr;
  13. exports.hasClass = hasClass;
  14. exports.addClass = addClass;
  15. exports.removeClass = removeClass;
  16. exports.toggleClass = toggleClass;
  17. const static_js_1 = require("../static.js");
  18. const utils_js_1 = require("../utils.js");
  19. const domhandler_1 = require("domhandler");
  20. const domutils_1 = require("domutils");
  21. const hasOwn = Object.prototype.hasOwnProperty;
  22. const rspace = /\s+/;
  23. const dataAttrPrefix = 'data-';
  24. // Attributes that are booleans
  25. const rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i;
  26. // Matches strings that look like JSON objects or arrays
  27. const rbrace = /^{[^]*}$|^\[[^]*]$/;
  28. function getAttr(elem, name, xmlMode) {
  29. var _a;
  30. if (!elem || !(0, domhandler_1.isTag)(elem))
  31. return undefined;
  32. (_a = elem.attribs) !== null && _a !== void 0 ? _a : (elem.attribs = {});
  33. // Return the entire attribs object if no attribute specified
  34. if (!name) {
  35. return elem.attribs;
  36. }
  37. if (hasOwn.call(elem.attribs, name)) {
  38. // Get the (decoded) attribute
  39. return !xmlMode && rboolean.test(name) ? name : elem.attribs[name];
  40. }
  41. // Mimic the DOM and return text content as value for `option's`
  42. if (elem.name === 'option' && name === 'value') {
  43. return (0, static_js_1.text)(elem.children);
  44. }
  45. // Mimic DOM with default value for radios/checkboxes
  46. if (elem.name === 'input' &&
  47. (elem.attribs['type'] === 'radio' || elem.attribs['type'] === 'checkbox') &&
  48. name === 'value') {
  49. return 'on';
  50. }
  51. return undefined;
  52. }
  53. /**
  54. * Sets the value of an attribute. The attribute will be deleted if the value is
  55. * `null`.
  56. *
  57. * @private
  58. * @param el - The element to set the attribute on.
  59. * @param name - The attribute's name.
  60. * @param value - The attribute's value.
  61. */
  62. function setAttr(el, name, value) {
  63. if (value === null) {
  64. removeAttribute(el, name);
  65. }
  66. else {
  67. el.attribs[name] = `${value}`;
  68. }
  69. }
  70. function attr(name, value) {
  71. // Set the value (with attr map support)
  72. if (typeof name === 'object' || value !== undefined) {
  73. if (typeof value === 'function') {
  74. if (typeof name !== 'string') {
  75. {
  76. throw new Error('Bad combination of arguments.');
  77. }
  78. }
  79. return (0, utils_js_1.domEach)(this, (el, i) => {
  80. if ((0, domhandler_1.isTag)(el))
  81. setAttr(el, name, value.call(el, i, el.attribs[name]));
  82. });
  83. }
  84. return (0, utils_js_1.domEach)(this, (el) => {
  85. if (!(0, domhandler_1.isTag)(el))
  86. return;
  87. if (typeof name === 'object') {
  88. for (const objName of Object.keys(name)) {
  89. const objValue = name[objName];
  90. setAttr(el, objName, objValue);
  91. }
  92. }
  93. else {
  94. setAttr(el, name, value);
  95. }
  96. });
  97. }
  98. return arguments.length > 1
  99. ? this
  100. : getAttr(this[0], name, this.options.xmlMode);
  101. }
  102. /**
  103. * Gets a node's prop.
  104. *
  105. * @private
  106. * @category Attributes
  107. * @param el - Element to get the prop of.
  108. * @param name - Name of the prop.
  109. * @param xmlMode - Disable handling of special HTML attributes.
  110. * @returns The prop's value.
  111. */
  112. function getProp(el, name, xmlMode) {
  113. return name in el
  114. ? // @ts-expect-error TS doesn't like us accessing the value directly here.
  115. el[name]
  116. : !xmlMode && rboolean.test(name)
  117. ? getAttr(el, name, false) !== undefined
  118. : getAttr(el, name, xmlMode);
  119. }
  120. /**
  121. * Sets the value of a prop.
  122. *
  123. * @private
  124. * @param el - The element to set the prop on.
  125. * @param name - The prop's name.
  126. * @param value - The prop's value.
  127. * @param xmlMode - Disable handling of special HTML attributes.
  128. */
  129. function setProp(el, name, value, xmlMode) {
  130. if (name in el) {
  131. // @ts-expect-error Overriding value
  132. el[name] = value;
  133. }
  134. else {
  135. setAttr(el, name, !xmlMode && rboolean.test(name) ? (value ? '' : null) : `${value}`);
  136. }
  137. }
  138. function prop(name, value) {
  139. var _a;
  140. if (typeof name === 'string' && value === undefined) {
  141. const el = this[0];
  142. if (!el || !(0, domhandler_1.isTag)(el))
  143. return undefined;
  144. switch (name) {
  145. case 'style': {
  146. const property = this.css();
  147. const keys = Object.keys(property);
  148. for (let i = 0; i < keys.length; i++) {
  149. property[i] = keys[i];
  150. }
  151. property.length = keys.length;
  152. return property;
  153. }
  154. case 'tagName':
  155. case 'nodeName': {
  156. return el.name.toUpperCase();
  157. }
  158. case 'href':
  159. case 'src': {
  160. const prop = (_a = el.attribs) === null || _a === void 0 ? void 0 : _a[name];
  161. if (typeof URL !== 'undefined' &&
  162. ((name === 'href' && (el.tagName === 'a' || el.tagName === 'link')) ||
  163. (name === 'src' &&
  164. (el.tagName === 'img' ||
  165. el.tagName === 'iframe' ||
  166. el.tagName === 'audio' ||
  167. el.tagName === 'video' ||
  168. el.tagName === 'source'))) &&
  169. prop !== undefined &&
  170. this.options.baseURI) {
  171. return new URL(prop, this.options.baseURI).href;
  172. }
  173. return prop;
  174. }
  175. case 'innerText': {
  176. return (0, domutils_1.innerText)(el);
  177. }
  178. case 'textContent': {
  179. return (0, domutils_1.textContent)(el);
  180. }
  181. case 'outerHTML': {
  182. return this.clone().wrap('<container />').parent().html();
  183. }
  184. case 'innerHTML': {
  185. return this.html();
  186. }
  187. default: {
  188. return getProp(el, name, this.options.xmlMode);
  189. }
  190. }
  191. }
  192. if (typeof name === 'object' || value !== undefined) {
  193. if (typeof value === 'function') {
  194. if (typeof name === 'object') {
  195. throw new TypeError('Bad combination of arguments.');
  196. }
  197. return (0, utils_js_1.domEach)(this, (el, i) => {
  198. if ((0, domhandler_1.isTag)(el)) {
  199. setProp(el, name, value.call(el, i, getProp(el, name, this.options.xmlMode)), this.options.xmlMode);
  200. }
  201. });
  202. }
  203. return (0, utils_js_1.domEach)(this, (el) => {
  204. if (!(0, domhandler_1.isTag)(el))
  205. return;
  206. if (typeof name === 'object') {
  207. for (const key of Object.keys(name)) {
  208. const val = name[key];
  209. setProp(el, key, val, this.options.xmlMode);
  210. }
  211. }
  212. else {
  213. setProp(el, name, value, this.options.xmlMode);
  214. }
  215. });
  216. }
  217. return undefined;
  218. }
  219. /**
  220. * Sets the value of a data attribute.
  221. *
  222. * @private
  223. * @param elem - The element to set the data attribute on.
  224. * @param name - The data attribute's name.
  225. * @param value - The data attribute's value.
  226. */
  227. function setData(elem, name, value) {
  228. var _a;
  229. (_a = elem.data) !== null && _a !== void 0 ? _a : (elem.data = {});
  230. if (typeof name === 'object')
  231. Object.assign(elem.data, name);
  232. else if (typeof name === 'string' && value !== undefined) {
  233. elem.data[name] = value;
  234. }
  235. }
  236. /**
  237. * Read _all_ HTML5 `data-*` attributes from the equivalent HTML5 `data-*`
  238. * attribute, and cache the value in the node's internal data store.
  239. *
  240. * @private
  241. * @category Attributes
  242. * @param el - Element to get the data attribute of.
  243. * @returns A map with all of the data attributes.
  244. */
  245. function readAllData(el) {
  246. for (const domName of Object.keys(el.attribs)) {
  247. if (!domName.startsWith(dataAttrPrefix)) {
  248. continue;
  249. }
  250. const jsName = (0, utils_js_1.camelCase)(domName.slice(dataAttrPrefix.length));
  251. if (!hasOwn.call(el.data, jsName)) {
  252. el.data[jsName] = parseDataValue(el.attribs[domName]);
  253. }
  254. }
  255. return el.data;
  256. }
  257. /**
  258. * Read the specified attribute from the equivalent HTML5 `data-*` attribute,
  259. * and (if present) cache the value in the node's internal data store.
  260. *
  261. * @private
  262. * @category Attributes
  263. * @param el - Element to get the data attribute of.
  264. * @param name - Name of the data attribute.
  265. * @returns The data attribute's value.
  266. */
  267. function readData(el, name) {
  268. const domName = dataAttrPrefix + (0, utils_js_1.cssCase)(name);
  269. const data = el.data;
  270. if (hasOwn.call(data, name)) {
  271. return data[name];
  272. }
  273. if (hasOwn.call(el.attribs, domName)) {
  274. return (data[name] = parseDataValue(el.attribs[domName]));
  275. }
  276. return undefined;
  277. }
  278. /**
  279. * Coerce string data-* attributes to their corresponding JavaScript primitives.
  280. *
  281. * @private
  282. * @category Attributes
  283. * @param value - The value to parse.
  284. * @returns The parsed value.
  285. */
  286. function parseDataValue(value) {
  287. if (value === 'null')
  288. return null;
  289. if (value === 'true')
  290. return true;
  291. if (value === 'false')
  292. return false;
  293. const num = Number(value);
  294. if (value === String(num))
  295. return num;
  296. if (rbrace.test(value)) {
  297. try {
  298. return JSON.parse(value);
  299. }
  300. catch {
  301. /* Ignore */
  302. }
  303. }
  304. return value;
  305. }
  306. function data(name, value) {
  307. var _a;
  308. const elem = this[0];
  309. if (!elem || !(0, domhandler_1.isTag)(elem))
  310. return;
  311. const dataEl = elem;
  312. (_a = dataEl.data) !== null && _a !== void 0 ? _a : (dataEl.data = {});
  313. // Return the entire data object if no data specified
  314. if (name == null) {
  315. return readAllData(dataEl);
  316. }
  317. // Set the value (with attr map support)
  318. if (typeof name === 'object' || value !== undefined) {
  319. (0, utils_js_1.domEach)(this, (el) => {
  320. if ((0, domhandler_1.isTag)(el)) {
  321. if (typeof name === 'object')
  322. setData(el, name);
  323. else
  324. setData(el, name, value);
  325. }
  326. });
  327. return this;
  328. }
  329. return readData(dataEl, name);
  330. }
  331. function val(value) {
  332. const querying = arguments.length === 0;
  333. const element = this[0];
  334. if (!element || !(0, domhandler_1.isTag)(element))
  335. return querying ? undefined : this;
  336. switch (element.name) {
  337. case 'textarea': {
  338. return this.text(value);
  339. }
  340. case 'select': {
  341. const option = this.find('option:selected');
  342. if (!querying) {
  343. if (this.attr('multiple') == null && typeof value === 'object') {
  344. return this;
  345. }
  346. this.find('option').removeAttr('selected');
  347. const values = typeof value === 'object' ? value : [value];
  348. for (const val of values) {
  349. this.find(`option[value="${val}"]`).attr('selected', '');
  350. }
  351. return this;
  352. }
  353. return this.attr('multiple')
  354. ? option.toArray().map((el) => (0, static_js_1.text)(el.children))
  355. : option.attr('value');
  356. }
  357. case 'input':
  358. case 'option': {
  359. return querying
  360. ? this.attr('value')
  361. : this.attr('value', value);
  362. }
  363. }
  364. return undefined;
  365. }
  366. /**
  367. * Remove an attribute.
  368. *
  369. * @private
  370. * @param elem - Node to remove attribute from.
  371. * @param name - Name of the attribute to remove.
  372. */
  373. function removeAttribute(elem, name) {
  374. if (!elem.attribs || !hasOwn.call(elem.attribs, name))
  375. return;
  376. delete elem.attribs[name];
  377. }
  378. /**
  379. * Splits a space-separated list of names to individual names.
  380. *
  381. * @category Attributes
  382. * @param names - Names to split.
  383. * @returns - Split names.
  384. */
  385. function splitNames(names) {
  386. return names ? names.trim().split(rspace) : [];
  387. }
  388. /**
  389. * Method for removing attributes by `name`.
  390. *
  391. * @category Attributes
  392. * @example
  393. *
  394. * ```js
  395. * $('.pear').removeAttr('class').html();
  396. * //=> <li>Pear</li>
  397. *
  398. * $('.apple').attr('id', 'favorite');
  399. * $('.apple').removeAttr('id class').html();
  400. * //=> <li>Apple</li>
  401. * ```
  402. *
  403. * @param name - Name of the attribute.
  404. * @returns The instance itself.
  405. * @see {@link https://api.jquery.com/removeAttr/}
  406. */
  407. function removeAttr(name) {
  408. const attrNames = splitNames(name);
  409. for (const attrName of attrNames) {
  410. (0, utils_js_1.domEach)(this, (elem) => {
  411. if ((0, domhandler_1.isTag)(elem))
  412. removeAttribute(elem, attrName);
  413. });
  414. }
  415. return this;
  416. }
  417. /**
  418. * Check to see if _any_ of the matched elements have the given `className`.
  419. *
  420. * @category Attributes
  421. * @example
  422. *
  423. * ```js
  424. * $('.pear').hasClass('pear');
  425. * //=> true
  426. *
  427. * $('apple').hasClass('fruit');
  428. * //=> false
  429. *
  430. * $('li').hasClass('pear');
  431. * //=> true
  432. * ```
  433. *
  434. * @param className - Name of the class.
  435. * @returns Indicates if an element has the given `className`.
  436. * @see {@link https://api.jquery.com/hasClass/}
  437. */
  438. function hasClass(className) {
  439. return this.toArray().some((elem) => {
  440. const clazz = (0, domhandler_1.isTag)(elem) && elem.attribs['class'];
  441. let idx = -1;
  442. if (clazz && className.length > 0) {
  443. while ((idx = clazz.indexOf(className, idx + 1)) > -1) {
  444. const end = idx + className.length;
  445. if ((idx === 0 || rspace.test(clazz[idx - 1])) &&
  446. (end === clazz.length || rspace.test(clazz[end]))) {
  447. return true;
  448. }
  449. }
  450. }
  451. return false;
  452. });
  453. }
  454. /**
  455. * Adds class(es) to all of the matched elements. Also accepts a `function`.
  456. *
  457. * @category Attributes
  458. * @example
  459. *
  460. * ```js
  461. * $('.pear').addClass('fruit').html();
  462. * //=> <li class="pear fruit">Pear</li>
  463. *
  464. * $('.apple').addClass('fruit red').html();
  465. * //=> <li class="apple fruit red">Apple</li>
  466. * ```
  467. *
  468. * @param value - Name of new class.
  469. * @returns The instance itself.
  470. * @see {@link https://api.jquery.com/addClass/}
  471. */
  472. function addClass(value) {
  473. // Support functions
  474. if (typeof value === 'function') {
  475. return (0, utils_js_1.domEach)(this, (el, i) => {
  476. if ((0, domhandler_1.isTag)(el)) {
  477. const className = el.attribs['class'] || '';
  478. addClass.call([el], value.call(el, i, className));
  479. }
  480. });
  481. }
  482. // Return if no value or not a string or function
  483. if (!value || typeof value !== 'string')
  484. return this;
  485. const classNames = value.split(rspace);
  486. const numElements = this.length;
  487. for (let i = 0; i < numElements; i++) {
  488. const el = this[i];
  489. // If selected element isn't a tag, move on
  490. if (!(0, domhandler_1.isTag)(el))
  491. continue;
  492. // If we don't already have classes — always set xmlMode to false here, as it doesn't matter for classes
  493. const className = getAttr(el, 'class', false);
  494. if (className) {
  495. let setClass = ` ${className} `;
  496. // Check if class already exists
  497. for (const cn of classNames) {
  498. const appendClass = `${cn} `;
  499. if (!setClass.includes(` ${appendClass}`))
  500. setClass += appendClass;
  501. }
  502. setAttr(el, 'class', setClass.trim());
  503. }
  504. else {
  505. setAttr(el, 'class', classNames.join(' ').trim());
  506. }
  507. }
  508. return this;
  509. }
  510. /**
  511. * Removes one or more space-separated classes from the selected elements. If no
  512. * `className` is defined, all classes will be removed. Also accepts a
  513. * `function`.
  514. *
  515. * @category Attributes
  516. * @example
  517. *
  518. * ```js
  519. * $('.pear').removeClass('pear').html();
  520. * //=> <li class="">Pear</li>
  521. *
  522. * $('.apple').addClass('red').removeClass().html();
  523. * //=> <li class="">Apple</li>
  524. * ```
  525. *
  526. * @param name - Name of the class. If not specified, removes all elements.
  527. * @returns The instance itself.
  528. * @see {@link https://api.jquery.com/removeClass/}
  529. */
  530. function removeClass(name) {
  531. // Handle if value is a function
  532. if (typeof name === 'function') {
  533. return (0, utils_js_1.domEach)(this, (el, i) => {
  534. if ((0, domhandler_1.isTag)(el)) {
  535. removeClass.call([el], name.call(el, i, el.attribs['class'] || ''));
  536. }
  537. });
  538. }
  539. const classes = splitNames(name);
  540. const numClasses = classes.length;
  541. const removeAll = arguments.length === 0;
  542. return (0, utils_js_1.domEach)(this, (el) => {
  543. if (!(0, domhandler_1.isTag)(el))
  544. return;
  545. if (removeAll) {
  546. // Short circuit the remove all case as this is the nice one
  547. el.attribs['class'] = '';
  548. }
  549. else {
  550. const elClasses = splitNames(el.attribs['class']);
  551. let changed = false;
  552. for (let j = 0; j < numClasses; j++) {
  553. const index = elClasses.indexOf(classes[j]);
  554. if (index >= 0) {
  555. elClasses.splice(index, 1);
  556. changed = true;
  557. /*
  558. * We have to do another pass to ensure that there are not duplicate
  559. * classes listed
  560. */
  561. j--;
  562. }
  563. }
  564. if (changed) {
  565. el.attribs['class'] = elClasses.join(' ');
  566. }
  567. }
  568. });
  569. }
  570. /**
  571. * Add or remove class(es) from the matched elements, depending on either the
  572. * class's presence or the value of the switch argument. Also accepts a
  573. * `function`.
  574. *
  575. * @category Attributes
  576. * @example
  577. *
  578. * ```js
  579. * $('.apple.green').toggleClass('fruit green red').html();
  580. * //=> <li class="apple fruit red">Apple</li>
  581. *
  582. * $('.apple.green').toggleClass('fruit green red', true).html();
  583. * //=> <li class="apple green fruit red">Apple</li>
  584. * ```
  585. *
  586. * @param value - Name of the class. Can also be a function.
  587. * @param stateVal - If specified the state of the class.
  588. * @returns The instance itself.
  589. * @see {@link https://api.jquery.com/toggleClass/}
  590. */
  591. function toggleClass(value, stateVal) {
  592. // Support functions
  593. if (typeof value === 'function') {
  594. return (0, utils_js_1.domEach)(this, (el, i) => {
  595. if ((0, domhandler_1.isTag)(el)) {
  596. toggleClass.call([el], value.call(el, i, el.attribs['class'] || '', stateVal), stateVal);
  597. }
  598. });
  599. }
  600. // Return if no value or not a string or function
  601. if (!value || typeof value !== 'string')
  602. return this;
  603. const classNames = value.split(rspace);
  604. const numClasses = classNames.length;
  605. const state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0;
  606. const numElements = this.length;
  607. for (let i = 0; i < numElements; i++) {
  608. const el = this[i];
  609. // If selected element isn't a tag, move on
  610. if (!(0, domhandler_1.isTag)(el))
  611. continue;
  612. const elementClasses = splitNames(el.attribs['class']);
  613. // Check if class already exists
  614. for (let j = 0; j < numClasses; j++) {
  615. // Check if the class name is currently defined
  616. const index = elementClasses.indexOf(classNames[j]);
  617. // Add if stateValue === true or we are toggling and there is no value
  618. if (state >= 0 && index < 0) {
  619. elementClasses.push(classNames[j]);
  620. }
  621. else if (state <= 0 && index >= 0) {
  622. // Otherwise remove but only if the item exists
  623. elementClasses.splice(index, 1);
  624. }
  625. }
  626. el.attribs['class'] = elementClasses.join(' ');
  627. }
  628. return this;
  629. }
  630. //# sourceMappingURL=attributes.js.map