├── .gitattributes ├── test ├── jasmine │ ├── jasmine_favicon.png │ ├── MIT.LICENSE │ ├── boot.js │ ├── jasmine-jsreporter.js │ ├── jasmine.css │ └── jasmine-html.js ├── fixtures │ ├── test.css │ ├── test-jquery.html │ ├── test-metrics.html │ ├── test-range.html │ ├── index.html │ ├── Browserstack-logo.svg │ ├── normalize.css │ ├── test.compat.html │ ├── test.html │ └── bootstrap.css ├── helpers │ └── environment.js ├── SpecRunner.html ├── utils.test.js ├── TextMetricsSpec.js └── index.test.js ├── .gitignore ├── src ├── index.compat.js ├── index.js └── utils.js ├── .editorconfig ├── bower.json ├── babel.config.js ├── browserstack.json ├── license ├── CHANGELOG.md ├── .github └── workflows │ └── test.yaml ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /test/jasmine/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezoerb/text-metrics/HEAD/test/jasmine/jasmine_favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | .DS_Store 4 | *.log 5 | node_modules 6 | dist 7 | lib 8 | es 9 | coverage 10 | 11 | -------------------------------------------------------------------------------- /src/index.compat.js: -------------------------------------------------------------------------------- 1 | import 'core-js/modules/es7.object.get-own-property-descriptors'; 2 | import 'core-js/modules/es6.symbol'; 3 | import 'core-js/modules/es6.array.from'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/map'; 6 | 7 | export * from '.'; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-metrics", 3 | "description": "In-memory text measurement using canvas", 4 | "main": "dist/text-metrics.min.js", 5 | "authors": ["Ben Zörb "], 6 | "license": "MIT", 7 | "keywords": ["text", "canvas", "measure", "width", "height", "font-size"], 8 | "homepage": "https://github.com/bezoerb/text-metrics", 9 | "moduleType": ["amd", "globals"], 10 | "ignore": ["**/.*", "node_modules", "bower_components", "test", "tests"] 11 | } 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const process = require('node:process'); 2 | 3 | const {NODE_ENV} = process.env; 4 | 5 | module.exports = { 6 | presets: [ 7 | [ 8 | '@babel/env', 9 | { 10 | targets: { 11 | browsers: ['> 5%'], 12 | node: '10', 13 | }, 14 | exclude: ['transform-async-to-generator', 'transform-regenerator'], 15 | modules: false, 16 | loose: true, 17 | }, 18 | ], 19 | ], 20 | plugins: [ 21 | // Don't use `loose` mode here - need to copy symbols when spreading 22 | '@babel/proposal-object-rest-spread', 23 | NODE_ENV === 'test' && '@babel/transform-modules-commonjs', 24 | ].filter(Boolean), 25 | }; 26 | -------------------------------------------------------------------------------- /test/fixtures/test.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | width: 50%; 3 | height: 80px; 4 | overflow: hidden; 5 | transform: translate3d(-50%, -50%, 0); 6 | position: absolute; 7 | left: 50%; 8 | top: 50%; 9 | backface-visibility: hidden; 10 | } 11 | 12 | p { 13 | font-size: 14px; 14 | line-height: 20px; 15 | padding: 0; 16 | margin: 0; 17 | } 18 | 19 | .is-stripped { 20 | position: relative; 21 | max-height: 80px; 22 | overflow: hidden; 23 | } 24 | 25 | .is-stripped:after { 26 | content: ''; 27 | position: absolute; 28 | bottom: 0; 29 | right: 0; 30 | height: 20px; 31 | width: 50%; 32 | background-image: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,255) 80%); 33 | } 34 | -------------------------------------------------------------------------------- /browserstack.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_framework": "jasmine", 3 | "test_path": "test/SpecRunner.html", 4 | "browsers": [ 5 | { 6 | "browser": "firefox", 7 | "browser_version": "latest", 8 | "os": "OS X", 9 | "os_version": "High Sierra", 10 | "cli_key": 1 11 | }, 12 | { 13 | "browser": "safari", 14 | "browser_version": "latest", 15 | "os": "OS X", 16 | "os_version": "Mojave", 17 | "cli_key": 2 18 | }, 19 | { 20 | "browser": "firefox", 21 | "browser_version": "latest", 22 | "os": "Windows", 23 | "os_version": "7", 24 | "cli_key": 4 25 | }, 26 | { 27 | "browser": "chrome", 28 | "browser_version": "latest", 29 | "os": "Windows", 30 | "os_version": "7", 31 | "cli_key": 5 32 | }, 33 | { 34 | "browser": "edge", 35 | "browser_version": "latest", 36 | "os": "Windows", 37 | "os_version": "10", 38 | "cli_key": 9 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/helpers/environment.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | const {TestEnvironment} = require('jest-environment-jsdom'); 4 | 5 | const css = fs.readFileSync(path.join(__dirname, '../fixtures/bootstrap.css'), 'utf8'); 6 | const html = fs.readFileSync(path.join(__dirname, '../fixtures/index.html'), 'utf8'); 7 | 8 | module.exports = class CustomEnvironment extends TestEnvironment { 9 | constructor(config, options) { 10 | const {projectConfig = {}} = config; 11 | const {testEnvironmentOptions = {}} = projectConfig; 12 | testEnvironmentOptions.html = html; 13 | super({...config, projectConfig: {...projectConfig, testEnvironmentOptions}}, options); 14 | 15 | const head = this.dom.window.document.querySelectorAll('head')[0]; 16 | const style = this.dom.window.document.createElement('style'); 17 | style.type = 'text/css'; 18 | style.innerHTML = css; 19 | head.append(style); 20 | 21 | this.global.jsdom = this.dom; 22 | } 23 | 24 | teardown() { 25 | this.global.jsdom = null; 26 | 27 | return super.teardown(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/jasmine/MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2017 Pivotal Labs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Ben Zörb (sommerlaune.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.1 / 2019-06-13 2 | 3 | ================== 4 | 5 | - Bump dependencies 6 | 7 | # v1.0.0 / 2019-01-22 8 | 9 | - rewrite 10 | - drop node 6 support 11 | - add node 10 test 12 | - minor tweaks 13 | - Bump dependencies 14 | - Bump dependencies & tweaked tests 15 | 16 | # v0.4.3 / 2017-05-02 17 | 18 | - fixed line calculation 19 | 20 | # v0.4.2 / 2017-05-02 21 | 22 | - Bump dependencies 23 | - Fixed some rounding issues 24 | 25 | # v0.4.1 / 2017-02-13 26 | 27 | - Minor tweaks & readme 28 | - Add .eslintrc for codacy 29 | 30 | # v0.4.0 / 2017-02-09 31 | 32 | - Dropped full htmlentity decode in favor of filesize 33 | 34 | # v0.3.1 / 2017-02-09 35 | 36 | - Performance improvements for 'height()' 37 | 38 | # v0.3.0 / 2017-02-07 39 | 40 | - Use innerText to see
41 | 42 | # v0.2.0 / 2017-02-06 43 | 44 | - 0.2.0 45 | - Add array.includes polyfill 46 | - switch to babel-preset-env 47 | - Consider linebreak code points 48 | 49 | # v0.1.4 / 2016-12-12 50 | 51 | - 0.1.4 52 | - Trim text 53 | 54 | # v0.1.3 / 2016-12-12 55 | 56 | - 0.1.3 57 | - Trim text 58 | 59 | # v0.1.2 / 2016-12-12 60 | 61 | - 0.1.2 62 | - minor fix 63 | 64 | # v0.1.1 / 2016-12-12 65 | 66 | - 0.1.1 67 | - readme 68 | - readme 69 | - readme 70 | - pass style overwrites to lines() 71 | - keep node 5 compatibility 72 | 73 | # v0.1.0 / 2016-12-11 74 | 75 | - 0.1.0 76 | - consider letter-spacing & word-spacing 77 | 78 | # v0.0.2 / 2016-12-09 79 | 80 | - 0.0.2 81 | - linter 82 | - build 83 | - build 84 | - build 85 | - build 86 | - 0.0.1 87 | 88 | # v0.0.1 / 2016-12-08 89 | 90 | - 0.0.1 91 | - initial commit 92 | -------------------------------------------------------------------------------- /test/fixtures/test-jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testpage 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus atque beatae cum dolores doloribus eligendi 14 | eos excepturi, incidunt laboriosam laborum, placeat praesentium quam quibusdam quisquam repudiandae sit tempora 15 | veniam voluptates.

16 |
17 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/fixtures/test-metrics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testpage 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus atque beatae cum dolores doloribus eligendi 14 | eos excepturi, incidunt laboriosam laborum, placeat praesentium quam quibusdam quisquam repudiandae sit tempora 15 | veniam voluptates.

16 |
17 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /test/fixtures/test-range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testpage 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus atque beatae cum dolores doloribus eligendi 13 | eos excepturi, incidunt laboriosam laborum, placeat praesentium quam quibusdam quisquam repudiandae sit tempora 14 | veniam voluptates.

