"use strict";

exports.__esModule = true;
exports.default = void 0;
var _DocPreprocessor = _interopRequireDefault(require("./DocPreprocessor"));
var _DocMeasure = _interopRequireDefault(require("./DocMeasure"));
var _DocumentContext = _interopRequireDefault(require("./DocumentContext"));
var _PageElementWriter = _interopRequireDefault(require("./PageElementWriter"));
var _columnCalculator = _interopRequireDefault(require("./columnCalculator"));
var _TableProcessor = _interopRequireDefault(require("./TableProcessor"));
var _Line = _interopRequireDefault(require("./Line"));
var _variableType = require("./helpers/variableType");
var _node = require("./helpers/node");
var _tools = require("./helpers/tools");
var _TextInlines = _interopRequireDefault(require("./TextInlines"));
var _StyleContextStack = _interopRequireDefault(require("./StyleContextStack"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function addAll(target, otherArray) {
  otherArray.forEach(item => {
    target.push(item);
  });
}

/**
 * Layout engine which turns document-definition-object into a set of pages, lines, inlines
 * and vectors ready to be rendered into a PDF
 */
class LayoutBuilder {
  /**
   * @param {object} pageSize - an object defining page width and height
   * @param {object} pageMargins - an object defining top, left, right and bottom margins
   * @param {object} svgMeasure
   */
  constructor(pageSize, pageMargins, svgMeasure) {
    this.pageSize = pageSize;
    this.pageMargins = pageMargins;
    this.svgMeasure = svgMeasure;
    this.tableLayouts = {};
    this.nestedLevel = 0;
    this.verticalAlignmentItemStack = [];
  }
  registerTableLayouts(tableLayouts) {
    this.tableLayouts = (0, _tools.pack)(this.tableLayouts, tableLayouts);
  }

  /**
   * Executes layout engine on document-definition-object and creates an array of pages
   * containing positioned Blocks, Lines and inlines
   *
   * @param {object} docStructure document-definition-object
   * @param {object} pdfDocument pdfkit document
   * @param {object} styleDictionary dictionary with style definitions
   * @param {object} defaultStyle default style definition
   * @param {object} background
   * @param {object} header
   * @param {object} footer
   * @param {object} watermark
   * @param {object} pageBreakBeforeFct
   * @returns {Array} an array of pages
   */
  layoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark, pageBreakBeforeFct) {
    function addPageBreaksIfNecessary(linearNodeList, pages) {
      if (typeof pageBreakBeforeFct !== 'function') {
        return false;
      }
      const hasRenderableContent = node => {
        if (!node || node.positions.length === 0) {
          return false;
        }
        if (node.text === '' && !node.listMarker) {
          return false;
        }
        return true;
      };
      linearNodeList = linearNodeList.filter(hasRenderableContent);
      linearNodeList.forEach(node => {
        let nodeInfo = {};
        ['id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns', 'headlineLevel', 'style', 'pageBreak', 'pageOrientation', 'width', 'height'].forEach(key => {
          if (node[key] !== undefined) {
            nodeInfo[key] = node[key];
          }
        });
        nodeInfo.startPosition = node.positions[0];
        nodeInfo.pageNumbers = Array.from(new Set(node.positions.map(node => node.pageNumber)));
        nodeInfo.pages = pages.length;
        nodeInfo.stack = Array.isArray(node.stack);
        node.nodeInfo = nodeInfo;
      });
      for (let index = 0; index < linearNodeList.length; index++) {
        let node = linearNodeList[index];
        if (node.pageBreak !== 'before' && !node.pageBreakCalculated) {
          node.pageBreakCalculated = true;
          let pageNumber = node.nodeInfo.pageNumbers[0];
          if (pageBreakBeforeFct(node.nodeInfo, {
            getFollowingNodesOnPage: () => {
              let followingNodesOnPage = [];
              for (let ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
                if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
                  followingNodesOnPage.push(linearNodeList[ii].nodeInfo);
                }
              }
              return followingNodesOnPage;
            },
            getNodesOnNextPage: () => {
              let nodesOnNextPage = [];
              for (let ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
                if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber + 1) > -1) {
                  nodesOnNextPage.push(linearNodeList[ii].nodeInfo);
                }
              }
              return nodesOnNextPage;
            },
            getPreviousNodesOnPage: () => {
              let previousNodesOnPage = [];
              for (let ii = 0; ii < index; ii++) {
                if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
                  previousNodesOnPage.push(linearNodeList[ii].nodeInfo);
                }
              }
              return previousNodesOnPage;
            }
          })) {
            node.pageBreak = 'before';
            return true;
          }
        }
      }
      return false;
    }
    this.docPreprocessor = new _DocPreprocessor.default();
    this.docMeasure = new _DocMeasure.default(pdfDocument, styleDictionary, defaultStyle, this.svgMeasure, this.tableLayouts);
    function resetXYs(result) {
      result.linearNodeList.forEach(node => {
        node.resetXY();
      });
    }
    let result = this.tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark);
    while (addPageBreaksIfNecessary(result.linearNodeList, result.pages)) {
      resetXYs(result);
      result = this.tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark);
    }
    return result.pages;
  }
  tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark) {
    const isNecessaryAddFirstPage = docStructure => {
      if (docStructure.stack && docStructure.stack.length > 0 && docStructure.stack[0].section) {
        return false;
      } else if (docStructure.section) {
        return false;
      }
      return true;
    };
    this.linearNodeList = [];
    docStructure = this.docPreprocessor.preprocessDocument(docStructure);
    docStructure = this.docMeasure.measureDocument(docStructure);
    this.writer = new _PageElementWriter.default(new _DocumentContext.default());
    this.writer.context().addListener('pageAdded', page => {
      let backgroundGetter = background;
      if (page.customProperties['background'] || page.customProperties['background'] === null) {
        backgroundGetter = page.customProperties['background'];
      }
      this.addBackground(backgroundGetter);
    });
    if (isNecessaryAddFirstPage(docStructure)) {
      this.writer.addPage(this.pageSize, null, this.pageMargins);
    }
    this.processNode(docStructure);
    this.addHeadersAndFooters(header, footer);
    this.addWatermark(watermark, pdfDocument, defaultStyle);
    return {
      pages: this.writer.context().pages,
      linearNodeList: this.linearNodeList
    };
  }
  addBackground(background) {
    let backgroundGetter = typeof background === 'function' ? background : () => background;
    let context = this.writer.context();
    let pageSize = context.getCurrentPage().pageSize;
    let pageBackground = backgroundGetter(context.page + 1, pageSize);
    if (pageBackground) {
      this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
      pageBackground = this.docPreprocessor.preprocessBlock(pageBackground);
      this.processNode(this.docMeasure.measureBlock(pageBackground));
      this.writer.commitUnbreakableBlock(0, 0);
      context.backgroundLength[context.page] += pageBackground.positions.length;
    }
  }
  addDynamicRepeatable(nodeGetter, sizeFunction, customPropertyName) {
    let pages = this.writer.context().pages;
    for (let pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
      this.writer.context().page = pageIndex;
      let customProperties = this.writer.context().getCurrentPage().customProperties;
      let pageNodeGetter = nodeGetter;
      if (customProperties[customPropertyName] || customProperties[customPropertyName] === null) {
        pageNodeGetter = customProperties[customPropertyName];
      }
      if (typeof pageNodeGetter === 'undefined' || pageNodeGetter === null) {
        continue;
      }
      let node = pageNodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
      if (node) {
        let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.writer.context().getCurrentPage().pageMargins);
        this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
        node = this.docPreprocessor.preprocessBlock(node);
        this.processNode(this.docMeasure.measureBlock(node));
        this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
      }
    }
  }
  addHeadersAndFooters(header, footer) {
    const headerSizeFct = (pageSize, pageMargins) => ({
      x: 0,
      y: 0,
      width: pageSize.width,
      height: pageMargins.top
    });
    const footerSizeFct = (pageSize, pageMargins) => ({
      x: 0,
      y: pageSize.height - pageMargins.bottom,
      width: pageSize.width,
      height: pageMargins.bottom
    });
    this.addDynamicRepeatable(header, headerSizeFct, 'header');
    this.addDynamicRepeatable(footer, footerSizeFct, 'footer');
  }
  addWatermark(watermark, pdfDocument, defaultStyle) {
    let pages = this.writer.context().pages;
    for (let i = 0, l = pages.length; i < l; i++) {
      let pageWatermark = watermark;
      if (pages[i].customProperties['watermark'] || pages[i].customProperties['watermark'] === null) {
        pageWatermark = pages[i].customProperties['watermark'];
      }
      if (pageWatermark === undefined || pageWatermark === null) {
        continue;
      }
      if ((0, _variableType.isString)(pageWatermark)) {
        pageWatermark = {
          'text': pageWatermark
        };
      }
      if (!pageWatermark.text) {
        // empty watermark text
        continue;
      }
      pages[i].watermark = getWatermarkObject({
        ...pageWatermark
      }, pages[i].pageSize, pdfDocument, defaultStyle);
    }
    function getWatermarkObject(watermark, pageSize, pdfDocument, defaultStyle) {
      watermark.font = watermark.font || defaultStyle.font || 'Roboto';
      watermark.fontSize = watermark.fontSize || 'auto';
      watermark.color = watermark.color || 'black';
      watermark.opacity = (0, _variableType.isNumber)(watermark.opacity) ? watermark.opacity : 0.6;
      watermark.bold = watermark.bold || false;
      watermark.italics = watermark.italics || false;
      watermark.angle = (0, _variableType.isValue)(watermark.angle) ? watermark.angle : null;
      if (watermark.angle === null) {
        watermark.angle = Math.atan2(pageSize.height, pageSize.width) * -180 / Math.PI;
      }
      if (watermark.fontSize === 'auto') {
        watermark.fontSize = getWatermarkFontSize(pageSize, watermark, pdfDocument);
      }
      let watermarkObject = {
        text: watermark.text,
        font: pdfDocument.provideFont(watermark.font, watermark.bold, watermark.italics),
        fontSize: watermark.fontSize,
        color: watermark.color,
        opacity: watermark.opacity,
        angle: watermark.angle
      };
      watermarkObject._size = getWatermarkSize(watermark, pdfDocument);
      return watermarkObject;
    }
    function getWatermarkSize(watermark, pdfDocument) {
      let textInlines = new _TextInlines.default(pdfDocument);
      let styleContextStack = new _StyleContextStack.default(null, {
        font: watermark.font,
        bold: watermark.bold,
        italics: watermark.italics
      });
      styleContextStack.push({
        fontSize: watermark.fontSize
      });
      let size = textInlines.sizeOfText(watermark.text, styleContextStack);
      let rotatedSize = textInlines.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
      return {
        size: size,
        rotatedSize: rotatedSize
      };
    }
    function getWatermarkFontSize(pageSize, watermark, pdfDocument) {
      let textInlines = new _TextInlines.default(pdfDocument);
      let styleContextStack = new _StyleContextStack.default(null, {
        font: watermark.font,
        bold: watermark.bold,
        italics: watermark.italics
      });
      let rotatedSize;

      /**
       * Binary search the best font size.
       * Initial bounds [0, 1000]
       * Break when range < 1
       */
      let a = 0;
      let b = 1000;
      let c = (a + b) / 2;
      while (Math.abs(a - b) > 1) {
        styleContextStack.push({
          fontSize: c
        });
        rotatedSize = textInlines.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
        if (rotatedSize.width > pageSize.width) {
          b = c;
          c = (a + b) / 2;
        } else if (rotatedSize.width < pageSize.width) {
          if (rotatedSize.height > pageSize.height) {
            b = c;
            c = (a + b) / 2;
          } else {
            a = c;
            c = (a + b) / 2;
          }
        }
        styleContextStack.pop();
      }
      /*
       End binary search
       */
      return c;
    }
  }
  processNode(node, isVerticalAlignmentAllowed = false) {
    const applyMargins = callback => {
      let margin = node._margin;
      if (node.pageBreak === 'before') {
        this.writer.moveToNextPage(node.pageOrientation);
      } else if (node.pageBreak === 'beforeOdd') {
        this.writer.moveToNextPage(node.pageOrientation);
        if ((this.writer.context().page + 1) % 2 === 1) {
          this.writer.moveToNextPage(node.pageOrientation);
        }
      } else if (node.pageBreak === 'beforeEven') {
        this.writer.moveToNextPage(node.pageOrientation);
        if ((this.writer.context().page + 1) % 2 === 0) {
          this.writer.moveToNextPage(node.pageOrientation);
        }
      }
      const isDetachedBlock = node.relativePosition || node.absolutePosition;

      // Detached nodes have no margins, their position is only determined by 'x' and 'y'
      if (margin && !isDetachedBlock) {
        const availableHeight = this.writer.context().availableHeight;
        // If top margin is bigger than available space, move to next page
        // Necessary for nodes inside tables
        if (availableHeight - margin[1] < 0) {
          // Consume the whole available space
          this.writer.context().moveDown(availableHeight);
          if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
            this.snakingAwarePageBreak(node.pageOrientation);
          } else {
            this.writer.moveToNextPage(node.pageOrientation);
          }
          /**
           * TODO - Something to consider:
           * Right now the node starts at the top of next page (after header)
           * Another option would be to apply just the top margin that has not been consumed in the page before
           * It would something like: this.write.context().moveDown(margin[1] - availableHeight)
           */
        } else {
          this.writer.context().moveDown(margin[1]);
        }
        // Apply lateral margins
        this.writer.context().addMargin(margin[0], margin[2]);
      }
      callback();

      // Detached nodes have no margins, their position is only determined by 'x' and 'y'
      if (margin && !isDetachedBlock) {
        const availableHeight = this.writer.context().availableHeight;
        // If bottom margin is bigger than available space, move to next page
        // Necessary for nodes inside tables
        if (availableHeight - margin[3] < 0) {
          this.writer.context().moveDown(availableHeight);
          if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
            this.snakingAwarePageBreak(node.pageOrientation);
          } else {
            this.writer.moveToNextPage(node.pageOrientation);
          }
          /**
           * TODO - Something to consider:
           * Right now next node starts at the top of next page (after header)
           * Another option would be to apply the bottom margin that has not been consumed in the next page?
           * It would something like: this.write.context().moveDown(margin[3] - availableHeight)
           */
        } else {
          this.writer.context().moveDown(margin[3]);
        }
        // Apply lateral margins
        this.writer.context().addMargin(-margin[0], -margin[2]);
      }
      if (node.pageBreak === 'after') {
        this.writer.moveToNextPage(node.pageOrientation);
      } else if (node.pageBreak === 'afterOdd') {
        this.writer.moveToNextPage(node.pageOrientation);
        if ((this.writer.context().page + 1) % 2 === 1) {
          this.writer.moveToNextPage(node.pageOrientation);
        }
      } else if (node.pageBreak === 'afterEven') {
        this.writer.moveToNextPage(node.pageOrientation);
        if ((this.writer.context().page + 1) % 2 === 0) {
          this.writer.moveToNextPage(node.pageOrientation);
        }
      }
    };
    this.linearNodeList.push(node);
    decorateNode(node);
    if (this.writer.context().getCurrentPage() !== null) {
      var prevTop = this.writer.context().getCurrentPosition().top;
    }
    applyMargins(() => {
      let verticalAlignment = node.verticalAlignment;
      if (isVerticalAlignmentAllowed && verticalAlignment) {
        var verticalAlignmentBegin = this.writer.beginVerticalAlignment(verticalAlignment);
      }
      let unbreakable = node.unbreakable;
      if (unbreakable) {
        this.writer.beginUnbreakableBlock();
      }
      let absPosition = node.absolutePosition;
      if (absPosition) {
        this.writer.context().beginDetachedBlock();
        this.writer.context().moveTo(absPosition.x || 0, absPosition.y || 0);
      }
      let relPosition = node.relativePosition;
      if (relPosition) {
        this.writer.context().beginDetachedBlock();
        this.writer.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
      }
      if (node.stack) {
        this.processVerticalContainer(node);
      } else if (node.section) {
        this.processSection(node);
      } else if (node.columns) {
        this.processColumns(node);
      } else if (node.ul) {
        this.processList(false, node);
      } else if (node.ol) {
        this.processList(true, node);
      } else if (node.table) {
        this.processTable(node);
      } else if (node.text !== undefined) {
        this.processLeaf(node);
      } else if (node.toc) {
        this.processToc(node);
      } else if (node.image) {
        this.processImage(node);
      } else if (node.svg) {
        this.processSVG(node);
      } else if (node.canvas) {
        this.processCanvas(node);
      } else if (node.qr) {
        this.processQr(node);
      } else if (node.attachment) {
        this.processAttachment(node);
      } else if (!node._span) {
        throw new Error(`Unrecognized document structure: ${(0, _node.stringifyNode)(node)}`);
      }
      if (absPosition || relPosition) {
        this.writer.context().endDetachedBlock();
      }
      if (unbreakable) {
        this.writer.commitUnbreakableBlock();
      }
      if (isVerticalAlignmentAllowed && verticalAlignment) {
        this.verticalAlignmentItemStack.push({
          begin: verticalAlignmentBegin,
          end: this.writer.endVerticalAlignment(verticalAlignment)
        });
      }
    });
    if (prevTop !== undefined) {
      // TODO: for vertical alignment and does not work (at least) when page break in node
      node.__height = this.writer.context().getCurrentPosition().top - prevTop;
    }
  }

  /**
   * Helper for page breaks that respects snaking column context.
   * When in snaking columns, first tries moving to next column.
   * If no columns available, moves to next page and resets x to left margin.
   * @param {string} pageOrientation - Optional page orientation for the new page
   */
  snakingAwarePageBreak(pageOrientation) {
    let ctx = this.writer.context();
    let snakingSnapshot = ctx.getSnakingSnapshot();
    if (!snakingSnapshot) {
      return;
    }

    // Try flowing to next column first
    if (this.writer.canMoveToNextColumn()) {
      this.writer.moveToNextColumn();
      return;
    }

    // No more columns available, move to new page
    this.writer.moveToNextPage(pageOrientation);

    // Reset snaking column state for the new page
    // Save lastColumnWidth before reset — if we're inside a nested
    // column group (e.g. product/price row), the reset would overwrite
    // it with the snaking column width, corrupting inner column layout.
    let savedLastColumnWidth = ctx.lastColumnWidth;
    ctx.resetSnakingColumnsForNewPage();
    ctx.lastColumnWidth = savedLastColumnWidth;
  }

  // vertical container
  processVerticalContainer(node) {
    node.stack.forEach(item => {
      this.processNode(item);
      addAll(node.positions, item.positions);

      //TODO: paragraph gap
    }, this);
  }

  // section
  processSection(sectionNode) {
    // TODO: properties

    let page = this.writer.context().getCurrentPage();
    if (!page || page && page.items.length) {
      // move to new empty page
      // page definition inherit from current page
      if (sectionNode.pageSize === 'inherit') {
        sectionNode.pageSize = page ? {
          width: page.pageSize.width,
          height: page.pageSize.height
        } : undefined;
      }
      if (sectionNode.pageOrientation === 'inherit') {
        sectionNode.pageOrientation = page ? page.pageSize.orientation : undefined;
      }
      if (sectionNode.pageMargins === 'inherit') {
        sectionNode.pageMargins = page ? page.pageMargins : undefined;
      }
      if (sectionNode.header === 'inherit') {
        sectionNode.header = page ? page.customProperties.header : undefined;
      }
      if (sectionNode.footer === 'inherit') {
        sectionNode.footer = page ? page.customProperties.footer : undefined;
      }
      if (sectionNode.background === 'inherit') {
        sectionNode.background = page ? page.customProperties.background : undefined;
      }
      if (sectionNode.watermark === 'inherit') {
        sectionNode.watermark = page ? page.customProperties.watermark : undefined;
      }
      if (sectionNode.header && typeof sectionNode.header !== 'function' && sectionNode.header !== null) {
        sectionNode.header = (0, _tools.convertToDynamicContent)(sectionNode.header);
      }
      if (sectionNode.footer && typeof sectionNode.footer !== 'function' && sectionNode.footer !== null) {
        sectionNode.footer = (0, _tools.convertToDynamicContent)(sectionNode.footer);
      }
      let customProperties = {};
      if (typeof sectionNode.header !== 'undefined') {
        customProperties.header = sectionNode.header;
      }
      if (typeof sectionNode.footer !== 'undefined') {
        customProperties.footer = sectionNode.footer;
      }
      if (typeof sectionNode.background !== 'undefined') {
        customProperties.background = sectionNode.background;
      }
      if (typeof sectionNode.watermark !== 'undefined') {
        customProperties.watermark = sectionNode.watermark;
      }
      this.writer.addPage(sectionNode.pageSize || this.pageSize, sectionNode.pageOrientation, sectionNode.pageMargins || this.pageMargins, customProperties);
    }
    this.processNode(sectionNode.section);
  }

  // columns
  processColumns(columnNode) {
    this.nestedLevel++;
    let columns = columnNode.columns;
    let availableWidth = this.writer.context().availableWidth;
    let gaps = gapArray(columnNode._gap);
    if (gaps) {
      availableWidth -= (gaps.length - 1) * columnNode._gap;
    }
    _columnCalculator.default.buildColumnWidths(columns, availableWidth);
    let result = this.processRow({
      marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
      cells: columns,
      widths: columns,
      gaps,
      snakingColumns: columnNode.snakingColumns
    });
    addAll(columnNode.positions, result.positions);
    this.nestedLevel--;
    if (this.nestedLevel === 0) {
      this.writer.context().resetMarginXTopParent();
    }
    function gapArray(gap) {
      if (!gap) {
        return null;
      }
      let gaps = [];
      gaps.push(0);
      for (let i = columns.length - 1; i > 0; i--) {
        gaps.push(gap);
      }
      return gaps;
    }
  }

  /**
  * Searches for a cell in the same row that starts a rowspan and is positioned immediately before the current cell.
  * Alternatively, it finds a cell where the colspan initiating the rowspan extends to the cell just before the current one.
  *
  * @param {Array<object>} arr - An array representing cells in a row.
  * @param {number} i - The index of the current cell to search backward from.
  * @returns {object|null} The starting cell of the rowspan if found; otherwise, `null`.
  */
  _findStartingRowSpanCell(arr, i) {
    let requiredColspan = 1;
    for (let index = i - 1; index >= 0; index--) {
      if (!arr[index]._span) {
        if (arr[index].rowSpan > 1 && (arr[index].colSpan || 1) === requiredColspan) {
          return arr[index];
        } else {
          return null;
        }
      }
      requiredColspan++;
    }
    return null;
  }

  /**
  * Retrieves a page break description for a specified page from a list of page breaks.
  *
  * @param {Array<object>} pageBreaks - An array of page break descriptions, each containing `prevPage` properties.
  * @param {number} page - The page number to find the associated page break for.
  * @returns {object|undefined} The page break description object for the specified page if found; otherwise, `undefined`.
  */
  _getPageBreak(pageBreaks, page) {
    return pageBreaks.find(desc => desc.prevPage === page);
  }
  _getPageBreakListBySpan(tableNode, page, rowIndex) {
    if (!tableNode || !tableNode._breaksBySpan) {
      return null;
    }
    const breaksList = tableNode._breaksBySpan.filter(desc => desc.prevPage === page && rowIndex <= desc.rowIndexOfSpanEnd);
    let y = Number.MAX_VALUE,
      prevY = Number.MIN_VALUE;
    breaksList.forEach(b => {
      prevY = Math.max(b.prevY, prevY);
      y = Math.min(b.y, y);
    });
    return {
      prevPage: page,
      prevY: prevY,
      y: y
    };
  }
  _findSameRowPageBreakByRowSpanData(breaksBySpan, page, rowIndex) {
    if (!breaksBySpan) {
      return null;
    }
    return breaksBySpan.find(desc => desc.prevPage === page && rowIndex === desc.rowIndexOfSpanEnd);
  }
  _updatePageBreaksData(pageBreaks, tableNode, rowIndex) {
    Object.keys(tableNode._bottomByPage).forEach(p => {
      const page = Number(p);
      const pageBreak = this._getPageBreak(pageBreaks, page);
      if (pageBreak) {
        pageBreak.prevY = Math.max(pageBreak.prevY, tableNode._bottomByPage[page]);
      }
      if (tableNode._breaksBySpan && tableNode._breaksBySpan.length > 0) {
        const breaksBySpanList = tableNode._breaksBySpan.filter(pb => pb.prevPage === page && rowIndex <= pb.rowIndexOfSpanEnd);
        if (breaksBySpanList && breaksBySpanList.length > 0) {
          breaksBySpanList.forEach(b => {
            b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
          });
        }
      }
    });
  }

  /**
  * Resolves the Y-coordinates for a target object by comparing two break points.
  *
  * @param {object} break1 - The first break point with `prevY` and `y` properties.
  * @param {object} break2 - The second break point with `prevY` and `y` properties.
  * @param {object} target - The target object to be updated with resolved Y-coordinates.
  * @property {number} target.prevY - Updated to the maximum `prevY` value between `break1` and `break2`.
  * @property {number} target.y - Updated to the minimum `y` value between `break1` and `break2`.
  */
  _resolveBreakY(break1, break2, target) {
    target.prevY = Math.max(break1.prevY, break2.prevY);
    target.y = Math.min(break1.y, break2.y);
  }
  _storePageBreakData(data, startsRowSpan, pageBreaks, tableNode) {
    if (!startsRowSpan) {
      let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
      let pageDescBySpan = this._getPageBreakListBySpan(tableNode, data.prevPage, data.rowIndex);
      if (!pageDesc) {
        pageDesc = {
          ...data
        };
        pageBreaks.push(pageDesc);
      }
      if (pageDescBySpan) {
        this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
      }
      this._resolveBreakY(pageDesc, data, pageDesc);
    } else {
      const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
      let pageDescBySpan = this._findSameRowPageBreakByRowSpanData(breaksBySpan, data.prevPage, data.rowIndex);
      if (!pageDescBySpan) {
        pageDescBySpan = {
          ...data,
          rowIndexOfSpanEnd: data.rowIndex + data.rowSpan - 1
        };
        if (!tableNode._breaksBySpan) {
          tableNode._breaksBySpan = [];
        }
        tableNode._breaksBySpan.push(pageDescBySpan);
      }
      pageDescBySpan.prevY = Math.max(pageDescBySpan.prevY, data.prevY);
      pageDescBySpan.y = Math.min(pageDescBySpan.y, data.y);
      let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
      if (pageDesc) {
        this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
      }
    }
  }
  /**
  * Calculates the left offset for a column based on the specified gap values.
  *
  * @param {number} i - The index of the column for which the offset is being calculated.
  * @param {Array<number>} gaps - An array of gap values for each column.
  * @returns {number} The left offset for the column. Returns `gaps[i]` if it exists, otherwise `0`.
  */
  _colLeftOffset(i, gaps) {
    if (gaps && gaps.length > i) {
      return gaps[i];
    }
    return 0;
  }

  /**
  * Retrieves the ending cell for a row span in case it exists in a specified table column.
  *
  * @param {Array<Array<object>>} tableBody - The table body, represented as a 2D array of cell objects.
  * @param {number} rowIndex - The index of the starting row for the row span.
  * @param {object} column - The column object containing row span information.
  * @param {number} columnIndex - The index of the column within the row.
  * @returns {object|null} The cell at the end of the row span if it exists; otherwise, `null`.
  * @throws {Error} If the row span extends beyond the total row count.
  */
  _getRowSpanEndingCell(tableBody, rowIndex, column, columnIndex) {
    if (column.rowSpan && column.rowSpan > 1) {
      let endingRow = rowIndex + column.rowSpan - 1;
      if (endingRow >= tableBody.length) {
        throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
      }
      return tableBody[endingRow][columnIndex];
    }
    return null;
  }
  processRow({
    marginX = [0, 0],
    dontBreakRows = false,
    rowsWithoutPageBreak = 0,
    cells,
    widths,
    gaps,
    tableNode,
    tableBody,
    rowIndex,
    height,
    snakingColumns = false
  }) {
    const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
    let pageBreaks = [];
    let pageBreaksByRowSpan = [];
    let positions = [];
    let willBreakByHeight = false;
    let verticalAlignmentCells = {};
    widths = widths || cells;

    // Check if row should break by height
    if (!isUnbreakableRow && height > this.writer.context().availableHeight) {
      willBreakByHeight = true;
    }

    // Use the marginX if we are in a top level table/column (not nested)
    const marginXParent = this.nestedLevel === 1 ? marginX : null;
    const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
    // Pass column gap and widths to context snapshot for snaking columns
    // to advance correctly and reset to first-column width on new pages.
    const columnGapForGroup = gaps && gaps.length > 1 ? gaps[1] : 0;
    const columnWidthsForContext = widths.map(w => w._calcWidth);
    this.writer.context().beginColumnGroup(marginXParent, _bottomByPage, snakingColumns, columnGapForGroup, columnWidthsForContext);

    // IMPORTANT: We iterate ALL columns even when snakingColumns is enabled.
    // This is intentional — beginColumn() must be called for each column to set up
    // proper geometry (widths, offsets) and rowspan/colspan tracking. The
    // completeColumnGroup() call at the end depends on this bookkeeping to compute
    // heights correctly. Content processing is skipped for columns > 0 via
    // skipForSnaking below, but the column structure must still be established.
    for (let i = 0, l = cells.length; i < l; i++) {
      let cell = cells[i];
      let cellIndexBegin = i;

      // Page change handler
      const storePageBreakClosure = data => {
        const startsRowSpan = cell.rowSpan && cell.rowSpan > 1;
        if (startsRowSpan) {
          data.rowSpan = cell.rowSpan;
        }
        data.rowIndex = rowIndex;
        this._storePageBreakData(data, startsRowSpan, pageBreaks, tableNode);
      };
      this.writer.addListener('pageChanged', storePageBreakClosure);
      let width = widths[i]._calcWidth;
      let leftOffset = this._colLeftOffset(i, gaps);
      // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
      let startingSpanCell = this._findStartingRowSpanCell(cells, i);
      if (cell.colSpan && cell.colSpan > 1) {
        for (let j = 1; j < cell.colSpan; j++) {
          width += widths[++i]._calcWidth + gaps[i];
        }
      }

      // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
      const rowSpanRightEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
      const rowSpanLeftEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, cellIndexBegin);
      if (rowSpanRightEndingCell) {
        // We store a reference of the ending cell in the first cell of the rowspan
        cell._endingCell = rowSpanRightEndingCell;
        cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
        cell._endingCell._startingRowSpanPage = cell._startingRowSpanPage;
      }
      if (rowSpanLeftEndingCell) {
        // We store a reference of the left ending cell in the first cell of the rowspan
        cell._leftEndingCell = rowSpanLeftEndingCell;
        cell._leftEndingCell._startingRowSpanY = cell._startingRowSpanY;
        cell._leftEndingCell._startingRowSpanPage = cell._startingRowSpanPage;
      }

      // If we are after a cell that started a rowspan
      let endOfRowSpanCell = null;
      if (startingSpanCell && startingSpanCell._endingCell) {
        // Reference to the last cell of the rowspan
        endOfRowSpanCell = startingSpanCell._endingCell;
        // Store if we are in an unbreakable block when we save the context and the originalX
        if (this.writer.transactionLevel > 0) {
          endOfRowSpanCell._isUnbreakableContext = true;
          endOfRowSpanCell._originalXOffset = this.writer.originalX;
        }
      }

      // We pass the endingSpanCell reference to store the context just after processing rowspan cell
      this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);

      // When snaking, only process content from the first column (i === 0).
      // Content overflows into subsequent columns via moveToNextColumn().
      // We skip content processing here but NOT the beginColumn() call above —
      // the column geometry setup is still needed for proper layout bookkeeping.
      const skipForSnaking = snakingColumns && i > 0;
      if (!cell._span && !skipForSnaking) {
        this.processNode(cell, true);
        this.writer.context().updateBottomByPage();
        if (cell.verticalAlignment) {
          verticalAlignmentCells[cellIndexBegin] = this.verticalAlignmentItemStack.length - 1;
        }
        addAll(positions, cell.positions);
      } else if (cell._columnEndingContext) {
        let discountY = 0;
        if (dontBreakRows) {
          // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
          const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
          const startsOnCurrentPage = typeof cell._startingRowSpanPage === 'number' && cell._startingRowSpanPage === ctxBeforeRowSpanLastRow.page;
          if (startsOnCurrentPage && typeof cell._startingRowSpanY === 'number') {
            discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
          }

          // Do not increase Y by applying a negative discount.
          discountY = Math.max(0, discountY);
        }
        let originalXOffset = 0;
        // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
        // We have to sum the originalX (X before starting unbreakable block) to X
        if (cell._isUnbreakableContext && !this.writer.transactionLevel) {
          originalXOffset = cell._originalXOffset;
        }
        // row-span ending
        // Recover the context after processing the rowspanned cell
        this.writer.context().markEnding(cell, originalXOffset, discountY);
      }
      this.writer.removeListener('pageChanged', storePageBreakClosure);
    }

    // Check if last cell is part of a span
    let endingSpanCell = null;
    const lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
    if (lastColumn) {
      // Previous column cell has a rowspan
      if (lastColumn._endingCell) {
        endingSpanCell = lastColumn._endingCell;
        // Previous column cell is part of a span
      } else if (lastColumn._span === true) {
        // We get the cell that started the span where we set a reference to the ending cell
        const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
        if (startingSpanCell) {
          // Context will be stored here (ending cell)
          endingSpanCell = startingSpanCell._endingCell;
          // Store if we are in an unbreakable block when we save the context and the originalX
          if (this.writer.transactionLevel > 0) {
            endingSpanCell._isUnbreakableContext = true;
            endingSpanCell._originalXOffset = this.writer.originalX;
          }
        }
      }
    }

    // If content did not break page, check if we should break by height
    if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
      this.writer.context().moveDown(this.writer.context().availableHeight);
      if (snakingColumns) {
        this.snakingAwarePageBreak();
      } else {
        this.writer.moveToNextPage();
      }
    }
    const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
    if (tableNode) {
      tableNode._bottomByPage = bottomByPage;
      // If there are page breaks in this row, update data with prevY of last cell
      this._updatePageBreaksData(pageBreaks, tableNode, rowIndex);
    }
    let rowHeight = this.writer.context().height;
    for (let i = 0, l = cells.length; i < l; i++) {
      let cell = cells[i];
      if (!cell._span && cell.verticalAlignment) {
        let itemBegin = this.verticalAlignmentItemStack[verticalAlignmentCells[i]].begin.item;
        itemBegin.viewHeight = rowHeight;
        itemBegin.nodeHeight = cell.__height;
        itemBegin.cell = cell;
        itemBegin.bottomY = this.writer.context().y;
        itemBegin.isCellContentMultiPage = !itemBegin.cell.positions.every(item => item.pageNumber === itemBegin.cell.positions[0].pageNumber);
        itemBegin.getViewHeight = function () {
          if (this.cell._willBreak) {
            return this.cell._bottomY - this.cell._rowTopPageY;
          }
          if (this.cell.rowSpan && this.cell.rowSpan > 1) {
            if (dontBreakRows) {
              let rowTopPageY = this.cell._leftEndingCell._startingRowSpanY + this.cell._leftEndingCell._rowTopPageYPadding;
              return this.cell._leftEndingCell._rowTopPageY - rowTopPageY + this.cell._leftEndingCell._bottomY;
            } else {
              if (this.cell.positions[0].pageNumber !== this.cell._leftEndingCell._lastPageNumber) {
                return this.bottomY - this.cell._leftEndingCell._bottomY;
              }
              return this.viewHeight + this.cell._leftEndingCell._bottomY - this.bottomY;
            }
          }
          return this.viewHeight;
        };
        itemBegin.getNodeHeight = function () {
          return this.nodeHeight;
        };
        let itemEnd = this.verticalAlignmentItemStack[verticalAlignmentCells[i]].end.item;
        itemEnd.isCellContentMultiPage = itemBegin.isCellContentMultiPage;
      }
    }
    return {
      pageBreaksBySpan: pageBreaksByRowSpan,
      pageBreaks: pageBreaks,
      positions: positions
    };
  }

  // lists
  processList(orderedList, node) {
    const addMarkerToFirstLeaf = line => {
      // I'm not very happy with the way list processing is implemented
      // (both code and algorithm should be rethinked)
      if (nextMarker) {
        let marker = nextMarker;
        nextMarker = null;
        if (marker.canvas) {
          let vector = marker.canvas[0];
          (0, _tools.offsetVector)(vector, -marker._minWidth, 0);
          this.writer.addVector(vector);
        } else if (marker._inlines) {
          let markerLine = new _Line.default(this.pageSize.width);
          markerLine.addInline(marker._inlines[0]);
          markerLine.x = -marker._minWidth;
          markerLine.y = line.getAscenderHeight() - markerLine.getAscenderHeight();
          this.writer.addLine(markerLine, true);
        }
      }
    };
    let items = orderedList ? node.ol : node.ul;
    let gapSize = node._gapSize;
    this.writer.context().addMargin(gapSize.width);
    let nextMarker;
    this.writer.addListener('lineAdded', addMarkerToFirstLeaf);
    items.forEach(item => {
      nextMarker = item.listMarker;
      this.processNode(item);
      addAll(node.positions, item.positions);
    });
    this.writer.removeListener('lineAdded', addMarkerToFirstLeaf);
    this.writer.context().addMargin(-gapSize.width);
  }

  // tables
  processTable(tableNode) {
    this.nestedLevel++;
    let processor = new _TableProcessor.default(tableNode);
    processor.beginTable(this.writer);
    let rowHeights = tableNode.table.heights;
    let lastRowHeight = 0;
    for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
      // Between table rows: check if we should move to the next snaking column.
      // This must happen AFTER the previous row's endRow (borders drawn) and
      // BEFORE this row's beginRow. At this point, the table row column group
      // has been completed, so canMoveToNextColumn() works correctly.
      if (i > 0 && this.writer.context().inSnakingColumns()) {
        // Estimate minimum space for next row: use last row's height as heuristic,
        // or fall back to a minimum of padding + line height + border
        let minRowHeight = lastRowHeight > 0 ? lastRowHeight : processor.rowPaddingTop + 14 + processor.rowPaddingBottom + processor.bottomLineWidth + processor.topLineWidth;
        if (this.writer.context().availableHeight < minRowHeight) {
          this.snakingAwarePageBreak();

          // Skip border when headerRows present (header repeat includes it)
          if (processor.layout.hLineWhenBroken !== false && !processor.headerRows) {
            processor.drawHorizontalLine(i, this.writer);
          }
        }
      }
      let rowYBefore = this.writer.context().y;

      // if dontBreakRows and row starts a rowspan
      // we store the 'y' of the beginning of each rowSpan
      if (processor.dontBreakRows) {
        tableNode.table.body[i].forEach(cell => {
          if (cell.rowSpan && cell.rowSpan > 1) {
            cell._startingRowSpanY = this.writer.context().y;
            cell._startingRowSpanPage = this.writer.context().page;
          }
        });
      }
      processor.beginRow(i, this.writer);
      let height;
      if (typeof rowHeights === 'function') {
        height = rowHeights(i);
      } else if (Array.isArray(rowHeights)) {
        height = rowHeights[i];
      } else {
        height = rowHeights;
      }
      if (height === 'auto') {
        height = undefined;
      }
      const pageBeforeProcessing = this.writer.context().page;
      let result = this.processRow({
        marginX: tableNode._margin ? [tableNode._margin[0], tableNode._margin[2]] : [0, 0],
        dontBreakRows: processor.dontBreakRows,
        rowsWithoutPageBreak: processor.rowsWithoutPageBreak,
        cells: tableNode.table.body[i],
        widths: tableNode.table.widths,
        gaps: tableNode._offsets.offsets,
        tableBody: tableNode.table.body,
        tableNode,
        rowIndex: i,
        height
      });
      addAll(tableNode.positions, result.positions);
      if (!result.pageBreaks || result.pageBreaks.length === 0) {
        const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
        const breakBySpanData = this._findSameRowPageBreakByRowSpanData(breaksBySpan, pageBeforeProcessing, i);
        if (breakBySpanData) {
          const finalBreakBySpanData = this._getPageBreakListBySpan(tableNode, breakBySpanData.prevPage, i);
          result.pageBreaks.push(finalBreakBySpanData);
        }
      }
      processor.endRow(i, this.writer, result.pageBreaks);

      // Track the height of the completed row for the next row's estimate
      let rowYAfter = this.writer.context().y;
      if (this.writer.context().page === pageBeforeProcessing) {
        lastRowHeight = rowYAfter - rowYBefore;
      }
    }
    processor.endTable(this.writer);
    this.nestedLevel--;
    if (this.nestedLevel === 0) {
      this.writer.context().resetMarginXTopParent();
    }
  }

  // leafs (texts)
  processLeaf(node) {
    let line = this.buildNextLine(node);
    if (line && (node.tocItem || node.id)) {
      line._node = node;
    }
    let currentHeight = line ? line.getHeight() : 0;
    let maxHeight = node.maxHeight || -1;
    if (line) {
      let nodeId = (0, _node.getNodeId)(node);
      if (nodeId) {
        line.id = nodeId;
      }
    }
    if (node.outline) {
      line._outline = {
        id: node.id,
        parentId: node.outlineParentId,
        text: node.outlineText || node.text,
        expanded: node.outlineExpanded || false
      };
    } else if (Array.isArray(node.text)) {
      for (let i = 0, l = node.text.length; i < l; i++) {
        let item = node.text[i];
        if (item.outline) {
          line._outline = {
            id: item.id,
            parentId: item.outlineParentId,
            text: item.outlineText || item.text,
            expanded: item.outlineExpanded || false
          };
        }
      }
    }
    if (node._tocItemRef) {
      line._pageNodeRef = node._tocItemRef;
    }
    if (node._pageRef) {
      line._pageNodeRef = node._pageRef._nodeRef;
    }
    if (line && line.inlines && Array.isArray(line.inlines)) {
      for (let i = 0, l = line.inlines.length; i < l; i++) {
        if (line.inlines[i]._tocItemRef) {
          line.inlines[i]._pageNodeRef = line.inlines[i]._tocItemRef;
        }
        if (line.inlines[i]._pageRef) {
          line.inlines[i]._pageNodeRef = line.inlines[i]._pageRef._nodeRef;
        }
      }
    }
    while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
      // Check if line fits vertically in current context
      if (line.getHeight() > this.writer.context().availableHeight && this.writer.context().y > this.writer.context().pageMargins.top) {
        // Line doesn't fit, forced move to next page/column
        // Only do snaking-specific break if we're in snaking columns AND NOT inside
        // a nested non-snaking group (like a table row). Table cells should use
        // standard page breaks — column breaks happen between table rows instead.
        if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
          this.snakingAwarePageBreak(node.pageOrientation);

          // Always reflow text after a snaking break (column or page).
          // This ensures text adapts to the new column width, whether it's narrower or wider.
          if (line.inlines && line.inlines.length > 0) {
            node._inlines.unshift(...line.inlines);
          }
          // Rebuild line with new width
          line = this.buildNextLine(node);
          continue;
        } else {
          this.writer.moveToNextPage(node.pageOrientation);
        }
      }
      let positions = this.writer.addLine(line);
      node.positions.push(positions);
      line = this.buildNextLine(node);
      if (line) {
        currentHeight += line.getHeight();
      }
    }
  }
  processToc(node) {
    if (!node.toc._table && node.toc.hideEmpty === true) {
      return;
    }
    if (node.toc.title) {
      this.processNode(node.toc.title);
    }
    if (node.toc._table) {
      this.processNode(node.toc._table);
    }
  }
  buildNextLine(textNode) {
    function cloneInline(inline) {
      let newInline = inline.constructor();
      for (let key in inline) {
        newInline[key] = inline[key];
      }
      return newInline;
    }
    function findMaxFitLength(text, maxWidth, measureFn) {
      let low = 1;
      let high = text.length;
      let bestFit = 1;
      while (low <= high) {
        const mid = Math.floor((low + high) / 2);
        const part = text.substring(0, mid);
        const width = measureFn(part);
        if (width <= maxWidth) {
          bestFit = mid;
          low = mid + 1;
        } else {
          high = mid - 1;
        }
      }
      return bestFit;
    }
    if (!textNode._inlines || textNode._inlines.length === 0) {
      return null;
    }
    let line = new _Line.default(this.writer.context().availableWidth);
    const textInlines = new _TextInlines.default(null);
    let isForceContinue = false;
    while (textNode._inlines && textNode._inlines.length > 0 && (line.hasEnoughSpaceForInline(textNode._inlines[0], textNode._inlines.slice(1)) || isForceContinue)) {
      let isHardWrap = false;
      let inline = textNode._inlines.shift();
      if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
        let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), txt => textInlines.widthOfText(txt, inline));
        if (maxChars < inline.text.length) {
          let newInline = cloneInline(inline);
          newInline.text = inline.text.substr(maxChars);
          inline.text = inline.text.substr(0, maxChars);
          newInline.width = textInlines.widthOfText(newInline.text, newInline);
          inline.width = textInlines.widthOfText(inline.text, inline);
          textNode._inlines.unshift(newInline);
          isHardWrap = true;
        }
      }
      line.addInline(inline);
      isForceContinue = inline.noNewLine && !isHardWrap;
    }
    line.lastLineInParagraph = textNode._inlines.length === 0;
    return line;
  }

  // images
  processImage(node) {
    let position = this.writer.addImage(node);
    node.positions.push(position);
  }
  processCanvas(node) {
    let positions = this.writer.addCanvas(node);
    addAll(node.positions, positions);
  }
  processSVG(node) {
    let position = this.writer.addSVG(node);
    node.positions.push(position);
  }
  processQr(node) {
    let position = this.writer.addQr(node);
    node.positions.push(position);
  }
  processAttachment(node) {
    let position = this.writer.addAttachment(node);
    node.positions.push(position);
  }
}
function decorateNode(node) {
  let x = node.x;
  let y = node.y;
  node.positions = [];
  if (Array.isArray(node.canvas)) {
    node.canvas.forEach(vector => {
      let x = vector.x;
      let y = vector.y;
      let x1 = vector.x1;
      let y1 = vector.y1;
      let x2 = vector.x2;
      let y2 = vector.y2;
      vector.resetXY = () => {
        vector.x = x;
        vector.y = y;
        vector.x1 = x1;
        vector.y1 = y1;
        vector.x2 = x2;
        vector.y2 = y2;
      };
    });
  }
  node.resetXY = () => {
    node.x = x;
    node.y = y;
    if (Array.isArray(node.canvas)) {
      node.canvas.forEach(vector => {
        vector.resetXY();
      });
    }
  };
}
var _default = exports.default = LayoutBuilder;