package svg
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/d2lang/mathjax-go/internal/font"
"github.com/d2lang/mathjax-go/internal/layout"
)
type tableDimensions struct {
H, D []float64
W []float64
NH, ND []float64
L float64
}
type tableColumnMeasure struct {
fixed bool
value float64
}
type tableLayout struct {
table *wrapper
rows []*wrapper
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
}
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(nodeName string , w *wrapper ) string {
return stringAttribute (w .node , nodeName , tableDefaults [nodeName ])
}
func tableBool(w *wrapper , name string , fallback bool ) bool {
return boolAttributeDefault (w .node , name , fallback )
}
func tableFields(w *wrapper , name string ) []string {
value := strings .TrimSpace (tableString (name , w ))
if value == "" {
return nil
}
return strings .Fields (value )
}
func repeatTableField(values []string , n int , fallback string ) []string {
if n < 0 {
return append ([]string {}, values [:max (0 , len (values )+n )]...)
}
if n == 0 {
return []string {}
}
if len (values ) == 0 {
values = []string {fallback }
}
result := make ([]string , n )
for i := range result {
result [i ] = values [min (i , len (values )-1 )]
}
return result
}
func tableLength(w *wrapper , value string , size ...float64 ) float64 {
reference := 1.0
if len (size ) != 0 {
reference = size [0 ]
}
return layout .Length2Em (value , reference , w .bbox .Scale , w .renderer .pxPerEm )
}
func tableLengths(w *wrapper , values []string ) []float64 {
result := make ([]float64 , len (values ))
for i , value := range values {
result [i ] = tableLength (w , value )
}
return result
}
func tableSum(values ...[]float64 ) float64 {
total := 0.0
for _ , list := range values {
for _ , value := range list {
total += value
}
}
return total
}
func tableMax(values []float64 ) float64 {
maximum := 0.0
for _ , value := range values {
if value > maximum {
maximum = value
}
}
return maximum
}
func tableIsPercent(value string ) bool {
value = strings .TrimSpace (value )
if !strings .HasSuffix (value , "%" ) {
return false
}
_ , err := strconv .ParseFloat (strings .TrimSpace (strings .TrimSuffix (value , "%" )), 64 )
return err == nil
}
func tableRows(w *wrapper ) []*wrapper {
return append ([]*wrapper (nil ), w .children ...)
}
func tableRowCells(row *wrapper ) []*wrapper {
if row == nil {
return nil
}
start := 0
if row .node .Kind == "mlabeledtr" && len (row .children ) != 0 {
start = 1
}
return row .children [start :]
}
func tableLabelCell(row *wrapper ) *wrapper {
if row != nil && row .node .Kind == "mlabeledtr" && len (row .children ) != 0 {
return row .children [0 ]
}
return nil
}
func initializeTablePWidth(w *wrapper ) {
if w == nil || w .node == nil || w .node .Kind != "mtable" {
return
}
for _ , row := range tableRows (w ) {
if tableLabelCell (row ) != nil {
w .bbox .PWidth = layout .FullWidth
return
}
}
if width := tableString ("width" , w ); tableIsPercent (width ) {
w .bbox .PWidth = width
}
}
func (w *wrapper ) tableState () *tableState {
if w .table == nil {
w .table = newTableLayout (w )
}
return w .table
}
func newTableLayout(w *wrapper ) *tableLayout {
container , containerI := findTableContainer (w )
t := &tableLayout {
table : w , rows : tableRows (w ),
container : container , containerI : containerI ,
isTop : container == nil || (container .node .Kind == "math" && container .parent == nil ),
}
t .numRows = len (t .rows )
for _ , row := range t .rows {
if n := len (tableRowCells (row )); n > t .numCols {
t .numCols = n
}
if tableLabelCell (row ) != nil {
t .hasLabels = true
}
}
frame := tableString ("frame" , w )
t .frame = frame != "none"
if t .frame && frame != "" {
t .frameLine = .07
}
frameSpacing := repeatTableField (tableFields (w , "framespacing" ), 2 , "0" )
if t .frame {
converted := tableLengths (w , frameSpacing )
copy (t .frameSpace [:], converted )
}
t .columnSpace = tableLengths (w , repeatTableField (tableFields (w , "columnspacing" ), t .numCols -1 , ".8em" ))
t .rowSpace = tableLengths (w , repeatTableField (tableFields (w , "rowspacing" ), t .numRows -1 , "1ex" ))
t .columnStyle = repeatTableField (tableFields (w , "columnlines" ), t .numCols -1 , "none" )
t .rowStyle = repeatTableField (tableFields (w , "rowlines" ), t .numRows -1 , "none" )
t .columnLines = tableLineWidths (t .columnStyle )
t .rowLines = tableLineWidths (t .rowStyle )
t .columns = t .columnMeasures ()
t .stretchRows ()
t .stretchColumns ()
t .computed = t .computedWidths ()
t .measureTable ()
return t
}
func findTableContainer(w *wrapper ) (*wrapper , int ) {
node , parent := w , w .parent
for parent != nil && (parent .node .Kind == "mrow" || parent .node .Flags .NotParent ) {
node = parent
parent = parent .parent
}
if parent == nil {
return nil , 0
}
for i , child := range parent .children {
if child == node {
return parent , i
}
}
return parent , 0
}
func (t *tableLayout ) resolvePercentageWidth (containerWidth float64 ) {
width := tableString ("width" , t .table )
if !tableIsPercent (width ) || t .resolved {
return
}
naturalWidth := t .width
reference := math .Max (containerWidth , t .left +t .width +t .right )
target := math .Max (t .width , tableLength (t .table , width , reference ))
if tableBool (t .table , "data-width-includes-label" , false ) {
target -= t .left + t .right
}
var values []string
if tableBool (t .table , "equalcolumns" , false ) {
values = make ([]string , t .numCols )
value := layout .Percent (1 / float64 (max (1 , t .numCols )))
for i := range values {
values [i ] = value
}
} else {
values = repeatTableField (tableFields (t .table , "columnwidth" ), t .numCols , "auto" )
}
t .columns = t .fixedColumnMeasures (values , target )
t .computed = t .computedWidths ()
t .measureTable ()
t .pWidth = t .width
if !t .isTop {
t .width = naturalWidth
} else {
t .table .bbox .W = t .pWidth
}
t .resolved = true
if !t .hasLabels {
t .table .bbox .PWidth = ""
if t .container != nil {
t .container .bbox .PWidth = ""
}
}
if t .pWidth != naturalWidth && t .table .parent != nil {
t .table .parent .invalidateBBox ()
}
}
func resolvedTableLayout(w *wrapper ) *tableLayout {
t := w .tableState ()
if !t .isTop && tableIsPercent (tableString ("width" , w )) {
t .resolvePercentageWidth (t .containerWrapWidth (make (map [*wrapper ]bool )))
}
return t
}
func tableCellChild(cell *wrapper ) *wrapper {
if cell == nil || len (cell .children ) == 0 {
return nil
}
return cell .children [0 ]
}
func tableCoreMO(child *wrapper ) *wrapper {
for child != nil && child .node .Flags .Embellished && child .node .Kind != "mo" {
index := child .node .Flags .CoreIndex
if index < 0 || index >= len (child .children ) {
return nil
}
child = child .children [index ]
}
if child == nil || child .node .Kind != "mo" {
return nil
}
return child
}
func tableStretchCore(child *wrapper , direction font .Direction ) *wrapper {
if child == nil || !child .canStretch (direction ) {
return nil
}
return tableCoreMO (child )
}
func invalidateStretchParents(core *wrapper ) {
for parent := core .parent ; parent != nil ; parent = parent .parent {
parent .bboxComputed = false
}
}
func (t *tableLayout ) stretchRows () {
equal := tableBool (t .table , "equalrows" , false )
equalHeight := 0.0
if equal {
equalHeight = t .equalRowHeight ()
}
for i , row := range t .rows {
var dimensions []float64
if equal {
h , d := t .data .H [i ], t .data .D [i ]
dimensions = []float64 {(equalHeight + h - d ) / 2 , (equalHeight - h + d ) / 2 }
}
t .stretchRow (row , dimensions )
}
}
func (t *tableLayout ) stretchRow (row *wrapper , dimensions []float64 ) {
cells := tableRowCells (row )
stretchable := make (map [*wrapper ]*wrapper )
for _ , cell := range cells {
child := tableCellChild (cell )
if core := tableStretchCore (child , font .DirectionVertical ); core != nil {
stretchable [child ] = core
}
}
if len (stretchable ) == 0 || len (row .children ) <= 1 {
return
}
if dimensions == nil {
height , depth := 0.0 , 0.0
all := len (stretchable ) > 1 && len (stretchable ) == len (row .children )
for _ , cell := range cells {
child := tableCellChild (cell )
if child == nil {
continue
}
_ , stretches := stretchable [child ]
if !all && stretches {
continue
}
bbox := child .outerBBox ()
height = math .Max (height , bbox .H *bbox .RScale )
depth = math .Max (depth , bbox .D *bbox .RScale )
}
dimensions = []float64 {height , depth }
}
for _ , core := range stretchable {
core .getStretchedVariant (dimensions , false )
invalidateStretchParents (core )
}
}
func (t *tableLayout ) stretchColumns () {
for column := 0 ; column < t .numCols ; column ++ {
var width *float64
if column < len (t .columns ) && t .columns [column ].fixed {
value := t .columns [column ].value
width = &value
}
t .stretchColumn (column , width )
}
}
func (t *tableLayout ) stretchColumn (column int , width *float64 ) {
stretchable := make (map [*wrapper ]*wrapper )
for _ , row := range t .rows {
cells := tableRowCells (row )
if column >= len (cells ) {
continue
}
child := tableCellChild (cells [column ])
if core := tableStretchCore (child , font .DirectionHorizontal ); core != nil {
stretchable [child ] = core
}
}
if len (stretchable ) == 0 || len (t .rows ) <= 1 {
return
}
target := 0.0
if width != nil {
target = *width
} else {
all := len (stretchable ) > 1 && len (stretchable ) == len (t .rows )
for _ , row := range t .rows {
cells := tableRowCells (row )
if column >= len (cells ) {
continue
}
child := tableCellChild (cells [column ])
if child == nil {
continue
}
_ , stretches := stretchable [child ]
if !all && stretches {
continue
}
bbox := child .outerBBox ()
target = math .Max (target , bbox .W *bbox .RScale )
}
}
for _ , core := range stretchable {
core .getStretchedVariant ([]float64 {target }, false )
invalidateStretchParents (core )
}
}
func tableLineWidths(styles []string ) []float64 {
widths := make ([]float64 , len (styles ))
for i , style := range styles {
if style != "none" {
widths [i ] = .07
}
}
return widths
}
func (t *tableLayout ) rowAlign (row , index int ) string {
aligns := repeatTableField (tableFields (t .table , "rowalign" ), t .numRows , "baseline" )
align := aligns [index ]
if row >= 0 && row < len (t .rows ) {
if explicit := stringAttributeExplicit (t .rows [row ].node , "rowalign" ); explicit != "" {
align = explicit
}
}
return align
}
func (t *tableLayout ) columnAlign (row *wrapper , cell *wrapper , column int ) string {
aligns := repeatTableField (tableFields (t .table , "columnalign" ), t .numCols , "center" )
align := "center"
if column >= 0 && column < len (aligns ) {
align = aligns [column ]
}
if row != nil {
rowAligns := strings .Fields (stringAttributeExplicit (row .node , "columnalign" ))
if len (rowAligns ) != 0 {
align = rowAligns [min (column , len (rowAligns )-1 )]
}
}
if cell != nil {
if explicit := stringAttributeExplicit (cell .node , "columnalign" ); explicit != "" {
align = explicit
}
}
return align
}
func (t *tableLayout ) cellRowAlign (row *wrapper , cell *wrapper , rowIndex int ) string {
align := t .rowAlign (rowIndex , rowIndex )
if cell != nil {
if explicit := stringAttributeExplicit (cell .node , "rowalign" ); explicit != "" {
align = explicit
}
}
return align
}
func (t *tableLayout ) useHeight () bool {
if value , ok := t .table .node .Property ("useHeight" ); ok {
return truthy (value )
}
return true
}
func (t *tableLayout ) updateHDW (cell *wrapper , column , row int , align string , H , D , W []float64 , tallest float64 ) float64 {
if cell == nil {
return tallest
}
bbox := cell .outerBBox ()
scale := 1.0
if cell .parent != nil {
scale = cell .parent .bbox .RScale
}
h , d , width := bbox .H *scale , bbox .D *scale , bbox .W *scale
if t .useHeight () {
h = math .Max (h , .75 )
d = math .Max (d , .25 )
}
if explicit := stringAttributeExplicit (cell .node , "rowalign" ); explicit != "" {
align = explicit
}
m := 0.0
if align != "baseline" && align != "axis" {
m = h + d
h , d = 0 , 0
}
H [row ] = math .Max (H [row ], h )
D [row ] = math .Max (D [row ], d )
tallest = math .Max (tallest , m )
if column >= 0 && column < len (W ) {
W [column ] = math .Max (W [column ], width )
}
return tallest
}
func extendTableHD(row int , H , D []float64 , tallest float64 ) {
delta := (tallest - (H [row ] + D [row ])) / 2
if delta < .00001 {
return
}
H [row ] += delta
D [row ] += delta
}
func (t *tableLayout ) measureCells () tableDimensions {
data := tableDimensions {
H : make ([]float64 , t .numRows ), D : make ([]float64 , t .numRows ),
W : make ([]float64 , t .numCols ), NH : make ([]float64 , t .numRows ), ND : make ([]float64 , t .numRows ),
}
labelWidths := []float64 {0 }
for j , row := range t .rows {
tallest := 0.0
align := t .rowAlign (j , j )
for i , cell := range tableRowCells (row ) {
tallest = t .updateHDW (cell , i , j , align , data .H , data .D , data .W , tallest )
}
data .NH [j ], data .ND [j ] = data .H [j ], data .D [j ]
if label := tableLabelCell (row ); label != nil {
tallest = t .updateHDW (label , 0 , j , align , data .H , data .D , labelWidths , tallest )
}
extendTableHD (j , data .H , data .D , tallest )
extendTableHD (j , data .NH , data .ND , tallest )
}
data .L = labelWidths [0 ]
return data
}
func (t *tableLayout ) tableData () *tableDimensions {
if t .data .W == nil {
t .data = t .measureCells ()
}
return &t .data
}
func (t *tableLayout ) columnMeasures () []tableColumnMeasure {
width := tableString ("width" , t .table )
if tableBool (t .table , "equalcolumns" , false ) {
return t .equalColumnMeasures (width )
}
values := repeatTableField (tableFields (t .table , "columnwidth" ), t .numCols , "auto" )
switch {
case width == "auto" :
return t .autoColumnMeasures (values )
case tableIsPercent (width ):
return t .percentColumnMeasures (values )
default :
return t .fixedColumnMeasures (values , tableLength (t .table , width ))
}
}
func (t *tableLayout ) equalColumnMeasures (width string ) []tableColumnMeasure {
n := max (1 , t .numCols )
measure := tableColumnMeasure {}
switch {
case width == "auto" :
measure = tableColumnMeasure {fixed : true , value : tableMax (t .tableData ().W )}
case tableIsPercent (width ):
measure = tableColumnMeasure {}
default :
decoration := tableSum (t .columnLines , t .columnSpace ) + 2 *t .frameSpace [0 ]
measure = tableColumnMeasure {fixed : true , value : math .Max (0 , tableLength (t .table , width )-decoration ) / float64 (n )}
}
result := make ([]tableColumnMeasure , t .numCols )
for i := range result {
result [i ] = measure
}
return result
}
func (t *tableLayout ) autoColumnMeasures (values []string ) []tableColumnMeasure {
result := make ([]tableColumnMeasure , len (values ))
for i , value := range values {
if value == "auto" || value == "fit" || tableIsPercent (value ) {
continue
}
result [i ] = tableColumnMeasure {fixed : true , value : tableLength (t .table , value )}
}
return result
}
func (t *tableLayout ) percentColumnMeasures (values []string ) []tableColumnMeasure {
hasFit := false
for _ , value := range values {
hasFit = hasFit || value == "fit"
}
if hasFit {
t .tableData ()
}
result := make ([]tableColumnMeasure , len (values ))
for i , value := range values {
switch {
case value == "fit" , tableIsPercent (value ):
case value == "auto" && !hasFit :
case value == "auto" :
result [i ] = tableColumnMeasure {fixed : true , value : t .data .W [i ]}
default :
result [i ] = tableColumnMeasure {fixed : true , value : tableLength (t .table , value )}
}
}
return result
}
func (t *tableLayout ) fixedColumnMeasures (values []string , width float64 ) []tableColumnMeasure {
fit , auto := []int {}, []int {}
for i , value := range values {
if value == "fit" {
fit = append (fit , i )
} else if value == "auto" {
auto = append (auto , i )
}
}
n := len (fit )
if n == 0 {
n = len (auto )
}
if n != 0 {
t .tableData ()
}
cwidth := width - tableSum (t .columnLines , t .columnSpace ) - 2 *t .frameSpace [0 ]
remaining := cwidth
for i , value := range values {
if value == "fit" || value == "auto" {
remaining -= t .data .W [i ]
} else {
remaining -= tableLength (t .table , value , cwidth )
}
}
extra := 0.0
if n != 0 && remaining > 0 {
extra = remaining / float64 (n )
}
result := make ([]tableColumnMeasure , len (values ))
for i , value := range values {
column := tableColumnMeasure {fixed : true }
switch value {
case "fit" :
column .value = t .data .W [i ] + extra
case "auto" :
column .value = t .data .W [i ]
if len (fit ) == 0 {
column .value += extra
}
default :
column .value = tableLength (t .table , value , cwidth )
}
result [i ] = column
}
return result
}
func (t *tableLayout ) computedWidths () []float64 {
widths := append ([]float64 (nil ), t .tableData ().W ...)
for i , column := range t .columns {
if column .fixed {
widths [i ] = column .value
}
}
if tableBool (t .table , "equalcolumns" , false ) {
maximum := tableMax (widths )
for i := range widths {
widths [i ] = maximum
}
}
return widths
}
func (t *tableLayout ) equalRowHeight () float64 {
t .tableData ()
maximum := math .Inf (-1 )
for i := range t .data .H {
maximum = max (maximum , t .data .H [i ]+t .data .D [i ])
}
return maximum
}
func (t *tableLayout ) rowHD (index int ) (float64 , float64 ) {
h , d := t .data .H [index ], t .data .D [index ]
if tableBool (t .table , "equalrows" , false ) {
hd := t .equalRowHeight ()
return (hd + h - d ) / 2 , (hd - h + d ) / 2
}
return h , d
}
func (t *tableLayout ) rowHalfSpacing () []float64 {
result := make ([]float64 , 0 , len (t .rowSpace )+2 )
result = append (result , t .frameSpace [1 ])
for _ , value := range t .rowSpace {
result = append (result , value /2 )
}
return append (result , t .frameSpace [1 ])
}
func (t *tableLayout ) columnHalfSpacing () []float64 {
result := make ([]float64 , 0 , len (t .columnSpace )+2 )
result = append (result , t .frameSpace [0 ])
for _ , value := range t .columnSpace {
result = append (result , value /2 )
}
return append (result , t .frameSpace [0 ])
}
func (t *tableLayout ) alignmentRow () (string , int , bool ) {
fields := strings .Fields (tableString ("align" , t .table ))
align := "axis"
if len (fields ) != 0 {
align = fields [0 ]
}
if len (fields ) < 2 {
return align , 0 , false
}
row , err := strconv .Atoi (fields [1 ])
if err != nil {
return align , 0 , false
}
if row < 0 {
row += t .numRows + 1
}
if row < 1 || row > t .numRows {
return align , 0 , false
}
return align , row - 1 , true
}
func (t *tableLayout ) verticalPosition (row int , align string ) float64 {
space := t .rowHalfSpacing ()
y := t .frameLine
for j := 0 ; j < row ; j ++ {
h , d := t .rowHD (j )
y += space [j ] + h + d + space [j +1 ] + t .rowLines [j ]
}
h , d := t .rowHD (row )
switch align {
case "center" :
y += space [row ] + (h +d )/2
case "bottom" :
y += space [row ] + h + d + space [row +1 ]
case "baseline" :
y += space [row ] + h
case "axis" :
y += space [row ] + h - t .table .renderer .params .Axis
}
return y
}
func (t *tableLayout ) bboxHD (height float64 ) (float64 , float64 ) {
align , row , hasRow := t .alignmentRow ()
if hasRow {
h := t .verticalPosition (row , align )
return h , height - h
}
half := height / 2
switch align {
case "top" :
return 0 , height
case "bottom" :
return height , 0
case "axis" :
return half + t .table .renderer .params .Axis , half - t .table .renderer .params .Axis
default :
return half , half
}
}
func (t *tableLayout ) containerAlign () string {
if t .isTop {
return "center"
}
if t .container == nil {
return "left"
}
switch t .container .node .Kind {
case "mtd" :
return stringAttribute (t .container .node , "columnalign" , "center" )
case "mfrac" :
if boolAttributeDefault (t .container .node , "bevelled" , false ) {
return "left"
}
if t .containerI == 0 {
return stringAttribute (t .container .node , "numalign" , "center" )
}
return stringAttribute (t .container .node , "denomalign" , "center" )
case "mpadded" :
return stringAttribute (t .container .node , "data-align" , "left" )
default :
return "left"
}
}
func (t *tableLayout ) containerWrapWidth (seen map [*wrapper ]bool ) float64 {
if t .isTop || t .container == nil {
return 80 * t .table .renderer .params .XHeight
}
container := t .container
if seen [container ] {
return container .getBBox ().W
}
seen [container ] = true
defer delete (seen , container )
switch container .node .Kind {
case "math" :
return container .getBBox ().W
case "mtd" :
row := container .parent
if row == nil || row .parent == nil || row .parent .node .Kind != "mtable" {
return container .getBBox ().W
}
column := -1
for i , cell := range tableRowCells (row ) {
if cell == container {
column = i
break
}
}
if column < 0 {
return container .getBBox ().W
}
outer := row .parent .tableState ()
if !outer .isTop && tableIsPercent (tableString ("width" , row .parent )) {
outer .resolvePercentageWidth (outer .containerWrapWidth (seen ))
}
if column < len (outer .computed ) {
return outer .computed [column ]
}
if column < len (outer .data .W ) {
return outer .data .W [column ]
}
case "mfrac" :
if boolAttributeDefault (container .node , "bevelled" , false ) && t .containerI < len (container .children ) {
return container .children [t .containerI ].outerBBox ().W
}
width := container .getBBox ().W - 2 *container .fractionPad ()
if container .fractionThickness () != 0 {
width -= .2
}
return width
case "mpadded" :
return container .getBBox ().W
}
if t .containerI >= 0 && t .containerI < len (container .children ) {
return container .children [t .containerI ].getBBox ().W
}
return container .getBBox ().W
}
func (t *tableLayout ) getPadAlignShift (side string ) (float64 , string , float64 ) {
pad := t .data .L + tableLength (t .table , tableString ("minlabelspacing" , t .table ))
align := t .containerAlign ()
shift := 0.0
if align == side {
if side == "left" {
shift = math .Max (pad , shift ) - pad
} else {
shift = math .Min (-pad , shift ) + pad
}
}
return pad , align , shift
}
func (t *tableLayout ) labelLR () (float64 , float64 ) {
if !t .hasLabels {
return 0 , 0
}
side := tableString ("side" , t .table )
pad , align , _ := t .getPadAlignShift (side )
labelsInWidth := boolAttributeDefault (t .table .node , "data-width-includes-label" , false )
if labelsInWidth && t .frame && t .frameSpace [0 ] != 0 {
pad -= t .frameSpace [0 ]
}
if align == "center" && !labelsInWidth {
return pad , pad
}
if side == "left" {
return pad , 0
}
return 0 , pad
}
func (t *tableLayout ) measureTable () {
if tableBool (t .table , "equalrows" , false ) {
t .height = tableSum (t .rowLines , t .rowSpace ) + t .equalRowHeight ()*float64 (t .numRows )
} else {
t .height = tableSum (t .data .H , t .data .D , t .rowLines , t .rowSpace )
}
t .height += 2 * (t .frameLine + t .frameSpace [1 ])
t .width = tableSum (t .computed , t .columnLines , t .columnSpace ) + 2 *(t .frameLine +t .frameSpace [0 ])
width := tableString ("width" , t .table )
if width != "auto" && !tableIsPercent (width ) {
t .width = math .Max (t .width , tableLength (t .table , width )+2 *t .frameLine )
}
t .h , t .d = t .bboxHD (t .height )
t .left , t .right = t .labelLR ()
}
func (w *wrapper ) computeTableBBox (bbox *layout .BBox ) {
t := w .tableState ()
bbox .Empty ()
bbox .W , bbox .H , bbox .D = t .width , t .h , t .d
bbox .L , bbox .R = t .left , t .right
bbox .PWidth = ""
if t .hasLabels {
bbox .PWidth = layout .FullWidth
} else if width := tableString ("width" , w ); tableIsPercent (width ) && !t .resolved {
bbox .PWidth = width
}
bbox .Clean ()
}
func tableBackground(element *Element ) *Element {
rect , ok := elementFirstChild (element ).(*Element )
if !ok || rect .Tag != "rect" {
return nil
}
for _ , attr := range rect .Attributes {
if attr .Name == "data-bgcolor" && attr .Value == "true" {
return rect
}
}
return nil
}
func setTableLineThickness(element *Element , thickness float64 , style string ) *Element {
if thickness != .07 {
element .SetAttr ("stroke-thickness" , fixed (thickness ))
if style != "solid" {
dashes := fixed (2 * thickness )
if style == "dotted" {
dashes = "0," + dashes
}
element .SetAttr ("stroke-dasharray" , dashes )
}
}
return element
}
func (t *tableLayout ) makeFrame () *Element {
style , thickness := tableString ("frame" , t .table ), t .frameLine
return setTableLineThickness (NewElement ("rect" ).
SetAttr ("data-frame" , "true" ).
SetAttr ("class" , "mjx-" +style ).
SetAttr ("width" , fixed (t .width -thickness )).
SetAttr ("height" , fixed (t .h +t .d -thickness )).
SetAttr ("x" , fixed (thickness /2 )).
SetAttr ("y" , fixed (thickness /2 -t .d )), thickness , style )
}
func (t *tableLayout ) makeVLine (x float64 , style string , thickness float64 ) *Element {
dot := 0.0
if style == "dotted" {
dot = thickness / 2
}
x = x + thickness /2
return setTableLineThickness (NewElement ("line" ).
SetAttr ("data-line" , "v" ).
SetAttr ("class" , "mjx-" +style ).
SetAttr ("x1" , fixed (x )).
SetAttr ("y1" , fixed (dot -t .d )).
SetAttr ("x2" , fixed (x )).
SetAttr ("y2" , fixed (t .h -dot )), thickness , style )
}
func (t *tableLayout ) makeHLine (y float64 , style string , thickness float64 ) *Element {
dot := 0.0
if style == "dotted" {
dot = thickness / 2
}
y -= thickness / 2
return setTableLineThickness (NewElement ("line" ).
SetAttr ("data-line" , "h" ).
SetAttr ("class" , "mjx-" +style ).
SetAttr ("x1" , fixed (dot )).
SetAttr ("y1" , fixed (y )).
SetAttr ("x2" , fixed (t .width -dot )).
SetAttr ("y2" , fixed (y )), thickness , style )
}
func tableRuleValue(values []float64 , index int ) float64 {
if index >= len (values ) {
return math .NaN ()
}
return values [index ]
}
func (t *tableLayout ) addColumnLines (element *Element ) {
space := t .columnHalfSpacing ()
x := t .frameLine
for i , style := range t .columnStyle {
x += tableRuleValue (space , i ) + tableRuleValue (t .computed , i ) + tableRuleValue (space , i +1 )
if style != "none" {
element .Append (t .makeVLine (x , style , t .columnLines [i ]))
}
x += t .columnLines [i ]
}
}
func (t *tableLayout ) addRowLines (element *Element ) {
space := t .rowHalfSpacing ()
y := t .h - t .frameLine
for i , style := range t .rowStyle {
h , d := math .NaN (), math .NaN ()
if i < t .numRows {
h , d = t .rowHD (i )
}
y -= tableRuleValue (space , i ) + h + d + tableRuleValue (space , i +1 )
if style != "none" {
element .Append (t .makeHLine (y , style , t .rowLines [i ]))
}
y -= t .rowLines [i ]
}
}
func (t *tableLayout ) getWidth () float64 {
if t .pWidth != 0 {
return t .pWidth
}
return t .width
}
func (t *tableLayout ) handlePWidth (element *Element ) float64 {
if t .pWidth == 0 {
return 0
}
bbox := t .table .getBBox ()
W := bbox .L + t .pWidth + bbox .R
containerWidth := 0.0
if t .isTop {
containerWidth = W
}
containerWidth = math .Max (containerWidth , t .containerWrapWidth (make (map [*wrapper ]bool ))) - bbox .L - bbox .R
dw := bbox .W - math .Min (t .pWidth , containerWidth )
dx := dw / 2
switch t .containerAlign () {
case "left" :
dx = 0
case "right" :
dx = dw
}
if fixed (dx ) != "0" {
children := append ([]Node (nil ), element .Children ...)
element .Children = nil
group := NewElement ("g" , children ...)
t .table .place (dx , 0 , group )
element .Append (group )
}
return dx
}
func (t *tableLayout ) topTable (element , labels *Element , side string ) {
bbox := t .table .getBBox ()
W := bbox .L + t .getWidth () + bbox .R
_ , align , shift := t .getPadAlignShift (side )
dx := shift + bbox .L
if align == "right" {
dx -= W
} else if align == "center" {
dx -= W / 2
}
const matrix = "matrix(1 0 0 -1 0 0)"
scale := jsFixed ((t .table .renderer .params .XHeight *1000 )/t .table .renderer .options .Ex , 2 )
transform := fmt .Sprintf ("translate(0 %s) %s scale(%s)" , fixed (bbox .H ), matrix , scale )
children := append ([]Node (nil ), element .Children ...)
element .Children = nil
tableContents := NewElement ("g" , children ...).SetAttr ("transform" , matrix )
tableSVG := NewElement ("svg" , tableContents ).
SetAttr ("data-table" , "true" ).
SetAttr ("preserveAspectRatio" , map [string ]string {
"left" : "xMinYMid" , "right" : "xMaxYMid" ,
}[align ]).
SetAttr ("viewBox" , strings .Join ([]string {
fixed (-dx ), fixed (-bbox .H ), "1" , fixed (bbox .H + bbox .D ),
}, " " ))
if align != "left" && align != "right" {
tableSVG .SetAttr ("preserveAspectRatio" , "xMidYMid" )
}
labelX := "0"
labelAspect := "xMinYMid"
if side != "left" {
labelX = fixed (t .data .L )
labelAspect = "xMaxYMid"
}
labelsSVG := NewElement ("svg" , labels ).
SetAttr ("data-labels" , "true" ).
SetAttr ("preserveAspectRatio" , labelAspect ).
SetAttr ("viewBox" , strings .Join ([]string {
labelX , fixed (-bbox .H ), "1" , fixed (bbox .H + bbox .D ),
}, " " ))
element .Append (NewElement ("g" , tableSVG , labelsSVG ).SetAttr ("transform" , transform ))
t .table .place (-bbox .L , 0 , element )
}
func (t *tableLayout ) subTable (element , labels *Element , side string , dx float64 ) {
bbox := t .table .getBBox ()
W := bbox .L + t .getWidth () + bbox .R
containerWidth := math .Max (W , t .containerWrapWidth (make (map [*wrapper ]bool )))
x := 0.0
align := t .containerAlign ()
if side == "left" {
switch align {
case "right" :
x = W - containerWidth + dx
case "center" :
x = (W -containerWidth )/2 + dx
}
x -= bbox .L
} else {
switch align {
case "left" :
x = containerWidth
case "right" :
x = W + dx
default :
x = (containerWidth +W )/2 + dx
}
x -= bbox .L + t .data .L
}
t .table .place (x , 0 , labels )
element .Append (labels )
}
func (t *tableLayout ) handleLabels (element , labels *Element , dx float64 ) {
if !t .hasLabels || labels == nil || len (labels .Children ) == 0 {
return
}
side := tableString ("side" , t .table )
if t .isTop {
t .topTable (element , labels , side )
} else {
t .subTable (element , labels , side , dx )
}
}
func (w *wrapper ) tableToSVG (parent *Element ) {
element := w .standardSVG (parent )
t := resolvedTableLayout (w )
if background := tableBackground (element ); background != nil {
background .SetAttr ("width" , fixed (t .getWidth ()))
}
labels := NewElement ("g" ).SetAttr ("data-labels" , "true" )
if t .isTop {
labels .SetAttr ("transform" , "matrix(1 0 0 -1 0 0)" )
}
space := t .rowHalfSpacing ()
y := t .h - t .frameLine
for i , row := range t .rows {
h , d := t .rowHD (i )
topLine , bottomLine := t .frameLine , t .frameLine
if i > 0 {
topLine = t .rowLines [i -1 ]
}
if i < len (t .rowLines ) {
bottomLine = t .rowLines [i ]
}
row .renderTableRow (element , t , i , h , d , space [i ], space [i +1 ], topLine , bottomLine , labels )
baseline := y - space [i ] - h
row .place (0 , baseline )
y -= space [i ] + h + d + space [i +1 ] + bottomLine
}
t .addColumnLines (element )
t .addRowLines (element )
if t .frame && t .frameLine != 0 {
element .Append (t .makeFrame ())
}
dx := t .handlePWidth (element )
t .handleLabels (element , labels , dx )
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .