UNPKG

modern-text

Version:

Measure and render text in a way that describes the DOM.

1,905 lines 61.3 kB
'use strict';

const modernIdoc = require('modern-idoc');
const modernPath2d = require('modern-path2d');
const modernFont = require('modern-font');

class Canvas2DRenderer {
  constructor(text, context) {
    this.text = text;
    this.context = context;
  }
  pixelRatio = window?.devicePixelRatio || 1;
  _setupView = () => {
    const pixelRatio = this.pixelRatio;
    const ctx = this.context;
    const { left, top, width, height } = this.text.boundingBox;
    const view = ctx.canvas;
    view.dataset.viewBox = String(`${left} ${top} ${width} ${height}`);
    view.dataset.pixelRatio = String(pixelRatio);
    const canvasWidth = width;
    const canvasHeight = height;
    view.width = Math.max(1, Math.ceil(canvasWidth * pixelRatio));
    view.height = Math.max(1, Math.ceil(canvasHeight * pixelRatio));
    view.style.width = `${canvasWidth}px`;
    view.style.height = `${canvasHeight}px`;
    ctx.clearRect(0, 0, view.width, view.height);
    ctx.scale(pixelRatio, pixelRatio);
    ctx.translate(-left, -top);
  };
  _setupColors = () => {
    const { paragraphs, computedStyle, glyphBox } = this.text;
    this.uploadColor(computedStyle, glyphBox);
    paragraphs.forEach((paragraph) => {
      this.uploadColor(paragraph.computedStyle, paragraph.lineBox);
      paragraph.fragments.forEach((fragment) => {
        this.uploadColor(fragment.computedStyle, fragment.inlineBox);
      });
    });
  };
  setup = () => {
    this._setupView();
    this._setupColors();
    return this;
  };
  _parseColor = (source, box) => {
    if (typeof source === "string" && modernIdoc.isGradient(source)) {
      const gradient = modernIdoc.parseGradient(source)[0];
      if (gradient) {
        switch (gradient.type) {
          case "linear-gradient": {
            let deg = 0;
            if (gradient.orientation) {
              switch (gradient.orientation.type) {
                case "angular":
                  deg = Number(gradient.orientation.value);
                  break;
              }
            }
            const { left, top, width, height } = box;
            const rad = deg * Math.PI / 180;
            const offsetX = width * Math.sin(rad);
            const offsetY = height * Math.cos(rad);
            const canvasGradient = this.context.createLinearGradient(
              left + width / 2 - offsetX,
              top + height / 2 + offsetY,
              left + width / 2 + offsetX,
              top + height / 2 - offsetY
            );
            gradient.colorStops.forEach((colorStop) => {
              let offset = 0;
              if (colorStop.length) {
                switch (colorStop.length.type) {
                  case "%":
                    offset = Number(colorStop.length.value) / 100;
                    break;
                }
              }
              switch (colorStop.type) {
                case "rgb":
                  canvasGradient.addColorStop(offset, `rgb(${colorStop.value.join(", ")})`);
                  break;
                case "rgba":
                  canvasGradient.addColorStop(offset, `rgba(${colorStop.value.join(", ")})`);
                  break;
                case "hex":
                  canvasGradient.addColorStop(offset, `#${colorStop.value}`);
                  break;
              }
            });
            return canvasGradient;
          }
        }
      }
    }
    return source;
  };
  _uploadedStyles = [
    "color",
    "backgroundColor",
    "textStrokeColor"
  ];
  uploadColor = (style, box) => {
    this._uploadedStyles.forEach((key) => {
      style[key] = this._parseColor(style[key], box);
    });
  };
  drawPath = (path, options = {}) => {
    const { clipRect } = options;
    const ctx = this.context;
    ctx.save();
    ctx.beginPath();
    if (clipRect) {
      ctx.rect(clipRect.left, clipRect.top, clipRect.width, clipRect.height);
      ctx.clip();
      ctx.beginPath();
    }
    path.drawTo(ctx, this._mergePathStyle(path, options));
    ctx.restore();
  };
  _mergePathStyle(path, style) {
    const {
      fontSize = this.text.computedStyle.fontSize
    } = style;
    const pathStyle = path.style;
    const stroke = style.stroke ?? pathStyle.stroke;
    const strokeWidth = style.strokeWidth ? style.strokeWidth * fontSize : pathStyle.strokeWidth;
    return {
      ...pathStyle,
      ...style,
      fill: style.fill ?? pathStyle.fill,
      stroke: strokeWidth === void 0 || strokeWidth > 0 ? stroke : void 0,
      strokeLinecap: style.strokeLinecap ?? pathStyle.strokeLinecap ?? "round",
      strokeLinejoin: style.strokeLinejoin ?? pathStyle.strokeLinejoin ?? "round",
      strokeWidth,
      shadowOffsetX: (style.shadowOffsetX ?? 0) * fontSize,
      shadowOffsetY: (style.shadowOffsetY ?? 0) * fontSize,
      shadowBlur: (style.shadowBlur ?? 0) * fontSize,
      shadowColor: style.shadowColor
    };
  }
  drawCharacter = (character, userStyle = {}) => {
    const ctx = this.context;
    const {
      computedStyle,
      path,
      glyphBox,
      isVertical,
      content,
      inlineBox,
      baseline,
      computedFill,
      computedOutline
    } = character;
    const style = {
      ...computedStyle,
      ...userStyle
    };
    const pathStyle = {
      strokeLinecap: computedOutline?.lineCap,
      strokeLinejoin: computedOutline?.lineJoin,
      ...style,
      fill: userStyle.color ?? computedFill?.color ?? computedStyle.color,
      strokeWidth: userStyle.textStrokeWidth ?? computedOutline?.width ?? computedStyle.textStrokeWidth,
      stroke: userStyle.textStrokeColor ?? computedOutline?.color ?? computedStyle.textStrokeColor
    };
    if (glyphBox) {
      this.drawPath(path, pathStyle);
    } else {
      ctx.save();
      ctx.beginPath();
      modernPath2d.setCanvasContext(ctx, this._mergePathStyle(path, pathStyle));
      ctx.font = `${style.fontSize}px ${style.fontFamily}`;
      if (isVertical) {
        ctx.textBaseline = "middle";
        ctx.fillText(content, inlineBox.left, inlineBox.top + inlineBox.height / 2);
      } else {
        ctx.textBaseline = "alphabetic";
        ctx.fillText(content, inlineBox.left, inlineBox.top + baseline);
      }
      ctx.restore();
    }
  };
}

