// Copyright 2017-2022 The MathJax Consortium
// SPDX-License-Identifier: Apache-2.0
// This file is a Go translation and modification of MathJax 3.2.2.

package svg

import (
	
	
	
	

	
	
)

// tableDimensions is the source-shaped subset of CommonMtable.TableData used
// by the SVG wrapper: row heights and depths, natural (unlabelled) row sizes,
// column widths, and the label-column width.
type tableDimensions struct {
	H, D   []float64
	W      []float64
	NH, ND []float64
	L      float64
}

type tableColumnMeasure struct {
	fixed bool
	value float64
}

// tableLayout is deliberately ephemeral. MathJax caches this information on
// its wrapper subclasses, but keeping it here avoids adding table-only state to
// the shared wrapper while producing the same deterministic geometry.
type tableLayout struct {
	table *wrapper
	rows  []*wrapper
	// container is the first non-mrow, non-NotParent ancestor.  containerI is
	// the position of the transparent ancestor that contains this table.  This
	// is CommonMtable.findContainer(), retained here because percentage widths
	// and labeled subtables are relative to that wrapper rather than the
	// table's immediate (often inferred-mrow) parent.
	container  *wrapper
	containerI int

	numRows, numCols int
	hasLabels        bool
	isTop            bool

	frame      bool
	frameLine  float64
	frameSpace [2]float64

	columnSpace []float64
	rowSpace    []float64
	columnLines []float64
	rowLines    []float64
	columnStyle []string
	rowStyle    []string

	columns  []tableColumnMeasure
	computed []float64
	data     tableDimensions

	width, height float64
	h, d          float64
	left, right   float64
	pWidth        float64
	resolved      bool
}

// tableState is named separately at the wrapper boundary to make the
// persistent per-render lifetime explicit while retaining tableLayout in the
// source-shaped geometry helpers and tests.
type tableState = tableLayout

var tableDefaults = map[string]string{
	"align":           "axis",
	"rowalign":        "baseline",
	"columnalign":     "center",
	"columnwidth":     "auto",
	"width":           "auto",
	"rowspacing":      "1ex",
	"columnspacing":   ".8em",
	"rowlines":        "none",
	"columnlines":     "none",
	"frame":           "none",
	"framespacing":    "0.4em 0.5ex",
	"side":            "right",
	"minlabelspacing": "0.8em",
}

func tableString( string,  *wrapper) string {
	return stringAttribute(.node, , tableDefaults[])
}

func tableBool( *wrapper,  string,  bool) bool {
	return boolAttributeDefault(.node, , )
}

func tableFields( *wrapper,  string) []string {
	 := strings.TrimSpace(tableString(, ))
	if  == "" {
		return nil
	}
	return strings.Fields()
}

func repeatTableField( []string,  int,  string) []string {
	// CommonMtable.getColumnAttributes/getRowAttributes trims with splice(n).
	// Empty tables request -1, which retains all but the last authored value.
	if  < 0 {
		return append([]string{}, [:max(0, len()+)]...)
	}
	if  == 0 {
		return []string{}
	}
	if len() == 0 {
		 = []string{}
	}
	 := make([]string, )
	for  := range  {
		[] = [min(, len()-1)]
	}
	return 
}

func tableLength( *wrapper,  string,  ...float64) float64 {
	 := 1.0
	if len() != 0 {
		 = [0]
	}
	return layout.Length2Em(, , .bbox.Scale, .renderer.pxPerEm)
}

func tableLengths( *wrapper,  []string) []float64 {
	 := make([]float64, len())
	for ,  := range  {
		[] = tableLength(, )
	}
	return 
}

func tableSum( ...[]float64) float64 {
	 := 0.0
	for ,  := range  {
		for ,  := range  {
			 += 
		}
	}
	return 
}

func tableMax( []float64) float64 {
	 := 0.0
	for ,  := range  {
		if  >  {
			 = 
		}
	}
	return 
}

func tableIsPercent( string) bool {
	 = strings.TrimSpace()
	if !strings.HasSuffix(, "%") {
		return false
	}
	,  := strconv.ParseFloat(strings.TrimSpace(strings.TrimSuffix(, "%")), 64)
	return  == nil
}

func tableRows( *wrapper) []*wrapper {
	return append([]*wrapper(nil), .children...)
}

