• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bezoerb / text-metrics / 10893022079

16 Sep 2024 10:27PM UTC coverage: 87.023%. Remained the same
10893022079

push

github

web-flow
Merge pull request #45 from bezoerb/dependabot/npm_and_yarn/webpack-5.94.0

Bump webpack from 5.89.0 to 5.94.0

180 of 216 branches covered (83.33%)

114 of 131 relevant lines covered (87.02%)

62.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.42
/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']);
1,546✔
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',
2✔
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',
2✔
73
  '\u2E30',
74
  '\u10100',
75
  '\u10101',
76
  '\u10102',
77
  '\u1039F',
2✔
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']);
54✔
85

86
// BK: Mandatory Break (A) (Non-tailorable) - http://www.unicode.org/reports/tr14/#BK
87
const BK = new Set(['\u000A']);
54✔
88

89
/* eslint-env es6, browser */
90
const DEFAULTS = {
91
  'font-size': '16px',
92
  'font-weight': '400',
93
  'font-family': 'Helvetica, Arial, sans-serif',
23✔
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);
54!
108

109
  const value = Number.parseFloat(value_);
110
  const unit = value_.replace(value, '');
1✔
111
  // eslint-disable-next-line default-case
112
  switch (unit) {
×
113
    case 'rem':
114
    case 'em': {
115
      return value * baseFontSize;
53✔
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');
25✔
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']);
25✔
138

139
  let wordAddon = 0;
35✔
140
  if (ws && !denyList.has(ws)) {
141
    wordAddon = pxValue(ws);
241✔
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;
44✔
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'];
44✔
171
  if (
172
    ['normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700', '800', '900'].includes(
44✔
173
      fontWeight.toString()
174
    )
175
  ) {
176
    font.push(fontWeight);
177
  }
178

179
  const fontStyle = prop(options, 'font-style', style.getPropertyValue('font-style'));
44✔
180
  if (['normal', 'italic', 'oblique'].includes(fontStyle)) {
44✔
181
    font.push(fontStyle);
182
  }
183

184
  const fontVariant = prop(options, 'font-variant', style.getPropertyValue('font-variant'));
44!
185
  if (['normal', 'small-caps'].includes(fontVariant)) {
44✔
186
    font.push(fontVariant);
187
  }
188

189
  const fontSize = prop(options, 'font-size', style.getPropertyValue('font-size')) || DEFAULTS['font-size'];
44✔
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'];
44✔
194
  font.push(fontFamily);
195

196
  return font.join(' ');
26✔
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';
22✔
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'
45!
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'
165✔
232
    ? element instanceof HTMLElement
233
    : Boolean(
234
        element &&
235
          typeof element === 'object' &&
236
          element !== null &&
237
          element.nodeType === 1 &&
14✔
238
          typeof element.nodeName === 'string'
33✔
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);
20✔
249
}
20✔
250

251
/**
252
 * Get style declaration if available
253
 *
254
 * @returns {CSSStyleDeclaration}
255
 */
256
export function getStyle(element, options) {
257
  const options_ = {...options};
40!
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));
129✔
269
  }
270

271
  return {
272
    getPropertyValue: (key) => prop(options, key),
33✔
273
  };
274
}
2✔
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': {
1✔
287
      return text;
288
    }
289

290
    case 'pre-wrap': {
1!
291
      return text;
292
    }
293

294
    case 'pre-line': {
295
      return (text || '').replace(/\s+/gm, ' ').trim();
30✔
296
    }
297

