modern-text 0.5.2 → 0.5.4

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
@@ -51,7 +51,7 @@ function parseCssLinearGradient(css, x, y, width, height) {
51
51
  }
52
52
 
53
53
  function drawPath(options) {
54
- const { ctx, path, fontSize, clipRect } = options;
54
+ const { ctx, path, fontSize } = options;
55
55
  ctx.save();
56
56
  ctx.beginPath();
57
57
  const pathStyle = path.style;
@@ -65,11 +65,6 @@ function drawPath(options) {
65
65
  shadowBlur: (options.shadowBlur ?? 0) * fontSize,
66
66
  shadowColor: options.shadowColor
67
67
  };
68
- if (clipRect) {
69
- ctx.rect(clipRect.left, clipRect.top, clipRect.width, clipRect.height);
70
- ctx.clip();
71
- ctx.beginPath();
72
- }
73
68
  path.drawTo(ctx, style);
74
69
  ctx.restore();
75
70
  }
@@ -351,6 +346,40 @@ class Character {
351
346
  }
352
347
  }
353
348
 
349
+ function parseValueNumber(value, ctx) {
350
+ if (typeof value === "number") {
351
+ return value;
352
+ } else {
353
+ if (value.endsWith("%")) {
354
+ value = value.substring(0, value.length - 1);
355
+ return Math.ceil(Number(value) / 100 * ctx.total);
356
+ } else if (value.endsWith("rem")) {
357
+ value = value.substring(0, value.length - 3);
358
+ return Number(value) * ctx.fontSize;
359
+ } else if (value.endsWith("em")) {
360
+ value = value.substring(0, value.length - 2);
361
+ return Number(value) * ctx.fontSize;
362
+ } else {
363
+ return Number(value);
364
+ }
365
+ }
366
+ }
367
+ function parseColormap(colormap) {
368
+ const _colormap = isNone(colormap) ? {} : colormap;
369
+ return Object.keys(_colormap).reduce((obj, key) => {
370
+ let value = _colormap[key];
371
+ const keyRgb = hexToRgb(key);
372
+ const valueRgb = hexToRgb(value);
373
+ if (keyRgb) {
374
+ key = keyRgb;
375
+ }
376
+ if (valueRgb) {
377
+ value = valueRgb;
378
+ }
379
+ obj[key] = value;
380
+ return obj;
381
+ }, {});
382
+ }
354
383
  function isNone(val) {
355
384
  return !val || val === "none";
356
385
  }
@@ -358,7 +387,18 @@ function isEqualObject(obj1, obj2) {
358
387
  const keys1 = Object.keys(obj1);
359
388
  const keys2 = Object.keys(obj2);
360
389
  const keys = Array.from(/* @__PURE__ */ new Set([...keys1, ...keys2]));
361
- return keys.every((key) => obj1[key] === obj2[key]);
390
+ return keys.every((key) => isEqualValue(obj1[key], obj2[key]));
391
+ }
392
+ function isEqualValue(val1, val2) {
393
+ const typeof1 = typeof val1;
394
+ const typeof2 = typeof val2;
395
+ if (typeof1 === typeof2) {
396
+ if (typeof1 === "object") {
397
+ return isEqualObject(val1, val2);
398
+ }
399
+ return val1 === val2;
400
+ }
401
+ return false;
362
402
  }
363
403
  function hexToRgb(hex) {
364
404
  const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
@@ -382,6 +422,30 @@ function filterEmpty(val) {
382
422
  }
383
423
  return res;
384
424
  }
425
+ function closestDivisor(dividend, targetDivisor) {
426
+ if (dividend <= 0) {
427
+ throw new Error("Dividend must be a positive integer.");
428
+ }
429
+ const divisors = [];
430
+ for (let i = 1; i <= Math.sqrt(dividend); i++) {
431
+ if (dividend % i === 0) {
432
+ divisors.push(i);
433
+ if (i !== dividend / i) {
434
+ divisors.push(dividend / i);
435
+ }
436
+ }
437
+ }
438
+ let closest = divisors[0];
439
+ let minDifference = Math.abs(closest - targetDivisor);
440
+ for (const divisor of divisors) {
441
+ const difference = Math.abs(divisor - targetDivisor);
442
+ if (difference < minDifference) {
443
+ closest = divisor;
444
+ minDifference = difference;
445
+ }
446
+ }
447
+ return closest;
448
+ }
385
449
 
386
450
  var __defProp$2 = Object.defineProperty;
387
451
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -653,39 +717,8 @@ class Measurer {
653
717
  }
654
718
  }
655
719
 
