UNPKG

modern-text

Version:

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

1,857 lines 59.5 kB
'use strict';

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

function parseColor(ctx, source, box) {
  if (typeof source === "string" && source.startsWith("linear-gradient")) {
    const { x0, y0, x1, y1, stops } = parseCssLinearGradient(source, box.left, box.top, box.width, box.height);
    const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
    stops.forEach((stop) => gradient.addColorStop(stop.offset, stop.color));
    return gradient;
  }
  return source;
}
function uploadColor(style, box, ctx) {
  if (style?.color) {
    style.color = parseColor(ctx, style.color, box);
  }
  if (style?.backgroundColor) {
    style.backgroundColor = parseColor(ctx, style.backgroundColor, box);
  }
  if (style?.textStrokeColor) {
    style.textStrokeColor = parseColor(ctx, style.textStrokeColor, box);
  }
}
function parseCssLinearGradient(css, x, y, width, height) {
  const str = css.match(/linear-gradient\((.+)\)$/)?.[1] ?? "";
  const first = str.split(",")[0];
  const cssDeg = first.includes("deg") ? first : "0deg";
  const matched = str.replace(cssDeg, "").matchAll(/(#|rgba|rgb)(.+?) ([\d.]+%)/gi);
  const deg = Number(cssDeg.replace("deg", "")) || 0;
  const rad = deg * Math.PI / 180;
  const offsetX = width * Math.sin(rad);
  const offsetY = height * Math.cos(rad);
  return {
    x0: x + width / 2 - offsetX,
    y0: y + height / 2 + offsetY,
    x1: x + width / 2 + offsetX,
    y1: y + height / 2 - offsetY,
    stops: Array.from(matched).map((res) => {
      let color = res[2];
      if (color.startsWith("(")) {
        color = color.split(",").length > 3 ? `rgba${color}` : `rgb${color}`;
      } else {
        color = `#${color}`;
      }
      return {
        offset: Number(res[3].replace("%", "")) / 100,
        color
      };
    })
  };
}

function drawPath(options) {
  const { ctx, path, fontSize, clipRect } = options;
  ctx.save();
  ctx.beginPath();
  const pathStyle = path.style;
  const style = {
    ...pathStyle,
    fill: options.color ?? pathStyle.fill,
    stroke: options.textStrokeColor ?? pathStyle.stroke,
    strokeWidth: options.textStrokeWidth ? options.textStrokeWidth * fontSize : pathStyle.strokeWidth,
    strokeLinecap: "round",
    strokeLinejoin: "round",
    shadowOffsetX: (options.shadowOffsetX ?? 0) * fontSize,
    shadowOffsetY: (options.shadowOffsetY ?? 0) * fontSize,
    shadowBlur: (options.shadowBlur ?? 0) * fontSize,
    shadowColor: options.shadowColor
  };
  if (clipRect) {
    ctx.rect(clipRect.left, clipRect.top, clipRect.width, clipRect.height);
    ctx.clip();
    ctx.beginPath();
  }
  path.drawTo(ctx, style);
  ctx.restore();
}

function setupView(ctx, pixelRatio, boundingBox) {
  const { left, top, width, height } = 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);
}

function uploadColors(ctx, text) {
  const { paragraphs, computedStyle: style, glyphBox } = text;
  uploadColor(style, glyphBox, ctx);
  paragraphs.forEach((paragraph) => {
    uploadColor(paragraph.computedStyle, paragraph.lineBox, ctx);
    paragraph.fragments.forEach((fragment) => {
      uploadColor(fragment.computedStyle, fragment.inlineBox, ctx);
    });
  });
}

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();
  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, isVertical } = 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.inlineBox.width = isVertical ? advanceHeight : advanceWidth;
    this.inlineBox.height = isVertical ? advanceWidth : 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();
    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);
  }
  drawTo(ctx, config = {}) {
    const style = this.computedStyle;
    const options = {
      ctx,
      path: this.path,
      fontSize: style.fontSize,
      color: style.color,
      ...config
    };
    if (this.glyphBox) {
      drawPath(options);
    } else {
      ctx.save();
      ctx.beginPath();
      const pathStyle = this.path.style;
      const _style = {
        ...pathStyle,
        fill: options.color ?? pathStyle.fill,
        stroke: options.textStrokeColor ?? pathStyle.stroke,
        strokeWidth: options.textStrokeWidth ? options.textStrokeWidth * options.fontSize : pathStyle.strokeWidth,
        shadowOffsetX: (options.shadowOffsetX ?? 0) * options.fontSize,
        shadowOffsetY: (options.shadowOffsetY ?? 0) * options.fontSize,
        shadowBlur: (options.shadowBlur ?? 0) * options.fontSize,
        shadowColor: options.shadowColor
      };
      modernPath2d.setCanvasContext(ctx, _style);
      ctx.font = `${options.fontSize}px ${options.fontFamily}`;
      if (this.isVertical) {
        ctx.textBaseline = "middle";
        ctx.fillText(this.content, this.inlineBox.left, this.inlineBox.top + this.inlineBox.height / 2);
      } else {
        ctx.textBaseline = "alphabetic";
        ctx.fillText(this.content, this.inlineBox.left, this.inlineBox.top + this.baseline);
      }
      ctx.restore();
    }
  }
}