const set1 = /* @__PURE__ */ new Set(["\xA9", "\xAE", "\xF7"]);
const set2 = /* @__PURE__ */ new Set([
  "\u2014",
  "\u2026",
  "\u201C",
  "\u201D",
  "\uFE4F",
  "\uFE4B",
  "\uFE4C",
  "\u2018",
  "\u2019",
  "\u02DC"
]);
const fsSelectionMap = {
  1: "italic",
  32: "bold"
};
const macStyleMap = {
  1: "italic",
  2: "bold"
};
const fontWeightMap = {
  100: -0.2,
  200: -0.1,
  300: 0,
  400: 0,
  normal: 0,
  500: 0.1,
  600: 0.2,
  700: 0.3,
  bold: 0.3,
  800: 0.4,
  900: 0.5
};
class Character {
  constructor(content, index, parent) {
    this.content = content;
    this.index = index;
    this.parent = parent;
  }
  path = new modernPath2d.Path2D().setMeta(this);
  lineBox = new modernPath2d.BoundingBox();
  inlineBox = new modernPath2d.BoundingBox();
  glyphBox;
  advanceWidth = 0;
  advanceHeight = 0;
  underlinePosition = 0;
  underlineThickness = 0;
  strikeoutPosition = 0;
  strikeoutSize = 0;
  ascender = 0;
  descender = 0;
  typoAscender = 0;
  typoDescender = 0;
  typoLineGap = 0;
  winAscent = 0;
  winDescent = 0;
  xHeight = 0;
  capHeight = 0;
  baseline = 0;
  centerDiviation = 0;
  fontStyle;
  get compatibleGlyphBox() {
    const size = this.computedStyle.fontSize * 0.8;
    return this.glyphBox ?? (this.isVertical ? new modernPath2d.BoundingBox(
      this.lineBox.left + this.lineBox.width / 2 - size / 2,
      this.lineBox.top,
      size,
      this.lineBox.height
    ) : new modernPath2d.BoundingBox(
      this.lineBox.left,
      this.lineBox.top + this.lineBox.height / 2 - size / 2,
      this.lineBox.width,
      size
    ));
  }
  get center() {
    return this.compatibleGlyphBox.center;
  }
  get computedFill() {
    return this.parent.computedFill;
  }
  get computedOutline() {
    return this.parent.computedOutline;
  }
  get computedStyle() {
    return this.parent.computedStyle;
  }
  get isVertical() {
    return this.computedStyle.writingMode.includes("vertical");
  }
  get fontSize() {
    return this.computedStyle.fontSize;
  }
  get fontHeight() {
    return this.fontSize * this.computedStyle.lineHeight;
  }
  _getFontSFNT(fonts) {
    const fontFamily = this.computedStyle.fontFamily;
    const _fonts = fonts ?? modernFont.fonts;
    const font = fontFamily ? _fonts.get(fontFamily) : _fonts.fallbackFont;
    let sfnt = font?.getSFNT();
    if (sfnt?.textToGlyphIndexes(this.content).includes(0)) {
      sfnt = _fonts.fallbackFont?.getSFNT();
    }
    return sfnt;
  }
  updateGlyph(sfnt = this._getFontSFNT()) {
    if (!sfnt) {
      return this;
    }
    const { hhea, os2, post, head } = sfnt;
    const unitsPerEm = head.unitsPerEm;
    const ascender = hhea.ascent;
    const descender = hhea.descent;
    const { content, computedStyle } = this;
    const { fontSize } = computedStyle;
    const rate = unitsPerEm / fontSize;
    const advanceWidth = sfnt.getAdvanceWidth(content, fontSize);
    const advanceHeight = (ascender + Math.abs(descender)) / rate;
    const baseline = ascender / rate;
    this.advanceWidth = advanceWidth;
    this.advanceHeight = advanceHeight;
    this.underlinePosition = (ascender - post.underlinePosition) / rate;
    this.underlineThickness = post.underlineThickness / rate;
    this.strikeoutPosition = (ascender - os2.yStrikeoutPosition) / rate;
    this.strikeoutSize = os2.yStrikeoutSize / rate;
    this.ascender = ascender / rate;
    this.descender = descender / rate;
    this.typoAscender = os2.sTypoAscender / rate;
    this.typoDescender = os2.sTypoDescender / rate;
    this.typoLineGap = os2.sTypoLineGap / rate;
    this.winAscent = os2.usWinAscent / rate;
    this.winDescent = os2.usWinDescent / rate;
    this.xHeight = os2.sxHeight / rate;
    this.capHeight = os2.sCapHeight / rate;
    this.baseline = baseline;
    this.centerDiviation = advanceHeight / 2 - baseline;
    this.fontStyle = fsSelectionMap[os2.fsSelection] ?? macStyleMap[head.macStyle];
    return this;
  }
  update(fonts) {
    const sfnt = this._getFontSFNT(fonts);
    if (!sfnt) {
      return this;
    }
    this.updateGlyph(sfnt);
    const {
      isVertical,
      content,
      computedStyle: style,
      baseline,
      inlineBox,
      ascender,
      descender,
      typoAscender,
      fontStyle,
      advanceWidth,
      advanceHeight
    } = this;
    const { left, top } = inlineBox;
    const needsItalic = style.fontStyle === "italic" && fontStyle !== "italic";
    let x = left;
    let y = top + baseline;
    let glyphIndex;
    const path = new modernPath2d.Path2D().setMeta(this);
    if (isVertical) {
      x += (advanceHeight - advanceWidth) / 2;
      if (Math.abs(advanceWidth - advanceHeight) > 0.1) {
        y -= (ascender - typoAscender) / (ascender + Math.abs(descender)) * advanceHeight;
      }
      glyphIndex = void 0;
    }
    if (isVertical && !set1.has(content) && (content.codePointAt(0) <= 256 || set2.has(content))) {
      path.addCommands(
        sfnt.getPathCommands(
          content,
          x,
          top + baseline - (advanceHeight - advanceWidth) / 2,
          style.fontSize
        )
      );
      const point = {
        y: top - (advanceHeight - advanceWidth) / 2 + advanceHeight / 2,
        x: x + advanceWidth / 2
      };
      if (needsItalic) {
        this._italic(
          path,
          isVertical ? {
            x: point.x,
            y: top - (advanceHeight - advanceWidth) / 2 + baseline
          } : void 0
        );
      }
      path.rotate(90, point);
    } else {
      if (glyphIndex !== void 0) {
        path.addCommands(
          sfnt.glyphs.get(glyphIndex).getPathCommands(x, y, style.fontSize)
        );
        if (needsItalic) {
          this._italic(
            path,
            isVertical ? {
              x: x + advanceWidth / 2,
              y: top + typoAscender / (ascender + Math.abs(descender)) * advanceHeight
            } : void 0
          );
        }
      } else {
        path.addCommands(sfnt.getPathCommands(content, x, y, style.fontSize));
        if (needsItalic) {
          this._italic(
            path,
            isVertical ? { x: x + advanceHeight / 2, y } : void 0
          );
        }
      }
    }
    const fontWeight = style.fontWeight ?? 400;
    if (fontWeight in fontWeightMap && ((fontWeight === 700 || fontWeight === "bold") && fontStyle !== "bold")) {
      path.bold(fontWeightMap[fontWeight] * style.fontSize * 0.05);
    }
    path.style = {
      fill: this.computedFill ?? style.color,
      fillRule: "nonzero",
      stroke: this.computedOutline ?? (style.textStrokeWidth ? style.textStrokeColor : "none"),
      strokeWidth: style.textStrokeWidth ? style.textStrokeWidth * style.fontSize * 0.03 : 0
    };
    this.path = path;
    this.glyphBox = this.getGlyphBoundingBox();
    return this;
  }
  _italic(path, startPoint) {
    path.skew(-0.24, 0, startPoint || {
      y: this.inlineBox.top + this.baseline,
      x: this.inlineBox.left + this.inlineBox.width / 2
    });
  }
  getGlyphMinMax(min, max, withStyle) {
    if (this.path.curves[0]?.curves.length) {
      return this.path.getMinMax(min, max, withStyle);
    } else {
      return void 0;
    }
  }
  getGlyphBoundingBox(withStyle) {
    const minMax = this.getGlyphMinMax(void 0, void 0, withStyle);
    if (!minMax) {
      return void 0;
    }
    const { min, max } = minMax;
    return new modernPath2d.BoundingBox(min.x, min.y, max.x - min.x, max.y - min.y);
  }
}

function createSvgLoader() {
  const loaded = /* @__PURE__ */ new Map();
  async function load(svg) {
    if (!loaded.has(svg)) {
      loaded.set(svg, "");
      try {
        loaded.set(svg, await fetch(svg).then((rep) => rep.text()));
      } catch (err) {
        console.warn(err);
        loaded.delete(svg);
      }
    }
  }
  function needsLoad(source) {
    return source.startsWith("/") || source.startsWith("./") || source.startsWith("http://") || source.startsWith("https://") || source.startsWith("blob://");
  }
  return {
    loaded,
    needsLoad,
    load
  };
}