func tableRowCells( *wrapper) []*wrapper {
	if  == nil {
		return nil
	}
	 := 0
	if .node.Kind == "mlabeledtr" && len(.children) != 0 {
		 = 1
	}
	return .children[:]
}

func tableLabelCell( *wrapper) *wrapper {
	if  != nil && .node.Kind == "mlabeledtr" && len(.children) != 0 {
		return .children[0]
	}
	return nil
}

func initializeTablePWidth( *wrapper) {
	if  == nil || .node == nil || .node.Kind != "mtable" {
		return
	}
	for ,  := range tableRows() {
		if tableLabelCell() != nil {
			.bbox.PWidth = layout.FullWidth
			return
		}
	}
	if  := tableString("width", ); tableIsPercent() {
		.bbox.PWidth = 
	}
}

func ( *wrapper) () *tableState {
	if .table == nil {
		.table = newTableLayout()
	}
	return .table
}

func newTableLayout( *wrapper) *tableLayout {
	,  := findTableContainer()
	 := &tableLayout{
		table: , rows: tableRows(),
		container: , containerI: ,
		isTop:  == nil || (.node.Kind == "math" && .parent == nil),
	}
	.numRows = len(.rows)
	for ,  := range .rows {
		if  := len(tableRowCells());  > .numCols {
			.numCols = 
		}
		if tableLabelCell() != nil {
			.hasLabels = true
		}
	}

	 := tableString("frame", )
	.frame =  != "none"
	// An explicitly empty frame is the AMS multline trick for retaining
	// framespacing without drawing (or reserving thickness for) a frame line.
	if .frame &&  != "" {
		.frameLine = .07
	}
	 := repeatTableField(tableFields(, "framespacing"), 2, "0")
	if .frame {
		 := tableLengths(, )
		copy(.frameSpace[:], )
	}

	.columnSpace = tableLengths(, repeatTableField(tableFields(, "columnspacing"), .numCols-1, ".8em"))
	.rowSpace = tableLengths(, repeatTableField(tableFields(, "rowspacing"), .numRows-1, "1ex"))
	.columnStyle = repeatTableField(tableFields(, "columnlines"), .numCols-1, "none")
	.rowStyle = repeatTableField(tableFields(, "rowlines"), .numRows-1, "none")
	.columnLines = tableLineWidths(.columnStyle)
	.rowLines = tableLineWidths(.rowStyle)

	// CommonMtable requests cell measurements lazily. Column specifications
	// and equal rows can cache them before stretching; otherwise, auto tables
	// first measure the stretched cells when computing their final widths.
	.columns = .columnMeasures()
	.stretchRows()
	.stretchColumns()
	.computed = .computedWidths()
	.measureTable()
	return 
}

// findTableContainer ports CommonMtable.findContainer. Inferred and explicit
// mrows, plus wrappers marked NotParent, are transparent for determining the
// table's width container.
func findTableContainer( *wrapper) (*wrapper, int) {
	,  := , .parent
	for  != nil && (.node.Kind == "mrow" || .node.Flags.NotParent) {
		 = 
		 = .parent
	}
	if  == nil {
		return nil, 0
	}
	for ,  := range .children {
		if  ==  {
			return , 
		}
	}
	return , 0
}

// resolvePercentageWidth is CommonMtable.setChildPWidths().  A nested table's
// natural bbox stays unchanged; pWidth records the resolved width used for
// columns and painting.  Top tables replace their bbox width with pWidth.
func ( *tableLayout) ( float64) {
	 := tableString("width", .table)
	if !tableIsPercent() || .resolved {
		return
	}
	 := .width
	 := math.Max(, .left+.width+.right)
	 := math.Max(.width, tableLength(.table, , ))
	if tableBool(.table, "data-width-includes-label", false) {
		 -= .left + .right
	}

	var  []string
	if tableBool(.table, "equalcolumns", false) {
		 = make([]string, .numCols)
		 := layout.Percent(1 / float64(max(1, .numCols)))
		for  := range  {
			[] = 
		}
	} else {
		 = repeatTableField(tableFields(.table, "columnwidth"), .numCols, "auto")
	}
	.columns = .fixedColumnMeasures(, )
	.computed = .computedWidths()
	.measureTable()
	.pWidth = .width
	if !.isTop {
		.width = 
	} else {
		.table.bbox.W = .pWidth
	}
	.resolved = true
	if !.hasLabels {
		.table.bbox.PWidth = ""
		if .container != nil {
			.container.bbox.PWidth = ""
		}
	}
	if .pWidth !=  && .table.parent != nil {
		.table.parent.invalidateBBox()
	}
}