function createSVGLoader() {
  const loaded = /* @__PURE__ */ new Map();
  async function load(svg) {
    if (!loaded.has(svg)) {
      loaded.set(svg, 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("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 dom = modernPath2d.svgToDom(
        loader.needsLoad(svg) ? loader.loaded.get(svg) ?? svg : svg
      );
      const pathSet = modernPath2d.svgToPath2DSet(dom);
      result = { dom, pathSet };
      parsed.set(svg, result);
    }
    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 = {}, parent) {
    this.content = content;
    this.style = style;
    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, parent) {
    this.style = style;
    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;
  }
  addFragment(content, style) {
    const fragment = new Fragment(content, style, this);
    this.fragments.push(fragment);
    return fragment;
  }
}

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.left,
        top: pBox.top,
        width: pBox.width,
        height: pBox.height
      });
      pDOM.querySelectorAll(":scope > *").forEach((fDOM, fragmentIndex) => {
        const fBox = fDOM.getBoundingClientRect();
        fragments.push({
          paragraphIndex,
          fragmentIndex,
          left: fBox.left,
          top: fBox.top,
          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 _p = paragraphs[p.paragraphIndex];
      _p.lineBox.left = p.left - rect.left;
      _p.lineBox.top = p.top - rect.top;
      _p.lineBox.width = p.width;
      _p.lineBox.height = p.height;
    });
    measured.fragments.forEach((f) => {
      const _f = paragraphs[f.paragraphIndex].fragments[f.fragmentIndex];
      _f.inlineBox.left = f.left - rect.left;
      _f.inlineBox.top = f.top - rect.top;
      _f.inlineBox.width = f.width;
      _f.inlineBox.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 } = item;
      const result = results[i];
      item.inlineBox.left = result.left;
      item.inlineBox.top = result.top;
      item.inlineBox.width = result.width;
      item.inlineBox.height = result.height;
      if (isVertical) {
        item.lineBox.left = result.left + (result.width - fontHeight) / 2;
        item.lineBox.top = result.top;
        item.lineBox.width = fontHeight;
        item.lineBox.height = result.height;
      } else {
        item.lineBox.left = result.left;
        item.lineBox.top = result.top + (result.height - fontHeight) / 2;
        item.lineBox.width = result.width;
        item.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,
    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);
      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: (ctx, text) => {
      const { boundingBox, computedStyle: style } = text;
      if (!modernIdoc.isNone(style.backgroundColor)) {
        ctx.fillStyle = style.backgroundColor;
        ctx.fillRect(...boundingBox.array);
      }
      pathSet.paths.forEach((path) => {
        drawPath({ ctx, path, fontSize: style.fontSize });
        if (text.debug) {
          const box = new modernPath2d.Path2DSet([path]).getBoundingBox();
          if (box) {
            ctx.strokeRect(box.x, box.y, box.width, box.height);
          }
        }
      });
      text.paragraphs.forEach((p) => {
        const { lineBox, style: style2 } = p;
        if (!modernIdoc.isNone(style2.backgroundColor)) {
          ctx.fillStyle = style2.backgroundColor;
          ctx.fillRect(...lineBox.array);
        }
        p.fragments.forEach((f) => {
          const { inlineBox, style: style3 } = f;
          if (!modernIdoc.isNone(style3.backgroundColor)) {
            ctx.fillStyle = style3.backgroundColor;
            ctx.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,
    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);
        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: (ctx, text) => {
      pathSet.paths.forEach((path, index) => {
        drawPath({
          ctx,
          path,
          fontSize: text.computedStyle.fontSize,
          clipRect: clipRects[index]
        });
        if (text.debug) {
          const box = new modernPath2d.Path2DSet([path]).getBoundingBox();
          if (box) {
            ctx.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, effects } = text;
      const boxes = [];
      characters.forEach((character) => {
        effects?.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: (ctx, text) => {
      const { paragraphs, glyphBox, effects } = text;
      if (effects) {
        effects.forEach((style) => {
          uploadColor(style, glyphBox, ctx);
          ctx.save();
          const [a, c, e, b, d, f] = getTransform2D(text, style).transpose().elements;
          ctx.transform(a, b, c, d, e, f);
          text.forEachCharacter((character) => {
            character.drawTo(ctx, style);
          });
          ctx.restore();
        });
      } else {
        paragraphs.forEach((paragraph) => {
          paragraph.fragments.forEach((fragment) => {
            fragment.characters.forEach((character) => {
              character.drawTo(ctx);
            });
          });
        });
      }
      if (text.debug) {
        paragraphs.forEach((paragraph) => {
          ctx.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: (ctx, text) => {
      const { effects, computedStyle: style } = text;
      if (effects) {
        effects.forEach((effectStyle) => {
          ctx.save();
          const [a, c, e, b, d, f] = getTransform2D(text, effectStyle).transpose().elements;
          ctx.transform(a, b, c, d, e, f);
          pathSet.paths.forEach((path) => {
            drawPath({
              ctx,
              path,
              fontSize: style.fontSize,
              ...effectStyle
            });
          });
          ctx.restore();
        });
      } else {
        pathSet.paths.forEach((path) => {
          drawPath({
            ctx,
            path,
            fontSize: style.fontSize
          });
        });
      }
    }
  });
}

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.EventEmitter {
  needsUpdate = true;
  computedStyle = { ...textDefaultStyle };
  paragraphs = [];
  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.updateParagraphs();
  }
  onUpdateProperty(key, newValue, oldValue, declaration) {
    this.emit("updateProperty", key, newValue, oldValue, declaration);
  }
  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() {
    await Promise.all(Array.from(this.plugins.values()).map((p) => p.load?.(this)));
  }
  updateParagraphs() {
    this.computedStyle = { ...textDefaultStyle, ...this.style };
    const { content } = this;
    const paragraphs = [];
    content.forEach((p) => {
      const { fragments, fill: pFill, outline: pOutline, ...pStyle } = p;
      const paragraph = new Paragraph(pStyle, this);
      paragraph.fill = pFill;
      paragraph.outline = pOutline;
      fragments.forEach((f) => {
        const { content: content2, fill: fFill, outline: fOutline, ...fStyle } = f;
        if (content2 !== void 0) {
          const fragment = paragraph.addFragment(content2, fStyle);
          fragment.fill = fFill;
          fragment.outline = fOutline;
        }
      });
      paragraphs.push(paragraph);
    });
    this.paragraphs = paragraphs;
    return this;
  }
  createDom() {
    this.updateParagraphs();
    return this.measurer.createDom(this.paragraphs, this.computedStyle);
  }
  measure(dom = this.measureDom) {
    const old = {
      paragraphs: this.paragraphs,
      lineBox: this.lineBox,
      rawGlyphBox: this.rawGlyphBox,
      glyphBox: this.glyphBox,
      pathBox: this.pathBox,
      boundingBox: this.boundingBox
    };
    this.updateParagraphs();
    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.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);
      }
    });
    return new modernPath2d.BoundingBox(
      min.x,
      min.y,
      max.x - min.x,
      max.y - min.y
    );
  }
  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) {
    const result = this.measure(dom);
    for (const key in result) {
      this[key] = result[key];
    }
    this.emit("update", { text: this });
    this.needsUpdate = false;
    return this;
  }
  render(options) {
    const { view, pixelRatio = 2 } = options;
    const ctx = view.getContext("2d");
    if (!ctx) {
      return;
    }
    if (this.needsUpdate) {
      this.update();
    }
    setupView(ctx, pixelRatio, this.boundingBox);
    uploadColors(ctx, this);
    Array.from(this.plugins.values()).sort((a, b) => (a.renderOrder ?? 0) - (b.renderOrder ?? 0)).forEach((plugin) => {
      if (plugin.render) {
        plugin.render?.(ctx, this);
      } else if (plugin.pathSet) {
        const style = this.computedStyle;
        plugin.pathSet.paths.forEach((path) => {
          drawPath({
            ctx,
            path,
            fontSize: style.fontSize
          });
        });
      }
    });
    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()
], 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()
], Text.prototype, "measureDom");
__decorateClass([
  modernIdoc.property()
], Text.prototype, "fonts");

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.drawPath = drawPath;
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.parseColor = parseColor;
exports.parseColormap = parseColormap;
exports.parseValueNumber = parseValueNumber;
exports.renderPlugin = renderPlugin;
exports.setupView = setupView;
exports.textDecorationPlugin = textDecorationPlugin;
exports.textDefaultStyle = textDefaultStyle;
exports.uploadColor = uploadColor;
exports.uploadColors = uploadColors;