svg-sizing.js (16378B)
1 // Simple implementation of SVG sizing 2 3 setup({explicit_done: true}); 4 5 var SVGSizing = (function() { 6 function parseLength(l) { 7 var match = /^([-+]?[0-9]+|[-+]?[0-9]*\.[0-9]+)(px|%)?$/.exec(l); 8 if (!match) 9 return null; 10 return new Length(Number(match[1]), match[2] ? match[2] : "px"); 11 } 12 13 function parseViewBox(input) { 14 if (!input) 15 return null; 16 17 var arr = input.split(' '); 18 return arr.map(function(a) { return parseInt(a); }); 19 } 20 21 // Only px and % are used 22 function convertToPx(input, percentRef) { 23 if (input == null) 24 return null; 25 var length = parseLength(input); 26 if (length.amount == 0) 27 return 0; 28 if (!length.unit) 29 length.unit = "px"; 30 if (length.unit == "%" && percentRef === undefined) 31 return null; 32 return length.amount * { px: 1, 33 "%": percentRef/100}[length.unit]; 34 } 35 36 function Length(amount, unit) { 37 this.amount = amount; 38 this.unit = unit; 39 } 40 41 function describe(data) { 42 function dumpObject(obj) { 43 var r = ""; 44 for (var property in obj) { 45 if (obj.hasOwnProperty(property)) { 46 var value = obj[property]; 47 if (typeof value == 'string') 48 value = "'" + value + "'"; 49 else if (value == null) 50 value = "null"; 51 else if (typeof value == 'object') 52 { 53 if (value instanceof Array) 54 value = "[" + value + "]"; 55 else 56 value = "{" + dumpObject(value) + "}"; 57 } 58 59 if (value != "null") 60 r += property + ": " + value + ", "; 61 } 62 } 63 return r; 64 } 65 var result = dumpObject(data); 66 if (result == "") 67 return "(initial values)"; 68 return result; 69 } 70 71 function mapPresentationalHintLength(testData, cssProperty, attr) { 72 if (attr) { 73 var l = parseLength(attr); 74 if (l) 75 testData.style[cssProperty] = l.amount + l.unit; 76 } 77 } 78 79 function computedWidthIsAuto(testData) { 80 return !testData.style["width"] || testData.style["width"] == 'auto'; 81 } 82 83 function computedHeightIsAuto(testData) { 84 return !testData.style["height"] || testData.style["height"] == 'auto' || 85 (parseLength(testData.style["height"]).unit == '%' && 86 containerComputedHeightIsAuto(testData)); 87 } 88 89 function containerComputedWidthIsAuto(testData) { 90 return !testData.config.containerWidthStyle || 91 testData.config.containerWidthStyle == 'auto'; 92 } 93 94 function containerComputedHeightIsAuto(testData) { 95 return !testData.config.containerHeightStyle || 96 testData.config.containerHeightStyle == 'auto'; 97 } 98 99 function intrinsicInformation(testData) { 100 if (testData.config.placeholder == 'iframe') 101 return {}; 102 103 var w = convertToPx(testData.config.svgWidthAttr) || 0; 104 var h = convertToPx(testData.config.svgHeightAttr) || 0; 105 var r = 0; 106 if (w && h) { 107 r = w / h; 108 } else { 109 var vb = parseViewBox(testData.config.svgViewBoxAttr); 110 if (vb) { 111 r = vb[2] / vb[3]; 112 } 113 if (r) { 114 if (!w && h) 115 w = h * r; 116 else if (!h && w) 117 h = w / r; 118 } 119 } 120 return { width: w, height: h, ratio: r }; 121 }; 122 123 function contentAttributeForPlaceholder(testData) { 124 if (testData.config.placeholder == 'object') 125 return "data"; 126 else 127 return "src"; 128 } 129 130 function TestData(config) { 131 this.config = config; 132 this.name = describe(config); 133 this.style = {}; 134 if (config.placeholder) { 135 mapPresentationalHintLength(this, "width", config.placeholderWidthAttr); 136 mapPresentationalHintLength(this, "height", config.placeholderHeightAttr); 137 } else { 138 if (config.svgWidthStyle) 139 this.style["width"] = config.svgWidthStyle; 140 else 141 mapPresentationalHintLength(this, "width", config.svgWidthAttr); 142 143 if (config.svgHeightStyle) 144 this.style["height"] = config.svgHeightStyle; 145 else 146 mapPresentationalHintLength(this, "height", config.svgHeightAttr); 147 } 148 } 149 150 TestData.prototype.computeInlineReplacedSize = function(outerWidth, outerHeight) { 151 var intrinsic = intrinsicInformation(this); 152 var self = this; 153 154 // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-height 155 function calculateUsedHeight() { 156 if (computedHeightIsAuto(self)) { 157 if (computedWidthIsAuto(self) && intrinsic.height) 158 return intrinsic.height; 159 if (intrinsic.ratio) 160 return calculateUsedWidth() / intrinsic.ratio; 161 if (intrinsic.height) 162 return intrinsic.height; 163 return 150; 164 } 165 166 return convertToPx(self.style["height"], 167 convertToPx(self.config.containerHeightStyle, 168 outerHeight)); 169 } 170 171 // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-width 172 function calculateUsedWidth() { 173 if (computedWidthIsAuto(self)) { 174 if (computedHeightIsAuto(self) && intrinsic.width) 175 return intrinsic.width; 176 if (!computedHeightIsAuto(self) && intrinsic.ratio) 177 return calculateUsedHeight() * intrinsic.ratio; 178 if (computedHeightIsAuto(self) && intrinsic.ratio) { 179 if (containerComputedWidthIsAuto(self)) { 180 // Note: While this is actually undefined in CSS 181 // 2.1, use the suggested value by examining the 182 // ancestor widths. 183 return outerWidth; 184 } else { 185 return convertToPx(self.config.containerWidthStyle, 186 outerWidth); 187 } 188 } 189 if (intrinsic.width) 190 return intrinsic.width; 191 return 300; 192 } 193 194 if (containerComputedWidthIsAuto(self)) 195 return convertToPx(self.style["width"], outerWidth); 196 else 197 return convertToPx(self.style["width"], 198 convertToPx(self.config.containerWidthStyle, 199 outerWidth)); 200 } 201 return { width: calculateUsedWidth(), 202 height: calculateUsedHeight() }; 203 }; 204 205 TestData.prototype.buildContainer = function (placeholder, options) { 206 options = options || {}; 207 208 var container = document.createElement("div"); 209 210 container.id = "container"; 211 if (this.config.containerWidthStyle) 212 container.style.width = this.config.containerWidthStyle; 213 214 if (this.config.containerHeightStyle) 215 container.style.height = this.config.containerHeightStyle; 216 217 if (options.pretty) 218 container.appendChild(document.createTextNode("\n\t\t")); 219 container.appendChild(placeholder); 220 if (options.pretty) 221 container.appendChild(document.createTextNode("\n\t")); 222 223 return container; 224 }; 225 226 TestData.prototype.buildSVGOrPlaceholder = function (options) { 227 options = options || {}; 228 var self = this; 229 230 if (this.config.placeholder) { 231 var generateSVGURI = function(testData, encoder) { 232 var res = '<svg xmlns="http://www.w3.org/2000/svg"'; 233 function addAttr(attr, prop) { 234 if (testData.config[prop]) 235 res += ' ' + attr + '="' + testData.config[prop] + '"'; 236 } 237 addAttr("width", "svgWidthAttr"); 238 addAttr("height", "svgHeightAttr"); 239 addAttr("viewBox", "svgViewBoxAttr"); 240 res += '></svg>'; 241 return 'data:image/svg+xml' + encoder(res); 242 }; 243 var placeholder = document.createElement(this.config.placeholder); 244 if (options.pretty) { 245 placeholder.appendChild(document.createTextNode("\n\t\t\t")); 246 placeholder.appendChild( 247 document.createComment( 248 generateSVGURI(this, function(x) { return "," + x; }))); 249 placeholder.appendChild(document.createTextNode("\n\t\t")); 250 } 251 placeholder.setAttribute("id", "test"); 252 if (this.config.placeholderWidthAttr) 253 placeholder.setAttribute("width", this.config.placeholderWidthAttr); 254 if (this.config.placeholderHeightAttr) 255 placeholder.setAttribute("height", this.config.placeholderHeightAttr); 256 placeholder.setAttribute(contentAttributeForPlaceholder(this), 257 generateSVGURI(this, function(x) { 258 return ";base64," + btoa(x); 259 })); 260 return placeholder; 261 } else { 262 var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 263 svgElement.setAttribute("id", "test"); 264 if (self.config.svgWidthStyle) 265 svgElement.style.width = self.config.svgWidthStyle; 266 if (self.config.svgHeightStyle) 267 svgElement.style.height = self.config.svgHeightStyle; 268 if (self.config.svgWidthAttr) 269 svgElement.setAttribute("width", self.config.svgWidthAttr); 270 if (self.config.svgHeightAttr) 271 svgElement.setAttribute("height", self.config.svgHeightAttr); 272 if (self.config.svgViewBoxAttr) 273 svgElement.setAttribute("viewBox", self.config.svgViewBoxAttr); 274 return svgElement; 275 } 276 }; 277 278 TestData.prototype.buildDemo = function (expectedRect, id) { 279 // Non-essential debugging tool 280 var self = this; 281 282 function buildDemoSerialization() { 283 var outerWidth = 800; 284 var outerHeight = 600; 285 286 var options = { pretty: true }; 287 var container = 288 self.buildContainer(self.buildSVGOrPlaceholder(options), options); 289 290 var root = document.createElement("html"); 291 var style = document.createElement("style"); 292 293 style.textContent = "\n" + 294 "\tbody { margin: 0; font-family: sans-serif }\n" + 295 "\tiframe { border: none }\n" + 296 "\t#expected {\n" + 297 "\t\twidth: " + (expectedRect.width) + "px; height: " 298 + (expectedRect.height) + "px;\n" + 299 "\t\tborder: 10px solid lime; position: absolute;\n" + 300 "\t\tbackground-color: red }\n" + 301 "\t#testContainer { position: absolute;\n" + 302 "\t\ttop: 10px; left: 10px; width: " + outerWidth + "px;\n" + 303 "\t\theight: " + outerHeight + "px }\n" + 304 "\t#test { background-color: green }\n" + 305 "\t.result { position: absolute; top: 0; right: 0;\n" + 306 "\t\tbackground-color: hsla(0,0%, 0%, 0.85); border-radius: 0.5em;\n" + 307 "\t\tpadding: 0.5em; border: 0.25em solid black }\n" + 308 "\t.pass { color: lime }\n" + 309 "\t.fail { color: red }\n"; 310 311 root.appendChild(document.createTextNode("\n")); 312 root.appendChild(style); 313 root.appendChild(document.createTextNode("\n")); 314 315 var script = document.createElement("script"); 316 script.textContent = "\n" + 317 "onload = function() {\n" + 318 "\tvar svgRect =\n" + 319 "\t\tdocument.querySelector('#test').getBoundingClientRect();\n" + 320 "\tpassed = (svgRect.width == " + expectedRect.width + " && " + 321 "svgRect.height == " + expectedRect.height + ");\n" + 322 "\tdocument.body.insertAdjacentHTML('beforeEnd',\n" + 323 "\t\t'<span class=\"result '+ (passed ? 'pass' : 'fail') " + 324 "+ '\">' + (passed ? 'Pass' : 'Fail') + '</span>');\n" + 325 "};\n"; 326 327 root.appendChild(script); 328 root.appendChild(document.createTextNode("\n")); 329 330 var expectedElement = document.createElement("div"); 331 expectedElement.id = "expected"; 332 root.appendChild(expectedElement); 333 root.appendChild(document.createTextNode("\n")); 334 335 var testContainer = document.createElement("div"); 336 testContainer.id = "testContainer"; 337 testContainer.appendChild(document.createTextNode("\n\t")); 338 testContainer.appendChild(container); 339 testContainer.appendChild(document.createTextNode("\n")); 340 root.appendChild(testContainer); 341 root.appendChild(document.createTextNode("\n")); 342 343 return "<!DOCTYPE html>\n" + root.outerHTML; 344 } 345 346 function pad(n, width, z) { 347 z = z || '0'; 348 n = n + ''; 349 return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; 350 } 351 352 function heightToDescription(height) { 353 if (!height || height == "auto") 354 return "auto"; 355 if (parseLength(height).unit == '%') 356 return "percentage"; 357 return "fixed"; 358 } 359 360 var demoRoot = document.querySelector('#demo'); 361 if (demoRoot) { 362 var demo = buildDemoSerialization(); 363 var iframe = document.createElement('iframe'); 364 iframe.style.width = (Math.max(900, expectedRect.width)) + "px"; 365 iframe.style.height = (Math.max(400, expectedRect.height)) + "px"; 366 iframe.src = "data:text/html;charset=utf-8," + encodeURIComponent(demo); 367 demoRoot.appendChild(iframe); 368 demoRoot.insertAdjacentHTML( 369 'beforeEnd', 370 '<p><a href="data:application/octet-stream;charset=utf-8;base64,' + 371 btoa(demo) + '" download="svg-in-' + this.config.placeholder + "-" + 372 heightToDescription(this.config.placeholderHeightAttr) + "-" + pad(id, 3) + 373 '.html">Download</a></p>'); 374 } 375 }; 376 377 return { 378 TestData: TestData, 379 doCombinationTest: function(values, func, testSingleId) { 380 function computeConfig(id) { 381 id--; 382 var multiplier = 1; 383 var config = {}; 384 for (var i=0; i<values.length; i++) { 385 // Compute offset into current array 386 var ii = (Math.floor(id / multiplier)) % values[i][1].length; 387 // Set corresponding value 388 config[values[i][0]] = values[i][1][ii]; 389 // Compute new multiplier 390 multiplier *= values[i][1].length; 391 } 392 if (id >= multiplier) 393 return null; 394 return config; 395 } 396 397 function cont(id) { 398 var config = computeConfig(id); 399 if (config && (!testSingleId || testSingleId == id)) { 400 var next = function() {func(config, id, cont)}; 401 // Make sure we don't blow the stack, without too much slowness 402 if (id % 20 === 0) { 403 step_timeout(next, 0); 404 } else { 405 next(); 406 } 407 } else { 408 done(); 409 } 410 }; 411 412 if (testSingleId) 413 cont(testSingleId); 414 else 415 cont(1); 416 } 417 }; 418 })();