function createSvgParser(loader) {
  const parsed = /* @__PURE__ */ new Map();
  function parse(svg) {
    let result = parsed.get(svg);
    if (!result) {
      const svgString = loader.needsLoad(svg) ? loader.loaded.get(svg) : svg;
      if (svgString) {
        const dom = modernPath2d.svgToDom(svgString);
        const pathSet = modernPath2d.svgToPath2DSet(dom);
        result = { dom, pathSet };
        parsed.set(svg, result);
      } else {
        const dom = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        dom.setAttribute("width", "0");
        dom.setAttribute("height", "0");
        dom.setAttribute("viewBox", "0 0 0 0");
        result = { dom, pathSet: new modernPath2d.Path2DSet() };
      }
    }
    return result;
  }
  return {
    parsed,
    parse
  };
}

function parseValueNumber(value, ctx) {
  if (typeof value === "number") {
    return value;
  } else {
    if (value.endsWith("%")) {
      value = value.substring(0, value.length - 1);
      return Math.ceil(Number(value) / 100 * ctx.total);
    } else if (value.endsWith("rem")) {
      value = value.substring(0, value.length - 3);
      return Number(value) * ctx.fontSize;
    } else if (value.endsWith("em")) {
      value = value.substring(0, value.length - 2);
      return Number(value) * ctx.fontSize;
    } else {
      return Number(value);
    }
  }
}
function parseColormap(colormap) {
  return modernIdoc.isNone(colormap) ? {} : colormap;
}
function isEqualObject(obj1, obj2) {
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  const keys = Array.from(/* @__PURE__ */ new Set([...keys1, ...keys2]));
  return keys.every((key) => isEqualValue(obj1[key], obj2[key]));
}
function isEqualValue(val1, val2) {
  const typeof1 = typeof val1;
  const typeof2 = typeof val2;
  if (typeof1 === typeof2) {
    if (typeof1 === "object") {
      return isEqualObject(val1, val2);
    }
    return val1 === val2;
  }
  return false;
}
function hexToRgb(hex) {
  const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
  const isValidHex = /^(?:[0-9A-F]{3}|[0-9A-F]{6})$/i.test(cleanHex);
  if (!isValidHex)
    return null;
  const fullHex = cleanHex.length === 3 ? cleanHex.split("").map((char) => char + char).join("") : cleanHex;
  const r = Number.parseInt(fullHex.slice(0, 2), 16);
  const g = Number.parseInt(fullHex.slice(2, 4), 16);
  const b = Number.parseInt(fullHex.slice(4, 6), 16);
  return `rgb(${r}, ${g}, ${b})`;
}
function filterEmpty(val) {
  if (!val)
    return val;
  const res = {};
  for (const key in val) {
    if (val[key] !== "" && val[key] !== void 0) {
      res[key] = val[key];
    }
  }
  return res;
}

class Fragment {
  constructor(content, style = {}, index, parent) {
    this.content = content;
    this.style = style;
    this.index = index;
    this.parent = parent;
    this.updateComputedStyle().initCharacters();
  }
  inlineBox = new modernPath2d.BoundingBox();
  fill;
  outline;
  get computedFill() {
    return this.fill ?? this.parent.computedFill;
  }
  get computedOutline() {
    return this.outline ?? this.parent.computedOutline;
  }
  get computedContent() {
    const style = this.computedStyle;
    return style.textTransform === "uppercase" ? this.content.toUpperCase() : style.textTransform === "lowercase" ? this.content.toLowerCase() : this.content;
  }
  updateComputedStyle() {
    this.computedStyle = {
      ...this.parent.computedStyle,
      ...filterEmpty(this.style)
    };
    return this;
  }
  initCharacters() {
    const characters = [];
    let index = 0;
    for (const c of this.computedContent) {
      characters.push(new Character(c, index++, this));
    }
    this.characters = characters;
    return this;
  }
}

class Paragraph {
  constructor(style, index, parent) {
    this.style = style;
    this.index = index;
    this.parent = parent;
    this.updateComputedStyle();
  }
  lineBox = new modernPath2d.BoundingBox();
  fragments = [];
  fill;
  outline;
  get computedFill() {
    return this.fill ?? this.parent.fill;
  }
  get computedOutline() {
    return this.outline ?? this.parent.outline;
  }
  updateComputedStyle() {
    this.computedStyle = {
      ...filterEmpty(this.parent.computedStyle),
      ...filterEmpty(this.style)
    };
    return this;
  }
}

function definePlugin(options) {
  return options;
}