15 |
16 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /test/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v3.3.0 6 | 7 | 8 | 9 | 10 | 11 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | Lo­rem ipsum d­o­lor sit amet, con­sectur a­dipisicing elit. Aliquam atque cum dolor 69 | explicabo ★. 70 |
71 |
Lorem Ipsum
72 |
Lorem Ipsum
73 |
Lorem ipsum dolor sit amet.
74 |
Lorem ipsum dolor sit amet.
75 |
Lorem ipsum dolor sit amet.
76 |
Lorem ipsum dolor sit amet.
77 |
78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6, browser, jest */ 2 | import {CSSStyleDeclaration} from 'cssstyle'; 3 | import {utils} from '..'; 4 | 5 | describe('Utils', () => { 6 | test('isElement', () => { 7 | const element = document.querySelector('h1'); 8 | expect(utils.isElement({})).toBeFalsy(); 9 | expect(utils.isElement(1)).toBeFalsy(); 10 | expect(utils.isElement(false)).toBeFalsy(); 11 | expect(utils.isElement(null)).toBeFalsy(); 12 | expect(utils.isElement(undefined)).toBeFalsy(); 13 | expect(utils.isElement(element)).toBeTruthy(); 14 | }); 15 | 16 | test('isObject', () => { 17 | expect(utils.isObject({})).toBeTruthy(); 18 | expect(utils.isObject(1)).toBeFalsy(); 19 | expect(utils.isObject(false)).toBeFalsy(); 20 | expect(utils.isObject(null)).toBeFalsy(); 21 | expect(utils.isObject(undefined)).toBeFalsy(); 22 | expect(utils.isObject([])).toBeFalsy(); 23 | }); 24 | 25 | test('isCSSStyleDeclaration', () => { 26 | const style = new CSSStyleDeclaration(); 27 | 28 | expect(utils.isCSSStyleDeclaration(style)).toBeTruthy(); 29 | expect(utils.isCSSStyleDeclaration({})).toBeFalsy(); 30 | }); 31 | 32 | test('getStyle', () => { 33 | const element = document.querySelector('h1'); 34 | const style = utils.getStyle(element); 35 | 36 | expect(typeof style.getPropertyValue === 'function').toBeTruthy(); 37 | expect(utils.isCSSStyleDeclaration(style)).toBeTruthy(); 38 | }); 39 | 40 | test('getFont', () => { 41 | const style = new CSSStyleDeclaration(); 42 | style.setProperty('font-family', 'Helvetica, Arial'); 43 | style.setProperty('font-size', '1em'); 44 | style.setProperty('font-style', 'italic'); 45 | style.setProperty('font-weight', 'bolder'); 46 | 47 | expect(utils.getFont(style)).toBe('bolder italic 16px Helvetica, Arial'); 48 | 49 | const element = document.querySelector('h1'); 50 | expect(utils.getFont(utils.getStyle(element))).toBe('500 36px Helvetica, Arial, sans-serif'); 51 | }); 52 | 53 | test('canGetComputedStyle', () => { 54 | const element = document.querySelector('h1'); 55 | 56 | expect(utils.canGetComputedStyle(element)).toBeTruthy(); 57 | expect(utils.canGetComputedStyle({})).toBeFalsy(); 58 | }); 59 | 60 | test('getStyledText', () => { 61 | const element = document.querySelector('h1'); 62 | const style = utils.getStyle(element); 63 | const text = 'This is a test string'; 64 | 65 | expect(utils.getStyledText(text, style)).toBe('THIS IS A TEST STRING'); 66 | }); 67 | 68 | test('prop', () => { 69 | const object = {a: 42, b: '42'}; 70 | 71 | expect(utils.prop(object, 'a', 0)).toBe(42); 72 | expect(utils.prop(object, 'b')).toBe('42'); 73 | // Expect(utils.prop(obj, 'c', 1)).toBe(1); 74 | // t.not(utils.prop(obj, 'c'), 1); 75 | }); 76 | 77 | test('normalizeOptions', () => { 78 | const object = {a: 1, 'font-size': '1px', lineHeight: 1}; 79 | const normalized = utils.normalizeOptions(object); 80 | 81 | expect(normalized.a).toBe(1); 82 | expect(normalized['font-size']).toBe('1px'); 83 | expect(normalized['line-height']).toBe(1); 84 | expect(normalized.lineHeight).toBeFalsy(); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | run: 10 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [16.x, 18.x] 17 | os: [ubuntu-latest, windows-latest] 18 | 19 | steps: 20 | - name: Clone repository 21 | uses: actions/checkout@v2 22 | 23 | - name: Install system dependencies (Linux) 24 | if: startsWith(matrix.os, 'ubuntu') 25 | run: | 26 | sudo apt update 27 | sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev 28 | 29 | - name: Install system dependencies (Windows) 30 | if: startsWith(matrix.os, 'windows') 31 | run: | 32 | Invoke-WebRequest "https://ftp-osl.osuosl.org/pub/gnome/binaries/win64/gtk+/2.22/gtk+-bundle_2.22.1-20101229_win64.zip" -OutFile "gtk.zip" 33 | Expand-Archive gtk.zip -DestinationPath "C:\GTK" 34 | Invoke-WebRequest "https://downloads.sourceforge.net/project/libjpeg-turbo/2.0.4/libjpeg-turbo-2.0.4-vc64.exe" -OutFile "libjpeg.exe" -UserAgent NativeHost 35 | .\libjpeg.exe /S 36 | 37 | - name: Set up Node.js 38 | uses: actions/setup-node@v1 39 | with: 40 | node-version: ${{ matrix.node }} 41 | 42 | - name: Set up npm cache 43 | uses: actions/cache@v2 44 | with: 45 | path: ~/.npm 46 | key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} 47 | restore-keys: | 48 | ${{ runner.OS }}-node-v${{ matrix.node }}-${{ hashFiles('package.json') }}-${{ hashFiles('package-lock.json') }} 49 | ${{ runner.OS }}-node-v${{ matrix.node }}- 50 | 51 | - name: Install npm dependencies 52 | run: npm ci 53 | 54 | - name: Run tests 55 | run: npm run lint && npm run test:cov 56 | 57 | - name: Run Coveralls 58 | uses: coverallsapp/github-action@master 59 | if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.node, '18') 60 | with: 61 | github-token: '${{ secrets.GITHUB_TOKEN }}' 62 | 63 | - name: 'BrowserStack Env Setup' 64 | uses: browserstack/github-actions/setup-env@master 65 | if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.node, '18') 66 | with: 67 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 68 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 69 | 70 | - name: 'BrowserStack Local Tunnel Setup' 71 | uses: browserstack/github-actions/setup-local@master 72 | if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.node, '18') 73 | with: 74 | local-testing: start 75 | local-identifier: random 76 | 77 | - name: 'Running test on BrowserStack' 78 | if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.node, '18') 79 | run: npx browserstack-runner 80 | 81 | - name: 'BrowserStackLocal Stop' 82 | uses: browserstack/github-actions/setup-local@master 83 | if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.node, '18') 84 | with: 85 | local-testing: stop 86 | -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testpage 6 | 7 | 92 | 93 | 94 | 95 |

TEST

96 |

TEST

97 | 98 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt.

99 | 100 |
101 | Lo­rem ipsum d­o­lor sit amet, con­sectur a­dipisicing elit. Aliquam atque cum dolor explicabo 102 | ★. 103 |
104 | 105 |
unicorn
106 | 107 |
Scales to full width
108 | 109 |
110 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt. 111 |
112 | 113 |

LONG WORD TEST

114 |
115 | Craspharetrapharetragravida.Vivamusconsequatlacusvelposuerecongue.Duisaloremvitaeexauctorscelerisquenoneuturpis.Utimperdietmagnasitametjustobibendumvehicula. 116 |
117 | 118 |
119 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Aliquam atque cum dolor explicabo 120 | incidunt. 121 |
122 | 123 |
124 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam a
tque cum 125 | dolor explicabo incidunt. 126 |
127 | 128 | 129 | -------------------------------------------------------------------------------- /test/TextMetricsSpec.js: -------------------------------------------------------------------------------- 1 | describe('TextMetrics', function () { 2 | it('is available', function () { 3 | expect(textMetrics).toBeDefined(); 4 | expect(textMetrics.init).toBeDefined(); 5 | expect(textMetrics.utils).toBeDefined(); 6 | }); 7 | 8 | it('has instance all methods', function () { 9 | var instance = textMetrics.init(); 10 | expect(instance.width).toBeDefined(); 11 | expect(instance.height).toBeDefined(); 12 | expect(instance.maxFontSize).toBeDefined(); 13 | expect(instance.lines).toBeDefined(); 14 | }); 15 | 16 | it('does not break without element', function () { 17 | const instance = textMetrics.init(); 18 | 19 | expect(instance.lines()).toEqual([]); 20 | expect(instance.maxFontSize()).toBe(undefined); 21 | expect(instance.width()).toBe(0); 22 | expect(instance.height()).toBe(0); 23 | }); 24 | 25 | it('Computes width without element', function () { 26 | var expected = textMetrics.utils.getContext2d('400 30px Helvetica, Arial, sans-serif').measureText('test'); 27 | var instance = textMetrics.init({ 28 | 'font-size': '30px', 29 | 'font-weight': '400', 30 | 'font-family': 'Helvetica, Arial, sans-serif', 31 | }); 32 | 33 | var val = instance.width('test'); 34 | 35 | expect(val).toBe(expected.width); 36 | }); 37 | 38 | it('computes lines correctly', function () { 39 | var expected = [ 40 | 'Lorem ipsum dolor sit amet, con-', 41 | 'sectur adipisicing elit. Aliquam', 42 | 'atque cum dolor explicabo ★.', 43 | ]; 44 | var el = document.querySelector('[data-test="1"]'); 45 | var instance = textMetrics.init(el); 46 | 47 | var lines = instance.lines(); 48 | expect(lines.length).toBe(expected.length); 49 | for (var i = 0; i < lines.length; i++) { 50 | expect(lines[i]).toBe(expected[i]); 51 | } 52 | }); 53 | 54 | it('computes lines with text overwrite', function () { 55 | var el = document.querySelector('[data-test="1"]'); 56 | var instance = textMetrics.init(el); 57 | 58 | var lines = instance.lines('test'); 59 | expect(lines.length).toBe(1); 60 | expect(lines[0]).toEqual('test'); 61 | }); 62 | 63 | it('computes max font-size', function () { 64 | var el1 = document.querySelector('[data-test="2"]'); 65 | var el2 = document.querySelector('[data-test="3"]'); 66 | var max1 = textMetrics.init(el1).maxFontSize(); 67 | var max2 = textMetrics.init(el2).maxFontSize(); 68 | 69 | expect(max1).toEqual('49px'); 70 | expect(max2).toEqual('56px'); 71 | }); 72 | 73 | it('computes width', function () { 74 | var threshold = 4; 75 | var el1 = document.querySelector('[data-test="4"]'); 76 | var el2 = document.querySelector('[data-test="5"]'); 77 | var w1 = textMetrics.init(el1).width(); 78 | var w2 = textMetrics.init(el2).width(); 79 | 80 | expect(w1 < w2).toBeTruthy(); 81 | expect(Math.ceil(w1)).toBeGreaterThanOrEqual(el1.offsetWidth - threshold); 82 | expect(Math.ceil(w2)).toBeGreaterThanOrEqual(el2.offsetWidth - threshold); 83 | var r1 = Math.abs(Math.ceil(w1) - Math.ceil(el1.offsetWidth)); 84 | var r2 = Math.abs(Math.ceil(w2) - Math.ceil(el2.offsetWidth)); 85 | 86 | expect(r1).toBeLessThanOrEqual(threshold); 87 | expect(r2).toBeLessThanOrEqual(threshold); 88 | }); 89 | 90 | it('computes height', function () { 91 | var el1 = document.querySelector('[data-test="6"]'); 92 | var el2 = document.querySelector('[data-test="7"]'); 93 | 94 | var w1 = textMetrics.init(el1).height(); 95 | var w2 = textMetrics.init(el2).height(); 96 | expect(Math.ceil(w1)).toBeGreaterThanOrEqual(el1.offsetHeight); 97 | expect(Math.ceil(w2)).toBeGreaterThanOrEqual(el2.offsetHeight); 98 | var r1 = Math.abs(Math.ceil(w1) - Math.ceil(el1.offsetHeight)); 99 | var r2 = Math.abs(Math.ceil(w2) - Math.ceil(el2.offsetHeight)); 100 | expect(r1).toBeLessThanOrEqual(2); 101 | expect(r2).toBeLessThanOrEqual(2); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-metrics", 3 | "version": "4.0.1", 4 | "description": "An efficient text measurement set for the browser.", 5 | "license": "MIT", 6 | "repository": "bezoerb/text-metrics", 7 | "authors": [ 8 | "Ben Zörb (https://github.com/bezoerb)" 9 | ], 10 | "engines": { 11 | "node": ">= 12" 12 | }, 13 | "scripts": { 14 | "clean": "rimraf lib dist es coverage", 15 | "format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"**/*.md\"", 16 | "lint": "xo", 17 | "test": "jest", 18 | "pretest": "npm run build", 19 | "test:watch": "npm test -- --watch", 20 | "test:cov": "npm test -- --coverage", 21 | "test:browser": "npx browserstack-runner", 22 | "test:all": "npm run test:cov && npm run test:browser", 23 | "build": "microbundle -f es,cjs,umd", 24 | "dev": "microbundle watch", 25 | "prepare": "npm run clean && npm run format && npm run build" 26 | }, 27 | "files": [ 28 | "dist", 29 | "src" 30 | ], 31 | "main": "dist/text-metrics.js", 32 | "umd:main": "dist/text-metrics.js", 33 | "unpkg": "dist/text-metrics.js", 34 | "module": "dist/text-metrics.mjs", 35 | "esmodule": "dist/text-metrics.mjs", 36 | "browser": "dist/text-metrics.js", 37 | "exports": { 38 | "import": "./dist/text-metrics.mjs", 39 | "require": "./dist/text-metrics.js", 40 | "default": "./dist/text-metrics.mjs" 41 | }, 42 | "keywords": [ 43 | "browser", 44 | "javascript", 45 | "text", 46 | "text-metrics", 47 | "fit-text", 48 | "width", 49 | "font-size", 50 | "height" 51 | ], 52 | "devDependencies": { 53 | "@babel/cli": "^7.23.0", 54 | "@babel/core": "^7.23.2", 55 | "@babel/node": "^7.22.19", 56 | "@babel/plugin-external-helpers": "^7.22.5", 57 | "@babel/plugin-proposal-object-rest-spread": "^7.20.7", 58 | "@babel/plugin-transform-runtime": "^7.23.2", 59 | "@babel/preset-env": "^7.23.2", 60 | "@babel/register": "^7.22.15", 61 | "babel-core": "^7.0.0-bridge.0", 62 | "babel-eslint": "^10.1.0", 63 | "babel-jest": "^29.7.0", 64 | "browserstack-runner": "^0.9.4", 65 | "canvas": "^2.11.2", 66 | "core-js": "^3.33.0", 67 | "coveralls": "^3.1.1", 68 | "cssstyle": "^3.0.0", 69 | "eslint": "^8.51.0", 70 | "eslint-config-xo": "^0.43.1", 71 | "eslint-plugin-ava": "^14.0.0", 72 | "eslint-plugin-eslint-comments": "^3.2.0", 73 | "eslint-plugin-import": "^2.28.1", 74 | "eslint-plugin-no-use-extend-native": "^0.5.0", 75 | "eslint-plugin-node": "^11.1.0", 76 | "eslint-plugin-promise": "^6.1.1", 77 | "eslint-plugin-unicorn": "^48.0.1", 78 | "finalhandler": "^1.2.0", 79 | "get-port": "^7.0.0", 80 | "jest": "^29.7.0", 81 | "jest-environment-jsdom": "^29.7.0", 82 | "jest-mock": "^29.7.0", 83 | "jest-util": "^29.7.0", 84 | "jsdom": "^22.1.0", 85 | "microbundle": "^0.15.1", 86 | "prettier": "^3.0.3", 87 | "regenerator-runtime": "^0.14.0", 88 | "rimraf": "^5.0.5", 89 | "selenium-webdriver": "^4.14.0", 90 | "serve-static": "^1.15.0", 91 | "sinon": "^16.1.0", 92 | "xo": "^0.56.0" 93 | }, 94 | "prettier": { 95 | "trailingComma": "es5", 96 | "singleQuote": true, 97 | "printWidth": 120, 98 | "bracketSpacing": false 99 | }, 100 | "xo": { 101 | "space": 2, 102 | "prettier": true, 103 | "ignores": [ 104 | "dist", 105 | "es", 106 | "lib", 107 | "test/jasmine", 108 | "test/TextMetricsSpec.js" 109 | ], 110 | "rules": { 111 | "valid-jsdoc": 0, 112 | "import/no-unresolved": 0, 113 | "import/no-unassigned-import": 0, 114 | "unicorn/no-reduce": 0, 115 | "unicorn/prefer-module": 0, 116 | "unicorn/prefer-string-replace-all": 0 117 | } 118 | }, 119 | "npmName": "text-metrics", 120 | "npmFileMap": [ 121 | { 122 | "basePath": "/dist/", 123 | "files": [ 124 | "*.js" 125 | ] 126 | } 127 | ], 128 | "jest": { 129 | "testEnvironment": "./test/helpers/environment.js", 130 | "transform": { 131 | "^.+\\.(js|jsx)?$": "babel-jest" 132 | } 133 | }, 134 | "browserslist": "last 2 versions, ie >= 10, ie_mob >= 10, ff >= 30, chrome >= 34, safari >= 8, opera >= 23, ios >= 7, android >= 4, bb >= 10" 135 | } 136 | -------------------------------------------------------------------------------- /test/jasmine/boot.js: -------------------------------------------------------------------------------- 1 | /** 2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js` and `jasmine_html.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. 3 | 4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. 5 | 6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. 7 | 8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem 9 | */ 10 | 11 | (function () { 12 | /** 13 | * ## Require & Instantiate 14 | * 15 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. 16 | */ 17 | window.jasmine = jasmineRequire.core(jasmineRequire); 18 | 19 | /** 20 | * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. 21 | */ 22 | jasmineRequire.html(jasmine); 23 | 24 | /** 25 | * Create the Jasmine environment. This is used to run all specs in a project. 26 | */ 27 | var env = jasmine.getEnv(); 28 | 29 | /** 30 | * ## The Global Interface 31 | * 32 | * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. 33 | */ 34 | var jasmineInterface = jasmineRequire.interface(jasmine, env); 35 | 36 | /** 37 | * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. 38 | */ 39 | extend(window, jasmineInterface); 40 | 41 | /** 42 | * ## Runner Parameters 43 | * 44 | * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. 45 | */ 46 | 47 | var queryString = new jasmine.QueryString({ 48 | getWindowLocation: function () { 49 | return window.location; 50 | }, 51 | }); 52 | 53 | var filterSpecs = !!queryString.getParam('spec'); 54 | 55 | var config = { 56 | failFast: queryString.getParam('failFast'), 57 | oneFailurePerSpec: queryString.getParam('oneFailurePerSpec'), 58 | hideDisabled: queryString.getParam('hideDisabled'), 59 | }; 60 | 61 | var random = queryString.getParam('random'); 62 | 63 | if (random !== undefined && random !== '') { 64 | config.random = random; 65 | } 66 | 67 | var seed = queryString.getParam('seed'); 68 | if (seed) { 69 | config.seed = seed; 70 | } 71 | 72 | /** 73 | * ## Reporters 74 | * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). 75 | */ 76 | var htmlReporter = new jasmine.HtmlReporter({ 77 | env: env, 78 | navigateWithNewParam: function (key, value) { 79 | return queryString.navigateWithNewParam(key, value); 80 | }, 81 | addToExistingQueryString: function (key, value) { 82 | return queryString.fullStringWithNewParam(key, value); 83 | }, 84 | getContainer: function () { 85 | return document.body; 86 | }, 87 | createElement: function () { 88 | return document.createElement.apply(document, arguments); 89 | }, 90 | createTextNode: function () { 91 | return document.createTextNode.apply(document, arguments); 92 | }, 93 | timer: new jasmine.Timer(), 94 | filterSpecs: filterSpecs, 95 | }); 96 | 97 | /** 98 | * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. 99 | */ 100 | env.addReporter(jasmineInterface.jsApiReporter); 101 | env.addReporter(htmlReporter); 102 | 103 | /** 104 | * Filter which specs will be run by matching the start of the full name against the `spec` query param. 105 | */ 106 | var specFilter = new jasmine.HtmlSpecFilter({ 107 | filterString: function () { 108 | return queryString.getParam('spec'); 109 | }, 110 | }); 111 | 112 | config.specFilter = function (spec) { 113 | return specFilter.matches(spec.getFullName()); 114 | }; 115 | 116 | env.configure(config); 117 | 118 | /** 119 | * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. 120 | */ 121 | window.setTimeout = window.setTimeout; 122 | window.setInterval = window.setInterval; 123 | window.clearTimeout = window.clearTimeout; 124 | window.clearInterval = window.clearInterval; 125 | 126 | /** 127 | * ## Execution 128 | * 129 | * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. 130 | */ 131 | var currentWindowOnload = window.onload; 132 | 133 | window.onload = function () { 134 | if (currentWindowOnload) { 135 | currentWindowOnload(); 136 | } 137 | htmlReporter.initialize(); 138 | env.execute(); 139 | }; 140 | 141 | /** 142 | * Helper function for readability above. 143 | */ 144 | function extend(destination, source) { 145 | for (var property in source) destination[property] = source[property]; 146 | return destination; 147 | } 148 | })(); 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # text-metrics 2 | 3 | [![NPM version][npm-image]][npm-url] [![Build Status][ci-image]][ci-url] [![BrowserStack Status][browserstack-image]][browserstack-url] [![Download][dlcounter-image]][dlcounter-url] [![Coverage Status][coveralls-image]][coveralls-url] 4 | 5 | > An lightweight & efficient text measurement set for the browser using canvas to prevent layout reflows. 6 | 7 | ## Features 8 | 9 | - Compute width 10 | - Compute height 11 | - Compute linebreaks 12 | - Compute max font-size to fit into element 13 | 14 | ## Installation 15 | 16 | If you're using node, you can run `npm install text-metrics`. 17 | 18 | text-metrics is also available via [Bower](https://github.com/bower/bower) (`bower install text-metrics`) 19 | 20 | Alternatively if you just want to grab the file yourself, you can download either the current stable [production version][min] or the [development version][max] directly. 21 | 22 | [min]: https://raw.github.com/bezoerb/text-metrics/master/dist/text-metrics.min.js 23 | [max]: https://raw.github.com/bezoerb/text-metrics/master/dist/text-metrics.js 24 | 25 | ## Setting it up 26 | 27 | text-metrics supports AMD (e.g. RequireJS), CommonJS (e.g. Node.js) and direct usage (e.g. loading globally with a <script> tag) loading methods. 28 | You should be able to do nearly anything, and then skip to the next section anyway and have it work. Just in case though, here's some specific examples that definitely do the right thing: 29 | 30 | ### CommonsJS (e.g. Node) 31 | 32 | text-metrics needs some browser environment to run. 33 | 34 | ```javascript 35 | const textMetrics = require('text-metrics'); 36 | 37 | const el = document.querySelector('h1'); 38 | const metrics = textMetrics.init(el); 39 | 40 | metrics.width('unicorns'); 41 | // -> 210 42 | 43 | metrics.height('Some long text with automatic word wraparound'); 44 | // -> 180 45 | metrics.lines('Some long text with automatic word wraparound'); 46 | // -> ['Some long text', 'with automatic', 'word', 'wraparound'] 47 | metrics.maxFontSize('Fitting Headline'); 48 | // -> 33px 49 | ``` 50 | 51 | ### Webpack / Browserify 52 | 53 | ```javascript 54 | const textMetrics = require('text-metrics'); 55 | textMetrics.init(document.querySelector('.textblock')).lines(); 56 | ``` 57 | 58 | ### AMD (e.g. RequireJS) 59 | 60 | ```javascript 61 | define(['text-metrics'], function (textMetrics) { 62 | textMetrics.init(document.querySelector('h1')).width('unicorns'); 63 | }); 64 | ``` 65 | 66 | ### Directly in your web page: 67 | 68 | ```html 69 | 70 | 73 | ``` 74 | 75 | ## API 76 | 77 | Construct textmetrics object: 78 | 79 | `textMetrics.init([el, overwrites])` 80 | 81 | You can call textMetrics with either an HTMLElement or with an object with style overwrites or with both. 82 | e.g. 83 | 84 | ```javascript 85 | // takes styles from h1 86 | textMetrics.init(document.querySelector('h1')); 87 | 88 | // takes styles from h1 and overwrites font-size 89 | textMetrics.init(document.querySelector('h1'), {fontSize: '20px'}); 90 | 91 | // only use given styles 92 | textMetrics.init({ 93 | fontSize: '14px', 94 | lineHeight: '20px', 95 | fontFamily: 'Helvetica, Arial, sans-serif', 96 | fontWeight: 400, 97 | width: 100, 98 | }); 99 | ``` 100 | 101 | ## Methods 102 | 103 | `width([text, [options, [overwrites]]])`
104 | `height([text, [options, [overwrites]]])`
105 | `lines([text, [options, [overwrites]]])`
106 | `maxFontSize([text, [options, [overwrites]]])`
107 | 108 | #### text 109 | 110 | Type: `string` 111 | Defaults to `el.innerText` if an element is available 112 | 113 | #### options 114 | 115 | Type: `object` 116 | 117 | | key | default | description | 118 | | --------- | ------- | ------------------------------------------------------------------ | 119 | | multiline | `false` | The width of widest line instead of the width of the complete text | 120 | 121 | #### overwrites 122 | 123 | Type: `object` 124 | 125 | Use to overwrite styles 126 | 127 | ## Performance 128 | 129 | I've compared this module with a very naive jQuery implementation as well as 130 | the . See https://jsperf.com/bezoerb-text-metrics 131 | Even if `Range.getBoundingClientRect` should be considered as a performance bottleneck according to 132 | [what-forces-layout](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) by Paul Irish, 133 | i couldn't detect any sort of recalculate style and it massively outperforms `textMetrics.height()`. 134 | 135 | ## Compatibility 136 | 137 | The normal build (3,2kb gzipped) should work well on modern browsers.
138 | These builds are tested using
Browserstack 139 | 140 | ## License 141 | 142 | Copyright (c) 2016 Ben Zörb 143 | Licensed under the [MIT license](http://bezoerb.mit-license.org/). 144 | 145 | [npm-url]: https://npmjs.org/package/text-metrics 146 | [npm-image]: https://badge.fury.io/js/text-metrics.svg 147 | [ci-url]: https://github.com/bezoerb/text-metrics/actions?workflow=Tests 148 | [ci-image]: https://github.com/bezoerb/text-metrics/workflows/Tests/badge.svg 149 | [dlcounter-url]: https://www.npmjs.com/package/text-metrics 150 | [dlcounter-image]: https://img.shields.io/npm/dm/text-metrics.svg 151 | [coveralls-url]: https://coveralls.io/github/bezoerb/text-metrics?branch=master 152 | [coveralls-image]: https://coveralls.io/repos/github/bezoerb/text-metrics/badge.svg?branch=master 153 | [browserstack-url]: https://automate.browserstack.com/public-build/WHRoUjI2QnRnb0dlUDVtaUpDbFFNdHJWVEVUT2VLdUZ0QVF6bWROT2Ntdz0tLWRzUmk1dkhwQnhZQThvTWNDVzJyL2c9PQ==--933d8c999a8c7868c7133144fdcd6ff2df8248d8 154 | [browserstack-image]: https://automate.browserstack.com/badge.svg?badge_key=WHRoUjI2QnRnb0dlUDVtaUpDbFFNdHJWVEVUT2VLdUZ0QVF6bWROT2Ntdz0tLWRzUmk1dkhwQnhZQThvTWNDVzJyL2c9PQ==--933d8c999a8c7868c7133144fdcd6ff2df8248d8 155 | 156 | 157 | ## License 158 | 159 | [MIT](https://bezoerb.mit-license.org/) © [Ben Zörb](http://sommerlaune.com) 160 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6, browser */ 2 | import * as _ from './utils.js'; 3 | 4 | class TextMetrics { 5 | constructor(element, overwrites = {}) { 6 | if (!_.isElement(element) && _.isObject(element)) { 7 | this.el = undefined; 8 | this.overwrites = _.normalizeOptions(element); 9 | } else { 10 | this.el = element; 11 | this.overwrites = _.normalizeOptions(overwrites); 12 | } 13 | 14 | this.style = _.getStyle(this.el, this.overwrites); 15 | this.font = _.prop(overwrites, 'font', null) || _.getFont(this.style, this.overwrites); 16 | } 17 | 18 | padding() { 19 | return this.el 20 | ? Number.parseInt(this.style.paddingLeft || 0, 10) + Number.parseInt(this.style.paddingRight || 0, 10) 21 | : 0; 22 | } 23 | 24 | parseArgs(text, options = {}, overwrites = {}) { 25 | if (typeof text === 'object' && text) { 26 | overwrites = options; 27 | options = text || {}; 28 | text = undefined; 29 | } 30 | 31 | const styles = {...this.overwrites, ..._.normalizeOptions(overwrites)}; 32 | const ws = _.prop(styles, 'white-space') || this.style.getPropertyValue('white-space'); 33 | 34 | if (!options) { 35 | options = {}; 36 | } 37 | 38 | if (!overwrites) { 39 | options = {}; 40 | } 41 | 42 | text = 43 | !text && this.el ? _.normalizeWhitespace(_.getText(this.el), ws) : _.prepareText(_.normalizeWhitespace(text, ws)); 44 | 45 | return {text, options, overwrites, styles}; 46 | } 47 | 48 | /** 49 | * Compute Text Metrics based for given text 50 | * 51 | * @param {string} text 52 | * @param {object} options 53 | * @param {object} overwrites 54 | * @returns {function} 55 | */ 56 | width(...args) { 57 | const {text, options, overwrites, styles} = this.parseArgs(...args); 58 | 59 | if (!text) { 60 | return 0; 61 | } 62 | 63 | const font = _.getFont(this.style, styles); 64 | const letterSpacing = _.prop(styles, 'letter-spacing') || this.style.getPropertyValue('letter-spacing'); 65 | const wordSpacing = _.prop(styles, 'word-spacing') || this.style.getPropertyValue('word-spacing'); 66 | const addSpacing = _.addWordAndLetterSpacing(wordSpacing, letterSpacing); 67 | const ctx = _.getContext2d(font); 68 | const styledText = _.getStyledText(text, this.style); 69 | 70 | if (options.multiline) { 71 | // eslint-disable-next-line unicorn/no-array-reduce 72 | return this.lines(styledText, options, overwrites).reduce((result, text) => { 73 | const w = ctx.measureText(text).width + addSpacing(text); 74 | 75 | return Math.max(result, w); 76 | }, 0); 77 | } 78 | 79 | return ctx.measureText(styledText).width + addSpacing(styledText); 80 | } 81 | 82 | /** 83 | * Compute height from textbox 84 | * 85 | * @param {string} text 86 | * @param {object} options 87 | * @param {object} overwrites 88 | * @returns {number} 89 | */ 90 | height(...args) { 91 | const {text, options, styles} = this.parseArgs(...args); 92 | 93 | const lineHeight = Number.parseFloat(_.prop(styles, 'line-height') || this.style.getPropertyValue('line-height')); 94 | 95 | return Math.ceil(this.lines(text, options, styles).length * lineHeight || 0); 96 | } 97 | 98 | /** 99 | * Compute lines of text with automatic word wraparound 100 | * element styles 101 | * 102 | * @param {string} text 103 | * @param {object} options 104 | * @param {object} overwrites 105 | * @returns {*} 106 | */ 107 | lines(...args) { 108 | const {text, options, overwrites, styles} = this.parseArgs(...args); 109 | 110 | const font = _.getFont(this.style, styles); 111 | 112 | // Get max width 113 | let max = 114 | Number.parseInt(_.prop(options, 'width') || _.prop(overwrites, 'width'), 10) || 115 | _.prop(this.el, 'offsetWidth', 0) || 116 | Number.parseInt(_.prop(styles, 'width', 0), 10) || 117 | Number.parseInt(this.style.width, 10); 118 | 119 | max -= this.padding(); 120 | 121 | const wordBreak = _.prop(styles, 'word-break') || this.style.getPropertyValue('word-break'); 122 | const letterSpacing = _.prop(styles, 'letter-spacing') || this.style.getPropertyValue('letter-spacing'); 123 | const wordSpacing = _.prop(styles, 'word-spacing') || this.style.getPropertyValue('word-spacing'); 124 | const ctx = _.getContext2d(font); 125 | const styledText = _.getStyledText(text, this.style); 126 | 127 | // Different scenario when break-word is allowed 128 | if (wordBreak === 'break-all') { 129 | return _.computeLinesBreakAll({ 130 | ctx, 131 | text: styledText, 132 | max, 133 | wordSpacing, 134 | letterSpacing, 135 | }); 136 | } 137 | 138 | return _.computeLinesDefault({ 139 | ctx, 140 | text: styledText, 141 | max, 142 | wordSpacing, 143 | letterSpacing, 144 | }); 145 | } 146 | 147 | /** 148 | * Compute Text Metrics based for given text 149 | * 150 | * @param {string} text 151 | * @param {object} options 152 | * @param {object} overwrites 153 | * @returns {string} Pixelvalue e.g. 14px 154 | */ 155 | maxFontSize(...args) { 156 | const {text, options, overwrites, styles} = this.parseArgs(...args); 157 | 158 | // Simple compute function which adds the size and computes the with 159 | const compute = (size) => { 160 | return Math.ceil( 161 | this.width(text, options, { 162 | ...styles, 163 | 'font-size': size + 'px', 164 | }) 165 | ); 166 | }; 167 | 168 | // Get max width 169 | let max = 170 | Number.parseInt(_.prop(options, 'width') || _.prop(overwrites, 'width'), 10) || 171 | _.prop(this.el, 'offsetWidth', 0) || 172 | Number.parseInt(_.prop(styles, 'width', 0), 10) || 173 | Number.parseInt(this.style.width, 10); 174 | 175 | max -= this.padding(); 176 | 177 | // Start with half the max size 178 | let size = Math.floor(max / 2); 179 | let cur = compute(size); 180 | 181 | // Compute next result based on first result 182 | size = Math.floor((size / cur) * max); 183 | cur = compute(size); 184 | 185 | // Happy cause we got it already 186 | if (Math.ceil(cur) === max) { 187 | return size ? size + 'px' : undefined; 188 | } 189 | 190 | // Go on by increase/decrease pixels 191 | const greater = cur > max && size > 0; 192 | while (cur > max && size > 0) { 193 | size -= 1; 194 | cur = compute(size); 195 | } 196 | 197 | if (!greater) { 198 | while (cur < max) { 199 | cur = compute(size + 1); 200 | if (cur > max) { 201 | return size ? size + 'px' : undefined; 202 | } 203 | 204 | size += 1; 205 | } 206 | } 207 | 208 | return size ? size + 'px' : undefined; 209 | } 210 | } 211 | 212 | export const init = (element, overwrites) => new TextMetrics(element, overwrites); 213 | 214 | export const utils = {..._}; 215 | -------------------------------------------------------------------------------- /test/fixtures/Browserstack-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 19 | Browserstack-logo-white 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 44 | 48 | 52 | 56 | 62 | 66 | 71 | 76 | 81 | 86 | 90 | 91 | -------------------------------------------------------------------------------- /test/fixtures/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env es6, browser, jest */ 2 | import {init, utils} from '..'; 3 | 4 | const {getContext2d} = utils; 5 | 6 | describe('index', () => { 7 | beforeAll(async () => {}); 8 | 9 | test('Compute with without element', () => { 10 | const expected = getContext2d('400 30px Helvetica, Arial, sans-serif').measureText('test'); 11 | 12 | const value = init({ 13 | 'font-size': '30px', 14 | 'font-weight': '400', 15 | 'font-family': 'Helvetica, Arial, sans-serif', 16 | }).width('test'); 17 | 18 | expect(value).toBe(expected.width); 19 | }); 20 | 21 | test('Does not break without element', () => { 22 | const value = init(); 23 | 24 | expect(value.lines()).toMatchObject([]); 25 | expect(value.maxFontSize()).toBe(undefined); 26 | expect(value.width()).toBe(0); 27 | expect(value.height()).toBe(0); 28 | }); 29 | 30 | test('Compute width for h1', () => { 31 | const expected = getContext2d('500 36px Helvetica, Arial, sans-serif').measureText('TEST'); 32 | 33 | const element = document.querySelector('h1'); 34 | const value = init(element).width('TEST'); 35 | expect(value).toBe(expected.width); 36 | }); 37 | 38 | test('Compute width for h2', () => { 39 | // Lowercase as this get's applied via css 40 | const expected = getContext2d('500 30px Helvetica, Arial, sans-serif').measureText('test'); 41 | 42 | const element = document.querySelector('h2'); 43 | const value = init(element).width('TEST'); 44 | expect(value).toBe(expected.width); 45 | }); 46 | 47 | test('Computes width', () => { 48 | const element = document.querySelector('h1'); 49 | const metrics = init(element); 50 | const v1 = metrics.width('-'); 51 | const v2 = metrics.width('--'); 52 | const v3 = metrics.width('---'); 53 | 54 | expect(v1 < v2 < v3).toBeTruthy(); 55 | }); 56 | 57 | test('Computes maxFontSize', () => { 58 | const element = document.querySelector('#max-font-size'); 59 | const value = init(element).maxFontSize('unicorn'); 60 | expect(value).toBe('182px'); 61 | }); 62 | 63 | test('computes lines correctly', () => { 64 | const expected = [ 65 | 'Lorem ipsum dolor sit amet, con-', 66 | 'sectur adipisicing elit. Aliquam', 67 | 'atque cum dolor explicabo ★.', 68 | ]; 69 | const element = document.querySelector('[data-test="1"]'); 70 | const instance = init(element); 71 | 72 | const lines = instance.lines(); 73 | expect(lines.length).toBe(expected.length); 74 | // eslint-disable-next-line unicorn/no-for-loop 75 | for (let i = 0; i < lines.length; i++) { 76 | expect(lines[i]).toBe(expected[i]); 77 | } 78 | }); 79 | 80 | test('Computes lines', () => { 81 | const element = document.querySelector('#height'); 82 | 83 | const text = 84 | 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt.'; 85 | const expected = [ 86 | 'Lorem ipsum', 87 | 'dolor sit amet,', 88 | 'consectetur', 89 | 'adipisicing elit.', 90 | 'Aliquam atque', 91 | 'cum dolor', 92 | 'explicabo', 93 | 'incidunt.', 94 | ]; 95 | 96 | const value = init(element).lines(text); 97 | 98 | expect(value.length).toBe(expected.length); 99 | 100 | for (const [i, element] of value.entries()) { 101 | expect(element).toBe(expected[i]); 102 | } 103 | }); 104 | 105 | test('Computes lines with breaks', () => { 106 | const element = document.querySelector('#lines'); 107 | const text = 108 | 'Lo­rem ipsum d­o­lor sit amet, c­onsectur a­dipisicing elit. Aliquam atque cum dolor explicabo ★.'; 109 | const expected = [ 110 | 'Lorem', 111 | 'ipsum dolor', 112 | 'sit amet, c-', 113 | 'onsectur a-', 114 | 'dipisicing', 115 | 'elit. Aliquam', 116 | 'atque cum', 117 | 'dolor', 118 | 'explicabo', 119 | '★.', 120 | ]; 121 | 122 | const value = init(element).lines(text); 123 | 124 | expect(value.length).toBe(expected.length); 125 | 126 | for (const [i, element] of value.entries()) { 127 | expect(element).toBe(expected[i]); 128 | } 129 | }); 130 | 131 | test('Computes lines with break-all', () => { 132 | const element = document.querySelector('#lines-break'); 133 | // Need to pass text as the jsdom implementation of innerText differs from browsers 134 | // https://github.com/tmpvar/jsdom/issues/1245 135 | const text = 136 | 'Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam atque cum dolor explicabo incidunt.'; 137 | const expected = [ 138 | 'Lorem ipsum d-', 139 | 'olor sit amet, c', 140 | '—onsectur a—', 141 | 'dipisicing elit. Al', 142 | 'i', 143 | 'quam atque cu', 144 | 'm dolor explica', 145 | 'bo incidunt.', 146 | ]; 147 | 148 | const value = init(element).lines(text); 149 | 150 | expect(value.length).toBe(expected.length); 151 | 152 | for (const [i, element] of value.entries()) { 153 | expect(element).toBe(expected[i]); 154 | } 155 | }); 156 | 157 | test('Computes height', () => { 158 | const text = 159 | 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt.'; 160 | 161 | const value = init({ 162 | 'font-size': '14px', 163 | 'line-height': '20px', 164 | 'font-family': 'Helvetica, Arial, sans-serif', 165 | 'font-weight': '400', 166 | width: 100, 167 | }).height(text); 168 | 169 | expect(value).toBe(160); 170 | }); 171 | 172 | test('Consider letter- and word-spacing', () => { 173 | const referenceline = 'sit amet, consectetur'; 174 | const reference = getContext2d('500 24px Helvetica, Arial, sans-serif').measureText(referenceline); 175 | // Width + word-spacing (10px) + letter-spacing (2px) 176 | const referenceWidth = reference.width + 20 + referenceline.length * 2; 177 | const element = document.querySelector('h3'); 178 | const metrics = init(element, {'line-height': '26px'}); 179 | 180 | const lines = metrics.lines(); 181 | const width = metrics.width(null, {multiline: true}); 182 | const height = metrics.height(); 183 | 184 | const expected = [ 185 | 'Lorem ipsum dolor', 186 | 'sit amet, consectetur', 187 | 'adipisicing elit.', 188 | 'Aliquam atque cum', 189 | 'dolor explicabo', 190 | 'incidunt.', 191 | ]; 192 | 193 | expect(lines.length).toBe(expected.length); 194 | 195 | for (const [i, element] of lines.entries()) { 196 | expect(element).toBe(expected[i]); 197 | } 198 | 199 | expect(width).toBe(referenceWidth); 200 | expect(height).toBe(26 * expected.length); 201 | }); 202 | 203 | test('Consider multiple line breaking characters', () => { 204 | const text = 205 | 'Von Kabeljau über Lachs und Thunfisch bis hin zu Zander – unsere Fisch-Vielfalt wird Sie begeistern. Bestimmt!'; 206 | const lines = init({ 207 | 'font-size': '14px', 208 | 'line-height': '20px', 209 | 'font-family': 'Helvetica, Arial, sans-serif', 210 | 'font-weight': '400', 211 | width: 400, 212 | }).lines(text); 213 | 214 | const expected = [ 215 | 'Von Kabeljau über Lachs und Thunfisch bis hin zu Zander –', 216 | 'unsere Fisch-Vielfalt wird Sie begeistern. Bestimmt!', 217 | ]; 218 | 219 | expect(lines.length).toBe(expected.length); 220 | for (const [i, element] of lines.entries()) { 221 | expect(element).toBe(expected[i]); 222 | } 223 | }); 224 | 225 | test.skip('Computes lines with one very long word', () => { 226 | const element = document.querySelector('#height'); 227 | const text = 228 | 'Craspharetrapharetragravida.Vivamusconsequatlacusvelposuerecongue.Duisaloremvitaeexauctorscelerisquenoneuturpis.Utimperdietmagnasitametjustobibendumvehicula.'; 229 | const expected = [ 230 | 'Craspharetraph', 231 | 'aretragravida.Vi', 232 | 'vamusconsequ', 233 | 'atlacusvelposu', 234 | 'erecongue.Duis', 235 | 'aloremvitaeexa', 236 | 'uctorscelerisqu', 237 | 'enoneuturpis.Ut', 238 | 'imperdietmagn', 239 | 'asitametjustobi', 240 | 'bendumvehicul', 241 | 'a.', 242 | ]; 243 | 244 | const value = init(element).lines(text, {}, {'word-break': 'break-all'}); 245 | 246 | expect(value.length).toBe(expected.length); 247 | 248 | for (const [i, element] of value.entries()) { 249 | expect(element).toBe(expected[i]); 250 | } 251 | }); 252 | 253 | test('correctly handles whitespace ', () => { 254 | const text = 'a b'; 255 | const expected1 = getContext2d('400 30px sans-serif').measureText(text.replace(/\s+/, ' ')); 256 | const expected2 = getContext2d('400 30px sans-serif').measureText(text); 257 | 258 | const styles = { 259 | 'font-size': '30px', 260 | 'font-weight': '400', 261 | 'font-family': 'sans-serif', 262 | }; 263 | 264 | const valueDefault = init(styles).width(text); 265 | const valuePre = init({...styles, 'white-space': 'pre'}).width(text); 266 | const valuePreWrap = init({...styles, 'white-space': 'pre-wrap'}).width(text); 267 | const valuePreLine = init({...styles, 'white-space': 'pre-line'}).width(text); 268 | expect(valueDefault).toBe(expected1.width); 269 | expect(valuePreLine).toBe(expected1.width); 270 | expect(valuePre).toBe(expected2.width); 271 | expect(valuePreWrap).toBe(expected2.width); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /test/fixtures/test.compat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testpage 6 | 7 | 8 | 9 | 108 | 109 | 110 | 111 |

TEST

112 | 113 | 114 |

TEST

115 | 116 |

117 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt. 118 |

119 | 120 |
unicorn
121 | 122 |
123 | Scales to full width 124 |
125 | 126 |
127 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt. 128 |
129 | 130 |

LONG WORD TEST

131 |
132 | Craspharetrapharetragravida.Vivamusconsequatlacusvelposuerecongue.Duisaloremvitaeexauctorscelerisquenoneuturpis.Utimperdietmagnasitametjustobibendumvehicula. 133 |
134 | 135 |
136 | Lo­rem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Aliquam atque cum dolor explicabo ★. 137 |
138 |
139 | Lo­rem ipsum d­o­lor sit amet, c–onsectur a–dipisicing elit. Aliquam atque cum dolor explicabo ★. 140 |
141 | 142 |
143 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam atque cum dolor explicabo incidunt. 144 |
145 | 146 |
147 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Aliquam atque cum dolor explicabo 148 | incidunt. 149 | 150 |
151 | 152 |
153 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam a
tque cum 154 | dolor explicabo incidunt. 155 |
156 | 157 |
158 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam atque cum dolor explicabo incidunt. 159 |
160 | 161 | 162 | 163 | 171 | 172 | 292 | 293 | 294 | -------------------------------------------------------------------------------- /test/fixtures/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Testpage 6 | 7 | 8 | 9 | 114 | 115 | 116 | 117 |

TEST

118 | 119 | 120 | 121 | 122 |

A B

123 | 124 | 125 |

TEST

126 | 127 |

128 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt. 129 |

130 | 131 |
unicorn
132 | 133 |
134 | Scales to full width 135 |
136 | 137 |
138 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquam atque cum dolor explicabo incidunt. 139 |
140 | 141 |

LONG WORD TEST

142 |
143 | Craspharetrapharetragravida.Vivamusconsequatlacusvelposuerecongue.Duisaloremvitaeexauctorscelerisquenoneuturpis.Utimperdietmagnasitametjustobibendumvehicula. 144 |
145 | 146 |
147 | Lo­rem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Aliquam atque cum dolor explicabo ★. 148 |
149 |
150 | Lo­rem ipsum d­o­lor sit amet, c–onsectur a–dipisicing elit. Aliquam atque cum dolor explicabo ★. 151 |
152 | 153 |
154 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam atque cum dolor explicabo incidunt. 155 |
156 | 157 |
158 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Aliquam atque cum dolor explicabo 159 | incidunt. 160 | 161 |
162 | 163 |
164 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam a
tque cum 165 | dolor explicabo incidunt. 166 |
167 | 168 |
169 | Lorem ipsum d­o­lor sit amet, c—onsectur a—dipisicing elit. Ali
quam atque cum dolor explicabo incidunt. 170 |
171 | 172 | 173 | 174 | 182 | 183 | 356 | 357 | 358 | -------------------------------------------------------------------------------- /test/jasmine/jasmine-jsreporter.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of the Jasmine JSReporter project from Ivan De Marino. 3 | 4 | Copyright (C) 2011-2014 Ivan De Marino 5 | Copyright (C) 2014 Alex Treppass 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL IVAN DE MARINO BE LIABLE FOR ANY 23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 28 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | (function (jasmine) { 31 | if (!jasmine) { 32 | throw new Error("[Jasmine JSReporter] 'Jasmine' library not found"); 33 | } 34 | 35 | // ------------------------------------------------------------------------ 36 | // Jasmine JSReporter for Jasmine 1.x 37 | // ------------------------------------------------------------------------ 38 | 39 | /** 40 | * Calculate elapsed time, in Seconds. 41 | * @param startMs Start time in Milliseconds 42 | * @param finishMs Finish time in Milliseconds 43 | * @return Elapsed time in Seconds */ 44 | function elapsedSec(startMs, finishMs) { 45 | return (finishMs - startMs) / 1000; 46 | } 47 | 48 | /** 49 | * Round an amount to the given number of Digits. 50 | * If no number of digits is given, than '2' is assumed. 51 | * @param amount Amount to round 52 | * @param numOfDecDigits Number of Digits to round to. Default value is '2'. 53 | * @return Rounded amount */ 54 | function round(amount, numOfDecDigits) { 55 | numOfDecDigits = numOfDecDigits || 2; 56 | return Math.round(amount * Math.pow(10, numOfDecDigits)) / Math.pow(10, numOfDecDigits); 57 | } 58 | 59 | /** 60 | * Create a new array which contains only the failed items. 61 | * @param items Items which will be filtered 62 | * @returns {Array} of failed items */ 63 | function failures(items) { 64 | var fs = [], 65 | i, 66 | v; 67 | for (i = 0; i < items.length; i += 1) { 68 | v = items[i]; 69 | if (!v.passed_) { 70 | fs.push(v); 71 | } 72 | } 73 | return fs; 74 | } 75 | 76 | /** 77 | * Collect information about a Suite, recursively, and return a JSON result. 78 | * @param suite The Jasmine Suite to get data from 79 | */ 80 | function getSuiteData(suite) { 81 | var suiteData = { 82 | description: suite.description, 83 | durationSec: 0, 84 | specs: [], 85 | suites: [], 86 | passed: true, 87 | }, 88 | specs = suite.specs(), 89 | suites = suite.suites(), 90 | i, 91 | ilen; 92 | 93 | // Loop over all the Suite's Specs 94 | for (i = 0, ilen = specs.length; i < ilen; ++i) { 95 | suiteData.specs[i] = { 96 | description: specs[i].description, 97 | durationSec: specs[i].durationSec, 98 | passed: specs[i].results().passedCount === specs[i].results().totalCount, 99 | skipped: specs[i].results().skipped, 100 | passedCount: specs[i].results().passedCount, 101 | failedCount: specs[i].results().failedCount, 102 | totalCount: specs[i].results().totalCount, 103 | failures: failures(specs[i].results().getItems()), 104 | }; 105 | suiteData.passed = !suiteData.specs[i].passed ? false : suiteData.passed; 106 | suiteData.durationSec += suiteData.specs[i].durationSec; 107 | } 108 | 109 | // Loop over all the Suite's sub-Suites 110 | for (i = 0, ilen = suites.length; i < ilen; ++i) { 111 | suiteData.suites[i] = getSuiteData(suites[i]); //< recursive population 112 | suiteData.passed = !suiteData.suites[i].passed ? false : suiteData.passed; 113 | suiteData.durationSec += suiteData.suites[i].durationSec; 114 | } 115 | 116 | // Rounding duration numbers to 3 decimal digits 117 | suiteData.durationSec = round(suiteData.durationSec, 4); 118 | 119 | return suiteData; 120 | } 121 | 122 | var JSReporter = function () {}; 123 | 124 | JSReporter.prototype = { 125 | reportRunnerStarting: function (runner) { 126 | // Nothing to do 127 | }, 128 | 129 | reportSpecStarting: function (spec) { 130 | // Start timing this spec 131 | spec.startedAt = new Date(); 132 | }, 133 | 134 | reportSpecResults: function (spec) { 135 | // Finish timing this spec and calculate duration/delta (in sec) 136 | spec.finishedAt = new Date(); 137 | // If the spec was skipped, reportSpecStarting is never called and spec.startedAt is undefined 138 | spec.durationSec = spec.startedAt ? elapsedSec(spec.startedAt.getTime(), spec.finishedAt.getTime()) : 0; 139 | }, 140 | 141 | reportSuiteResults: function (suite) { 142 | // Nothing to do 143 | }, 144 | 145 | reportRunnerResults: function (runner) { 146 | var suites = runner.suites(), 147 | i, 148 | j, 149 | ilen; 150 | 151 | // Attach results to the "jasmine" object to make those results easy to scrap/find 152 | jasmine.runnerResults = { 153 | suites: [], 154 | durationSec: 0, 155 | passed: true, 156 | }; 157 | 158 | // Loop over all the Suites 159 | for (i = 0, ilen = suites.length, j = 0; i < ilen; ++i) { 160 | if (suites[i].parentSuite === null) { 161 | jasmine.runnerResults.suites[j] = getSuiteData(suites[i]); 162 | // If 1 suite fails, the whole runner fails 163 | jasmine.runnerResults.passed = !jasmine.runnerResults.suites[j].passed ? false : jasmine.runnerResults.passed; 164 | // Add up all the durations 165 | jasmine.runnerResults.durationSec += jasmine.runnerResults.suites[j].durationSec; 166 | j++; 167 | } 168 | } 169 | 170 | // Decorate the 'jasmine' object with getters 171 | jasmine.getJSReport = function () { 172 | if (jasmine.runnerResults) { 173 | return jasmine.runnerResults; 174 | } 175 | return null; 176 | }; 177 | jasmine.getJSReportAsString = function () { 178 | return JSON.stringify(jasmine.getJSReport()); 179 | }; 180 | }, 181 | }; 182 | 183 | // export public 184 | jasmine.JSReporter = JSReporter; 185 | 186 | // ------------------------------------------------------------------------ 187 | // Jasmine JSReporter for Jasmine 2.0 188 | // ------------------------------------------------------------------------ 189 | 190 | /* 191 | Simple timer implementation 192 | */ 193 | var Timer = function () {}; 194 | 195 | Timer.prototype.start = function () { 196 | this.startTime = new Date().getTime(); 197 | return this; 198 | }; 199 | 200 | Timer.prototype.elapsed = function () { 201 | if (this.startTime == null) { 202 | return -1; 203 | } 204 | return new Date().getTime() - this.startTime; 205 | }; 206 | 207 | /* 208 | Utility methods 209 | */ 210 | var _extend = function (obj1, obj2) { 211 | for (var prop in obj2) { 212 | obj1[prop] = obj2[prop]; 213 | } 214 | return obj1; 215 | }; 216 | var _clone = function (obj) { 217 | if (obj !== Object(obj)) { 218 | return obj; 219 | } 220 | return _extend({}, obj); 221 | }; 222 | 223 | jasmine.JSReporter2 = function () { 224 | this.specs = {}; 225 | this.suites = {}; 226 | this.rootSuites = []; 227 | this.suiteStack = []; 228 | 229 | // export methods under jasmine namespace 230 | jasmine.getJSReport = this.getJSReport; 231 | jasmine.getJSReportAsString = this.getJSReportAsString; 232 | }; 233 | 234 | var JSR = jasmine.JSReporter2.prototype; 235 | 236 | // Reporter API methods 237 | // -------------------- 238 | 239 | JSR.suiteStarted = function (suite) { 240 | suite = this._cacheSuite(suite); 241 | // build up suite tree as we go 242 | suite.specs = []; 243 | suite.suites = []; 244 | suite.passed = true; 245 | suite.parentId = this.suiteStack.slice(this.suiteStack.length - 1)[0]; 246 | if (suite.parentId) { 247 | this.suites[suite.parentId].suites.push(suite); 248 | } else { 249 | this.rootSuites.push(suite.id); 250 | } 251 | this.suiteStack.push(suite.id); 252 | suite.timer = new Timer().start(); 253 | }; 254 | 255 | JSR.suiteDone = function (suite) { 256 | suite = this._cacheSuite(suite); 257 | suite.duration = suite.timer.elapsed(); 258 | suite.durationSec = suite.duration / 1000; 259 | this.suiteStack.pop(); 260 | 261 | // maintain parent suite state 262 | var parent = this.suites[suite.parentId]; 263 | if (parent) { 264 | parent.passed = parent.passed && suite.passed; 265 | } 266 | 267 | // keep report representation clean 268 | delete suite.timer; 269 | delete suite.id; 270 | delete suite.parentId; 271 | delete suite.fullName; 272 | }; 273 | 274 | JSR.specStarted = function (spec) { 275 | spec = this._cacheSpec(spec); 276 | spec.timer = new Timer().start(); 277 | // build up suites->spec tree as we go 278 | spec.suiteId = this.suiteStack.slice(this.suiteStack.length - 1)[0]; 279 | this.suites[spec.suiteId].specs.push(spec); 280 | }; 281 | 282 | JSR.specDone = function (spec) { 283 | spec = this._cacheSpec(spec); 284 | 285 | spec.duration = spec.timer.elapsed(); 286 | spec.durationSec = spec.duration / 1000; 287 | 288 | spec.skipped = spec.status === 'pending'; 289 | spec.passed = spec.skipped || spec.status === 'passed'; 290 | 291 | spec.totalCount = spec.passedExpectations.length + spec.failedExpectations.length; 292 | spec.passedCount = spec.passedExpectations.length; 293 | spec.failedCount = spec.failedExpectations.length; 294 | spec.failures = []; 295 | 296 | for (var i = 0, j = spec.failedExpectations.length; i < j; i++) { 297 | var fail = spec.failedExpectations[i]; 298 | spec.failures.push({ 299 | type: 'expect', 300 | expected: fail.expected, 301 | passed: false, 302 | message: fail.message, 303 | matcherName: fail.matcherName, 304 | trace: { 305 | stack: fail.stack, 306 | }, 307 | }); 308 | } 309 | 310 | // maintain parent suite state 311 | var parent = this.suites[spec.suiteId]; 312 | if (spec.failed) { 313 | parent.failingSpecs.push(spec); 314 | } 315 | parent.passed = parent.passed && spec.passed; 316 | 317 | // keep report representation clean 318 | delete spec.timer; 319 | delete spec.totalExpectations; 320 | delete spec.passedExpectations; 321 | delete spec.suiteId; 322 | delete spec.fullName; 323 | delete spec.id; 324 | delete spec.status; 325 | delete spec.failedExpectations; 326 | }; 327 | 328 | JSR.jasmineDone = function () { 329 | this._buildReport(); 330 | }; 331 | 332 | JSR.getJSReport = function () { 333 | if (jasmine.jsReport) { 334 | return jasmine.jsReport; 335 | } 336 | }; 337 | 338 | JSR.getJSReportAsString = function () { 339 | if (jasmine.jsReport) { 340 | return JSON.stringify(jasmine.jsReport); 341 | } 342 | }; 343 | 344 | // Private methods 345 | // --------------- 346 | 347 | JSR._haveSpec = function (spec) { 348 | return this.specs[spec.id] != null; 349 | }; 350 | 351 | JSR._cacheSpec = function (spec) { 352 | var existing = this.specs[spec.id]; 353 | if (existing == null) { 354 | existing = this.specs[spec.id] = _clone(spec); 355 | } else { 356 | _extend(existing, spec); 357 | } 358 | return existing; 359 | }; 360 | 361 | JSR._haveSuite = function (suite) { 362 | return this.suites[suite.id] != null; 363 | }; 364 | 365 | JSR._cacheSuite = function (suite) { 366 | var existing = this.suites[suite.id]; 367 | if (existing == null) { 368 | existing = this.suites[suite.id] = _clone(suite); 369 | } else { 370 | _extend(existing, suite); 371 | } 372 | return existing; 373 | }; 374 | 375 | JSR._buildReport = function () { 376 | var overallDuration = 0; 377 | var overallPassed = true; 378 | var overallSuites = []; 379 | 380 | for (var i = 0, j = this.rootSuites.length; i < j; i++) { 381 | var suite = this.suites[this.rootSuites[i]]; 382 | overallDuration += suite.duration; 383 | overallPassed = overallPassed && suite.passed; 384 | overallSuites.push(suite); 385 | } 386 | 387 | jasmine.jsReport = { 388 | passed: overallPassed, 389 | durationSec: overallDuration / 1000, 390 | suites: overallSuites, 391 | }; 392 | }; 393 | })(jasmine); 394 | -------------------------------------------------------------------------------- /test/fixtures/bootstrap.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=eacc7dbdd9726f77f9e3a4881c7a796d) 9 | * Config saved to config.json and https://gist.github.com/eacc7dbdd9726f77f9e3a4881c7a796d 10 | */ 11 | /*! 12 | * Bootstrap v3.3.6 (http://getbootstrap.com) 13 | * Copyright 2011-2015 Twitter, Inc. 14 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 15 | */ 16 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 17 | html { 18 | font-family: sans-serif; 19 | -ms-text-size-adjust: 100%; 20 | -webkit-text-size-adjust: 100%; 21 | } 22 | body { 23 | margin: 0; 24 | } 25 | article, 26 | aside, 27 | details, 28 | figcaption, 29 | figure, 30 | footer, 31 | header, 32 | hgroup, 33 | main, 34 | menu, 35 | nav, 36 | section, 37 | summary { 38 | display: block; 39 | } 40 | audio, 41 | canvas, 42 | progress, 43 | video { 44 | display: inline-block; 45 | vertical-align: baseline; 46 | } 47 | audio:not([controls]) { 48 | display: none; 49 | height: 0; 50 | } 51 | [hidden], 52 | template { 53 | display: none; 54 | } 55 | a { 56 | background-color: transparent; 57 | } 58 | a:active, 59 | a:hover { 60 | outline: 0; 61 | } 62 | abbr[title] { 63 | border-bottom: 1px dotted; 64 | } 65 | b, 66 | strong { 67 | font-weight: bold; 68 | } 69 | dfn { 70 | font-style: italic; 71 | } 72 | h1 { 73 | font-size: 2em; 74 | margin: 0.67em 0; 75 | } 76 | mark { 77 | background: #ff0; 78 | color: #000; 79 | } 80 | small { 81 | font-size: 80%; 82 | } 83 | sub, 84 | sup { 85 | font-size: 75%; 86 | line-height: 0; 87 | position: relative; 88 | vertical-align: baseline; 89 | } 90 | sup { 91 | top: -0.5em; 92 | } 93 | sub { 94 | bottom: -0.25em; 95 | } 96 | img { 97 | border: 0; 98 | } 99 | svg:not(:root) { 100 | overflow: hidden; 101 | } 102 | figure { 103 | margin: 1em 40px; 104 | } 105 | hr { 106 | -webkit-box-sizing: content-box; 107 | -moz-box-sizing: content-box; 108 | box-sizing: content-box; 109 | height: 0; 110 | } 111 | pre { 112 | overflow: auto; 113 | } 114 | code, 115 | kbd, 116 | pre, 117 | samp { 118 | font-family: monospace, monospace; 119 | font-size: 1em; 120 | } 121 | button, 122 | input, 123 | optgroup, 124 | select, 125 | textarea { 126 | color: inherit; 127 | font: inherit; 128 | margin: 0; 129 | } 130 | button { 131 | overflow: visible; 132 | } 133 | button, 134 | select { 135 | text-transform: none; 136 | } 137 | button, 138 | html input[type="button"], 139 | input[type="reset"], 140 | input[type="submit"] { 141 | -webkit-appearance: button; 142 | cursor: pointer; 143 | } 144 | button[disabled], 145 | html input[disabled] { 146 | cursor: default; 147 | } 148 | button::-moz-focus-inner, 149 | input::-moz-focus-inner { 150 | border: 0; 151 | padding: 0; 152 | } 153 | input { 154 | line-height: normal; 155 | } 156 | input[type="checkbox"], 157 | input[type="radio"] { 158 | -webkit-box-sizing: border-box; 159 | -moz-box-sizing: border-box; 160 | box-sizing: border-box; 161 | padding: 0; 162 | } 163 | input[type="number"]::-webkit-inner-spin-button, 164 | input[type="number"]::-webkit-outer-spin-button { 165 | height: auto; 166 | } 167 | input[type="search"] { 168 | -webkit-appearance: textfield; 169 | -webkit-box-sizing: content-box; 170 | -moz-box-sizing: content-box; 171 | box-sizing: content-box; 172 | } 173 | input[type="search"]::-webkit-search-cancel-button, 174 | input[type="search"]::-webkit-search-decoration { 175 | -webkit-appearance: none; 176 | } 177 | fieldset { 178 | border: 1px solid #c0c0c0; 179 | margin: 0 2px; 180 | padding: 0.35em 0.625em 0.75em; 181 | } 182 | legend { 183 | border: 0; 184 | padding: 0; 185 | } 186 | textarea { 187 | overflow: auto; 188 | } 189 | optgroup { 190 | font-weight: bold; 191 | } 192 | table { 193 | border-collapse: collapse; 194 | border-spacing: 0; 195 | } 196 | td, 197 | th { 198 | padding: 0; 199 | } 200 | * { 201 | -webkit-box-sizing: border-box; 202 | -moz-box-sizing: border-box; 203 | box-sizing: border-box; 204 | } 205 | *:before, 206 | *:after { 207 | -webkit-box-sizing: border-box; 208 | -moz-box-sizing: border-box; 209 | box-sizing: border-box; 210 | } 211 | html { 212 | font-size: 10px; 213 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 214 | } 215 | body { 216 | font-family: Helvetica, Arial, sans-serif; 217 | font-size: 14px; 218 | line-height: 1.42857143; 219 | color: #333333; 220 | background-color: #ffffff; 221 | } 222 | input, 223 | button, 224 | select, 225 | textarea { 226 | font-family: inherit; 227 | font-size: inherit; 228 | line-height: inherit; 229 | } 230 | a { 231 | color: #337ab7; 232 | text-decoration: none; 233 | } 234 | a:hover, 235 | a:focus { 236 | color: #23527c; 237 | text-decoration: underline; 238 | } 239 | a:focus { 240 | outline: thin dotted; 241 | outline: 5px auto -webkit-focus-ring-color; 242 | outline-offset: -2px; 243 | } 244 | figure { 245 | margin: 0; 246 | } 247 | img { 248 | vertical-align: middle; 249 | } 250 | .img-responsive { 251 | display: block; 252 | max-width: 100%; 253 | height: auto; 254 | } 255 | .img-rounded { 256 | border-radius: 6px; 257 | } 258 | .img-thumbnail { 259 | padding: 4px; 260 | line-height: 1.42857143; 261 | background-color: #ffffff; 262 | border: 1px solid #dddddd; 263 | border-radius: 4px; 264 | -webkit-transition: all 0.2s ease-in-out; 265 | -o-transition: all 0.2s ease-in-out; 266 | transition: all 0.2s ease-in-out; 267 | display: inline-block; 268 | max-width: 100%; 269 | height: auto; 270 | } 271 | .img-circle { 272 | border-radius: 50%; 273 | } 274 | hr { 275 | margin-top: 20px; 276 | margin-bottom: 20px; 277 | border: 0; 278 | border-top: 1px solid #eeeeee; 279 | } 280 | .sr-only { 281 | position: absolute; 282 | width: 1px; 283 | height: 1px; 284 | margin: -1px; 285 | padding: 0; 286 | overflow: hidden; 287 | clip: rect(0, 0, 0, 0); 288 | border: 0; 289 | } 290 | .sr-only-focusable:active, 291 | .sr-only-focusable:focus { 292 | position: static; 293 | width: auto; 294 | height: auto; 295 | margin: 0; 296 | overflow: visible; 297 | clip: auto; 298 | } 299 | [role="button"] { 300 | cursor: pointer; 301 | } 302 | h1, 303 | h2, 304 | h3, 305 | h4, 306 | h5, 307 | h6, 308 | .h1, 309 | .h2, 310 | .h3, 311 | .h4, 312 | .h5, 313 | .h6 { 314 | font-family: Helvetica, Arial, sans-serif; 315 | font-weight: 500; 316 | line-height: 1.1; 317 | color: inherit; 318 | } 319 | h1 small, 320 | h2 small, 321 | h3 small, 322 | h4 small, 323 | h5 small, 324 | h6 small, 325 | .h1 small, 326 | .h2 small, 327 | .h3 small, 328 | .h4 small, 329 | .h5 small, 330 | .h6 small, 331 | h1 .small, 332 | h2 .small, 333 | h3 .small, 334 | h4 .small, 335 | h5 .small, 336 | h6 .small, 337 | .h1 .small, 338 | .h2 .small, 339 | .h3 .small, 340 | .h4 .small, 341 | .h5 .small, 342 | .h6 .small { 343 | font-weight: normal; 344 | line-height: 1; 345 | color: #777777; 346 | } 347 | h1, 348 | .h1, 349 | h2, 350 | .h2, 351 | h3, 352 | .h3 { 353 | margin-top: 20px; 354 | margin-bottom: 10px; 355 | } 356 | h1 small, 357 | .h1 small, 358 | h2 small, 359 | .h2 small, 360 | h3 small, 361 | .h3 small, 362 | h1 .small, 363 | .h1 .small, 364 | h2 .small, 365 | .h2 .small, 366 | h3 .small, 367 | .h3 .small { 368 | font-size: 65%; 369 | } 370 | h4, 371 | .h4, 372 | h5, 373 | .h5, 374 | h6, 375 | .h6 { 376 | margin-top: 10px; 377 | margin-bottom: 10px; 378 | } 379 | h4 small, 380 | .h4 small, 381 | h5 small, 382 | .h5 small, 383 | h6 small, 384 | .h6 small, 385 | h4 .small, 386 | .h4 .small, 387 | h5 .small, 388 | .h5 .small, 389 | h6 .small, 390 | .h6 .small { 391 | font-size: 75%; 392 | } 393 | h1, 394 | .h1 { 395 | font-size: 36px; 396 | } 397 | h2, 398 | .h2 { 399 | font-size: 30px; 400 | } 401 | h3, 402 | .h3 { 403 | font-size: 24px; 404 | } 405 | h4, 406 | .h4 { 407 | font-size: 18px; 408 | } 409 | h5, 410 | .h5 { 411 | font-size: 14px; 412 | } 413 | h6, 414 | .h6 { 415 | font-size: 12px; 416 | } 417 | p { 418 | margin: 0 0 10px; 419 | } 420 | .lead { 421 | margin-bottom: 20px; 422 | font-size: 16px; 423 | font-weight: 300; 424 | line-height: 1.4; 425 | } 426 | @media (min-width: 768px) { 427 | .lead { 428 | font-size: 21px; 429 | } 430 | } 431 | small, 432 | .small { 433 | font-size: 85%; 434 | } 435 | mark, 436 | .mark { 437 | background-color: #fcf8e3; 438 | padding: .2em; 439 | } 440 | .text-left { 441 | text-align: left; 442 | } 443 | .text-right { 444 | text-align: right; 445 | } 446 | .text-center { 447 | text-align: center; 448 | } 449 | .text-justify { 450 | text-align: justify; 451 | } 452 | .text-nowrap { 453 | white-space: nowrap; 454 | } 455 | .text-lowercase { 456 | text-transform: lowercase; 457 | } 458 | .text-uppercase { 459 | text-transform: uppercase; 460 | } 461 | .text-capitalize { 462 | text-transform: capitalize; 463 | } 464 | .text-muted { 465 | color: #777777; 466 | } 467 | .text-primary { 468 | color: #337ab7; 469 | } 470 | a.text-primary:hover, 471 | a.text-primary:focus { 472 | color: #286090; 473 | } 474 | .text-success { 475 | color: #3c763d; 476 | } 477 | a.text-success:hover, 478 | a.text-success:focus { 479 | color: #2b542c; 480 | } 481 | .text-info { 482 | color: #31708f; 483 | } 484 | a.text-info:hover, 485 | a.text-info:focus { 486 | color: #245269; 487 | } 488 | .text-warning { 489 | color: #8a6d3b; 490 | } 491 | a.text-warning:hover, 492 | a.text-warning:focus { 493 | color: #66512c; 494 | } 495 | .text-danger { 496 | color: #a94442; 497 | } 498 | a.text-danger:hover, 499 | a.text-danger:focus { 500 | color: #843534; 501 | } 502 | .bg-primary { 503 | color: #fff; 504 | background-color: #337ab7; 505 | } 506 | a.bg-primary:hover, 507 | a.bg-primary:focus { 508 | background-color: #286090; 509 | } 510 | .bg-success { 511 | background-color: #dff0d8; 512 | } 513 | a.bg-success:hover, 514 | a.bg-success:focus { 515 | background-color: #c1e2b3; 516 | } 517 | .bg-info { 518 | background-color: #d9edf7; 519 | } 520 | a.bg-info:hover, 521 | a.bg-info:focus { 522 | background-color: #afd9ee; 523 | } 524 | .bg-warning { 525 | background-color: #fcf8e3; 526 | } 527 | a.bg-warning:hover, 528 | a.bg-warning:focus { 529 | background-color: #f7ecb5; 530 | } 531 | .bg-danger { 532 | background-color: #f2dede; 533 | } 534 | a.bg-danger:hover, 535 | a.bg-danger:focus { 536 | background-color: #e4b9b9; 537 | } 538 | .page-header { 539 | padding-bottom: 9px; 540 | margin: 40px 0 20px; 541 | border-bottom: 1px solid #eeeeee; 542 | } 543 | ul, 544 | ol { 545 | margin-top: 0; 546 | margin-bottom: 10px; 547 | } 548 | ul ul, 549 | ol ul, 550 | ul ol, 551 | ol ol { 552 | margin-bottom: 0; 553 | } 554 | .list-unstyled { 555 | padding-left: 0; 556 | list-style: none; 557 | } 558 | .list-inline { 559 | padding-left: 0; 560 | list-style: none; 561 | margin-left: -5px; 562 | } 563 | .list-inline > li { 564 | display: inline-block; 565 | padding-left: 5px; 566 | padding-right: 5px; 567 | } 568 | dl { 569 | margin-top: 0; 570 | margin-bottom: 20px; 571 | } 572 | dt, 573 | dd { 574 | line-height: 1.42857143; 575 | } 576 | dt { 577 | font-weight: bold; 578 | } 579 | dd { 580 | margin-left: 0; 581 | } 582 | @media (min-width: 768px) { 583 | .dl-horizontal dt { 584 | float: left; 585 | width: 160px; 586 | clear: left; 587 | text-align: right; 588 | overflow: hidden; 589 | text-overflow: ellipsis; 590 | white-space: nowrap; 591 | } 592 | .dl-horizontal dd { 593 | margin-left: 180px; 594 | } 595 | } 596 | abbr[title], 597 | abbr[data-original-title] { 598 | cursor: help; 599 | border-bottom: 1px dotted #777777; 600 | } 601 | .initialism { 602 | font-size: 90%; 603 | text-transform: uppercase; 604 | } 605 | blockquote { 606 | padding: 10px 20px; 607 | margin: 0 0 20px; 608 | font-size: 17.5px; 609 | border-left: 5px solid #eeeeee; 610 | } 611 | blockquote p:last-child, 612 | blockquote ul:last-child, 613 | blockquote ol:last-child { 614 | margin-bottom: 0; 615 | } 616 | blockquote footer, 617 | blockquote small, 618 | blockquote .small { 619 | display: block; 620 | font-size: 80%; 621 | line-height: 1.42857143; 622 | color: #777777; 623 | } 624 | blockquote footer:before, 625 | blockquote small:before, 626 | blockquote .small:before { 627 | content: '\2014 \00A0'; 628 | } 629 | .blockquote-reverse, 630 | blockquote.pull-right { 631 | padding-right: 15px; 632 | padding-left: 0; 633 | border-right: 5px solid #eeeeee; 634 | border-left: 0; 635 | text-align: right; 636 | } 637 | .blockquote-reverse footer:before, 638 | blockquote.pull-right footer:before, 639 | .blockquote-reverse small:before, 640 | blockquote.pull-right small:before, 641 | .blockquote-reverse .small:before, 642 | blockquote.pull-right .small:before { 643 | content: ''; 644 | } 645 | .blockquote-reverse footer:after, 646 | blockquote.pull-right footer:after, 647 | .blockquote-reverse small:after, 648 | blockquote.pull-right small:after, 649 | .blockquote-reverse .small:after, 650 | blockquote.pull-right .small:after { 651 | content: '\00A0 \2014'; 652 | } 653 | address { 654 | margin-bottom: 20px; 655 | font-style: normal; 656 | line-height: 1.42857143; 657 | } 658 | .clearfix:before, 659 | .clearfix:after, 660 | .dl-horizontal dd:before, 661 | .dl-horizontal dd:after { 662 | content: " "; 663 | display: table; 664 | } 665 | .clearfix:after, 666 | .dl-horizontal dd:after { 667 | clear: both; 668 | } 669 | .center-block { 670 | display: block; 671 | margin-left: auto; 672 | margin-right: auto; 673 | } 674 | .pull-right { 675 | float: right !important; 676 | } 677 | .pull-left { 678 | float: left !important; 679 | } 680 | .hide { 681 | display: none !important; 682 | } 683 | .show { 684 | display: block !important; 685 | } 686 | .invisible { 687 | visibility: hidden; 688 | } 689 | .text-hide { 690 | font: 0/0 a; 691 | color: transparent; 692 | text-shadow: none; 693 | background-color: transparent; 694 | border: 0; 695 | } 696 | .hidden { 697 | display: none !important; 698 | } 699 | .affix { 700 | position: fixed; 701 | } 702 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | B2 Break Opportunity Before and After Em dash Provide a line break opportunity before and after the character 3 | BA Break After Spaces, hyphens Generally provide a line break opportunity after the character 4 | BB Break Before Punctuation used in dictionaries Generally provide a line break opportunity before the character 5 | HY Hyphen HYPHEN-MINUS Provide a line break opportunity after the character, except in numeric context 6 | CB Contingent Break Opportunity Inline objects Provide a line break opportunity contingent on additional information 7 | */ 8 | 9 | // B2 Break Opportunity Before and After - http://www.unicode.org/reports/tr14/#B2 10 | const B2 = new Set(['\u2014']); 11 | 12 | const SHY = new Set([ 13 | // Soft hyphen 14 | '\u00AD', 15 | ]); 16 | 17 | // BA: Break After (remove on break) - http://www.unicode.org/reports/tr14/#BA 18 | const BAI = new Set([ 19 | // Spaces 20 | '\u0020', 21 | '\u1680', 22 | '\u2000', 23 | '\u2001', 24 | '\u2002', 25 | '\u2003', 26 | '\u2004', 27 | '\u2005', 28 | '\u2006', 29 | '\u2008', 30 | '\u2009', 31 | '\u200A', 32 | '\u205F', 33 | '\u3000', 34 | // Tab 35 | '\u0009', 36 | // ZW Zero Width Space - http://www.unicode.org/reports/tr14/#ZW 37 | '\u200B', 38 | // Mandatory breaks not interpreted by html 39 | '\u2028', 40 | '\u2029', 41 | ]); 42 | 43 | const BA = new Set([ 44 | // Hyphen 45 | '\u058A', 46 | '\u2010', 47 | '\u2012', 48 | '\u2013', 49 | // Visible Word Dividers 50 | '\u05BE', 51 | '\u0F0B', 52 | '\u1361', 53 | '\u17D8', 54 | '\u17DA', 55 | '\u2027', 56 | '\u007C', 57 | // Historic Word Separators 58 | '\u16EB', 59 | '\u16EC', 60 | '\u16ED', 61 | '\u2056', 62 | '\u2058', 63 | '\u2059', 64 | '\u205A', 65 | '\u205B', 66 | '\u205D', 67 | '\u205E', 68 | '\u2E19', 69 | '\u2E2A', 70 | '\u2E2B', 71 | '\u2E2C', 72 | '\u2E2D', 73 | '\u2E30', 74 | '\u10100', 75 | '\u10101', 76 | '\u10102', 77 | '\u1039F', 78 | '\u103D0', 79 | '\u1091F', 80 | '\u12470', 81 | ]); 82 | 83 | // BB: Break Before - http://www.unicode.org/reports/tr14/#BB 84 | const BB = new Set(['\u00B4', '\u1FFD']); 85 | 86 | // BK: Mandatory Break (A) (Non-tailorable) - http://www.unicode.org/reports/tr14/#BK 87 | const BK = new Set(['\u000A']); 88 | 89 | /* eslint-env es6, browser */ 90 | const DEFAULTS = { 91 | 'font-size': '16px', 92 | 'font-weight': '400', 93 | 'font-family': 'Helvetica, Arial, sans-serif', 94 | }; 95 | 96 | /** 97 | * We only support rem/em/pt conversion 98 | * @param val 99 | * @param options 100 | * @return {*} 101 | */ 102 | function pxValue(value_, options) { 103 | if (!options) { 104 | options = {}; 105 | } 106 | 107 | const baseFontSize = Number.parseInt(prop(options, 'base-font-size', 16), 10); 108 | 109 | const value = Number.parseFloat(value_); 110 | const unit = value_.replace(value, ''); 111 | // eslint-disable-next-line default-case 112 | switch (unit) { 113 | case 'rem': 114 | case 'em': { 115 | return value * baseFontSize; 116 | } 117 | 118 | case 'pt': { 119 | return value * (96 / 72); 120 | } 121 | 122 | case 'px': { 123 | return value; 124 | } 125 | } 126 | 127 | throw new Error('The unit ' + unit + ' is not supported'); 128 | } 129 | 130 | /** 131 | * Get computed word- and letter spacing for text 132 | * @param ws 133 | * @param ls 134 | * @return {function(*)} 135 | */ 136 | export function addWordAndLetterSpacing(ws, ls) { 137 | const denyList = new Set(['inherit', 'initial', 'unset', 'normal']); 138 | 139 | let wordAddon = 0; 140 | if (ws && !denyList.has(ws)) { 141 | wordAddon = pxValue(ws); 142 | } 143 | 144 | let letterAddon = 0; 145 | if (ls && !denyList.has(ls)) { 146 | letterAddon = pxValue(ls); 147 | } 148 | 149 | return (text) => { 150 | const words = text.trim().replace(/\s+/gi, ' ').split(' ').length - 1; 151 | const chars = text.length; 152 | 153 | return words * wordAddon + chars * letterAddon; 154 | }; 155 | } 156 | 157 | /** 158 | * Map css styles to canvas font property 159 | * 160 | * font: font-style font-variant font-weight font-size/line-height font-family; 161 | * http://www.w3schools.com/tags/canvas_font.asp 162 | * 163 | * @param {CSSStyleDeclaration} style 164 | * @param {object} options 165 | * @returns {string} 166 | */ 167 | export function getFont(style, options) { 168 | const font = []; 169 | 170 | const fontWeight = prop(options, 'font-weight', style.getPropertyValue('font-weight')) || DEFAULTS['font-weight']; 171 | if ( 172 | ['normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700', '800', '900'].includes( 173 | fontWeight.toString() 174 | ) 175 | ) { 176 | font.push(fontWeight); 177 | } 178 | 179 | const fontStyle = prop(options, 'font-style', style.getPropertyValue('font-style')); 180 | if (['normal', 'italic', 'oblique'].includes(fontStyle)) { 181 | font.push(fontStyle); 182 | } 183 | 184 | const fontVariant = prop(options, 'font-variant', style.getPropertyValue('font-variant')); 185 | if (['normal', 'small-caps'].includes(fontVariant)) { 186 | font.push(fontVariant); 187 | } 188 | 189 | const fontSize = prop(options, 'font-size', style.getPropertyValue('font-size')) || DEFAULTS['font-size']; 190 | const fontSizeValue = pxValue(fontSize); 191 | font.push(fontSizeValue + 'px'); 192 | 193 | const fontFamily = prop(options, 'font-family', style.getPropertyValue('font-family')) || DEFAULTS['font-family']; 194 | font.push(fontFamily); 195 | 196 | return font.join(' '); 197 | } 198 | 199 | /** 200 | * Check for CSSStyleDeclaration 201 | * 202 | * @param val 203 | * @returns {bool} 204 | */ 205 | export function isCSSStyleDeclaration(value) { 206 | return value && typeof value.getPropertyValue === 'function'; 207 | } 208 | 209 | /** 210 | * Check wether we can get computed style 211 | * 212 | * @param el 213 | * @returns {bool} 214 | */ 215 | export function canGetComputedStyle(element) { 216 | return ( 217 | isElement(element) && 218 | element.style && 219 | typeof window !== 'undefined' && 220 | typeof window.getComputedStyle === 'function' 221 | ); 222 | } 223 | 224 | /** 225 | * Check for DOM element 226 | * 227 | * @param el 228 | * @retutns {bool} 229 | */ 230 | export function isElement(element) { 231 | return typeof HTMLElement === 'object' 232 | ? element instanceof HTMLElement 233 | : Boolean( 234 | element && 235 | typeof element === 'object' && 236 | element !== null && 237 | element.nodeType === 1 && 238 | typeof element.nodeName === 'string' 239 | ); 240 | } 241 | 242 | /** 243 | * Check if argument is object 244 | * @param obj 245 | * @returns {boolean} 246 | */ 247 | export function isObject(object) { 248 | return typeof object === 'object' && object !== null && !Array.isArray(object); 249 | } 250 | 251 | /** 252 | * Get style declaration if available 253 | * 254 | * @returns {CSSStyleDeclaration} 255 | */ 256 | export function getStyle(element, options) { 257 | const options_ = {...options}; 258 | const {style} = options_; 259 | if (!options) { 260 | options = {}; 261 | } 262 | 263 | if (isCSSStyleDeclaration(style)) { 264 | return style; 265 | } 266 | 267 | if (canGetComputedStyle(element)) { 268 | return window.getComputedStyle(element, prop(options, 'pseudoElt', null)); 269 | } 270 | 271 | return { 272 | getPropertyValue: (key) => prop(options, key), 273 | }; 274 | } 275 | 276 | /** 277 | * Normalize whitespace 278 | * https://developer.mozilla.org/de/docs/Web/CSS/white-space 279 | * 280 | * @param {string} text 281 | * @param {string} ws whitespace value 282 | * @returns {string} 283 | */ 284 | export function normalizeWhitespace(text, ws) { 285 | switch (ws) { 286 | case 'pre': { 287 | return text; 288 | } 289 | 290 | case 'pre-wrap': { 291 | return text; 292 | } 293 | 294 | case 'pre-line': { 295 | return (text || '').replace(/\s+/gm, ' ').trim(); 296 | } 297 | 298 | default: { 299 | return (text || '') 300 | .replace(/[\r\n]/gm, ' ') 301 | .replace(/\s+/gm, ' ') 302 | .trim(); 303 | } 304 | } 305 | } 306 | 307 | /** 308 | * Get styled text 309 | * 310 | * @param {string} text 311 | * @param {CSSStyleDeclaration} style 312 | * @returns {string} 313 | */ 314 | export function getStyledText(text, style) { 315 | switch (style.getPropertyValue('text-transform')) { 316 | case 'uppercase': { 317 | return text.toUpperCase(); 318 | } 319 | 320 | case 'lowercase': { 321 | return text.toLowerCase(); 322 | } 323 | 324 | default: { 325 | return text; 326 | } 327 | } 328 | } 329 | 330 | /** 331 | * Trim text and repace some breaking htmlentities for convenience 332 | * Point user to https://mths.be/he for real htmlentity decode 333 | * @param text 334 | * @returns {string} 335 | */ 336 | export function prepareText(text) { 337 | // Convert to unicode 338 | text = (text || '') 339 | .replace(//gi, '\u200B') 340 | .replace(//gi, '\u000A') 341 | .replace(/­/gi, '\u00AD') 342 | .replace(/—/gi, '\u2014'); 343 | 344 | if (/&#(\d+)(;?)|&#[xX]([a-fA-F\d]+)(;?)|&([\da-zA-Z]+);/g.test(text) && console) { 345 | console.error( 346 | 'text-metrics: Found encoded htmlenties. You may want to use https://mths.be/he to decode your text first.' 347 | ); 348 | } 349 | 350 | return text; 351 | } 352 | 353 | /** 354 | * Get textcontent from element 355 | * Try innerText first 356 | * @param el 357 | */ 358 | export function getText(element) { 359 | if (!element) { 360 | return ''; 361 | } 362 | 363 | const text = element.textContent || element.textContent || ''; 364 | 365 | return text; 366 | } 367 | 368 | /** 369 | * Get property from src 370 | * 371 | * @param src 372 | * @param attr 373 | * @param defaultValue 374 | * @returns {*} 375 | */ 376 | export function prop(src, attr, defaultValue) { 377 | return (src && src[attr] !== undefined && src[attr]) || defaultValue; 378 | } 379 | 380 | /** 381 | * Normalize options 382 | * 383 | * @param options 384 | * @returns {*} 385 | */ 386 | export function normalizeOptions(options) { 387 | const options_ = {}; 388 | 389 | // Normalize keys (fontSize => font-size) 390 | for (const key of Object.keys(options || {})) { 391 | const dashedKey = key.replace(/([A-Z])/g, ($1) => '-' + $1.toLowerCase()); 392 | options_[dashedKey] = options[key]; 393 | } 394 | 395 | return options_; 396 | } 397 | 398 | /** 399 | * Get Canvas 400 | * @param font 401 | * @throws {Error} 402 | * @return {Context2d} 403 | */ 404 | export function getContext2d(font) { 405 | try { 406 | const ctx = document.createElement('canvas').getContext('2d'); 407 | const dpr = window.devicePixelRatio || 1; 408 | const bsr = 409 | ctx.webkitBackingStorePixelRatio || 410 | ctx.mozBackingStorePixelRatio || 411 | ctx.msBackingStorePixelRatio || 412 | ctx.oBackingStorePixelRatio || 413 | ctx.backingStorePixelRatio || 414 | 1; 415 | ctx.font = font; 416 | ctx.setTransform(dpr / bsr, 0, 0, dpr / bsr, 0, 0); 417 | return ctx; 418 | } catch (error) { 419 | throw new Error('Canvas support required' + error.message); 420 | } 421 | } 422 | 423 | /** 424 | * Check breaking character 425 | * http://www.unicode.org/reports/tr14/#Table1 426 | * 427 | * @param chr 428 | */ 429 | function checkBreak(chr) { 430 | return ( 431 | (B2.has(chr) && 'B2') || 432 | (BAI.has(chr) && 'BAI') || 433 | (SHY.has(chr) && 'SHY') || 434 | (BA.has(chr) && 'BA') || 435 | (BB.has(chr) && 'BB') || 436 | (BK.has(chr) && 'BK') 437 | ); 438 | } 439 | 440 | export function computeLinesDefault({ctx, text, max, wordSpacing, letterSpacing}) { 441 | const addSpacing = addWordAndLetterSpacing(wordSpacing, letterSpacing); 442 | const lines = []; 443 | const parts = []; 444 | const breakpoints = []; 445 | let line = ''; 446 | let part = ''; 447 | 448 | if (!text) { 449 | return []; 450 | } 451 | 452 | // Compute array of breakpoints 453 | for (const chr of text) { 454 | const type = checkBreak(chr); 455 | if (part === '' && type === 'BAI') { 456 | continue; 457 | } 458 | 459 | if (type) { 460 | breakpoints.push({chr, type}); 461 | 462 | parts.push(part); 463 | part = ''; 464 | } else { 465 | part += chr; 466 | } 467 | } 468 | 469 | if (part) { 470 | parts.push(part); 471 | } 472 | 473 | // Loop over text parts and compute the lines 474 | for (const [i, part] of parts.entries()) { 475 | if (i === 0) { 476 | line = part; 477 | continue; 478 | } 479 | 480 | const breakpoint = breakpoints[i - 1]; 481 | // Special treatment as we only render the soft hyphen if we need to split 482 | const chr = breakpoint.type === 'SHY' ? '' : breakpoint.chr; 483 | if (breakpoint.type === 'BK') { 484 | lines.push(line); 485 | line = part; 486 | continue; 487 | } 488 | 489 | // Measure width 490 | const rawWidth = ctx.measureText(line + chr + part).width + addSpacing(line + chr + part); 491 | const width = Math.round(rawWidth); 492 | 493 | // Still fits in line 494 | if (width <= max) { 495 | line += chr + part; 496 | continue; 497 | } 498 | 499 | // Line is to long, we split at the breakpoint 500 | switch (breakpoint.type) { 501 | case 'SHY': { 502 | lines.push(line + '-'); 503 | line = part; 504 | break; 505 | } 506 | 507 | case 'BA': { 508 | lines.push(line + chr); 509 | line = part; 510 | break; 511 | } 512 | 513 | case 'BAI': { 514 | lines.push(line); 515 | line = part; 516 | break; 517 | } 518 | 519 | case 'BB': { 520 | lines.push(line); 521 | line = chr + part; 522 | break; 523 | } 524 | 525 | case 'B2': { 526 | if (Number.parseInt(ctx.measureText(line + chr).width + addSpacing(line + chr), 10) <= max) { 527 | lines.push(line + chr); 528 | line = part; 529 | } else if (Number.parseInt(ctx.measureText(chr + part).width + addSpacing(chr + part), 10) <= max) { 530 | lines.push(line); 531 | line = chr + part; 532 | } else { 533 | lines.push(line, chr); 534 | line = part; 535 | } 536 | 537 | break; 538 | } 539 | 540 | default: { 541 | throw new Error('Undefoined break'); 542 | } 543 | } 544 | } 545 | 546 | if ([...line].length > 0) { 547 | lines.push(line); 548 | } 549 | 550 | return lines; 551 | } 552 | 553 | export function computeLinesBreakAll({ctx, text, max, wordSpacing, letterSpacing}) { 554 | const addSpacing = addWordAndLetterSpacing(wordSpacing, letterSpacing); 555 | const lines = []; 556 | let line = ''; 557 | let index = 0; 558 | 559 | if (!text) { 560 | return []; 561 | } 562 | 563 | for (const chr of text) { 564 | const type = checkBreak(chr); 565 | // Mandatory break found (br's converted to \u000A and innerText keeps br's as \u000A 566 | if (type === 'BK') { 567 | lines.push(line); 568 | line = ''; 569 | continue; 570 | } 571 | 572 | const lineLength = line.length; 573 | if (BAI.has(chr) && (lineLength === 0 || BAI.has(line[lineLength - 1]))) { 574 | continue; 575 | } 576 | 577 | // Measure width 578 | let rawWidth = ctx.measureText(line + chr).width + addSpacing(line + chr); 579 | let width = Math.ceil(rawWidth); 580 | 581 | // Check if we can put char behind the shy 582 | if (type === 'SHY') { 583 | const next = text[index + 1] || ''; 584 | rawWidth = ctx.measureText(line + chr + next).width + addSpacing(line + chr + next); 585 | width = Math.ceil(rawWidth); 586 | } 587 | 588 | // Needs at least one character 589 | if (width > max && [...line].length > 0) { 590 | switch (type) { 591 | case 'SHY': { 592 | lines.push(line + '-'); 593 | line = ''; 594 | break; 595 | } 596 | 597 | case 'BA': { 598 | lines.push(line + chr); 599 | line = ''; 600 | break; 601 | } 602 | 603 | case 'BAI': { 604 | lines.push(line); 605 | line = ''; 606 | break; 607 | } 608 | 609 | default: { 610 | lines.push(line); 611 | line = chr; 612 | break; 613 | } 614 | } 615 | } else if (chr !== '\u00AD') { 616 | line += chr; 617 | } 618 | 619 | index++; 620 | } 621 | 622 | if ([...line].length > 0) { 623 | lines.push(line); 624 | } 625 | 626 | return lines; 627 | } 628 | -------------------------------------------------------------------------------- /test/jasmine/jasmine.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | body { overflow-y: scroll; } 3 | 4 | .jasmine_html-reporter { background-color: #eee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333; } 5 | .jasmine_html-reporter a { text-decoration: none; } 6 | .jasmine_html-reporter a:hover { text-decoration: underline; } 7 | .jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; } 8 | .jasmine_html-reporter .jasmine-banner, .jasmine_html-reporter .jasmine-symbol-summary, .jasmine_html-reporter .jasmine-summary, .jasmine_html-reporter .jasmine-result-message, .jasmine_html-reporter .jasmine-spec .jasmine-description, .jasmine_html-reporter .jasmine-spec-detail .jasmine-description, .jasmine_html-reporter .jasmine-alert .jasmine-bar, .jasmine_html-reporter .jasmine-stack-trace { padding-left: 9px; padding-right: 9px; } 9 | .jasmine_html-reporter .jasmine-banner { position: relative; } 10 | .jasmine_html-reporter .jasmine-banner .jasmine-title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -moz-background-size: 100%; -o-background-size: 100%; -webkit-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; } 11 | .jasmine_html-reporter .jasmine-banner .jasmine-version { margin-left: 14px; position: relative; top: 6px; } 12 | .jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; } 13 | .jasmine_html-reporter .jasmine-version { color: #aaa; } 14 | .jasmine_html-reporter .jasmine-banner { margin-top: 14px; } 15 | .jasmine_html-reporter .jasmine-duration { color: #fff; float: right; line-height: 28px; padding-right: 9px; } 16 | .jasmine_html-reporter .jasmine-symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } 17 | .jasmine_html-reporter .jasmine-symbol-summary li { display: inline-block; height: 10px; width: 14px; font-size: 16px; } 18 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed { font-size: 14px; } 19 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-passed:before { color: #007069; content: "•"; } 20 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed { line-height: 9px; } 21 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-failed:before { color: #ca3a11; content: "×"; font-weight: bold; margin-left: -1px; } 22 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded { font-size: 14px; } 23 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded:before { color: #bababa; content: "•"; } 24 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-excluded-no-display { font-size: 14px; display: none; } 25 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending { line-height: 17px; } 26 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-pending:before { color: #ba9d37; content: "*"; } 27 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty { font-size: 14px; } 28 | .jasmine_html-reporter .jasmine-symbol-summary li.jasmine-empty:before { color: #ba9d37; content: "•"; } 29 | .jasmine_html-reporter .jasmine-run-options { float: right; margin-right: 5px; border: 1px solid #8a4182; color: #8a4182; position: relative; line-height: 20px; } 30 | .jasmine_html-reporter .jasmine-run-options .jasmine-trigger { cursor: pointer; padding: 8px 16px; } 31 | .jasmine_html-reporter .jasmine-run-options .jasmine-payload { position: absolute; display: none; right: -1px; border: 1px solid #8a4182; background-color: #eee; white-space: nowrap; padding: 4px 8px; } 32 | .jasmine_html-reporter .jasmine-run-options .jasmine-payload.jasmine-open { display: block; } 33 | .jasmine_html-reporter .jasmine-bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 34 | .jasmine_html-reporter .jasmine-bar.jasmine-failed, .jasmine_html-reporter .jasmine-bar.jasmine-errored { background-color: #ca3a11; border-bottom: 1px solid #eee; } 35 | .jasmine_html-reporter .jasmine-bar.jasmine-passed { background-color: #007069; } 36 | .jasmine_html-reporter .jasmine-bar.jasmine-incomplete { background-color: #bababa; } 37 | .jasmine_html-reporter .jasmine-bar.jasmine-skipped { background-color: #bababa; } 38 | .jasmine_html-reporter .jasmine-bar.jasmine-warning { background-color: #ba9d37; color: #333; } 39 | .jasmine_html-reporter .jasmine-bar.jasmine-menu { background-color: #fff; color: #aaa; } 40 | .jasmine_html-reporter .jasmine-bar.jasmine-menu a { color: #333; } 41 | .jasmine_html-reporter .jasmine-bar a { color: white; } 42 | .jasmine_html-reporter.jasmine-spec-list .jasmine-bar.jasmine-menu.jasmine-failure-list, .jasmine_html-reporter.jasmine-spec-list .jasmine-results .jasmine-failures { display: none; } 43 | .jasmine_html-reporter.jasmine-failure-list .jasmine-bar.jasmine-menu.jasmine-spec-list, .jasmine_html-reporter.jasmine-failure-list .jasmine-summary { display: none; } 44 | .jasmine_html-reporter .jasmine-results { margin-top: 14px; } 45 | .jasmine_html-reporter .jasmine-summary { margin-top: 14px; } 46 | .jasmine_html-reporter .jasmine-summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } 47 | .jasmine_html-reporter .jasmine-summary ul.jasmine-suite { margin-top: 7px; margin-bottom: 7px; } 48 | .jasmine_html-reporter .jasmine-summary li.jasmine-passed a { color: #007069; } 49 | .jasmine_html-reporter .jasmine-summary li.jasmine-failed a { color: #ca3a11; } 50 | .jasmine_html-reporter .jasmine-summary li.jasmine-empty a { color: #ba9d37; } 51 | .jasmine_html-reporter .jasmine-summary li.jasmine-pending a { color: #ba9d37; } 52 | .jasmine_html-reporter .jasmine-summary li.jasmine-excluded a { color: #bababa; } 53 | .jasmine_html-reporter .jasmine-specs li.jasmine-passed a:before { content: "• "; } 54 | .jasmine_html-reporter .jasmine-specs li.jasmine-failed a:before { content: "× "; } 55 | .jasmine_html-reporter .jasmine-specs li.jasmine-empty a:before { content: "* "; } 56 | .jasmine_html-reporter .jasmine-specs li.jasmine-pending a:before { content: "• "; } 57 | .jasmine_html-reporter .jasmine-specs li.jasmine-excluded a:before { content: "• "; } 58 | .jasmine_html-reporter .jasmine-description + .jasmine-suite { margin-top: 0; } 59 | .jasmine_html-reporter .jasmine-suite { margin-top: 14px; } 60 | .jasmine_html-reporter .jasmine-suite a { color: #333; } 61 | .jasmine_html-reporter .jasmine-failures .jasmine-spec-detail { margin-bottom: 28px; } 62 | .jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description { background-color: #ca3a11; color: white; } 63 | .jasmine_html-reporter .jasmine-failures .jasmine-spec-detail .jasmine-description a { color: white; } 64 | .jasmine_html-reporter .jasmine-result-message { padding-top: 14px; color: #333; white-space: pre-wrap; } 65 | .jasmine_html-reporter .jasmine-result-message span.jasmine-result { display: block; } 66 | .jasmine_html-reporter .jasmine-stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666; border: 1px solid #ddd; background: white; white-space: pre; } 67 | -------------------------------------------------------------------------------- /test/jasmine/jasmine-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2018 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | jasmineRequire.html = function (j$) { 24 | j$.ResultsNode = jasmineRequire.ResultsNode(); 25 | j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); 26 | j$.QueryString = jasmineRequire.QueryString(); 27 | j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); 28 | }; 29 | 30 | jasmineRequire.HtmlReporter = function (j$) { 31 | var noopTimer = { 32 | start: function () {}, 33 | elapsed: function () { 34 | return 0; 35 | }, 36 | }; 37 | 38 | function ResultsStateBuilder() { 39 | this.topResults = new j$.ResultsNode({}, '', null); 40 | this.currentParent = this.topResults; 41 | this.specsExecuted = 0; 42 | this.failureCount = 0; 43 | this.pendingSpecCount = 0; 44 | } 45 | 46 | ResultsStateBuilder.prototype.suiteStarted = function (result) { 47 | this.currentParent.addChild(result, 'suite'); 48 | this.currentParent = this.currentParent.last(); 49 | }; 50 | 51 | ResultsStateBuilder.prototype.suiteDone = function (result) { 52 | this.currentParent.updateResult(result); 53 | if (this.currentParent !== this.topResults) { 54 | this.currentParent = this.currentParent.parent; 55 | } 56 | 57 | if (result.status === 'failed') { 58 | this.failureCount++; 59 | } 60 | }; 61 | 62 | ResultsStateBuilder.prototype.specStarted = function (result) {}; 63 | 64 | ResultsStateBuilder.prototype.specDone = function (result) { 65 | this.currentParent.addChild(result, 'spec'); 66 | 67 | if (result.status !== 'excluded') { 68 | this.specsExecuted++; 69 | } 70 | 71 | if (result.status === 'failed') { 72 | this.failureCount++; 73 | } 74 | 75 | if (result.status == 'pending') { 76 | this.pendingSpecCount++; 77 | } 78 | }; 79 | 80 | function HtmlReporter(options) { 81 | var config = function () { 82 | return (options.env && options.env.configuration()) || {}; 83 | }, 84 | getContainer = options.getContainer, 85 | createElement = options.createElement, 86 | createTextNode = options.createTextNode, 87 | navigateWithNewParam = options.navigateWithNewParam || function () {}, 88 | addToExistingQueryString = options.addToExistingQueryString || defaultQueryString, 89 | filterSpecs = options.filterSpecs, 90 | timer = options.timer || noopTimer, 91 | htmlReporterMain, 92 | symbols, 93 | deprecationWarnings = []; 94 | 95 | this.initialize = function () { 96 | clearPrior(); 97 | htmlReporterMain = createDom( 98 | 'div', 99 | {className: 'jasmine_html-reporter'}, 100 | createDom( 101 | 'div', 102 | {className: 'jasmine-banner'}, 103 | createDom('a', {className: 'jasmine-title', href: 'http://jasmine.github.io/', target: '_blank'}), 104 | createDom('span', {className: 'jasmine-version'}, j$.version) 105 | ), 106 | createDom('ul', {className: 'jasmine-symbol-summary'}), 107 | createDom('div', {className: 'jasmine-alert'}), 108 | createDom('div', {className: 'jasmine-results'}, createDom('div', {className: 'jasmine-failures'})) 109 | ); 110 | getContainer().appendChild(htmlReporterMain); 111 | }; 112 | 113 | var totalSpecsDefined; 114 | this.jasmineStarted = function (options) { 115 | totalSpecsDefined = options.totalSpecsDefined || 0; 116 | timer.start(); 117 | }; 118 | 119 | var summary = createDom('div', {className: 'jasmine-summary'}); 120 | 121 | var stateBuilder = new ResultsStateBuilder(); 122 | 123 | this.suiteStarted = function (result) { 124 | stateBuilder.suiteStarted(result); 125 | }; 126 | 127 | this.suiteDone = function (result) { 128 | stateBuilder.suiteDone(result); 129 | 130 | if (result.status === 'failed') { 131 | failures.push(failureDom(result)); 132 | } 133 | addDeprecationWarnings(result); 134 | }; 135 | 136 | this.specStarted = function (result) { 137 | stateBuilder.specStarted(result); 138 | }; 139 | 140 | var failures = []; 141 | this.specDone = function (result) { 142 | stateBuilder.specDone(result); 143 | 144 | if (noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') { 145 | console.error("Spec '" + result.fullName + "' has no expectations."); 146 | } 147 | 148 | if (!symbols) { 149 | symbols = find('.jasmine-symbol-summary'); 150 | } 151 | 152 | symbols.appendChild( 153 | createDom('li', { 154 | className: this.displaySpecInCorrectFormat(result), 155 | id: 'spec_' + result.id, 156 | title: result.fullName, 157 | }) 158 | ); 159 | 160 | if (result.status === 'failed') { 161 | failures.push(failureDom(result)); 162 | } 163 | 164 | addDeprecationWarnings(result); 165 | }; 166 | 167 | this.displaySpecInCorrectFormat = function (result) { 168 | return noExpectations(result) ? 'jasmine-empty' : this.resultStatus(result.status); 169 | }; 170 | 171 | this.resultStatus = function (status) { 172 | if (status === 'excluded') { 173 | return config().hideDisabled ? 'jasmine-excluded-no-display' : 'jasmine-excluded'; 174 | } 175 | return 'jasmine-' + status; 176 | }; 177 | 178 | this.jasmineDone = function (doneResult) { 179 | var banner = find('.jasmine-banner'); 180 | var alert = find('.jasmine-alert'); 181 | var order = doneResult && doneResult.order; 182 | var i; 183 | alert.appendChild( 184 | createDom('span', {className: 'jasmine-duration'}, 'finished in ' + timer.elapsed() / 1000 + 's') 185 | ); 186 | 187 | banner.appendChild(optionsMenu(config())); 188 | 189 | if (stateBuilder.specsExecuted < totalSpecsDefined) { 190 | var skippedMessage = 'Ran ' + stateBuilder.specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; 191 | var skippedLink = addToExistingQueryString('spec', ''); 192 | alert.appendChild( 193 | createDom( 194 | 'span', 195 | {className: 'jasmine-bar jasmine-skipped'}, 196 | createDom('a', {href: skippedLink, title: 'Run all specs'}, skippedMessage) 197 | ) 198 | ); 199 | } 200 | var statusBarMessage = ''; 201 | var statusBarClassName = 'jasmine-overall-result jasmine-bar '; 202 | var globalFailures = (doneResult && doneResult.failedExpectations) || []; 203 | var failed = stateBuilder.failureCount + globalFailures.length > 0; 204 | 205 | if (totalSpecsDefined > 0 || failed) { 206 | statusBarMessage += 207 | pluralize('spec', stateBuilder.specsExecuted) + ', ' + pluralize('failure', stateBuilder.failureCount); 208 | if (stateBuilder.pendingSpecCount) { 209 | statusBarMessage += ', ' + pluralize('pending spec', stateBuilder.pendingSpecCount); 210 | } 211 | } 212 | 213 | if (doneResult.overallStatus === 'passed') { 214 | statusBarClassName += ' jasmine-passed '; 215 | } else if (doneResult.overallStatus === 'incomplete') { 216 | statusBarClassName += ' jasmine-incomplete '; 217 | statusBarMessage = 'Incomplete: ' + doneResult.incompleteReason + ', ' + statusBarMessage; 218 | } else { 219 | statusBarClassName += ' jasmine-failed '; 220 | } 221 | 222 | var seedBar; 223 | if (order && order.random) { 224 | seedBar = createDom( 225 | 'span', 226 | {className: 'jasmine-seed-bar'}, 227 | ', randomized with seed ', 228 | createDom('a', {title: 'randomized with seed ' + order.seed, href: seedHref(order.seed)}, order.seed) 229 | ); 230 | } 231 | 232 | alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage, seedBar)); 233 | 234 | var errorBarClassName = 'jasmine-bar jasmine-errored'; 235 | var afterAllMessagePrefix = 'AfterAll '; 236 | 237 | for (i = 0; i < globalFailures.length; i++) { 238 | alert.appendChild(createDom('span', {className: errorBarClassName}, globalFailureMessage(globalFailures[i]))); 239 | } 240 | 241 | function globalFailureMessage(failure) { 242 | if (failure.globalErrorType === 'load') { 243 | var prefix = 'Error during loading: ' + failure.message; 244 | 245 | if (failure.filename) { 246 | return prefix + ' in ' + failure.filename + ' line ' + failure.lineno; 247 | } else { 248 | return prefix; 249 | } 250 | } else { 251 | return afterAllMessagePrefix + failure.message; 252 | } 253 | } 254 | 255 | addDeprecationWarnings(doneResult); 256 | 257 | var warningBarClassName = 'jasmine-bar jasmine-warning'; 258 | for (i = 0; i < deprecationWarnings.length; i++) { 259 | var warning = deprecationWarnings[i]; 260 | alert.appendChild(createDom('span', {className: warningBarClassName}, 'DEPRECATION: ' + warning)); 261 | } 262 | 263 | var results = find('.jasmine-results'); 264 | results.appendChild(summary); 265 | 266 | summaryList(stateBuilder.topResults, summary); 267 | 268 | if (failures.length) { 269 | alert.appendChild( 270 | createDom( 271 | 'span', 272 | {className: 'jasmine-menu jasmine-bar jasmine-spec-list'}, 273 | createDom('span', {}, 'Spec List | '), 274 | createDom('a', {className: 'jasmine-failures-menu', href: '#'}, 'Failures') 275 | ) 276 | ); 277 | alert.appendChild( 278 | createDom( 279 | 'span', 280 | {className: 'jasmine-menu jasmine-bar jasmine-failure-list'}, 281 | createDom('a', {className: 'jasmine-spec-list-menu', href: '#'}, 'Spec List'), 282 | createDom('span', {}, ' | Failures ') 283 | ) 284 | ); 285 | 286 | find('.jasmine-failures-menu').onclick = function () { 287 | setMenuModeTo('jasmine-failure-list'); 288 | }; 289 | find('.jasmine-spec-list-menu').onclick = function () { 290 | setMenuModeTo('jasmine-spec-list'); 291 | }; 292 | 293 | setMenuModeTo('jasmine-failure-list'); 294 | 295 | var failureNode = find('.jasmine-failures'); 296 | for (i = 0; i < failures.length; i++) { 297 | failureNode.appendChild(failures[i]); 298 | } 299 | } 300 | }; 301 | 302 | return this; 303 | 304 | function failureDom(result) { 305 | var failure = createDom( 306 | 'div', 307 | {className: 'jasmine-spec-detail jasmine-failed'}, 308 | failureDescription(result, stateBuilder.currentParent), 309 | createDom('div', {className: 'jasmine-messages'}) 310 | ); 311 | var messages = failure.childNodes[1]; 312 | 313 | for (var i = 0; i < result.failedExpectations.length; i++) { 314 | var expectation = result.failedExpectations[i]; 315 | messages.appendChild(createDom('div', {className: 'jasmine-result-message'}, expectation.message)); 316 | messages.appendChild(createDom('div', {className: 'jasmine-stack-trace'}, expectation.stack)); 317 | } 318 | 319 | return failure; 320 | } 321 | 322 | function summaryList(resultsTree, domParent) { 323 | var specListNode; 324 | for (var i = 0; i < resultsTree.children.length; i++) { 325 | var resultNode = resultsTree.children[i]; 326 | if (filterSpecs && !hasActiveSpec(resultNode)) { 327 | continue; 328 | } 329 | if (resultNode.type === 'suite') { 330 | var suiteListNode = createDom( 331 | 'ul', 332 | {className: 'jasmine-suite', id: 'suite-' + resultNode.result.id}, 333 | createDom( 334 | 'li', 335 | {className: 'jasmine-suite-detail jasmine-' + resultNode.result.status}, 336 | createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description) 337 | ) 338 | ); 339 | 340 | summaryList(resultNode, suiteListNode); 341 | domParent.appendChild(suiteListNode); 342 | } 343 | if (resultNode.type === 'spec') { 344 | if (domParent.getAttribute('class') !== 'jasmine-specs') { 345 | specListNode = createDom('ul', {className: 'jasmine-specs'}); 346 | domParent.appendChild(specListNode); 347 | } 348 | var specDescription = resultNode.result.description; 349 | if (noExpectations(resultNode.result)) { 350 | specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription; 351 | } 352 | if (resultNode.result.status === 'pending' && resultNode.result.pendingReason !== '') { 353 | specDescription = specDescription + ' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason; 354 | } 355 | specListNode.appendChild( 356 | createDom( 357 | 'li', 358 | { 359 | className: 'jasmine-' + resultNode.result.status, 360 | id: 'spec-' + resultNode.result.id, 361 | }, 362 | createDom('a', {href: specHref(resultNode.result)}, specDescription) 363 | ) 364 | ); 365 | } 366 | } 367 | } 368 | 369 | function optionsMenu(config) { 370 | var optionsMenuDom = createDom( 371 | 'div', 372 | {className: 'jasmine-run-options'}, 373 | createDom('span', {className: 'jasmine-trigger'}, 'Options'), 374 | createDom( 375 | 'div', 376 | {className: 'jasmine-payload'}, 377 | createDom( 378 | 'div', 379 | {className: 'jasmine-stop-on-failure'}, 380 | createDom('input', { 381 | className: 'jasmine-fail-fast', 382 | id: 'jasmine-fail-fast', 383 | type: 'checkbox', 384 | }), 385 | createDom('label', {className: 'jasmine-label', for: 'jasmine-fail-fast'}, 'stop execution on spec failure') 386 | ), 387 | createDom( 388 | 'div', 389 | {className: 'jasmine-throw-failures'}, 390 | createDom('input', { 391 | className: 'jasmine-throw', 392 | id: 'jasmine-throw-failures', 393 | type: 'checkbox', 394 | }), 395 | createDom( 396 | 'label', 397 | {className: 'jasmine-label', for: 'jasmine-throw-failures'}, 398 | 'stop spec on expectation failure' 399 | ) 400 | ), 401 | createDom( 402 | 'div', 403 | {className: 'jasmine-random-order'}, 404 | createDom('input', { 405 | className: 'jasmine-random', 406 | id: 'jasmine-random-order', 407 | type: 'checkbox', 408 | }), 409 | createDom('label', {className: 'jasmine-label', for: 'jasmine-random-order'}, 'run tests in random order') 410 | ), 411 | createDom( 412 | 'div', 413 | {className: 'jasmine-hide-disabled'}, 414 | createDom('input', { 415 | className: 'jasmine-disabled', 416 | id: 'jasmine-hide-disabled', 417 | type: 'checkbox', 418 | }), 419 | createDom('label', {className: 'jasmine-label', for: 'jasmine-hide-disabled'}, 'hide disabled tests') 420 | ) 421 | ) 422 | ); 423 | 424 | var failFastCheckbox = optionsMenuDom.querySelector('#jasmine-fail-fast'); 425 | failFastCheckbox.checked = config.failFast; 426 | failFastCheckbox.onclick = function () { 427 | navigateWithNewParam('failFast', !config.failFast); 428 | }; 429 | 430 | var throwCheckbox = optionsMenuDom.querySelector('#jasmine-throw-failures'); 431 | throwCheckbox.checked = config.oneFailurePerSpec; 432 | throwCheckbox.onclick = function () { 433 | navigateWithNewParam('throwFailures', !config.oneFailurePerSpec); 434 | }; 435 | 436 | var randomCheckbox = optionsMenuDom.querySelector('#jasmine-random-order'); 437 | randomCheckbox.checked = config.random; 438 | randomCheckbox.onclick = function () { 439 | navigateWithNewParam('random', !config.random); 440 | }; 441 | 442 | var hideDisabled = optionsMenuDom.querySelector('#jasmine-hide-disabled'); 443 | hideDisabled.checked = config.hideDisabled; 444 | hideDisabled.onclick = function () { 445 | navigateWithNewParam('hideDisabled', !config.hideDisabled); 446 | }; 447 | 448 | var optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'), 449 | optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'), 450 | isOpen = /\bjasmine-open\b/; 451 | 452 | optionsTrigger.onclick = function () { 453 | if (isOpen.test(optionsPayload.className)) { 454 | optionsPayload.className = optionsPayload.className.replace(isOpen, ''); 455 | } else { 456 | optionsPayload.className += ' jasmine-open'; 457 | } 458 | }; 459 | 460 | return optionsMenuDom; 461 | } 462 | 463 | function failureDescription(result, suite) { 464 | var wrapper = createDom( 465 | 'div', 466 | {className: 'jasmine-description'}, 467 | createDom('a', {title: result.description, href: specHref(result)}, result.description) 468 | ); 469 | var suiteLink; 470 | 471 | while (suite && suite.parent) { 472 | wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild); 473 | suiteLink = createDom('a', {href: suiteHref(suite)}, suite.result.description); 474 | wrapper.insertBefore(suiteLink, wrapper.firstChild); 475 | 476 | suite = suite.parent; 477 | } 478 | 479 | return wrapper; 480 | } 481 | 482 | function suiteHref(suite) { 483 | var els = []; 484 | 485 | while (suite && suite.parent) { 486 | els.unshift(suite.result.description); 487 | suite = suite.parent; 488 | } 489 | 490 | return addToExistingQueryString('spec', els.join(' ')); 491 | } 492 | 493 | function addDeprecationWarnings(result) { 494 | if (result && result.deprecationWarnings) { 495 | for (var i = 0; i < result.deprecationWarnings.length; i++) { 496 | var warning = result.deprecationWarnings[i].message; 497 | if (!j$.util.arrayContains(warning)) { 498 | deprecationWarnings.push(warning); 499 | } 500 | } 501 | } 502 | } 503 | 504 | function find(selector) { 505 | return getContainer().querySelector('.jasmine_html-reporter ' + selector); 506 | } 507 | 508 | function clearPrior() { 509 | // return the reporter 510 | var oldReporter = find(''); 511 | 512 | if (oldReporter) { 513 | getContainer().removeChild(oldReporter); 514 | } 515 | } 516 | 517 | function createDom(type, attrs, childrenVarArgs) { 518 | var el = createElement(type); 519 | 520 | for (var i = 2; i < arguments.length; i++) { 521 | var child = arguments[i]; 522 | 523 | if (typeof child === 'string') { 524 | el.appendChild(createTextNode(child)); 525 | } else { 526 | if (child) { 527 | el.appendChild(child); 528 | } 529 | } 530 | } 531 | 532 | for (var attr in attrs) { 533 | if (attr == 'className') { 534 | el[attr] = attrs[attr]; 535 | } else { 536 | el.setAttribute(attr, attrs[attr]); 537 | } 538 | } 539 | 540 | return el; 541 | } 542 | 543 | function pluralize(singular, count) { 544 | var word = count == 1 ? singular : singular + 's'; 545 | 546 | return '' + count + ' ' + word; 547 | } 548 | 549 | function specHref(result) { 550 | return addToExistingQueryString('spec', result.fullName); 551 | } 552 | 553 | function seedHref(seed) { 554 | return addToExistingQueryString('seed', seed); 555 | } 556 | 557 | function defaultQueryString(key, value) { 558 | return '?' + key + '=' + value; 559 | } 560 | 561 | function setMenuModeTo(mode) { 562 | htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode); 563 | } 564 | 565 | function noExpectations(result) { 566 | return result.failedExpectations.length + result.passedExpectations.length === 0 && result.status === 'passed'; 567 | } 568 | 569 | function hasActiveSpec(resultNode) { 570 | if (resultNode.type == 'spec' && resultNode.result.status != 'excluded') { 571 | return true; 572 | } 573 | 574 | if (resultNode.type == 'suite') { 575 | for (var i = 0, j = resultNode.children.length; i < j; i++) { 576 | if (hasActiveSpec(resultNode.children[i])) { 577 | return true; 578 | } 579 | } 580 | } 581 | } 582 | } 583 | 584 | return HtmlReporter; 585 | }; 586 | 587 | jasmineRequire.HtmlSpecFilter = function () { 588 | function HtmlSpecFilter(options) { 589 | var filterString = 590 | options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); 591 | var filterPattern = new RegExp(filterString); 592 | 593 | this.matches = function (specName) { 594 | return filterPattern.test(specName); 595 | }; 596 | } 597 | 598 | return HtmlSpecFilter; 599 | }; 600 | 601 | jasmineRequire.ResultsNode = function () { 602 | function ResultsNode(result, type, parent) { 603 | this.result = result; 604 | this.type = type; 605 | this.parent = parent; 606 | 607 | this.children = []; 608 | 609 | this.addChild = function (result, type) { 610 | this.children.push(new ResultsNode(result, type, this)); 611 | }; 612 | 613 | this.last = function () { 614 | return this.children[this.children.length - 1]; 615 | }; 616 | 617 | this.updateResult = function (result) { 618 | this.result = result; 619 | }; 620 | } 621 | 622 | return ResultsNode; 623 | }; 624 | 625 | jasmineRequire.QueryString = function () { 626 | function QueryString(options) { 627 | this.navigateWithNewParam = function (key, value) { 628 | options.getWindowLocation().search = this.fullStringWithNewParam(key, value); 629 | }; 630 | 631 | this.fullStringWithNewParam = function (key, value) { 632 | var paramMap = queryStringToParamMap(); 633 | paramMap[key] = value; 634 | return toQueryString(paramMap); 635 | }; 636 | 637 | this.getParam = function (key) { 638 | return queryStringToParamMap()[key]; 639 | }; 640 | 641 | return this; 642 | 643 | function toQueryString(paramMap) { 644 | var qStrPairs = []; 645 | for (var prop in paramMap) { 646 | qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])); 647 | } 648 | return '?' + qStrPairs.join('&'); 649 | } 650 | 651 | function queryStringToParamMap() { 652 | var paramStr = options.getWindowLocation().search.substring(1), 653 | params = [], 654 | paramMap = {}; 655 | 656 | if (paramStr.length > 0) { 657 | params = paramStr.split('&'); 658 | for (var i = 0; i < params.length; i++) { 659 | var p = params[i].split('='); 660 | var value = decodeURIComponent(p[1]); 661 | if (value === 'true' || value === 'false') { 662 | value = JSON.parse(value); 663 | } 664 | paramMap[decodeURIComponent(p[0])] = value; 665 | } 666 | } 667 | 668 | return paramMap; 669 | } 670 | } 671 | 672 | return QueryString; 673 | }; 674 | --------------------------------------------------------------------------------