manipulation.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. var parse = require('../parse'),
  2. $ = require('../static'),
  3. updateDOM = parse.update,
  4. evaluate = parse.evaluate,
  5. utils = require('../utils'),
  6. domEach = utils.domEach,
  7. cloneDom = utils.cloneDom,
  8. isHtml = utils.isHtml,
  9. slice = Array.prototype.slice,
  10. _ = {
  11. flatten: require('lodash.flatten'),
  12. bind: require('lodash.bind'),
  13. forEach: require('lodash.foreach')
  14. };
  15. // Create an array of nodes, recursing into arrays and parsing strings if
  16. // necessary
  17. exports._makeDomArray = function makeDomArray(elem, clone) {
  18. if (elem == null) {
  19. return [];
  20. } else if (elem.cheerio) {
  21. return clone ? cloneDom(elem.get(), elem.options) : elem.get();
  22. } else if (Array.isArray(elem)) {
  23. return _.flatten(elem.map(function(el) {
  24. return this._makeDomArray(el, clone);
  25. }, this));
  26. } else if (typeof elem === 'string') {
  27. return evaluate(elem, this.options);
  28. } else {
  29. return clone ? cloneDom([elem]) : [elem];
  30. }
  31. };
  32. var _insert = function(concatenator) {
  33. return function() {
  34. var elems = slice.call(arguments),
  35. lastIdx = this.length - 1;
  36. return domEach(this, function(i, el) {
  37. var dom, domSrc;
  38. if (typeof elems[0] === 'function') {
  39. domSrc = elems[0].call(el, i, $.html(el.children));
  40. } else {
  41. domSrc = elems;
  42. }
  43. dom = this._makeDomArray(domSrc, i < lastIdx);
  44. concatenator(dom, el.children, el);
  45. });
  46. };
  47. };
  48. /*
  49. * Modify an array in-place, removing some number of elements and adding new
  50. * elements directly following them.
  51. *
  52. * @param {Array} array Target array to splice.
  53. * @param {Number} spliceIdx Index at which to begin changing the array.
  54. * @param {Number} spliceCount Number of elements to remove from the array.
  55. * @param {Array} newElems Elements to insert into the array.
  56. *
  57. * @api private
  58. */
  59. var uniqueSplice = function(array, spliceIdx, spliceCount, newElems, parent) {
  60. var spliceArgs = [spliceIdx, spliceCount].concat(newElems),
  61. prev = array[spliceIdx - 1] || null,
  62. next = array[spliceIdx] || null;
  63. var idx, len, prevIdx, node, oldParent;
  64. // Before splicing in new elements, ensure they do not already appear in the
  65. // current array.
  66. for (idx = 0, len = newElems.length; idx < len; ++idx) {
  67. node = newElems[idx];
  68. oldParent = node.parent || node.root;
  69. prevIdx = oldParent && oldParent.children.indexOf(newElems[idx]);
  70. if (oldParent && prevIdx > -1) {
  71. oldParent.children.splice(prevIdx, 1);
  72. if (parent === oldParent && spliceIdx > prevIdx) {
  73. spliceArgs[0]--;
  74. }
  75. }
  76. node.root = null;
  77. node.parent = parent;
  78. if (node.prev) {
  79. node.prev.next = node.next || null;
  80. }
  81. if (node.next) {
  82. node.next.prev = node.prev || null;
  83. }
  84. node.prev = newElems[idx - 1] || prev;
  85. node.next = newElems[idx + 1] || next;
  86. }
  87. if (prev) {
  88. prev.next = newElems[0];
  89. }
  90. if (next) {
  91. next.prev = newElems[newElems.length - 1];
  92. }
  93. return array.splice.apply(array, spliceArgs);
  94. };
  95. exports.appendTo = function(target) {
  96. if (!target.cheerio) {
  97. target = this.constructor.call(this.constructor, target, null, this._originalRoot);
  98. }
  99. target.append(this);
  100. return this;
  101. };
  102. exports.prependTo = function(target) {
  103. if (!target.cheerio) {
  104. target = this.constructor.call(this.constructor, target, null, this._originalRoot);
  105. }
  106. target.prepend(this);
  107. return this;
  108. };
  109. exports.append = _insert(function(dom, children, parent) {
  110. uniqueSplice(children, children.length, 0, dom, parent);
  111. });
  112. exports.prepend = _insert(function(dom, children, parent) {
  113. uniqueSplice(children, 0, 0, dom, parent);
  114. });
  115. exports.wrap = function(wrapper) {
  116. var wrapperFn = typeof wrapper === 'function' && wrapper,
  117. lastIdx = this.length - 1;
  118. _.forEach(this, _.bind(function(el, i) {
  119. var parent = el.parent || el.root,
  120. siblings = parent.children,
  121. dom, index;
  122. if (!parent) {
  123. return;
  124. }
  125. if (wrapperFn) {
  126. wrapper = wrapperFn.call(el, i);
  127. }
  128. if (typeof wrapper === 'string' && !isHtml(wrapper)) {
  129. wrapper = this.parents().last().find(wrapper).clone();
  130. }
  131. dom = this._makeDomArray(wrapper, i < lastIdx).slice(0, 1);
  132. index = siblings.indexOf(el);
  133. updateDOM([el], dom[0]);
  134. // The previous operation removed the current element from the `siblings`
  135. // array, so the `dom` array can be inserted without removing any
  136. // additional elements.
  137. uniqueSplice(siblings, index, 0, dom, parent);
  138. }, this));
  139. return this;
  140. };
  141. exports.after = function() {
  142. var elems = slice.call(arguments),
  143. lastIdx = this.length - 1;
  144. domEach(this, function(i, el) {
  145. var parent = el.parent || el.root;
  146. if (!parent) {
  147. return;
  148. }
  149. var siblings = parent.children,
  150. index = siblings.indexOf(el),
  151. domSrc, dom;
  152. // If not found, move on
  153. if (index < 0) return;
  154. if (typeof elems[0] === 'function') {
  155. domSrc = elems[0].call(el, i, $.html(el.children));
  156. } else {
  157. domSrc = elems;
  158. }
  159. dom = this._makeDomArray(domSrc, i < lastIdx);
  160. // Add element after `this` element
  161. uniqueSplice(siblings, index + 1, 0, dom, parent);
  162. });
  163. return this;
  164. };
  165. exports.insertAfter = function(target) {
  166. var clones = [],
  167. self = this;
  168. if (typeof target === 'string') {
  169. target = this.constructor.call(this.constructor, target, null, this._originalRoot);
  170. }
  171. target = this._makeDomArray(target);
  172. self.remove();
  173. domEach(target, function(i, el) {
  174. var clonedSelf = self._makeDomArray(self.clone());
  175. var parent = el.parent || el.root;
  176. if (!parent) {
  177. return;
  178. }
  179. var siblings = parent.children,
  180. index = siblings.indexOf(el);
  181. // If not found, move on
  182. if (index < 0) return;
  183. // Add cloned `this` element(s) after target element
  184. uniqueSplice(siblings, index + 1, 0, clonedSelf, parent);
  185. clones.push(clonedSelf);
  186. });
  187. return this.constructor.call(this.constructor, this._makeDomArray(clones));
  188. };
  189. exports.before = function() {
  190. var elems = slice.call(arguments),
  191. lastIdx = this.length - 1;
  192. domEach(this, function(i, el) {
  193. var parent = el.parent || el.root;
  194. if (!parent) {
  195. return;
  196. }
  197. var siblings = parent.children,
  198. index = siblings.indexOf(el),
  199. domSrc, dom;
  200. // If not found, move on
  201. if (index < 0) return;
  202. if (typeof elems[0] === 'function') {
  203. domSrc = elems[0].call(el, i, $.html(el.children));
  204. } else {
  205. domSrc = elems;
  206. }
  207. dom = this._makeDomArray(domSrc, i < lastIdx);
  208. // Add element before `el` element
  209. uniqueSplice(siblings, index, 0, dom, parent);
  210. });
  211. return this;
  212. };
  213. exports.insertBefore = function(target) {
  214. var clones = [],
  215. self = this;
  216. if (typeof target === 'string') {
  217. target = this.constructor.call(this.constructor, target, null, this._originalRoot);
  218. }
  219. target = this._makeDomArray(target);
  220. self.remove();
  221. domEach(target, function(i, el) {
  222. var clonedSelf = self._makeDomArray(self.clone());
  223. var parent = el.parent || el.root;
  224. if (!parent) {
  225. return;
  226. }
  227. var siblings = parent.children,
  228. index = siblings.indexOf(el);
  229. // If not found, move on
  230. if (index < 0) return;
  231. // Add cloned `this` element(s) after target element
  232. uniqueSplice(siblings, index, 0, clonedSelf, parent);
  233. clones.push(clonedSelf);
  234. });
  235. return this.constructor.call(this.constructor, this._makeDomArray(clones));
  236. };
  237. /*
  238. remove([selector])
  239. */
  240. exports.remove = function(selector) {
  241. var elems = this;
  242. // Filter if we have selector
  243. if (selector)
  244. elems = elems.filter(selector);
  245. domEach(elems, function(i, el) {
  246. var parent = el.parent || el.root;
  247. if (!parent) {
  248. return;
  249. }
  250. var siblings = parent.children,
  251. index = siblings.indexOf(el);
  252. if (index < 0) return;
  253. siblings.splice(index, 1);
  254. if (el.prev) {
  255. el.prev.next = el.next;
  256. }
  257. if (el.next) {
  258. el.next.prev = el.prev;
  259. }
  260. el.prev = el.next = el.parent = el.root = null;
  261. });
  262. return this;
  263. };
  264. exports.replaceWith = function(content) {
  265. var self = this;
  266. domEach(this, function(i, el) {
  267. var parent = el.parent || el.root;
  268. if (!parent) {
  269. return;
  270. }
  271. var siblings = parent.children,
  272. dom = self._makeDomArray(typeof content === 'function' ? content.call(el, i, el) : content),
  273. index;
  274. // In the case that `dom` contains nodes that already exist in other
  275. // structures, ensure those nodes are properly removed.
  276. updateDOM(dom, null);
  277. index = siblings.indexOf(el);
  278. // Completely remove old element
  279. uniqueSplice(siblings, index, 1, dom, parent);
  280. el.parent = el.prev = el.next = el.root = null;
  281. });
  282. return this;
  283. };
  284. exports.empty = function() {
  285. domEach(this, function(i, el) {
  286. _.forEach(el.children, function(el) {
  287. el.next = el.prev = el.parent = null;
  288. });
  289. el.children.length = 0;
  290. });
  291. return this;
  292. };
  293. /**
  294. * Set/Get the HTML
  295. */
  296. exports.html = function(str) {
  297. if (str === undefined) {
  298. if (!this[0] || !this[0].children) return null;
  299. return $.html(this[0].children, this.options);
  300. }
  301. var opts = this.options;
  302. domEach(this, function(i, el) {
  303. _.forEach(el.children, function(el) {
  304. el.next = el.prev = el.parent = null;
  305. });
  306. var content = str.cheerio ? str.clone().get() : evaluate('' + str, opts);
  307. updateDOM(content, el);
  308. });
  309. return this;
  310. };
  311. exports.toString = function() {
  312. return $.html(this, this.options);
  313. };
  314. exports.text = function(str) {
  315. // If `str` is undefined, act as a "getter"
  316. if (str === undefined) {
  317. return $.text(this);
  318. } else if (typeof str === 'function') {
  319. // Function support
  320. return domEach(this, function(i, el) {
  321. var $el = [el];
  322. return exports.text.call($el, str.call(el, i, $.text($el)));
  323. });
  324. }
  325. // Append text node to each selected elements
  326. domEach(this, function(i, el) {
  327. _.forEach(el.children, function(el) {
  328. el.next = el.prev = el.parent = null;
  329. });
  330. var elem = {
  331. data: '' + str,
  332. type: 'text',
  333. parent: el,
  334. prev: null,
  335. next: null,
  336. children: []
  337. };
  338. updateDOM(elem, el);
  339. });
  340. return this;
  341. };
  342. exports.clone = function() {
  343. return this._make(cloneDom(this.get(), this.options));
  344. };