import { Atom , Branch , isColRowBranch , isNamedBranch , ToLatexOptions , } from '../core/atom-class' ; import { Box } from '../core/box' ; import { VBox , VBoxElementAndShift } from '../core/v-box' ; import { makeLeftRightDelim } from '../core/delimiters' ; import { MathstyleName } from '../core/mathstyle' ; import { Context } from '../core/context' ; import { joinLatex } from '../core/tokenizer' ; import { PlaceholderAtom } from './placeholder' ; import { AXIS_HEIGHT , BASELINE_SKIP } from '../core/font-metrics' ; import { Dimension } from '../public/core' ; import { convertDimensionToEm } from '../core/registers-utils' ; export type ColumnFormat = { align ? : 'l' | 'c' | 'r' | 'm' ; } | { gap ? : number | Atom [ ] ; } | { separator ? : 'solid' | 'dashed' ; } ; export type ColSeparationType = 'align' | 'alignat' | 'gather' | 'small' | 'CD' | undefined ; export type ArrayAtomConstructorOptions = { mathstyleName ? : MathstyleName ; columns ? : ColumnFormat [ ] ; colSeparationType ? : ColSeparationType ; leftDelim ? : string ; rightDelim ? : string ; jot ? : number ; arraystretch ? : number ; arraycolsep ? : number ; } ; type ArrayRow = { cells : Box [ ] ; height : number ; depth : number ; pos : number ; } ; /**
 * Normalize the array:
 * - ensure it is dense (not sparse)
 * - fold rows that overflow (longer than maximum number of columns)
 * - ensure each cell begins with a `first` atom
 * - remove last row if empty
 */ function normalizeArray ( atom , array , colFormat ) { let maxColCount = 0 ; for ( const colSpec of colFormat ) { if ( 'align' in colSpec ) maxColCount += 1 ; } let colCount = 0 ; const rows = [ ] ; for ( const row of array ) { let colIndex = 0 ; colCount = Math . max ( colCount , Math . min ( row . length , maxColCount ) ) ; while ( colIndex < row . length ) { const newRow = [ ] ; const lastCol = Math . min ( row . length , colIndex + maxColCount ) ; while ( colIndex < lastCol ) { if ( row [ colIndex ] . length > 0 && row [ colIndex ] [ 0 ] . type !== 'first' ) { newRow . push ( [ new Atom ( 'first' , { mode : atom . mode } ) , ... row [ colIndex ] , ] ) ; } else { newRow . push ( row [ colIndex ] ) ; } colIndex += 1 ; } rows . push ( newRow ) ; } } if ( rows [ rows . length - 1 ] . length === 1 && rows [ rows . length - 1 ] [ 0 ] . length === 0 ) { rows . pop ( ) ; } const result = [ ] ; for ( const row of rows ) { if ( row . length !== colCount ) { for ( let i = row . length ; i < colCount ; i ++ ) { row . push ( [ new Atom ( 'first' , { mode : atom . mode } ) , new PlaceholderAtom ( ) , ] ) ; } } result . push ( row ) ; } let rowIndex = 0 ; let colIndex = 0 ; for ( const row of result ) { colIndex = 0 ; for ( const cell of row ) { for ( const element of cell ) { element . parent = atom ; element . treeBranch = [ rowIndex , colIndex ] ; } colIndex += 1 ; } rowIndex += 1 ; } atom . isDirty = true ; return result ; } export class ArrayAtom extends Atom { array : ( undefined | Atom [ ] ) [ ] [ ] ; environmentName : string ; rowGaps : Dimension [ ] ; colFormat : ColumnFormat [ ] ; arraystretch ? : number ; arraycolsep ? : number ; colSeparationType ? : ColSeparationType ; jot ? : number ; leftDelim ? : string ; rightDelim ? : string ; mathstyleName ? : MathstyleName ; constructor ( envName , array , rowGaps , options = { } ) { super ( 'array' ) ; this . environmentName = envName ; this . rowGaps = rowGaps ; if ( options . mathstyleName ) this . mathstyleName = options . mathstyleName ; if ( options . columns ) { if ( options . columns . length === 0 ) { this . colFormat = [ { align : 'l' } ] ; } else { this . colFormat = options . columns ; } } if ( ! this . colFormat ) { this . colFormat = [ { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , { align : 'l' } , ] ; } this . array = normalizeArray ( this , array , this . colFormat ) ; if ( options . leftDelim ) this . leftDelim = options . leftDelim ; if ( options . rightDelim ) this . rightDelim = options . rightDelim ; if ( options . jot !== undefined ) this . jot = options . jot ; if ( options . arraycolsep ) this . arraycolsep = options . arraycolsep ; this . colSeparationType = options . colSeparationType ; this . arraystretch = options . arraystretch ?? 1.0 ; } branch ( cell ) { if ( ! isColRowBranch ( cell ) ) return undefined ; return this . array [ cell [ 0 ] ] [ cell [ 1 ] ] ?? undefined ; } get branches ( ) : Branch [ ] { const result = super . branches ; this . array . forEach ( ( _ , col ) => { this . array [ col ] . forEach ( ( _ , row ) => { if ( this . array [ col ] [ row ] ) { result . push ( [ col , row ] ) ; } } ) ; } ) ; return result ; } createBranch ( cell ) { if ( ! isColRowBranch ( cell ) ) return [ ] ; this . isDirty = true ; return this . branch ( cell ) ?? [ ] ; } get rowCount ( ) : number { return this . array . length ; } get colCount ( ) : number { return this . array [ 0 ] . length ; } removeBranch ( name ) { if ( isNamedBranch ( name ) ) { return super . removeBranch ( name ) ; } const children = this . branch ( name ) ! ; this . array [ name [ 0 ] ] [ name [ 1 ] ] = undefined ; children . forEach ( ( x ) => { x . parent = undefined ; x . treeBranch = undefined ; } ) ; console . assert ( children [ 0 ] . type === 'first' ) ; children . shift ( ) ; this . isDirty = true ; return children ; } get hasChildren ( ) : boolean { return this . children . length > 0 ; } get children ( ) : Atom [ ] { const result = [ ] ; for ( const row of this . array ) { for ( const cell of row ) { if ( cell ) { for ( const atom of cell ) { result . push ( ... atom . children ) ; result . push ( atom ) ; } } } } return [ ... result , ... super . children ] ; } render ( context ) { const innerContext = new Context ( context , this . style , this . mathstyleName ) ; const arrayRuleWidth = innerContext . getRegisterAsEm ( 'arrayrulewidth' ) ; const arrayColSep = innerContext . getRegisterAsEm ( 'arraycolsep' ) ; const doubleRuleSep = innerContext . getRegisterAsEm ( 'doublerulesep' ) ; const arraystretch = this . arraystretch ?? 1.0 ; let arraycolsep = typeof this . arraycolsep === 'number' ? this . arraycolsep : arrayColSep ; if ( this . colSeparationType === 'small' ) { const localMultiplier = new Context ( context , undefined , 'scriptstyle' ) . scalingFactor ; arraycolsep = 0.2778 * ( localMultiplier / context . scalingFactor ) ; } const arrayskip = arraystretch * BASELINE_SKIP ; const arstrutHeight = 0.7 * arrayskip ; const arstrutDepth = 0.3 * arrayskip ; let totalHeight = 0 ; const body = [ ] ; let nc = 0 ; const nr = this . array . length ; for ( let r = 0 ; r < nr ; ++ r ) { const inrow = this . array [ r ] ; nc = Math . max ( nc , inrow . length ) ; const cellContext = new Context ( innerContext , this . style , this . mathstyleName ) ; let height = arstrutHeight / cellContext . scalingFactor ; let depth = arstrutDepth / cellContext . scalingFactor ; const outrow = { cells : [ ] , height : 0 , depth : 0 , pos : 0 } ; for ( const element of inrow ) { const elt = Atom . createBox ( cellContext , element , { newList : true } ) ?? new Box ( null , { newList : true } ) ; depth = Math . max ( depth , elt . depth ) ; height = Math . max ( height , elt . height ) ; outrow . cells . push ( elt ) ; } let gap = convertDimensionToEm ( this . rowGaps [ r ] ) ?? 0 ; if ( gap > 0 ) { gap += arstrutDepth ; depth = Math . max ( depth , gap ) ; gap = 0 ; } if ( this . jot !== undefined ) { depth += this . jot ; } outrow . height = height ; outrow . depth = depth ; totalHeight += height ; outrow . pos = totalHeight ; totalHeight += depth + gap ; body . push ( outrow ) ; } const offset = totalHeight / 2 + AXIS_HEIGHT ; const contentCols = [ ] ; for ( let colIndex = 0 ; colIndex < nc ; colIndex ++ ) { const stack = [ ] ; for ( const row of body ) { const element = row . cells [ colIndex ] ; element . depth = row . depth ; element . height = row . height ; stack . push ( { box : element , shift : row . pos - offset } ) ; } if ( stack . length > 0 ) { contentCols . push ( new VBox ( { individualShift : stack } ) ) ; } } const cols = [ ] ; let previousColContent = false ; let previousColRule = false ; let currentContentCol = 0 ; let firstColumn = ! this . leftDelim ; const { colFormat } = this ; for ( const colDesc of colFormat ) { if ( 'align' in colDesc && currentContentCol >= contentCols . length ) { break ; } if ( 'align' in colDesc ) { if ( previousColContent ) { cols . push ( makeColGap ( 2 * arraycolsep ) ) ; } else if ( previousColRule || firstColumn ) { cols . push ( makeColGap ( arraycolsep ) ) ; } cols . push ( new Box ( contentCols [ currentContentCol ] , { classes : 'col-align-' + colDesc . align , } ) ) ; currentContentCol ++ ; previousColContent = true ; previousColRule = false ; firstColumn = false ; } else if ( 'gap' in colDesc ) { if ( typeof colDesc . gap === 'number' ) { cols . push ( makeColGap ( colDesc . gap ) ) ; } else { const col = makeColOfRepeatingElements ( context , body , offset , colDesc . gap ) ; if ( col ) cols . push ( col ) ; } previousColContent = false ; previousColRule = false ; firstColumn = false ; } else if ( 'separator' in colDesc ) { const separator = new Box ( null , { classes : 'vertical-separator' } ) ; separator . setStyle ( 'height' , totalHeight , 'em' ) ; separator . setStyle ( 'border-right' , `${ arrayRuleWidth }em ${ colDesc . separator } currentColor` ) ; separator . setStyle ( 'vertical-align' , - ( totalHeight - offset ) , 'em' ) ; let gap = 0 ; if ( previousColRule ) { gap = doubleRuleSep - arrayRuleWidth ; } else if ( previousColContent ) { gap = arraycolsep - arrayRuleWidth ; } separator . left = gap ; cols . push ( separator ) ; previousColContent = false ; previousColRule = true ; firstColumn = false ; } } if ( previousColContent && ! this . rightDelim ) { cols . push ( makeColGap ( arraycolsep ) ) ; } const inner = new Box ( cols , { classes : 'mtable' } ) ; if ( ( ! this . leftDelim || this . leftDelim === '.' ) && ( ! this . rightDelim || this . rightDelim === '.' ) ) { return inner ; } const innerHeight = inner . height ; const innerDepth = inner . depth ; const result = this . bind ( context , new Box ( [ this . bind ( context , makeLeftRightDelim ( 'mopen' , this . leftDelim ?? '.' , innerHeight , innerDepth , innerContext ) ) , inner , this . bind ( context , makeLeftRightDelim ( 'mclose' , this . rightDelim ?? '.' , innerHeight , innerDepth , innerContext ) ) , ] , { type : 'mord' } ) ) ; if ( ! result ) return null ; if ( this . caret ) result . caret = this . caret ; return this . attachSupsub ( context , { base : result } ) ; } serialize ( options ) { let result = '\\begin{' + this . environmentName + '}' ; if ( this . environmentName === 'array' ) { result += '{' ; if ( this . colFormat !== undefined ) { for ( const format of this . colFormat ) { if ( 'align' in format ) { result += format . align ; } else if ( 'separator' in format && format . separator === 'solid' ) { result += '|' ; } else if ( 'separator' in format && format . separator === 'dashed' ) { result += ':' ; } } } result += '}' ; } for ( let row = 0 ; row < this . array . length ; row ++ ) { for ( let col = 0 ; col < this . array [ row ] . length ; col ++ ) { if ( col > 0 ) result += ' & ' ; result = joinLatex ( [ result , Atom . serialize ( this . array [ row ] [ col ] , options ) , ] ) ; } if ( row < this . array . length - 1 ) { result += ' \\\\ ' ; } } result += '\\end{' + this . environmentName + '}' ; return result ; } getCell ( row , col ) { return this . array [ row ] [ col ] ; } setCell ( _row , _column , _value ) { console . assert ( this . type === 'array' && Array . isArray ( this . array ) ) ; this . isDirty = true ; } addRowBefore ( _row ) { console . assert ( this . type === 'array' && Array . isArray ( this . array ) ) ; this . isDirty = true ; } addRowAfter ( _row ) { console . assert ( this . type === 'array' && Array . isArray ( this . array ) ) ; this . isDirty = true ; } addColumnBefore ( _col ) { console . assert ( this . type === 'array' && Array . isArray ( this . array ) ) ; this . isDirty = true ; } addColumnAfter ( _col ) { console . assert ( this . type === 'array' && Array . isArray ( this . array ) ) ; this . isDirty = true ; } get cells ( ) : Atom [ ] [ ] { const result = [ ] ; for ( const row of this . array ) { for ( const cell of row ) { if ( cell ) result . push ( cell ) ; } } return result ; } } /**
 * Create a column separator box.
 *
 */ function makeColGap ( width ) { const separator = new Box ( null , { classes : 'arraycolsep' } ) ; separator . width = width ; return separator ; } /**
 * Create a column of repeating elements.
 */ function makeColOfRepeatingElements ( context , rows , offset , element ) { if ( ! element ) return null ; const col = [ ] ; for ( const row of rows ) { const cell = Atom . createBox ( context , element , { newList : true } ) ; if ( cell ) { cell . depth = row . depth ; cell . height = row . height ; col . push ( { box : cell , shift : row . pos - offset } ) ; } } return new VBox ( { individualShift : col } ) . wrap ( context ) ; }