class Measurer {
  static notZeroStyles = /* @__PURE__ */ new Set([
    "width",
    "height"
  ]);
  static pxStyles = /* @__PURE__ */ new Set([
    "width",
    "height",
    "fontSize",
    "letterSpacing",
    "textStrokeWidth",
    "textIndent",
    "shadowOffsetX",
    "shadowOffsetY",
    "shadowBlur",
    "margin",
    "marginLeft",
    "marginTop",
    "marginRight",
    "marginBottom",
    "padding",
    "paddingLeft",
    "paddingTop",
    "paddingRight",
    "paddingBottom"
  ]);
  _toDomStyle(style) {
    const _style = {};
    for (const key in style) {
      const value = style[key];
      if (Measurer.notZeroStyles.has(key) && value === 0) ; else if (typeof value === "number" && Measurer.pxStyles.has(key)) {
        _style[key] = `${value}px`;
      } else {
        _style[key] = value;
      }
    }
    return _style;
  }
  /**
   * <section style="...">
   *   <ul>
   *     <li style="...">
   *       <span style="...">...</span>
   *       <span>...</span>
   *     </li>
   *   </ul>
   * </section>
   */
  createDom(paragraphs, rootStyle) {
    const dom = document.createElement("section");
    const style = { ...rootStyle };
    const isHorizontal = rootStyle.writingMode.includes("horizontal");
    switch (rootStyle.textAlign) {
      case "start":
      case "left":
        style.justifyContent = "flex-start";
        break;
      case "center":
        style.justifyContent = "center";
        break;
      case "end":
      case "right":
        style.justifyContent = "flex-end";
        break;
    }
    switch (rootStyle.verticalAlign) {
      case "top":
        style.alignItems = "flex-start";
        break;
      case "middle":
        style.alignItems = "center";
        break;
      case "bottom":
        style.alignItems = "flex-end";
        break;
    }
    const isFlex = Boolean(style.justifyContent || style.alignItems);
    Object.assign(dom.style, {
      ...this._toDomStyle({
        ...style,
        boxSizing: style.boxSizing ?? "border-box",
        display: style.display ?? (isFlex ? "inline-flex" : void 0),
        width: style.width ?? "max-content",
        height: style.height ?? "max-content"
      }),
      whiteSpace: "pre-wrap",
      wordBreak: "break-all"
    });
    const ul = document.createElement("ul");
    Object.assign(ul.style, {
      verticalAlign: "inherit",
      listStyleType: "inherit",
      padding: "0",
      margin: "0",
      width: isFlex && isHorizontal ? "100%" : void 0,
      height: isFlex && !isHorizontal ? "100%" : void 0
    });
    paragraphs.forEach((paragraph) => {
      const li = document.createElement("li");
      Object.assign(li.style, {
        verticalAlign: "inherit",
        ...this._toDomStyle(paragraph.style)
      });
      paragraph.fragments.forEach((fragment) => {
        const span = document.createElement("span");
        Object.assign(span.style, {
          verticalAlign: "inherit",
          ...this._toDomStyle(fragment.style)
        });
        span.appendChild(document.createTextNode(fragment.content));
        li.appendChild(span);
      });
      ul.appendChild(li);
    });
    dom.appendChild(ul);
    return dom;
  }
  measureDomText(text) {
    const range = document.createRange();
    range.selectNodeContents(text);
    const data = text.data ?? "";
    let offset = 0;
    return Array.from(data).map((char) => {
      const start = offset += data.substring(offset).indexOf(char);
      const end = start + char.length;
      offset += char.length;
      range.setStart(text, Math.max(start, 0));
      range.setEnd(text, end);
      const rects = range.getClientRects?.() ?? [range.getBoundingClientRect()];
      let rect = rects[rects.length - 1];
      if (rects.length > 1 && rect.width < 2) {
        rect = rects[rects.length - 2];
      }
      const content = range.toString();
      if (content !== "" && rect && rect.width + rect.height !== 0) {
        return {
          content,
          top: rect.top,
          left: rect.left,
          height: rect.height,
          width: rect.width
        };
      }
      return void 0;
    }).filter(Boolean);
  }
  measureDom(dom) {
    const paragraphs = [];
    const fragments = [];
    const characters = [];
    dom.querySelectorAll("li").forEach((pDom, paragraphIndex) => {
      const pBox = pDom.getBoundingClientRect();
      paragraphs.push({
        paragraphIndex,
        left: pBox.x,
        top: pBox.y,
        width: pBox.width,
        height: pBox.height
      });
      pDom.querySelectorAll(":scope > *").forEach((fDom, fragmentIndex) => {
        const fBox = fDom.getBoundingClientRect();
        fragments.push({
          paragraphIndex,
          fragmentIndex,
          left: fBox.x,
          top: fBox.y,
          width: fBox.width,
          height: fBox.height
        });
        let characterIndex = 0;
        if (!fDom.children.length && fDom.firstChild instanceof window.Text) {
          this.measureDomText(fDom.firstChild).forEach((char) => {
            characters.push({
              ...char,
              newParagraphIndex: -1,
              paragraphIndex,
              fragmentIndex,
              characterIndex: characterIndex++,
              textWidth: -1,
              textHeight: -1
            });
          });
        } else {
          fDom.querySelectorAll(":scope > *").forEach((cDOM) => {
            if (cDOM.firstChild instanceof window.Text) {
              this.measureDomText(cDOM.firstChild).forEach((char) => {
                characters.push({
                  ...char,
                  newParagraphIndex: -1,
                  paragraphIndex,
                  fragmentIndex,
                  characterIndex: characterIndex++,
                  textWidth: -1,
                  textHeight: -1
                });
              });
            }
          });
        }
      });
    });
    return {
      paragraphs,
      fragments,
      characters
    };
  }
  measureParagraphDom(paragraphs, dom) {
    const rect = dom.getBoundingClientRect();
    const measured = this.measureDom(dom);
    measured.paragraphs.forEach((p) => {
      const box = paragraphs[p.paragraphIndex].lineBox;
      box.left = p.left - rect.left;
      box.top = p.top - rect.top;
      box.width = p.width;
      box.height = p.height;
    });
    measured.fragments.forEach((f) => {
      const box = paragraphs[f.paragraphIndex].fragments[f.fragmentIndex].inlineBox;
      box.left = f.left - rect.left;
      box.top = f.top - rect.top;
      box.width = f.width;
      box.height = f.height;
    });
    const results = [];
    let i = 0;
    measured.characters.forEach((character) => {
      const { paragraphIndex, fragmentIndex, characterIndex } = character;
      results.push({
        ...character,
        newParagraphIndex: paragraphIndex,
        left: character.left - rect.left,
        top: character.top - rect.top
      });
      const item = paragraphs[paragraphIndex].fragments[fragmentIndex].characters[characterIndex];
      const { fontHeight, isVertical, inlineBox, lineBox } = item;
      const result = results[i];
      inlineBox.left = result.left;
      inlineBox.top = result.top;
      inlineBox.width = result.width;
      inlineBox.height = result.height;
      if (isVertical) {
        lineBox.left = result.left + (result.width - fontHeight) / 2;
        lineBox.top = result.top;
        lineBox.width = fontHeight;
        lineBox.height = result.height;
      } else {
        lineBox.left = result.left;
        lineBox.top = result.top + (result.height - fontHeight) / 2;
        lineBox.width = result.width;
        lineBox.height = fontHeight;
      }
      i++;
    });
    return {
      paragraphs,
      boundingBox: new modernPath2d.BoundingBox(0, 0, rect.width, rect.height)
    };
  }
  measure(paragraphs, rootStyle, dom) {
    let destory;
    if (!dom) {
      dom = this.createDom(paragraphs, rootStyle);
      Object.assign(dom.style, {
        position: "fixed",
        visibility: "hidden"
      });
      document.body.appendChild(dom);
      destory = () => dom?.parentNode?.removeChild(dom);
    }
    const result = this.measureParagraphDom(paragraphs, dom);
    destory?.();
    return result;
  }
}

function backgroundPlugin() {
  const pathSet = new modernPath2d.Path2DSet();
  const loader = createSvgLoader();
  const parser = createSvgParser(loader);
  return {
    name: "background",
    pathSet,
    context: {
      loader,
      parser
    },
    load: async (text) => {
      const { backgroundImage } = text.computedStyle;
      if (backgroundImage && loader.needsLoad(backgroundImage)) {
        await loader.load(backgroundImage);
      }
    },
    update: (text) => {
      pathSet.paths.length = 0;
      const { computedStyle, lineBox, isVertical } = text;
      const {
        backgroundImage,
        backgroundSize,
        backgroundColormap
      } = computedStyle;
      if (modernIdoc.isNone(backgroundImage))
        return;
      const { pathSet: imagePathSet } = parser.parse(backgroundImage);
      const imageBox = imagePathSet.getBoundingBox(true) ?? new modernPath2d.BoundingBox();
      let x, y, width, height;
      if (isVertical) {
        ({ x: y, y: x, width: height, height: width } = lineBox);
      } else {
        ({ x, y, width, height } = lineBox);
      }
      const colormap = parseColormap(backgroundColormap ?? "none");
      const paths = imagePathSet.paths.map((p) => {
        const cloned = p.clone();
        if (cloned.style.fill && cloned.style.fill in colormap) {
          cloned.style.fill = colormap[cloned.style.fill];
        }
        if (cloned.style.stroke && cloned.style.stroke in colormap) {
          cloned.style.stroke = colormap[cloned.style.stroke];
        }
        return cloned;
      });
      let scaleX;
      let scaleY;
      if (backgroundSize === "rigid") {
        scaleX = Math.max(text.fontSize * 5 / imageBox.width);
        scaleY = scaleX;
        const dist = new modernPath2d.Vector2();
        dist.x = imageBox.width - width / scaleX;
        dist.y = imageBox.height - height / scaleY;
        paths.forEach((path) => {
          path.applyTransform((p) => {
            const hasX = p.x > imageBox.left + imageBox.width / 2;
            const hasY = p.y > imageBox.top + imageBox.height / 2;
            if (hasX) {
              p.x -= dist.x;
            }
            if (hasY) {
              p.y -= dist.y;
            }
          });
        });
      } else {
        scaleX = width / imageBox.width;
        scaleY = height / imageBox.height;
      }
      const transform = new modernPath2d.Matrix3();
      transform.translate(-imageBox.x, -imageBox.y);
      transform.scale(scaleX, scaleY);
      if (isVertical) {
        transform.translate(-width / 2, -height / 2);
        transform.rotate(-Math.PI / 2);
        transform.translate(height / 2, width / 2);
      }
      transform.translate(x, y);
      paths.forEach((path) => {
        path.applyTransform((p) => {
          p.applyMatrix3(transform);
        });
      });
      pathSet.paths.push(...paths);
    },
    renderOrder: -2,
    render: (renderer) => {
      const { text, context } = renderer;
      const { boundingBox, computedStyle: style } = text;
      if (!modernIdoc.isNone(style.backgroundColor)) {
        context.fillStyle = style.backgroundColor;
        context.fillRect(...boundingBox.array);
      }
      pathSet.paths.forEach((path) => {
        renderer.drawPath(path);
        if (text.debug) {
          const box = new modernPath2d.Path2DSet([path]).getBoundingBox();
          if (box) {
            context.strokeRect(box.x, box.y, box.width, box.height);
          }
        }
      });
      text.paragraphs.forEach((p) => {
        const { lineBox, style: style2 } = p;
        if (!modernIdoc.isNone(style2.backgroundColor)) {
          context.fillStyle = style2.backgroundColor;
          context.fillRect(...lineBox.array);
        }
        p.fragments.forEach((f) => {
          const { inlineBox, style: style3 } = f;
          if (!modernIdoc.isNone(style3.backgroundColor)) {
            context.fillStyle = style3.backgroundColor;
            context.fillRect(...inlineBox.array);
          }
        });
      });
    }
  };
}

