73fbe9470e65b1218a31c918b99795ac7efe40b3.svn-base 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. var log = require('./log');
  2. var punycode = require('punycode');
  3. var NodeContainer = require('./nodecontainer');
  4. var TextContainer = require('./textcontainer');
  5. var PseudoElementContainer = require('./pseudoelementcontainer');
  6. var FontMetrics = require('./fontmetrics');
  7. var Color = require('./color');
  8. var StackingContext = require('./stackingcontext');
  9. var utils = require('./utils');
  10. var bind = utils.bind;
  11. var getBounds = utils.getBounds;
  12. var parseBackgrounds = utils.parseBackgrounds;
  13. var offsetBounds = utils.offsetBounds;
  14. function NodeParser(element, renderer, support, imageLoader, options) {
  15. log("Starting NodeParser");
  16. this.renderer = renderer;
  17. this.options = options;
  18. this.range = null;
  19. this.support = support;
  20. this.renderQueue = [];
  21. this.stack = new StackingContext(true, 1, element.ownerDocument, null);
  22. var parent = new NodeContainer(element, null);
  23. if (options.background) {
  24. renderer.rectangle(0, 0, renderer.width, renderer.height, new Color(options.background));
  25. }
  26. if (element === element.ownerDocument.documentElement) {
  27. // http://www.w3.org/TR/css3-background/#special-backgrounds
  28. var canvasBackground = new NodeContainer(parent.color('backgroundColor').isTransparent() ? element.ownerDocument.body : element.ownerDocument.documentElement, null);
  29. renderer.rectangle(0, 0, renderer.width, renderer.height, canvasBackground.color('backgroundColor'));
  30. }
  31. parent.visibile = parent.isElementVisible();
  32. this.createPseudoHideStyles(element.ownerDocument);
  33. this.disableAnimations(element.ownerDocument);
  34. this.nodes = flatten([parent].concat(this.getChildren(parent)).filter(function(container) {
  35. return container.visible = container.isElementVisible();
  36. }).map(this.getPseudoElements, this));
  37. this.fontMetrics = new FontMetrics();
  38. log("Fetched nodes, total:", this.nodes.length);
  39. log("Calculate overflow clips");
  40. this.calculateOverflowClips();
  41. log("Start fetching images");
  42. this.images = imageLoader.fetch(this.nodes.filter(isElement));
  43. this.ready = this.images.ready.then(bind(function() {
  44. log("Images loaded, starting parsing");
  45. log("Creating stacking contexts");
  46. this.createStackingContexts();
  47. log("Sorting stacking contexts");
  48. this.sortStackingContexts(this.stack);
  49. this.parse(this.stack);
  50. log("Render queue created with " + this.renderQueue.length + " items");
  51. return new Promise(bind(function(resolve) {
  52. if (!options.async) {
  53. this.renderQueue.forEach(this.paint, this);
  54. resolve();
  55. } else if (typeof(options.async) === "function") {
  56. options.async.call(this, this.renderQueue, resolve);
  57. } else if (this.renderQueue.length > 0){
  58. this.renderIndex = 0;
  59. this.asyncRenderer(this.renderQueue, resolve);
  60. } else {
  61. resolve();
  62. }
  63. }, this));
  64. }, this));
  65. }
  66. NodeParser.prototype.calculateOverflowClips = function() {
  67. this.nodes.forEach(function(container) {
  68. if (isElement(container)) {
  69. if (isPseudoElement(container)) {
  70. container.appendToDOM();
  71. }
  72. container.borders = this.parseBorders(container);
  73. var clip = (container.css('overflow') === "hidden") ? [container.borders.clip] : [];
  74. var cssClip = container.parseClip();
  75. if (cssClip && ["absolute", "fixed"].indexOf(container.css('position')) !== -1) {
  76. clip.push([["rect",
  77. container.bounds.left + cssClip.left,
  78. container.bounds.top + cssClip.top,
  79. cssClip.right - cssClip.left,
  80. cssClip.bottom - cssClip.top
  81. ]]);
  82. }
  83. container.clip = hasParentClip(container) ? container.parent.clip.concat(clip) : clip;
  84. container.backgroundClip = (container.css('overflow') !== "hidden") ? container.clip.concat([container.borders.clip]) : container.clip;
  85. if (isPseudoElement(container)) {
  86. container.cleanDOM();
  87. }
  88. } else if (isTextNode(container)) {
  89. container.clip = hasParentClip(container) ? container.parent.clip : [];
  90. }
  91. if (!isPseudoElement(container)) {
  92. container.bounds = null;
  93. }
  94. }, this);
  95. };
  96. function hasParentClip(container) {
  97. return container.parent && container.parent.clip.length;
  98. }
  99. NodeParser.prototype.asyncRenderer = function(queue, resolve, asyncTimer) {
  100. asyncTimer = asyncTimer || Date.now();
  101. this.paint(queue[this.renderIndex++]);
  102. if (queue.length === this.renderIndex) {
  103. resolve();
  104. } else if (asyncTimer + 20 > Date.now()) {
  105. this.asyncRenderer(queue, resolve, asyncTimer);
  106. } else {
  107. setTimeout(bind(function() {
  108. this.asyncRenderer(queue, resolve);
  109. }, this), 0);
  110. }
  111. };
  112. NodeParser.prototype.createPseudoHideStyles = function(document) {
  113. this.createStyles(document, '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + ':before { content: "" !important; display: none !important; }' +
  114. '.' + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER + ':after { content: "" !important; display: none !important; }');
  115. };
  116. NodeParser.prototype.disableAnimations = function(document) {
  117. this.createStyles(document, '* { -webkit-animation: none !important; -moz-animation: none !important; -o-animation: none !important; animation: none !important; ' +
  118. '-webkit-transition: none !important; -moz-transition: none !important; -o-transition: none !important; transition: none !important;}');
  119. };
  120. NodeParser.prototype.createStyles = function(document, styles) {
  121. var hidePseudoElements = document.createElement('style');
  122. hidePseudoElements.innerHTML = styles;
  123. document.body.appendChild(hidePseudoElements);
  124. };
  125. NodeParser.prototype.getPseudoElements = function(container) {
  126. var nodes = [[container]];
  127. if (container.node.nodeType === Node.ELEMENT_NODE) {
  128. var before = this.getPseudoElement(container, ":before");
  129. var after = this.getPseudoElement(container, ":after");
  130. if (before) {
  131. nodes.push(before);
  132. }
  133. if (after) {
  134. nodes.push(after);
  135. }
  136. }
  137. return flatten(nodes);
  138. };
  139. function toCamelCase(str) {
  140. return str.replace(/(\-[a-z])/g, function(match){
  141. return match.toUpperCase().replace('-','');
  142. });
  143. }
  144. NodeParser.prototype.getPseudoElement = function(container, type) {
  145. var style = container.computedStyle(type);
  146. if(!style || !style.content || style.content === "none" || style.content === "-moz-alt-content" || style.display === "none") {
  147. return null;
  148. }
  149. var content = stripQuotes(style.content);
  150. var isImage = content.substr(0, 3) === 'url';
  151. var pseudoNode = document.createElement(isImage ? 'img' : 'html2canvaspseudoelement');
  152. var pseudoContainer = new PseudoElementContainer(pseudoNode, container, type);
  153. for (var i = style.length-1; i >= 0; i--) {
  154. var property = toCamelCase(style.item(i));
  155. pseudoNode.style[property] = style[property];
  156. }
  157. pseudoNode.className = PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_BEFORE + " " + PseudoElementContainer.prototype.PSEUDO_HIDE_ELEMENT_CLASS_AFTER;
  158. if (isImage) {
  159. pseudoNode.src = parseBackgrounds(content)[0].args[0];
  160. return [pseudoContainer];
  161. } else {
  162. var text = document.createTextNode(content);
  163. pseudoNode.appendChild(text);
  164. return [pseudoContainer, new TextContainer(text, pseudoContainer)];
  165. }
  166. };
  167. NodeParser.prototype.getChildren = function(parentContainer) {
  168. return flatten([].filter.call(parentContainer.node.childNodes, renderableNode).map(function(node) {
  169. var container = [node.nodeType === Node.TEXT_NODE ? new TextContainer(node, parentContainer) : new NodeContainer(node, parentContainer)].filter(nonIgnoredElement);
  170. return node.nodeType === Node.ELEMENT_NODE && container.length && node.tagName !== "TEXTAREA" ? (container[0].isElementVisible() ? container.concat(this.getChildren(container[0])) : []) : container;
  171. }, this));
  172. };
  173. NodeParser.prototype.newStackingContext = function(container, hasOwnStacking) {
  174. var stack = new StackingContext(hasOwnStacking, container.getOpacity(), container.node, container.parent);
  175. container.cloneTo(stack);
  176. var parentStack = hasOwnStacking ? stack.getParentStack(this) : stack.parent.stack;
  177. parentStack.contexts.push(stack);
  178. container.stack = stack;
  179. };
  180. NodeParser.prototype.createStackingContexts = function() {
  181. this.nodes.forEach(function(container) {
  182. if (isElement(container) && (this.isRootElement(container) || hasOpacity(container) || isPositionedForStacking(container) || this.isBodyWithTransparentRoot(container) || container.hasTransform())) {
  183. this.newStackingContext(container, true);
  184. } else if (isElement(container) && ((isPositioned(container) && zIndex0(container)) || isInlineBlock(container) || isFloating(container))) {
  185. this.newStackingContext(container, false);
  186. } else {
  187. container.assignStack(container.parent.stack);
  188. }
  189. }, this);
  190. };
  191. NodeParser.prototype.isBodyWithTransparentRoot = function(container) {
  192. return container.node.nodeName === "BODY" && container.parent.color('backgroundColor').isTransparent();
  193. };
  194. NodeParser.prototype.isRootElement = function(container) {
  195. return container.parent === null;
  196. };
  197. NodeParser.prototype.sortStackingContexts = function(stack) {
  198. stack.contexts.sort(zIndexSort(stack.contexts.slice(0)));
  199. stack.contexts.forEach(this.sortStackingContexts, this);
  200. };
  201. NodeParser.prototype.parseTextBounds = function(container) {
  202. return function(text, index, textList) {
  203. if (container.parent.css("textDecoration").substr(0, 4) !== "none" || text.trim().length !== 0) {
  204. if (this.support.rangeBounds && !container.parent.hasTransform()) {
  205. var offset = textList.slice(0, index).join("").length;
  206. return this.getRangeBounds(container.node, offset, text.length);
  207. } else if (container.node && typeof(container.node.data) === "string") {
  208. var replacementNode = container.node.splitText(text.length);
  209. var bounds = this.getWrapperBounds(container.node, container.parent.hasTransform());
  210. container.node = replacementNode;
  211. return bounds;
  212. }
  213. } else if(!this.support.rangeBounds || container.parent.hasTransform()){
  214. container.node = container.node.splitText(text.length);
  215. }
  216. return {};
  217. };
  218. };
  219. NodeParser.prototype.getWrapperBounds = function(node, transform) {
  220. var wrapper = node.ownerDocument.createElement('html2canvaswrapper');
  221. var parent = node.parentNode,
  222. backupText = node.cloneNode(true);
  223. wrapper.appendChild(node.cloneNode(true));
  224. parent.replaceChild(wrapper, node);
  225. var bounds = transform ? offsetBounds(wrapper) : getBounds(wrapper);
  226. parent.replaceChild(backupText, wrapper);
  227. return bounds;
  228. };
  229. NodeParser.prototype.getRangeBounds = function(node, offset, length) {
  230. var range = this.range || (this.range = node.ownerDocument.createRange());
  231. range.setStart(node, offset);
  232. range.setEnd(node, offset + length);
  233. return range.getBoundingClientRect();
  234. };
  235. function ClearTransform() {}
  236. NodeParser.prototype.parse = function(stack) {
  237. // http://www.w3.org/TR/CSS21/visuren.html#z-index
  238. var negativeZindex = stack.contexts.filter(negativeZIndex); // 2. the child stacking contexts with negative stack levels (most negative first).
  239. var descendantElements = stack.children.filter(isElement);
  240. var descendantNonFloats = descendantElements.filter(not(isFloating));
  241. var nonInlineNonPositionedDescendants = descendantNonFloats.filter(not(isPositioned)).filter(not(inlineLevel)); // 3 the in-flow, non-inline-level, non-positioned descendants.
  242. var nonPositionedFloats = descendantElements.filter(not(isPositioned)).filter(isFloating); // 4. the non-positioned floats.
  243. var inFlow = descendantNonFloats.filter(not(isPositioned)).filter(inlineLevel); // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
  244. var stackLevel0 = stack.contexts.concat(descendantNonFloats.filter(isPositioned)).filter(zIndex0); // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
  245. var text = stack.children.filter(isTextNode).filter(hasText);
  246. var positiveZindex = stack.contexts.filter(positiveZIndex); // 7. the child stacking contexts with positive stack levels (least positive first).
  247. negativeZindex.concat(nonInlineNonPositionedDescendants).concat(nonPositionedFloats)
  248. .concat(inFlow).concat(stackLevel0).concat(text).concat(positiveZindex).forEach(function(container) {
  249. this.renderQueue.push(container);
  250. if (isStackingContext(container)) {
  251. this.parse(container);
  252. this.renderQueue.push(new ClearTransform());
  253. }
  254. }, this);
  255. };
  256. NodeParser.prototype.paint = function(container) {
  257. try {
  258. if (container instanceof ClearTransform) {
  259. this.renderer.ctx.restore();
  260. } else if (isTextNode(container)) {
  261. if (isPseudoElement(container.parent)) {
  262. container.parent.appendToDOM();
  263. }
  264. this.paintText(container);
  265. if (isPseudoElement(container.parent)) {
  266. container.parent.cleanDOM();
  267. }
  268. } else {
  269. this.paintNode(container);
  270. }
  271. } catch(e) {
  272. log(e);
  273. if (this.options.strict) {
  274. throw e;
  275. }
  276. }
  277. };
  278. NodeParser.prototype.paintNode = function(container) {
  279. if (isStackingContext(container)) {
  280. this.renderer.setOpacity(container.opacity);
  281. this.renderer.ctx.save();
  282. if (container.hasTransform()) {
  283. this.renderer.setTransform(container.parseTransform());
  284. }
  285. }
  286. if (container.node.nodeName === "INPUT" && container.node.type === "checkbox") {
  287. this.paintCheckbox(container);
  288. } else if (container.node.nodeName === "INPUT" && container.node.type === "radio") {
  289. this.paintRadio(container);
  290. } else {
  291. this.paintElement(container);
  292. }
  293. };
  294. NodeParser.prototype.paintElement = function(container) {
  295. var bounds = container.parseBounds();
  296. this.renderer.clip(container.backgroundClip, function() {
  297. this.renderer.renderBackground(container, bounds, container.borders.borders.map(getWidth));
  298. }, this);
  299. this.renderer.clip(container.clip, function() {
  300. this.renderer.renderBorders(container.borders.borders);
  301. }, this);
  302. this.renderer.clip(container.backgroundClip, function() {
  303. switch (container.node.nodeName) {
  304. case "svg":
  305. case "IFRAME":
  306. var imgContainer = this.images.get(container.node);
  307. if (imgContainer) {
  308. this.renderer.renderImage(container, bounds, container.borders, imgContainer);
  309. } else {
  310. log("Error loading <" + container.node.nodeName + ">", container.node);
  311. }
  312. break;
  313. case "IMG":
  314. var imageContainer = this.images.get(container.node.src);
  315. if (imageContainer) {
  316. this.renderer.renderImage(container, bounds, container.borders, imageContainer);
  317. } else {
  318. log("Error loading <img>", container.node.src);
  319. }
  320. break;
  321. case "CANVAS":
  322. this.renderer.renderImage(container, bounds, container.borders, {image: container.node});
  323. break;
  324. case "SELECT":
  325. case "INPUT":
  326. case "TEXTAREA":
  327. this.paintFormValue(container);
  328. break;
  329. }
  330. }, this);
  331. };
  332. NodeParser.prototype.paintCheckbox = function(container) {
  333. var b = container.parseBounds();
  334. var size = Math.min(b.width, b.height);
  335. var bounds = {width: size - 1, height: size - 1, top: b.top, left: b.left};
  336. var r = [3, 3];
  337. var radius = [r, r, r, r];
  338. var borders = [1,1,1,1].map(function(w) {
  339. return {color: new Color('#A5A5A5'), width: w};
  340. });
  341. var borderPoints = calculateCurvePoints(bounds, radius, borders);
  342. this.renderer.clip(container.backgroundClip, function() {
  343. this.renderer.rectangle(bounds.left + 1, bounds.top + 1, bounds.width - 2, bounds.height - 2, new Color("#DEDEDE"));
  344. this.renderer.renderBorders(calculateBorders(borders, bounds, borderPoints, radius));
  345. if (container.node.checked) {
  346. this.renderer.font(new Color('#424242'), 'normal', 'normal', 'bold', (size - 3) + "px", 'arial');
  347. this.renderer.text("\u2714", bounds.left + size / 6, bounds.top + size - 1);
  348. }
  349. }, this);
  350. };
  351. NodeParser.prototype.paintRadio = function(container) {
  352. var bounds = container.parseBounds();
  353. var size = Math.min(bounds.width, bounds.height) - 2;
  354. this.renderer.clip(container.backgroundClip, function() {
  355. this.renderer.circleStroke(bounds.left + 1, bounds.top + 1, size, new Color('#DEDEDE'), 1, new Color('#A5A5A5'));
  356. if (container.node.checked) {
  357. this.renderer.circle(Math.ceil(bounds.left + size / 4) + 1, Math.ceil(bounds.top + size / 4) + 1, Math.floor(size / 2), new Color('#424242'));
  358. }
  359. }, this);
  360. };
  361. NodeParser.prototype.paintFormValue = function(container) {
  362. var value = container.getValue();
  363. if (value.length > 0) {
  364. var document = container.node.ownerDocument;
  365. var wrapper = document.createElement('html2canvaswrapper');
  366. var properties = ['lineHeight', 'textAlign', 'fontFamily', 'fontWeight', 'fontSize', 'color',
  367. 'paddingLeft', 'paddingTop', 'paddingRight', 'paddingBottom',
  368. 'width', 'height', 'borderLeftStyle', 'borderTopStyle', 'borderLeftWidth', 'borderTopWidth',
  369. 'boxSizing', 'whiteSpace', 'wordWrap'];
  370. properties.forEach(function(property) {
  371. try {
  372. wrapper.style[property] = container.css(property);
  373. } catch(e) {
  374. // Older IE has issues with "border"
  375. log("html2canvas: Parse: Exception caught in renderFormValue: " + e.message);
  376. }
  377. });
  378. var bounds = container.parseBounds();
  379. wrapper.style.position = "fixed";
  380. wrapper.style.left = bounds.left + "px";
  381. wrapper.style.top = bounds.top + "px";
  382. wrapper.textContent = value;
  383. document.body.appendChild(wrapper);
  384. this.paintText(new TextContainer(wrapper.firstChild, container));
  385. document.body.removeChild(wrapper);
  386. }
  387. };
  388. NodeParser.prototype.paintText = function(container) {
  389. container.applyTextTransform();
  390. var characters = punycode.ucs2.decode(container.node.data);
  391. var textList = (!this.options.letterRendering || noLetterSpacing(container)) && !hasUnicode(container.node.data) ? getWords(characters) : characters.map(function(character) {
  392. return punycode.ucs2.encode([character]);
  393. });
  394. var weight = container.parent.fontWeight();
  395. var size = container.parent.css('fontSize');
  396. var family = container.parent.css('fontFamily');
  397. var shadows = container.parent.parseTextShadows();
  398. this.renderer.font(container.parent.color('color'), container.parent.css('fontStyle'), container.parent.css('fontVariant'), weight, size, family);
  399. if (shadows.length) {
  400. // TODO: support multiple text shadows
  401. this.renderer.fontShadow(shadows[0].color, shadows[0].offsetX, shadows[0].offsetY, shadows[0].blur);
  402. } else {
  403. this.renderer.clearShadow();
  404. }
  405. this.renderer.clip(container.parent.clip, function() {
  406. textList.map(this.parseTextBounds(container), this).forEach(function(bounds, index) {
  407. if (bounds) {
  408. this.renderer.text(textList[index], bounds.left, bounds.bottom);
  409. this.renderTextDecoration(container.parent, bounds, this.fontMetrics.getMetrics(family, size));
  410. }
  411. }, this);
  412. }, this);
  413. };
  414. NodeParser.prototype.renderTextDecoration = function(container, bounds, metrics) {
  415. switch(container.css("textDecoration").split(" ")[0]) {
  416. case "underline":
  417. // Draws a line at the baseline of the font
  418. // TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size
  419. this.renderer.rectangle(bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, container.color("color"));
  420. break;
  421. case "overline":
  422. this.renderer.rectangle(bounds.left, Math.round(bounds.top), bounds.width, 1, container.color("color"));
  423. break;
  424. case "line-through":
  425. // TODO try and find exact position for line-through
  426. this.renderer.rectangle(bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, container.color("color"));
  427. break;
  428. }
  429. };
  430. var borderColorTransforms = {
  431. inset: [
  432. ["darken", 0.60],
  433. ["darken", 0.10],
  434. ["darken", 0.10],
  435. ["darken", 0.60]
  436. ]
  437. };
  438. NodeParser.prototype.parseBorders = function(container) {
  439. var nodeBounds = container.parseBounds();
  440. var radius = getBorderRadiusData(container);
  441. var borders = ["Top", "Right", "Bottom", "Left"].map(function(side, index) {
  442. var style = container.css('border' + side + 'Style');
  443. var color = container.color('border' + side + 'Color');
  444. if (style === "inset" && color.isBlack()) {
  445. color = new Color([255, 255, 255, color.a]); // this is wrong, but
  446. }
  447. var colorTransform = borderColorTransforms[style] ? borderColorTransforms[style][index] : null;
  448. return {
  449. width: container.cssInt('border' + side + 'Width'),
  450. color: colorTransform ? color[colorTransform[0]](colorTransform[1]) : color,
  451. args: null
  452. };
  453. });
  454. var borderPoints = calculateCurvePoints(nodeBounds, radius, borders);
  455. return {
  456. clip: this.parseBackgroundClip(container, borderPoints, borders, radius, nodeBounds),
  457. borders: calculateBorders(borders, nodeBounds, borderPoints, radius)
  458. };
  459. };
  460. function calculateBorders(borders, nodeBounds, borderPoints, radius) {
  461. return borders.map(function(border, borderSide) {
  462. if (border.width > 0) {
  463. var bx = nodeBounds.left;
  464. var by = nodeBounds.top;
  465. var bw = nodeBounds.width;
  466. var bh = nodeBounds.height - (borders[2].width);
  467. switch(borderSide) {
  468. case 0:
  469. // top border
  470. bh = borders[0].width;
  471. border.args = drawSide({
  472. c1: [bx, by],
  473. c2: [bx + bw, by],
  474. c3: [bx + bw - borders[1].width, by + bh],
  475. c4: [bx + borders[3].width, by + bh]
  476. }, radius[0], radius[1],
  477. borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner);
  478. break;
  479. case 1:
  480. // right border
  481. bx = nodeBounds.left + nodeBounds.width - (borders[1].width);
  482. bw = borders[1].width;
  483. border.args = drawSide({
  484. c1: [bx + bw, by],
  485. c2: [bx + bw, by + bh + borders[2].width],
  486. c3: [bx, by + bh],
  487. c4: [bx, by + borders[0].width]
  488. }, radius[1], radius[2],
  489. borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner);
  490. break;
  491. case 2:
  492. // bottom border
  493. by = (by + nodeBounds.height) - (borders[2].width);
  494. bh = borders[2].width;
  495. border.args = drawSide({
  496. c1: [bx + bw, by + bh],
  497. c2: [bx, by + bh],
  498. c3: [bx + borders[3].width, by],
  499. c4: [bx + bw - borders[3].width, by]
  500. }, radius[2], radius[3],
  501. borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner);
  502. break;
  503. case 3:
  504. // left border
  505. bw = borders[3].width;
  506. border.args = drawSide({
  507. c1: [bx, by + bh + borders[2].width],
  508. c2: [bx, by],
  509. c3: [bx + bw, by + borders[0].width],
  510. c4: [bx + bw, by + bh]
  511. }, radius[3], radius[0],
  512. borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner);
  513. break;
  514. }
  515. }
  516. return border;
  517. });
  518. }
  519. NodeParser.prototype.parseBackgroundClip = function(container, borderPoints, borders, radius, bounds) {
  520. var backgroundClip = container.css('backgroundClip'),
  521. borderArgs = [];
  522. switch(backgroundClip) {
  523. case "content-box":
  524. case "padding-box":
  525. parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width);
  526. parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width);
  527. parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width);
  528. parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width);
  529. break;
  530. default:
  531. parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top);
  532. parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top);
  533. parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height);
  534. parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height);
  535. break;
  536. }
  537. return borderArgs;
  538. };
  539. function getCurvePoints(x, y, r1, r2) {
  540. var kappa = 4 * ((Math.sqrt(2) - 1) / 3);
  541. var ox = (r1) * kappa, // control point offset horizontal
  542. oy = (r2) * kappa, // control point offset vertical
  543. xm = x + r1, // x-middle
  544. ym = y + r2; // y-middle
  545. return {
  546. topLeft: bezierCurve({x: x, y: ym}, {x: x, y: ym - oy}, {x: xm - ox, y: y}, {x: xm, y: y}),
  547. topRight: bezierCurve({x: x, y: y}, {x: x + ox,y: y}, {x: xm, y: ym - oy}, {x: xm, y: ym}),
  548. bottomRight: bezierCurve({x: xm, y: y}, {x: xm, y: y + oy}, {x: x + ox, y: ym}, {x: x, y: ym}),
  549. bottomLeft: bezierCurve({x: xm, y: ym}, {x: xm - ox, y: ym}, {x: x, y: y + oy}, {x: x, y:y})
  550. };
  551. }
  552. function calculateCurvePoints(bounds, borderRadius, borders) {
  553. var x = bounds.left,
  554. y = bounds.top,
  555. width = bounds.width,
  556. height = bounds.height,
  557. tlh = borderRadius[0][0] < width / 2 ? borderRadius[0][0] : width / 2,
  558. tlv = borderRadius[0][1] < height / 2 ? borderRadius[0][1] : height / 2,
  559. trh = borderRadius[1][0] < width / 2 ? borderRadius[1][0] : width / 2,
  560. trv = borderRadius[1][1] < height / 2 ? borderRadius[1][1] : height / 2,
  561. brh = borderRadius[2][0] < width / 2 ? borderRadius[2][0] : width / 2,
  562. brv = borderRadius[2][1] < height / 2 ? borderRadius[2][1] : height / 2,
  563. blh = borderRadius[3][0] < width / 2 ? borderRadius[3][0] : width / 2,
  564. blv = borderRadius[3][1] < height / 2 ? borderRadius[3][1] : height / 2;
  565. var topWidth = width - trh,
  566. rightHeight = height - brv,
  567. bottomWidth = width - brh,
  568. leftHeight = height - blv;
  569. return {
  570. topLeftOuter: getCurvePoints(x, y, tlh, tlv).topLeft.subdivide(0.5),
  571. topLeftInner: getCurvePoints(x + borders[3].width, y + borders[0].width, Math.max(0, tlh - borders[3].width), Math.max(0, tlv - borders[0].width)).topLeft.subdivide(0.5),
  572. topRightOuter: getCurvePoints(x + topWidth, y, trh, trv).topRight.subdivide(0.5),
  573. topRightInner: getCurvePoints(x + Math.min(topWidth, width + borders[3].width), y + borders[0].width, (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width, trv - borders[0].width).topRight.subdivide(0.5),
  574. bottomRightOuter: getCurvePoints(x + bottomWidth, y + rightHeight, brh, brv).bottomRight.subdivide(0.5),
  575. bottomRightInner: getCurvePoints(x + Math.min(bottomWidth, width - borders[3].width), y + Math.min(rightHeight, height + borders[0].width), Math.max(0, brh - borders[1].width), brv - borders[2].width).bottomRight.subdivide(0.5),
  576. bottomLeftOuter: getCurvePoints(x, y + leftHeight, blh, blv).bottomLeft.subdivide(0.5),
  577. bottomLeftInner: getCurvePoints(x + borders[3].width, y + leftHeight, Math.max(0, blh - borders[3].width), blv - borders[2].width).bottomLeft.subdivide(0.5)
  578. };
  579. }
  580. function bezierCurve(start, startControl, endControl, end) {
  581. var lerp = function (a, b, t) {
  582. return {
  583. x: a.x + (b.x - a.x) * t,
  584. y: a.y + (b.y - a.y) * t
  585. };
  586. };
  587. return {
  588. start: start,
  589. startControl: startControl,
  590. endControl: endControl,
  591. end: end,
  592. subdivide: function(t) {
  593. var ab = lerp(start, startControl, t),
  594. bc = lerp(startControl, endControl, t),
  595. cd = lerp(endControl, end, t),
  596. abbc = lerp(ab, bc, t),
  597. bccd = lerp(bc, cd, t),
  598. dest = lerp(abbc, bccd, t);
  599. return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)];
  600. },
  601. curveTo: function(borderArgs) {
  602. borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]);
  603. },
  604. curveToReversed: function(borderArgs) {
  605. borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]);
  606. }
  607. };
  608. }
  609. function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) {
  610. var borderArgs = [];
  611. if (radius1[0] > 0 || radius1[1] > 0) {
  612. borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]);
  613. outer1[1].curveTo(borderArgs);
  614. } else {
  615. borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]);
  616. }
  617. if (radius2[0] > 0 || radius2[1] > 0) {
  618. borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]);
  619. outer2[0].curveTo(borderArgs);
  620. borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]);
  621. inner2[0].curveToReversed(borderArgs);
  622. } else {
  623. borderArgs.push(["line", borderData.c2[0], borderData.c2[1]]);
  624. borderArgs.push(["line", borderData.c3[0], borderData.c3[1]]);
  625. }
  626. if (radius1[0] > 0 || radius1[1] > 0) {
  627. borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]);
  628. inner1[1].curveToReversed(borderArgs);
  629. } else {
  630. borderArgs.push(["line", borderData.c4[0], borderData.c4[1]]);
  631. }
  632. return borderArgs;
  633. }
  634. function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) {
  635. if (radius1[0] > 0 || radius1[1] > 0) {
  636. borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]);
  637. corner1[0].curveTo(borderArgs);
  638. corner1[1].curveTo(borderArgs);
  639. } else {
  640. borderArgs.push(["line", x, y]);
  641. }
  642. if (radius2[0] > 0 || radius2[1] > 0) {
  643. borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]);
  644. }
  645. }
  646. function negativeZIndex(container) {
  647. return container.cssInt("zIndex") < 0;
  648. }
  649. function positiveZIndex(container) {
  650. return container.cssInt("zIndex") > 0;
  651. }
  652. function zIndex0(container) {
  653. return container.cssInt("zIndex") === 0;
  654. }
  655. function inlineLevel(container) {
  656. return ["inline", "inline-block", "inline-table"].indexOf(container.css("display")) !== -1;
  657. }
  658. function isStackingContext(container) {
  659. return (container instanceof StackingContext);
  660. }
  661. function hasText(container) {
  662. return container.node.data.trim().length > 0;
  663. }
  664. function noLetterSpacing(container) {
  665. return (/^(normal|none|0px)$/.test(container.parent.css("letterSpacing")));
  666. }
  667. function getBorderRadiusData(container) {
  668. return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) {
  669. var value = container.css('border' + side + 'Radius');
  670. var arr = value.split(" ");
  671. if (arr.length <= 1) {
  672. arr[1] = arr[0];
  673. }
  674. return arr.map(asInt);
  675. });
  676. }
  677. function renderableNode(node) {
  678. return (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE);
  679. }
  680. function isPositionedForStacking(container) {
  681. var position = container.css("position");
  682. var zIndex = (["absolute", "relative", "fixed"].indexOf(position) !== -1) ? container.css("zIndex") : "auto";
  683. return zIndex !== "auto";
  684. }
  685. function isPositioned(container) {
  686. return container.css("position") !== "static";
  687. }
  688. function isFloating(container) {
  689. return container.css("float") !== "none";
  690. }
  691. function isInlineBlock(container) {
  692. return ["inline-block", "inline-table"].indexOf(container.css("display")) !== -1;
  693. }
  694. function not(callback) {
  695. var context = this;
  696. return function() {
  697. return !callback.apply(context, arguments);
  698. };
  699. }
  700. function isElement(container) {
  701. return container.node.nodeType === Node.ELEMENT_NODE;
  702. }
  703. function isPseudoElement(container) {
  704. return container.isPseudoElement === true;
  705. }
  706. function isTextNode(container) {
  707. return container.node.nodeType === Node.TEXT_NODE;
  708. }
  709. function zIndexSort(contexts) {
  710. return function(a, b) {
  711. return (a.cssInt("zIndex") + (contexts.indexOf(a) / contexts.length)) - (b.cssInt("zIndex") + (contexts.indexOf(b) / contexts.length));
  712. };
  713. }
  714. function hasOpacity(container) {
  715. return container.getOpacity() < 1;
  716. }
  717. function asInt(value) {
  718. return parseInt(value, 10);
  719. }
  720. function getWidth(border) {
  721. return border.width;
  722. }
  723. function nonIgnoredElement(nodeContainer) {
  724. return (nodeContainer.node.nodeType !== Node.ELEMENT_NODE || ["SCRIPT", "HEAD", "TITLE", "OBJECT", "BR", "OPTION"].indexOf(nodeContainer.node.nodeName) === -1);
  725. }
  726. function flatten(arrays) {
  727. return [].concat.apply([], arrays);
  728. }
  729. function stripQuotes(content) {
  730. var first = content.substr(0, 1);
  731. return (first === content.substr(content.length - 1) && first.match(/'|"/)) ? content.substr(1, content.length - 2) : content;
  732. }
  733. function getWords(characters) {
  734. var words = [], i = 0, onWordBoundary = false, word;
  735. while(characters.length) {
  736. if (isWordBoundary(characters[i]) === onWordBoundary) {
  737. word = characters.splice(0, i);
  738. if (word.length) {
  739. words.push(punycode.ucs2.encode(word));
  740. }
  741. onWordBoundary =! onWordBoundary;
  742. i = 0;
  743. } else {
  744. i++;
  745. }
  746. if (i >= characters.length) {
  747. word = characters.splice(0, i);
  748. if (word.length) {
  749. words.push(punycode.ucs2.encode(word));
  750. }
  751. }
  752. }
  753. return words;
  754. }
  755. function isWordBoundary(characterCode) {
  756. return [
  757. 32, // <space>
  758. 13, // \r
  759. 10, // \n
  760. 9, // \t
  761. 45 // -
  762. ].indexOf(characterCode) !== -1;
  763. }
  764. function hasUnicode(string) {
  765. return (/[^\u0000-\u00ff]/).test(string);
  766. }
  767. module.exports = NodeParser;