├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── composer.json ├── gulpfile.js ├── holder.js ├── holder.min.js ├── package-lock.json ├── package.js ├── package.json ├── src ├── index.js ├── lib │ ├── color.js │ ├── constants.js │ ├── dom.js │ ├── index.js │ ├── renderers │ │ ├── canvas.js │ │ ├── svg-dom.js │ │ └── svg-text.js │ ├── scenegraph.js │ ├── svg.js │ ├── utils.js │ └── vendor │ │ ├── ondomready.js │ │ ├── polyfills.js │ │ ├── querystring.js │ │ └── shaven │ │ ├── buildTransformString.js │ │ ├── defaults.js │ │ ├── escape.js │ │ ├── index.js │ │ ├── mapAttributeValue.js │ │ ├── parseSugarString.js │ │ ├── server.js │ │ └── stringifyStyleObject.js └── meteor │ ├── package.js │ └── shim.js └── test ├── .gitignore ├── detach.html ├── holder.js ├── image.jpg ├── index.html ├── index.js ├── phantom.js ├── renderperf ├── .gitignore ├── fps.html ├── index.html ├── releases │ └── holder-master │ │ └── holder.js ├── setup.sh └── testcase.html └── runner.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor config from Quickstart 2 | # qkst.io/dotfiles/editorconfig 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.js] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [Makefile] 18 | indent_style = tab 19 | indent_size = 8 20 | 21 | [*.{js}] 22 | charset = utf-8 23 | trim_trailing_whitespace = true 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "quotes": ["error", "single"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore from Quickstart 2 | # qkst.io/dotfiles/gitignore 3 | 4 | # Sensitive files 5 | *.key 6 | *.pem 7 | 8 | # JavaScript 9 | node_modules/ 10 | bower_components/ 11 | npm-debug.log 12 | 13 | # Executables 14 | *.out 15 | *.exe 16 | *.app 17 | 18 | # Logs 19 | *.log 20 | logs 21 | 22 | # Archives 23 | *.[7gx]z 24 | *.[tr]ar 25 | *.zip 26 | *.dmg 27 | *.deb 28 | *.rpm 29 | *.xpi 30 | *.msi 31 | 32 | # Linux 33 | *~ 34 | .Trash-* 35 | 36 | # OS X 37 | ._* 38 | .fuse_* 39 | .Spotlight-* 40 | .DS_Store 41 | 42 | # Windows 43 | *.tmp 44 | *.bak 45 | *.lnk 46 | Thumbs.db 47 | 48 | # Ansible 49 | *.retry 50 | 51 | # Vagrant 52 | .vagrant 53 | 54 | # IDEA 55 | .idea/* 56 | 57 | # Other VCS 58 | .hg 59 | .bzr 60 | .svn 61 | 62 | # Other temporary files 63 | *.dump 64 | *.swp 65 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | env: 5 | global: 6 | - secure: oFLrNQrXJ5F2XrJ9Fe515PY6JyYbQCHeIVDY+7yc7E90bIZJEb0lVuRGMHrY0Win2WrwdtqdvljMk48YwtY0i6w3z2FbUPn5bNStPMXlxJ9pb6bnXwi1ixNjljveMRo8ZGZYvm66HPYaVn59PUELE4mfIFTYooqdlbGtZTMaRc8= 7 | - secure: qJqn+4QtqjPlKe0vkZlYPUXLE9xODLt0Azs1A4vu4SJoI4jtU6BBcccAkG232JAKC2GiLG5daMMeDMRoUCXHANqyQBf0i/ep56DGwVncmrdNVcGubZdcR8Blj6Ib+aHUgXS0sRrX5PETCBv7t+FZVIzQM4Acq1OiPQEAnTWqAmM= 8 | addons: 9 | sauce_connect: true 10 | sudo: false 11 | install: 12 | - npm install -g gulp 13 | - npm install 14 | - gulp 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2021 Ivan Malopinsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Holder 2 | 3 | ![](http://imsky.github.io/holder/images/header.png) 4 | 5 | Holder renders image placeholders in browser using SVG. 6 | 7 | Used by thousands of [open source projects](https://github.com/search?q=holder.js+in%3Apath&type=Code&ref=searchresults) (including [Bootstrap v3](https://getbootstrap.com)) and [many](https://nerdydata.com/technology-reports/Holder.js/1cf03fe7-0e02-40ef-be69-f00ca9547fc9) [other](http://libscore.com/#Holder) sites. 8 | 9 | No extra requests, small bundle size, highly customizable. 10 | 11 | [![npm](https://img.shields.io/npm/v/holderjs.svg)](https://www.npmjs.com/package/holderjs) 12 | [![Travis Build Status](https://img.shields.io/travis/imsky/holder.svg)](https://travis-ci.org/imsky/holder) 13 | [![Package Quality](http://npm.packagequality.com/shield/holderjs.svg)](http://packagequality.com/#?package=holderjs) 14 | [![NerdyData Popularity](https://badges.nerdydata.com/1cf03fe7-0e02-40ef-be69-f00ca9547fc9)](https://nerdydata.com/technology-reports/Holder.js/1cf03fe7-0e02-40ef-be69-f00ca9547fc9) 15 | [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/holderjs/badge?style=rounded)](https://www.jsdelivr.com/package/npm/holderjs) 16 | 17 | ## Installing 18 | 19 | * [npm](https://npmjs.com/): `npm install holderjs` 20 | * [yarn](https://yarnpkg.com/): `yarn add holderjs` 21 | * [unpkg](https://unpkg.com/): 22 | * [cdnjs](http://cdnjs.com/): 23 | * [jsDelivr](https://www.jsdelivr.com): 24 | * [Bower](https://bower.io/): `bower install holderjs` 25 | * [Rails Assets](https://rails-assets.org): `gem 'rails-assets-holderjs'` 26 | * [Meteor](https://atmospherejs.com/): `meteor add imsky:holder` 27 | * [Composer](https://packagist.org/): `php composer.phar update imsky/holder` 28 | * [NuGet](https://www.nuget.org/): `Install-Package Holder.js` 29 | 30 | ## Usage 31 | 32 | Include ``holder.js`` in your HTML: 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | Holder will then process all images with a specific ``src`` attribute, like this one: 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | The above tag will render as a placeholder 300 pixels wide and 200 pixels tall. 45 | 46 | To avoid console 404 errors, you can use ``data-src`` instead of ``src``. 47 | 48 | ### Programmatic usage 49 | 50 | To programmatically insert a placeholder use the `run()` API: 51 | 52 | ```js 53 | var myImage = document.getElementById('myImage'); 54 | 55 | Holder.run({ 56 | images: myImage 57 | }); 58 | ``` 59 | 60 | ## Placeholder options 61 | 62 | Placeholder options are set through URL properties, e.g. `holder.js/300x200?x=y&a=b`. Multiple options are separated by the `&` character. 63 | 64 | * `theme`: The theme to use for the placeholder. Example: `holder.js/300x200?theme=sky` 65 | * `random`: Use random theme. Example: `holder.js/300x200?random=yes` 66 | * `bg`: Background color. Example: `holder.js/300x200?bg=2a2025` 67 | * `fg`: Foreground (text) color. Example: `holder.js/300x200?fg=ffffff` 68 | * `text`: Custom text. Example: `holder.js/300x200?text=Hello` 69 | * `size`: Custom text size. Defaults to `pt` units. Example: `holder.js/300x200?size=50` 70 | * `font`: Custom text font. Example: `holder.js/300x200?font=Helvetica` 71 | * `align`: Custom text alignment (left, right). Example: `holder.js/300x200?align=left` 72 | * `outline`: Draw outline and diagonals for placeholder. Example: `holder.js/300x200?outline=yes` 73 | * `lineWrap`: Maximum line length to image width ratio. Example: `holder.js/300x200?lineWrap=0.5` 74 | 75 | ### Themes 76 | 77 | ![](http://imsky.github.io/holder/images/holder_sky.png)![](http://imsky.github.io/holder/images/holder_vine.png)![](http://imsky.github.io/holder/images/holder_lava.png) 78 | 79 | Holder includes support for themes, to help placeholders blend in with your layout. 80 | 81 | There are 6 default themes: ``sky``, ``vine``, ``lava``, ``gray``, ``industrial``, and ``social``. 82 | 83 | #### Customizing themes 84 | 85 | Themes have 5 properties: ``fg``, ``bg``, ``size``, ``font`` and ``fontweight``. The ``size`` property specifies the minimum font size for the theme. The ``fontweight`` default value is ``bold``. You can create a sample theme like this: 86 | 87 | ```js 88 | Holder.addTheme("dark", { 89 | bg: "#000", 90 | fg: "#aaa", 91 | size: 11, 92 | font: "Monaco", 93 | fontweight: "normal" 94 | }); 95 | ``` 96 | 97 | If you have a group of placeholders where you'd like to use particular text, you can do so by adding a ``text`` property to the theme: 98 | 99 | ```js 100 | Holder.addTheme("thumbnail", { bg: "#fff", text: "Thumbnail" }); 101 | ``` 102 | 103 | #### Using custom themes 104 | 105 | There are two ways to use custom themes with Holder: 106 | 107 | * Include theme at runtime to render placeholders already using the theme name 108 | * Include theme at any point and re-render placeholders that are using the theme name 109 | 110 | The first approach is the easiest. After you include ``holder.js``, add a ``script`` tag that adds the theme you want: 111 | 112 | ```html 113 | 114 | 119 | ``` 120 | 121 | The second approach requires that you call ``run`` after you add the theme, like this: 122 | 123 | ```js 124 | Holder.addTheme("bright", {bg: "white", fg: "gray", size: 12}).run(); 125 | ``` 126 | 127 | #### Using custom themes and domain on specific images 128 | 129 | You can use Holder in different areas on different images with custom themes: 130 | 131 | ```html 132 | 133 | ``` 134 | 135 | ```js 136 | Holder.run({ 137 | domain: "example.com", 138 | themes: { 139 | "simple": { 140 | bg: "#fff", 141 | fg: "#000", 142 | size: 12 143 | } 144 | }, 145 | images: "#new" 146 | }); 147 | ``` 148 | 149 | #### Inserting an image with custom theme 150 | 151 | You can add a placeholder programmatically by chaining Holder calls: 152 | 153 | ```js 154 | Holder.addTheme("new", { 155 | fg: "#ccc", 156 | bg: "#000", 157 | size: 10 158 | }).addImage("holder.js/200x100?theme=new", "body").run(); 159 | ``` 160 | 161 | The first argument in ``addImage`` is the ``src`` attribute, and the second is a CSS selector of the parent element. 162 | 163 | ### Text 164 | 165 | Holder automatically adds line breaks to text that goes outside of the image boundaries. If the text is so long it goes out of both horizontal and vertical boundaries, the text is moved to the top. If you prefer to control the line breaks, you can add `\n` to the `text` property: 166 | 167 | ```html 168 | 169 | `````` 170 | 171 | If you would like to disable line wrapping, set the `nowrap` option to `true`: 172 | 173 | ```html 174 | 175 | ``` 176 | 177 | When using the `text` option, the image dimesions are not shown. To reinsert the dimension into the text, simple use the special `holder_dimensions` placeholder. 178 | 179 | ```html 180 | 181 | ``` 182 | 183 | Placeholders using a custom font are rendered using canvas by default, due to SVG's constraints on cross-domain resource linking. If you're using only locally available fonts, you can disable this behavior by setting `noFontFallback` to `true` in `Holder.run` options. However, if you need to render a SVG placeholder using an externally loaded font, you have to use the `object` tag instead of the `img` tag and add a `holderjs` class to the appropriate `link` tags. Here's an example: 184 | 185 | ```html 186 | 187 | 188 | 189 | 190 | 191 | ``` 192 | 193 | **Important:** When testing locally, font URLs must have a `http` or `https` protocol defined. 194 | 195 | **Important:** Fonts served from locations other than public registries (i.e. Google Fonts, Typekit, etc.) require the correct CORS headers to be set. See [How to use CDN with Webfonts](https://www.maxcdn.com/one/tutorial/how-to-use-cdn-with-webfonts/) for more details. 196 | 197 | `` placeholders work like `` placeholders, with the added benefit of their DOM being able to be inspected and modified. As with `` placeholders, the `data-src` attribute is more reliable than the `data` attribute. 198 | 199 | ### Fluid placeholders 200 | 201 | **Important:** Percentages are specified with the `p` character, not with the `%` character. 202 | 203 | Specifying a dimension in percentages creates a fluid placeholder that responds to media queries. 204 | 205 | ```html 206 | 207 | ``` 208 | 209 | By default, the fluid placeholder will show its current size in pixels. To display the original dimensions, i.e. 100%x75, set the ``textmode`` flag to ``literal`` like so: `holder.js/100px75?textmode=literal`. 210 | 211 | ### Automatically sized placeholders 212 | 213 | If you'd like to avoid Holder enforcing an image size, use the ``auto`` flag like so: 214 | 215 | ```html 216 | 217 | ``` 218 | 219 | The above will render a placeholder without any embedded CSS for height or width. 220 | 221 | To show the current size of an automatically sized placeholder, set the ``textmode`` flag to ``exact`` like so: `holder.js/200x200?auto=yes&textmode=exact`. 222 | 223 | ### Preventing updates on window resize 224 | 225 | Both fluid placeholders and automatically sized placeholders in exact mode are updated when the window is resized. To set whether or not a particular image is updated on window resize, you can use the `setResizeUpdate` method like so: 226 | 227 | ```js 228 | var img = $('#placeholder').get(0); 229 | Holder.setResizeUpdate(img, false); 230 | ``` 231 | 232 | The above will pause any render updates on the specified image (which must be a DOM object). 233 | 234 | To enable updates again, run the following: 235 | 236 | ```js 237 | Holder.setResizeUpdate(img, true); 238 | ``` 239 | 240 | This will enable updates and immediately render the placeholder. 241 | 242 | ### Background placeholders 243 | 244 | Holder can render placeholders as background images for elements with the `holderjs` class, like this: 245 | 246 | ```css 247 | #sample {background:url(?holder.js/200x200?theme=social) no-repeat} 248 | ``` 249 | 250 | ```html 251 |
252 | ``` 253 | 254 | The Holder URL in CSS should have a `?` in front. Like in image placeholders, you can specify the Holder URL in a `data-background-src` attribute: 255 | 256 | ```html 257 |
258 | ``` 259 | 260 | **Important:** Make sure to define a height and/or width for elements with background placeholders. Fluid background placeholders are not yet supported. 261 | 262 | ## Runtime settings 263 | 264 | Holder provides several options at runtime that affect the process of image generation. These are passed in through `Holder.run` calls. 265 | 266 | * `domain`: The domain to use for image generation. Default value: `holder.js`. 267 | * `dataAttr`: The HTML attribute used to define a fallback to the native `src` attribute. Default value: `data-src`. 268 | * `renderer`: The renderer to use. Options available: `svg`, `canvas`. Default value: `svg`. 269 | * `images`: The CSS selector used for finding `img` tags. Default value: `img`. 270 | * `objects`: The CSS selector used for finding `object` placeholders. Default value: `object`. 271 | * `bgnodes`: The CSS selector used for finding elements that have background palceholders. Default value: `body .holderjs`. 272 | * `stylenodes`: The CSS selector used for finding stylesheets to import into SVG placeholders. Default value: `head link.holderjs`. 273 | * `noFontFallback`: Do not fall back to canvas if using custom fonts. 274 | * `noBackgroundSize`: Do not set `background-size` for background placeholders. 275 | 276 | ### Using custom settings on load 277 | 278 | You can prevent Holder from running its default configuration by executing ``Holder.run`` with your custom settings right after including ``holder.js``. However, you'll have to execute ``Holder.run`` again to render any placeholders that use the default configuration. 279 | 280 | ## Using with [lazyload.js](https://github.com/tuupola/jquery_lazyload) 281 | 282 | 283 | Holder is compatible with ``lazyload.js`` and works with both fluid and fixed-width images. For best results, run `.lazyload({skip_invisible:false})`. 284 | 285 | ## Using with React 286 | 287 | When using Holder in a React component, execute `Holder.run` in `componentDidMount` to enable rendering after state changes. See [this issue](https://github.com/imsky/holder/issues/225) for more details. 288 | 289 | ## Using with Vue 290 | 291 | You can use Holder in Vue 2+ projects with [vue-holderjs](https://github.com/boogermann/vue-holderjs). 292 | 293 | ## Using with Angular.js 294 | 295 | You can use Holder in Angular projects with [ng-holder](https://github.com/joshvillbrandt/ng-holder) or with [angular-2-holderjs](https://github.com/aogriffiths/angular-2-holderjs) for Angular 2 projects. 296 | 297 | ## Using with Meteor 298 | 299 | Because Meteor includes scripts at the top of the document by default, the DOM may not be fully available when Holder is called. For this reason, place Holder-related code in a "DOM ready" event listener. 300 | 301 | ## Using with Webpack 302 | 303 | If you're using `ProvidePlugin` in your Webpack config, make sure to configure it as follows: 304 | 305 | ```js 306 | plugins: [ 307 | new webpack.ProvidePlugin({ 308 | 'Holder': 'holderjs', 309 | 'holder': 'holderjs', 310 | 'window.Holder': 'holderjs' 311 | }) 312 | ] 313 | ``` 314 | 315 | ## Browser support 316 | 317 | * Chrome 318 | * Firefox 3+ 319 | * Safari 4+ 320 | * Internet Explorer 9+ (with partial support for 6-8) 321 | * Opera 12+ 322 | * Android (with fallback) 323 | 324 | ## Source 325 | 326 | * GitHub: 327 | * GitLab: 328 | 329 | ## License 330 | 331 | Holder is provided under the [MIT License](http://opensource.org/licenses/MIT). 332 | 333 | ## Credits 334 | 335 | Holder is a project by [Ivan Malopinsky](http://imsky.co). 336 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "holderjs", 3 | "main": "holder.js", 4 | "license": "MIT" 5 | } 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imsky/holder", 3 | "description": "Client-side image placeholders.", 4 | "homepage": "http://holderjs.com", 5 | "keywords": [ 6 | "images", 7 | "placeholders", 8 | "client-side", 9 | "canvas", 10 | "generation", 11 | "development" 12 | ], 13 | "license": "MIT", 14 | "type": "component", 15 | "authors": [ 16 | { 17 | "name": "Ivan Malopinsky", 18 | "homepage": "http://imsky.co" 19 | } 20 | ], 21 | "require": { 22 | "robloach/component-installer": "*" 23 | }, 24 | "extra": { 25 | "component": { 26 | "scripts": [ 27 | "holder.js" 28 | ], 29 | "files": [ 30 | "holder.min.js" 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var beautify = require('gulp-jsbeautifier'); 2 | var concat = require('gulp-concat'); 3 | var eslint = require('gulp-eslint'); 4 | var gulp = require('gulp'); 5 | var gulputil = require('gulp-util'); 6 | var header = require('gulp-header'); 7 | var rename = require('gulp-rename'); 8 | var replace = require('gulp-replace'); 9 | var todo = require('gulp-todo'); 10 | var uglify = require('gulp-uglify'); 11 | var webpack = require('webpack-stream'); 12 | 13 | var moment = require('moment'); 14 | var pkg = require('./package.json'); 15 | 16 | var banner = 17 | '/*!\n\n' + 18 | '<%= pkg.officialName %> - <%= pkg.summary %>\nVersion <%= pkg.version %>+<%= build %>\n' + 19 | '\u00A9 <%= year %> <%= pkg.author.name %> - <%= pkg.author.url %>\n\n' + 20 | 'Site: <%= pkg.homepage %>\n' + 21 | 'Issues: <%= pkg.bugs.url %>\n' + 22 | 'License: <%= pkg.license %>\n\n' + 23 | '*/\n'; 24 | 25 | function generateBuild() { 26 | var date = new Date; 27 | return Math.floor((date - (new Date(date.getFullYear(), 0, 0))) / 1000).toString(36) 28 | } 29 | 30 | var build = generateBuild(); 31 | 32 | gulp.task('lint', function() { 33 | return gulp.src([ 34 | 'src/lib/*.js', 35 | 'src/lib/renderers/*.js', 36 | 'src/renderers/*.js', 37 | 'src/index.js' 38 | ]) 39 | .pipe(eslint()) 40 | .pipe(eslint.format()) 41 | .pipe(eslint.failAfterError()); 42 | }); 43 | 44 | gulp.task('todo', function() { 45 | return gulp.src([ 46 | 'src/lib/*.js', 47 | 'src/lib/renderers/*.js', 48 | 'src/renderers/*.js', 49 | 'src/index.js' 50 | ]) 51 | .pipe(todo()) 52 | .pipe(gulp.dest('./')); 53 | }); 54 | 55 | gulp.task('build', ['lint'], function() { 56 | return gulp.src('src/index.js') 57 | .pipe(webpack({ 58 | output: { 59 | library: 'Holder', 60 | filename: 'holder.js', 61 | libraryTarget: 'umd' 62 | } 63 | })) 64 | .pipe(gulp.dest('./')); 65 | }); 66 | 67 | gulp.task('bundle', ['build'], function() { 68 | return gulp.src([ 69 | 'src/lib/vendor/polyfills.js', 70 | 'holder.js', 71 | 'src/meteor/shim.js' 72 | ]) 73 | .pipe(concat('holder.js')) 74 | .pipe(gulp.dest('./')); 75 | }); 76 | 77 | gulp.task('minify', ['bundle'], function() { 78 | return gulp.src('holder.js') 79 | .pipe(uglify()) 80 | .pipe(rename('holder.min.js')) 81 | .pipe(gulp.dest('./')); 82 | }); 83 | 84 | gulp.task('banner', ['minify'], function() { 85 | return gulp.src(['holder*.js']) 86 | .pipe(replace('%version%', pkg.version)) 87 | .pipe(header(banner, { 88 | pkg: pkg, 89 | year: moment().format('YYYY'), 90 | build: build 91 | })) 92 | .pipe(gulp.dest('./')); 93 | }); 94 | 95 | gulp.task('beautify', function() { 96 | return gulp.src(['src/lib/*.js']) 97 | .pipe(beautify()) 98 | .pipe(gulp.dest('src/lib/')); 99 | }); 100 | 101 | gulp.task('meteor', function() { 102 | return gulp.src('src/meteor/package.js') 103 | .pipe(replace('%version%', pkg.version)) 104 | .pipe(replace('%summary%', pkg.description)) 105 | .pipe(gulp.dest('./')); 106 | }); 107 | 108 | gulp.task('watch', function() { 109 | gulp.watch('src/*.js', ['default']); 110 | }); 111 | 112 | gulp.task('default', ['bundle', 'minify', 'banner', 'meteor'], function() { 113 | gulputil.log('Finished build ' + build); 114 | build = generateBuild(); 115 | }); 116 | -------------------------------------------------------------------------------- /holder.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | Holder - client side image placeholders 4 | Version 2.9.9+jl7z 5 | © 2021 Ivan Malopinsky - https://imsky.co 6 | 7 | Site: http://holderjs.com 8 | Issues: https://github.com/imsky/holder/issues 9 | License: MIT 10 | 11 | */ 12 | !function(e){if(e.document){var t=e.document;t.querySelectorAll||(t.querySelectorAll=function(n){var r,i=t.createElement("style"),o=[];for(t.documentElement.firstChild.appendChild(i),t._qsa=[],i.styleSheet.cssText=n+"{x-qsa:expression(document._qsa && document._qsa.push(this))}",e.scrollBy(0,0),i.parentNode.removeChild(i);t._qsa.length;)r=t._qsa.shift(),r.style.removeAttribute("x-qsa"),o.push(r);return t._qsa=null,o}),t.querySelector||(t.querySelector=function(e){var n=t.querySelectorAll(e);return n.length?n[0]:null}),t.getElementsByClassName||(t.getElementsByClassName=function(e){return e=String(e).replace(/^|\s+/g,"."),t.querySelectorAll(e)}),Object.keys||(Object.keys=function(e){if(e!==Object(e))throw TypeError("Object.keys called on non-object");var t,n=[];for(t in e)Object.prototype.hasOwnProperty.call(e,t)&&n.push(t);return n}),Array.prototype.forEach||(Array.prototype.forEach=function(e){if(void 0===this||null===this)throw TypeError();var t=Object(this),n=t.length>>>0;if("function"!=typeof e)throw TypeError();var r,i=arguments[1];for(r=0;n>r;r++)r in t&&e.call(i,t[r],r,t)}),function(e){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";e.atob=e.atob||function(e){e=String(e);var n,r=0,i=[],o=0,a=0;if(e=e.replace(/\s/g,""),e.length%4===0&&(e=e.replace(/=+$/,"")),e.length%4===1)throw Error("InvalidCharacterError");if(/[^+\/0-9A-Za-z]/.test(e))throw Error("InvalidCharacterError");for(;r>16&255)),i.push(String.fromCharCode(o>>8&255)),i.push(String.fromCharCode(255&o)),a=0,o=0),r+=1;return 12===a?(o>>=4,i.push(String.fromCharCode(255&o))):18===a&&(o>>=2,i.push(String.fromCharCode(o>>8&255)),i.push(String.fromCharCode(255&o))),i.join("")},e.btoa=e.btoa||function(e){e=String(e);var n,r,i,o,a,s,l,u=0,c=[];if(/[^\x00-\xFF]/.test(e))throw Error("InvalidCharacterError");for(;u>2,a=(3&n)<<4|r>>4,s=(15&r)<<2|i>>6,l=63&i,u===e.length+2?(s=64,l=64):u===e.length+1&&(l=64),c.push(t.charAt(o),t.charAt(a),t.charAt(s),t.charAt(l));return c.join("")}}(e),Object.prototype.hasOwnProperty||(Object.prototype.hasOwnProperty=function(e){var t=this.__proto__||this.constructor.prototype;return e in this&&(!(e in t)||t[e]!==this[e])}),function(){if("performance"in e==!1&&(e.performance={}),Date.now=Date.now||function(){return(new Date).getTime()},"now"in e.performance==!1){var t=Date.now();performance.timing&&performance.timing.navigationStart&&(t=performance.timing.navigationStart),e.performance.now=function(){return Date.now()-t}}}(),e.requestAnimationFrame||(e.webkitRequestAnimationFrame&&e.webkitCancelAnimationFrame?!function(e){e.requestAnimationFrame=function(t){return webkitRequestAnimationFrame(function(){t(e.performance.now())})},e.cancelAnimationFrame=e.webkitCancelAnimationFrame}(e):e.mozRequestAnimationFrame&&e.mozCancelAnimationFrame?!function(e){e.requestAnimationFrame=function(t){return mozRequestAnimationFrame(function(){t(e.performance.now())})},e.cancelAnimationFrame=e.mozCancelAnimationFrame}(e):!function(e){e.requestAnimationFrame=function(t){return e.setTimeout(t,1e3/60)},e.cancelAnimationFrame=e.clearTimeout}(e))}}(this),function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Holder=t():e.Holder=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={exports:{},id:r,loaded:!1};return e[r].call(i.exports,i,i.exports,t),i.loaded=!0,i.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){(function(t){function r(e,t,n,r){var a=i(n.substr(n.lastIndexOf(e.domain)),e);a&&o({mode:null,el:r,flags:a,engineSettings:t})}function i(e,t){var n={theme:j(z.settings.themes.gray,null),stylesheets:t.stylesheets,instanceOptions:t},r=e.indexOf("?"),i=[e];-1!==r&&(i=[e.slice(0,r),e.slice(r+1)]);var o=i[0].split("/");n.holderURL=e;var a=o[1],s=a.match(/([\d]+p?)x([\d]+p?)/);if(!s)return!1;if(n.fluid=-1!==a.indexOf("p"),n.dimensions={width:s[1].replace("p","%"),height:s[2].replace("p","%")},2===i.length){var l=y.parse(i[1]);if(b.truthy(l.ratio)){n.fluid=!0;var u=parseFloat(n.dimensions.width.replace("%","")),c=parseFloat(n.dimensions.height.replace("%",""));c=Math.floor(100*(c/u)),u=100,n.dimensions.width=u+"%",n.dimensions.height=c+"%"}if(n.auto=b.truthy(l.auto),l.bg&&(n.theme.bg=b.parseColor(l.bg)),l.fg&&(n.theme.fg=b.parseColor(l.fg)),l.bg&&!l.fg&&(n.autoFg=!0),l.theme&&Object.prototype.hasOwnProperty.call(n.instanceOptions.themes,l.theme)&&(n.theme=j(n.instanceOptions.themes[l.theme],null)),l.text&&(n.text=l.text),l.textmode&&(n.textmode=l.textmode),l.size&&parseFloat(l.size)&&(n.size=parseFloat(l.size)),null!=l.fixedSize&&(n.fixedSize=b.truthy(l.fixedSize)),l.font&&(n.font=l.font),l.align&&(n.align=l.align),l.lineWrap&&(n.lineWrap=l.lineWrap),n.nowrap=b.truthy(l.nowrap),n.outline=b.truthy(l.outline),b.truthy(l.random)){z.vars.cache.themeKeys=z.vars.cache.themeKeys||Object.keys(n.instanceOptions.themes);var f=z.vars.cache.themeKeys[0|Math.random()*z.vars.cache.themeKeys.length];n.theme=j(n.instanceOptions.themes[f],null)}}return n}function o(e){var t=e.mode,n=e.el,r=e.flags,i=e.engineSettings,o=r.dimensions,s=r.theme,l=o.width+"x"+o.height;t=null==t?r.fluid?"fluid":"image":t;var f=/holder_([a-z]+)/g,d=!1;if(null!=r.text&&(s.text=r.text,"object"===n.nodeName.toLowerCase())){for(var h=s.text.split("\\n"),p=0;p1){var x,O=0,A=0,E=0;w=new u.Group("line"+E),("left"===e.align||"right"===e.align)&&(a=e.width*(1-2*(1-i)));for(var j=0;j=a||C===!0)&&(t(m,w,O,m.properties.leading),m.add(w),O=0,A+=m.properties.leading,E+=1,w=new u.Group("line"+E),w.y=A),C!==!0&&(b.moveTo(O,0),O+=y.spaceWidth+T.width,w.add(b))}if(t(m,w,O,m.properties.leading),m.add(w),"left"===e.align)m.moveTo(e.width-o,null,null);else if("right"===e.align){for(x in m.children)w=m.children[x],w.moveTo(e.width-w.width,null,null);m.moveTo(0-(e.width-o),null,null)}else{for(x in m.children)w=m.children[x],w.moveTo((m.width-w.width)/2,null,null);m.moveTo((e.width-m.width)/2,null,null)}m.moveTo(null,(e.height-m.height)/2,null),(e.height-m.height)/2<0&&m.moveTo(null,0,null)}else b=new u.Text(e.text),w=new u.Group("line0"),w.add(b),m.add(w),"left"===e.align?m.moveTo(e.width-o,null,null):"right"===e.align?m.moveTo(0-(e.width-o),null,null):m.moveTo((e.width-y.boundingBox.width)/2,null,null),m.moveTo(null,(e.height-y.boundingBox.height)/2,null);return s}function l(e,t,n,r){var i=parseInt(e,10),o=parseInt(t,10),a=Math.max(i,o),s=Math.min(i,o),l=.8*Math.min(s,a*r);return Math.round(Math.max(n,l))}function u(e){var t;t=null==e||null==e.nodeType?z.vars.resizableImages:[e];for(var n=0,r=t.length;r>n;n++){var i=t[n];if(i.holderData){var o=i.holderData.flags,s=T(i);if(s){if(!i.holderData.resizeUpdate)continue;if(o.fluid&&o.auto){var l=i.holderData.fluidConfig;switch(l.mode){case"width":s.height=s.width/l.ratio;break;case"height":s.width=s.height*l.ratio}}var u={mode:"image",holderSettings:{dimensions:s,theme:o.theme,flags:o},el:i,engineSettings:i.holderData.engineSettings};"exact"==o.textmode&&(o.exactDimensions=s,u.holderSettings.dimensions=o.dimensions),a(u)}else h(i)}}}function c(e){if(e.holderData){var t=T(e);if(t){var n=e.holderData.flags,r={fluidHeight:"%"==n.dimensions.height.slice(-1),fluidWidth:"%"==n.dimensions.width.slice(-1),mode:null,initialDimensions:t};r.fluidWidth&&!r.fluidHeight?(r.mode="width",r.ratio=r.initialDimensions.width/parseFloat(n.dimensions.height)):!r.fluidWidth&&r.fluidHeight&&(r.mode="height",r.ratio=parseFloat(n.dimensions.width)/r.initialDimensions.height),e.holderData.fluidConfig=r}else h(e)}}function f(){var e,n=[],r=Object.keys(z.vars.invisibleImages);r.forEach(function(t){e=z.vars.invisibleImages[t],T(e)&&"img"==e.nodeName.toLowerCase()&&(n.push(e),delete z.vars.invisibleImages[t])}),n.length&&k.run({images:n}),setTimeout(function(){t.requestAnimationFrame(f)},10)}function d(){z.vars.visibilityCheckStarted||(t.requestAnimationFrame(f),z.vars.visibilityCheckStarted=!0)}function h(e){e.holderData.invisibleId||(z.vars.invisibleId+=1,z.vars.invisibleImages["i"+z.vars.invisibleId]=e,e.holderData.invisibleId=z.vars.invisibleId)}function p(e){z.vars.debounceTimer||e.call(this),z.vars.debounceTimer&&t.clearTimeout(z.vars.debounceTimer),z.vars.debounceTimer=t.setTimeout(function(){z.vars.debounceTimer=null,e.call(this)},z.setup.debounce)}function g(){p(function(){u(null)})}var m=n(2),y=n(3),v=n(6),b=n(7),w=n(8),x=n(9),S=n(10),O=n(11),A=n(12),E=n(27),j=b.extend,T=b.dimensionCheck,C=O.svg_ns,k={version:O.version,addTheme:function(e,t){return null!=e&&null!=t&&(z.settings.themes[e]=t),delete z.vars.cache.themeKeys,this},addImage:function(e,t){var n=x.getNodeArray(t);return n.forEach(function(t){var n=x.newEl("img"),r={};r[z.setup.dataAttr]=e,x.setAttr(n,r),t.appendChild(n)}),this},setResizeUpdate:function(e,t){e.holderData&&(e.holderData.resizeUpdate=!!t,e.holderData.resizeUpdate&&u(e))},run:function(e){e=e||{};var n={},a=j(z.settings,e);z.vars.preempted=!0,z.vars.dataAttr=a.dataAttr||z.setup.dataAttr,n.renderer=a.renderer?a.renderer:z.setup.renderer,-1===z.setup.renderers.join(",").indexOf(n.renderer)&&(n.renderer=z.setup.supportsSVG?"svg":z.setup.supportsCanvas?"canvas":"html");var s=x.getNodeArray(a.images),l=x.getNodeArray(a.bgnodes),u=x.getNodeArray(a.stylenodes),c=x.getNodeArray(a.objects);return n.stylesheets=[],n.svgXMLStylesheet=!0,n.noFontFallback=!!a.noFontFallback,n.noBackgroundSize=!!a.noBackgroundSize,u.forEach(function(e){if(e.attributes.rel&&e.attributes.href&&"stylesheet"==e.attributes.rel.value){var t=e.attributes.href.value,r=x.newEl("a");r.href=t;var i=r.protocol+"//"+r.host+r.pathname+r.search;n.stylesheets.push(i)}}),l.forEach(function(e){if(t.getComputedStyle){var r=t.getComputedStyle(e,null).getPropertyValue("background-image"),s=e.getAttribute("data-background-src"),l=s||r,u=null,c=a.domain+"/",f=l.indexOf(c);if(0===f)u=l;else if(1===f&&"?"===l[0])u=l.slice(1);else{var d=l.substr(f).match(/([^"]*)"?\)/);if(null!==d)u=d[1];else if(0===l.indexOf("url("))throw"Holder: unable to parse background URL: "+l}if(u){var h=i(u,a);h&&o({mode:"background",el:e,flags:h,engineSettings:n})}}}),c.forEach(function(e){var t={};try{t.data=e.getAttribute("data"),t.dataSrc=e.getAttribute(z.vars.dataAttr)}catch(i){t.error=i}var o=null!=t.data&&0===t.data.indexOf(a.domain),s=null!=t.dataSrc&&0===t.dataSrc.indexOf(a.domain);o?r(a,n,t.data,e):s&&r(a,n,t.dataSrc,e)}),s.forEach(function(e){var t={};try{t.src=e.getAttribute("src"),t.dataSrc=e.getAttribute(z.vars.dataAttr),t.rendered=e.getAttribute("data-holder-rendered")}catch(i){t.error=i}var o=null!=t.src,s=null!=t.dataSrc&&0===t.dataSrc.indexOf(a.domain),l=null!=t.rendered&&"true"==t.rendered;o?0===t.src.indexOf(a.domain)?r(a,n,t.src,e):s&&(l?r(a,n,t.dataSrc,e):!function(e,t,n,i,o){b.imageExists(e,function(e){e||r(t,n,i,o)})}(t.src,a,n,t.dataSrc,e)):s&&r(a,n,t.dataSrc,e)}),this}},z={settings:{domain:"holder.js",images:"img",objects:"object",bgnodes:"body .holderjs",stylenodes:"head link.holderjs",themes:{gray:{bg:"#EEEEEE",fg:"#AAAAAA"},social:{bg:"#3a5a97",fg:"#FFFFFF"},industrial:{bg:"#434A52",fg:"#C2F200"},sky:{bg:"#0D8FDB",fg:"#FFFFFF"},vine:{bg:"#39DBAC",fg:"#1E292C"},lava:{bg:"#F8591A",fg:"#1C2846"}}},defaults:{size:10,units:"pt",scale:1/16}},M=function(){var e=null,t=null,n=null;return function(r){var i=r.root;if(z.setup.supportsSVG){var o=!1,a=function(e){return document.createTextNode(e)};(null==e||e.parentNode!==document.body)&&(o=!0),e=w.initSVG(e,i.properties.width,i.properties.height),e.style.display="block",o&&(t=x.newEl("text",C),n=a(null),x.setAttr(t,{x:0}),t.appendChild(n),e.appendChild(t),document.body.appendChild(e),e.style.visibility="hidden",e.style.position="absolute",e.style.top="-100%",e.style.left="-100%");var s=i.children.holderTextGroup,l=s.properties;x.setAttr(t,{y:l.font.size,style:b.cssProps({"font-weight":l.font.weight,"font-size":l.font.size+l.font.units,"font-family":l.font.family})});var u=x.newEl("textarea");u.innerHTML=l.text,n.nodeValue=u.value;var c=t.getBBox(),f=Math.ceil(c.width/i.properties.width),d=l.text.split(" "),h=l.text.match(/\\n/g);f+=null==h?0:h.length,n.nodeValue=l.text.replace(/[ ]+/g,"");var p=t.getComputedTextLength(),g=c.width-p,m=Math.round(g/Math.max(1,d.length-1)),y=[];if(f>1){n.nodeValue="";for(var v=0;v=0?t:1)}function o(e){x?i(e):S.push(e)}null==document.readyState&&document.addEventListener&&(document.addEventListener("DOMContentLoaded",function A(){document.removeEventListener("DOMContentLoaded",A,!1),document.readyState="complete"},!1),document.readyState="loading");var a=e.document,s=a.documentElement,l="load",u=!1,c="on"+l,f="complete",d="readyState",h="attachEvent",p="detachEvent",g="addEventListener",m="DOMContentLoaded",y="onreadystatechange",v="removeEventListener",b=g in a,w=u,x=u,S=[];if(a[d]===f)i(t);else if(b)a[g](m,n,u),e[g](l,n,u);else{a[h](y,n),e[h](c,n);try{w=null==e.frameElement&&s}catch(O){}w&&w.doScroll&&!function E(){if(!x){try{w.doScroll("left")}catch(e){return i(E,50)}r(),t()}}()}return o.version="1.4.0",o.isReady=function(){return x},o}e.exports="undefined"!=typeof window&&n(window)},function(e,t,n){var r=encodeURIComponent,i=decodeURIComponent,o=n(4),a=n(5),s=/(\w+)\[(\d+)\]/,l=/\w+\.\w+/;t.parse=function(e){if("string"!=typeof e)return{};if(e=o(e),""===e)return{};"?"===e.charAt(0)&&(e=e.slice(1));for(var t={},n=e.split("&"),r=0;r=0;r--)n=e.charCodeAt(r),t.unshift(n>128?["&#",n,";"].join(""):e[r]);return t.join("")},t.imageExists=function(e,t){var n=new Image;n.onerror=function(){t.call(this,!1)},n.onload=function(){t.call(this,!0)},n.src=e},t.decodeHtmlEntity=function(e){return e.replace(/&#(\d+);/g,function(e,t){return String.fromCharCode(t)})},t.dimensionCheck=function(e){var t={height:e.clientHeight,width:e.clientWidth};return t.height&&t.width?t:!1},t.truthy=function(e){return"string"==typeof e?"true"===e||"yes"===e||"1"===e||"on"===e||"✓"===e:!!e},t.parseColor=function(e){var t,n=/(^(?:#?)[0-9a-f]{6}$)|(^(?:#?)[0-9a-f]{3}$)/i,r=/^rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/,i=/^rgba\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0*\.\d{1,}|1)\)$/,o=e.match(n);if(null!==o)return t=o[1]||o[2],"#"!==t[0]?"#"+t:t;if(o=e.match(r),null!==o)return t="rgb("+o.slice(1).join(",")+")";if(o=e.match(i),null!==o){var a=function(e){return"0."+e.split(".")[1]},s=o.slice(1).map(function(e,t){return 3===t?a(e):e});return t="rgba("+s.join(",")+")"}return null},t.canvasRatio=function(){var t=1,n=1;if(e.document){var r=e.document.createElement("canvas");if(r.getContext){var i=r.getContext("2d");t=e.devicePixelRatio||1,n=i.webkitBackingStorePixelRatio||i.mozBackingStorePixelRatio||i.msBackingStorePixelRatio||i.oBackingStorePixelRatio||i.backingStorePixelRatio||1}}return t/n}}).call(t,function(){return this}())},function(e,t,n){(function(e){var r=n(9),i="http://www.w3.org/2000/svg",o=8;t.initSVG=function(e,t,n){var a,s,l=!1;e&&e.querySelector?(s=e.querySelector("style"),null===s&&(l=!0)):(e=r.newEl("svg",i),l=!0),l&&(a=r.newEl("defs",i),s=r.newEl("style",i),r.setAttr(s,{type:"text/css"}),a.appendChild(s),e.appendChild(a)),e.webkitMatchesSelector&&e.setAttribute("xmlns",i);for(var u=0;u=0;l--){var u=s.createProcessingInstruction("xml-stylesheet",'href="'+a[l]+'" rel="stylesheet"');s.insertBefore(u,s.firstChild)}s.removeChild(s.documentElement),o=i.serializeToString(s)}var c=i.serializeToString(t);return c=c.replace(/&(#[0-9]{2,};)/g,"&$1"),o+c}}}).call(t,function(){return this}())},function(e,t){(function(e){t.newEl=function(t,n){return e.document?null==n?e.document.createElement(t):e.document.createElementNS(n,t):void 0},t.setAttr=function(e,t){for(var n in t)e.setAttribute(n,t[n])},t.createXML=function(){return e.DOMParser?(new DOMParser).parseFromString("","application/xml"):void 0},t.getNodeArray=function(t){var n=null;return"string"==typeof t?n=document.querySelectorAll(t):e.NodeList&&t instanceof e.NodeList?n=t:e.Node&&t instanceof e.Node?n=[t]:e.HTMLCollection&&t instanceof e.HTMLCollection?n=t:t instanceof Array?n=t:null===t&&(n=[]),n=Array.prototype.slice.call(n)}}).call(t,function(){return this}())},function(e,t){var n=function(e,t){"string"==typeof e&&(this.original=e,"#"===e.charAt(0)&&(e=e.slice(1)),/[^a-f0-9]+/i.test(e)||(3===e.length&&(e=e.replace(/./g,"$&$&")),6===e.length&&(this.alpha=1,t&&t.alpha&&(this.alpha=t.alpha),this.set(parseInt(e,16)))))};n.rgb2hex=function(e,t,n){function r(e){var t=(0|e).toString(16);return 16>e&&(t="0"+t),t}return[e,t,n].map(r).join("")},n.hsl2rgb=function(e,t,n){var r=e/60,i=(1-Math.abs(2*n-1))*t,o=i*(1-Math.abs(parseInt(r)%2-1)),a=n-i/2,s=0,l=0,u=0;return r>=0&&1>r?(s=i,l=o):r>=1&&2>r?(s=o,l=i):r>=2&&3>r?(l=i,u=o):r>=3&&4>r?(l=o,u=i):r>=4&&5>r?(s=o,u=i):r>=5&&6>r&&(s=i,u=o),s+=a,l+=a,u+=a,s=parseInt(255*s),l=parseInt(255*l),u=parseInt(255*u),[s,l,u]},n.prototype.set=function(e){this.raw=e;var t=(16711680&this.raw)>>16,n=(65280&this.raw)>>8,r=255&this.raw,i=.2126*t+.7152*n+.0722*r,o=-.09991*t-.33609*n+.436*r,a=.615*t-.55861*n-.05639*r;return this.rgb={r:t,g:n,b:r},this.yuv={y:i,u:o,v:a},this},n.prototype.lighten=function(e){var t=Math.min(1,Math.max(0,Math.abs(e)))*(0>e?-1:1),r=255*t|0,i=Math.min(255,Math.max(0,this.rgb.r+r)),o=Math.min(255,Math.max(0,this.rgb.g+r)),a=Math.min(255,Math.max(0,this.rgb.b+r)),s=n.rgb2hex(i,o,a);return new n(s)},n.prototype.toHex=function(e){return(e?"#":"")+this.raw.toString(16)},n.prototype.lighterThan=function(e){return e instanceof n||(e=new n(e)),this.yuv.y>e.yuv.y},n.prototype.blendAlpha=function(e){e instanceof n||(e=new n(e));var t=e,r=this,i=t.alpha*t.rgb.r+(1-t.alpha)*r.rgb.r,o=t.alpha*t.rgb.g+(1-t.alpha)*r.rgb.g,a=t.alpha*t.rgb.b+(1-t.alpha)*r.rgb.b;return new n(n.rgb2hex(i,o,a))},e.exports=n},function(e,t){e.exports={version:"2.9.9",svg_ns:"http://www.w3.org/2000/svg"}},function(e,t,n){function r(e,t){return f.element({tag:t,width:e.width,height:e.height,fill:e.properties.fill})}function i(e){return u.cssProps({fill:e.fill,"font-weight":e.font.weight,"font-family":e.font.family+", monospace","font-size":e.font.size+e.font.units})}function o(e,t,n){var r=n/2;return["M",r,r,"H",e-r,"V",t-r,"H",r,"V",0,"M",0,r,"L",e,t-r,"M",0,t-r,"L",e,r].join(" ")}var a=n(13)["default"],s=n(8),l=n(11),u=n(7),c=l.svg_ns,f={element:function(e){var t=e.tag,n=e.content||"";return delete e.tag,delete e.content,[t,n,e]}};e.exports=function(e,t){var n=t.engineSettings,l=n.stylesheets,u=l.map(function(e){return''}).join("\n"),d="holder_"+Number(new Date).toString(16),h=e.root,p=h.children.holderTextGroup,g="#"+d+" text { "+i(p.properties)+" } ";p.y+=.8*p.textPositionData.boundingBox.height;var m=[];Object.keys(p.children).forEach(function(e){var t=p.children[e];Object.keys(t.children).forEach(function(e){var n=t.children[e],r=p.x+t.x+n.x,i=p.y+t.y+n.y,o=f.element({tag:"text",content:n.properties.text,x:r,y:i});m.push(o)})});var y=f.element({tag:"g",content:m}),v=null;if(h.children.holderBg.properties.outline){var b=h.children.holderBg.properties.outline;v=f.element({tag:"path",d:o(h.children.holderBg.width,h.children.holderBg.height,b.width),"stroke-width":b.width,stroke:b.fill,fill:"none"})}var w=r(h.children.holderBg,"rect"),x=[];x.push(w),b&&x.push(v),x.push(y);var S=f.element({tag:"g",id:d,content:x}),O=f.element({tag:"style",content:g,type:"text/css"}),A=f.element({tag:"defs",content:O}),E=f.element({tag:"svg",content:[A,S],width:h.properties.width,height:h.properties.height,xmlns:c,viewBox:[0,0,h.properties.width,h.properties.height].join(" "),preserveAspectRatio:"none"}),j=String(a(E));/&(x)?#[0-9A-Fa-f]/.test(j[0])&&(j=j.replace(/&#/gm,"&#")),j=u+j;var T=s.svgStringToDataURI(j,"background"===t.mode);return T}},function(e,t,n){e.exports=n(14)},function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function i(e){return e&&e.__esModule?e:{"default":e}}function o(e){function t(e){var t=l["default"](e),n={tag:t.tag,attr:{},children:[]};return t.id&&(n.attr.id=t.id,m["default"](!o.returnObject.ids.hasOwnProperty(t.id),'Ids must be unique and "'+t.id+'" is already assigned'),o.returnObject.ids[t.id]=n),t["class"]&&(n.attr["class"]=t["class"]),t.reference&&(m["default"](!o.returnObject.ids.hasOwnProperty(t.reference),'References must be unique and "'+t.id+'" is already assigned'),o.returnObject.references[t.reference]=n),o.escapeHTML=null!=t.escapeHTML?t.escapeHTML:o.escapeHTML,n}function n(e){if(Array.isArray(e)&&0===e.length)return{};var r=1,i=void 0,s=["area","base","br","col","command","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"],l=e.slice(0);if("string"==typeof l[0])l[0]=t(l[0]);else{if(!Array.isArray(l[0]))throw new Error("First element of array must be a string, or an array and not "+JSON.stringify(l[0]));r=0}for(;r]/.test(g))&&(m=o.quotationMark+g+o.quotationMark),d+=" "+h+"="+m}d+=">",-1===s.indexOf(l[0].tag)&&(l[0].children.forEach(function(e){return d+=e}),d+=""),l[0]=d}return o.returnObject[0]=l[0],o.returnObject.rootElement=l[0],o.returnObject.toString=function(){return l[0]},i&&i(l[0]),o.returnObject}var r=Array.isArray(e),i="undefined"==typeof e?"undefined":a(e);if(!r&&"object"!==i)throw new Error("Argument must be either an array or an object and not "+JSON.stringify(e));if(r&&0===e.length)return{};var o={},s=[];return Array.isArray(e)?s=e.slice(0):(s=e.elementArray.slice(0),o=Object.assign(o,e),delete o.elementArray),o=Object.assign({},d["default"],o,{returnObject:{ids:{},references:{}}}),n(s)}Object.defineProperty(t,"__esModule",{value:!0});var a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e; 13 | 14 | };t["default"]=o;var s=n(15),l=i(s),u=n(16),c=r(u),f=n(17),d=i(f),h=n(18),p=i(h),g=n(21),m=i(g);o.setDefaults=function(e){return Object.assign(d["default"],e),o}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=function(e){var t=e.match(/^[\w-]+/),n={tag:t?t[0]:"div"},r=e.match(/#([\w-]+)/),i=e.match(/\.[\w-]+/g),o=e.match(/\$([\w-]+)/);return r&&(n.id=r[1]),i&&(n["class"]=i.join(" ").replace(/\./g,"")),o&&(n.reference=o[1]),(e.endsWith("&")||e.endsWith("!"))&&(n.escapeHTML=!1),n}},function(e,t){"use strict";function n(e){return e||0===e?String(e).replace(/&/g,"&").replace(/"/g,"""):""}function r(e){return String(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}Object.defineProperty(t,"__esModule",{value:!0}),t.attribute=n,t.HTML=r},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t["default"]={namespace:"xhtml",autoNamespacing:!0,escapeHTML:!0,quotationMark:'"',quoteAttributes:!0,convertTransformArray:!0}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=n(19),a=r(o),s=n(20),l=r(s);t["default"]=function(e,t){return void 0===t?"":"style"===e&&"object"===("undefined"==typeof t?"undefined":i(t))?l["default"](t):"transform"===e&&Array.isArray(t)?a["default"](t):t}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=function(e){return e.map(function(e){var t=[];return"rotate"===e.type&&e.degrees&&t.push(e.degrees),e.x&&t.push(e.x),e.y&&t.push(e.y),e.type+"("+t+")"}).join(" ")}},function(e,t){"use strict";function n(e,t){return null!==t&&t!==!1&&void 0!==t?"string"==typeof t||"object"===("undefined"==typeof t?"undefined":r(t))?t:String(t):void 0}Object.defineProperty(t,"__esModule",{value:!0});var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};t["default"]=function(e){return JSON.stringify(e,n).slice(2,-2).replace(/","/g,";").replace(/":"/g,":").replace(/\\"/g,"'")}},function(e,t,n){(function(t){"use strict";function r(e,t){if(e===t)return 0;for(var n=e.length,r=t.length,i=0,o=Math.min(n,r);o>i;++i)if(e[i]!==t[i]){n=e[i],r=t[i];break}return r>n?-1:n>r?1:0}function i(e){return t.Buffer&&"function"==typeof t.Buffer.isBuffer?t.Buffer.isBuffer(e):!(null==e||!e._isBuffer)}function o(e){return Object.prototype.toString.call(e)}function a(e){return i(e)?!1:"function"!=typeof t.ArrayBuffer?!1:"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e?e instanceof DataView?!0:e.buffer&&e.buffer instanceof ArrayBuffer?!0:!1:!1}function s(e){if(S.isFunction(e)){if(E)return e.name;var t=e.toString(),n=t.match(T);return n&&n[1]}}function l(e,t){return"string"==typeof e?e.length=0;s--)if(l[s]!==u[s])return!1;for(s=l.length-1;s>=0;s--)if(a=l[s],!h(e[a],t[a],n,r))return!1;return!0}function m(e,t,n){h(e,t,!0)&&f(e,t,n,"notDeepStrictEqual",m)}function y(e,t){if(!e||!t)return!1;if("[object RegExp]"==Object.prototype.toString.call(t))return t.test(e);try{if(e instanceof t)return!0}catch(n){}return Error.isPrototypeOf(t)?!1:t.call({},e)===!0}function v(e){var t;try{e()}catch(n){t=n}return t}function b(e,t,n,r){var i;if("function"!=typeof t)throw new TypeError('"block" argument must be a function');"string"==typeof n&&(r=n,n=null),i=v(t),r=(n&&n.name?" ("+n.name+").":".")+(r?" "+r:"."),e&&!i&&f(i,n,"Missing expected exception"+r);var o="string"==typeof r,a=!e&&S.isError(i),s=!e&&i&&!n;if((a&&o&&y(i,n)||s)&&f(i,n,"Got unwanted exception"+r),e&&i&&n&&!y(i,n)||!e&&i)throw i}function w(e,t){e||f(e,!0,t,"==",w)}var x=n(22),S=n(23),O=Object.prototype.hasOwnProperty,A=Array.prototype.slice,E=function(){return"foo"===function(){}.name}(),j=e.exports=d,T=/\s*function\s+([^\(\s]*)\s*/;j.AssertionError=function(e){this.name="AssertionError",this.actual=e.actual,this.expected=e.expected,this.operator=e.operator,e.message?(this.message=e.message,this.generatedMessage=!1):(this.message=c(this),this.generatedMessage=!0);var t=e.stackStartFunction||f;if(Error.captureStackTrace)Error.captureStackTrace(this,t);else{var n=new Error;if(n.stack){var r=n.stack,i=s(t),o=r.indexOf("\n"+i);if(o>=0){var a=r.indexOf("\n",o+1);r=r.substring(a+1)}this.stack=r}}},S.inherits(j.AssertionError,Error),j.fail=f,j.ok=d,j.equal=function(e,t,n){e!=t&&f(e,t,n,"==",j.equal)},j.notEqual=function(e,t,n){e==t&&f(e,t,n,"!=",j.notEqual)},j.deepEqual=function(e,t,n){h(e,t,!1)||f(e,t,n,"deepEqual",j.deepEqual)},j.deepStrictEqual=function(e,t,n){h(e,t,!0)||f(e,t,n,"deepStrictEqual",j.deepStrictEqual)},j.notDeepEqual=function(e,t,n){h(e,t,!1)&&f(e,t,n,"notDeepEqual",j.notDeepEqual)},j.notDeepStrictEqual=m,j.strictEqual=function(e,t,n){e!==t&&f(e,t,n,"===",j.strictEqual)},j.notStrictEqual=function(e,t,n){e===t&&f(e,t,n,"!==",j.notStrictEqual)},j["throws"]=function(e,t,n){b(!0,e,t,n)},j.doesNotThrow=function(e,t,n){b(!1,e,t,n)},j.ifError=function(e){if(e)throw e},j.strict=x(w,j,{equal:j.strictEqual,deepEqual:j.deepStrictEqual,notEqual:j.notStrictEqual,notDeepEqual:j.notDeepStrictEqual}),j.strict.strict=j.strict;var C=Object.keys||function(e){var t=[];for(var n in e)O.call(e,n)&&t.push(n);return t}}).call(t,function(){return this}())},function(e,t){"use strict";function n(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}function r(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;10>n;n++)t["_"+String.fromCharCode(n)]=n;var r=Object.getOwnPropertyNames(t).map(function(e){return t[e]});if("0123456789"!==r.join(""))return!1;var i={};return"abcdefghijklmnopqrst".split("").forEach(function(e){i[e]=e}),"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},i)).join("")?!1:!0}catch(o){return!1}}var i=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;e.exports=r()?Object.assign:function(e,t){for(var r,s,l=n(e),u=1;u=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),g(n)?r.showHidden=n:n&&t._extend(r,n),x(r.showHidden)&&(r.showHidden=!1),x(r.depth)&&(r.depth=2),x(r.colors)&&(r.colors=!1),x(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=o),l(r,e,r.depth)}function o(e,t){var n=i.styles[t];return n?"["+i.colors[n][0]+"m"+e+"["+i.colors[n][1]+"m":e}function a(e,t){return e}function s(e){var t={};return e.forEach(function(e,n){t[e]=!0}),t}function l(e,n,r){if(e.customInspect&&n&&j(n.inspect)&&n.inspect!==t.inspect&&(!n.constructor||n.constructor.prototype!==n)){var i=n.inspect(r,e);return b(i)||(i=l(e,i,r)),i}var o=u(e,n);if(o)return o;var a=Object.keys(n),g=s(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(n)),E(n)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return c(n);if(0===a.length){if(j(n)){var m=n.name?": "+n.name:"";return e.stylize("[Function"+m+"]","special")}if(S(n))return e.stylize(RegExp.prototype.toString.call(n),"regexp");if(A(n))return e.stylize(Date.prototype.toString.call(n),"date");if(E(n))return c(n)}var y="",v=!1,w=["{","}"];if(p(n)&&(v=!0,w=["[","]"]),j(n)){var x=n.name?": "+n.name:"";y=" [Function"+x+"]"}if(S(n)&&(y=" "+RegExp.prototype.toString.call(n)),A(n)&&(y=" "+Date.prototype.toUTCString.call(n)),E(n)&&(y=" "+c(n)),0===a.length&&(!v||0==n.length))return w[0]+y+w[1];if(0>r)return S(n)?e.stylize(RegExp.prototype.toString.call(n),"regexp"):e.stylize("[Object]","special");e.seen.push(n);var O;return O=v?f(e,n,r,g,a):a.map(function(t){return d(e,n,r,g,t,v)}),e.seen.pop(),h(O,y,w)}function u(e,t){if(x(t))return e.stylize("undefined","undefined");if(b(t)){var n="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(n,"string")}return v(t)?e.stylize(""+t,"number"):g(t)?e.stylize(""+t,"boolean"):m(t)?e.stylize("null","null"):void 0}function c(e){return"["+Error.prototype.toString.call(e)+"]"}function f(e,t,n,r,i){for(var o=[],a=0,s=t.length;s>a;++a)o.push(M(t,String(a))?d(e,t,n,r,String(a),!0):"");return i.forEach(function(i){i.match(/^\d+$/)||o.push(d(e,t,n,r,i,!0))}),o}function d(e,t,n,r,i,o){var a,s,u;if(u=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]},u.get?s=u.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):u.set&&(s=e.stylize("[Setter]","special")),M(r,i)||(a="["+i+"]"),s||(e.seen.indexOf(u.value)<0?(s=m(n)?l(e,u.value,null):l(e,u.value,n-1),s.indexOf("\n")>-1&&(s=o?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n"))):s=e.stylize("[Circular]","special")),x(a)){if(o&&i.match(/^\d+$/))return s;a=JSON.stringify(""+i),a.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function h(e,t,n){var r=0,i=e.reduce(function(e,t){return r++,t.indexOf("\n")>=0&&r++,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0);return i>60?n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1]:n[0]+t+" "+e.join(", ")+" "+n[1]}function p(e){return Array.isArray(e)}function g(e){return"boolean"==typeof e}function m(e){return null===e}function y(e){return null==e}function v(e){return"number"==typeof e}function b(e){return"string"==typeof e}function w(e){return"symbol"==typeof e}function x(e){return void 0===e}function S(e){return O(e)&&"[object RegExp]"===C(e)}function O(e){return"object"==typeof e&&null!==e}function A(e){return O(e)&&"[object Date]"===C(e)}function E(e){return O(e)&&("[object Error]"===C(e)||e instanceof Error)}function j(e){return"function"==typeof e}function T(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||"undefined"==typeof e}function C(e){return Object.prototype.toString.call(e)}function k(e){return 10>e?"0"+e.toString(10):e.toString(10)}function z(){var e=new Date,t=[k(e.getHours()),k(e.getMinutes()),k(e.getSeconds())].join(":");return[e.getDate(),L[e.getMonth()],t].join(" ")}function M(e,t){return Object.prototype.hasOwnProperty.call(e,t)}var D=/%[sdj%]/g;t.format=function(e){if(!b(e)){for(var t=[],n=0;n=o)return e;switch(e){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(t){return"[Circular]"}default:return e}}),s=r[n];o>n;s=r[++n])a+=m(s)||!O(s)?" "+s:" "+i(s);return a},t.deprecate=function(n,i){function o(){if(!a){if(r.throwDeprecation)throw new Error(i);r.traceDeprecation?console.trace(i):console.error(i),a=!0}return n.apply(this,arguments)}if(x(e.process))return function(){return t.deprecate(n,i).apply(this,arguments)};if(r.noDeprecation===!0)return n;var a=!1;return o};var F,q={};t.debuglog=function(e){if(x(F)&&(F=r.env.NODE_DEBUG||""),e=e.toUpperCase(),!q[e])if(new RegExp("\\b"+e+"\\b","i").test(F)){var n=r.pid;q[e]=function(){var r=t.format.apply(t,arguments);console.error("%s %d: %s",e,n,r)}}else q[e]=function(){};return q[e]},t.inspect=i,i.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},i.styles={special:"cyan",number:"yellow","boolean":"yellow",undefined:"grey","null":"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=p,t.isBoolean=g,t.isNull=m,t.isNullOrUndefined=y,t.isNumber=v,t.isString=b,t.isSymbol=w,t.isUndefined=x,t.isRegExp=S,t.isObject=O,t.isDate=A,t.isError=E,t.isFunction=j,t.isPrimitive=T,t.isBuffer=n(25);var L=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];t.log=function(){console.log("%s - %s",z(),t.format.apply(t,arguments))},t.inherits=n(26),t._extend=function(e,t){if(!t||!O(t))return e;for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e}}).call(t,function(){return this}(),n(24))},function(e,t){function n(){throw new Error("setTimeout has not been defined")}function r(){throw new Error("clearTimeout has not been defined")}function i(e){if(c===setTimeout)return setTimeout(e,0);if((c===n||!c)&&setTimeout)return c=setTimeout,setTimeout(e,0);try{return c(e,0)}catch(t){try{return c.call(null,e,0)}catch(t){return c.call(this,e,0)}}}function o(e){if(f===clearTimeout)return clearTimeout(e);if((f===r||!f)&&clearTimeout)return f=clearTimeout,clearTimeout(e);try{return f(e)}catch(t){try{return f.call(null,e)}catch(t){return f.call(this,e)}}}function a(){g&&h&&(g=!1,h.length?p=h.concat(p):m=-1,p.length&&s())}function s(){if(!g){var e=i(a);g=!0;for(var t=p.length;t;){for(h=p,p=[];++m1)for(var n=1;n= 0 && H < 1) { 53 | r = C; 54 | g = X; 55 | } else if (H >= 1 && H < 2) { 56 | r = X; 57 | g = C; 58 | } else if (H >= 2 && H < 3) { 59 | g = C; 60 | b = X; 61 | } else if (H >= 3 && H < 4) { 62 | g = X; 63 | b = C; 64 | } else if (H >= 4 && H < 5) { 65 | r = X; 66 | b = C; 67 | } else if (H >= 5 && H < 6) { 68 | r = C; 69 | b = X; 70 | } 71 | 72 | r += m; 73 | g += m; 74 | b += m; 75 | 76 | r = parseInt(r * 255); 77 | g = parseInt(g * 255); 78 | b = parseInt(b * 255); 79 | 80 | return [r, g, b]; 81 | }; 82 | 83 | /** 84 | * Sets the color from a raw RGB888 integer 85 | * @param raw RGB888 representation of color 86 | */ 87 | //todo: refactor into a static method 88 | //todo: factor out individual color spaces 89 | //todo: add HSL, CIELAB, and CIELUV 90 | Color.prototype.set = function (val) { 91 | this.raw = val; 92 | 93 | var r = (this.raw & 0xFF0000) >> 16; 94 | var g = (this.raw & 0x00FF00) >> 8; 95 | var b = (this.raw & 0x0000FF); 96 | 97 | // BT.709 98 | var y = 0.2126 * r + 0.7152 * g + 0.0722 * b; 99 | var u = -0.09991 * r - 0.33609 * g + 0.436 * b; 100 | var v = 0.615 * r - 0.55861 * g - 0.05639 * b; 101 | 102 | this.rgb = { 103 | r: r, 104 | g: g, 105 | b: b 106 | }; 107 | 108 | this.yuv = { 109 | y: y, 110 | u: u, 111 | v: v 112 | }; 113 | 114 | return this; 115 | }; 116 | 117 | /** 118 | * Lighten or darken a color 119 | * @param multiplier Amount to lighten or darken (-1 to 1) 120 | */ 121 | Color.prototype.lighten = function(multiplier) { 122 | var cm = Math.min(1, Math.max(0, Math.abs(multiplier))) * (multiplier < 0 ? -1 : 1); 123 | var bm = (255 * cm) | 0; 124 | var cr = Math.min(255, Math.max(0, this.rgb.r + bm)); 125 | var cg = Math.min(255, Math.max(0, this.rgb.g + bm)); 126 | var cb = Math.min(255, Math.max(0, this.rgb.b + bm)); 127 | var hex = Color.rgb2hex(cr, cg, cb); 128 | return new Color(hex); 129 | }; 130 | 131 | /** 132 | * Output color in hex format 133 | * @param addHash Add a hash character to the beginning of the output 134 | */ 135 | Color.prototype.toHex = function(addHash) { 136 | return (addHash ? '#' : '') + this.raw.toString(16); 137 | }; 138 | 139 | /** 140 | * Returns whether or not current color is lighter than another color 141 | * @param color Color to compare against 142 | */ 143 | Color.prototype.lighterThan = function(color) { 144 | if (!(color instanceof Color)) { 145 | color = new Color(color); 146 | } 147 | 148 | return this.yuv.y > color.yuv.y; 149 | }; 150 | 151 | /** 152 | * Returns the result of mixing current color with another color 153 | * @param color Color to mix with 154 | * @param multiplier How much to mix with the other color 155 | */ 156 | /* 157 | Color.prototype.mix = function (color, multiplier) { 158 | if (!(color instanceof Color)) { 159 | color = new Color(color); 160 | } 161 | 162 | var r = this.rgb.r; 163 | var g = this.rgb.g; 164 | var b = this.rgb.b; 165 | var a = this.alpha; 166 | 167 | var m = typeof multiplier !== 'undefined' ? multiplier : 0.5; 168 | 169 | //todo: write a lerp function 170 | r = r + m * (color.rgb.r - r); 171 | g = g + m * (color.rgb.g - g); 172 | b = b + m * (color.rgb.b - b); 173 | a = a + m * (color.alpha - a); 174 | 175 | return new Color(Color.rgbToHex(r, g, b), { 176 | 'alpha': a 177 | }); 178 | }; 179 | */ 180 | 181 | /** 182 | * Returns the result of blending another color on top of current color with alpha 183 | * @param color Color to blend on top of current color, i.e. "Ca" 184 | */ 185 | //todo: see if .blendAlpha can be merged into .mix 186 | Color.prototype.blendAlpha = function(color) { 187 | if (!(color instanceof Color)) { 188 | color = new Color(color); 189 | } 190 | 191 | var Ca = color; 192 | var Cb = this; 193 | 194 | //todo: write alpha blending function 195 | var r = Ca.alpha * Ca.rgb.r + (1 - Ca.alpha) * Cb.rgb.r; 196 | var g = Ca.alpha * Ca.rgb.g + (1 - Ca.alpha) * Cb.rgb.g; 197 | var b = Ca.alpha * Ca.rgb.b + (1 - Ca.alpha) * Cb.rgb.b; 198 | 199 | return new Color(Color.rgb2hex(r, g, b)); 200 | }; 201 | 202 | module.exports = Color; 203 | -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'version': '%version%', 3 | 'svg_ns': 'http://www.w3.org/2000/svg' 4 | }; -------------------------------------------------------------------------------- /src/lib/dom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic new DOM element function 3 | * 4 | * @param tag Tag to create 5 | * @param namespace Optional namespace value 6 | */ 7 | exports.newEl = function(tag, namespace) { 8 | if (!global.document) return; 9 | 10 | if (namespace == null) { 11 | return global.document.createElement(tag); 12 | } else { 13 | return global.document.createElementNS(namespace, tag); 14 | } 15 | }; 16 | 17 | /** 18 | * Generic setAttribute function 19 | * 20 | * @param el Reference to DOM element 21 | * @param attrs Object with attribute keys and values 22 | */ 23 | exports.setAttr = function (el, attrs) { 24 | for (var a in attrs) { 25 | el.setAttribute(a, attrs[a]); 26 | } 27 | }; 28 | 29 | /** 30 | * Creates a XML document 31 | * @private 32 | */ 33 | exports.createXML = function() { 34 | if (!global.DOMParser) return; 35 | return new DOMParser().parseFromString('', 'application/xml'); 36 | }; 37 | 38 | /** 39 | * Converts a value into an array of DOM nodes 40 | * 41 | * @param val A string, a NodeList, a Node, or an HTMLCollection 42 | */ 43 | exports.getNodeArray = function(val) { 44 | var retval = null; 45 | if (typeof(val) == 'string') { 46 | retval = document.querySelectorAll(val); 47 | } else if (global.NodeList && val instanceof global.NodeList) { 48 | retval = val; 49 | } else if (global.Node && val instanceof global.Node) { 50 | retval = [val]; 51 | } else if (global.HTMLCollection && val instanceof global.HTMLCollection) { 52 | retval = val; 53 | } else if (val instanceof Array) { 54 | retval = val; 55 | } else if (val === null) { 56 | retval = []; 57 | } 58 | 59 | retval = Array.prototype.slice.call(retval); 60 | 61 | return retval; 62 | }; 63 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Holder.js - client side image placeholders 3 | (c) 2012-2020 Ivan Malopinsky - http://imsky.co 4 | */ 5 | 6 | //Libraries and functions 7 | var onDomReady = require('./vendor/ondomready'); 8 | var querystring = require('./vendor/querystring'); 9 | 10 | var SceneGraph = require('./scenegraph'); 11 | var utils = require('./utils'); 12 | var SVG = require('./svg'); 13 | var DOM = require('./dom'); 14 | var Color = require('./color'); 15 | var constants = require('./constants'); 16 | 17 | var svgRenderer = require('./renderers/svg-text'); 18 | var sgCanvasRenderer = require('./renderers/canvas'); 19 | 20 | var extend = utils.extend; 21 | var dimensionCheck = utils.dimensionCheck; 22 | 23 | //Constants and definitions 24 | var SVG_NS = constants.svg_ns; 25 | 26 | var Holder = { 27 | version: constants.version, 28 | 29 | /** 30 | * Adds a theme to default settings 31 | * 32 | * @param {string} name Theme name 33 | * @param {Object} theme Theme object, with foreground, background, size, font, and fontweight properties. 34 | */ 35 | addTheme: function(name, theme) { 36 | name != null && theme != null && (App.settings.themes[name] = theme); 37 | delete App.vars.cache.themeKeys; 38 | return this; 39 | }, 40 | 41 | /** 42 | * Appends a placeholder to an element 43 | * 44 | * @param {string} src Placeholder URL string 45 | * @param el A selector or a reference to a DOM node 46 | */ 47 | addImage: function(src, el) { 48 | //todo: use jquery fallback if available for all QSA references 49 | var nodes = DOM.getNodeArray(el); 50 | nodes.forEach(function (node) { 51 | var img = DOM.newEl('img'); 52 | var domProps = {}; 53 | domProps[App.setup.dataAttr] = src; 54 | DOM.setAttr(img, domProps); 55 | node.appendChild(img); 56 | }); 57 | return this; 58 | }, 59 | 60 | /** 61 | * Sets whether or not an image is updated on resize. 62 | * If an image is set to be updated, it is immediately rendered. 63 | * 64 | * @param {Object} el Image DOM element 65 | * @param {Boolean} value Resizable update flag value 66 | */ 67 | setResizeUpdate: function(el, value) { 68 | if (el.holderData) { 69 | el.holderData.resizeUpdate = !!value; 70 | if (el.holderData.resizeUpdate) { 71 | updateResizableElements(el); 72 | } 73 | } 74 | }, 75 | 76 | /** 77 | * Runs Holder with options. By default runs Holder on all images with "holder.js" in their source attributes. 78 | * 79 | * @param {Object} userOptions Options object, can contain domain, themes, images, and bgnodes properties 80 | */ 81 | run: function(userOptions) { 82 | //todo: split processing into separate queues 83 | userOptions = userOptions || {}; 84 | var engineSettings = {}; 85 | var options = extend(App.settings, userOptions); 86 | 87 | App.vars.preempted = true; 88 | App.vars.dataAttr = options.dataAttr || App.setup.dataAttr; 89 | 90 | engineSettings.renderer = options.renderer ? options.renderer : App.setup.renderer; 91 | if (App.setup.renderers.join(',').indexOf(engineSettings.renderer) === -1) { 92 | engineSettings.renderer = App.setup.supportsSVG ? 'svg' : (App.setup.supportsCanvas ? 'canvas' : 'html'); 93 | } 94 | 95 | var images = DOM.getNodeArray(options.images); 96 | var bgnodes = DOM.getNodeArray(options.bgnodes); 97 | var stylenodes = DOM.getNodeArray(options.stylenodes); 98 | var objects = DOM.getNodeArray(options.objects); 99 | 100 | engineSettings.stylesheets = []; 101 | engineSettings.svgXMLStylesheet = true; 102 | engineSettings.noFontFallback = !!options.noFontFallback; 103 | engineSettings.noBackgroundSize = !!options.noBackgroundSize; 104 | 105 | stylenodes.forEach(function (styleNode) { 106 | if (styleNode.attributes.rel && styleNode.attributes.href && styleNode.attributes.rel.value == 'stylesheet') { 107 | var href = styleNode.attributes.href.value; 108 | //todo: write isomorphic relative-to-absolute URL function 109 | var proxyLink = DOM.newEl('a'); 110 | proxyLink.href = href; 111 | var stylesheetURL = proxyLink.protocol + '//' + proxyLink.host + proxyLink.pathname + proxyLink.search; 112 | engineSettings.stylesheets.push(stylesheetURL); 113 | } 114 | }); 115 | 116 | bgnodes.forEach(function (bgNode) { 117 | //Skip processing background nodes if getComputedStyle is unavailable, since only modern browsers would be able to use canvas or SVG to render to background 118 | if (!global.getComputedStyle) return; 119 | var backgroundImage = global.getComputedStyle(bgNode, null).getPropertyValue('background-image'); 120 | var dataBackgroundImage = bgNode.getAttribute('data-background-src'); 121 | var rawURL = dataBackgroundImage || backgroundImage; 122 | 123 | var holderURL = null; 124 | var holderString = options.domain + '/'; 125 | var holderStringIndex = rawURL.indexOf(holderString); 126 | 127 | if (holderStringIndex === 0) { 128 | holderURL = rawURL; 129 | } else if (holderStringIndex === 1 && rawURL[0] === '?') { 130 | holderURL = rawURL.slice(1); 131 | } else { 132 | var fragment = rawURL.substr(holderStringIndex).match(/([^"]*)"?\)/); 133 | if (fragment !== null) { 134 | holderURL = fragment[1]; 135 | } else if (rawURL.indexOf('url(') === 0) { 136 | throw 'Holder: unable to parse background URL: ' + rawURL; 137 | } 138 | } 139 | 140 | if (holderURL) { 141 | var holderFlags = parseURL(holderURL, options); 142 | if (holderFlags) { 143 | prepareDOMElement({ 144 | mode: 'background', 145 | el: bgNode, 146 | flags: holderFlags, 147 | engineSettings: engineSettings 148 | }); 149 | } 150 | } 151 | }); 152 | 153 | objects.forEach(function (object) { 154 | var objectAttr = {}; 155 | 156 | try { 157 | objectAttr.data = object.getAttribute('data'); 158 | objectAttr.dataSrc = object.getAttribute(App.vars.dataAttr); 159 | } catch (e) { 160 | objectAttr.error = e; 161 | } 162 | 163 | var objectHasSrcURL = objectAttr.data != null && objectAttr.data.indexOf(options.domain) === 0; 164 | var objectHasDataSrcURL = objectAttr.dataSrc != null && objectAttr.dataSrc.indexOf(options.domain) === 0; 165 | 166 | if (objectHasSrcURL) { 167 | prepareImageElement(options, engineSettings, objectAttr.data, object); 168 | } else if (objectHasDataSrcURL) { 169 | prepareImageElement(options, engineSettings, objectAttr.dataSrc, object); 170 | } 171 | }); 172 | 173 | images.forEach(function (image) { 174 | var imageAttr = {}; 175 | 176 | try { 177 | imageAttr.src = image.getAttribute('src'); 178 | imageAttr.dataSrc = image.getAttribute(App.vars.dataAttr); 179 | imageAttr.rendered = image.getAttribute('data-holder-rendered'); 180 | } catch (e) { 181 | imageAttr.error = e; 182 | } 183 | 184 | var imageHasSrc = imageAttr.src != null; 185 | var imageHasDataSrcURL = imageAttr.dataSrc != null && imageAttr.dataSrc.indexOf(options.domain) === 0; 186 | var imageRendered = imageAttr.rendered != null && imageAttr.rendered == 'true'; 187 | 188 | if (imageHasSrc) { 189 | if (imageAttr.src.indexOf(options.domain) === 0) { 190 | prepareImageElement(options, engineSettings, imageAttr.src, image); 191 | } else if (imageHasDataSrcURL) { 192 | //Image has a valid data-src and an invalid src 193 | if (imageRendered) { 194 | //If the placeholder has already been render, re-render it 195 | prepareImageElement(options, engineSettings, imageAttr.dataSrc, image); 196 | } else { 197 | //If the placeholder has not been rendered, check if the image exists and render a fallback if it doesn't 198 | (function(src, options, engineSettings, dataSrc, image) { 199 | utils.imageExists(src, function(exists) { 200 | if (!exists) { 201 | prepareImageElement(options, engineSettings, dataSrc, image); 202 | } 203 | }); 204 | })(imageAttr.src, options, engineSettings, imageAttr.dataSrc, image); 205 | } 206 | } 207 | } else if (imageHasDataSrcURL) { 208 | prepareImageElement(options, engineSettings, imageAttr.dataSrc, image); 209 | } 210 | }); 211 | 212 | return this; 213 | } 214 | }; 215 | 216 | var App = { 217 | settings: { 218 | domain: 'holder.js', 219 | images: 'img', 220 | objects: 'object', 221 | bgnodes: 'body .holderjs', 222 | stylenodes: 'head link.holderjs', 223 | themes: { 224 | 'gray': { 225 | bg: '#EEEEEE', 226 | fg: '#AAAAAA' 227 | }, 228 | 'social': { 229 | bg: '#3a5a97', 230 | fg: '#FFFFFF' 231 | }, 232 | 'industrial': { 233 | bg: '#434A52', 234 | fg: '#C2F200' 235 | }, 236 | 'sky': { 237 | bg: '#0D8FDB', 238 | fg: '#FFFFFF' 239 | }, 240 | 'vine': { 241 | bg: '#39DBAC', 242 | fg: '#1E292C' 243 | }, 244 | 'lava': { 245 | bg: '#F8591A', 246 | fg: '#1C2846' 247 | } 248 | } 249 | }, 250 | defaults: { 251 | size: 10, 252 | units: 'pt', 253 | scale: 1 / 16 254 | } 255 | }; 256 | 257 | /** 258 | * Processes provided source attribute and sets up the appropriate rendering workflow 259 | * 260 | * @private 261 | * @param options Instance options from Holder.run 262 | * @param renderSettings Instance configuration 263 | * @param src Image URL 264 | * @param el Image DOM element 265 | */ 266 | function prepareImageElement(options, engineSettings, src, el) { 267 | var holderFlags = parseURL(src.substr(src.lastIndexOf(options.domain)), options); 268 | if (holderFlags) { 269 | prepareDOMElement({ 270 | mode: null, 271 | el: el, 272 | flags: holderFlags, 273 | engineSettings: engineSettings 274 | }); 275 | } 276 | } 277 | 278 | /** 279 | * Processes a Holder URL and extracts configuration from query string 280 | * 281 | * @private 282 | * @param url URL 283 | * @param instanceOptions Instance options from Holder.run 284 | */ 285 | function parseURL(url, instanceOptions) { 286 | var holder = { 287 | theme: extend(App.settings.themes.gray, null), 288 | stylesheets: instanceOptions.stylesheets, 289 | instanceOptions: instanceOptions 290 | }; 291 | 292 | var firstQuestionMark = url.indexOf('?'); 293 | var parts = [url]; 294 | 295 | if (firstQuestionMark !== -1) { 296 | parts = [url.slice(0, firstQuestionMark), url.slice(firstQuestionMark + 1)]; 297 | } 298 | 299 | var basics = parts[0].split('/'); 300 | 301 | holder.holderURL = url; 302 | 303 | var dimensions = basics[1]; 304 | var dimensionData = dimensions.match(/([\d]+p?)x([\d]+p?)/); 305 | 306 | if (!dimensionData) return false; 307 | 308 | holder.fluid = dimensions.indexOf('p') !== -1; 309 | 310 | holder.dimensions = { 311 | width: dimensionData[1].replace('p', '%'), 312 | height: dimensionData[2].replace('p', '%') 313 | }; 314 | 315 | if (parts.length === 2) { 316 | var options = querystring.parse(parts[1]); 317 | 318 | // Dimensions 319 | 320 | if (utils.truthy(options.ratio)) { 321 | holder.fluid = true; 322 | var ratioWidth = parseFloat(holder.dimensions.width.replace('%', '')); 323 | var ratioHeight = parseFloat(holder.dimensions.height.replace('%', '')); 324 | 325 | ratioHeight = Math.floor(100 * (ratioHeight / ratioWidth)); 326 | ratioWidth = 100; 327 | 328 | holder.dimensions.width = ratioWidth + '%'; 329 | holder.dimensions.height = ratioHeight + '%'; 330 | } 331 | 332 | holder.auto = utils.truthy(options.auto); 333 | 334 | // Colors 335 | 336 | if (options.bg) { 337 | holder.theme.bg = utils.parseColor(options.bg); 338 | } 339 | 340 | if (options.fg) { 341 | holder.theme.fg = utils.parseColor(options.fg); 342 | } 343 | 344 | //todo: add automatic foreground to themes without foreground 345 | if (options.bg && !options.fg) { 346 | holder.autoFg = true; 347 | } 348 | 349 | if (options.theme && Object.prototype.hasOwnProperty.call(holder.instanceOptions.themes, options.theme)) { 350 | holder.theme = extend(holder.instanceOptions.themes[options.theme], null); 351 | } 352 | 353 | // Text 354 | 355 | if (options.text) { 356 | holder.text = options.text; 357 | } 358 | 359 | if (options.textmode) { 360 | holder.textmode = options.textmode; 361 | } 362 | 363 | if (options.size && parseFloat(options.size)) { 364 | holder.size = parseFloat(options.size); 365 | } 366 | 367 | if (options.fixedSize != null) { 368 | holder.fixedSize = utils.truthy(options.fixedSize); 369 | } 370 | 371 | if (options.font) { 372 | holder.font = options.font; 373 | } 374 | 375 | if (options.align) { 376 | holder.align = options.align; 377 | } 378 | 379 | if (options.lineWrap) { 380 | holder.lineWrap = options.lineWrap; 381 | } 382 | 383 | holder.nowrap = utils.truthy(options.nowrap); 384 | 385 | // Miscellaneous 386 | 387 | holder.outline = utils.truthy(options.outline); 388 | 389 | if (utils.truthy(options.random)) { 390 | App.vars.cache.themeKeys = App.vars.cache.themeKeys || Object.keys(holder.instanceOptions.themes); 391 | var _theme = App.vars.cache.themeKeys[0 | Math.random() * App.vars.cache.themeKeys.length]; 392 | holder.theme = extend(holder.instanceOptions.themes[_theme], null); 393 | } 394 | } 395 | 396 | return holder; 397 | } 398 | 399 | /** 400 | * Modifies the DOM to fit placeholders and sets up resizable image callbacks (for fluid and automatically sized placeholders) 401 | * 402 | * @private 403 | * @param settings DOM prep settings 404 | */ 405 | function prepareDOMElement(prepSettings) { 406 | var mode = prepSettings.mode; 407 | var el = prepSettings.el; 408 | var flags = prepSettings.flags; 409 | var _engineSettings = prepSettings.engineSettings; 410 | var dimensions = flags.dimensions, 411 | theme = flags.theme; 412 | var dimensionsCaption = dimensions.width + 'x' + dimensions.height; 413 | mode = mode == null ? (flags.fluid ? 'fluid' : 'image') : mode; 414 | var holderTemplateRe = /holder_([a-z]+)/g; 415 | var dimensionsInText = false; 416 | 417 | if (flags.text != null) { 418 | theme.text = flags.text; 419 | 420 | // SVG embedding doesn't parse Unicode properly 421 | if (el.nodeName.toLowerCase() === 'object') { 422 | var textLines = theme.text.split('\\n'); 423 | for (var k = 0; k < textLines.length; k++) { 424 | textLines[k] = utils.encodeHtmlEntity(textLines[k]); 425 | } 426 | theme.text = textLines.join('\\n'); 427 | } 428 | } 429 | 430 | if (theme.text) { 431 | var holderTemplateMatches = theme.text.match(holderTemplateRe); 432 | 433 | if (holderTemplateMatches !== null) { 434 | //todo: optimize template replacement 435 | holderTemplateMatches.forEach(function (match) { 436 | if (match === 'holder_dimensions') { 437 | theme.text = theme.text.replace(match, dimensionsCaption); 438 | } 439 | }); 440 | } 441 | } 442 | 443 | var holderURL = flags.holderURL; 444 | var engineSettings = extend(_engineSettings, null); 445 | 446 | if (flags.font) { 447 | /* 448 | If external fonts are used in a placeholder rendered with SVG, Holder falls back to canvas. 449 | 450 | This is done because Firefox and Chrome disallow embedded SVGs from referencing external assets. 451 | The workaround is either to change the placeholder tag from to or to use the canvas renderer. 452 | */ 453 | theme.font = flags.font; 454 | if (!engineSettings.noFontFallback && el.nodeName.toLowerCase() === 'img' && App.setup.supportsCanvas && engineSettings.renderer === 'svg') { 455 | engineSettings = extend(engineSettings, { 456 | renderer: 'canvas' 457 | }); 458 | } 459 | } 460 | 461 | //Chrome and Opera require a quick 10ms re-render if web fonts are used with canvas 462 | if (flags.font && engineSettings.renderer == 'canvas') { 463 | engineSettings.reRender = true; 464 | } 465 | 466 | if (mode == 'background') { 467 | if (el.getAttribute('data-background-src') == null) { 468 | DOM.setAttr(el, { 469 | 'data-background-src': holderURL 470 | }); 471 | } 472 | } else { 473 | var domProps = {}; 474 | domProps[App.vars.dataAttr] = holderURL; 475 | DOM.setAttr(el, domProps); 476 | } 477 | 478 | flags.theme = theme; 479 | 480 | //todo consider using all renderSettings in holderData 481 | el.holderData = { 482 | flags: flags, 483 | engineSettings: engineSettings 484 | }; 485 | 486 | if (mode == 'image' || mode == 'fluid') { 487 | DOM.setAttr(el, { 488 | 'alt': theme.text ? (dimensionsInText ? theme.text : theme.text + ' [' + dimensionsCaption + ']') : dimensionsCaption 489 | }); 490 | } 491 | 492 | var renderSettings = { 493 | mode: mode, 494 | el: el, 495 | holderSettings: { 496 | dimensions: dimensions, 497 | theme: theme, 498 | flags: flags 499 | }, 500 | engineSettings: engineSettings 501 | }; 502 | 503 | if (mode == 'image') { 504 | if (!flags.auto) { 505 | el.style.width = dimensions.width + 'px'; 506 | el.style.height = dimensions.height + 'px'; 507 | } 508 | 509 | if (engineSettings.renderer == 'html') { 510 | el.style.backgroundColor = theme.bg; 511 | } else { 512 | render(renderSettings); 513 | 514 | if (flags.textmode == 'exact') { 515 | el.holderData.resizeUpdate = true; 516 | App.vars.resizableImages.push(el); 517 | updateResizableElements(el); 518 | } 519 | } 520 | } else if (mode == 'background' && engineSettings.renderer != 'html') { 521 | render(renderSettings); 522 | } else if (mode == 'fluid') { 523 | el.holderData.resizeUpdate = true; 524 | 525 | if (dimensions.height.slice(-1) == '%') { 526 | el.style.height = dimensions.height; 527 | } else if (flags.auto == null || !flags.auto) { 528 | el.style.height = dimensions.height + 'px'; 529 | } 530 | if (dimensions.width.slice(-1) == '%') { 531 | el.style.width = dimensions.width; 532 | } else if (flags.auto == null || !flags.auto) { 533 | el.style.width = dimensions.width + 'px'; 534 | } 535 | if (el.style.display == 'inline' || el.style.display === '' || el.style.display == 'none') { 536 | el.style.display = 'block'; 537 | } 538 | 539 | setInitialDimensions(el); 540 | 541 | if (engineSettings.renderer == 'html') { 542 | el.style.backgroundColor = theme.bg; 543 | } else { 544 | App.vars.resizableImages.push(el); 545 | updateResizableElements(el); 546 | } 547 | } 548 | } 549 | 550 | /** 551 | * Core function that takes output from renderers and sets it as the source or background-image of the target element 552 | * 553 | * @private 554 | * @param renderSettings Renderer settings 555 | */ 556 | function render(renderSettings) { 557 | var image = null; 558 | var mode = renderSettings.mode; 559 | var el = renderSettings.el; 560 | var holderSettings = renderSettings.holderSettings; 561 | var engineSettings = renderSettings.engineSettings; 562 | 563 | switch (engineSettings.renderer) { 564 | case 'svg': 565 | if (!App.setup.supportsSVG) return; 566 | break; 567 | case 'canvas': 568 | if (!App.setup.supportsCanvas) return; 569 | break; 570 | default: 571 | return; 572 | } 573 | 574 | //todo: move generation of scene up to flag generation to reduce extra object creation 575 | var scene = { 576 | width: holderSettings.dimensions.width, 577 | height: holderSettings.dimensions.height, 578 | theme: holderSettings.theme, 579 | flags: holderSettings.flags 580 | }; 581 | 582 | var sceneGraph = buildSceneGraph(scene); 583 | 584 | function getRenderedImage() { 585 | var image = null; 586 | switch (engineSettings.renderer) { 587 | case 'canvas': 588 | image = sgCanvasRenderer(sceneGraph, renderSettings); 589 | break; 590 | case 'svg': 591 | image = svgRenderer(sceneGraph, renderSettings); 592 | break; 593 | default: 594 | throw 'Holder: invalid renderer: ' + engineSettings.renderer; 595 | } 596 | 597 | return image; 598 | } 599 | 600 | image = getRenderedImage(); 601 | 602 | if (image == null) { 603 | throw 'Holder: couldn\'t render placeholder'; 604 | } 605 | 606 | //todo: add canvas rendering 607 | if (mode == 'background') { 608 | el.style.backgroundImage = 'url(' + image + ')'; 609 | 610 | if (!engineSettings.noBackgroundSize) { 611 | el.style.backgroundSize = scene.width + 'px ' + scene.height + 'px'; 612 | } 613 | } else { 614 | if (el.nodeName.toLowerCase() === 'img') { 615 | DOM.setAttr(el, { 616 | 'src': image 617 | }); 618 | } else if (el.nodeName.toLowerCase() === 'object') { 619 | DOM.setAttr(el, { 620 | 'data': image, 621 | 'type': 'image/svg+xml' 622 | }); 623 | } 624 | if (engineSettings.reRender) { 625 | global.setTimeout(function () { 626 | var image = getRenderedImage(); 627 | if (image == null) { 628 | throw 'Holder: couldn\'t render placeholder'; 629 | } 630 | //todo: refactor this code into a function 631 | if (el.nodeName.toLowerCase() === 'img') { 632 | DOM.setAttr(el, { 633 | 'src': image 634 | }); 635 | } else if (el.nodeName.toLowerCase() === 'object') { 636 | DOM.setAttr(el, { 637 | 'data': image, 638 | 'type': 'image/svg+xml' 639 | }); 640 | } 641 | }, 150); 642 | } 643 | } 644 | //todo: account for re-rendering 645 | DOM.setAttr(el, { 646 | 'data-holder-rendered': true 647 | }); 648 | } 649 | 650 | /** 651 | * Core function that takes a Holder scene description and builds a scene graph 652 | * 653 | * @private 654 | * @param scene Holder scene object 655 | */ 656 | //todo: make this function reusable 657 | //todo: merge app defaults and setup properties into the scene argument 658 | function buildSceneGraph(scene) { 659 | var fontSize = App.defaults.size; 660 | var fixedSize = scene.flags.fixedSize != null ? scene.flags.fixedSize : scene.theme.fixedSize; 661 | if (parseFloat(scene.theme.size)) { 662 | fontSize = scene.theme.size; 663 | } else if (parseFloat(scene.flags.size)) { 664 | fontSize = scene.flags.size; 665 | } 666 | 667 | scene.font = { 668 | family: scene.theme.font ? scene.theme.font : 'Arial, Helvetica, Open Sans, sans-serif', 669 | size: fixedSize ? fontSize : textSize(scene.width, scene.height, fontSize, App.defaults.scale), 670 | units: scene.theme.units ? scene.theme.units : App.defaults.units, 671 | weight: scene.theme.fontweight ? scene.theme.fontweight : 'bold' 672 | }; 673 | 674 | scene.text = scene.theme.text || Math.floor(scene.width) + 'x' + Math.floor(scene.height); 675 | 676 | scene.noWrap = scene.theme.nowrap || scene.flags.nowrap; 677 | 678 | scene.align = scene.theme.align || scene.flags.align || 'center'; 679 | 680 | switch (scene.flags.textmode) { 681 | case 'literal': 682 | scene.text = scene.flags.dimensions.width + 'x' + scene.flags.dimensions.height; 683 | break; 684 | case 'exact': 685 | if (!scene.flags.exactDimensions) break; 686 | scene.text = Math.floor(scene.flags.exactDimensions.width) + 'x' + Math.floor(scene.flags.exactDimensions.height); 687 | break; 688 | } 689 | 690 | var lineWrap = scene.flags.lineWrap || App.setup.lineWrapRatio; 691 | var sceneMargin = scene.width * lineWrap; 692 | var maxLineWidth = sceneMargin; 693 | 694 | var sceneGraph = new SceneGraph({ 695 | width: scene.width, 696 | height: scene.height 697 | }); 698 | 699 | var Shape = sceneGraph.Shape; 700 | 701 | var holderBg = new Shape.Rect('holderBg', { 702 | fill: scene.theme.bg 703 | }); 704 | 705 | holderBg.resize(scene.width, scene.height); 706 | sceneGraph.root.add(holderBg); 707 | 708 | if (scene.flags.outline) { 709 | var outlineColor = new Color(holderBg.properties.fill); 710 | outlineColor = outlineColor.lighten(outlineColor.lighterThan('7f7f7f') ? -0.1 : 0.1); 711 | holderBg.properties.outline = { 712 | fill: outlineColor.toHex(true), 713 | width: 2 714 | }; 715 | } 716 | 717 | var holderTextColor = scene.theme.fg; 718 | 719 | if (scene.flags.autoFg) { 720 | var holderBgColor = new Color(holderBg.properties.fill); 721 | var lightColor = new Color('fff'); 722 | var darkColor = new Color('000', { 723 | 'alpha': 0.285714 724 | }); 725 | 726 | holderTextColor = holderBgColor.blendAlpha(holderBgColor.lighterThan('7f7f7f') ? darkColor : lightColor).toHex(true); 727 | } 728 | 729 | var holderTextGroup = new Shape.Group('holderTextGroup', { 730 | text: scene.text, 731 | align: scene.align, 732 | font: scene.font, 733 | fill: holderTextColor 734 | }); 735 | 736 | holderTextGroup.moveTo(null, null, 1); 737 | sceneGraph.root.add(holderTextGroup); 738 | 739 | var tpdata = holderTextGroup.textPositionData = stagingRenderer(sceneGraph); 740 | if (!tpdata) { 741 | throw 'Holder: staging fallback not supported yet.'; 742 | } 743 | holderTextGroup.properties.leading = tpdata.boundingBox.height; 744 | 745 | var textNode = null; 746 | var line = null; 747 | 748 | function finalizeLine(parent, line, width, height) { 749 | line.width = width; 750 | line.height = height; 751 | parent.width = Math.max(parent.width, line.width); 752 | parent.height += line.height; 753 | } 754 | 755 | if (tpdata.lineCount > 1) { 756 | var offsetX = 0; 757 | var offsetY = 0; 758 | var lineIndex = 0; 759 | var lineKey; 760 | line = new Shape.Group('line' + lineIndex); 761 | 762 | //Double margin so that left/right-aligned next is not flush with edge of image 763 | if (scene.align === 'left' || scene.align === 'right') { 764 | maxLineWidth = scene.width * (1 - (1 - lineWrap) * 2); 765 | } 766 | 767 | for (var i = 0; i < tpdata.words.length; i++) { 768 | var word = tpdata.words[i]; 769 | textNode = new Shape.Text(word.text); 770 | var newline = word.text == '\\n'; 771 | if (!scene.noWrap && (offsetX + word.width >= maxLineWidth || newline === true)) { 772 | finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading); 773 | holderTextGroup.add(line); 774 | offsetX = 0; 775 | offsetY += holderTextGroup.properties.leading; 776 | lineIndex += 1; 777 | line = new Shape.Group('line' + lineIndex); 778 | line.y = offsetY; 779 | } 780 | if (newline === true) { 781 | continue; 782 | } 783 | textNode.moveTo(offsetX, 0); 784 | offsetX += tpdata.spaceWidth + word.width; 785 | line.add(textNode); 786 | } 787 | 788 | finalizeLine(holderTextGroup, line, offsetX, holderTextGroup.properties.leading); 789 | holderTextGroup.add(line); 790 | 791 | if (scene.align === 'left') { 792 | holderTextGroup.moveTo(scene.width - sceneMargin, null, null); 793 | } else if (scene.align === 'right') { 794 | for (lineKey in holderTextGroup.children) { 795 | line = holderTextGroup.children[lineKey]; 796 | line.moveTo(scene.width - line.width, null, null); 797 | } 798 | 799 | holderTextGroup.moveTo(0 - (scene.width - sceneMargin), null, null); 800 | } else { 801 | for (lineKey in holderTextGroup.children) { 802 | line = holderTextGroup.children[lineKey]; 803 | line.moveTo((holderTextGroup.width - line.width) / 2, null, null); 804 | } 805 | 806 | holderTextGroup.moveTo((scene.width - holderTextGroup.width) / 2, null, null); 807 | } 808 | 809 | holderTextGroup.moveTo(null, (scene.height - holderTextGroup.height) / 2, null); 810 | 811 | //If the text exceeds vertical space, move it down so the first line is visible 812 | if ((scene.height - holderTextGroup.height) / 2 < 0) { 813 | holderTextGroup.moveTo(null, 0, null); 814 | } 815 | } else { 816 | textNode = new Shape.Text(scene.text); 817 | line = new Shape.Group('line0'); 818 | line.add(textNode); 819 | holderTextGroup.add(line); 820 | 821 | if (scene.align === 'left') { 822 | holderTextGroup.moveTo(scene.width - sceneMargin, null, null); 823 | } else if (scene.align === 'right') { 824 | holderTextGroup.moveTo(0 - (scene.width - sceneMargin), null, null); 825 | } else { 826 | holderTextGroup.moveTo((scene.width - tpdata.boundingBox.width) / 2, null, null); 827 | } 828 | 829 | holderTextGroup.moveTo(null, (scene.height - tpdata.boundingBox.height) / 2, null); 830 | } 831 | 832 | //todo: renderlist 833 | return sceneGraph; 834 | } 835 | 836 | /** 837 | * Adaptive text sizing function 838 | * 839 | * @private 840 | * @param width Parent width 841 | * @param height Parent height 842 | * @param fontSize Requested text size 843 | * @param scale Proportional scale of text 844 | */ 845 | function textSize(width, height, fontSize, scale) { 846 | var stageWidth = parseInt(width, 10); 847 | var stageHeight = parseInt(height, 10); 848 | 849 | var bigSide = Math.max(stageWidth, stageHeight); 850 | var smallSide = Math.min(stageWidth, stageHeight); 851 | 852 | var newHeight = 0.8 * Math.min(smallSide, bigSide * scale); 853 | return Math.round(Math.max(fontSize, newHeight)); 854 | } 855 | 856 | /** 857 | * Iterates over resizable (fluid or auto) placeholders and renders them 858 | * 859 | * @private 860 | * @param element Optional element selector, specified only if a specific element needs to be re-rendered 861 | */ 862 | function updateResizableElements(element) { 863 | var images; 864 | if (element == null || element.nodeType == null) { 865 | images = App.vars.resizableImages; 866 | } else { 867 | images = [element]; 868 | } 869 | for (var i = 0, l = images.length; i < l; i++) { 870 | var el = images[i]; 871 | if (el.holderData) { 872 | var flags = el.holderData.flags; 873 | var dimensions = dimensionCheck(el); 874 | if (dimensions) { 875 | if (!el.holderData.resizeUpdate) { 876 | continue; 877 | } 878 | 879 | if (flags.fluid && flags.auto) { 880 | var fluidConfig = el.holderData.fluidConfig; 881 | switch (fluidConfig.mode) { 882 | case 'width': 883 | dimensions.height = dimensions.width / fluidConfig.ratio; 884 | break; 885 | case 'height': 886 | dimensions.width = dimensions.height * fluidConfig.ratio; 887 | break; 888 | } 889 | } 890 | 891 | var settings = { 892 | mode: 'image', 893 | holderSettings: { 894 | dimensions: dimensions, 895 | theme: flags.theme, 896 | flags: flags 897 | }, 898 | el: el, 899 | engineSettings: el.holderData.engineSettings 900 | }; 901 | 902 | if (flags.textmode == 'exact') { 903 | flags.exactDimensions = dimensions; 904 | settings.holderSettings.dimensions = flags.dimensions; 905 | } 906 | 907 | render(settings); 908 | } else { 909 | setInvisible(el); 910 | } 911 | } 912 | } 913 | } 914 | 915 | /** 916 | * Sets up aspect ratio metadata for fluid placeholders, in order to preserve proportions when resizing 917 | * 918 | * @private 919 | * @param el Image DOM element 920 | */ 921 | function setInitialDimensions(el) { 922 | if (el.holderData) { 923 | var dimensions = dimensionCheck(el); 924 | if (dimensions) { 925 | var flags = el.holderData.flags; 926 | 927 | var fluidConfig = { 928 | fluidHeight: flags.dimensions.height.slice(-1) == '%', 929 | fluidWidth: flags.dimensions.width.slice(-1) == '%', 930 | mode: null, 931 | initialDimensions: dimensions 932 | }; 933 | 934 | if (fluidConfig.fluidWidth && !fluidConfig.fluidHeight) { 935 | fluidConfig.mode = 'width'; 936 | fluidConfig.ratio = fluidConfig.initialDimensions.width / parseFloat(flags.dimensions.height); 937 | } else if (!fluidConfig.fluidWidth && fluidConfig.fluidHeight) { 938 | fluidConfig.mode = 'height'; 939 | fluidConfig.ratio = parseFloat(flags.dimensions.width) / fluidConfig.initialDimensions.height; 940 | } 941 | 942 | el.holderData.fluidConfig = fluidConfig; 943 | } else { 944 | setInvisible(el); 945 | } 946 | } 947 | } 948 | 949 | /** 950 | * Iterates through all current invisible images, and if they're visible, renders them and removes them from further checks. Runs every animation frame. 951 | * 952 | * @private 953 | */ 954 | function visibilityCheck() { 955 | var renderableImages = []; 956 | var keys = Object.keys(App.vars.invisibleImages); 957 | var el; 958 | 959 | keys.forEach(function (key) { 960 | el = App.vars.invisibleImages[key]; 961 | if (dimensionCheck(el) && el.nodeName.toLowerCase() == 'img') { 962 | renderableImages.push(el); 963 | delete App.vars.invisibleImages[key]; 964 | } 965 | }); 966 | 967 | if (renderableImages.length) { 968 | Holder.run({ 969 | images: renderableImages 970 | }); 971 | } 972 | 973 | // Done to prevent 100% CPU usage via aggressive calling of requestAnimationFrame 974 | setTimeout(function () { 975 | global.requestAnimationFrame(visibilityCheck); 976 | }, 10); 977 | } 978 | 979 | /** 980 | * Starts checking for invisible placeholders if not doing so yet. Does nothing otherwise. 981 | * 982 | * @private 983 | */ 984 | function startVisibilityCheck() { 985 | if (!App.vars.visibilityCheckStarted) { 986 | global.requestAnimationFrame(visibilityCheck); 987 | App.vars.visibilityCheckStarted = true; 988 | } 989 | } 990 | 991 | /** 992 | * Sets a unique ID for an image detected to be invisible and adds it to the map of invisible images checked by visibilityCheck 993 | * 994 | * @private 995 | * @param el Invisible DOM element 996 | */ 997 | function setInvisible(el) { 998 | if (!el.holderData.invisibleId) { 999 | App.vars.invisibleId += 1; 1000 | App.vars.invisibleImages['i' + App.vars.invisibleId] = el; 1001 | el.holderData.invisibleId = App.vars.invisibleId; 1002 | } 1003 | } 1004 | 1005 | //todo: see if possible to convert stagingRenderer to use HTML only 1006 | var stagingRenderer = (function() { 1007 | var svg = null, 1008 | stagingText = null, 1009 | stagingTextNode = null; 1010 | return function(graph) { 1011 | var rootNode = graph.root; 1012 | if (App.setup.supportsSVG) { 1013 | var firstTimeSetup = false; 1014 | var tnode = function(text) { 1015 | return document.createTextNode(text); 1016 | }; 1017 | if (svg == null || svg.parentNode !== document.body) { 1018 | firstTimeSetup = true; 1019 | } 1020 | 1021 | svg = SVG.initSVG(svg, rootNode.properties.width, rootNode.properties.height); 1022 | //Show staging element before staging 1023 | svg.style.display = 'block'; 1024 | 1025 | if (firstTimeSetup) { 1026 | stagingText = DOM.newEl('text', SVG_NS); 1027 | stagingTextNode = tnode(null); 1028 | DOM.setAttr(stagingText, { 1029 | x: 0 1030 | }); 1031 | stagingText.appendChild(stagingTextNode); 1032 | svg.appendChild(stagingText); 1033 | document.body.appendChild(svg); 1034 | svg.style.visibility = 'hidden'; 1035 | svg.style.position = 'absolute'; 1036 | svg.style.top = '-100%'; 1037 | svg.style.left = '-100%'; 1038 | //todo: workaround for zero-dimension tag in Opera 12 1039 | //svg.setAttribute('width', 0); 1040 | //svg.setAttribute('height', 0); 1041 | } 1042 | 1043 | var holderTextGroup = rootNode.children.holderTextGroup; 1044 | var htgProps = holderTextGroup.properties; 1045 | DOM.setAttr(stagingText, { 1046 | 'y': htgProps.font.size, 1047 | 'style': utils.cssProps({ 1048 | 'font-weight': htgProps.font.weight, 1049 | 'font-size': htgProps.font.size + htgProps.font.units, 1050 | 'font-family': htgProps.font.family 1051 | }) 1052 | }); 1053 | 1054 | //Unescape HTML entities to get approximately the right width 1055 | var txt = DOM.newEl('textarea'); 1056 | txt.innerHTML = htgProps.text; 1057 | stagingTextNode.nodeValue = txt.value; 1058 | 1059 | //Get bounding box for the whole string (total width and height) 1060 | var stagingTextBBox = stagingText.getBBox(); 1061 | 1062 | //Get line count and split the string into words 1063 | var lineCount = Math.ceil(stagingTextBBox.width / rootNode.properties.width); 1064 | var words = htgProps.text.split(' '); 1065 | var newlines = htgProps.text.match(/\\n/g); 1066 | lineCount += newlines == null ? 0 : newlines.length; 1067 | 1068 | //Get bounding box for the string with spaces removed 1069 | stagingTextNode.nodeValue = htgProps.text.replace(/[ ]+/g, ''); 1070 | var computedNoSpaceLength = stagingText.getComputedTextLength(); 1071 | 1072 | //Compute average space width 1073 | var diffLength = stagingTextBBox.width - computedNoSpaceLength; 1074 | var spaceWidth = Math.round(diffLength / Math.max(1, words.length - 1)); 1075 | 1076 | //Get widths for every word with space only if there is more than one line 1077 | var wordWidths = []; 1078 | if (lineCount > 1) { 1079 | stagingTextNode.nodeValue = ''; 1080 | for (var i = 0; i < words.length; i++) { 1081 | if (words[i].length === 0) continue; 1082 | stagingTextNode.nodeValue = utils.decodeHtmlEntity(words[i]); 1083 | var bbox = stagingText.getBBox(); 1084 | wordWidths.push({ 1085 | text: words[i], 1086 | width: bbox.width 1087 | }); 1088 | } 1089 | } 1090 | 1091 | //Hide staging element after staging 1092 | svg.style.display = 'none'; 1093 | 1094 | return { 1095 | spaceWidth: spaceWidth, 1096 | lineCount: lineCount, 1097 | boundingBox: stagingTextBBox, 1098 | words: wordWidths 1099 | }; 1100 | } else { 1101 | //todo: canvas fallback for measuring text on android 2.3 1102 | return false; 1103 | } 1104 | }; 1105 | })(); 1106 | 1107 | //Helpers 1108 | 1109 | /** 1110 | * Prevents a function from being called too often, waits until a timer elapses to call it again 1111 | * 1112 | * @param fn Function to call 1113 | */ 1114 | function debounce(fn) { 1115 | if (!App.vars.debounceTimer) fn.call(this); 1116 | if (App.vars.debounceTimer) global.clearTimeout(App.vars.debounceTimer); 1117 | App.vars.debounceTimer = global.setTimeout(function() { 1118 | App.vars.debounceTimer = null; 1119 | fn.call(this); 1120 | }, App.setup.debounce); 1121 | } 1122 | 1123 | /** 1124 | * Holder-specific resize/orientation change callback, debounced to prevent excessive execution 1125 | */ 1126 | function resizeEvent() { 1127 | debounce(function() { 1128 | updateResizableElements(null); 1129 | }); 1130 | } 1131 | 1132 | //Set up flags 1133 | 1134 | for (var flag in App.flags) { 1135 | if (!Object.prototype.hasOwnProperty.call(App.flags, flag)) continue; 1136 | App.flags[flag].match = function(val) { 1137 | return val.match(this.regex); 1138 | }; 1139 | } 1140 | 1141 | //Properties set once on setup 1142 | 1143 | App.setup = { 1144 | renderer: 'html', 1145 | debounce: 100, 1146 | ratio: 1, 1147 | supportsCanvas: false, 1148 | supportsSVG: false, 1149 | lineWrapRatio: 0.9, 1150 | dataAttr: 'data-src', 1151 | renderers: ['html', 'canvas', 'svg'] 1152 | }; 1153 | 1154 | //Properties modified during runtime 1155 | 1156 | App.vars = { 1157 | preempted: false, 1158 | resizableImages: [], 1159 | invisibleImages: {}, 1160 | invisibleId: 0, 1161 | visibilityCheckStarted: false, 1162 | debounceTimer: null, 1163 | cache: {} 1164 | }; 1165 | 1166 | //Pre-flight 1167 | 1168 | (function() { 1169 | var canvas = DOM.newEl('canvas'); 1170 | 1171 | if (canvas.getContext) { 1172 | if (canvas.toDataURL('image/png').indexOf('data:image/png') != -1) { 1173 | App.setup.renderer = 'canvas'; 1174 | App.setup.supportsCanvas = true; 1175 | } 1176 | } 1177 | 1178 | if (!!document.createElementNS && !!document.createElementNS(SVG_NS, 'svg').createSVGRect) { 1179 | App.setup.renderer = 'svg'; 1180 | App.setup.supportsSVG = true; 1181 | } 1182 | })(); 1183 | 1184 | //Starts checking for invisible placeholders 1185 | startVisibilityCheck(); 1186 | 1187 | if (onDomReady) { 1188 | onDomReady(function() { 1189 | if (!App.vars.preempted) { 1190 | Holder.run(); 1191 | } 1192 | if (global.addEventListener) { 1193 | global.addEventListener('resize', resizeEvent, false); 1194 | global.addEventListener('orientationchange', resizeEvent, false); 1195 | } else { 1196 | global.attachEvent('onresize', resizeEvent); 1197 | } 1198 | 1199 | if (typeof global.Turbolinks == 'object') { 1200 | global.document.addEventListener('page:change', function() { 1201 | Holder.run(); 1202 | }); 1203 | } 1204 | }); 1205 | } 1206 | 1207 | module.exports = Holder; 1208 | -------------------------------------------------------------------------------- /src/lib/renderers/canvas.js: -------------------------------------------------------------------------------- 1 | var DOM = require('../dom'); 2 | var utils = require('../utils'); 3 | 4 | module.exports = (function() { 5 | var canvas = DOM.newEl('canvas'); 6 | var ctx = null; 7 | 8 | return function(sceneGraph) { 9 | if (ctx == null) { 10 | ctx = canvas.getContext('2d'); 11 | } 12 | 13 | var dpr = utils.canvasRatio(); 14 | var root = sceneGraph.root; 15 | canvas.width = dpr * root.properties.width; 16 | canvas.height = dpr * root.properties.height ; 17 | ctx.textBaseline = 'middle'; 18 | 19 | var bg = root.children.holderBg; 20 | var bgWidth = dpr * bg.width; 21 | var bgHeight = dpr * bg.height; 22 | //todo: parametrize outline width (e.g. in scene object) 23 | var outlineWidth = 2; 24 | var outlineOffsetWidth = outlineWidth / 2; 25 | 26 | ctx.fillStyle = bg.properties.fill; 27 | ctx.fillRect(0, 0, bgWidth, bgHeight); 28 | 29 | if (bg.properties.outline) { 30 | //todo: abstract this into a method 31 | ctx.strokeStyle = bg.properties.outline.fill; 32 | ctx.lineWidth = bg.properties.outline.width; 33 | ctx.moveTo(outlineOffsetWidth, outlineOffsetWidth); 34 | // TL, TR, BR, BL 35 | ctx.lineTo(bgWidth - outlineOffsetWidth, outlineOffsetWidth); 36 | ctx.lineTo(bgWidth - outlineOffsetWidth, bgHeight - outlineOffsetWidth); 37 | ctx.lineTo(outlineOffsetWidth, bgHeight - outlineOffsetWidth); 38 | ctx.lineTo(outlineOffsetWidth, outlineOffsetWidth); 39 | // Diagonals 40 | ctx.moveTo(0, outlineOffsetWidth); 41 | ctx.lineTo(bgWidth, bgHeight - outlineOffsetWidth); 42 | ctx.moveTo(0, bgHeight - outlineOffsetWidth); 43 | ctx.lineTo(bgWidth, outlineOffsetWidth); 44 | ctx.stroke(); 45 | } 46 | 47 | var textGroup = root.children.holderTextGroup; 48 | ctx.font = textGroup.properties.font.weight + ' ' + (dpr * textGroup.properties.font.size) + textGroup.properties.font.units + ' ' + textGroup.properties.font.family + ', monospace'; 49 | ctx.fillStyle = textGroup.properties.fill; 50 | 51 | for (var lineKey in textGroup.children) { 52 | var line = textGroup.children[lineKey]; 53 | for (var wordKey in line.children) { 54 | var word = line.children[wordKey]; 55 | var x = dpr * (textGroup.x + line.x + word.x); 56 | var y = dpr * (textGroup.y + line.y + word.y + (textGroup.properties.leading / 2)); 57 | 58 | ctx.fillText(word.properties.text, x, y); 59 | } 60 | } 61 | 62 | return canvas.toDataURL('image/png'); 63 | }; 64 | })(); -------------------------------------------------------------------------------- /src/lib/renderers/svg-dom.js: -------------------------------------------------------------------------------- 1 | var SVG = require('../svg'); 2 | var DOM = require('../dom'); 3 | var utils = require('../utils'); 4 | var constants = require('../constants'); 5 | 6 | var SVG_NS = constants.svg_ns; 7 | 8 | var generatorComment = '\n' + 9 | 'Created with Holder.js ' + constants.version + '.\n' + 10 | 'Learn more at http://holderjs.com\n' + 11 | '(c) 2012-2021 Ivan Malopinsky - https://imsky.co\n'; 12 | 13 | module.exports = (function() { 14 | //Prevent IE <9 from initializing SVG renderer 15 | if (!global.XMLSerializer) return; 16 | var xml = DOM.createXML(); 17 | var svg = SVG.initSVG(null, 0, 0); 18 | var bgEl = DOM.newEl('rect', SVG_NS); 19 | svg.appendChild(bgEl); 20 | 21 | //todo: create a reusable pool for textNodes, resize if more words present 22 | 23 | return function(sceneGraph, renderSettings) { 24 | var root = sceneGraph.root; 25 | 26 | SVG.initSVG(svg, root.properties.width, root.properties.height); 27 | 28 | var groups = svg.querySelectorAll('g'); 29 | 30 | for (var i = 0; i < groups.length; i++) { 31 | groups[i].parentNode.removeChild(groups[i]); 32 | } 33 | 34 | var holderURL = renderSettings.holderSettings.flags.holderURL; 35 | var holderId = 'holder_' + (Number(new Date()) + 32768 + (0 | Math.random() * 32768)).toString(16); 36 | var sceneGroupEl = DOM.newEl('g', SVG_NS); 37 | var textGroup = root.children.holderTextGroup; 38 | var tgProps = textGroup.properties; 39 | var textGroupEl = DOM.newEl('g', SVG_NS); 40 | var tpdata = textGroup.textPositionData; 41 | var textCSSRule = '#' + holderId + ' text { ' + 42 | utils.cssProps({ 43 | 'fill': tgProps.fill, 44 | 'font-weight': tgProps.font.weight, 45 | 'font-family': tgProps.font.family + ', monospace', 46 | 'font-size': tgProps.font.size + tgProps.font.units 47 | }) + ' } '; 48 | var commentNode = xml.createComment('\n' + 'Source URL: ' + holderURL + generatorComment); 49 | var holderCSS = xml.createCDATASection(textCSSRule); 50 | var styleEl = svg.querySelector('style'); 51 | var bg = root.children.holderBg; 52 | 53 | DOM.setAttr(sceneGroupEl, { 54 | id: holderId 55 | }); 56 | 57 | svg.insertBefore(commentNode, svg.firstChild); 58 | styleEl.appendChild(holderCSS); 59 | 60 | sceneGroupEl.appendChild(bgEl); 61 | 62 | //todo: abstract this into a cross-browser SVG outline method 63 | if (bg.properties.outline) { 64 | var outlineEl = DOM.newEl('path', SVG_NS); 65 | var outlineWidth = bg.properties.outline.width; 66 | var outlineOffsetWidth = outlineWidth / 2; 67 | DOM.setAttr(outlineEl, { 68 | 'd': [ 69 | 'M', outlineOffsetWidth, outlineOffsetWidth, 70 | 'H', bg.width - outlineOffsetWidth, 71 | 'V', bg.height - outlineOffsetWidth, 72 | 'H', outlineOffsetWidth, 73 | 'V', 0, 74 | 'M', 0, outlineOffsetWidth, 75 | 'L', bg.width, bg.height - outlineOffsetWidth, 76 | 'M', 0, bg.height - outlineOffsetWidth, 77 | 'L', bg.width, outlineOffsetWidth 78 | ].join(' '), 79 | 'stroke-width': bg.properties.outline.width, 80 | 'stroke': bg.properties.outline.fill, 81 | 'fill': 'none' 82 | }); 83 | sceneGroupEl.appendChild(outlineEl); 84 | } 85 | 86 | sceneGroupEl.appendChild(textGroupEl); 87 | svg.appendChild(sceneGroupEl); 88 | 89 | DOM.setAttr(bgEl, { 90 | 'width': bg.width, 91 | 'height': bg.height, 92 | 'fill': bg.properties.fill 93 | }); 94 | 95 | textGroup.y += tpdata.boundingBox.height * 0.8; 96 | 97 | for (var lineKey in textGroup.children) { 98 | var line = textGroup.children[lineKey]; 99 | for (var wordKey in line.children) { 100 | var word = line.children[wordKey]; 101 | var x = textGroup.x + line.x + word.x; 102 | var y = textGroup.y + line.y + word.y; 103 | 104 | var textEl = DOM.newEl('text', SVG_NS); 105 | var textNode = document.createTextNode(null); 106 | 107 | DOM.setAttr(textEl, { 108 | 'x': x, 109 | 'y': y 110 | }); 111 | 112 | textNode.nodeValue = word.properties.text; 113 | textEl.appendChild(textNode); 114 | textGroupEl.appendChild(textEl); 115 | } 116 | } 117 | 118 | //todo: factor the background check up the chain, perhaps only return reference 119 | var svgString = SVG.svgStringToDataURI(SVG.serializeSVG(svg, renderSettings.engineSettings), renderSettings.mode === 'background'); 120 | return svgString; 121 | }; 122 | })(); 123 | -------------------------------------------------------------------------------- /src/lib/renderers/svg-text.js: -------------------------------------------------------------------------------- 1 | var shaven = require('../vendor/shaven').default; 2 | 3 | var SVG = require('../svg'); 4 | var constants = require('../constants'); 5 | var utils = require('../utils'); 6 | 7 | var SVG_NS = constants.svg_ns; 8 | 9 | var templates = { 10 | 'element': function (options) { 11 | var tag = options.tag; 12 | var content = options.content || ''; 13 | delete options.tag; 14 | delete options.content; 15 | return [tag, content, options]; 16 | } 17 | }; 18 | 19 | //todo: deprecate tag arg, infer tag from shape object 20 | function convertShape (shape, tag) { 21 | return templates.element({ 22 | 'tag': tag, 23 | 'width': shape.width, 24 | 'height': shape.height, 25 | 'fill': shape.properties.fill 26 | }); 27 | } 28 | 29 | function textCss (properties) { 30 | return utils.cssProps({ 31 | 'fill': properties.fill, 32 | 'font-weight': properties.font.weight, 33 | 'font-family': properties.font.family + ', monospace', 34 | 'font-size': properties.font.size + properties.font.units 35 | }); 36 | } 37 | 38 | function outlinePath (bgWidth, bgHeight, outlineWidth) { 39 | var outlineOffsetWidth = outlineWidth / 2; 40 | 41 | return [ 42 | 'M', outlineOffsetWidth, outlineOffsetWidth, 43 | 'H', bgWidth - outlineOffsetWidth, 44 | 'V', bgHeight - outlineOffsetWidth, 45 | 'H', outlineOffsetWidth, 46 | 'V', 0, 47 | 'M', 0, outlineOffsetWidth, 48 | 'L', bgWidth, bgHeight - outlineOffsetWidth, 49 | 'M', 0, bgHeight - outlineOffsetWidth, 50 | 'L', bgWidth, outlineOffsetWidth 51 | ].join(' '); 52 | } 53 | 54 | module.exports = function (sceneGraph, renderSettings) { 55 | var engineSettings = renderSettings.engineSettings; 56 | var stylesheets = engineSettings.stylesheets; 57 | var stylesheetXml = stylesheets.map(function (stylesheet) { 58 | return ''; 59 | }).join('\n'); 60 | 61 | var holderId = 'holder_' + Number(new Date()).toString(16); 62 | 63 | var root = sceneGraph.root; 64 | var textGroup = root.children.holderTextGroup; 65 | 66 | var css = '#' + holderId + ' text { ' + textCss(textGroup.properties) + ' } '; 67 | 68 | // push text down to be equally vertically aligned with canvas renderer 69 | textGroup.y += textGroup.textPositionData.boundingBox.height * 0.8; 70 | 71 | var wordTags = []; 72 | 73 | Object.keys(textGroup.children).forEach(function (lineKey) { 74 | var line = textGroup.children[lineKey]; 75 | 76 | Object.keys(line.children).forEach(function (wordKey) { 77 | var word = line.children[wordKey]; 78 | var x = textGroup.x + line.x + word.x; 79 | var y = textGroup.y + line.y + word.y; 80 | var wordTag = templates.element({ 81 | 'tag': 'text', 82 | 'content': word.properties.text, 83 | 'x': x, 84 | 'y': y 85 | }); 86 | 87 | wordTags.push(wordTag); 88 | }); 89 | }); 90 | 91 | var text = templates.element({ 92 | 'tag': 'g', 93 | 'content': wordTags 94 | }); 95 | 96 | var outline = null; 97 | 98 | if (root.children.holderBg.properties.outline) { 99 | var outlineProperties = root.children.holderBg.properties.outline; 100 | outline = templates.element({ 101 | 'tag': 'path', 102 | 'd': outlinePath(root.children.holderBg.width, root.children.holderBg.height, outlineProperties.width), 103 | 'stroke-width': outlineProperties.width, 104 | 'stroke': outlineProperties.fill, 105 | 'fill': 'none' 106 | }); 107 | } 108 | 109 | var bg = convertShape(root.children.holderBg, 'rect'); 110 | 111 | var sceneContent = []; 112 | 113 | sceneContent.push(bg); 114 | if (outlineProperties) { 115 | sceneContent.push(outline); 116 | } 117 | sceneContent.push(text); 118 | 119 | var scene = templates.element({ 120 | 'tag': 'g', 121 | 'id': holderId, 122 | 'content': sceneContent 123 | }); 124 | 125 | var style = templates.element({ 126 | 'tag': 'style', 127 | //todo: figure out how to add CDATA directive 128 | 'content': css, 129 | 'type': 'text/css' 130 | }); 131 | 132 | var defs = templates.element({ 133 | 'tag': 'defs', 134 | 'content': style 135 | }); 136 | 137 | var svg = templates.element({ 138 | 'tag': 'svg', 139 | 'content': [defs, scene], 140 | 'width': root.properties.width, 141 | 'height': root.properties.height, 142 | 'xmlns': SVG_NS, 143 | 'viewBox': [0, 0, root.properties.width, root.properties.height].join(' '), 144 | 'preserveAspectRatio': 'none' 145 | }); 146 | 147 | var output = String(shaven(svg)); 148 | 149 | if (/&(x)?#[0-9A-Fa-f]/.test(output[0])) { 150 | output = output.replace(/&#/gm, '&#'); 151 | } 152 | 153 | output = stylesheetXml + output; 154 | 155 | var svgString = SVG.svgStringToDataURI(output, renderSettings.mode === 'background'); 156 | 157 | return svgString; 158 | }; 159 | -------------------------------------------------------------------------------- /src/lib/scenegraph.js: -------------------------------------------------------------------------------- 1 | var SceneGraph = function(sceneProperties) { 2 | var nodeCount = 1; 3 | 4 | //todo: move merge to helpers section 5 | function merge(parent, child) { 6 | for (var prop in child) { 7 | parent[prop] = child[prop]; 8 | } 9 | return parent; 10 | } 11 | 12 | var SceneNode = function(name) { 13 | nodeCount++; 14 | this.parent = null; 15 | this.children = {}; 16 | this.id = nodeCount; 17 | this.name = 'n' + nodeCount; 18 | if (typeof name !== 'undefined') { 19 | this.name = name; 20 | } 21 | this.x = this.y = this.z = 0; 22 | this.width = this.height = 0; 23 | }; 24 | 25 | SceneNode.prototype.resize = function(width, height) { 26 | if (width != null) { 27 | this.width = width; 28 | } 29 | if (height != null) { 30 | this.height = height; 31 | } 32 | }; 33 | 34 | SceneNode.prototype.moveTo = function(x, y, z) { 35 | this.x = x != null ? x : this.x; 36 | this.y = y != null ? y : this.y; 37 | this.z = z != null ? z : this.z; 38 | }; 39 | 40 | SceneNode.prototype.add = function(child) { 41 | var name = child.name; 42 | if (typeof this.children[name] === 'undefined') { 43 | this.children[name] = child; 44 | child.parent = this; 45 | } else { 46 | throw 'SceneGraph: child already exists: ' + name; 47 | } 48 | }; 49 | 50 | var RootNode = function() { 51 | SceneNode.call(this, 'root'); 52 | this.properties = sceneProperties; 53 | }; 54 | 55 | RootNode.prototype = new SceneNode(); 56 | 57 | var Shape = function(name, props) { 58 | SceneNode.call(this, name); 59 | this.properties = { 60 | 'fill': '#000000' 61 | }; 62 | if (typeof props !== 'undefined') { 63 | merge(this.properties, props); 64 | } else if (typeof name !== 'undefined' && typeof name !== 'string') { 65 | throw 'SceneGraph: invalid node name'; 66 | } 67 | }; 68 | 69 | Shape.prototype = new SceneNode(); 70 | 71 | var Group = function() { 72 | Shape.apply(this, arguments); 73 | this.type = 'group'; 74 | }; 75 | 76 | Group.prototype = new Shape(); 77 | 78 | var Rect = function() { 79 | Shape.apply(this, arguments); 80 | this.type = 'rect'; 81 | }; 82 | 83 | Rect.prototype = new Shape(); 84 | 85 | var Text = function(text) { 86 | Shape.call(this); 87 | this.type = 'text'; 88 | this.properties.text = text; 89 | }; 90 | 91 | Text.prototype = new Shape(); 92 | 93 | var root = new RootNode(); 94 | 95 | this.Shape = { 96 | 'Rect': Rect, 97 | 'Text': Text, 98 | 'Group': Group 99 | }; 100 | 101 | this.root = root; 102 | return this; 103 | }; 104 | 105 | module.exports = SceneGraph; 106 | -------------------------------------------------------------------------------- /src/lib/svg.js: -------------------------------------------------------------------------------- 1 | var DOM = require('./dom'); 2 | 3 | var SVG_NS = 'http://www.w3.org/2000/svg'; 4 | var NODE_TYPE_COMMENT = 8; 5 | 6 | /** 7 | * Generic SVG element creation function 8 | * 9 | * @param svg SVG context, set to null if new 10 | * @param width Document width 11 | * @param height Document height 12 | */ 13 | exports.initSVG = function(svg, width, height) { 14 | var defs, style, initialize = false; 15 | 16 | if (svg && svg.querySelector) { 17 | style = svg.querySelector('style'); 18 | if (style === null) { 19 | initialize = true; 20 | } 21 | } else { 22 | svg = DOM.newEl('svg', SVG_NS); 23 | initialize = true; 24 | } 25 | 26 | if (initialize) { 27 | defs = DOM.newEl('defs', SVG_NS); 28 | style = DOM.newEl('style', SVG_NS); 29 | DOM.setAttr(style, { 30 | 'type': 'text/css' 31 | }); 32 | defs.appendChild(style); 33 | svg.appendChild(defs); 34 | } 35 | 36 | //IE throws an exception if this is set and Chrome requires it to be set 37 | if (svg.webkitMatchesSelector) { 38 | svg.setAttribute('xmlns', SVG_NS); 39 | } 40 | 41 | //Remove comment nodes 42 | for (var i = 0; i < svg.childNodes.length; i++) { 43 | if (svg.childNodes[i].nodeType === NODE_TYPE_COMMENT) { 44 | svg.removeChild(svg.childNodes[i]); 45 | } 46 | } 47 | 48 | //Remove CSS 49 | while (style.childNodes.length) { 50 | style.removeChild(style.childNodes[0]); 51 | } 52 | 53 | DOM.setAttr(svg, { 54 | 'width': width, 55 | 'height': height, 56 | 'viewBox': '0 0 ' + width + ' ' + height, 57 | 'preserveAspectRatio': 'none' 58 | }); 59 | 60 | return svg; 61 | }; 62 | 63 | /** 64 | * Converts serialized SVG to a string suitable for data URI use 65 | * @param svgString Serialized SVG string 66 | * @param [base64] Use base64 encoding for data URI 67 | */ 68 | exports.svgStringToDataURI = function() { 69 | var rawPrefix = 'data:image/svg+xml;charset=UTF-8,'; 70 | var base64Prefix = 'data:image/svg+xml;charset=UTF-8;base64,'; 71 | 72 | return function(svgString, base64) { 73 | if (base64) { 74 | return base64Prefix + btoa(global.unescape(encodeURIComponent(svgString))); 75 | } else { 76 | return rawPrefix + encodeURIComponent(svgString); 77 | } 78 | }; 79 | }(); 80 | 81 | /** 82 | * Returns serialized SVG with XML processing instructions 83 | * 84 | * @param svg SVG context 85 | * @param stylesheets CSS stylesheets to include 86 | */ 87 | exports.serializeSVG = function(svg, engineSettings) { 88 | if (!global.XMLSerializer) return; 89 | var serializer = new XMLSerializer(); 90 | var svgCSS = ''; 91 | var stylesheets = engineSettings.stylesheets; 92 | 93 | //External stylesheets: Processing Instruction method 94 | if (engineSettings.svgXMLStylesheet) { 95 | var xml = DOM.createXML(); 96 | //Add directives 97 | for (var i = stylesheets.length - 1; i >= 0; i--) { 98 | var csspi = xml.createProcessingInstruction('xml-stylesheet', 'href="' + stylesheets[i] + '" rel="stylesheet"'); 99 | xml.insertBefore(csspi, xml.firstChild); 100 | } 101 | 102 | xml.removeChild(xml.documentElement); 103 | svgCSS = serializer.serializeToString(xml); 104 | } 105 | 106 | var svgText = serializer.serializeToString(svg); 107 | svgText = svgText.replace(/&(#[0-9]{2,};)/g, '&$1'); 108 | return svgCSS + svgText; 109 | }; 110 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shallow object clone and merge 3 | * 4 | * @param a Object A 5 | * @param b Object B 6 | * @returns {Object} New object with all of A's properties, and all of B's properties, overwriting A's properties 7 | */ 8 | exports.extend = function(a, b) { 9 | var c = {}; 10 | for (var x in a) { 11 | if (Object.prototype.hasOwnProperty.call(a,x)) { 12 | c[x] = a[x]; 13 | } 14 | } 15 | if (b != null) { 16 | for (var y in b) { 17 | if (Object.prototype.hasOwnProperty.call(b, y)) { 18 | c[y] = b[y]; 19 | } 20 | } 21 | } 22 | return c; 23 | }; 24 | 25 | /** 26 | * Takes a k/v list of CSS properties and returns a rule 27 | * 28 | * @param props CSS properties object 29 | */ 30 | exports.cssProps = function(props) { 31 | var ret = []; 32 | for (var p in props) { 33 | if (Object.prototype.hasOwnProperty.call(props, p)) { 34 | ret.push(p + ':' + props[p]); 35 | } 36 | } 37 | return ret.join(';'); 38 | }; 39 | 40 | /** 41 | * Encodes HTML entities in a string 42 | * 43 | * @param str Input string 44 | */ 45 | exports.encodeHtmlEntity = function(str) { 46 | var buf = []; 47 | var charCode = 0; 48 | for (var i = str.length - 1; i >= 0; i--) { 49 | charCode = str.charCodeAt(i); 50 | if (charCode > 128) { 51 | buf.unshift(['&#', charCode, ';'].join('')); 52 | } else { 53 | buf.unshift(str[i]); 54 | } 55 | } 56 | return buf.join(''); 57 | }; 58 | 59 | /** 60 | * Checks if an image exists 61 | * 62 | * @param src URL of image 63 | * @param callback Callback to call once image status has been found 64 | */ 65 | exports.imageExists = function(src, callback) { 66 | var image = new Image(); 67 | image.onerror = function() { 68 | callback.call(this, false); 69 | }; 70 | image.onload = function() { 71 | callback.call(this, true); 72 | }; 73 | image.src = src; 74 | }; 75 | 76 | /** 77 | * Decodes HTML entities in a string 78 | * 79 | * @param str Input string 80 | */ 81 | exports.decodeHtmlEntity = function(str) { 82 | return str.replace(/&#(\d+);/g, function(match, dec) { 83 | return String.fromCharCode(dec); 84 | }); 85 | }; 86 | 87 | 88 | /** 89 | * Returns an element's dimensions if it's visible, `false` otherwise. 90 | * 91 | * @param el DOM element 92 | */ 93 | exports.dimensionCheck = function(el) { 94 | var dimensions = { 95 | height: el.clientHeight, 96 | width: el.clientWidth 97 | }; 98 | 99 | if (dimensions.height && dimensions.width) { 100 | return dimensions; 101 | } else { 102 | return false; 103 | } 104 | }; 105 | 106 | 107 | /** 108 | * Returns true if value is truthy or if it is "semantically truthy" 109 | * @param val 110 | */ 111 | exports.truthy = function(val) { 112 | if (typeof val === 'string') { 113 | return val === 'true' || val === 'yes' || val === '1' || val === 'on' || val === '✓'; 114 | } 115 | return !!val; 116 | }; 117 | 118 | /** 119 | * Parses input into a well-formed CSS color 120 | * @param val 121 | */ 122 | exports.parseColor = function(val) { 123 | var hexre = /(^(?:#?)[0-9a-f]{6}$)|(^(?:#?)[0-9a-f]{3}$)/i; 124 | var rgbre = /^rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/; 125 | var rgbare = /^rgba\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0*\.\d{1,}|1)\)$/; 126 | 127 | var match = val.match(hexre); 128 | var retval; 129 | 130 | if (match !== null) { 131 | retval = match[1] || match[2]; 132 | if (retval[0] !== '#') { 133 | return '#' + retval; 134 | } else { 135 | return retval; 136 | } 137 | } 138 | 139 | match = val.match(rgbre); 140 | 141 | if (match !== null) { 142 | retval = 'rgb(' + match.slice(1).join(',') + ')'; 143 | return retval; 144 | } 145 | 146 | match = val.match(rgbare); 147 | 148 | if (match !== null) { 149 | var normalizeAlpha = function (a) { return '0.' + a.split('.')[1]; }; 150 | var fixedMatch = match.slice(1).map(function (e, i) { 151 | return (i === 3) ? normalizeAlpha(e) : e; 152 | }); 153 | retval = 'rgba(' + fixedMatch.join(',') + ')'; 154 | return retval; 155 | } 156 | 157 | return null; 158 | }; 159 | 160 | /** 161 | * Provides the correct scaling ratio for canvas drawing operations on HiDPI screens (e.g. Retina displays) 162 | */ 163 | exports.canvasRatio = function () { 164 | var devicePixelRatio = 1; 165 | var backingStoreRatio = 1; 166 | 167 | if (global.document) { 168 | var canvas = global.document.createElement('canvas'); 169 | if (canvas.getContext) { 170 | var ctx = canvas.getContext('2d'); 171 | devicePixelRatio = global.devicePixelRatio || 1; 172 | backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; 173 | } 174 | } 175 | 176 | return devicePixelRatio / backingStoreRatio; 177 | }; 178 | -------------------------------------------------------------------------------- /src/lib/vendor/ondomready.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * onDomReady.js 1.4.0 (c) 2013 Tubal Martin - MIT license 3 | * 4 | * Specially modified to work with Holder.js 5 | */ 6 | 7 | function _onDomReady(win) { 8 | //Lazy loading fix for Firefox < 3.6 9 | //http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html 10 | if (document.readyState == null && document.addEventListener) { 11 | document.addEventListener("DOMContentLoaded", function DOMContentLoaded() { 12 | document.removeEventListener("DOMContentLoaded", DOMContentLoaded, false); 13 | document.readyState = "complete"; 14 | }, false); 15 | document.readyState = "loading"; 16 | } 17 | 18 | var doc = win.document, 19 | docElem = doc.documentElement, 20 | 21 | LOAD = "load", 22 | FALSE = false, 23 | ONLOAD = "on"+LOAD, 24 | COMPLETE = "complete", 25 | READYSTATE = "readyState", 26 | ATTACHEVENT = "attachEvent", 27 | DETACHEVENT = "detachEvent", 28 | ADDEVENTLISTENER = "addEventListener", 29 | DOMCONTENTLOADED = "DOMContentLoaded", 30 | ONREADYSTATECHANGE = "onreadystatechange", 31 | REMOVEEVENTLISTENER = "removeEventListener", 32 | 33 | // W3C Event model 34 | w3c = ADDEVENTLISTENER in doc, 35 | _top = FALSE, 36 | 37 | // isReady: Is the DOM ready to be used? Set to true once it occurs. 38 | isReady = FALSE, 39 | 40 | // Callbacks pending execution until DOM is ready 41 | callbacks = []; 42 | 43 | // Handle when the DOM is ready 44 | function ready( fn ) { 45 | if ( !isReady ) { 46 | 47 | // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). 48 | if ( !doc.body ) { 49 | return defer( ready ); 50 | } 51 | 52 | // Remember that the DOM is ready 53 | isReady = true; 54 | 55 | // Execute all callbacks 56 | while ( fn = callbacks.shift() ) { 57 | defer( fn ); 58 | } 59 | } 60 | } 61 | 62 | // The ready event handler 63 | function completed( event ) { 64 | // readyState === "complete" is good enough for us to call the dom ready in oldIE 65 | if ( w3c || event.type === LOAD || doc[READYSTATE] === COMPLETE ) { 66 | detach(); 67 | ready(); 68 | } 69 | } 70 | 71 | // Clean-up method for dom ready events 72 | function detach() { 73 | if ( w3c ) { 74 | doc[REMOVEEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE ); 75 | win[REMOVEEVENTLISTENER]( LOAD, completed, FALSE ); 76 | } else { 77 | doc[DETACHEVENT]( ONREADYSTATECHANGE, completed ); 78 | win[DETACHEVENT]( ONLOAD, completed ); 79 | } 80 | } 81 | 82 | // Defers a function, scheduling it to run after the current call stack has cleared. 83 | function defer( fn, wait ) { 84 | // Allow 0 to be passed 85 | setTimeout( fn, +wait >= 0 ? wait : 1 ); 86 | } 87 | 88 | // Attach the listeners: 89 | 90 | // Catch cases where onDomReady is called after the browser event has already occurred. 91 | // we once tried to use readyState "interactive" here, but it caused issues like the one 92 | // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 93 | if ( doc[READYSTATE] === COMPLETE ) { 94 | // Handle it asynchronously to allow scripts the opportunity to delay ready 95 | defer( ready ); 96 | 97 | // Standards-based browsers support DOMContentLoaded 98 | } else if ( w3c ) { 99 | // Use the handy event callback 100 | doc[ADDEVENTLISTENER]( DOMCONTENTLOADED, completed, FALSE ); 101 | 102 | // A fallback to window.onload, that will always work 103 | win[ADDEVENTLISTENER]( LOAD, completed, FALSE ); 104 | 105 | // If IE event model is used 106 | } else { 107 | // Ensure firing before onload, maybe late but safe also for iframes 108 | doc[ATTACHEVENT]( ONREADYSTATECHANGE, completed ); 109 | 110 | // A fallback to window.onload, that will always work 111 | win[ATTACHEVENT]( ONLOAD, completed ); 112 | 113 | // If IE and not a frame 114 | // continually check to see if the document is ready 115 | try { 116 | _top = win.frameElement == null && docElem; 117 | } catch(e) {} 118 | 119 | if ( _top && _top.doScroll ) { 120 | (function doScrollCheck() { 121 | if ( !isReady ) { 122 | try { 123 | // Use the trick by Diego Perini 124 | // http://javascript.nwbox.com/IEContentLoaded/ 125 | _top.doScroll("left"); 126 | } catch(e) { 127 | return defer( doScrollCheck, 50 ); 128 | } 129 | 130 | // detach all dom ready events 131 | detach(); 132 | 133 | // and execute any waiting functions 134 | ready(); 135 | } 136 | })(); 137 | } 138 | } 139 | 140 | function onDomReady( fn ) { 141 | // If DOM is ready, execute the function (async), otherwise wait 142 | isReady ? defer( fn ) : callbacks.push( fn ); 143 | } 144 | 145 | // Add version 146 | onDomReady.version = "1.4.0"; 147 | // Add method to check if DOM is ready 148 | onDomReady.isReady = function(){ 149 | return isReady; 150 | }; 151 | 152 | return onDomReady; 153 | } 154 | 155 | module.exports = typeof window !== "undefined" && _onDomReady(window); -------------------------------------------------------------------------------- /src/lib/vendor/polyfills.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | if (!window.document) return; 3 | var document = window.document; 4 | 5 | //https://github.com/inexorabletash/polyfill/blob/master/web.js 6 | if (!document.querySelectorAll) { 7 | document.querySelectorAll = function (selectors) { 8 | var style = document.createElement('style'), elements = [], element; 9 | document.documentElement.firstChild.appendChild(style); 10 | document._qsa = []; 11 | 12 | style.styleSheet.cssText = selectors + '{x-qsa:expression(document._qsa && document._qsa.push(this))}'; 13 | window.scrollBy(0, 0); 14 | style.parentNode.removeChild(style); 15 | 16 | while (document._qsa.length) { 17 | element = document._qsa.shift(); 18 | element.style.removeAttribute('x-qsa'); 19 | elements.push(element); 20 | } 21 | document._qsa = null; 22 | return elements; 23 | }; 24 | } 25 | 26 | if (!document.querySelector) { 27 | document.querySelector = function (selectors) { 28 | var elements = document.querySelectorAll(selectors); 29 | return (elements.length) ? elements[0] : null; 30 | }; 31 | } 32 | 33 | if (!document.getElementsByClassName) { 34 | document.getElementsByClassName = function (classNames) { 35 | classNames = String(classNames).replace(/^|\s+/g, '.'); 36 | return document.querySelectorAll(classNames); 37 | }; 38 | } 39 | 40 | //https://github.com/inexorabletash/polyfill 41 | // ES5 15.2.3.14 Object.keys ( O ) 42 | // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys 43 | if (!Object.keys) { 44 | Object.keys = function (o) { 45 | if (o !== Object(o)) { throw TypeError('Object.keys called on non-object'); } 46 | var ret = [], p; 47 | for (p in o) { 48 | if (Object.prototype.hasOwnProperty.call(o, p)) { 49 | ret.push(p); 50 | } 51 | } 52 | return ret; 53 | }; 54 | } 55 | 56 | // ES5 15.4.4.18 Array.prototype.forEach ( callbackfn [ , thisArg ] ) 57 | // From https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach 58 | if (!Array.prototype.forEach) { 59 | Array.prototype.forEach = function (fun /*, thisp */) { 60 | if (this === void 0 || this === null) { throw TypeError(); } 61 | 62 | var t = Object(this); 63 | var len = t.length >>> 0; 64 | if (typeof fun !== "function") { throw TypeError(); } 65 | 66 | var thisp = arguments[1], i; 67 | for (i = 0; i < len; i++) { 68 | if (i in t) { 69 | fun.call(thisp, t[i], i, t); 70 | } 71 | } 72 | }; 73 | } 74 | 75 | //https://github.com/inexorabletash/polyfill/blob/master/web.js 76 | (function (global) { 77 | var B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 78 | global.atob = global.atob || function (input) { 79 | input = String(input); 80 | var position = 0, 81 | output = [], 82 | buffer = 0, bits = 0, n; 83 | 84 | input = input.replace(/\s/g, ''); 85 | if ((input.length % 4) === 0) { input = input.replace(/=+$/, ''); } 86 | if ((input.length % 4) === 1) { throw Error('InvalidCharacterError'); } 87 | if (/[^+/0-9A-Za-z]/.test(input)) { throw Error('InvalidCharacterError'); } 88 | 89 | while (position < input.length) { 90 | n = B64_ALPHABET.indexOf(input.charAt(position)); 91 | buffer = (buffer << 6) | n; 92 | bits += 6; 93 | 94 | if (bits === 24) { 95 | output.push(String.fromCharCode((buffer >> 16) & 0xFF)); 96 | output.push(String.fromCharCode((buffer >> 8) & 0xFF)); 97 | output.push(String.fromCharCode(buffer & 0xFF)); 98 | bits = 0; 99 | buffer = 0; 100 | } 101 | position += 1; 102 | } 103 | 104 | if (bits === 12) { 105 | buffer = buffer >> 4; 106 | output.push(String.fromCharCode(buffer & 0xFF)); 107 | } else if (bits === 18) { 108 | buffer = buffer >> 2; 109 | output.push(String.fromCharCode((buffer >> 8) & 0xFF)); 110 | output.push(String.fromCharCode(buffer & 0xFF)); 111 | } 112 | 113 | return output.join(''); 114 | }; 115 | 116 | global.btoa = global.btoa || function (input) { 117 | input = String(input); 118 | var position = 0, 119 | out = [], 120 | o1, o2, o3, 121 | e1, e2, e3, e4; 122 | 123 | if (/[^\x00-\xFF]/.test(input)) { throw Error('InvalidCharacterError'); } 124 | 125 | while (position < input.length) { 126 | o1 = input.charCodeAt(position++); 127 | o2 = input.charCodeAt(position++); 128 | o3 = input.charCodeAt(position++); 129 | 130 | // 111111 112222 222233 333333 131 | e1 = o1 >> 2; 132 | e2 = ((o1 & 0x3) << 4) | (o2 >> 4); 133 | e3 = ((o2 & 0xf) << 2) | (o3 >> 6); 134 | e4 = o3 & 0x3f; 135 | 136 | if (position === input.length + 2) { 137 | e3 = 64; e4 = 64; 138 | } 139 | else if (position === input.length + 1) { 140 | e4 = 64; 141 | } 142 | 143 | out.push(B64_ALPHABET.charAt(e1), 144 | B64_ALPHABET.charAt(e2), 145 | B64_ALPHABET.charAt(e3), 146 | B64_ALPHABET.charAt(e4)); 147 | } 148 | 149 | return out.join(''); 150 | }; 151 | }(window)); 152 | 153 | //https://gist.github.com/jimeh/332357 154 | if (!Object.prototype.hasOwnProperty){ 155 | /*jshint -W001, -W103 */ 156 | Object.prototype.hasOwnProperty = function(prop) { 157 | var proto = this.__proto__ || this.constructor.prototype; 158 | return (prop in this) && (!(prop in proto) || proto[prop] !== this[prop]); 159 | }; 160 | /*jshint +W001, +W103 */ 161 | } 162 | 163 | // @license http://opensource.org/licenses/MIT 164 | // copyright Paul Irish 2015 165 | 166 | 167 | // Date.now() is supported everywhere except IE8. For IE8 we use the Date.now polyfill 168 | // github.com/Financial-Times/polyfill-service/blob/master/polyfills/Date.now/polyfill.js 169 | // as Safari 6 doesn't have support for NavigationTiming, we use a Date.now() timestamp for relative values 170 | 171 | // if you want values similar to what you'd get with real perf.now, place this towards the head of the page 172 | // but in reality, you're just getting the delta between now() calls, so it's not terribly important where it's placed 173 | 174 | 175 | (function(){ 176 | 177 | if ('performance' in window === false) { 178 | window.performance = {}; 179 | } 180 | 181 | Date.now = (Date.now || function () { // thanks IE8 182 | return new Date().getTime(); 183 | }); 184 | 185 | if ('now' in window.performance === false){ 186 | 187 | var nowOffset = Date.now(); 188 | 189 | if (performance.timing && performance.timing.navigationStart){ 190 | nowOffset = performance.timing.navigationStart; 191 | } 192 | 193 | window.performance.now = function now(){ 194 | return Date.now() - nowOffset; 195 | }; 196 | } 197 | 198 | })(); 199 | 200 | //requestAnimationFrame polyfill for older Firefox/Chrome versions 201 | if (!window.requestAnimationFrame) { 202 | if (window.webkitRequestAnimationFrame && window.webkitCancelAnimationFrame) { 203 | //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-webkit.js 204 | (function (global) { 205 | global.requestAnimationFrame = function (callback) { 206 | return webkitRequestAnimationFrame(function () { 207 | callback(global.performance.now()); 208 | }); 209 | }; 210 | 211 | global.cancelAnimationFrame = global.webkitCancelAnimationFrame; 212 | }(window)); 213 | } else if (window.mozRequestAnimationFrame && window.mozCancelAnimationFrame) { 214 | //https://github.com/Financial-Times/polyfill-service/blob/master/polyfills/requestAnimationFrame/polyfill-moz.js 215 | (function (global) { 216 | global.requestAnimationFrame = function (callback) { 217 | return mozRequestAnimationFrame(function () { 218 | callback(global.performance.now()); 219 | }); 220 | }; 221 | 222 | global.cancelAnimationFrame = global.mozCancelAnimationFrame; 223 | }(window)); 224 | } else { 225 | (function (global) { 226 | global.requestAnimationFrame = function (callback) { 227 | return global.setTimeout(callback, 1000 / 60); 228 | }; 229 | 230 | global.cancelAnimationFrame = global.clearTimeout; 231 | })(window); 232 | } 233 | } 234 | })(this); 235 | -------------------------------------------------------------------------------- /src/lib/vendor/querystring.js: -------------------------------------------------------------------------------- 1 | //Modified version of component/querystring 2 | //Changes: updated dependencies, dot notation parsing, JSHint fixes 3 | //Fork at https://github.com/imsky/querystring 4 | 5 | /** 6 | * Module dependencies. 7 | */ 8 | 9 | var encode = encodeURIComponent; 10 | var decode = decodeURIComponent; 11 | var trim = require('trim'); 12 | var type = require('component-type'); 13 | 14 | var arrayRegex = /(\w+)\[(\d+)\]/; 15 | var objectRegex = /\w+\.\w+/; 16 | 17 | /** 18 | * Parse the given query `str`. 19 | * 20 | * @param {String} str 21 | * @return {Object} 22 | * @api public 23 | */ 24 | 25 | exports.parse = function(str){ 26 | if ('string' !== typeof str) return {}; 27 | 28 | str = trim(str); 29 | if ('' === str) return {}; 30 | if ('?' === str.charAt(0)) str = str.slice(1); 31 | 32 | var obj = {}; 33 | var pairs = str.split('&'); 34 | for (var i = 0; i < pairs.length; i++) { 35 | var parts = pairs[i].split('='); 36 | var key = decode(parts[0]); 37 | var m, ctx, prop; 38 | 39 | if (m = arrayRegex.exec(key)) { 40 | obj[m[1]] = obj[m[1]] || []; 41 | obj[m[1]][m[2]] = decode(parts[1]); 42 | continue; 43 | } 44 | 45 | if (m = objectRegex.test(key)) { 46 | m = key.split('.'); 47 | ctx = obj; 48 | 49 | while (m.length) { 50 | prop = m.shift(); 51 | 52 | if (!prop.length) continue; 53 | 54 | if (!ctx[prop]) { 55 | ctx[prop] = {}; 56 | } else if (ctx[prop] && typeof ctx[prop] !== 'object') { 57 | break; 58 | } 59 | 60 | if (!m.length) { 61 | ctx[prop] = decode(parts[1]); 62 | } 63 | 64 | ctx = ctx[prop]; 65 | } 66 | 67 | continue; 68 | } 69 | 70 | obj[parts[0]] = null == parts[1] ? '' : decode(parts[1]); 71 | } 72 | 73 | return obj; 74 | }; 75 | 76 | /** 77 | * Stringify the given `obj`. 78 | * 79 | * @param {Object} obj 80 | * @return {String} 81 | * @api public 82 | */ 83 | 84 | exports.stringify = function(obj){ 85 | if (!obj) return ''; 86 | var pairs = []; 87 | 88 | for (var key in obj) { 89 | var value = obj[key]; 90 | 91 | if ('array' == type(value)) { 92 | for (var i = 0; i < value.length; ++i) { 93 | pairs.push(encode(key + '[' + i + ']') + '=' + encode(value[i])); 94 | } 95 | continue; 96 | } 97 | 98 | pairs.push(encode(key) + '=' + encode(obj[key])); 99 | } 100 | 101 | return pairs.join('&'); 102 | }; 103 | -------------------------------------------------------------------------------- /src/lib/vendor/shaven/buildTransformString.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | // Create transform string from list transform objects 8 | 9 | exports.default = function (transformObjects) { 10 | 11 | return transformObjects.map(function (transformation) { 12 | var values = []; 13 | 14 | if (transformation.type === 'rotate' && transformation.degrees) { 15 | values.push(transformation.degrees); 16 | } 17 | if (transformation.x) values.push(transformation.x); 18 | if (transformation.y) values.push(transformation.y); 19 | 20 | return transformation.type + '(' + values + ')'; 21 | }).join(' '); 22 | }; -------------------------------------------------------------------------------- /src/lib/vendor/shaven/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = { 7 | namespace: 'xhtml', 8 | autoNamespacing: true, 9 | escapeHTML: true, 10 | quotationMark: '"', 11 | quoteAttributes: true, 12 | convertTransformArray: true 13 | }; -------------------------------------------------------------------------------- /src/lib/vendor/shaven/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.attribute = attribute; 7 | exports.HTML = HTML; 8 | function attribute(string) { 9 | return string || string === 0 ? String(string).replace(/&/g, '&').replace(/"/g, '"') : ''; 10 | } 11 | 12 | function HTML(string) { 13 | return String(string).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); 14 | } -------------------------------------------------------------------------------- /src/lib/vendor/shaven/index.js: -------------------------------------------------------------------------------- 1 | // vendored shaven 1.3.0 due to published package.json including an outdated node engine 2 | module.exports = require('./server'); 3 | -------------------------------------------------------------------------------- /src/lib/vendor/shaven/mapAttributeValue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | var _buildTransformString = require('./buildTransformString'); 10 | 11 | var _buildTransformString2 = _interopRequireDefault(_buildTransformString); 12 | 13 | var _stringifyStyleObject = require('./stringifyStyleObject'); 14 | 15 | var _stringifyStyleObject2 = _interopRequireDefault(_stringifyStyleObject); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | exports.default = function (key, value) { 20 | if (value === undefined) { 21 | return ''; 22 | } 23 | 24 | if (key === 'style' && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') { 25 | return (0, _stringifyStyleObject2.default)(value); 26 | } 27 | 28 | if (key === 'transform' && Array.isArray(value)) { 29 | return (0, _buildTransformString2.default)(value); 30 | } 31 | 32 | return value; 33 | }; -------------------------------------------------------------------------------- /src/lib/vendor/shaven/parseSugarString.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (sugarString) { 8 | var tags = sugarString.match(/^[\w-]+/); 9 | var properties = { 10 | tag: tags ? tags[0] : 'div' 11 | }; 12 | var ids = sugarString.match(/#([\w-]+)/); 13 | var classes = sugarString.match(/\.[\w-]+/g); 14 | var references = sugarString.match(/\$([\w-]+)/); 15 | 16 | if (ids) properties.id = ids[1]; 17 | 18 | if (classes) { 19 | properties.class = classes.join(' ').replace(/\./g, ''); 20 | } 21 | 22 | if (references) properties.reference = references[1]; 23 | 24 | if (sugarString.endsWith('&') || sugarString.endsWith('!')) { 25 | properties.escapeHTML = false; 26 | } 27 | 28 | return properties; 29 | }; -------------------------------------------------------------------------------- /src/lib/vendor/shaven/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | exports.default = shaven; 10 | 11 | var _parseSugarString = require('./parseSugarString'); 12 | 13 | var _parseSugarString2 = _interopRequireDefault(_parseSugarString); 14 | 15 | var _escape = require('./escape'); 16 | 17 | var escape = _interopRequireWildcard(_escape); 18 | 19 | var _defaults = require('./defaults'); 20 | 21 | var _defaults2 = _interopRequireDefault(_defaults); 22 | 23 | var _mapAttributeValue = require('./mapAttributeValue'); 24 | 25 | var _mapAttributeValue2 = _interopRequireDefault(_mapAttributeValue); 26 | 27 | var _assert = require('assert'); 28 | 29 | var _assert2 = _interopRequireDefault(_assert); 30 | 31 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 32 | 33 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 34 | 35 | function shaven(arrayOrObject) { 36 | var isArray = Array.isArray(arrayOrObject); 37 | var objType = typeof arrayOrObject === 'undefined' ? 'undefined' : _typeof(arrayOrObject); 38 | 39 | if (!isArray && objType !== 'object') { 40 | throw new Error('Argument must be either an array or an object ' + 'and not ' + JSON.stringify(arrayOrObject)); 41 | } 42 | 43 | if (isArray && arrayOrObject.length === 0) { 44 | // Ignore empty arrays 45 | return {}; 46 | } 47 | 48 | var config = {}; 49 | var elementArray = []; 50 | 51 | if (Array.isArray(arrayOrObject)) { 52 | elementArray = arrayOrObject.slice(0); 53 | } else { 54 | elementArray = arrayOrObject.elementArray.slice(0); 55 | config = Object.assign(config, arrayOrObject); 56 | delete config.elementArray; 57 | } 58 | 59 | config = Object.assign({}, _defaults2.default, config, { 60 | returnObject: { // Shaven object to return at last 61 | ids: {}, 62 | references: {} 63 | } 64 | }); 65 | 66 | function createElement(sugarString) { 67 | var properties = (0, _parseSugarString2.default)(sugarString); 68 | var element = { 69 | tag: properties.tag, 70 | attr: {}, 71 | children: [] 72 | }; 73 | 74 | if (properties.id) { 75 | element.attr.id = properties.id; 76 | (0, _assert2.default)(!config.returnObject.ids.hasOwnProperty(properties.id), 'Ids must be unique and "' + properties.id + '" is already assigned'); 77 | config.returnObject.ids[properties.id] = element; 78 | } 79 | if (properties.class) { 80 | element.attr.class = properties.class; 81 | } 82 | if (properties.reference) { 83 | (0, _assert2.default)(!config.returnObject.ids.hasOwnProperty(properties.reference), 'References must be unique and "' + properties.id + '" is already assigned'); 84 | config.returnObject.references[properties.reference] = element; 85 | } 86 | 87 | config.escapeHTML = properties.escapeHTML != null ? properties.escapeHTML : config.escapeHTML; 88 | 89 | return element; 90 | } 91 | 92 | function buildDom(elemArray) { 93 | if (Array.isArray(elemArray) && elemArray.length === 0) { 94 | // Ignore empty arrays 95 | return {}; 96 | } 97 | 98 | var index = 1; 99 | var createdCallback = void 0; 100 | var selfClosingHTMLTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']; 101 | // Clone to avoid mutation problems 102 | var array = elemArray.slice(0); 103 | 104 | if (typeof array[0] === 'string') { 105 | array[0] = createElement(array[0]); 106 | } else if (Array.isArray(array[0])) { 107 | index = 0; 108 | } else { 109 | throw new Error('First element of array must be a string, ' + 'or an array and not ' + JSON.stringify(array[0])); 110 | } 111 | 112 | for (; index < array.length; index++) { 113 | 114 | // Don't render element if value is false or null 115 | if (array[index] === false || array[index] === null) { 116 | array[0] = false; 117 | break; 118 | } 119 | 120 | // Continue with next array value if current value is undefined or true 121 | else if (array[index] === undefined || array[index] === true) { 122 | continue; 123 | } else if (typeof array[index] === 'string') { 124 | if (config.escapeHTML) { 125 | // eslint-disable-next-line new-cap 126 | array[index] = escape.HTML(array[index]); 127 | } 128 | 129 | array[0].children.push(array[index]); 130 | } else if (typeof array[index] === 'number') { 131 | 132 | array[0].children.push(array[index]); 133 | } else if (Array.isArray(array[index])) { 134 | 135 | if (Array.isArray(array[index][0])) { 136 | array[index].reverse().forEach(function (subArray) { 137 | // eslint-disable-line no-loop-func 138 | array.splice(index + 1, 0, subArray); 139 | }); 140 | 141 | if (index !== 0) continue; 142 | index++; 143 | } 144 | 145 | array[index] = buildDom(array[index]); 146 | 147 | if (array[index][0]) { 148 | array[0].children.push(array[index][0]); 149 | } 150 | } else if (typeof array[index] === 'function') { 151 | createdCallback = array[index]; 152 | } else if (_typeof(array[index]) === 'object') { 153 | for (var attributeKey in array[index]) { 154 | if (!array[index].hasOwnProperty(attributeKey)) continue; 155 | 156 | var attributeValue = array[index][attributeKey]; 157 | 158 | if (array[index].hasOwnProperty(attributeKey) && attributeValue !== null && attributeValue !== false) { 159 | array[0].attr[attributeKey] = (0, _mapAttributeValue2.default)(attributeKey, attributeValue); 160 | } 161 | } 162 | } else { 163 | throw new TypeError('"' + array[index] + '" is not allowed as a value'); 164 | } 165 | } 166 | 167 | if (array[0] !== false) { 168 | var HTMLString = '<' + array[0].tag; 169 | 170 | for (var key in array[0].attr) { 171 | if (array[0].attr.hasOwnProperty(key)) { 172 | var _attributeValue = escape.attribute(array[0].attr[key]); 173 | var value = _attributeValue; 174 | 175 | if (config.quoteAttributes || /[ "'=<>]/.test(_attributeValue)) { 176 | value = config.quotationMark + _attributeValue + config.quotationMark; 177 | } 178 | 179 | HTMLString += ' ' + key + '=' + value; 180 | } 181 | } 182 | 183 | HTMLString += '>'; 184 | 185 | if (!(selfClosingHTMLTags.indexOf(array[0].tag) !== -1)) { 186 | array[0].children.forEach(function (child) { 187 | return HTMLString += child; 188 | }); 189 | 190 | HTMLString += ''; 191 | } 192 | 193 | array[0] = HTMLString; 194 | } 195 | 196 | // Return root element on index 0 197 | config.returnObject[0] = array[0]; 198 | config.returnObject.rootElement = array[0]; 199 | 200 | config.returnObject.toString = function () { 201 | return array[0]; 202 | }; 203 | 204 | if (createdCallback) createdCallback(array[0]); 205 | 206 | return config.returnObject; 207 | } 208 | 209 | return buildDom(elementArray); 210 | } 211 | 212 | shaven.setDefaults = function (object) { 213 | Object.assign(_defaults2.default, object); 214 | return shaven; 215 | }; 216 | -------------------------------------------------------------------------------- /src/lib/vendor/shaven/stringifyStyleObject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 8 | 9 | function sanitizeProperties(key, value) { 10 | if (value === null || value === false || value === undefined) return; 11 | if (typeof value === 'string' || (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') return value; 12 | 13 | return String(value); 14 | } 15 | 16 | exports.default = function (styleObject) { 17 | return JSON.stringify(styleObject, sanitizeProperties).slice(2, -2).replace(/","/g, ';').replace(/":"/g, ':').replace(/\\"/g, '\''); 18 | }; -------------------------------------------------------------------------------- /src/meteor/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: '%summary%', 3 | version: '%version%', 4 | name: 'imsky:holder', 5 | git: 'https://github.com/imsky/holder', 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('0.9.0'); 10 | api.export('Holder', 'client'); 11 | api.addFiles('holder.js', 'client'); 12 | }); 13 | -------------------------------------------------------------------------------- /src/meteor/shim.js: -------------------------------------------------------------------------------- 1 | (function(ctx, isMeteorPackage) { 2 | if (isMeteorPackage) { 3 | Holder = ctx.Holder; 4 | } 5 | })(this, typeof Meteor !== 'undefined' && typeof Package !== 'undefined'); 6 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | benchmark.js 2 | lodash.js 3 | require.js 4 | test.js 5 | ua-parser.js 6 | *.svg 7 | *.woff 8 | *.ttf 9 | *.png 10 | !index.html 11 | !detach.html 12 | -------------------------------------------------------------------------------- /test/detach.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Holder Detached DOM Testing 5 | 6 | 13 | 14 | 15 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | Plain placeholder 38 |
39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /test/holder.js: -------------------------------------------------------------------------------- 1 | ../holder.js -------------------------------------------------------------------------------- /test/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imsky/holder/178166049c8ff03847b687a99d533e8efa92f08d/test/image.jpg -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Holder Testing 5 | 6 | 7 | 8 | 9 | 23 | 24 | 25 |
26 | 27 |
28 | Plain placeholder 29 |
30 |
31 |
32 | 33 |
34 | Themed placeholder 35 |
36 |
37 |
38 | 39 |
40 | Placeholder with random theme 41 |
42 |
43 |
44 | 45 |
46 | Placeholder with rgb color values 47 |
48 |
49 |
50 | 51 |
52 | Placeholder with rgba color values 53 |
54 |
55 |
56 | 57 |
58 | Placeholder with text 59 |
60 |
61 |
62 | 63 |
64 | Placeholder with lengthy text 65 |
66 |
67 |
68 | 69 |
70 | Canvas placeholder with lengthy text 71 |
72 |
73 |
74 | 75 |
76 | Placeholder with very lengthy text 77 |
78 |
79 |
80 | 81 |
82 | Placeholder with custom newline 83 |
84 |
85 |
86 | 87 |
88 | Placeholder with disabled line wrap 89 |
90 |
91 |
92 | 93 |
94 | Placeholder with left-aligned text 95 |
96 |
97 |
98 | 99 |
100 | Placeholder with right-aligned text 101 |
102 |
103 |
104 | 105 |
106 | Placeholder with right-aligned text and custom margin 107 |
108 |
109 |
110 | 119 |
120 | Placeholder with (randomly) missing image source 121 |
122 |
123 |
124 | 125 |
126 | Placeholder using a ratio 127 |
128 |
129 |
130 |
131 | 132 |
133 |
134 | Auto-sized placeholder 135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 | Auto-sized placeholder in exact mode 143 |
144 |
145 |
146 |
147 |
148 | <div> with background placeholder 149 |
150 |
151 | 152 |
153 |
154 |
155 | <div> with manually-sized background placeholder 156 |
157 |
158 | 159 |
160 |
161 |
162 | <div> with manually-sized background placeholder with CSS 163 |
164 |
165 | 166 |
167 |
168 |
169 | <div> with data-background-src placeholder 170 |
171 |
172 | 173 |
174 |
175 |
176 | Placeholder added using addImage with a selector 177 |
178 |
179 |
180 | 181 |
182 | Auto-sized canvas placeholder with custom domain and theme 183 |
184 |
185 |
186 | 187 |
188 | Placeholder using Pacifico font 189 |
190 |
191 |
192 | 193 |
194 | object placeholder using Pacifico font 195 |
196 |
197 |
198 |
199 | 200 | 201 | 202 |
203 |
204 | 205 |
206 |
207 | Placeholders demonstrating adaptive text size 208 |
209 |
210 |
211 | 212 |
213 | Placeholder using Font Awesome 5 font 214 |
215 |
216 |
217 | 218 |
219 | object placeholder using Font Awesome 5 font 220 |
221 |
222 |
223 | 224 |
225 | Placeholder with custom font size 226 |
227 |
228 |
229 | 230 |
231 | Placeholder with custom font size specified with units 232 |
233 |
234 |
235 |
236 | 237 | 238 |
239 |
240 | SVG and canvas placeholders with outline enabled 241 |
242 |
243 |
244 |
245 | 246 | 247 |
248 |
249 | Placeholders with automatic foreground color 250 |
251 |
252 |
253 |
254 | 255 | 256 |
257 |
258 | Placeholders with automatic and default color 259 |
260 |
261 |
262 |
263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 |
272 |
273 | Placeholders with automatic foreground color, grayscale 274 |
275 |
276 |
277 |
278 |
279 | Placeholder added using addImage with a DOM node 280 |
281 |
282 |
283 | 284 |
285 | Placeholder that triggers an error due to an invalid bg color 286 |
287 |
288 |
289 | 290 |
291 | Placeholder that has a ? in text 292 |
293 |
294 |
295 | 296 |
297 | Full-page fluid placeholder 298 |
299 |
300 |
301 | 302 |
303 | Full-page fluid object placeholder 304 |
305 |
306 |
307 | 308 |
309 | Full-page fluid placeholder in literal mode 310 |
311 |
312 |
313 | 314 |
315 | Full-page fluid placeholder with embedded dimension text 316 |
317 |
318 |
319 | 320 |
321 | Full-page fluid placewholder with fixed font size 322 |
323 |
324 |
325 | 326 |
327 | Full-page fluid placeholder that toggles resizable updates when clicked 328 |
329 |
330 |
331 | 334 |
335 | Fluid placeholder inside invisible parent element. 336 |
337 |
338 |
339 | 340 |
341 | Full-page fluid placeholder with outline enabled 342 |
343 |
344 | 345 | 346 | 347 | 350 | 385 | 386 | 387 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var runner = require('./runner'); 2 | var server = require('node-http-server'); 3 | 4 | server.deploy({ 5 | 'port': 8000, 6 | 'root': __dirname 7 | }); 8 | 9 | runner({ 10 | 'browserName': 'chrome' 11 | }, function (err, retval) { 12 | console.log('Test result: ', retval); 13 | 14 | if (!retval) { 15 | process.exitCode = -1; 16 | } 17 | 18 | process.exit(); 19 | }); -------------------------------------------------------------------------------- /test/phantom.js: -------------------------------------------------------------------------------- 1 | var page = require('webpage').create(); 2 | 3 | page.onConsoleMessage = function (message) { 4 | console.log('Page: ', message); 5 | }; 6 | 7 | page.open('index.html', function (status) { 8 | console.log(status); 9 | 10 | if (status === 'success') { 11 | page.render('phantom.png'); 12 | } 13 | 14 | phantom.exit(); 15 | }); 16 | -------------------------------------------------------------------------------- /test/renderperf/.gitignore: -------------------------------------------------------------------------------- 1 | releases/* 2 | !releases/holder-master 3 | -------------------------------------------------------------------------------- /test/renderperf/fps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Holder.js Render Benchmark 5 | 6 | 9 | 10 | 11 |
12 |
Test is running…
13 | 14 | 15 | 16 | 40 | 41 | -------------------------------------------------------------------------------- /test/renderperf/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Holder.js Render Performance Benchmark 5 | 6 | 7 | 8 | 9 | 58 | 59 | -------------------------------------------------------------------------------- /test/renderperf/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | RELEASES=(v1.0 v1.1 v1.2 v1.3 v1.4 v1.5 v1.6 v1.7 v1.8 v1.9 1.9.0 v2.0 v2.1.0 v2.2.0 v2.3.0 v2.3.2 v2.4.0 v2.5.0 v2.5.2 v2.6.0 v2.6.1 v2.7.0 v2.7.1 v2.8.0 v2.8.1 v2.8.2) 4 | 5 | mkdir -p releases 6 | 7 | for release in ${RELEASES[*]} 8 | do 9 | echo "Downloading Holder $release" 10 | wget -qO- https://github.com/imsky/holder/archive/$release.tar.gz | tar xz -C releases/ 11 | done -------------------------------------------------------------------------------- /test/renderperf/testcase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Holder.js Render Benchmark 5 | 6 | 7 | 8 |
Test is running…
9 | 10 | 11 | 40 | 41 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | var client = require('webdriverio'); 2 | 3 | module.exports = function (options, cb) { 4 | var retval = true; 5 | 6 | client.remote({ 7 | 'user': process.env.SAUCE_USERNAME, 8 | 'key': process.env.SAUCE_ACCESS_KEY, 9 | 'host': 'localhost', 10 | 'port': 4445, 11 | 'desiredCapabilities': { 12 | 'browserName': options.browserName, 13 | 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, 14 | 'name': 'Holder.js Test', 15 | 'tags': [options.browserName] 16 | } 17 | }) 18 | .init() 19 | .url('http://localhost:8000') 20 | .execute(function () { 21 | var expectImages = document.querySelectorAll('img').length - document.querySelectorAll('img[data-exclude]').length; 22 | var renderedImages = document.querySelectorAll('img[data-holder-rendered]').length; 23 | return {'expected': expectImages, 'rendered': renderedImages}; 24 | }, function (err, ret) { 25 | var expected = ret.value.expected; 26 | var rendered = ret.value.rendered; 27 | console.log('Expected', expected); 28 | console.log('Rendered', rendered); 29 | if (expected !== rendered) { 30 | retval = false; 31 | } 32 | }) 33 | .pause(15 * 1000) 34 | .end(function () { 35 | cb(null, retval); 36 | }); 37 | }; 38 | --------------------------------------------------------------------------------