// resolvedTableLayout supplies the late percentage-width pass for nested
// tables.  By SVG emission time every container has its natural bbox, so the
// source wrapper's getWrapWidth() contract can be reproduced without a
// browser measurement or shared mutable state.
func resolvedTableLayout( *wrapper) *tableLayout {
	 := .tableState()
	if !.isTop && tableIsPercent(tableString("width", )) {
		.resolvePercentageWidth(.containerWrapWidth(make(map[*wrapper]bool)))
	}
	return 
}

func tableCellChild( *wrapper) *wrapper {
	if  == nil || len(.children) == 0 {
		return nil
	}
	return .children[0]
}

// tableCoreMO is the wrapper equivalent of MmlNode.coreMO().  CommonWrapper's
// canStretch() delegates through embellished wrappers, while the actual
// getStretchedVariant() call is made on this core mo.
func tableCoreMO( *wrapper) *wrapper {
	for  != nil && .node.Flags.Embellished && .node.Kind != "mo" {
		 := .node.Flags.CoreIndex
		if  < 0 ||  >= len(.children) {
			return nil
		}
		 = .children[]
	}
	if  == nil || .node.Kind != "mo" {
		return nil
	}
	return 
}

func tableStretchCore( *wrapper,  font.Direction) *wrapper {
	// CommonMtable asks the cell's outer wrapper whether it stretches.  That
	// delegates through embellished movers/rows and deliberately permits a
	// second getStretchedVariant call: AMS-CD first sizes an arrow to its long
	// label, then the table column pass clamps it back to its 2.75em minsize.
	if  == nil || !.canStretch() {
		return nil
	}
	return tableCoreMO()
}

func invalidateStretchParents( *wrapper) {
	for  := .parent;  != nil;  = .parent {
		.bboxComputed = false
	}
}

func ( *tableLayout) () {
	 := tableBool(.table, "equalrows", false)
	 := 0.0
	if  {
		 = .equalRowHeight()
	}
	for ,  := range .rows {
		var  []float64
		if  {
			,  := .data.H[], .data.D[]
			 = []float64{( +  - ) / 2, ( -  + ) / 2}
		}
		.stretchRow(, )
	}
}

func ( *tableLayout) ( *wrapper,  []float64) {
	 := tableRowCells()
	 := make(map[*wrapper]*wrapper)
	for ,  := range  {
		 := tableCellChild()
		if  := tableStretchCore(, font.DirectionVertical);  != nil {
			[] = 
		}
	}
	if len() == 0 || len(.children) <= 1 {
		return
	}
	if  == nil {
		,  := 0.0, 0.0
		 := len() > 1 && len() == len(.children)
		for ,  := range  {
			 := tableCellChild()
			if  == nil {
				continue
			}
			,  := []
			if ! &&  {
				continue
			}
			 := .outerBBox()
			 = math.Max(, .H*.RScale)
			 = math.Max(, .D*.RScale)
		}
		 = []float64{, }
	}
	for ,  := range  {
		.getStretchedVariant(, false)
		invalidateStretchParents()
	}
}

func ( *tableLayout) () {
	for  := 0;  < .numCols; ++ {
		var  *float64
		if  < len(.columns) && .columns[].fixed {
			 := .columns[].value
			 = &
		}
		.stretchColumn(, )
	}
}

func ( *tableLayout) ( int,  *float64) {
	 := make(map[*wrapper]*wrapper)
	for ,  := range .rows {
		 := tableRowCells()
		if  >= len() {
			continue
		}
		 := tableCellChild([])
		if  := tableStretchCore(, font.DirectionHorizontal);  != nil {
			[] = 
		}
	}
	if len() == 0 || len(.rows) <= 1 {
		return
	}
	 := 0.0
	if  != nil {
		 = *
	} else {
		 := len() > 1 && len() == len(.rows)
		for ,  := range .rows {
			 := tableRowCells()
			if  >= len() {
				continue
			}
			 := tableCellChild([])
			if  == nil {
				continue
			}
			,  := []
			if ! &&  {
				continue
			}
			 := .outerBBox()
			 = math.Max(, .W*.RScale)
		}
	}
	for ,  := range  {
		.getStretchedVariant([]float64{}, false)
		invalidateStretchParents()
	}
}