298
    default: {
299
      return (text || '')
300
        .replace(/[\r\n]/gm, ' ')
301
        .replace(/\s+/gm, ' ')
26✔
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')) {
5✔
316
    case 'uppercase': {
1✔
317
      return text.toUpperCase();
318
    }
319

320
    case 'lowercase': {
20✔
321
      return text.toLowerCase();
29✔
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(/<wbr>/gi, '\u200B')
340
    .replace(/<br\s*\/?>/gi, '\u000A')
341
    .replace(/&shy;/gi, '\u00AD')
31✔
342
    .replace(/&mdash;/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.'
4!
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 || '';
581✔
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;
51✔
378
}
51!
379

380
/**
381
 * Normalize options
382
 *
383
 * @param options
384
 * @returns {*}
385
 */
386
export function normalizeOptions(options) {
387
  const options_ = {};
43✔
388

389
  // Normalize keys (fontSize => font-size)
390
  for (const key of Object.keys(options || {})) {
1✔
391
    const dashedKey = key.replace(/([A-Z])/g, ($1) => '-' + $1.toLowerCase());
51✔
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');
31!
407
    const dpr = window.devicePixelRatio || 1;
408
    const bsr =
409
      ctx.webkitBackingStorePixelRatio ||
410
      ctx.mozBackingStorePixelRatio ||
411
      ctx.msBackingStorePixelRatio ||
412
      ctx.oBackingStorePixelRatio ||
31✔
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);
921!
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}) {
20✔
441
  const addSpacing = addWordAndLetterSpacing(wordSpacing, letterSpacing);
10✔
442
  const lines = [];
8✔
443
  const parts = [];
444
  const breakpoints = [];
445
  let line = '';
446
  let part = '';
817✔
447

448
  if (!text) {
817✔
449
    return [];
450
  }
451

452
  // Compute array of breakpoints
453
  for (const chr of text) {
816✔
454
    const type = checkBreak(chr);
455
    if (part === '' && type === 'BAI') {
456
      continue;
457
    }
458

459
    if (type) {
460
      breakpoints.push({chr, type});
8✔
461

462
      parts.push(part);
463
      part = '';
464
    } else {
465
      part += chr;
466
    }
467
  }
125✔
468

469
  if (part) {
470
    parts.push(part);
8✔
471
  }
472

473
  // Loop over text parts and compute the lines
474
  for (const [i, part] of parts.entries()) {
117✔
475
    if (i === 0) {
476
      line = part;
477
      continue;
117!
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') {
117✔
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);
117!
491
    const width = Math.round(rawWidth);
3✔
492

493
    // Still fits in line
494
    if (width <= max) {
495
      line += chr + part;
3✔
496
      continue;
497
    }
498

499
    // Line is to long, we split at the breakpoint
500
    switch (breakpoint.type) {
1✔
501
      case 'SHY': {
502
        lines.push(line + '-');
1✔
503
        line = part;
504
        break;
37✔
505
      }
506

507
      case 'BA': {
508
        lines.push(line + chr);
37✔
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: {
8✔
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}) {
2!
554
  const addSpacing = addWordAndLetterSpacing(wordSpacing, letterSpacing);
1✔
555
  const lines = [];
556
  let line = '';
557
  let index = 0;
104✔
558

559
  if (!text) {
104✔
560
    return [];
561
  }
562

563
  for (const chr of text) {
1✔
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') {
103!
567
      lines.push(line);
129✔
568
      line = '';
569
      continue;
570
    }
571

572
    const lineLength = line.length;
×
573
    if (BAI.has(chr) && (lineLength === 0 || BAI.has(line[lineLength - 1]))) {
103✔
574
      continue;
103✔
575
    }
576

577
    // Measure width
578
    let rawWidth = ctx.measureText(line + chr).width + addSpacing(line + chr);
103!
579
    let width = Math.ceil(rawWidth);
2✔
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);
103!
585
      width = Math.ceil(rawWidth);
586
    }
587

588
    // Needs at least one character
589
    if (width > max && [...line].length > 0) {
1✔
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;
5✔
601
        }
602

603
        case 'BAI': {
604
          lines.push(line);
97✔
605
          line = '';
606
          break;
103✔
607
        }
608

609
        default: {
1✔
610
          lines.push(line);
2✔
611
          line = chr;
612
          break;
613
        }
614
      }
615
    } else if (chr !== '\u00AD') {
616
      line += chr;
617
    }
618

619
    index++;
2✔
620
  }
621

622
  if ([...line].length > 0) {
623
    lines.push(line);
624
  }
625

626
  return lines;
627
}
41✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc