attributes.js 20 KB

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