func tableLineWidths( []string) []float64 {
	 := make([]float64, len())
	for ,  := range  {
		if  != "none" {
			[] = .07
		}
	}
	return 
}

func ( *tableLayout) (,  int) string {
	 := repeatTableField(tableFields(.table, "rowalign"), .numRows, "baseline")
	 := []
	if  >= 0 &&  < len(.rows) {
		if  := stringAttributeExplicit(.rows[].node, "rowalign");  != "" {
			 = 
		}
	}
	return 
}

func ( *tableLayout) ( *wrapper,  *wrapper,  int) string {
	 := repeatTableField(tableFields(.table, "columnalign"), .numCols, "center")
	 := "center"
	if  >= 0 &&  < len() {
		 = []
	}
	if  != nil {
		 := strings.Fields(stringAttributeExplicit(.node, "columnalign"))
		if len() != 0 {
			 = [min(, len()-1)]
		}
	}
	if  != nil {
		if  := stringAttributeExplicit(.node, "columnalign");  != "" {
			 = 
		}
	}
	return 
}

func ( *tableLayout) ( *wrapper,  *wrapper,  int) string {
	 := .rowAlign(, )
	if  != nil {
		if  := stringAttributeExplicit(.node, "rowalign");  != "" {
			 = 
		}
	}
	return 
}

func ( *tableLayout) () bool {
	if ,  := .table.node.Property("useHeight");  {
		return truthy()
	}
	return true
}

func ( *tableLayout) ( *wrapper, ,  int,  string, , ,  []float64,  float64) float64 {
	if  == nil {
		return 
	}
	 := .outerBBox()
	 := 1.0
	if .parent != nil {
		 = .parent.bbox.RScale
	}
	, ,  := .H*, .D*, .W*
	if .useHeight() {
		 = math.Max(, .75)
		 = math.Max(, .25)
	}
	if  := stringAttributeExplicit(.node, "rowalign");  != "" {
		 = 
	}
	 := 0.0
	if  != "baseline" &&  != "axis" {
		 =  + 
		,  = 0, 0
	}
	[] = math.Max([], )
	[] = math.Max([], )
	 = math.Max(, )
	if  >= 0 &&  < len() {
		[] = math.Max([], )
	}
	return 
}

func extendTableHD( int, ,  []float64,  float64) {
	 := ( - ([] + [])) / 2
	if  < .00001 {
		return
	}
	[] += 
	[] += 
}

func ( *tableLayout) () tableDimensions {
	 := tableDimensions{
		H: make([]float64, .numRows), D: make([]float64, .numRows),
		W: make([]float64, .numCols), NH: make([]float64, .numRows), ND: make([]float64, .numRows),
	}
	 := []float64{0}
	for ,  := range .rows {
		 := 0.0
		 := .rowAlign(, )
		for ,  := range tableRowCells() {
			 = .updateHDW(, , , , .H, .D, .W, )
		}
		.NH[], .ND[] = .H[], .D[]
		if  := tableLabelCell();  != nil {
			 = .updateHDW(, 0, , , .H, .D, , )
		}
		extendTableHD(, .H, .D, )
		extendTableHD(, .NH, .ND, )
	}
	.L = [0]
	return 
}

// tableData follows CommonMtable.getTableData: the first request fixes the
// table's measurements. Later stretching must not invalidate this snapshot.
func ( *tableLayout) () *tableDimensions {
	if .data.W == nil {
		.data = .measureCells()
	}
	return &.data
}

func ( *tableLayout) () []tableColumnMeasure {
	 := tableString("width", .table)
	if tableBool(.table, "equalcolumns", false) {
		return .equalColumnMeasures()
	}
	 := repeatTableField(tableFields(.table, "columnwidth"), .numCols, "auto")
	switch {
	case  == "auto":
		return .autoColumnMeasures()
	case tableIsPercent():
		return .percentColumnMeasures()
	default:
		return .fixedColumnMeasures(, tableLength(.table, ))
	}
}