function getHighlightStyle(style) {
  const {
    highlight,
    highlightImage,
    highlightReferImage,
    highlightColormap,
    highlightLine,
    highlightSize,
    highlightThickness
  } = style;
  return {
    image: highlight?.image ?? highlightImage ?? "none",
    referImage: highlight?.referImage ?? highlightReferImage ?? "none",
    colormap: highlight?.colormap ?? highlightColormap ?? "none",
    line: highlight?.line ?? highlightLine ?? "none",
    size: highlight?.size ?? highlightSize ?? "cover",
    thickness: highlight?.thickness ?? highlightThickness ?? "100%"
  };
}
function highlightPlugin() {
  const pathSet = new modernPath2d.Path2DSet();
  const clipRects = [];
  const loader = createSvgLoader();
  const parser = createSvgParser(loader);
  return definePlugin({
    name: "highlight",
    pathSet,
    context: {
      clipRects,
      loader,
      parser
    },
    load: async (text) => {
      const set = /* @__PURE__ */ new Set();
      text.forEachCharacter((character) => {
        const { computedStyle: style } = character;
        const { image, referImage } = getHighlightStyle(style);
        if (image && loader.needsLoad(image)) {
          set.add(image);
        }
        if (referImage && loader.needsLoad(referImage)) {
          set.add(referImage);
        }
      });
      await Promise.all(Array.from(set).map((src) => loader.load(src)));
    },
    update: (text) => {
      clipRects.length = 0;
      pathSet.paths.length = 0;
      let groups = [];
      let group;
      let prevHighlight;
      text.forEachCharacter((character) => {
        const {
          computedStyle: style
        } = character;
        const highlight = getHighlightStyle(style);
        const {
          image,
          colormap,
          line,
          size,
          thickness
        } = highlight;
        if (!modernIdoc.isNone(image)) {
          const { inlineBox, isVertical } = character;
          const { fontSize } = style;
          if ((!prevHighlight || isEqualValue(prevHighlight.image, image) && isEqualValue(prevHighlight.colormap, colormap) && isEqualValue(prevHighlight.line, line) && isEqualValue(prevHighlight.size, size) && isEqualValue(prevHighlight.thickness, thickness)) && group?.length && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top) && group[0].fontSize === fontSize) {
            group.push(character);
          } else {
            group = [];
            group.push(character);
            groups.push(group);
          }
        } else {
          if (group?.length) {
            group = [];
            groups.push(group);
          }
        }
        prevHighlight = highlight;
      });
      groups = groups.filter((characters) => characters.length);
      for (let i = 0; i < groups.length; i++) {
        const characters = groups[i];
        const char = characters[0];
        const groupBox = modernPath2d.BoundingBox.from(...characters.map((c) => c.compatibleGlyphBox));
        if (!groupBox.height || !groupBox.width) {
          continue;
        }
        const {
          computedStyle: style,
          isVertical,
          inlineBox,
          compatibleGlyphBox: glyphBox,
          strikeoutPosition,
          underlinePosition
        } = char;
        const { fontSize } = style;
        const {
          image,
          referImage,
          colormap,
          line,
          size,
          thickness
        } = getHighlightStyle(style);
        const _thickness = parseValueNumber(thickness, { fontSize, total: groupBox.width }) / groupBox.width;
        const _colormap = parseColormap(colormap);
        const { pathSet: imagePathSet, dom: imageDom } = parser.parse(image);
        const imageBox = imagePathSet.getBoundingBox(true) ?? new modernPath2d.BoundingBox();
        const styleScale = fontSize / imageBox.width * 2;
        const targetBox = new modernPath2d.BoundingBox().copy(groupBox);
        if (isVertical) {
          targetBox.width = groupBox.height;
          targetBox.height = groupBox.width;
          targetBox.left = groupBox.left + groupBox.width;
        }
        const rawWidth = Math.floor(targetBox.width);
        let userWidth = rawWidth;
        if (size !== "cover") {
          userWidth = parseValueNumber(size, { fontSize, total: groupBox.width }) || rawWidth;
          targetBox.width = userWidth;
        }
        const hasReferImage = !modernIdoc.isNone(referImage) && modernIdoc.isNone(line);
        if (hasReferImage) {
          imageBox.copy(
            parser.parse(referImage).pathSet.getBoundingBox(true)
          );
        } else {
          let _line;
          if (modernIdoc.isNone(line)) {
            if (imageBox.width / imageBox.height > 4) {
              _line = "underline";
              const viewBox = imageDom.getAttribute("viewBox");
              if (viewBox) {
                const [_x, y, _w, h] = viewBox.split(" ").map((v) => Number(v));
                const viewCenter = y + h / 2;
                if (imageBox.y < viewCenter && imageBox.y + imageBox.height > viewCenter) {
                  _line = "line-through";
                } else if (imageBox.y + imageBox.height < viewCenter) {
                  _line = "overline";
                } else {
                  _line = "underline";
                }
              }
            } else {
              _line = "outline";
            }
          } else {
            _line = line;
          }
          switch (_line) {
            case "outline": {
              const paddingX = targetBox.width * 0.2;
              const paddingY = targetBox.height * 0.2;
              if (isVertical) {
                targetBox.x -= paddingY / 2;
                targetBox.y -= paddingX / 2;
                targetBox.x -= targetBox.height;
              } else {
                targetBox.x -= paddingX / 2;
                targetBox.y -= paddingY / 2;
              }
              targetBox.width += paddingX;
              targetBox.height += paddingY;
              break;
            }
            case "overline":
              targetBox.height = imageBox.height * styleScale;
              if (isVertical) {
                targetBox.x = inlineBox.left + inlineBox.width;
              } else {
                targetBox.y = inlineBox.top;
              }
              break;
            case "line-through":
              targetBox.height = imageBox.height * styleScale;
              if (isVertical) {
                targetBox.x = inlineBox.left + inlineBox.width - strikeoutPosition + targetBox.height / 2;
              } else {
                targetBox.y = inlineBox.top + strikeoutPosition - targetBox.height / 2;
              }
              break;
            case "underline":
              targetBox.height = imageBox.height * styleScale;
              if (isVertical) {
                targetBox.x = glyphBox.left + glyphBox.width - underlinePosition;
              } else {
                targetBox.y = inlineBox.top + underlinePosition;
              }
              break;
          }
        }
        const transform = new modernPath2d.Matrix3();
        transform.translate(-imageBox.x, -imageBox.y);
        transform.scale(targetBox.width / imageBox.width, targetBox.height / imageBox.height);
        if (isVertical) {
          const tx = targetBox.width / 2;
          const ty = targetBox.height / 2;
          if (!hasReferImage) {
            transform.translate(-tx, -ty);
          }
          transform.rotate(-Math.PI / 2);
          if (!hasReferImage) {
            transform.translate(ty, tx);
          }
        }
        transform.translate(targetBox.x, targetBox.y);
        for (let i2 = 0; i2 < Math.ceil(rawWidth / userWidth); i2++) {
          const _transform = transform.clone();
          if (isVertical) {
            _transform.translate(0, i2 * targetBox.width);
          } else {
            _transform.translate(i2 * targetBox.width, 0);
          }
          imagePathSet.paths.forEach((originalPath) => {
            const path = originalPath.clone().applyTransform(_transform);
            if (path.style.strokeWidth)
              path.style.strokeWidth *= styleScale * _thickness;
            if (path.style.strokeMiterlimit)
              path.style.strokeMiterlimit *= styleScale;
            if (path.style.strokeDashoffset)
              path.style.strokeDashoffset *= styleScale;
            if (path.style.strokeDasharray)
              path.style.strokeDasharray = path.style.strokeDasharray.map((v) => v * styleScale);
            if (path.style.fill && path.style.fill in _colormap) {
              path.style.fill = _colormap[path.style.fill];
            }
            if (path.style.stroke && path.style.stroke in _colormap) {
              path.style.stroke = _colormap[path.style.stroke];
            }
            pathSet.paths.push(path);
            if (rawWidth !== userWidth) {
              if (isVertical) {
                clipRects[pathSet.paths.length - 1] = new modernPath2d.BoundingBox(
                  groupBox.left - groupBox.width * 2,
                  groupBox.top,
                  groupBox.width * 4,
                  groupBox.height
                );
              } else {
                clipRects[pathSet.paths.length - 1] = new modernPath2d.BoundingBox(
                  groupBox.left,
                  groupBox.top - groupBox.height * 2,
                  groupBox.width,
                  groupBox.height * 4
                );
              }
            }
          });
        }
      }
    },
    renderOrder: -1,
    getBoundingBox: () => {
      const boundingBoxs = [];
      pathSet.paths.forEach((path, index) => {
        const clipRect = clipRects[index];
        let box = path.getBoundingBox();
        if (clipRect) {
          const x = Math.max(box.x, clipRect.x);
          const y = Math.max(box.y, clipRect.y);
          const right = Math.min(box.right, clipRect.right);
          const bottom = Math.min(box.bottom, clipRect.bottom);
          box = new modernPath2d.BoundingBox(x, y, right - x, bottom - y);
        }
        boundingBoxs.push(box);
      });
      return modernPath2d.BoundingBox.from(...boundingBoxs);
    },
    render: (renderer) => {
      const { text, context } = renderer;
      pathSet.paths.forEach((path, index) => {
        renderer.drawPath(path, { clipRect: clipRects[index] });
        if (text.debug) {
          const box = new modernPath2d.Path2DSet([path]).getBoundingBox();
          if (box) {
            context.strokeRect(box.x, box.y, box.width, box.height);
          }
        }
      });
    }
  });
}

