';
198 |
199 | FamilyName.parse = function(str) {
200 | var first = str.charAt(0);
201 | var last = str.charAt(str.length - 1);
202 |
203 | var isFirstQuote = /'|"/.test(first);
204 | var isLastQuote = /'|"/.test(last);
205 |
206 | if((isFirstQuote || isLastQuote) && first !== last) return;
207 | if(isFirstQuote && isLastQuote) str = str.slice(1, -1);
208 |
209 | return new FamilyName(str);
210 | };
211 |
212 | FamilyName.is = function(value) {
213 | return value.type === FamilyName.TYPE;
214 | };
215 |
216 | FamilyName.prototype.type = FamilyName.TYPE;
217 | FamilyName.prototype.toString = function() {
218 | return util.format('"%s"', this.name);
219 | };
220 |
221 | var Keyword = define(function(keyword) {
222 | this.keyword = keyword;
223 | this.normalized = keyword.toLowerCase();
224 | this.type = keyword;
225 | });
226 |
227 | Keyword.prototype.parse = function(str) {
228 | if(this.normalized === str.toLowerCase()) return this;
229 | };
230 |
231 | Keyword.prototype.is = function(value) {
232 | return !!(value.keyword && value.keyword === this.keyword);
233 | };
234 |
235 | Keyword.prototype.toString = function() {
236 | return this.keyword;
237 | };
238 |
239 | Object.keys(declarations).forEach(function(property) {
240 | var definition = declarations[property];
241 | if(typeof definition === 'string') return;
242 |
243 | keywords(definition.values, Keyword);
244 | });
245 |
246 | Keyword.Initial = new Keyword('initial');
247 | Keyword.Inherit = new Keyword('inherit');
248 |
249 | exports.CommaSeparated = CommaSeparated;
250 | exports.Length = Length;
251 | exports.Percentage = Percentage;
252 | exports.Number = Number;
253 | exports.Color = Color;
254 | exports.FamilyName = FamilyName;
255 | exports.Keyword = Keyword;
256 |
--------------------------------------------------------------------------------
/source/draw.js:
--------------------------------------------------------------------------------
1 | var Viewport = require('./layout/viewport');
2 | var LineBox = require('./layout/line-box');
3 | var TextBox = require('./layout/text-box');
4 | var ImageBox = require('./layout/image-box');
5 |
6 | var background = require('./draw/background');
7 | var border = require('./draw/border');
8 | var text = require('./draw/text');
9 | var image = require('./draw/image');
10 |
11 | var drawChildren = function(box, context) {
12 | if(!box.children) return;
13 |
14 | box.children.forEach(function(child) {
15 | draw(child, context);
16 | });
17 | };
18 |
19 | var draw = function(box, context) {
20 | if(box instanceof Viewport || box instanceof LineBox) return drawChildren(box, context);
21 | if(box instanceof TextBox) return text(box, context);
22 |
23 | background(box, context);
24 | border(box, context);
25 |
26 | if(box instanceof ImageBox) image(box, context);
27 |
28 | drawChildren(box, context);
29 | };
30 |
31 | module.exports = draw;
32 |
--------------------------------------------------------------------------------
/source/draw/background.js:
--------------------------------------------------------------------------------
1 | module.exports = function(box, context) {
2 | var color = box.style['background-color'];
3 |
4 | if((!box.innerWidth() && !box.innerHeight()) || !color.alpha) return;
5 |
6 | var x = box.position.x - box.padding.left;
7 | var y = box.position.y - box.padding.top;
8 | var width = box.padding.left + box.dimensions.width + box.padding.right;
9 | var height = box.padding.top + box.dimensions.height + box.padding.bottom;
10 |
11 | context.fillStyle = color.toString();
12 | context.fillRect(x, y, width, height);
13 | };
14 |
--------------------------------------------------------------------------------
/source/draw/border.js:
--------------------------------------------------------------------------------
1 | var drawBorderTop = function(box, context) {
2 | if(!box.border.top) return;
3 |
4 | var x = box.position.x - box.padding.left - box.border.left;
5 | var y = box.position.y - box.padding.top - box.border.top;
6 | var width = box.border.left + box.padding.left + box.dimensions.width + box.padding.right + box.border.right;
7 | var height = box.border.top;
8 |
9 | context.fillStyle = box.style['border-top-color'].toString();
10 | context.fillRect(x, y, width, height);
11 | };
12 |
13 | var drawBorderRight = function(box, context) {
14 | if(!box.border.right) return;
15 |
16 | var x = box.position.x + box.dimensions.width + box.padding.right;
17 | var y = box.position.y - box.padding.top - box.border.top;
18 | var width = box.border.right;
19 | var height = box.border.top + box.padding.top + box.dimensions.height + box.padding.bottom + box.border.bottom;
20 |
21 | context.fillStyle = box.style['border-right-color'].toString();
22 | context.fillRect(x, y, width, height);
23 | };
24 |
25 | var drawBorderBottom = function(box, context) {
26 | if(!box.border.bottom) return;
27 |
28 | var x = box.position.x - box.padding.left - box.border.left;
29 | var y = box.position.y + box.dimensions.height + box.padding.bottom;
30 | var width = box.border.left + box.padding.left + box.dimensions.width + box.padding.right + box.border.right;
31 | var height = box.border.bottom;
32 |
33 | context.fillStyle = box.style['border-bottom-color'].toString();
34 | context.fillRect(x, y, width, height);
35 | };
36 |
37 | var drawBorderLeft = function(box, context) {
38 | if(!box.border.left) return;
39 |
40 | var x = box.position.x - box.padding.left - box.border.left;
41 | var y = box.position.y - box.padding.top - box.border.top;
42 | var width = box.border.left;
43 | var height = box.border.top + box.padding.top + box.dimensions.height + box.padding.bottom + box.border.bottom;
44 |
45 | context.fillStyle = box.style['border-left-color'].toString();
46 | context.fillRect(x, y, width, height);
47 | };
48 |
49 | module.exports = function(box, context) {
50 | drawBorderTop(box, context);
51 | drawBorderRight(box, context);
52 | drawBorderBottom(box, context);
53 | drawBorderLeft(box, context);
54 | };
55 |
--------------------------------------------------------------------------------
/source/draw/image.js:
--------------------------------------------------------------------------------
1 | module.exports = function(box, context) {
2 | if(!box.image.complete) return;
3 |
4 | context.drawImage(box.image.data,
5 | box.position.x,
6 | box.position.y,
7 | box.dimensions.width,
8 | box.dimensions.height);
9 | };
10 |
--------------------------------------------------------------------------------
/source/draw/text.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 |
3 | module.exports = function(box, context) {
4 | var style = box.style;
5 |
6 | context.font = util.format('%s %s %s %s',
7 | style['font-style'],
8 | style['font-weight'],
9 | style['font-size'],
10 | style['font-family']);
11 | context.textBaseline = 'bottom';
12 | context.fillStyle = style['color'].toString();
13 | context.fillText(box.display, box.position.x, box.position.y + box.dimensions.height);
14 | };
15 |
--------------------------------------------------------------------------------
/source/html.js:
--------------------------------------------------------------------------------
1 | var htmlparser = require('htmlparser2');
2 | var ElementType = require('domelementtype');
3 | var DomHandler = require('domhandler').DomHandler;
4 |
5 | module.exports = function(html, callback) {
6 | var stylesheets = [];
7 | var scripts = [];
8 | var images = [];
9 | var anchors = [];
10 | var title = null;
11 |
12 | var ondone = function(err, html) {
13 | if(err) return callback(err);
14 | callback(null, {
15 | html: html,
16 | stylesheets: stylesheets,
17 | scripts: scripts,
18 | images: images,
19 | anchors: anchors,
20 | title: title
21 | });
22 | };
23 |
24 | var handler = new DomHandler(ondone, {
25 | withDomLvl1: true,
26 | normalizeWhitespace: false
27 | }, function(element) {
28 | if(element.type === ElementType.Script) scripts.push(element);
29 | if(element.type === ElementType.Style) stylesheets.push(element);
30 | if(element.type === ElementType.Tag && element.name === 'link' && element.attribs.rel === 'stylesheet') stylesheets.push(element);
31 | if(element.type === ElementType.Tag && element.name === 'img') images.push(element);
32 | if(element.type === ElementType.Tag && element.name === 'a') anchors.push(element);
33 | if(element.type === ElementType.Tag && element.name === 'title') title = element;
34 | });
35 |
36 | var parser = new htmlparser.Parser(handler);
37 |
38 | parser.write(html);
39 | parser.done();
40 | };
41 |
--------------------------------------------------------------------------------
/source/images.js:
--------------------------------------------------------------------------------
1 | var url = require('url');
2 | var afterAll = require('after-all');
3 | var he = require('he');
4 |
5 | var empty = function(src) {
6 | return {
7 | width: 0,
8 | height: 0,
9 | src: src,
10 | complete: false,
11 | data: null
12 | };
13 | };
14 |
15 | module.exports = function(base, nodes, callback) {
16 | if(!nodes.length) return callback(null, []);
17 |
18 | var images = new Array(nodes.length);
19 | var next = afterAll(function(err) {
20 | callback(err, images);
21 | });
22 |
23 | nodes.forEach(function(node, i) {
24 | var src = node.attribs.src;
25 | var cb = next(function(err, image) {
26 | image = image || empty(src);
27 |
28 | images[i] = node.image = {
29 | width: image.width,
30 | height: image.height,
31 | src: image.src,
32 | complete: image.complete,
33 | data: image
34 | };
35 | });
36 |
37 | if(!src) return cb();
38 |
39 | src = he.decode(src);
40 | src = url.resolve(base, src);
41 |
42 | var image = new Image();
43 | image.onload = function() {
44 | cb(null, image);
45 | };
46 | image.onerror = function() {
47 | cb();
48 | };
49 |
50 | image.src = src;
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/source/index.js:
--------------------------------------------------------------------------------
1 | var events = require('events');
2 | var afterAll = require('after-all');
3 | var extend = require('xtend/mutable');
4 |
5 | var html = require('./html');
6 | var stylesheets = require('./stylesheets');
7 | var css = require('./css');
8 | var images = require('./images');
9 | var layout = require('./layout');
10 | var draw = require('./draw');
11 | var Stylesheet = require('./css/stylesheet');
12 |
13 | var noop = function() {};
14 |
15 | var clone = function(obj) {
16 | return JSON.parse(JSON.stringify(obj));
17 | };
18 |
19 | var defaultStylesheets = function(stylesheets) {
20 | return (stylesheets || []).map(function(sheet, i) {
21 | return Stylesheet.parse(sheet, i - stylesheets.length);
22 | });
23 | };
24 |
25 | module.exports = function(options, callback) {
26 | callback = callback || noop;
27 |
28 | var page = new events.EventEmitter();
29 | var emit = page.emit.bind(page);
30 | var ondone = function(err) {
31 | if(err) {
32 | emit('fail', err);
33 | return callback(err);
34 | }
35 |
36 | emit('ready', page);
37 | callback(null, page);
38 | };
39 |
40 | extend(page, options, {
41 | stylesheets: null,
42 | viewport: clone(options.viewport)
43 | });
44 |
45 | var position = options.viewport.position;
46 | options.viewport.position = extend({ x: 0, y: 0 }, position);
47 |
48 | html(options.content, function(err, document) {
49 | if(err) return ondone(err);
50 |
51 | page.document = document;
52 | emit('html', document);
53 |
54 | var next = afterAll(function(err) {
55 | if(err) return ondone(err);
56 |
57 | var tree = layout(document.html, options.viewport);
58 | page.layout = tree;
59 | emit('layout', tree);
60 |
61 | draw(tree, options.context);
62 | emit('draw');
63 |
64 | ondone();
65 | });
66 |
67 | images(options.url, document.images, next(function(err, images) {
68 | page.images = images;
69 | emit('images', images);
70 | }));
71 |
72 | stylesheets(options.url, document.stylesheets, next(function(err, stylesheets) {
73 | stylesheets = defaultStylesheets(options.stylesheets).concat(stylesheets);
74 | css(stylesheets, document.html);
75 | page.stylesheets = stylesheets;
76 | emit('stylesheets', stylesheets);
77 | }));
78 | });
79 |
80 | return page;
81 | };
82 |
--------------------------------------------------------------------------------
/source/layout.js:
--------------------------------------------------------------------------------
1 | var ElementType = require('domelementtype');
2 |
3 | var values = require('./css/values');
4 | var Viewport = require('./layout/viewport');
5 | var BlockBox = require('./layout/block-box');
6 | var LineBox = require('./layout/line-box');
7 | var LineBreakBox = require('./layout/line-break-box');
8 | var InlineBox = require('./layout/inline-box');
9 | var TextBox = require('./layout/text-box');
10 | var ImageBox = require('./layout/image-box');
11 |
12 | var None = values.Keyword.None;
13 | var Block = values.Keyword.Block;
14 | var Inline = values.Keyword.Inline;
15 | var LineBreak = values.Keyword.LineBreak;
16 |
17 | var isInlineLevelBox = function(box) {
18 | return box instanceof InlineBox ||
19 | box instanceof TextBox ||
20 | box instanceof LineBreakBox ||
21 | box instanceof ImageBox.Inline;
22 | };
23 |
24 | var isInlineContainerBox = function(box) {
25 | return box instanceof InlineBox ||
26 | box instanceof LineBox;
27 | };
28 |
29 | var isBlockLevelBox = function(box) {
30 | return box instanceof Viewport ||
31 | box instanceof LineBox ||
32 | box instanceof BlockBox ||
33 | box instanceof ImageBox.Block;
34 | };
35 |
36 | var isBlockContainerBox = function(box) {
37 | return box instanceof Viewport ||
38 | box instanceof BlockBox;
39 | };
40 |
41 | var branch = function(ancestor, descedant) {
42 | var first, current;
43 |
44 | while(descedant !== ancestor) {
45 | var d = descedant.clone();
46 | descedant.addLink(d);
47 |
48 | if(current) d.attach(current);
49 | if(!first) first = d;
50 |
51 | current = d;
52 |
53 | if(!descedant.parent) throw new Error('No ancestor match');
54 | descedant = descedant.parent;
55 | }
56 |
57 | if(current) ancestor.attach(current);
58 | return first;
59 | };
60 |
61 | var build = function(parent, nodes) {
62 | nodes.forEach(function(node) {
63 | var box;
64 |
65 | if(ElementType.isTag(node)) {
66 | var style = node.style;
67 | var display = style.display;
68 |
69 | if(None.is(display)) {
70 | return;
71 | } else if(node.name === 'img') {
72 | var image = node.image;
73 |
74 | if(Block.is(display)) box = new ImageBox.Block(parent, style, image);
75 | else box = new ImageBox.Inline(parent, style, image);
76 | } else if(Inline.is(display)) {
77 | box = new InlineBox(parent, style);
78 | } else if(Block.is(display)) {
79 | box = new BlockBox(parent, style);
80 | } else if(LineBreak.is(display)) {
81 | box = new LineBreakBox(parent, style);
82 | }
83 |
84 | build(box, node.childNodes);
85 | } else if(node.type === ElementType.Text) {
86 | box = new TextBox(parent, node.data);
87 | }
88 |
89 | if(box) parent.children.push(box);
90 | });
91 | };
92 |
93 | var blocks = function(parent, boxes, ancestor) {
94 | ancestor = ancestor || parent;
95 |
96 | var isInline = isInlineContainerBox(parent);
97 | var resume;
98 |
99 | boxes.forEach(function(child) {
100 | var isBlock = isBlockLevelBox(child);
101 | var box;
102 |
103 | if(isInline && isBlock) {
104 | box = child.clone(ancestor);
105 | parent = branch(ancestor, parent);
106 | resume = parent.parent;
107 | } else {
108 | box = child.cloneWithLinks(parent);
109 | }
110 |
111 | if(child.children) {
112 | var a = isBlockContainerBox(box) ? box : ancestor;
113 | parent = blocks(box, child.children, a) || parent;
114 | }
115 | });
116 |
117 | return resume;
118 | };
119 |
120 | var lines = function(parent, boxes) {
121 | var isBlock = isBlockContainerBox(parent);
122 | var line;
123 |
124 | boxes.forEach(function(child) {
125 | var isInline = isInlineLevelBox(child);
126 | var box;
127 |
128 | if(isBlock && isInline) {
129 | if(!line) {
130 | line = new LineBox(parent);
131 | parent.children.push(line);
132 | }
133 |
134 | box = child.cloneWithLinks(line);
135 | } else {
136 | line = null;
137 | box = child.cloneWithLinks(parent);
138 | }
139 |
140 | if(child.children) lines(box, child.children);
141 | });
142 | };
143 |
144 | module.exports = function(html, viewport) {
145 | viewport = new Viewport(viewport.position, viewport.dimensions);
146 |
147 | build(viewport, html);
148 |
149 | viewport = [
150 | blocks,
151 | lines
152 | ].reduce(function(acc, fn) {
153 | var a = acc.clone();
154 | fn(a, acc.children);
155 | return a;
156 | }, viewport);
157 |
158 | viewport.layout();
159 |
160 | return viewport;
161 | };
162 |
--------------------------------------------------------------------------------
/source/layout/block-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 |
3 | var ParentBox = require('./parent-box');
4 | var values = require('../css/values');
5 |
6 | var Auto = values.Keyword.Auto;
7 | var Percentage = values.Percentage;
8 | var Length = values.Length;
9 |
10 | var auto = function(value) {
11 | return Auto.is(value);
12 | };
13 |
14 | var BlockBox = function(parent, style) {
15 | ParentBox.call(this, parent, style);
16 | };
17 |
18 | util.inherits(BlockBox, ParentBox);
19 |
20 | BlockBox.prototype.addLine = function(child, branch) {
21 | var children = this.children;
22 | var i = children.indexOf(child);
23 |
24 | this.attach(branch, i + 1);
25 | };
26 |
27 | BlockBox.prototype.layout = function(offset) {
28 | this._layoutWidth();
29 | this._layoutPosition(offset);
30 | this._layoutChildren();
31 | this._layoutHeight();
32 | };
33 |
34 | BlockBox.prototype._layoutWidth = function() {
35 | var self = this;
36 | var parent = this.parent;
37 | var style = this.style;
38 |
39 | var width = style.width;
40 |
41 | var marginLeft = style['margin-left'];
42 | var marginRight = style['margin-right'];
43 |
44 | var borderLeft = this.styledBorderWidth('left');
45 | var borderRight = this.styledBorderWidth('right');
46 |
47 | var paddingLeft = style['padding-left'];
48 | var paddingRight = style['padding-right'];
49 |
50 | var total = [
51 | width,
52 | marginLeft,
53 | marginRight,
54 | borderLeft,
55 | borderRight,
56 | paddingLeft,
57 | paddingRight
58 | ].reduce(function(acc, v) {
59 | return acc + self.toPx(v);
60 | }, 0);
61 |
62 | var underflow = parent.dimensions.width - total;
63 |
64 | if(!auto(width) && underflow < 0) {
65 | if(auto(marginLeft)) marginLeft = Length.px(0);
66 | if(auto(marginRight)) marginRight = Length.px(0);
67 | }
68 |
69 | var isWidthAuto = auto(width);
70 | var isMarginLeftAuto = auto(marginLeft);
71 | var isMarginRightAuto = auto(marginRight);
72 |
73 | if(!isWidthAuto && !isMarginLeftAuto && !isMarginRightAuto) {
74 | var margin = this.toPx(marginRight);
75 | marginRight = Length.px(margin + underflow);
76 | } else if(!isWidthAuto && !isMarginLeftAuto && isMarginRightAuto) {
77 | marginRight = Length.px(underflow);
78 | } else if(!isWidthAuto && isMarginLeftAuto && !isMarginRightAuto) {
79 | marginLeft = Length.px(underflow);
80 | } else if(isWidthAuto && !isMarginLeftAuto && !isMarginRightAuto) {
81 | if(isMarginLeftAuto) marginLeft = Length.px(0);
82 | if(isMarginRightAuto) marginRight = Length.px(0);
83 |
84 | if(underflow >= 0) {
85 | width = Length.px(underflow);
86 | } else {
87 | var margin = this.toPx(marginRight);
88 |
89 | width = Length.px(0);
90 | marginRight = Length.px(margin + underflow);
91 | }
92 | } else if(!isWidthAuto && isMarginLeftAuto && isMarginRightAuto) {
93 | marginLeft = Length.px(underflow / 2);
94 | marginRight = Length.px(underflow / 2);
95 | }
96 |
97 | this.dimensions.width = this.toPx(width);
98 |
99 | this.margin.left = this.toPx(marginLeft);
100 | this.margin.right = this.toPx(marginRight);
101 |
102 | this.border.left = this.toPx(borderLeft);
103 | this.border.right = this.toPx(borderRight);
104 |
105 | this.padding.left = this.toPx(paddingLeft);
106 | this.padding.right = this.toPx(paddingRight);
107 | };
108 |
109 | BlockBox.prototype._layoutPosition = function(offset) {
110 | var parent = this.parent;
111 | var style = this.style;
112 |
113 | this.margin.top = this.toPx(style['margin-top']);
114 | this.margin.bottom = this.toPx(style['margin-bottom']);
115 |
116 | this.border.top = this.toPx(this.styledBorderWidth('top'));
117 | this.border.bottom = this.toPx(this.styledBorderWidth('bottom'));
118 |
119 | this.padding.top = this.toPx(style['padding-top']);
120 | this.padding.bottom = this.toPx(style['padding-bottom']);
121 |
122 | this.position.x = parent.position.x + this.leftWidth();
123 | this.position.y = parent.position.y + offset.height + this.topWidth();
124 | };
125 |
126 | BlockBox.prototype._layoutChildren = function() {
127 | var offset = { width: 0, height: 0 };
128 |
129 | this.forEach(function(child) {
130 | child.layout(offset);
131 | offset.height += child.height();
132 | });
133 |
134 | this.dimensions.height = offset.height;
135 | };
136 |
137 | BlockBox.prototype._layoutHeight = function() {
138 | var parent = this.parent;
139 | var height = this.style.height;
140 | var parentHeight = parent.style.height;
141 |
142 | if(Length.is(height)) {
143 | this.dimensions.height = height.length;
144 | } else if(Percentage.is(height) && Length.is(parentHeight)) {
145 | this.dimensions.height = parentHeight.length * height.percentage / 100;
146 | }
147 | };
148 |
149 | module.exports = BlockBox;
150 |
--------------------------------------------------------------------------------
/source/layout/box.js:
--------------------------------------------------------------------------------
1 | var values = require('../css/values');
2 |
3 | var None = values.Keyword.None;
4 | var Length = values.Length;
5 |
6 | var Widths = function() {
7 | this.top = 0;
8 | this.right = 0;
9 | this.bottom = 0;
10 | this.left = 0;
11 | };
12 |
13 | Widths.prototype.some = function() {
14 | return [
15 | this.top,
16 | this.right,
17 | this.bottom,
18 | this.left
19 | ].some(function(v) {
20 | return v !== 0;
21 | });
22 | };
23 |
24 | Widths.prototype.reset = function() {
25 | this.top =
26 | this.right =
27 | this.bottom =
28 | this.left = 0;
29 | };
30 |
31 | var Box = function(style) {
32 | this.style = style;
33 |
34 | this.position = { x: 0, y: 0 };
35 | this.dimensions = { width: 0, height: 0 };
36 |
37 | this.margin = new Widths();
38 | this.border = new Widths();
39 | this.padding = new Widths();
40 | };
41 |
42 | Box.prototype.topWidth = function() {
43 | return this.margin.top + this.border.top + this.padding.top;
44 | };
45 |
46 | Box.prototype.rightWidth = function() {
47 | return this.margin.right + this.border.right + this.padding.right;
48 | };
49 |
50 | Box.prototype.bottomWidth = function() {
51 | return this.margin.bottom + this.border.bottom + this.padding.bottom;
52 | };
53 |
54 | Box.prototype.leftWidth = function() {
55 | return this.margin.left + this.border.left + this.padding.left;
56 | };
57 |
58 | Box.prototype.innerWidth = function() {
59 | return this.padding.left + this.dimensions.width + this.padding.right;
60 | };
61 |
62 | Box.prototype.innerHeight = function() {
63 | return this.padding.top + this.dimensions.height + this.padding.bottom;
64 | };
65 |
66 | Box.prototype.outerWidth = function() {
67 | return this.border.left + this.innerWidth() + this.border.right;
68 | };
69 |
70 | Box.prototype.outerHeight = function() {
71 | return this.border.top + this.innerHeight() + this.border.bottom;
72 | };
73 |
74 | Box.prototype.width = function() {
75 | return this.margin.left + this.outerWidth() + this.margin.right;
76 | };
77 |
78 | Box.prototype.height = function() {
79 | return this.margin.top + this.outerHeight() + this.margin.bottom;
80 | };
81 |
82 | Box.prototype.translate = function(dx, dy) {
83 | this.position.x += dx;
84 | this.position.y += dy;
85 | };
86 |
87 | Box.prototype.styledBorderWidth = function(direction) {
88 | var borderWidth = this.style['border-' + direction + '-width'];
89 | var borderStyle = this.style['border-' + direction + '-style'];
90 |
91 | return None.is(borderStyle) ? Length.px(0) : borderWidth;
92 | };
93 |
94 | module.exports = Box;
95 |
--------------------------------------------------------------------------------
/source/layout/image-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 |
3 | var Box = require('./box');
4 | var ParentBox = require('./parent-box');
5 | var values = require('../css/values');
6 |
7 | var Auto = values.Keyword.Auto;
8 | var Length = values.Length;
9 | var Percentage = values.Percentage;
10 |
11 | var ImageBox = function(parent, style, image) {
12 | Box.call(this, style);
13 |
14 | this.parent = parent;
15 | this.image = image;
16 | };
17 |
18 | util.inherits(ImageBox, Box);
19 |
20 | ImageBox.prototype.layout = function() {
21 | var style = this.style;
22 | var image = this.image;
23 | var width = style.width;
24 | var height = style.height;
25 | var ratio = image.width / image.height;
26 |
27 | var isWidthAuto = Auto.is(width);
28 | var isHeightAuto = Auto.is(height);
29 |
30 | if(isWidthAuto && isHeightAuto) {
31 | width = Length.px(image.width);
32 | height = Length.px(image.height);
33 | } else if(isWidthAuto) {
34 | width = Length.px(this.toPx(height) * ratio);
35 | } else if(isHeightAuto) {
36 | height = Length.px(this.toPx(width) / ratio);
37 | }
38 |
39 | this.dimensions.width = this.toPx(width);
40 | this.dimensions.height = this.toPx(height);
41 |
42 | this.margin.top = this.toPx(style['margin-top']);
43 | this.margin.bottom = this.toPx(style['margin-bottom']);
44 |
45 | this.border.left = this.toPx(this.styledBorderWidth('left'));
46 | this.border.right = this.toPx(this.styledBorderWidth('right'));
47 | this.border.top = this.toPx(this.styledBorderWidth('top'));
48 | this.border.bottom = this.toPx(this.styledBorderWidth('bottom'));
49 |
50 | this.padding.left = this.toPx(style['padding-left']);
51 | this.padding.right = this.toPx(style['padding-right']);
52 | this.padding.top = this.toPx(style['padding-top']);
53 | this.padding.bottom = this.toPx(style['padding-bottom']);
54 | };
55 |
56 | ImageBox.prototype.collapseWhitespace = function() {
57 | return false;
58 | };
59 |
60 | ImageBox.prototype.hasContent = function() {
61 | return true;
62 | };
63 |
64 | ImageBox.prototype.clone = function(parent) {
65 | var clone = new this.constructor(parent, this.style, this.image);
66 | if(parent) parent.children.push(clone);
67 |
68 | return clone;
69 | };
70 |
71 | ImageBox.prototype.cloneWithLinks = ParentBox.prototype.cloneWithLinks;
72 | ImageBox.prototype.addLink = ParentBox.prototype.addLink;
73 | ImageBox.prototype.toPx = ParentBox.prototype.toPx;
74 |
75 | var InlineImageBox = function(parent, style, image) {
76 | ImageBox.call(this, parent, style, image);
77 | this.baseline = 0;
78 | };
79 |
80 | util.inherits(InlineImageBox, ImageBox);
81 |
82 | InlineImageBox.prototype.layout = function(offset, line) {
83 | ImageBox.prototype.layout.call(this);
84 |
85 | var style = this.style;
86 |
87 | this.margin.left = this.toPx(style['margin-left']);
88 | this.margin.right = this.toPx(style['margin-right']);
89 |
90 | var parent = this.parent;
91 | var x = parent.position.x + offset.width + this.leftWidth();
92 | var available = line.position.x + line.dimensions.width - x;
93 |
94 | if(this.width() > available && !this._isFirst(line)) {
95 | this._reset();
96 | return parent.addLine(this);
97 | }
98 |
99 | this._layoutBaseline();
100 |
101 | this.position.x = x;
102 | this.position.y = this.baseline - this.dimensions.height - this.bottomWidth();
103 | };
104 |
105 | InlineImageBox.prototype.linePosition = function() {
106 | return {
107 | x: this.position.x - this.leftWidth(),
108 | y: this.position.y - this.topWidth()
109 | };
110 | };
111 |
112 | InlineImageBox.prototype.lineHeight = function() {
113 | return this.height();
114 | };
115 |
116 | InlineImageBox.prototype._layoutBaseline = function() {
117 | var parent = this.parent;
118 | var style = this.style;
119 | var alignment = this.style['vertical-align'];
120 |
121 | if(Length.is(alignment)) {
122 | this.baseline = parent.baseline - alignment.length;
123 | } else if(Percentage.is(alignment)) {
124 | var size = this.toPx(style['font-size']);
125 | var lineHeight = style['line-height'];
126 |
127 | var lh = values.Number.is(lineHeight) ?
128 | lineHeight.number * size : this.toPx(lineHeight);
129 |
130 | this.baseline = parent.baseline - (alignment.percentage * lh / 100);
131 | } else {
132 | this.baseline = parent.baseline;
133 | }
134 | };
135 |
136 | InlineImageBox.prototype._reset = function() {
137 | this.padding.reset();
138 | this.border.reset();
139 | this.margin.reset();
140 |
141 | this.baseline = 0;
142 | this.dimensions.width = 0;
143 | this.dimensions.height = 0;
144 | };
145 |
146 | InlineImageBox.prototype._isFirst = function(line) {
147 | return line.contents().indexOf(this) === 0;
148 | };
149 |
150 | var BlockImageBox = function(parent, style, image) {
151 | ImageBox.call(this, parent, style, image);
152 | };
153 |
154 | util.inherits(BlockImageBox, ImageBox);
155 |
156 | BlockImageBox.prototype.layout = function(offset) {
157 | ImageBox.prototype.layout.call(this);
158 |
159 | this._layoutWidth();
160 | this._layoutPosition(offset);
161 | };
162 |
163 | BlockImageBox.prototype._layoutWidth = function() {
164 | var self = this;
165 | var style = this.style;
166 | var parent = this.parent;
167 |
168 | var marginLeft = style['margin-left'];
169 | var marginRight = style['margin-right'];
170 |
171 | var total = [
172 | this.dimensions.width,
173 | this.padding.left,
174 | this.padding.right,
175 | this.border.left,
176 | this.border.right,
177 | marginLeft,
178 | marginRight
179 | ].reduce(function(acc, v) {
180 | return acc + (typeof v === 'number' ? v : self.toPx(v));
181 | }, 0);
182 |
183 | var underflow = parent.dimensions.width - total;
184 |
185 | if(underflow < 0) {
186 | if(Auto.is(marginLeft)) marginLeft = Length.px(0);
187 | if(Auto.is(marginRight)) marginRight = Length.px(0);
188 | }
189 |
190 | var isMarginLeftAuto = Auto.is(marginLeft);
191 | var isMarginRightAuto = Auto.is(marginRight);
192 |
193 | if(!isMarginLeftAuto && !isMarginRightAuto) {
194 | var margin = this.toPx(marginRight);
195 | marginRight = Length.px(margin + underflow);
196 | } else if(!isMarginLeftAuto && isMarginRightAuto) {
197 | marginRight = Length.px(underflow);
198 | } else if(isMarginLeftAuto && !isMarginRightAuto) {
199 | marginLeft = Length.px(underflow);
200 | } else {
201 | marginLeft = Length.px(underflow / 2);
202 | marginRight = Length.px(underflow / 2);
203 | }
204 |
205 | this.margin.left = this.toPx(marginLeft);
206 | this.margin.right = this.toPx(marginRight);
207 | };
208 |
209 | BlockImageBox.prototype._layoutPosition = function(offset) {
210 | var parent = this.parent;
211 |
212 | this.position.x = parent.position.x + this.leftWidth();
213 | this.position.y = parent.position.y + offset.height + this.topWidth();
214 | };
215 |
216 | ImageBox.Inline = InlineImageBox;
217 | ImageBox.Block = BlockImageBox;
218 |
219 | module.exports = ImageBox;
220 |
--------------------------------------------------------------------------------
/source/layout/inline-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 | var textHeight = require('text-height');
3 |
4 | var ParentBox = require('./parent-box');
5 | var values = require('../css/values');
6 |
7 | var Length = values.Length;
8 | var Percentage = values.Percentage;
9 |
10 | var InlineBox = function(parent, style) {
11 | ParentBox.call(this, parent, style);
12 | this.baseline = 0;
13 | };
14 |
15 | util.inherits(InlineBox, ParentBox);
16 |
17 | InlineBox.prototype.layout = function(offset, line) {
18 | this._layoutWidth();
19 | this._layoutBaseline();
20 | this._layoutPosition(offset);
21 | this._layoutHeight();
22 | this._layoutChildren(line);
23 | this._layoutWidth();
24 | };
25 |
26 | InlineBox.prototype.linePosition = function() {
27 | var lineHeight = this.lineHeight();
28 | var size = this.toPx(this.style['font-size']);
29 | var leading = (lineHeight - size) / 2;
30 |
31 | return {
32 | x: this.position.x,
33 | y: this.position.y - leading
34 | };
35 | };
36 |
37 | InlineBox.prototype.lineHeight = function() {
38 | var style = this.style;
39 | var size = this.toPx(style['font-size']);
40 | var lineHeight = style['line-height'];
41 |
42 | return values.Number.is(lineHeight) ?
43 | lineHeight.number * size : this.toPx(lineHeight);
44 | };
45 |
46 | InlineBox.prototype._layoutWidth = function() {
47 | var self = this;
48 | var style = this.style;
49 |
50 | var iif = function(direction, value) {
51 | return self[direction + 'Link'] ? 0 : self.toPx(value);
52 | };
53 |
54 | this.margin.left = iif('left', style['margin-left']);
55 | this.border.left = iif('left', this.styledBorderWidth('left'));
56 | this.padding.left = iif('left', style['padding-left']);
57 |
58 | this.margin.right = iif('right', style['margin-right']);
59 | this.border.right = iif('right', this.styledBorderWidth('right'));
60 | this.padding.right = iif('right', style['padding-right']);
61 | };
62 |
63 | InlineBox.prototype._layoutPosition = function(offset) {
64 | var parent = this.parent;
65 | var style = this.style;
66 | var size = this.toPx(style['font-size']);
67 |
68 | this.border.top = this.toPx(this.styledBorderWidth('top'));
69 | this.border.bottom = this.toPx(this.styledBorderWidth('bottom'));
70 |
71 | this.padding.top = this.toPx(style['padding-top']);
72 | this.padding.bottom = this.toPx(style['padding-bottom']);
73 |
74 | this.position.x = parent.position.x + offset.width + this.leftWidth();
75 | this.position.y = this.baseline - size + this._textHeight().descent;
76 | };
77 |
78 | InlineBox.prototype._layoutChildren = function(line) {
79 | var offset = { width: 0, height: 0 };
80 |
81 | this.forEach(function(child) {
82 | child.layout(offset, line);
83 | offset.width += child.width();
84 | });
85 |
86 | this.dimensions.width = offset.width;
87 | };
88 |
89 | InlineBox.prototype._layoutHeight = function() {
90 | this.dimensions.height = this.toPx(this.style['font-size']);
91 | };
92 |
93 | InlineBox.prototype._layoutBaseline = function() {
94 | var parent = this.parent;
95 | var alignment = this.style['vertical-align'];
96 |
97 | if(Length.is(alignment)) {
98 | this.baseline = parent.baseline - alignment.length;
99 | } else if(Percentage.is(alignment)) {
100 | this.baseline = parent.baseline - (alignment.percentage * this.lineHeight() / 100);
101 | } else {
102 | this.baseline = parent.baseline;
103 | }
104 | };
105 |
106 | InlineBox.prototype._textHeight = function() {
107 | var style = this.style;
108 | return textHeight({
109 | size: style['font-size'].toString(),
110 | family: style['font-family'].toString(),
111 | weight: style['font-weight'].keyword,
112 | style: style['font-style'].keyword
113 | });
114 | };
115 |
116 | module.exports = InlineBox;
117 |
--------------------------------------------------------------------------------
/source/layout/line-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 | var textHeight = require('text-height');
3 |
4 | var ParentBox = require('./parent-box');
5 | var TextBox = require('./text-box');
6 | var ImageBox = require('./image-box');
7 | var LineBreakBox = require('./line-break-box');
8 | var values = require('../css/values');
9 |
10 | var LineBox = function(parent, style) {
11 | ParentBox.call(this, parent, style);
12 | this.baseline = 0;
13 | };
14 |
15 | util.inherits(LineBox, ParentBox);
16 |
17 | LineBox.prototype.flatten = function() {
18 | var descedants = [];
19 | var flatten = function(parent) {
20 | descedants.push(parent);
21 | if(parent.children) parent.children.forEach(flatten);
22 | };
23 |
24 | this.children.forEach(flatten);
25 | return descedants;
26 | };
27 |
28 | LineBox.prototype.contents = function(breaks) {
29 | return this.flatten().filter(function(box) {
30 | return box instanceof TextBox ||
31 | box instanceof ImageBox ||
32 | (breaks && box instanceof LineBreakBox);
33 | });
34 | };
35 |
36 | LineBox.prototype.addLine = function(child, branch) {
37 | ParentBox.prototype.addLine.call(this, child, branch, true);
38 | };
39 |
40 | LineBox.prototype.layout = function(offset) {
41 | var parent = this.parent;
42 |
43 | this.dimensions.width = parent.dimensions.width;
44 |
45 | this.position.x = parent.position.x;
46 | this.position.y = parent.position.y + offset.height;
47 |
48 | this._layoutStrut();
49 | this._layoutChildren();
50 | this._layoutHeight();
51 | };
52 |
53 | LineBox.prototype.collapseWhitespace = function() {
54 | return ParentBox.prototype.collapseWhitespace.call(this, false);
55 | };
56 |
57 | LineBox.prototype._layoutStrut = function() {
58 | var style = this.style;
59 | var size = this.toPx(style['font-size']);
60 | var height = textHeight({
61 | size: style['font-size'].toString(),
62 | family: style['font-family'].toString(),
63 | weight: style['font-weight'].keyword,
64 | style: style['font-style'].keyword
65 | });
66 |
67 | this.baseline = this.position.y + size - height.descent;
68 | };
69 |
70 | LineBox.prototype._layoutChildren = function() {
71 | var self = this;
72 | var offset = { width: 0, height: 0 };
73 |
74 | this.forEach(function(child) {
75 | child.layout(offset, self);
76 | offset.width += child.width();
77 | });
78 | };
79 |
80 | LineBox.prototype._layoutHeight = function() {
81 | if(!this.hasContent()) return;
82 |
83 | var minY = this._linePosition();
84 | var maxY = minY + this._lineHeight();
85 |
86 | var height = function(parent) {
87 | var position = parent.linePosition();
88 |
89 | minY = Math.min(minY, position.y);
90 | maxY = Math.max(maxY, position.y + parent.lineHeight());
91 | if(parent.children) parent.children.forEach(height);
92 | };
93 |
94 | this.children.forEach(height);
95 | this.dimensions.height = maxY - minY;
96 | this.translateChildren(0, this.position.y - minY);
97 | };
98 |
99 | LineBox.prototype._linePosition = function() {
100 | var lineHeight = this._lineHeight();
101 | var size = this.toPx(this.style['font-size']);
102 | var leading = (lineHeight - size) / 2;
103 |
104 | return this.position.y - leading;
105 | };
106 |
107 | LineBox.prototype._lineHeight = function() {
108 | var style = this.style;
109 | var size = this.toPx(style['font-size']);
110 | var lineHeight = style['line-height'];
111 |
112 | return values.Number.is(lineHeight) ?
113 | lineHeight.number * size : this.toPx(lineHeight);
114 | };
115 |
116 | module.exports = LineBox;
117 |
--------------------------------------------------------------------------------
/source/layout/line-break-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 |
3 | var Box = require('./box');
4 | var ParentBox = require('./parent-box');
5 |
6 | var LineBreakBox = function(parent, style) {
7 | Box.call(this, style);
8 | this.parent = parent;
9 |
10 | this.leftLink = false;
11 | this.rightLink = false;
12 | };
13 |
14 | util.inherits(LineBreakBox, Box);
15 |
16 | LineBreakBox.prototype.layout = function(offset, line) {
17 | var parent = this.parent;
18 |
19 | this.position.x = parent.position.x + offset.width;
20 | this.position.y = parent.position.y;
21 |
22 | parent.breakLine(this);
23 | };
24 |
25 | LineBreakBox.prototype.collapseWhitespace = function() {
26 | return false;
27 | };
28 |
29 | LineBreakBox.prototype.hasContent = function() {
30 | return true;
31 | };
32 |
33 | LineBreakBox.prototype.linePosition = function() {
34 | return this.position;
35 | };
36 |
37 | LineBreakBox.prototype.lineHeight = function() {
38 | return 0;
39 | };
40 |
41 | LineBreakBox.prototype.isPx = ParentBox.prototype.isPx;
42 | LineBreakBox.prototype.clone = ParentBox.prototype.clone;
43 | LineBreakBox.prototype.cloneWithLinks = ParentBox.prototype.cloneWithLinks;
44 |
45 | module.exports = LineBreakBox;
46 |
--------------------------------------------------------------------------------
/source/layout/parent-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 |
3 | var Box = require('./box');
4 | var compute = require('../css/compute');
5 | var values = require('../css/values');
6 |
7 | var Auto = values.Keyword.Auto;
8 | var Percentage = values.Percentage;
9 |
10 | var ParentBox = function(parent, style) {
11 | Box.call(this, style);
12 |
13 | this.style = style || compute({}, parent.style);
14 | this.parent = parent;
15 | this.children = [];
16 |
17 | this.leftLink = false;
18 | this.rightLink = false;
19 | };
20 |
21 | util.inherits(ParentBox, Box);
22 |
23 | ParentBox.prototype.layout = function() {};
24 |
25 | ParentBox.prototype.addLink = function(box) {
26 | box.leftLink = true;
27 | box.rightLink = this.rightLink;
28 |
29 | this.rightLink = true;
30 | };
31 |
32 | ParentBox.prototype.addLine = function(child, branch, force) {
33 | this.stopEach();
34 |
35 | var parent = this.parent;
36 | var i = this.children.indexOf(child);
37 | if(i === 0 && !branch && !force) return parent.addLine(this);
38 |
39 | var children = this.children.slice();
40 | var box = this.clone();
41 |
42 | if(branch) box.attach(branch);
43 | else box.attach(child);
44 |
45 | for(var j = i + 1; j < children.length; j++) {
46 | box.attach(children[j]);
47 | }
48 |
49 | this.addLink(box);
50 | parent.addLine(this, box);
51 | };
52 |
53 | ParentBox.prototype.breakLine = function(child) {
54 | var children = this.children.slice();
55 | var box = this.clone();
56 | var i = children.indexOf(child);
57 |
58 | for(var j = i + 1; j < children.length; j++) {
59 | box.attach(children[j]);
60 | }
61 |
62 | this.addLink(box);
63 | this.parent.addLine(this, box);
64 | };
65 |
66 | ParentBox.prototype.hasContent = function() {
67 | var hasOutline = this.padding.some() ||
68 | this.border.some() ||
69 | this.margin.some();
70 |
71 | return hasOutline || this.children.some(function(child) {
72 | return child.hasContent();
73 | });
74 | };
75 |
76 | ParentBox.prototype.collapseWhitespace = function(strip) {
77 | this.children.forEach(function(child) {
78 | strip = child.collapseWhitespace(strip);
79 | });
80 |
81 | return strip;
82 | };
83 |
84 | ParentBox.prototype.attach = function(node, i) {
85 | if(node.parent) node.parent.detach(node);
86 |
87 | node.parent = this;
88 |
89 | if(i !== undefined) this.children.splice(i, 0, node);
90 | else this.children.push(node);
91 | };
92 |
93 | ParentBox.prototype.detach = function(node) {
94 | var children = this.children;
95 | var i = children.indexOf(node);
96 |
97 | if(i < 0) return;
98 |
99 | node.parent = null;
100 | children.splice(i, 1);
101 | };
102 |
103 | ParentBox.prototype.clone = function(parent) {
104 | var clone = new this.constructor(parent, this.style);
105 | if(parent) parent.children.push(clone);
106 |
107 | return clone;
108 | };
109 |
110 | ParentBox.prototype.cloneWithLinks = function(parent) {
111 | var clone = this.clone(parent);
112 | clone.leftLink = this.leftLink;
113 | clone.rightLink = this.rightLink;
114 |
115 | return clone;
116 | };
117 |
118 | ParentBox.prototype.forEach = function(fn) {
119 | var children = this.children;
120 | var stop = false;
121 |
122 | this._stop = function() {
123 | stop = true;
124 | };
125 |
126 | for(var i = 0; i < children.length && !stop; i++) {
127 | fn(children[i], i);
128 | }
129 | };
130 |
131 | ParentBox.prototype.stopEach = function() {
132 | if(this._stop) this._stop();
133 | };
134 |
135 | ParentBox.prototype.translate = function(dx, dy) {
136 | Box.prototype.translate.call(this, dx, dy);
137 | this.translateChildren(dx, dy);
138 | };
139 |
140 | ParentBox.prototype.translateChildren = function(dx, dy) {
141 | this.children.forEach(function(child) {
142 | child.translate(dx, dy);
143 | });
144 | };
145 |
146 | ParentBox.prototype.visibleWidth = function() {
147 | var min = function(box) {
148 | return box.position.x - box.leftWidth();
149 | };
150 |
151 | var max = function(box) {
152 | return box.position.x + box.dimensions.width + box.rightWidth();
153 | };
154 |
155 | var minX = min(this);
156 | var maxX = max(this);
157 |
158 | var height = function(parent) {
159 | minX = Math.min(minX, min(parent));
160 | maxX = Math.max(maxX, max(parent));
161 |
162 | if(parent.children) parent.children.forEach(height);
163 | };
164 |
165 | this.children.forEach(height);
166 | return maxX - minX;
167 | };
168 |
169 | ParentBox.prototype.visibleHeight = function() {
170 | var min = function(box) {
171 | return box.position.y - box.topWidth();
172 | };
173 |
174 | var max = function(box) {
175 | return box.position.y + box.dimensions.height + box.bottomWidth();
176 | };
177 |
178 | var minY = min(this);
179 | var maxY = max(this);
180 |
181 | var height = function(parent) {
182 | minY = Math.min(minY, min(parent));
183 | maxY = Math.max(maxY, max(parent));
184 |
185 | if(parent.children) parent.children.forEach(height);
186 | };
187 |
188 | this.children.forEach(height);
189 | return maxY - minY;
190 | };
191 |
192 | ParentBox.prototype.toPx = function(value) {
193 | if(Auto.is(value)) return 0;
194 | if(Percentage.is(value)) {
195 | var width = this.parent.dimensions.width;
196 | return width * value.percentage / 100;
197 | }
198 |
199 | return value.length;
200 | };
201 |
202 | module.exports = ParentBox;
203 |
--------------------------------------------------------------------------------
/source/layout/text-box.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 | var textWidth = require('text-width');
3 | var he = require('he');
4 |
5 | var values = require('../css/values');
6 | var collapse = require('./whitespace/collapse');
7 | var breaks = require('./whitespace/breaks');
8 | var Box = require('./box');
9 | var ParentBox = require('./parent-box');
10 | var Viewport = require('./viewport');
11 |
12 | var Normal = values.Keyword.Normal;
13 | var Nowrap = values.Keyword.Nowrap;
14 | var PreLine = values.Keyword.PreLine;
15 | var PreWrap = values.Keyword.PreWrap;
16 |
17 | var NEWLINE = '\n';
18 | var TAB = ' ';
19 |
20 | var isBreakable = function(box) {
21 | var format = box.style['white-space'];
22 | return Normal.is(format) || PreWrap.is(format) || PreLine.is(format);
23 | };
24 |
25 | var TextString = function(str, style) {
26 | this.original = str;
27 | this.style = style;
28 |
29 | this.normalized = he.decode(str).replace(/\t/g, TAB);
30 | };
31 |
32 | TextString.prototype.trimLeft = function() {
33 | this.normalized = this.normalized.replace(/^ /, '');
34 | };
35 |
36 | TextString.prototype.trimRight = function() {
37 | this.normalized = this.normalized.replace(/ $/, '');
38 | };
39 |
40 | TextString.prototype.append = function(str) {
41 | return new TextString(this.original + str, this.style);
42 | };
43 |
44 | TextString.prototype.width = function() {
45 | var style = this.style;
46 |
47 | return textWidth(this.normalized, {
48 | size: style['font-size'].toString(),
49 | family: style['font-family'].toString(),
50 | weight: style['font-weight'].keyword,
51 | style: style['font-style'].keyword
52 | });
53 | };
54 |
55 | var TextBox = function(styleOrParent, text) {
56 | var isParent = styleOrParent instanceof ParentBox || styleOrParent instanceof Viewport;
57 | var parent = isParent ? styleOrParent : null;
58 | var style = isParent ? styleOrParent.style : styleOrParent;
59 |
60 | text = text || '';
61 |
62 | Box.call(this, style);
63 | this.parent = parent;
64 | this.text = text;
65 | this.display = text;
66 |
67 | this.leftLink = false;
68 | this.rightLink = false;
69 | this.preservedNewline = false;
70 | };
71 |
72 | util.inherits(TextBox, Box);
73 |
74 | TextBox.prototype.layout = function(offset, line) {
75 | var parent = this.parent;
76 | var style = this.style;
77 | var format = style['white-space'].keyword;
78 | var textContext = this._textContext(line);
79 | var lines = breaks.hard(this.text, format);
80 |
81 | var textString = function(t) {
82 | return new TextString(t || '', style);
83 | };
84 |
85 | var text = textString(lines[0]);
86 | var isCollapsible = this._isCollapsible();
87 | var isBreakable = this._isBreakable() || textContext.precededByBreakable;
88 | var isMultiline = lines.length > 1;
89 | var isTrimable = isCollapsible && textContext.precededByEmpty;
90 |
91 | if(isTrimable) text.trimLeft();
92 | if(isCollapsible && (textContext.followedByEmpty || isMultiline)) text.trimRight();
93 |
94 | var x = parent.position.x + offset.width;
95 | var available = line.position.x + line.dimensions.width - x;
96 | var rest;
97 |
98 | if(isBreakable && available < 0 && !textContext.first) {
99 | rest = this.text;
100 | text = textString();
101 | } else if(isBreakable && text.width() > available) {
102 | var i = 0;
103 | var words = breaks.soft(text.original, format);
104 | var fillCurrent, fillNext = textString(words[i]);
105 |
106 | if(isTrimable) fillNext.trimLeft();
107 |
108 | while(fillNext.width() <= available && i++ < words.length) {
109 | fillCurrent = fillNext;
110 | fillNext = fillNext.append(words[i]);
111 | if(isTrimable) fillNext.trimLeft();
112 | }
113 |
114 | fillCurrent = fillCurrent || textString();
115 |
116 | if(!fillCurrent.width() && textContext.first) {
117 | i = 0;
118 |
119 | do {
120 | fillCurrent = fillCurrent.append(words[i]);
121 | if(isTrimable) fillCurrent.trimLeft();
122 | } while(!fillCurrent.width() && i++ < words.length);
123 | }
124 |
125 | if(isCollapsible) fillCurrent.trimRight();
126 |
127 | var newline = fillCurrent.original === text.original ? 1 : 0;
128 |
129 | rest = this.text.slice(fillCurrent.original.length + newline);
130 | text = fillCurrent;
131 | } else {
132 | rest = this.text.slice(text.original.length + 1);
133 | }
134 |
135 | if(this.text.charAt(text.length) === NEWLINE) this.preservedNewline = true;
136 |
137 | if(rest || isMultiline) {
138 | var textBox = rest === this.text ? null : new TextBox(style, rest);
139 | parent.addLine(this, textBox);
140 | if(!textBox) return;
141 | }
142 |
143 | this.dimensions.height = this.toPx(style['font-size']);
144 | this.dimensions.width = text.width();
145 |
146 | this.position.x = x;
147 | this.position.y = parent.position.y;
148 |
149 | this.display = text.normalized;
150 | };
151 |
152 | TextBox.prototype.endsWithCollapsibleWhitespace = function() {
153 | var text = collapse(this.text, { format: this.style['white-space'].keyword });
154 | return / $/.test(text) && this._isCollapsible();
155 | };
156 |
157 | TextBox.prototype.collapseWhitespace = function(strip) {
158 | var wh = this.endsWithCollapsibleWhitespace();
159 | var text = collapse(this.text, {
160 | format: this.style['white-space'].keyword,
161 | strip: strip
162 | });
163 |
164 | this.text = text;
165 | return wh;
166 | };
167 |
168 | TextBox.prototype.hasContent = function() {
169 | return this._isCollapsible() ? !this._isWhitespace() : (this.preservedNewline || !!this.dimensions.width);
170 | };
171 |
172 | TextBox.prototype.linePosition = function() {
173 | return this.position;
174 | };
175 |
176 | TextBox.prototype.lineHeight = function() {
177 | return this.dimensions.height;
178 | };
179 |
180 | TextBox.prototype.clone = function(parent) {
181 | var clone = new TextBox(parent, this.text);
182 | parent.children.push(clone);
183 |
184 | return clone;
185 | };
186 |
187 | TextBox.prototype.cloneWithLinks = ParentBox.prototype.cloneWithLinks;
188 | TextBox.prototype.addLink = ParentBox.prototype.addLink;
189 | TextBox.prototype.toPx = ParentBox.prototype.toPx;
190 |
191 | TextBox.prototype._isCollapsible = function() {
192 | var format = this.style['white-space'];
193 | return Normal.is(format) || Nowrap.is(format) || PreLine.is(format);
194 | };
195 |
196 | TextBox.prototype._isBreakable = function() {
197 | return isBreakable(this);
198 | };
199 |
200 | TextBox.prototype._isWhitespace = function() {
201 | return /^[\t\n\r ]*$/.test(this.text);
202 | };
203 |
204 | TextBox.prototype._textContext = function(line) {
205 | var contents = line.contents();
206 | var i = contents.indexOf(this);
207 | var precededByBreakable = false;
208 | var precededByEmpty = true;
209 | var followedByEmpty = true;
210 |
211 | for(var j = 0; j < contents.length; j++) {
212 | var empty = !contents[j].hasContent();
213 | if(j < i) precededByBreakable = precededByBreakable || isBreakable(contents[j]);
214 | if(j < i) precededByEmpty = precededByEmpty && empty;
215 | if(j > i) followedByEmpty = followedByEmpty && empty;
216 | }
217 |
218 | return {
219 | first: i === 0,
220 | last: i === (contents.length - 1),
221 | precededByBreakable: precededByBreakable,
222 | precededByEmpty: precededByEmpty,
223 | followedByEmpty: followedByEmpty
224 | };
225 | };
226 |
227 | module.exports = TextBox;
228 |
--------------------------------------------------------------------------------
/source/layout/viewport.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 |
3 | var values = require('../css/values');
4 | var compute = require('../css/compute');
5 | var Box = require('./box');
6 | var ParentBox = require('./parent-box');
7 | var BlockBox = require('./block-box');
8 |
9 | var Length = values.Length;
10 | var Auto = values.Keyword.Auto;
11 |
12 | var Viewport = function(position, dimensions) {
13 | var height = (typeof dimensions.height === 'number') ?
14 | Length.px(dimensions.height) : Auto;
15 |
16 | Box.call(this, compute({
17 | width: Length.px(dimensions.width),
18 | height: height
19 | }));
20 |
21 | this.position = position;
22 | this.dimensions = dimensions;
23 | this.children = [];
24 | };
25 |
26 | util.inherits(Viewport, Box);
27 |
28 | Viewport.prototype.clone = function() {
29 | return new Viewport(this.position, this.dimensions);
30 | };
31 |
32 | Viewport.prototype.layout = function() {
33 | var offset = { width: 0, height: 0 };
34 |
35 | this.collapseWhitespace(false);
36 |
37 | this.children.forEach(function(child) {
38 | child.layout(offset);
39 | offset.height += child.height();
40 | });
41 |
42 | var dimensions = this.dimensions;
43 | if(typeof dimensions.height !== 'number') dimensions.height = offset.height;
44 | };
45 |
46 | Viewport.prototype.attach = ParentBox.prototype.attach;
47 | Viewport.prototype.detach = ParentBox.prototype.detach;
48 | Viewport.prototype.collapseWhitespace = ParentBox.prototype.collapseWhitespace;
49 | Viewport.prototype.addLink = ParentBox.prototype.addLink;
50 | Viewport.prototype.visibleWidth = ParentBox.prototype.visibleWidth;
51 | Viewport.prototype.visibleHeight = ParentBox.prototype.visibleHeight;
52 | Viewport.prototype.addLine = BlockBox.prototype.addLine;
53 |
54 | module.exports = Viewport;
55 |
--------------------------------------------------------------------------------
/source/layout/whitespace/breaks.js:
--------------------------------------------------------------------------------
1 | exports.hard = function(str, format) {
2 | var hard = format === 'pre' || format === 'pre-wrap' || format === 'pre-line';
3 | return hard ? str.split('\n') : [str];
4 | };
5 |
6 | exports.soft = function(str, format) {
7 | var soft = format === 'normal' || format === 'pre-wrap' || format === 'pre-line';
8 | return soft ? str.split(/( +|-+)/) : [str];
9 | };
10 |
--------------------------------------------------------------------------------
/source/layout/whitespace/collapse.js:
--------------------------------------------------------------------------------
1 | module.exports = function(str, options) {
2 | options = options || {};
3 |
4 | var format = options.format || 'normal';
5 | var strip = options.strip;
6 |
7 | str = str.replace(/\r\n?/g, '\n');
8 |
9 | if(format === 'normal' || format === 'nowrap' || format === 'pre-line') {
10 | str = str.replace(/[\t ]*\n[\t ]*/g, '\n');
11 | }
12 | if(format === 'normal' || format === 'nowrap') {
13 | str = str.replace(/\n/g, ' ');
14 | }
15 | if(format === 'normal' || format === 'nowrap' || format === 'pre-line') {
16 | str = str.replace(/\t/g, ' ');
17 | str = str.replace(/ +/g, ' ');
18 |
19 | if(strip) str = str.replace(/^ /, '');
20 | }
21 |
22 | return str;
23 | };
24 |
--------------------------------------------------------------------------------
/source/stylesheets.js:
--------------------------------------------------------------------------------
1 | var url = require('url');
2 |
3 | var ElementType = require('domelementtype');
4 | var afterAll = require('after-all');
5 | var xhr = require('xhr');
6 | var he = require('he');
7 |
8 | var Stylesheet = require('./css/stylesheet');
9 |
10 | var link = function(base, node, i, callback) {
11 | var href = node.attribs.href;
12 | if(!href) return callback(null, Stylesheet.empty(i));
13 |
14 | href = he.decode(href);
15 | href = url.resolve(base, href);
16 |
17 | xhr({
18 | method: 'GET',
19 | url: href
20 | }, function(err, response, body) {
21 | var errored = err || !/2\d\d/.test(response.statusCode);
22 | var stylesheet = errored ? Stylesheet.empty(i) : Stylesheet.parse(body, i);
23 |
24 | callback(null, stylesheet);
25 | });
26 | };
27 |
28 | var style = function(node, i) {
29 | var text = node.childNodes && node.childNodes[0];
30 | if(!text || text.type !== ElementType.Text) return Stylesheet.empty(i);
31 |
32 | return Stylesheet.parse(text.data, i);
33 | };
34 |
35 | module.exports = function(base, nodes, callback) {
36 | if(!nodes.length) return callback(null, []);
37 |
38 | var stylesheets = new Array(nodes.length);
39 | var next = afterAll(function(err) {
40 | callback(err, stylesheets);
41 | });
42 |
43 | nodes.forEach(function(node, i) {
44 | var cb = next(function(err, stylesheet) {
45 | node.stylesheet = stylesheet;
46 | stylesheets[i] = stylesheet;
47 | });
48 |
49 | if(node.name === 'link') link(base, node, i, cb);
50 | else cb(null, style(node, i));
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/test/assets/block-image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
39 |
40 |
41 |
42 | Lorem ipsum dolor sit amet
43 |
44 | consectetur adipiscing elit.
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/test/assets/block-in-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
27 |
28 |
29 |
30 | H
31 | e
32 | l
33 | l
34 | o
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test/assets/br.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
27 |
28 |
29 |
30 |
31 | Lorem ipsum dolor sit amet,
32 | consectetur adipiscing elit.
33 |
34 |
35 |
36 | Donec ac convallis nisi.
37 | Mauris fermentum est nec placerat tincidunt.
38 |
39 |
40 |
41 | Nunc non risus gravida, pharetra felis et,
42 | scelerisque lacus.
43 |
44 |
45 |
46 |
47 | Suspendisse lacinia sit amet est id ornare.
48 |
49 |
50 |
51 |
52 |
53 | Quisque sit amet lectus ornare,
54 | ullamcorper diam in,
55 | varius mi.
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/test/assets/column-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
49 |
50 |
51 |
52 | H
53 | e
54 | l
55 | l
56 | o
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/test/assets/css/default.css:
--------------------------------------------------------------------------------
1 | * {
2 | display: inline;
3 | }
4 |
5 | html {
6 | display: block;
7 | font-size: 16px;
8 | font-family: "Times New Roman";
9 | line-height: 1.2;
10 | }
11 |
12 | head, link, style, meta, title, script {
13 | display: none;
14 | }
15 |
16 | body {
17 | display: block;
18 | margin-top: 8px;
19 | margin-right: 8px;
20 | margin-bottom: 8px;
21 | margin-left: 8px;
22 | }
23 |
24 | div {
25 | display: block;
26 | }
27 |
28 | br {
29 | display: line-break;
30 | }
31 |
--------------------------------------------------------------------------------
/test/assets/css/reset.css:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | table {
41 | border-collapse: collapse;
42 | border-spacing: 0;
43 | }
--------------------------------------------------------------------------------
/test/assets/empty-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/test/assets/font-size.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
32 |
33 |
34 |
35 | Lorem
36 | ipsum
37 | dolor
38 | sit
39 | amet
40 | consectetur
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/test/assets/image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/test/assets/images/waves.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kapetan/repaint/2b603fae5704891a4f0102ef9ea5f353827f3604/test/assets/images/waves.png
--------------------------------------------------------------------------------
/test/assets/inline-image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
38 |
39 |
40 |
41 | Lorem ipsum dolor sit amet
42 |
43 | consectetur adipiscing elit.
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/test/assets/line-height.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
45 |
46 |
47 |
48 | Lorem ipsum dolor sit amet consectetur
49 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
50 | Nunc non risus gravida , pharetra felis et, scelerisque lacus.
51 | Suspendisse lacinia sit amet est id ornare.
52 | Quisque sit amet lectus ornare,
53 | ullamcorper diam in, varius mi.
54 |
55 |
56 |
57 | Lorem ipsum dolor sit amet consectetur
58 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/test/assets/mixed-white-space.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
31 |
32 |
33 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
34 |
35 |
36 |
37 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
38 |
39 |
40 |
41 | Suspendisse lacinia sit amet est id ornare.
42 |
43 |
44 |
45 |
46 |
47 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/test/assets/multiline-font-size.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
32 |
33 |
34 |
35 | Lorem
36 | ipsum
37 | dolor
38 | sit
39 | amet
40 | consectetur
41 |
42 |
43 |
44 | Donec
45 | ac
46 | convallis
47 | nisi.
48 | Mauris
49 | fermentum
50 |
51 |
52 |
53 | Nunc
54 | non
55 | risus
56 | gravida,
57 | pharetra
58 | felis
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/test/assets/multiline-image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
42 |
43 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
44 |
45 | Donec ac convallis nisi.
46 |
47 |
48 |
49 | Mauris fermentum est nec placerat tincidunt.
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/test/assets/multiline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
29 |
30 |
31 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
32 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
33 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
34 | Suspendisse lacinia sit amet est id ornare.
35 | Quisque sit amet lectus ornare, ullamcorper diam in, varius mi.
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/test/assets/nested-block-in-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
27 |
28 |
29 |
30 |
31 | H
32 |
33 | e
34 |
35 | l
36 |
37 | l
38 |
39 | o
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/test/assets/nested-block.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
46 |
47 |
48 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/test/assets/nested-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
46 |
47 |
48 |
49 | H
50 |
51 | e
52 |
53 | l
54 |
55 | l
56 | o
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/test/assets/padded-all.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
45 | Donec ac convallis nisi.
46 | Mauris fermentum est nec placerat tincidunt.
47 |
48 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/test/assets/padded-block-in-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
45 | Donec ac convallis nisi.
46 | Mauris fermentum est nec placerat tincidunt.
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/test/assets/padded-br.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
45 |
46 | Mauris fermentum est nec placerat tincidunt.
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/test/assets/padded-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
53 |
54 |
55 |
56 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
57 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
58 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
59 | Suspendisse lacinia sit amet est id ornare.
60 | Quisque sit amet lectus ornare, ullamcorper diam in, varius mi.
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/test/assets/pre.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
55 |
56 |
57 |
58 |
59 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
60 |
61 |
62 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
63 |
64 |
65 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
66 |
67 |
68 | Suspendisse lacinia sit amet est id ornare. Lorem ipsum dolor sit amet
69 |
70 |
71 | consectetur adipiscing elit.
72 |
73 |
74 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
75 |
76 |
77 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
78 |
79 |
80 |
81 |
82 | Lorem ipsum
83 |
84 |
85 | dolor sit amet
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/test/assets/shorthand.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
33 |
34 |
35 |
38 |
39 |
40 | Lorem ipsum dolor sit amet
41 | consectetur
42 | adipiscing elit
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/test/assets/simple-block.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/test/assets/simple-inline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
33 |
34 |
35 | Hello
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test/assets/stack-block.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/test/assets/vertical-align-image.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
24 |
25 |
26 |
27 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
28 |
29 |
30 |
31 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/test/assets/vertical-align.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
130 |
131 |
132 |
133 | Lorem
134 | ipsum
135 | dolor
136 | sit
137 | amet
138 |
139 |
140 |
141 | consectetur
142 | Donec
143 | ac
144 | convallis
145 | nisi.
146 |
147 |
148 |
149 | Mauris
150 |
151 | fermentum
152 |
153 | est
154 |
155 | nec
156 |
157 | placerat
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | tincidunt.
166 |
167 | Nunc
168 |
169 | non
170 |
171 | risus
172 |
173 | gravida
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/test/assets/white-space.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
33 |
34 |
35 |
36 |
37 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
38 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
39 |
40 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
41 | Suspendisse lacinia sit amet est id ornare.
42 |
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
45 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
46 |
47 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
48 | Suspendisse lacinia sit amet est id ornare.
49 |
50 |
51 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
52 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
53 |
54 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
55 | Suspendisse lacinia sit amet est id ornare.
56 |
57 |
58 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
59 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
60 |
61 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
62 | Suspendisse lacinia sit amet est id ornare.
63 |
64 |
65 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
66 | Donec ac convallis nisi. Mauris fermentum est nec placerat tincidunt.
67 |
68 | Nunc non risus gravida, pharetra felis et, scelerisque lacus.
69 | Suspendisse lacinia sit amet est id ornare.
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Browser Test
5 |
6 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | var util = require('util');
2 | var fs = require('fs');
3 | var qs = require('querystring');
4 | var url = require('url');
5 |
6 | var serialize = require('./serialize');
7 | var render = require('../');
8 |
9 | var query = qs.parse(window.location.search.replace(/^\?/, ''));
10 |
11 | var assets = {};
12 | assets['simple-block.html'] = fs.readFileSync(__dirname + '/assets/simple-block.html', 'utf-8');
13 | assets['nested-block.html'] = fs.readFileSync(__dirname + '/assets/nested-block.html', 'utf-8');
14 | assets['stack-block.html'] = fs.readFileSync(__dirname + '/assets/stack-block.html', 'utf-8');
15 | assets['simple-inline.html'] = fs.readFileSync(__dirname + '/assets/simple-inline.html', 'utf-8');
16 | assets['empty-inline.html'] = fs.readFileSync(__dirname + '/assets/empty-inline.html', 'utf-8');
17 | assets['nested-inline.html'] = fs.readFileSync(__dirname + '/assets/nested-inline.html', 'utf-8');
18 | assets['column-inline.html'] = fs.readFileSync(__dirname + '/assets/column-inline.html', 'utf-8');
19 | assets['white-space.html'] = [fs.readFileSync(__dirname + '/assets/white-space.html', 'utf-8'), 512, 768];
20 | assets['pre.html'] = [fs.readFileSync(__dirname + '/assets/pre.html', 'utf-8'), 512, 384];
21 | assets['mixed-white-space.html'] = fs.readFileSync(__dirname + '/assets/mixed-white-space.html', 'utf-8');
22 | assets['multiline.html'] = fs.readFileSync(__dirname + '/assets/multiline.html', 'utf-8');
23 | assets['br.html'] = fs.readFileSync(__dirname + '/assets/br.html', 'utf-8');
24 | assets['block-in-inline.html'] = fs.readFileSync(__dirname + '/assets/block-in-inline.html', 'utf-8');
25 | assets['nested-block-in-inline.html'] = fs.readFileSync(__dirname + '/assets/nested-block-in-inline.html', 'utf-8');
26 | assets['padded-block-in-inline.html'] = fs.readFileSync(__dirname + '/assets/padded-block-in-inline.html', 'utf-8');
27 | assets['padded-inline.html'] = fs.readFileSync(__dirname + '/assets/padded-inline.html', 'utf-8');
28 | assets['padded-br.html'] = fs.readFileSync(__dirname + '/assets/padded-br.html', 'utf-8');
29 | assets['padded-all.html'] = fs.readFileSync(__dirname + '/assets/padded-all.html', 'utf-8');
30 | assets['font-size.html'] = fs.readFileSync(__dirname + '/assets/font-size.html', 'utf-8');
31 | assets['multiline-font-size.html'] = fs.readFileSync(__dirname + '/assets/multiline-font-size.html', 'utf-8');
32 | assets['image.html'] = fs.readFileSync(__dirname + '/assets/image.html', 'utf-8');
33 | assets['inline-image.html'] = fs.readFileSync(__dirname + '/assets/inline-image.html', 'utf-8');
34 | assets['block-image.html'] = fs.readFileSync(__dirname + '/assets/block-image.html', 'utf-8');
35 | assets['multiline-image.html'] = fs.readFileSync(__dirname + '/assets/multiline-image.html', 'utf-8');
36 | assets['line-height.html'] = [fs.readFileSync(__dirname + '/assets/line-height.html', 'utf-8'), 512, 384];
37 | assets['vertical-align.html'] = fs.readFileSync(__dirname + '/assets/vertical-align.html', 'utf-8');
38 | assets['vertical-align-image.html'] = fs.readFileSync(__dirname + '/assets/vertical-align-image.html', 'utf-8');
39 | assets['shorthand.html'] = fs.readFileSync(__dirname + '/assets/shorthand.html', 'utf-8');
40 |
41 | var resolve = function(name) {
42 | var loc = window.location;
43 | var path = url.resolve(loc.pathname, name);
44 |
45 | return util.format('%s//%s%s', loc.protocol, loc.host, path);
46 | };
47 |
48 | var canvas = function(element, options, callback) {
49 | var canvas = document.createElement('canvas');
50 | var dimensions = options.viewport.dimensions;
51 | var dpr = window.devicePixelRatio || 1;
52 |
53 | canvas.width = dimensions.width * dpr;
54 | canvas.height = dimensions.height * dpr;
55 | canvas.style.width = dimensions.width + 'px';
56 | canvas.style.height = dimensions.height + 'px';
57 |
58 | element.appendChild(canvas);
59 | var context = canvas.getContext('2d');
60 | context.scale(dpr, dpr);
61 | options.context = context;
62 |
63 | render(options, callback);
64 | };
65 |
66 | var iframe = function(element, options) {
67 | var dimensions = options.viewport.dimensions;
68 | var iframe = document.createElement('iframe');
69 | element.appendChild(iframe);
70 |
71 | iframe.width = dimensions.width;
72 | iframe.height = dimensions.height;
73 |
74 | var doc = iframe.contentDocument;
75 |
76 | doc.open();
77 | doc.write(options.content);
78 | doc.close();
79 | };
80 |
81 | var row = function(element, options) {
82 | var row = document.createElement('div');
83 | row.className = 'row clearfix';
84 |
85 | var top = document.createElement('div');
86 | top.className = 'top';
87 |
88 | var name = options.url.split('/').pop();
89 | var content = document.createTextNode(name);
90 | var link = document.createElement('a');
91 | link.href = window.location.pathname + '?name=' + encodeURIComponent(name);
92 |
93 | link.appendChild(content);
94 | top.appendChild(link);
95 |
96 | var left = document.createElement('div');
97 | left.className = 'left-column';
98 |
99 | var right = document.createElement('div');
100 | right.className = 'right-column';
101 |
102 | row.appendChild(top);
103 | row.appendChild(left);
104 | row.appendChild(right);
105 |
106 | element.appendChild(row);
107 |
108 | iframe(right, options);
109 | canvas(left, options, function(err, page) {
110 | if(err) throw err;
111 |
112 | console.log('--', page.url);
113 | console.log(serialize(page.layout));
114 | });
115 | };
116 |
117 | var container = document.getElementById('container');
118 |
119 | Object.keys(assets).forEach(function(asset) {
120 | if(query.name && query.name !== asset) return;
121 | if(query.name) document.title += util.format(' (%s)', asset);
122 |
123 | var data = assets[asset];
124 | data = Array.isArray(data) ? data : [data, 512, 256];
125 |
126 | row(container, {
127 | url: resolve(asset),
128 | content: data[0],
129 | viewport: {
130 | position: { x: 0, y: 0 },
131 | dimensions: { width: data[1], height: data[2] }
132 | }
133 | });
134 | });
135 |
--------------------------------------------------------------------------------
/test/serialize.js:
--------------------------------------------------------------------------------
1 | var Viewport = require('../source/layout/viewport');
2 | var BlockBox = require('../source/layout/block-box');
3 | var LineBox = require('../source/layout/line-box');
4 | var LineBreakBox = require('../source/layout/line-break-box');
5 | var InlineBox = require('../source/layout/inline-box');
6 | var TextBox = require('../source/layout/text-box');
7 | var ImageBox = require('../source/layout/image-box');
8 |
9 | var indent = function(i) {
10 | return (new Array(i + 1)).join('| ');
11 | };
12 |
13 | var name = function(box) {
14 | if(box instanceof Viewport) return 'Viewport';
15 | if(box instanceof BlockBox) return 'BlockBox';
16 | if(box instanceof LineBox) return 'LineBox';
17 | if(box instanceof LineBreakBox) return 'LineBreakBox';
18 | if(box instanceof InlineBox) return 'InlineBox';
19 | if(box instanceof TextBox) return 'TextBox';
20 | if(box instanceof ImageBox.Block) return 'BlockImageBox';
21 | if(box instanceof ImageBox.Inline) return 'InlineImageBox';
22 | };
23 |
24 | var attributes = function(box) {
25 | return '(' + [
26 | ['x', box.position.x],
27 | ['y', box.position.y],
28 | ['width', box.dimensions.width],
29 | ['height', box.dimensions.height]
30 | ].map(function(pair) {
31 | return pair[0] + '=' + pair[1];
32 | }).join(', ') + ')';
33 | };
34 |
35 | var toString = function(box, indentation) {
36 | var space = indent(indentation);
37 |
38 | if(box instanceof TextBox) return [space, name(box), attributes(box), '[', JSON.stringify(box.display), ']'].join('');
39 | if(box instanceof ImageBox) return [space, name(box), attributes(box), '[', JSON.stringify(box.image.src), ']'].join('');
40 | if(box instanceof LineBreakBox) return [space, name(box)].join('');
41 |
42 | var children = box.children
43 | .map(function(child) {
44 | return toString(child, indentation + 1);
45 | }).filter(Boolean);
46 |
47 | return [space + name(box) + attributes(box) + '[']
48 | .concat(children)
49 | .concat([space + ']'])
50 | .join('\n');
51 | };
52 |
53 | module.exports = function(box) {
54 | return toString(box, 0);
55 | };
56 |
--------------------------------------------------------------------------------