func ( *tableLayout) ( string) []tableColumnMeasure {
	 := max(1, .numCols)
	 := tableColumnMeasure{}
	switch {
	case  == "auto":
		 = tableColumnMeasure{fixed: true, value: tableMax(.tableData().W)}
	case tableIsPercent():
		// MathJax leaves percentage columns unresolved until the parent-width pass.
		 = tableColumnMeasure{}
	default:
		 := tableSum(.columnLines, .columnSpace) + 2*.frameSpace[0]
		 = tableColumnMeasure{fixed: true, value: math.Max(0, tableLength(.table, )-) / float64()}
	}
	 := make([]tableColumnMeasure, .numCols)
	for  := range  {
		[] = 
	}
	return 
}

func ( *tableLayout) ( []string) []tableColumnMeasure {
	 := make([]tableColumnMeasure, len())
	for ,  := range  {
		if  == "auto" ||  == "fit" || tableIsPercent() {
			continue
		}
		[] = tableColumnMeasure{fixed: true, value: tableLength(.table, )}
	}
	return 
}

func ( *tableLayout) ( []string) []tableColumnMeasure {
	 := false
	for ,  := range  {
		 =  ||  == "fit"
	}
	if  {
		.tableData()
	}
	 := make([]tableColumnMeasure, len())
	for ,  := range  {
		switch {
		case  == "fit", tableIsPercent():
		case  == "auto" && !:
		case  == "auto":
			[] = tableColumnMeasure{fixed: true, value: .data.W[]}
		default:
			[] = tableColumnMeasure{fixed: true, value: tableLength(.table, )}
		}
	}
	return 
}

func ( *tableLayout) ( []string,  float64) []tableColumnMeasure {
	,  := []int{}, []int{}
	for ,  := range  {
		if  == "fit" {
			 = append(, )
		} else if  == "auto" {
			 = append(, )
		}
	}
	 := len()
	if  == 0 {
		 = len()
	}
	if  != 0 {
		.tableData()
	}
	 :=  - tableSum(.columnLines, .columnSpace) - 2*.frameSpace[0]
	 := 
	for ,  := range  {
		if  == "fit" ||  == "auto" {
			 -= .data.W[]
		} else {
			 -= tableLength(.table, , )
		}
	}
	 := 0.0
	if  != 0 &&  > 0 {
		 =  / float64()
	}
	 := make([]tableColumnMeasure, len())
	for ,  := range  {
		 := tableColumnMeasure{fixed: true}
		switch  {
		case "fit":
			.value = .data.W[] + 
		case "auto":
			.value = .data.W[]
			if len() == 0 {
				.value += 
			}
		default:
			.value = tableLength(.table, , )
		}
		[] = 
	}
	return 
}

func ( *tableLayout) () []float64 {
	 := append([]float64(nil), .tableData().W...)
	for ,  := range .columns {
		if .fixed {
			[] = .value
		}
	}
	if tableBool(.table, "equalcolumns", false) {
		 := tableMax()
		for  := range  {
			[] = 
		}
	}
	return 
}

func ( *tableLayout) () float64 {
	.tableData()
	// CommonMtable takes Math.max over the row totals, including an empty
	// list's -Infinity and NaN precedence over positive Infinity.
	 := math.Inf(-1)
	for  := range .data.H {
		 = max(, .data.H[]+.data.D[])
	}
	return 
}

func ( *tableLayout) ( int) (float64, float64) {
	,  := .data.H[], .data.D[]
	if tableBool(.table, "equalrows", false) {
		 := .equalRowHeight()
		return ( +  - ) / 2, ( -  + ) / 2
	}
	return , 
}

func ( *tableLayout) () []float64 {
	 := make([]float64, 0, len(.rowSpace)+2)
	 = append(, .frameSpace[1])
	for ,  := range .rowSpace {
		 = append(, /2)
	}
	return append(, .frameSpace[1])
}

func ( *tableLayout) () []float64 {
	 := make([]float64, 0, len(.columnSpace)+2)
	 = append(, .frameSpace[0])
	for ,  := range .columnSpace {
		 = append(, /2)
	}
	return append(, .frameSpace[0])
}

func ( *tableLayout) () (string, int, bool) {
	 := strings.Fields(tableString("align", .table))
	 := "axis"
	if len() != 0 {
		 = [0]
	}
	if len() < 2 {
		return , 0, false
	}
	,  := strconv.Atoi([1])
	if  != nil {
		return , 0, false
	}
	if  < 0 {
		 += .numRows + 1
	}
	if  < 1 ||  > .numRows {
		return , 0, false
	}
	return ,  - 1, true
}