function genDisc(r, color) {
  return `<svg width="${r * 2}" height="${r * 2}" xmlns="http://www.w3.org/2000/svg">
  <circle cx="${r}" cy="${r}" r="${r}" fill="${color}" />
</svg>`;
}
function listStylePlugin() {
  const pathSet = new modernPath2d.Path2DSet();
  return definePlugin({
    name: "listStyle",
    pathSet,
    update: (text) => {
      pathSet.paths.length = 0;
      const { paragraphs, isVertical, fontSize } = text;
      const padding = fontSize * 0.45;
      paragraphs.forEach((paragraph) => {
        const {
          computedStyle: style
        } = paragraph;
        const {
          color,
          listStyleImage,
          listStyleColormap,
          listStyleSize,
          listStyleType
        } = style;
        const colormap = parseColormap(listStyleColormap);
        let size = listStyleSize;
        let image;
        if (!modernIdoc.isNone(listStyleImage)) {
          image = listStyleImage;
        } else if (!modernIdoc.isNone(listStyleType)) {
          const r = fontSize * 0.38 / 2;
          size = size === "cover" ? r * 2 : size;
          switch (listStyleType) {
            case "disc":
              image = genDisc(r, String(color));
              break;
          }
        }
        if (!image) {
          return;
        }
        const imagePathSet = modernPath2d.svgToPath2DSet(image);
        const imageBox = imagePathSet.getBoundingBox();
        const char = paragraph.fragments[0]?.characters[0];
        if (!char) {
          return;
        }
        const { inlineBox } = char;
        const scale = size === "cover" ? 1 : parseValueNumber(size, { total: fontSize, fontSize }) / fontSize;
        const m = new modernPath2d.Matrix3();
        if (isVertical) {
          const _scale = fontSize / imageBox.height * scale;
          m.translate(-imageBox.left, -imageBox.top).rotate(Math.PI / 2).scale(_scale, _scale).translate(
            inlineBox.left + (inlineBox.width - imageBox.height * _scale) / 2,
            inlineBox.top - padding
          );
        } else {
          const _scale = fontSize / imageBox.height * scale;
          m.translate(-imageBox.left, -imageBox.top).scale(_scale, _scale).translate(
            inlineBox.left - imageBox.width * _scale - padding,
            inlineBox.top + (inlineBox.height - imageBox.height * _scale) / 2
          );
        }
        pathSet.paths.push(...imagePathSet.paths.map((p) => {
          const path = p.clone();
          path.applyTransform(m);
          if (path.style.fill && path.style.fill in colormap) {
            path.style.fill = colormap[path.style.fill];
          }
          if (path.style.stroke && path.style.stroke in colormap) {
            path.style.stroke = colormap[path.style.stroke];
          }
          return path;
        }));
      });
    }
  });
}

function outlinePlugin() {
  return {
    name: "outline"
    // TODO
  };
}

