attributes.js 21 KB

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