656
- function parseCharsPerRepeat(size, fontSize, total) {
657
- if (size === "cover") {
658
- return 0;
659
- } else if (typeof size === "string") {
660
- if (size.endsWith("%")) {
661
- const rate = Number(size.substring(0, size.length - 1)) / 100;
662
- return Math.ceil(rate * total / fontSize);
663
- } else if (size.endsWith("rem")) {
664
- return Number(size.substring(0, size.length - 3));
665
- } else {
666
- return Math.ceil(Number(size) / fontSize);
667
- }
668
- } else {
669
- return Math.ceil(size / fontSize);
670
- }
671
- }
672
- function parseThickness(thickness, fontSize, total) {
673
- if (typeof thickness === "string") {
674
- if (thickness.endsWith("%")) {
675
- return Number(thickness.substring(0, thickness.length - 1)) / 100;
676
- } else if (thickness.endsWith("rem")) {
677
- const value = Number(thickness.substring(0, thickness.length - 3));
678
- return value * fontSize / total;
679
- } else {
680
- return Number(thickness) / total;
681
- }
682
- } else {
683
- return thickness / total;
684
- }
685
- }
686
720
  function highlight() {
687
721
  const paths = [];
688
- const clipRects = [];
689
722
  const svgStringToSvgPaths = /* @__PURE__ */ new Map();
690
723
  function getPaths(svg) {
691
724
  let result = svgStringToSvgPaths.get(svg);
@@ -708,7 +741,7 @@ function highlight() {
708
741
  text.forEachCharacter((character) => {
709
742
  const { isVertical, computedStyle: style, inlineBox, fontSize } = character;
710
743
  if (!isNone(style.highlightImage) && character.glyphBox) {
711
- if (style.highlightSize !== "1rem" && (!prevStyle || prevStyle.highlightImage === style.highlightImage && isEqualObject(prevStyle.highlightImageColors, style.highlightImageColors) && prevStyle.highlightLine === style.highlightLine && prevStyle.highlightSize === style.highlightSize && prevStyle.highlightThickness === style.highlightThickness && prevStyle.highlightOverflow === style.highlightOverflow) && group?.length && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top) && group[0].fontSize === fontSize) {
744
+ if (style.highlightSize !== "1rem" && (!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 === fontSize) {
712
745
  group.push(character);
713
746
  } else {
714
747
  group = [];
@@ -718,120 +751,119 @@ function highlight() {
718
751
  }
719
752
  prevStyle = style;
720
753
  });
721
- groups.filter((characters) => characters.length).map((characters) => {
754
+ groups.filter((characters) => characters.length).forEach((characters) => {
722
755
  const char = characters[0];
723
- return {
724
- char,
725
- groupBox: BoundingBox.from(...characters.map((c) => c.glyphBox))
726
- };
727
- }).forEach((group2) => {
728
- const { char, groupBox } = group2;
756
+ const groupBox = BoundingBox.from(...characters.map((c) => c.glyphBox));
729
757
  const { computedStyle: style } = char;
730
758
  const {
731
759
  fontSize,
732
760
  writingMode,
733
- highlightThickness,
734
- highlightSize,
735
- highlightLine,
736
- highlightOverflow,
737
761
  highlightImage,
738
- highlightImageColors
762
+ highlightReferImage,
763
+ highlightColormap,
764
+ highlightLine,
765
+ highlightSize,
766
+ highlightThickness
739
767
  } = style;
740
768
  const isVertical = writingMode.includes("vertical");
741
- const thickness = parseThickness(highlightThickness, fontSize, groupBox.width);
742
- const charsPerRepeat = parseCharsPerRepeat(highlightSize, fontSize, groupBox.width);
743
- const overflow = isNone(highlightOverflow) ? charsPerRepeat ? "hidden" : "visible" : highlightOverflow;
744
- const colors = Object.keys(highlightImageColors).reduce((obj, key) => {
745
- let value = highlightImageColors[key];
746
- const keyRgb = hexToRgb(key);
747
- const valueRgb = hexToRgb(value);
748
- if (keyRgb) {
749
- key = keyRgb;
750
- }
751
- if (valueRgb) {
752
- value = valueRgb;
753
- }
754
- obj[key] = value;
755
- return obj;
756
- }, {});
769
+ const thickness = parseValueNumber(highlightThickness, { fontSize, total: groupBox.width }) / groupBox.width;
770
+ const colormap = parseColormap(highlightColormap);
757
771
  const { paths: svgPaths, dom: svgDom } = getPaths(highlightImage);
758
772
  const aBox = getPathsBoundingBox(svgPaths, true);
759
773
  const styleScale = fontSize / aBox.width * 2;
760
774
  const cBox = new BoundingBox().copy(groupBox);
761
- cBox.width = charsPerRepeat ? fontSize * charsPerRepeat : isVertical ? groupBox.height : groupBox.width;
762
- cBox.height = isVertical ? groupBox.width : groupBox.height;
763
- const width = isVertical ? cBox.height : cBox.width;
764
- let line;
765
- if (isNone(highlightLine)) {
766
- if (aBox.width / aBox.height > 4) {
767
- line = "underline";
768
- const viewBox = svgDom.getAttribute("viewBox");
769
- if (viewBox) {
770
- const aCenter = aBox.y + aBox.height / 2;
771
- const [_x, y, _w, h] = viewBox.split(" ").map((v) => Number(v));
772
- const viewCenter = y + h / 2;
773
- if (aBox.y < viewCenter && aBox.y + aBox.height > viewCenter) {
774
- line = "line-through";
775
- } else if (viewCenter > aCenter) {
776
- line = "overline";
777
- } else {
778
- line = "underline";
775
+ if (isVertical) {
776
+ cBox.width = groupBox.height;
777
+ cBox.height = groupBox.width;
778
+ cBox.left = groupBox.left + groupBox.width;
779
+ }
780
+ const rawWidth = Math.floor(cBox.width);
781
+ let userWidth = rawWidth;
782
+ if (highlightSize !== "cover") {
783
+ userWidth = parseValueNumber(highlightSize, { fontSize, total: groupBox.width });
784
+ userWidth = closestDivisor(rawWidth, userWidth);
785
+ cBox.width = userWidth;
786
+ }
787
+ if (!isNone(highlightReferImage) && isNone(highlightLine)) {
788
+ const bBox = getPathsBoundingBox(getPaths(highlightReferImage).paths, true);
789
+ aBox.copy(bBox);
790
+ } else {
791
+ let line;
792
+ if (isNone(highlightLine)) {
793
+ if (aBox.width / aBox.height > 4) {
794
+ line = "underline";
795
+ const viewBox = svgDom.getAttribute("viewBox");
796
+ if (viewBox) {
797
+ const [_x, y, _w, h] = viewBox.split(" ").map((v) => Number(v));
798
+ const viewCenter = y + h / 2;
799
+ if (aBox.y < viewCenter && aBox.y + aBox.height > viewCenter) {
800
+ line = "line-through";
801
+ } else if (aBox.y + aBox.height < viewCenter) {
802
+ line = "overline";
803
+ } else {
804
+ line = "underline";
805
+ }
779
806
  }
807
+ } else {
808
+ line = "outline";
780
809
  }
781
810
  } else {
782
- line = "outline";
811
+ line = highlightLine;
783
812
  }
784
- } else {
785
- line = highlightLine;
786
- }
787
- switch (line) {
788
- case "outline": {
789
- const paddingX = cBox.width * 0.2;
790
- const paddingY = cBox.height * 0.2;
791
- cBox.width += paddingX;
792
- cBox.height += paddingY;
793
- if (isVertical) {
794
- cBox.x -= paddingY / 2;
795
- cBox.y -= paddingX / 2;
796
- cBox.x += cBox.height;
797
- } else {
798
- cBox.x -= paddingX / 2;
799
- cBox.y -= paddingY / 2;
813
+ switch (line) {
814
+ case "outline": {
815
+ const paddingX = cBox.width * 0.2;
816
+ const paddingY = cBox.height * 0.2;
817
+ cBox.width += paddingX;
818
+ cBox.height += paddingY;
819
+ if (isVertical) {
820
+ cBox.x -= paddingY / 2;
821
+ cBox.y -= paddingX / 2;
822
+ cBox.x += cBox.height;
823
+ } else {
824
+ cBox.x -= paddingX / 2;
825
+ cBox.y -= paddingY / 2;
826
+ }
827
+ break;
800
828
  }
801
- break;
829
+ case "overline":
830
+ cBox.height = aBox.height * styleScale;
831
+ if (isVertical) {
832
+ cBox.x = char.inlineBox.left + char.inlineBox.width;
833
+ } else {
834
+ cBox.y = char.inlineBox.top;
835
+ }
836
+ break;
837
+ case "line-through":
838
+ cBox.height = aBox.height * styleScale;
839
+ if (isVertical) {
840
+ cBox.x = char.inlineBox.left + char.inlineBox.width - char.strikeoutPosition + cBox.height / 2;
841
+ } else {
842
+ cBox.y = char.inlineBox.top + char.strikeoutPosition - cBox.height / 2;
843
+ }
844
+ break;
845
+ case "underline":
846
+ cBox.height = aBox.height * styleScale;
847
+ if (isVertical) {
848
+ cBox.x = char.inlineBox.left + char.inlineBox.width - char.underlinePosition;
849
+ } else {
850
+ cBox.y = char.inlineBox.top + char.underlinePosition;
851
+ }
852
+ break;
802
853
  }
803
- case "overline":
804
- cBox.height = aBox.height * styleScale;
805
- if (isVertical) {
806
- cBox.x = char.inlineBox.left + char.inlineBox.width;
807
- } else {
808
- cBox.y = char.inlineBox.top;
809
- }
810
- break;
811
- case "line-through":
812
- cBox.height = aBox.height * styleScale;
813
- if (isVertical) {
814
- cBox.x = char.inlineBox.left + char.inlineBox.width - char.strikeoutPosition + cBox.height / 2;
815
- } else {
816
- cBox.y = char.inlineBox.top + char.strikeoutPosition - cBox.height / 2;
817
- }
818
- break;
819
- case "underline":
820
- cBox.height = aBox.height * styleScale;
821
- if (isVertical) {
822
- cBox.x = char.inlineBox.left + char.inlineBox.width - char.underlinePosition;
823
- } else {
824
- cBox.y = char.inlineBox.top + char.underlinePosition;
825
- }
826
- break;
827
854
  }
828
855
  const transform = new Matrix3().translate(-aBox.x, -aBox.y).scale(cBox.width / aBox.width, cBox.height / aBox.height);
829
856
  if (isVertical) {
830
857
  transform.rotate(-Math.PI / 2);
831
858
  }
832
859
  transform.translate(cBox.x, cBox.y);
833
- for (let i = 0, len = Math.ceil(groupBox.width / width); i < len; i++) {
834
- const _transform = transform.clone().translate(i * width, 0);
860
+ for (let i = 0, len = rawWidth / userWidth; i < len; i++) {
861
+ const _transform = transform.clone();
862
+ if (isVertical) {
863
+ _transform.translate(0, i * cBox.width);
864
+ } else {
865
+ _transform.translate(i * cBox.width, 0);
866
+ }
835
867
  svgPaths.forEach((originalPath) => {
836
868
  const path = originalPath.clone().matrix(_transform);
837
869
  if (path.style.strokeWidth) {
@@ -846,30 +878,23 @@ function highlight() {
846
878
  if (path.style.strokeDasharray) {
847
879
  path.style.strokeDasharray = path.style.strokeDasharray.map((v) => v * styleScale);
848
880
  }
849
- if (path.style.fill && path.style.fill in colors) {
850
- path.style.fill = colors[path.style.fill];
881
+ if (path.style.fill && path.style.fill in colormap) {
882
+ path.style.fill = colormap[path.style.fill];
851
883
  }
852
- if (path.style.stroke && path.style.stroke in colors) {
853
- path.style.stroke = colors[path.style.stroke];
884
+ if (path.style.stroke && path.style.stroke in colormap) {
885
+ path.style.stroke = colormap[path.style.stroke];
854
886
  }
855
887
  paths.push(path);
856
- clipRects[paths.length - 1] = overflow === "hidden" ? new BoundingBox(
857
- groupBox.left,
858
- groupBox.top - groupBox.height,
859
- groupBox.width,
860
- groupBox.height * 3
861
- ) : void 0;
862
888
  });
863
889
  }
864
890
  });
865
891
  },
866
892
  renderOrder: -1,
867
893
  render: (ctx, text) => {
868
- paths.forEach((path, index) => {
894
+ paths.forEach((path) => {
869
895
  drawPath({
870
896
  ctx,
871
897
  path,
872
- clipRect: clipRects[index],
873
898
  fontSize: text.computedStyle.fontSize
874
899
  });
875
900
  if (text.debug) {
@@ -883,22 +908,6 @@ function highlight() {
883
908
  });
884
909
  }
885
910
 
886
- function parseScale(size, fontSize, total) {
887
- if (size === "cover") {
888
- return 1;
889
- } else if (typeof size === "string") {
890
- if (size.endsWith("%")) {
891
- return Number(size.substring(0, size.length - 1)) / 100;
892
- } else if (size.endsWith("rem")) {
893
- const value = Number(size.substring(0, size.length - 3));
894
- return value * fontSize / total;
895
- } else {
896
- return Number(size) / total;
897
- }
898
- } else {
899
- return size / total;
900
- }
901
- }
902
911
  function listStyle() {
903
912
  const paths = [];
904
913
  return definePlugin({
@@ -910,20 +919,8 @@ function listStyle() {
910
919
  const padding = fontSize * 0.45;
911
920
  paragraphs.forEach((paragraph) => {
912
921
  const { computedStyle: style } = paragraph;
913
- const { listStyleImage, listStyleImageColors, listStyleSize, listStyleType, color } = style;
914
- const colors = Object.keys(listStyleImageColors).reduce((obj, key) => {
915
- let value = listStyleImageColors[key];
916
- const keyRgb = hexToRgb(key);
917
- const valueRgb = hexToRgb(value);
918
- if (keyRgb) {
919
- key = keyRgb;
920
- }
921
- if (valueRgb) {
922
- value = valueRgb;
923
- }
924
- obj[key] = value;
925
- return obj;
926
- }, {});
922
+ const { listStyleImage, listStyleColormap, listStyleSize, listStyleType, color } = style;
923
+ const colormap = parseColormap(listStyleColormap);
927
924
  let size = listStyleSize;
928
925
  let image;
929
926
  if (!isNone(listStyleImage)) {
@@ -947,9 +944,9 @@ function listStyle() {
947
944
  const box = paragraph.lineBox;
948
945
  const fBox = paragraph.fragments[0].inlineBox;
949
946
  if (fBox) {
947
+ const scale = size === "cover" ? 1 : parseValueNumber(size, { total: fontSize, fontSize }) / fontSize;
950
948
  const m = new Matrix3();
951
949
  if (isVertical) {
952
- const scale = parseScale(size, fontSize, fontSize);
953
950
  const reScale = fontSize / imageBox.height * scale;
954
951
  m.translate(-imageBox.left, -imageBox.top);
955
952
  m.rotate(Math.PI / 2);
@@ -957,7 +954,6 @@ function listStyle() {
957
954
  m.translate(fontSize / 2 - imageBox.height * reScale / 2, 0);
958
955
  m.translate(box.left + (box.width - fontSize) / 2, fBox.top - padding);
959
956
  } else {
960
- const scale = parseScale(size, fontSize, fontSize);
961
957
  const reScale = fontSize / imageBox.height * scale;
962
958
  m.translate(-imageBox.left, -imageBox.top);
963
959
  m.translate(-imageBox.width, 0);
@@ -968,11 +964,11 @@ function listStyle() {
968
964
  paths.push(...imagePaths.map((p) => {
969
965
  const path = p.clone();
970
966
  path.matrix(m);
971
- if (path.style.fill && path.style.fill in colors) {
972
- path.style.fill = colors[path.style.fill];
967
+ if (path.style.fill && path.style.fill in colormap) {
968
+ path.style.fill = colormap[path.style.fill];
973
969
  }
974
- if (path.style.stroke && path.style.stroke in colors) {
975
- path.style.stroke = colors[path.style.stroke];
970
+ if (path.style.stroke && path.style.stroke in colormap) {
971
+ path.style.stroke = colormap[path.style.stroke];
976
972
  }
977
973
  return path;
978
974
  }));
@@ -1237,16 +1233,16 @@ const defaultTextStyles = {
1237
1233
  // listStyle
1238
1234
  listStyleType: "none",
1239
1235
  listStyleImage: "none",
1240
- listStyleImageColors: {},
1236
+ listStyleColormap: "none",
1241
1237
  listStyleSize: "cover",
1242
1238
  listStylePosition: "outside",
1243
1239
  // highlight
1244
1240
  highlightImage: "none",
1245
- highlightImageColors: {},
1241
+ highlightReferImage: "none",
1242
+ highlightColormap: "none",
1246
1243
  highlightLine: "none",
1247
1244
  highlightSize: "cover",
1248
1245
  highlightThickness: "100%",
1249
- highlightOverflow: "none",
1250
1246
  // shadow
1251
1247
  shadowColor: "rgba(0, 0, 0, 0)",
1252
1248
  shadowOffsetX: 0,
@@ -1478,4 +1474,4 @@ function renderText(options) {
1478
1474
  return new Text(options).render(options);
1479
1475
  }
1480
1476
 
1481
- export { Character, Fragment, Measurer, Paragraph, Text, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, hexToRgb, highlight, isEqualObject, isNone, listStyle, measureText, parseColor, render, renderText, setupView, textDecoration, uploadColor, uploadColors };
1477
+ export { Character, Fragment, Measurer, Paragraph, Text, closestDivisor, defaultTextStyles, definePlugin, drawPath, filterEmpty, getTransform2D, hexToRgb, highlight, isEqualObject, isEqualValue, isNone, listStyle, measureText, 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.2",
4
+ "version": "0.5.4",
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",