func ( *tableLayout) ( int,  string) float64 {
	 := .rowHalfSpacing()
	 := .frameLine
	for  := 0;  < ; ++ {
		,  := .rowHD()
		 += [] +  +  + [+1] + .rowLines[]
	}
	,  := .rowHD()
	switch  {
	case "center":
		 += [] + (+)/2
	case "bottom":
		 += [] +  +  + [+1]
	case "baseline":
		 += [] + 
	case "axis":
		 += [] +  - .table.renderer.params.Axis
	}
	return 
}

func ( *tableLayout) ( float64) (float64, float64) {
	, ,  := .alignmentRow()
	if  {
		 := .verticalPosition(, )
		return ,  - 
	}
	 :=  / 2
	switch  {
	case "top":
		return 0, 
	case "bottom":
		return , 0
	case "axis":
		return  + .table.renderer.params.Axis,  - .table.renderer.params.Axis
	default:
		return , 
	}
}

func ( *tableLayout) () string {
	if .isTop {
		// D2's pinned SVG output jax uses displayAlign="center" and zero
		// displayIndent.
		return "center"
	}
	if .container == nil {
		return "left"
	}
	switch .container.node.Kind {
	case "mtd":
		return stringAttribute(.container.node, "columnalign", "center")
	case "mfrac":
		if boolAttributeDefault(.container.node, "bevelled", false) {
			return "left"
		}
		if .containerI == 0 {
			return stringAttribute(.container.node, "numalign", "center")
		}
		return stringAttribute(.container.node, "denomalign", "center")
	case "mpadded":
		return stringAttribute(.container.node, "data-align", "left")
	default:
		return "left"
	}
}

func ( *tableLayout) ( map[*wrapper]bool) float64 {
	if .isTop || .container == nil {
		return 80 * .table.renderer.params.XHeight
	}
	 := .container
	if [] {
		return .getBBox().W
	}
	[] = true
	defer delete(, )

	switch .node.Kind {
	case "math":
		return .getBBox().W
	case "mtd":
		 := .parent
		if  == nil || .parent == nil || .parent.node.Kind != "mtable" {
			return .getBBox().W
		}
		 := -1
		for ,  := range tableRowCells() {
			if  ==  {
				 = 
				break
			}
		}
		if  < 0 {
			return .getBBox().W
		}
		 := .parent.tableState()
		if !.isTop && tableIsPercent(tableString("width", .parent)) {
			.resolvePercentageWidth(.())
		}
		if  < len(.computed) {
			return .computed[]
		}
		if  < len(.data.W) {
			return .data.W[]
		}
	case "mfrac":
		if boolAttributeDefault(.node, "bevelled", false) && .containerI < len(.children) {
			return .children[.containerI].outerBBox().W
		}
		 := .getBBox().W - 2*.fractionPad()
		if .fractionThickness() != 0 {
			 -= .2
		}
		return 
	case "mpadded":
		return .getBBox().W
	}
	if .containerI >= 0 && .containerI < len(.children) {
		return .children[.containerI].getBBox().W
	}
	return .getBBox().W
}

func ( *tableLayout) ( string) (float64, string, float64) {
	 := .data.L + tableLength(.table, tableString("minlabelspacing", .table))
	 := .containerAlign()
	 := 0.0
	if  ==  {
		if  == "left" {
			 = math.Max(, ) - 
		} else {
			 = math.Min(-, ) + 
		}
	}
	return , , 
}

func ( *tableLayout) () (float64, float64) {
	if !.hasLabels {
		return 0, 0
	}
	 := tableString("side", .table)
	, ,  := .getPadAlignShift()
	 := boolAttributeDefault(.table.node, "data-width-includes-label", false)
	if  && .frame && .frameSpace[0] != 0 {
		 -= .frameSpace[0]
	}
	if  == "center" && ! {
		return , 
	}
	if  == "left" {
		return , 0
	}
	return 0, 
}

func ( *tableLayout) () {
	if tableBool(.table, "equalrows", false) {
		.height = tableSum(.rowLines, .rowSpace) + .equalRowHeight()*float64(.numRows)
	} else {
		.height = tableSum(.data.H, .data.D, .rowLines, .rowSpace)
	}
	.height += 2 * (.frameLine + .frameSpace[1])
	.width = tableSum(.computed, .columnLines, .columnSpace) + 2*(.frameLine+.frameSpace[0])
	 := tableString("width", .table)
	if  != "auto" && !tableIsPercent() {
		.width = math.Max(.width, tableLength(.table, )+2*.frameLine)
	}
	.h, .d = .bboxHD(.height)
	.left, .right = .labelLR()
}