const tempV1 = new modernPath2d.Vector2();
const tempM1 = new modernPath2d.Matrix3();
const tempM2 = new modernPath2d.Matrix3();
function renderPlugin() {
  const pathSet = new modernPath2d.Path2DSet();
  return definePlugin({
    name: "render",
    pathSet,
    update: (text) => {
      pathSet.paths.length = 0;
      const { paragraphs } = text;
      paragraphs.forEach((paragraph) => {
        paragraph.fragments.forEach((fragment) => {
          fragment.characters.forEach((character) => {
            pathSet.paths.push(character.path);
          });
        });
      });
    },
    getBoundingBox: (text) => {
      const { characters, fontSize, computedEffects } = text;
      const boxes = [];
      characters.forEach((character) => {
        computedEffects.forEach((style) => {
          if (!character.glyphBox) {
            return;
          }
          const aabb = character.glyphBox.clone();
          const m = getTransform2D(text, style);
          tempV1.set(aabb.left, aabb.top);
          tempV1.applyMatrix3(m);
          aabb.left = tempV1.x;
          aabb.top = tempV1.y;
          tempV1.set(aabb.right, aabb.bottom);
          tempV1.applyMatrix3(m);
          aabb.width = tempV1.x - aabb.left;
          aabb.height = tempV1.y - aabb.top;
          const shadowOffsetX = (style.shadowOffsetX ?? 0) * fontSize;
          const shadowOffsetY = (style.shadowOffsetY ?? 0) * fontSize;
          const textStrokeWidth = Math.max(0.1, style.textStrokeWidth ?? 0) * fontSize;
          aabb.left += shadowOffsetX - textStrokeWidth;
          aabb.top += shadowOffsetY - textStrokeWidth;
          aabb.width += textStrokeWidth * 2;
          aabb.height += textStrokeWidth * 2;
          boxes.push(aabb);
        });
      });
      return boxes.length ? modernPath2d.BoundingBox.from(...boxes) : void 0;
    },
    render: (renderer) => {
      const { text, context } = renderer;
      const { paragraphs, glyphBox, computedEffects } = text;
      if (paragraphs.length && computedEffects.length) {
        computedEffects.forEach((style) => {
          renderer.uploadColor(style, glyphBox);
          context.save();
          const [a, c, e, b, d, f] = getTransform2D(text, style).transpose().elements;
          context.transform(a, b, c, d, e, f);
          text.forEachCharacter((character) => {
            renderer.drawCharacter(character, style);
          });
          context.restore();
        });
      } else {
        paragraphs.forEach((paragraph) => {
          paragraph.fragments.forEach((fragment) => {
            fragment.characters.forEach((character) => {
              renderer.drawCharacter(character);
            });
          });
        });
      }
      if (text.debug) {
        paragraphs.forEach((paragraph) => {
          context.strokeRect(
            paragraph.lineBox.x,
            paragraph.lineBox.y,
            paragraph.lineBox.width,
            paragraph.lineBox.height
          );
        });
      }
    }
  });
}
function getTransform2D(text, style) {
  const { fontSize, glyphBox } = text;
  const translateX = (style.translateX ?? 0) * fontSize;
  const translateY = (style.translateY ?? 0) * fontSize;
  const PI_2 = Math.PI * 2;
  const skewX = (style.skewX ?? 0) / 360 * PI_2;
  const skewY = (style.skewY ?? 0) / 360 * PI_2;
  const { left, top, width, height } = glyphBox;
  const centerX = left + width / 2;
  const centerY = top + height / 2;
  tempM1.identity();
  tempM2.makeTranslation(translateX, translateY);
  tempM1.multiply(tempM2);
  tempM2.makeTranslation(centerX, centerY);
  tempM1.multiply(tempM2);
  tempM2.set(1, Math.tan(skewX), 0, Math.tan(skewY), 1, 0, 0, 0, 1);
  tempM1.multiply(tempM2);
  tempM2.makeTranslation(-centerX, -centerY);
  tempM1.multiply(tempM2);
  return tempM1.clone();
}

function textDecorationPlugin() {
  const pathSet = new modernPath2d.Path2DSet();
  return definePlugin({
    name: "textDecoration",
    pathSet,
    update: (text) => {
      pathSet.paths.length = 0;
      const groups = [];
      let group;
      let prevStyle;
      text.forEachCharacter((character) => {
        const {
          computedStyle: style,
          isVertical,
          inlineBox,
          underlinePosition,
          underlineThickness,
          strikeoutPosition,
          strikeoutSize
        } = character;
        const { color, textDecoration, writingMode } = style;
        if (!modernIdoc.isNone(textDecoration)) {
          let flag = false;
          if (prevStyle?.textDecoration === textDecoration && prevStyle?.writingMode === writingMode && prevStyle?.color === color && (isVertical ? group[0].inlineBox.left === inlineBox.left : group[0].inlineBox.top === inlineBox.top)) {
            switch (textDecoration) {
              case "underline":
                if (group[0].underlinePosition === underlinePosition && group[0].underlineThickness === underlineThickness) {
                  flag = true;
                }
                break;
              case "line-through":
                if (group[0].strikeoutPosition === strikeoutPosition && group[0].strikeoutSize === strikeoutSize) {
                  flag = true;
                }
                break;
            }
          }
          if (flag) {
            group.push(character);
          } else {
            group = [];
            group.push(character);
            groups.push(group);
          }
          prevStyle = style;
        } else {
          prevStyle = void 0;
        }
      });
      groups.forEach((group2) => {
        const {
          computedStyle: style,
          isVertical,
          underlinePosition,
          underlineThickness,
          strikeoutPosition,
          strikeoutSize
        } = group2[0];
        const {
          color,
          textDecoration
        } = style;
        const inlineBox = modernPath2d.BoundingBox.from(...group2.map((c) => c.inlineBox));
        const { left, top, width, height } = inlineBox;
        let position = isVertical ? left + width : top;
        const direction = isVertical ? -1 : 1;
        let thickness = 0;
        switch (textDecoration) {
          case "overline":
            thickness = underlineThickness * 2;
            break;
          case "underline":
            position += direction * underlinePosition;
            thickness = underlineThickness * 2;
            break;
          case "line-through":
            position += direction * strikeoutPosition;
            thickness = strikeoutSize * 2;
            break;
        }
        position -= thickness;
        let path;
        if (isVertical) {
          path = new modernPath2d.Path2D([
            { type: "M", x: position, y: top },
            { type: "L", x: position, y: top + height },
            { type: "L", x: position + thickness, y: top + height },
            { type: "L", x: position + thickness, y: top },
            { type: "Z" }
          ], {
            fill: color
          });
        } else {
          path = new modernPath2d.Path2D([
            { type: "M", x: left, y: position },
            { type: "L", x: left + width, y: position },
            { type: "L", x: left + width, y: position + thickness },
            { type: "L", x: left, y: position + thickness },
            { type: "Z" }
          ], {
            fill: color
          });
        }
        pathSet.paths.push(path);
      });
    },
    render: (renderer) => {
      const { text, context } = renderer;
      const { computedEffects } = text;
      if (computedEffects.length) {
        computedEffects.forEach((effectStyle) => {
          context.save();
          const [a, c, e, b, d, f] = getTransform2D(text, effectStyle).transpose().elements;
          context.transform(a, b, c, d, e, f);
          pathSet.paths.forEach((path) => {
            renderer.drawPath(path, effectStyle);
          });
          context.restore();
        });
      } else {
        pathSet.paths.forEach((path) => {
          renderer.drawPath(path);
        });
      }
    }
  });
}

