modern-text 0.5.13 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -454,6 +454,9 @@ function filterEmpty(val) {
454
454
  }
455
455
  return res;
456
456
  }
457
+ function needsFetch(source) {
458
+ return source.startsWith("http://") || source.startsWith("https://") || source.startsWith("blob://");
459
+ }
457
460
 
458
461
  var __defProp$3 = Object.defineProperty;
459
462
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -524,9 +527,6 @@ function definePlugin(options) {
524
527
  }
525
528
 
526
529
  class Measurer {
527
- constructor(_text) {
528
- this._text = _text;
529
- }
530
530
  _styleToDomStyle(style) {
531
531
  const _style = { ...style };
532
532
  for (const key in style) {
@@ -558,8 +558,7 @@ class Measurer {
558
558
  * </ul>
559
559
  * </section>
560
560
  */
561
- createDom() {
562
- const { paragraphs, computedStyle } = this._text;
561
+ createDom(paragraphs, rootStyle) {
563
562
  const documentFragment = document.createDocumentFragment();
564
563
  const dom = document.createElement("section");
565
564
  Object.assign(dom.style, {
@@ -567,7 +566,7 @@ class Measurer {
567
566
  height: "max-content",
568
567
  whiteSpace: "pre-wrap",
569
568
  wordBreak: "break-all",
570
- ...this._styleToDomStyle(computedStyle),
569
+ ...this._styleToDomStyle(rootStyle),
571
570
  position: "fixed",
572
571
  visibility: "hidden"
573
572
  });
@@ -662,8 +661,7 @@ class Measurer {
662
661
  characters
663
662
  };
664
663
  }
665
- measureDom(dom) {
666
- const { paragraphs } = this._text;
664
+ measureDom(paragraphs, dom) {
667
665
  const rect = dom.getBoundingClientRect();
668
666
  const measured = this._measureDom(dom);
669
667
  measured.paragraphs.forEach((p) => {
@@ -715,12 +713,12 @@ class Measurer {
715
713
  boundingBox: new BoundingBox(0, 0, rect.width, rect.height)
716
714
  };
717
715
  }
718
- measure(dom) {
716
+ measure(paragraphs, rootStyle, dom) {
719
717
  let destory;
720
718
  if (!dom) {
721
- ({ dom, destory } = this.createDom());
719
+ ({ dom, destory } = this.createDom(paragraphs, rootStyle));
722
720
  }
723
- const result = this.measureDom(dom);
721
+ const result = this.measureDom(paragraphs, dom);
724
722
  destory?.();
725
723
  return result;
726
724
  }
@@ -823,33 +821,74 @@ class EventEmitter {
823
821
  function highlight() {
824
822
  const paths = [];
825
823
  const clipRects = [];
826
- const svgStringToSvgPaths = /* @__PURE__ */ new Map();
827
- async function getPaths(svg) {
828
- let result = svgStringToSvgPaths.get(svg);
829
- if (!result) {
830
- if (svg.startsWith("http")) {
831
- svg = await fetch(svg).then((rep) => rep.text());
824
+ const loaded = /* @__PURE__ */ new Map();
825
+ const parsed = /* @__PURE__ */ new Map();
826
+ async function loadSvg(svg) {
827
+ if (!loaded.has(svg)) {
828
+ loaded.set(svg, svg);
829
+ try {
830
+ loaded.set(svg, await fetch(svg).then((rep) => rep.text()));
831
+ } catch (err) {
832
+ console.warn(err);
833
+ loaded.delete(svg);
832
834
  }
833
- const dom = parseSvgToDom(svg);
835
+ }
836
+ }
837
+ function getPaths(svg) {
838
+ let result = parsed.get(svg);
839
+ if (!result) {
840
+ const dom = parseSvgToDom(
841
+ needsFetch(svg) ? loaded.get(svg) ?? svg : svg
842
+ );
834
843
  const paths2 = parseSvg(dom);
835
844
  result = { dom, paths: paths2 };
836
- svgStringToSvgPaths.set(svg, result);
845
+ parsed.set(svg, result);
837
846
  }
838
847
  return result;
839
848
  }
840
849
  return definePlugin({
841
850
  name: "highlight",
842
851
  paths,
843
- update: async (text) => {
852
+ load: async (text) => {
853
+ const promises = [];
854
+ text.forEachCharacter((character) => {
855
+ const { computedStyle: style } = character;
856
+ const { highlightImage, highlightReferImage } = style;
857
+ if (needsFetch(highlightImage)) {
858
+ promises.push(loadSvg(highlightImage));
859
+ }
860
+ if (needsFetch(highlightReferImage)) {
861
+ promises.push(loadSvg(highlightReferImage));
862
+ }
863
+ });
864
+ await Promise.all(promises);
865
+ },
866
+ update: (text) => {
844
867
  clipRects.length = 0;
845
868
  paths.length = 0;
846
869
  let groups = [];
847
870
  let group;
848
871
  let prevStyle;
849
872
  text.forEachCharacter((character) => {
850
- const { isVertical, computedStyle: style, inlineBox } = character;
851
- if (!isNone(style.highlightImage) && character.glyphBox) {
852
- if ((!prevStyle || isEqualValue(prevStyle.highlightImage, style.highlightImage) && isEqualValue(prevStyle.highlightColormap, style.highlightColormap) && isEqualValue(prevStyle.highlightLine, style.highlightLine) && isEqualValue(prevStyle.highlightSize, style.highlightSize) && isEqualValue(prevStyle.highlightThickness, style.highlightThickness)) && group?.length && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top) && group[0].fontSize === style.fontSize) {
873
+ const {
874
+ computedStyle: style
875
+ } = character;
876
+ const {
877
+ highlightImage
878
+ } = style;
879
+ if (!isNone(highlightImage)) {
880
+ const {
881
+ inlineBox,
882
+ isVertical
883
+ } = character;
884
+ const {
885
+ fontSize,
886
+ highlightColormap,
887
+ highlightLine,
888
+ highlightSize,
889
+ highlightThickness
890
+ } = style;
891
+ if ((!prevStyle || isEqualValue(prevStyle.highlightImage, highlightImage) && isEqualValue(prevStyle.highlightColormap, highlightColormap) && isEqualValue(prevStyle.highlightLine, highlightLine) && isEqualValue(prevStyle.highlightSize, highlightSize) && isEqualValue(prevStyle.highlightThickness, highlightThickness)) && group?.length && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top) && group[0].fontSize === fontSize) {
853
892
  group.push(character);
854
893
  } else {
855
894
  group = [];
@@ -868,8 +907,12 @@ function highlight() {
868
907
  for (let i = 0; i < groups.length; i++) {
869
908
  const characters = groups[i];
870
909
  const char = characters[0];
871
- const groupBox = BoundingBox.from(...characters.map((c) => c.glyphBox));
872
- const { computedStyle: style } = char;
910
+ const groupBox = BoundingBox.from(
911
+ ...characters.filter((c) => c.glyphBox).map((c) => c.glyphBox)
912
+ );
913
+ const {
914
+ computedStyle: style
915
+ } = char;
873
916
  const {
874
917
  fontSize,
875
918
  writingMode,
@@ -883,7 +926,7 @@ function highlight() {
883
926
  const isVertical = writingMode.includes("vertical");
884
927
  const thickness = parseValueNumber(highlightThickness, { fontSize, total: groupBox.width }) / groupBox.width;
885
928
  const colormap = parseColormap(highlightColormap);
886
- const { paths: svgPaths, dom: svgDom } = await getPaths(highlightImage);
929
+ const { paths: svgPaths, dom: svgDom } = getPaths(highlightImage);
887
930
  const aBox = getPathsBoundingBox(svgPaths, true);
888
931
  const styleScale = fontSize / aBox.width * 2;
889
932
  const cBox = new BoundingBox().copy(groupBox);
@@ -899,7 +942,7 @@ function highlight() {
899
942
  cBox.width = userWidth;
900
943
  }
901
944
  if (!isNone(highlightReferImage) && isNone(highlightLine)) {
902
- const bBox = getPathsBoundingBox((await getPaths(highlightReferImage)).paths, true);
945
+ const bBox = getPathsBoundingBox(getPaths(highlightReferImage).paths, true);
903
946
  aBox.copy(bBox);
904
947
  } else {
905
948
  let line;
@@ -1050,8 +1093,16 @@ function listStyle() {
1050
1093
  const { paragraphs, isVertical, fontSize } = text;
1051
1094
  const padding = fontSize * 0.45;
1052
1095
  paragraphs.forEach((paragraph) => {
1053
- const { computedStyle: style } = paragraph;
1054
- const { listStyleImage, listStyleColormap, listStyleSize, listStyleType, color } = style;
1096
+ const {
1097
+ computedStyle: style
1098
+ } = paragraph;
1099
+ const {
1100
+ color,
1101
+ listStyleImage,
1102
+ listStyleColormap,
1103
+ listStyleSize,
1104
+ listStyleType
1105
+ } = style;
1055
1106
  const colormap = parseColormap(listStyleColormap);
1056
1107
  let size = listStyleSize;
1057
1108
  let image;
@@ -1080,18 +1131,10 @@ function listStyle() {
1080
1131
  const m = new Matrix3();
1081
1132
  if (isVertical) {
1082
1133
  const reScale = fontSize / imageBox.height * scale;
1083
- m.translate(-imageBox.left, -imageBox.top);
1084
- m.rotate(Math.PI / 2);
1085
- m.scale(reScale, reScale);
1086
- m.translate(fontSize / 2 - imageBox.height * reScale / 2, 0);
1087
- m.translate(box.left + (box.width - fontSize) / 2, fBox.top - padding);
1134
+ m.translate(-imageBox.left, -imageBox.top).rotate(Math.PI / 2).scale(reScale, reScale).translate(fontSize / 2 - imageBox.height * reScale / 2, 0).translate(box.left + (box.width - fontSize) / 2, fBox.top - padding);
1088
1135
  } else {
1089
1136
  const reScale = fontSize / imageBox.height * scale;
1090
- m.translate(-imageBox.left, -imageBox.top);
1091
- m.translate(-imageBox.width, 0);
1092
- m.scale(reScale, reScale);
1093
- m.translate(0, fontSize / 2 - imageBox.height * reScale / 2);
1094
- m.translate(fBox.left - padding, box.top + (box.height - fontSize) / 2);
1137
+ m.translate(-imageBox.left, -imageBox.top).translate(-imageBox.width, 0).scale(reScale, reScale).translate(0, fontSize / 2 - imageBox.height * reScale / 2).translate(fBox.left - padding, box.top + (box.height - fontSize) / 2);
1095
1138
  }
1096
1139
  paths.push(...imagePaths.map((p) => {
1097
1140
  const path = p.clone();
@@ -1227,11 +1270,24 @@ function textDecoration() {
1227
1270
  let group;
1228
1271
  let prevStyle;
1229
1272
  text.forEachCharacter((character) => {
1230
- const { computedStyle: style, isVertical, inlineBox, underlinePosition, underlineThickness, strikeoutPosition, strikeoutSize } = character;
1231
- if (!isNone(style.textDecoration)) {
1273
+ const {
1274
+ computedStyle: style,
1275
+ isVertical,
1276
+ inlineBox,
1277
+ underlinePosition,
1278
+ underlineThickness,
1279
+ strikeoutPosition,
1280
+ strikeoutSize
1281
+ } = character;
1282
+ const {
1283
+ color,
1284
+ textDecoration: textDecoration2,
1285
+ writingMode
1286
+ } = style;
1287
+ if (!isNone(textDecoration2)) {
1232
1288
  let flag = false;
1233
- if (prevStyle?.textDecoration === style.textDecoration && prevStyle?.writingMode === style.writingMode && prevStyle?.color === style.color && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top)) {
1234
- switch (style.textDecoration) {
1289
+ if (prevStyle?.textDecoration === textDecoration2 && prevStyle?.writingMode === writingMode && prevStyle?.color === color && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top)) {
1290
+ switch (textDecoration2) {
1235
1291
  case "underline":
1236
1292
  if (group[0].underlinePosition === underlinePosition && group[0].underlineThickness === underlineThickness) {
1237
1293
  flag = true;
@@ -1257,8 +1313,18 @@ function textDecoration() {
1257
1313
  }
1258
1314
  });
1259
1315
  groups.forEach((group2) => {
1260
- const { computedStyle: style, isVertical, underlinePosition, underlineThickness, strikeoutPosition, strikeoutSize } = group2[0];
1261
- const { textDecoration: textDecoration2, color } = style;
1316
+ const {
1317
+ computedStyle: style,
1318
+ isVertical,
1319
+ underlinePosition,
1320
+ underlineThickness,
1321
+ strikeoutPosition,
1322
+ strikeoutSize
1323
+ } = group2[0];
1324
+ const {
1325
+ color,
1326
+ textDecoration: textDecoration2
1327
+ } = style;
1262
1328
  const { left, top, width, height } = BoundingBox.from(...group2.map((c) => c.inlineBox));
1263
1329
  let position = isVertical ? left + width : top;
1264
1330
  const direction = isVertical ? -1 : 1;
@@ -1401,7 +1467,7 @@ class Text extends EventEmitter {
1401
1467
  __publicField(this, "glyphBox", new BoundingBox());
1402
1468
  __publicField(this, "pathBox", new BoundingBox());
1403
1469
  __publicField(this, "boundingBox", new BoundingBox());
1404
- __publicField(this, "measurer", new Measurer(this));
1470
+ __publicField(this, "measurer", new Measurer());
1405
1471
  __publicField(this, "plugins", /* @__PURE__ */ new Map());
1406
1472
  __publicField(this, "fonts");
1407
1473
  this.debug = options.debug ?? false;
@@ -1435,6 +1501,9 @@ class Text extends EventEmitter {
1435
1501
  });
1436
1502
  return this;
1437
1503
  }
1504
+ async load() {
1505
+ await Promise.all(Array.from(this.plugins.values()).map((p) => p.load?.(this)));
1506
+ }
1438
1507
  updateParagraphs() {
1439
1508
  this.computedStyle = { ...defaultTextStyles, ...this.style };
1440
1509
  let { content, computedStyle: style } = this;
@@ -1486,7 +1555,7 @@ class Text extends EventEmitter {
1486
1555
  this.paragraphs = paragraphs;
1487
1556
  return this;
1488
1557
  }
1489
- async measure(dom = this.measureDom) {
1558
+ measure(dom = this.measureDom) {
1490
1559
  const old = {
1491
1560
  paragraphs: this.paragraphs,
1492
1561
  lineBox: this.lineBox,
@@ -1496,18 +1565,16 @@ class Text extends EventEmitter {
1496
1565
  boundingBox: this.boundingBox
1497
1566
  };
1498
1567
  this.updateParagraphs();
1499
- const result = this.measurer.measure(dom);
1568
+ const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom);
1500
1569
  this.paragraphs = result.paragraphs;
1501
1570
  this.lineBox = result.boundingBox;
1502
1571
  this.characters.forEach((c) => {
1503
1572
  c.update(this.fonts);
1504
1573
  });
1505
1574
  this.rawGlyphBox = this.getGlyphBox();
1506
- const plugins = Array.from(this.plugins.values());
1507
- plugins.sort((a, b) => (a.updateOrder ?? 0) - (b.updateOrder ?? 0));
1508
- for (let i = 0; i < plugins.length; i++) {
1509
- await plugins[i].update?.(this);
1510
- }
1575
+ Array.from(this.plugins.values()).sort((a, b) => (a.updateOrder ?? 0) - (b.updateOrder ?? 0)).forEach((plugin) => {
1576
+ plugin.update?.(this);
1577
+ });
1511
1578
  this.glyphBox = this.getGlyphBox();
1512
1579
  this.updatePathBox().updateBoundingBox();
1513
1580
  for (const key in old) {
@@ -1563,30 +1630,28 @@ class Text extends EventEmitter {
1563
1630
  this.needsUpdate = true;
1564
1631
  return this;
1565
1632
  }
1566
- async update() {
1567
- const result = await this.measure();
1633
+ update() {
1634
+ const result = this.measure();
1568
1635
  for (const key in result) {
1569
1636
  this[key] = result[key];
1570
1637
  }
1571
1638
  this.emit("update", { text: this });
1639
+ return this;
1572
1640
  }
1573
- async render(options) {
1641
+ render(options) {
1574
1642
  const { view, pixelRatio = 2 } = options;
1575
1643
  const ctx = view.getContext("2d");
1576
1644
  if (!ctx) {
1577
1645
  return;
1578
1646
  }
1579
1647
  if (this.needsUpdate) {
1580
- await this.update();
1648
+ this.update();
1581
1649
  }
1582
1650
  setupView(ctx, pixelRatio, this.boundingBox);
1583
1651
  uploadColors(ctx, this);
1584
- const plugins = Array.from(this.plugins.values());
1585
- plugins.sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0));
1586
- for (let i = 0; i < plugins.length; i++) {
1587
- const plugin = plugins[i];
1652
+ Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
1588
1653
  if (plugin.render) {
1589
- await plugin.render?.(ctx, this);
1654
+ plugin.render?.(ctx, this);
1590
1655
  } else if (plugin.paths) {
1591
1656
  const style = this.computedStyle;
1592
1657
  plugin.paths.forEach((path) => {
@@ -1597,17 +1662,21 @@ class Text extends EventEmitter {
1597
1662
  });
1598
1663
  });
1599
1664
  }
1600
- }
1665
+ });
1601
1666
  this.emit("render", { text: this, view, pixelRatio });
1602
1667
  }
1603
1668
  }
1604
1669
 
1605
- function measureText(options) {
1606
- return new Text(options).measure();
1670
+ async function measureText(options) {
1671
+ const text = new Text(options);
1672
+ await text.load();
1673
+ return text.measure();
1607
1674
  }
1608
1675
 
1609
- function renderText(options) {
1610
- return new Text(options).render(options);
1676
+ async function renderText(options) {
1677
+ const text = new Text(options);
1678
+ await text.load();
1679
+ text.render(options);
1611
1680
  }
1612
1681
 
1613
- export { Character, Fragment, Measurer, Paragraph, Text, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, hexToRgb, highlight, isEqualObject, isEqualValue, isNone, listStyle, measureText, parseColor, parseColormap, parseValueNumber, render, renderText, setupView, textDecoration, uploadColor, uploadColors };
1682
+ export { Character, Fragment, Measurer, Paragraph, Text, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, hexToRgb, highlight, isEqualObject, isEqualValue, isNone, listStyle, measureText, needsFetch, parseColor, parseColormap, parseValueNumber, render, renderText, setupView, textDecoration, uploadColor, uploadColors };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "modern-text",
3
3
  "type": "module",
4
- "version": "0.5.13",
4
+ "version": "0.6.0",
5
5
  "packageManager": "pnpm@9.9.0",
6
6
  "description": "Measure and render text in a way that describes the DOM.",
7
7
  "author": "wxm",