// computeTableBBox ports CommonMtable.computeBBox. Percentage widths retain
// their markers until a parent-width pass resolves their natural geometry.
func ( *wrapper) ( *layout.BBox) {
	 := .tableState()
	.Empty()
	.W, .H, .D = .width, .h, .d
	.L, .R = .left, .right
	.PWidth = ""
	if .hasLabels {
		.PWidth = layout.FullWidth
	} else if  := tableString("width", ); tableIsPercent() && !.resolved {
		.PWidth = 
	}
	.Clean()
}

func tableBackground( *Element) *Element {
	,  := elementFirstChild().(*Element)
	if ! || .Tag != "rect" {
		return nil
	}
	for ,  := range .Attributes {
		if .Name == "data-bgcolor" && .Value == "true" {
			return 
		}
	}
	return nil
}

func setTableLineThickness( *Element,  float64,  string) *Element {
	if  != .07 {
		.SetAttr("stroke-thickness", fixed())
		if  != "solid" {
			 := fixed(2 * )
			if  == "dotted" {
				 = "0," + 
			}
			.SetAttr("stroke-dasharray", )
		}
	}
	return 
}

func ( *tableLayout) () *Element {
	,  := tableString("frame", .table), .frameLine
	return setTableLineThickness(NewElement("rect").
		SetAttr("data-frame", "true").
		SetAttr("class", "mjx-"+).
		SetAttr("width", fixed(.width-)).
		SetAttr("height", fixed(.h+.d-)).
		SetAttr("x", fixed(/2)).
		SetAttr("y", fixed(/2-.d)), , )
}

func ( *tableLayout) ( float64,  string,  float64) *Element {
	 := 0.0
	if  == "dotted" {
		 =  / 2
	}
	 =  + /2
	return setTableLineThickness(NewElement("line").
		SetAttr("data-line", "v").
		SetAttr("class", "mjx-"+).
		SetAttr("x1", fixed()).
		SetAttr("y1", fixed(-.d)).
		SetAttr("x2", fixed()).
		SetAttr("y2", fixed(.h-)), , )
}

func ( *tableLayout) ( float64,  string,  float64) *Element {
	 := 0.0
	if  == "dotted" {
		 =  / 2
	}
	 -=  / 2
	return setTableLineThickness(NewElement("line").
		SetAttr("data-line", "h").
		SetAttr("class", "mjx-"+).
		SetAttr("x1", fixed()).
		SetAttr("y1", fixed()).
		SetAttr("x2", fixed(.width-)).
		SetAttr("y2", fixed()), , )
}

// tableRuleValue preserves the source undefined-to-NaN arithmetic when an
// empty table retains authored line entries after a negative splice.
func tableRuleValue( []float64,  int) float64 {
	if  >= len() {
		return math.NaN()
	}
	return []
}

func ( *tableLayout) ( *Element) {
	 := .columnHalfSpacing()
	 := .frameLine
	for ,  := range .columnStyle {
		 += tableRuleValue(, ) + tableRuleValue(.computed, ) + tableRuleValue(, +1)
		if  != "none" {
			.Append(.makeVLine(, , .columnLines[]))
		}
		 += .columnLines[]
	}
}

func ( *tableLayout) ( *Element) {
	 := .rowHalfSpacing()
	 := .h - .frameLine
	for ,  := range .rowStyle {
		,  := math.NaN(), math.NaN()
		if  < .numRows {
			,  = .rowHD()
		}
		 -= tableRuleValue(, ) +  +  + tableRuleValue(, +1)
		if  != "none" {
			.Append(.makeHLine(, , .rowLines[]))
		}
		 -= .rowLines[]
	}
}

func ( *tableLayout) () float64 {
	if .pWidth != 0 {
		return .pWidth
	}
	return .width
}