var __defProp = Object.defineProperty;
var __decorateClass = (decorators, target, key, kind) => {
  var result = void 0 ;
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
    if (decorator = decorators[i])
      result = (decorator(target, key, result) ) || result;
  if (result) __defProp(target, key, result);
  return result;
};
const textDefaultStyle = modernIdoc.getDefaultStyle();
class Text extends modernIdoc.Reactivable {
  needsUpdate = true;
  computedStyle = { ...textDefaultStyle };
  computedEffects = [];
  paragraphs = [];
  inlineBox = new modernPath2d.BoundingBox();
  lineBox = new modernPath2d.BoundingBox();
  rawGlyphBox = new modernPath2d.BoundingBox();
  glyphBox = new modernPath2d.BoundingBox();
  pathBox = new modernPath2d.BoundingBox();
  boundingBox = new modernPath2d.BoundingBox();
  measurer = new Measurer();
  plugins = /* @__PURE__ */ new Map();
  pathSets = [];
  get fontSize() {
    return this.computedStyle.fontSize;
  }
  get isVertical() {
    return this.computedStyle.writingMode.includes("vertical");
  }
  get characters() {
    return this.paragraphs.flatMap((p) => p.fragments.flatMap((f) => f.characters));
  }
  constructor(options = {}) {
    super();
    this.set(options);
  }
  set(options = {}) {
    const {
      content,
      effects,
      style,
      measureDom,
      fonts,
      fill,
      outline
    } = modernIdoc.normalizeText(options);
    this.debug = options.debug ?? false;
    this.content = content;
    this.effects = effects;
    this.style = style;
    this.measureDom = measureDom;
    this.fonts = fonts;
    this.fill = fill;
    this.outline = outline;
    this.use(backgroundPlugin()).use(outlinePlugin()).use(listStylePlugin()).use(textDecorationPlugin()).use(highlightPlugin()).use(renderPlugin());
    (options.plugins ?? []).forEach((plugin) => {
      this.use(plugin);
    });
    this._update();
  }
  use(plugin) {
    this.plugins.set(plugin.name, plugin);
    return this;
  }
  forEachCharacter(handle) {
    this.paragraphs.forEach((p, paragraphIndex) => {
      p.fragments.forEach((f, fragmentIndex) => {
        f.characters.forEach((c, characterIndex) => {
          handle(c, { paragraphIndex, fragmentIndex, characterIndex });
        });
      });
    });
    return this;
  }
  async load() {
    this._update();
    await Promise.all(Array.from(this.plugins.values()).map((p) => p.load?.(this)));
  }
  _update() {
    this.computedStyle = { ...textDefaultStyle, ...this.style };
    this.computedEffects = this.effects?.map((v) => ({ ...v })) ?? [];
    const paragraphs = [];
    this.content.forEach((p, pIndex) => {
      const { fragments, fill: pFill, outline: pOutline, ...pStyle } = p;
      const paragraph = new Paragraph(pStyle, pIndex, this);
      paragraph.fill = pFill;
      paragraph.outline = pOutline;
      fragments.forEach((f, fIndex) => {
        const { content, fill: fFill, outline: fOutline, ...fStyle } = f;
        if (content !== void 0) {
          const fragment = new Fragment(content, fStyle, fIndex, paragraph);
          paragraph.fragments.push(fragment);
          fragment.fill = fFill;
          fragment.outline = fOutline;
        }
      });
      paragraphs.push(paragraph);
    });
    this.paragraphs = paragraphs;
    return this;
  }
  createDom() {
    this._update();
    return this.measurer.createDom(this.paragraphs, this.computedStyle);
  }
  measure(dom = this.measureDom) {
    const old = {
      paragraphs: this.paragraphs,
      inlineBox: this.inlineBox,
      lineBox: this.lineBox,
      rawGlyphBox: this.rawGlyphBox,
      glyphBox: this.glyphBox,
      pathBox: this.pathBox,
      boundingBox: this.boundingBox
    };
    this._update();
    const result = this.measurer.measure(this.paragraphs, this.computedStyle, dom);
    this.paragraphs = result.paragraphs;
    this.lineBox = result.boundingBox;
    this.characters.forEach((c) => {
      c.update(this.fonts);
    });
    this.rawGlyphBox = this.getGlyphBox();
    Array.from(this.plugins.values()).sort((a, b) => (a.updateOrder ?? 0) - (b.updateOrder ?? 0)).forEach((plugin) => {
      plugin.update?.(this);
    });
    this.pathSets.length = 0;
    Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
      if (plugin.pathSet?.paths.length) {
        this.pathSets.push(plugin.pathSet);
      }
    });
    this.glyphBox = this.getGlyphBox();
    this._updateInlineBox()._updatePathBox()._updateBoundingBox();
    for (const key in old) {
      result[key] = this[key];
      this[key] = old[key];
    }
    this.emit("measure", { text: this, result });
    return result;
  }
  getGlyphBox() {
    const min = modernPath2d.Vector2.MAX;
    const max = modernPath2d.Vector2.MIN;
    this.characters.forEach((c) => {
      if (!c.getGlyphMinMax(min, max)) {
        const { inlineBox: glyphBox } = c;
        const { left, top, width, height } = glyphBox;
        const a = new modernPath2d.Vector2(left, top);
        const b = new modernPath2d.Vector2(left + width, top + height);
        min.min(a, b);
        max.max(a, b);
      }
    });
    if (min.x === Number.MIN_SAFE_INTEGER || min.y === Number.MIN_SAFE_INTEGER || max.x === Number.MAX_SAFE_INTEGER || max.y === Number.MAX_SAFE_INTEGER) {
      return new modernPath2d.BoundingBox(0, 0, 0, 0);
    }
    return new modernPath2d.BoundingBox(
      min.x,
      min.y,
      max.x - min.x,
      max.y - min.y
    );
  }
  _updateInlineBox() {
    this.inlineBox = modernPath2d.BoundingBox.from(
      ...this.paragraphs.flatMap((p) => p.fragments.map((f) => f.inlineBox))
    );
    return this;
  }
  _updatePathBox() {
    this.pathBox = modernPath2d.BoundingBox.from(
      this.glyphBox,
      ...Array.from(this.plugins.values()).map((plugin) => {
        return plugin.getBoundingBox ? plugin.getBoundingBox(this) : plugin.pathSet?.getBoundingBox();
      }).filter(Boolean)
    );
    return this;
  }
  _updateBoundingBox() {
    this.boundingBox = modernPath2d.BoundingBox.from(
      this.rawGlyphBox,
      this.lineBox,
      this.pathBox
    );
    return this;
  }
  requestUpdate() {
    this.needsUpdate = true;
    return this;
  }
  update(dom = this.measureDom) {
    this.needsUpdate = false;
    const result = this.measure(dom);
    for (const key in result) {
      this[key] = result[key];
    }
    this.emit("update", { text: this });
    return this;
  }
  render(options) {
    const { view, pixelRatio = 2 } = options;
    const ctx = view.getContext("2d");
    if (!ctx) {
      return;
    }
    if (this.needsUpdate) {
      this.update();
    }
    const renderer = new Canvas2DRenderer(this, ctx);
    renderer.pixelRatio = pixelRatio;
    renderer.setup();
    Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
      if (plugin.render) {
        plugin.render?.(renderer);
      } else if (plugin.pathSet) {
        plugin.pathSet.paths.forEach((path) => {
          renderer.drawPath(path);
        });
      }
    });
    this.emit("render", { text: this, view, pixelRatio });
    options.onContext?.(ctx);
  }
  toString() {
    return this.content.flatMap((p) => p.fragments.map((f) => f.content)).join("");
  }
}
__decorateClass([
  modernIdoc.property({ internal: true })
], Text.prototype, "debug");
__decorateClass([
  modernIdoc.property()
], Text.prototype, "content");
__decorateClass([
  modernIdoc.property()
], Text.prototype, "style");
__decorateClass([
  modernIdoc.property()
], Text.prototype, "effects");
__decorateClass([
  modernIdoc.property()
], Text.prototype, "fill");
__decorateClass([
  modernIdoc.property()
], Text.prototype, "outline");
__decorateClass([
  modernIdoc.property({ internal: true })
], Text.prototype, "measureDom");
__decorateClass([
  modernIdoc.property({ internal: true })
], Text.prototype, "fonts");

exports.Canvas2DRenderer = Canvas2DRenderer;
exports.Character = Character;
exports.Fragment = Fragment;
exports.Measurer = Measurer;
exports.Paragraph = Paragraph;
exports.Text = Text;
exports.backgroundPlugin = backgroundPlugin;
exports.createSvgLoader = createSvgLoader;
exports.createSvgParser = createSvgParser;
exports.definePlugin = definePlugin;
exports.filterEmpty = filterEmpty;
exports.getHighlightStyle = getHighlightStyle;
exports.getTransform2D = getTransform2D;
exports.hexToRgb = hexToRgb;
exports.highlightPlugin = highlightPlugin;
exports.isEqualObject = isEqualObject;
exports.isEqualValue = isEqualValue;
exports.listStylePlugin = listStylePlugin;
exports.outlinePlugin = outlinePlugin;
exports.parseColormap = parseColormap;
exports.parseValueNumber = parseValueNumber;
exports.renderPlugin = renderPlugin;
exports.textDecorationPlugin = textDecorationPlugin;
exports.textDefaultStyle = textDefaultStyle;