DomParser.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. /**
  2. * DomParser.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /**
  11. * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make
  12. * sure that the node tree is valid according to the specified schema.
  13. * So for example: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
  14. *
  15. * @example
  16. * var parser = new tinymce.html.DomParser({validate: true}, schema);
  17. * var rootNode = parser.parse('<h1>content</h1>');
  18. *
  19. * @class tinymce.html.DomParser
  20. * @version 3.4
  21. */
  22. define("tinymce/html/DomParser", [
  23. "tinymce/html/Node",
  24. "tinymce/html/Schema",
  25. "tinymce/html/SaxParser",
  26. "tinymce/util/Tools"
  27. ], function(Node, Schema, SaxParser, Tools) {
  28. var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
  29. /**
  30. * Constructs a new DomParser instance.
  31. *
  32. * @constructor
  33. * @method DomParser
  34. * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
  35. * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
  36. */
  37. return function(settings, schema) {
  38. var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {};
  39. settings = settings || {};
  40. settings.validate = "validate" in settings ? settings.validate : true;
  41. settings.root_name = settings.root_name || 'body';
  42. self.schema = schema = schema || new Schema();
  43. function fixInvalidChildren(nodes) {
  44. var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i;
  45. var nonEmptyElements, nonSplitableElements, textBlockElements, sibling, nextNode;
  46. nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
  47. nonEmptyElements = schema.getNonEmptyElements();
  48. textBlockElements = schema.getTextBlockElements();
  49. for (ni = 0; ni < nodes.length; ni++) {
  50. node = nodes[ni];
  51. // Already removed or fixed
  52. if (!node.parent || node.fixed) {
  53. continue;
  54. }
  55. // If the invalid element is a text block and the text block is within a parent LI element
  56. // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office
  57. if (textBlockElements[node.name] && node.parent.name == 'li') {
  58. // Move sibling text blocks after LI element
  59. sibling = node.next;
  60. while (sibling) {
  61. if (textBlockElements[sibling.name]) {
  62. sibling.name = 'li';
  63. sibling.fixed = true;
  64. node.parent.insert(sibling, node.parent);
  65. } else {
  66. break;
  67. }
  68. sibling = sibling.next;
  69. }
  70. // Unwrap current text block
  71. node.unwrap(node);
  72. continue;
  73. }
  74. // Get list of all parent nodes until we find a valid parent to stick the child into
  75. parents = [node];
  76. for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) &&
  77. !nonSplitableElements[parent.name]; parent = parent.parent) {
  78. parents.push(parent);
  79. }
  80. // Found a suitable parent
  81. if (parent && parents.length > 1) {
  82. // Reverse the array since it makes looping easier
  83. parents.reverse();
  84. // Clone the related parent and insert that after the moved node
  85. newParent = currentNode = self.filterNode(parents[0].clone());
  86. // Start cloning and moving children on the left side of the target node
  87. for (i = 0; i < parents.length - 1; i++) {
  88. if (schema.isValidChild(currentNode.name, parents[i].name)) {
  89. tempNode = self.filterNode(parents[i].clone());
  90. currentNode.append(tempNode);
  91. } else {
  92. tempNode = currentNode;
  93. }
  94. for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
  95. nextNode = childNode.next;
  96. tempNode.append(childNode);
  97. childNode = nextNode;
  98. }
  99. currentNode = tempNode;
  100. }
  101. if (!newParent.isEmpty(nonEmptyElements)) {
  102. parent.insert(newParent, parents[0], true);
  103. parent.insert(node, newParent);
  104. } else {
  105. parent.insert(node, parents[0], true);
  106. }
  107. // Check if the element is empty by looking through it's contents and special treatment for <p><br /></p>
  108. parent = parents[0];
  109. if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === 'br') {
  110. parent.empty().remove();
  111. }
  112. } else if (node.parent) {
  113. // If it's an LI try to find a UL/OL for it or wrap it
  114. if (node.name === 'li') {
  115. sibling = node.prev;
  116. if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
  117. sibling.append(node);
  118. continue;
  119. }
  120. sibling = node.next;
  121. if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) {
  122. sibling.insert(node, sibling.firstChild, true);
  123. continue;
  124. }
  125. node.wrap(self.filterNode(new Node('ul', 1)));
  126. continue;
  127. }
  128. // Try wrapping the element in a DIV
  129. if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) {
  130. node.wrap(self.filterNode(new Node('div', 1)));
  131. } else {
  132. // We failed wrapping it, then remove or unwrap it
  133. if (node.name === 'style' || node.name === 'script') {
  134. node.empty().remove();
  135. } else {
  136. node.unwrap();
  137. }
  138. }
  139. }
  140. }
  141. }
  142. /**
  143. * Runs the specified node though the element and attributes filters.
  144. *
  145. * @method filterNode
  146. * @param {tinymce.html.Node} Node the node to run filters on.
  147. * @return {tinymce.html.Node} The passed in node.
  148. */
  149. self.filterNode = function(node) {
  150. var i, name, list;
  151. // Run element filters
  152. if (name in nodeFilters) {
  153. list = matchedNodes[name];
  154. if (list) {
  155. list.push(node);
  156. } else {
  157. matchedNodes[name] = [node];
  158. }
  159. }
  160. // Run attribute filters
  161. i = attributeFilters.length;
  162. while (i--) {
  163. name = attributeFilters[i].name;
  164. if (name in node.attributes.map) {
  165. list = matchedAttributes[name];
  166. if (list) {
  167. list.push(node);
  168. } else {
  169. matchedAttributes[name] = [node];
  170. }
  171. }
  172. }
  173. return node;
  174. };
  175. /**
  176. * Adds a node filter function to the parser, the parser will collect the specified nodes by name
  177. * and then execute the callback ones it has finished parsing the document.
  178. *
  179. * @example
  180. * parser.addNodeFilter('p,h1', function(nodes, name) {
  181. * for (var i = 0; i < nodes.length; i++) {
  182. * console.log(nodes[i].name);
  183. * }
  184. * });
  185. * @method addNodeFilter
  186. * @method {String} name Comma separated list of nodes to collect.
  187. * @param {function} callback Callback function to execute once it has collected nodes.
  188. */
  189. self.addNodeFilter = function(name, callback) {
  190. each(explode(name), function(name) {
  191. var list = nodeFilters[name];
  192. if (!list) {
  193. nodeFilters[name] = list = [];
  194. }
  195. list.push(callback);
  196. });
  197. };
  198. /**
  199. * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes
  200. * and then execute the callback ones it has finished parsing the document.
  201. *
  202. * @example
  203. * parser.addAttributeFilter('src,href', function(nodes, name) {
  204. * for (var i = 0; i < nodes.length; i++) {
  205. * console.log(nodes[i].name);
  206. * }
  207. * });
  208. * @method addAttributeFilter
  209. * @method {String} name Comma separated list of nodes to collect.
  210. * @param {function} callback Callback function to execute once it has collected nodes.
  211. */
  212. self.addAttributeFilter = function(name, callback) {
  213. each(explode(name), function(name) {
  214. var i;
  215. for (i = 0; i < attributeFilters.length; i++) {
  216. if (attributeFilters[i].name === name) {
  217. attributeFilters[i].callbacks.push(callback);
  218. return;
  219. }
  220. }
  221. attributeFilters.push({name: name, callbacks: [callback]});
  222. });
  223. };
  224. /**
  225. * Parses the specified HTML string into a DOM like node tree and returns the result.
  226. *
  227. * @example
  228. * var rootNode = new DomParser({...}).parse('<b>text</b>');
  229. * @method parse
  230. * @param {String} html Html string to sax parse.
  231. * @param {Object} args Optional args object that gets passed to all filter functions.
  232. * @return {tinymce.html.Node} Root node containing the tree.
  233. */
  234. self.parse = function(html, args) {
  235. var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate;
  236. var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement;
  237. var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements;
  238. var children, nonEmptyElements, rootBlockName;
  239. args = args || {};
  240. matchedNodes = {};
  241. matchedAttributes = {};
  242. blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements());
  243. nonEmptyElements = schema.getNonEmptyElements();
  244. children = schema.children;
  245. validate = settings.validate;
  246. rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block;
  247. whiteSpaceElements = schema.getWhiteSpaceElements();
  248. startWhiteSpaceRegExp = /^[ \t\r\n]+/;
  249. endWhiteSpaceRegExp = /[ \t\r\n]+$/;
  250. allWhiteSpaceRegExp = /[ \t\r\n]+/g;
  251. isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
  252. function addRootBlocks() {
  253. var node = rootNode.firstChild, next, rootBlockNode;
  254. // Removes whitespace at beginning and end of block so:
  255. // <p> x </p> -> <p>x</p>
  256. function trim(rootBlockNode) {
  257. if (rootBlockNode) {
  258. node = rootBlockNode.firstChild;
  259. if (node && node.type == 3) {
  260. node.value = node.value.replace(startWhiteSpaceRegExp, '');
  261. }
  262. node = rootBlockNode.lastChild;
  263. if (node && node.type == 3) {
  264. node.value = node.value.replace(endWhiteSpaceRegExp, '');
  265. }
  266. }
  267. }
  268. // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
  269. if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
  270. return;
  271. }
  272. while (node) {
  273. next = node.next;
  274. if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
  275. !blockElements[node.name] && !node.attr('data-mce-type'))) {
  276. if (!rootBlockNode) {
  277. // Create a new root block element
  278. rootBlockNode = createNode(rootBlockName, 1);
  279. rootBlockNode.attr(settings.forced_root_block_attrs);
  280. rootNode.insert(rootBlockNode, node);
  281. rootBlockNode.append(node);
  282. } else {
  283. rootBlockNode.append(node);
  284. }
  285. } else {
  286. trim(rootBlockNode);
  287. rootBlockNode = null;
  288. }
  289. node = next;
  290. }
  291. trim(rootBlockNode);
  292. }
  293. function createNode(name, type) {
  294. var node = new Node(name, type), list;
  295. if (name in nodeFilters) {
  296. list = matchedNodes[name];
  297. if (list) {
  298. list.push(node);
  299. } else {
  300. matchedNodes[name] = [node];
  301. }
  302. }
  303. return node;
  304. }
  305. function removeWhitespaceBefore(node) {
  306. var textNode, textVal, sibling;
  307. for (textNode = node.prev; textNode && textNode.type === 3; ) {
  308. textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
  309. if (textVal.length > 0) {
  310. textNode.value = textVal;
  311. textNode = textNode.prev;
  312. } else {
  313. sibling = textNode.prev;
  314. textNode.remove();
  315. textNode = sibling;
  316. }
  317. }
  318. }
  319. function cloneAndExcludeBlocks(input) {
  320. var name, output = {};
  321. for (name in input) {
  322. if (name !== 'li' && name != 'p') {
  323. output[name] = input[name];
  324. }
  325. }
  326. return output;
  327. }
  328. parser = new SaxParser({
  329. validate: validate,
  330. allow_script_urls: settings.allow_script_urls,
  331. allow_conditional_comments: settings.allow_conditional_comments,
  332. // Exclude P and LI from DOM parsing since it's treated better by the DOM parser
  333. self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
  334. cdata: function(text) {
  335. node.append(createNode('#cdata', 4)).value = text;
  336. },
  337. text: function(text, raw) {
  338. var textNode;
  339. // Trim all redundant whitespace on non white space elements
  340. if (!isInWhiteSpacePreservedElement) {
  341. text = text.replace(allWhiteSpaceRegExp, ' ');
  342. if (node.lastChild && blockElements[node.lastChild.name]) {
  343. text = text.replace(startWhiteSpaceRegExp, '');
  344. }
  345. }
  346. // Do we need to create the node
  347. if (text.length !== 0) {
  348. textNode = createNode('#text', 3);
  349. textNode.raw = !!raw;
  350. node.append(textNode).value = text;
  351. }
  352. },
  353. comment: function(text) {
  354. node.append(createNode('#comment', 8)).value = text;
  355. },
  356. pi: function(name, text) {
  357. node.append(createNode(name, 7)).value = text;
  358. removeWhitespaceBefore(node);
  359. },
  360. doctype: function(text) {
  361. var newNode;
  362. newNode = node.append(createNode('#doctype', 10));
  363. newNode.value = text;
  364. removeWhitespaceBefore(node);
  365. },
  366. start: function(name, attrs, empty) {
  367. var newNode, attrFiltersLen, elementRule, attrName, parent;
  368. elementRule = validate ? schema.getElementRule(name) : {};
  369. if (elementRule) {
  370. newNode = createNode(elementRule.outputName || name, 1);
  371. newNode.attributes = attrs;
  372. newNode.shortEnded = empty;
  373. node.append(newNode);
  374. // Check if node is valid child of the parent node is the child is
  375. // unknown we don't collect it since it's probably a custom element
  376. parent = children[node.name];
  377. if (parent && children[newNode.name] && !parent[newNode.name]) {
  378. invalidChildren.push(newNode);
  379. }
  380. attrFiltersLen = attributeFilters.length;
  381. while (attrFiltersLen--) {
  382. attrName = attributeFilters[attrFiltersLen].name;
  383. if (attrName in attrs.map) {
  384. list = matchedAttributes[attrName];
  385. if (list) {
  386. list.push(newNode);
  387. } else {
  388. matchedAttributes[attrName] = [newNode];
  389. }
  390. }
  391. }
  392. // Trim whitespace before block
  393. if (blockElements[name]) {
  394. removeWhitespaceBefore(newNode);
  395. }
  396. // Change current node if the element wasn't empty i.e not <br /> or <img />
  397. if (!empty) {
  398. node = newNode;
  399. }
  400. // Check if we are inside a whitespace preserved element
  401. if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
  402. isInWhiteSpacePreservedElement = true;
  403. }
  404. }
  405. },
  406. end: function(name) {
  407. var textNode, elementRule, text, sibling, tempNode;
  408. elementRule = validate ? schema.getElementRule(name) : {};
  409. if (elementRule) {
  410. if (blockElements[name]) {
  411. if (!isInWhiteSpacePreservedElement) {
  412. // Trim whitespace of the first node in a block
  413. textNode = node.firstChild;
  414. if (textNode && textNode.type === 3) {
  415. text = textNode.value.replace(startWhiteSpaceRegExp, '');
  416. // Any characters left after trim or should we remove it
  417. if (text.length > 0) {
  418. textNode.value = text;
  419. textNode = textNode.next;
  420. } else {
  421. sibling = textNode.next;
  422. textNode.remove();
  423. textNode = sibling;
  424. // Remove any pure whitespace siblings
  425. while (textNode && textNode.type === 3) {
  426. text = textNode.value;
  427. sibling = textNode.next;
  428. if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
  429. textNode.remove();
  430. textNode = sibling;
  431. }
  432. textNode = sibling;
  433. }
  434. }
  435. }
  436. // Trim whitespace of the last node in a block
  437. textNode = node.lastChild;
  438. if (textNode && textNode.type === 3) {
  439. text = textNode.value.replace(endWhiteSpaceRegExp, '');
  440. // Any characters left after trim or should we remove it
  441. if (text.length > 0) {
  442. textNode.value = text;
  443. textNode = textNode.prev;
  444. } else {
  445. sibling = textNode.prev;
  446. textNode.remove();
  447. textNode = sibling;
  448. // Remove any pure whitespace siblings
  449. while (textNode && textNode.type === 3) {
  450. text = textNode.value;
  451. sibling = textNode.prev;
  452. if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
  453. textNode.remove();
  454. textNode = sibling;
  455. }
  456. textNode = sibling;
  457. }
  458. }
  459. }
  460. }
  461. // Trim start white space
  462. // Removed due to: #5424
  463. /*textNode = node.prev;
  464. if (textNode && textNode.type === 3) {
  465. text = textNode.value.replace(startWhiteSpaceRegExp, '');
  466. if (text.length > 0)
  467. textNode.value = text;
  468. else
  469. textNode.remove();
  470. }*/
  471. }
  472. // Check if we exited a whitespace preserved element
  473. if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
  474. isInWhiteSpacePreservedElement = false;
  475. }
  476. // Handle empty nodes
  477. if (elementRule.removeEmpty || elementRule.paddEmpty) {
  478. if (node.isEmpty(nonEmptyElements)) {
  479. if (elementRule.paddEmpty) {
  480. node.empty().append(new Node('#text', '3')).value = '\u00a0';
  481. } else {
  482. // Leave nodes that have a name like <a name="name">
  483. if (!node.attributes.map.name && !node.attributes.map.id) {
  484. tempNode = node.parent;
  485. node.empty().remove();
  486. node = tempNode;
  487. return;
  488. }
  489. }
  490. }
  491. }
  492. node = node.parent;
  493. }
  494. }
  495. }, schema);
  496. rootNode = node = new Node(args.context || settings.root_name, 11);
  497. parser.parse(html);
  498. // Fix invalid children or report invalid children in a contextual parsing
  499. if (validate && invalidChildren.length) {
  500. if (!args.context) {
  501. fixInvalidChildren(invalidChildren);
  502. } else {
  503. args.invalid = true;
  504. }
  505. }
  506. // Wrap nodes in the root into block elements if the root is body
  507. if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
  508. addRootBlocks();
  509. }
  510. // Run filters only when the contents is valid
  511. if (!args.invalid) {
  512. // Run node filters
  513. for (name in matchedNodes) {
  514. list = nodeFilters[name];
  515. nodes = matchedNodes[name];
  516. // Remove already removed children
  517. fi = nodes.length;
  518. while (fi--) {
  519. if (!nodes[fi].parent) {
  520. nodes.splice(fi, 1);
  521. }
  522. }
  523. for (i = 0, l = list.length; i < l; i++) {
  524. list[i](nodes, name, args);
  525. }
  526. }
  527. // Run attribute filters
  528. for (i = 0, l = attributeFilters.length; i < l; i++) {
  529. list = attributeFilters[i];
  530. if (list.name in matchedAttributes) {
  531. nodes = matchedAttributes[list.name];
  532. // Remove already removed children
  533. fi = nodes.length;
  534. while (fi--) {
  535. if (!nodes[fi].parent) {
  536. nodes.splice(fi, 1);
  537. }
  538. }
  539. for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
  540. list.callbacks[fi](nodes, list.name, args);
  541. }
  542. }
  543. }
  544. }
  545. return rootNode;
  546. };
  547. // Remove <br> at end of block elements Gecko and WebKit injects BR elements to
  548. // make it possible to place the caret inside empty blocks. This logic tries to remove
  549. // these elements and keep br elements that where intended to be there intact
  550. if (settings.remove_trailing_brs) {
  551. self.addNodeFilter('br', function(nodes) {
  552. var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements());
  553. var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName;
  554. var elementRule, textNode;
  555. // Remove brs from body element as well
  556. blockElements.body = 1;
  557. // Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
  558. for (i = 0; i < l; i++) {
  559. node = nodes[i];
  560. parent = node.parent;
  561. if (blockElements[node.parent.name] && node === parent.lastChild) {
  562. // Loop all nodes to the left of the current node and check for other BR elements
  563. // excluding bookmarks since they are invisible
  564. prev = node.prev;
  565. while (prev) {
  566. prevName = prev.name;
  567. // Ignore bookmarks
  568. if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
  569. // Found a non BR element
  570. if (prevName !== "br") {
  571. break;
  572. }
  573. // Found another br it's a <br><br> structure then don't remove anything
  574. if (prevName === 'br') {
  575. node = null;
  576. break;
  577. }
  578. }
  579. prev = prev.prev;
  580. }
  581. if (node) {
  582. node.remove();
  583. // Is the parent to be considered empty after we removed the BR
  584. if (parent.isEmpty(nonEmptyElements)) {
  585. elementRule = schema.getElementRule(parent.name);
  586. // Remove or padd the element depending on schema rule
  587. if (elementRule) {
  588. if (elementRule.removeEmpty) {
  589. parent.remove();
  590. } else if (elementRule.paddEmpty) {
  591. parent.empty().append(new Node('#text', 3)).value = '\u00a0';
  592. }
  593. }
  594. }
  595. }
  596. } else {
  597. // Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
  598. // so they become <p><b><i>&nbsp;</i></b></p>
  599. lastParent = node;
  600. while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
  601. lastParent = parent;
  602. if (blockElements[parent.name]) {
  603. break;
  604. }
  605. parent = parent.parent;
  606. }
  607. if (lastParent === parent) {
  608. textNode = new Node('#text', 3);
  609. textNode.value = '\u00a0';
  610. node.replace(textNode);
  611. }
  612. }
  613. }
  614. });
  615. }
  616. // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
  617. if (!settings.allow_html_in_named_anchor) {
  618. self.addAttributeFilter('id,name', function(nodes) {
  619. var i = nodes.length, sibling, prevSibling, parent, node;
  620. while (i--) {
  621. node = nodes[i];
  622. if (node.name === 'a' && node.firstChild && !node.attr('href')) {
  623. parent = node.parent;
  624. // Move children after current node
  625. sibling = node.lastChild;
  626. do {
  627. prevSibling = sibling.prev;
  628. parent.insert(sibling, node);
  629. sibling = prevSibling;
  630. } while (sibling);
  631. }
  632. }
  633. });
  634. }
  635. };
  636. });