// handlePWidth ports SVGmtable.handlePWidth.  The resolved numeric width is
// intentionally separate from the nested table's natural bbox width.
func ( *tableLayout) ( *Element) float64 {
	if .pWidth == 0 {
		return 0
	}
	 := .table.getBBox()
	 := .L + .pWidth + .R
	 := 0.0
	if .isTop {
		 = 
	}
	 = math.Max(, .containerWrapWidth(make(map[*wrapper]bool))) - .L - .R
	 := .W - math.Min(.pWidth, )
	 :=  / 2
	switch .containerAlign() {
	case "left":
		 = 0
	case "right":
		 = 
	}
	if fixed() != "0" {
		 := append([]Node(nil), .Children...)
		.Children = nil
		 := NewElement("g", ...)
		.table.place(, 0, )
		.Append()
	}
	return 
}

func ( *tableLayout) (,  *Element,  string) {
	 := .table.getBBox()
	 := .L + .getWidth() + .R
	, ,  := .getPadAlignShift()
	 :=  + .L
	if  == "right" {
		 -= 
	} else if  == "center" {
		 -=  / 2
	}

	const  = "matrix(1 0 0 -1 0 0)"
	 := jsFixed((.table.renderer.params.XHeight*1000)/.table.renderer.options.Ex, 2)
	 := fmt.Sprintf("translate(0 %s) %s scale(%s)", fixed(.H), , )

	 := append([]Node(nil), .Children...)
	.Children = nil
	 := NewElement("g", ...).SetAttr("transform", )
	 := NewElement("svg", ).
		SetAttr("data-table", "true").
		SetAttr("preserveAspectRatio", map[string]string{
			"left": "xMinYMid", "right": "xMaxYMid",
		}[]).
		SetAttr("viewBox", strings.Join([]string{
			fixed(-), fixed(-.H), "1", fixed(.H + .D),
		}, " "))
	if  != "left" &&  != "right" {
		.SetAttr("preserveAspectRatio", "xMidYMid")
	}
	 := "0"
	 := "xMinYMid"
	if  != "left" {
		 = fixed(.data.L)
		 = "xMaxYMid"
	}
	 := NewElement("svg", ).
		SetAttr("data-labels", "true").
		SetAttr("preserveAspectRatio", ).
		SetAttr("viewBox", strings.Join([]string{
			, fixed(-.H), "1", fixed(.H + .D),
		}, " "))
	.Append(NewElement("g", , ).SetAttr("transform", ))
	// The parent adds bbox.L when appending this wrapper.  Remove it here so
	// the responsive table and labels share the source wrapper's origin.
	.table.place(-.L, 0, )
}

func ( *tableLayout) (,  *Element,  string,  float64) {
	 := .table.getBBox()
	 := .L + .getWidth() + .R
	 := math.Max(, .containerWrapWidth(make(map[*wrapper]bool)))
	 := 0.0
	 := .containerAlign()
	if  == "left" {
		switch  {
		case "right":
			 =  -  + 
		case "center":
			 = (-)/2 + 
		}
		 -= .L
	} else {
		switch  {
		case "left":
			 = 
		case "right":
			 =  + 
		default:
			 = (+)/2 + 
		}
		 -= .L + .data.L
	}
	.table.place(, 0, )
	.Append()
}

func ( *tableLayout) (,  *Element,  float64) {
	if !.hasLabels ||  == nil || len(.Children) == 0 {
		return
	}
	 := tableString("side", .table)
	if .isTop {
		.topTable(, , )
	} else {
		.subTable(, , , )
	}
}

// tableToSVG ports SVGmtable.toSVG, including responsive top-level labels and
// explicit nested-label placement.
func ( *wrapper) ( *Element) {
	 := .standardSVG()
	 := resolvedTableLayout()
	if  := tableBackground();  != nil {
		.SetAttr("width", fixed(.getWidth()))
	}
	 := NewElement("g").SetAttr("data-labels", "true")
	if .isTop {
		.SetAttr("transform", "matrix(1 0 0 -1 0 0)")
	}
	 := .rowHalfSpacing()
	 := .h - .frameLine
	for ,  := range .rows {
		,  := .rowHD()
		,  := .frameLine, .frameLine
		if  > 0 {
			 = .rowLines[-1]
		}
		if  < len(.rowLines) {
			 = .rowLines[]
		}
		.renderTableRow(, , , , , [], [+1], , , )
		 :=  - [] - 
		.place(0, )
		 -= [] +  +  + [+1] + 
	}
	.addColumnLines()
	.addRowLines()
	if .frame && .frameLine != 0 {
		.Append(.makeFrame())
	}
	 := .handlePWidth()
	.handleLabels(, , )
}