diff --git a/CSharpMath.Core.Tests/Atom/MulticolumnTests.cs b/CSharpMath.Core.Tests/Atom/MulticolumnTests.cs
new file mode 100644
index 00000000..11338f24
--- /dev/null
+++ b/CSharpMath.Core.Tests/Atom/MulticolumnTests.cs
@@ -0,0 +1,70 @@
+using System.Linq;
+using CSharpMath.Atom;
+using CSharpMath.Atom.Atoms;
+using Xunit;
+
+namespace CSharpMath.Core.AtomTests {
+ public class MulticolumnTests {
+ static Table Parse(string source) {
+ var result = LaTeXParser.MathListFromLaTeX(source);
+ Assert.Null(result.Error);
+ var (list, _) = result;
+ return Assert.IsType
(Assert.Single(list));
+ }
+
+ [Fact]
+ public void ParsesSpanAlignmentAndRoundTrips() {
+ var source = @"\begin{array}{|l|c|r|}\multicolumn{2}{l|}{a}&b\\c&d&e\end{array}";
+ var table = Parse(source);
+ Assert.Equal(3, table.NColumns);
+ Assert.Equal(2, table.GetColumnSpan(0, 0));
+ Assert.Equal(ColumnAlignment.Left, table.GetSpanAlignment(0, 0));
+ Assert.Equal("l|", table.SpanSpecifications[0][0]);
+ Assert.Contains(@"\multicolumn{2}{l|}", LaTeXParser.MathListToLaTeX(new MathList(table)).ToString());
+ Assert.Equal(table, Parse(LaTeXParser.MathListToLaTeX(new MathList(table)).ToString()));
+ }
+
+ [Theory]
+ [InlineData(@"\multicolumn{2}{c}{x}")]
+ [InlineData(@"\begin{array}{cc}\multicolumn{0}{c}{x}\end{array}")]
+ [InlineData(@"\begin{array}{cc}\multicolumn{3}{c}{x}\end{array}")]
+ [InlineData(@"\begin{array}{cc}\multicolumn{2}{x}{x}\end{array}")]
+ public void RejectsUnsupportedOrInvalidSpans(string source) {
+ Assert.NotNull(LaTeXParser.MathListFromLaTeX(source).Error);
+ }
+
+ [Fact]
+ public void MatrixSupportsSpans() {
+ var result = LaTeXParser.MathListFromLaTeX(@"\begin{matrix}\multicolumn{2}{c}{x}&y\\z&w\end{matrix}");
+ Assert.Null(result.Error);
+ var (list, _) = result;
+ var table = Assert.IsType(Assert.Single(list));
+ Assert.Equal(2, table.GetColumnSpan(0, 0));
+ }
+
+ [Fact]
+ public void SpanAlignmentAndFollowingCellsRemainDistinctAcrossRows() {
+ var table = Parse(@"\begin{array}{|l|c|r|}\hline\multicolumn{2}{|c|}{x}&z\\a&\multicolumn{2}{r|}{q}\end{array}");
+ Assert.Equal(3, table.NColumns);
+ Assert.Equal(2, table.GetColumnSpan(0, 0));
+ Assert.Equal(ColumnAlignment.Center, table.GetSpanAlignment(0, 0));
+ Assert.Equal(1, table.GetColumnSpan(0, 1));
+ Assert.Equal(2, table.GetColumnSpan(1, 1));
+ Assert.Equal(ColumnAlignment.Right, table.GetSpanAlignment(1, 1));
+ Assert.Equal(1, table.GetColumnSpan(-1, -1));
+ Assert.Null(table.GetSpanAlignment(-1, -1));
+ }
+
+ [Fact]
+ public void DelimitersAndHorizontalRulesComposeWithSpans() {
+ var result = LaTeXParser.MathListFromLaTeX(@"\left(\begin{array}{cc}\multicolumn{2}{c}{x}\\y&z\\\hline\end{array}\right)");
+ Assert.Null(result.Error);
+ var (parsed, _) = result;
+ var outer = Assert.IsType(Assert.Single(parsed));
+ var table = Assert.IsType(Assert.Single(outer.InnerList));
+ Assert.Equal(2, table.NColumns);
+ Assert.Equal(2, table.GetColumnSpan(0, 0));
+ Assert.NotEmpty(table.HorizontalLines);
+ }
+ }
+}
diff --git a/CSharpMath/Atom/Atoms/Table.cs b/CSharpMath/Atom/Atoms/Table.cs
index b596d181..9227ee66 100644
--- a/CSharpMath/Atom/Atoms/Table.cs
+++ b/CSharpMath/Atom/Atoms/Table.cs
@@ -2,6 +2,21 @@
using System.Linq;
namespace CSharpMath.Atom.Atoms {
+ internal sealed class MulticolumnAtom : MathAtom {
+ public MulticolumnAtom(int span, ColumnAlignment alignment, MathList content, string specification = "") : base(string.Empty) {
+ if (span < 1) throw new System.ArgumentOutOfRangeException(nameof(span));
+ Span = span; Alignment = alignment; Specification = specification; Content = content;
+ }
+ public int Span { get; }
+ public ColumnAlignment Alignment { get; }
+ public string Specification { get; }
+ public MathList Content { get; }
+ public override bool ScriptsAllowed => false;
+ protected override MathAtom CloneInside(bool finalize) => new MulticolumnAtom(Span, Alignment, Content.Clone(finalize), Specification);
+ public override string DebugString => $@"\multicolumn{{{Span}}}{{{Specification}}}{{{Content.DebugString}}}";
+ public override bool Equals(object obj) => obj is MulticolumnAtom m && Span == m.Span && Alignment == m.Alignment && Specification == m.Specification && Content.Equals(m.Content);
+ public override int GetHashCode() => (Span, Alignment, Specification, Content).GetHashCode();
+ }
///A table. Not part of TeX.
public sealed class Table : MathAtom, IMathListContainer {
public Table(string? environment, List>? cells = null) : base(string.Empty) =>
@@ -16,6 +31,9 @@ public Table() : this(null) { }
CellStyle = CellStyle,
VerticalLines = VerticalLines.ToList(),
HorizontalLines = HorizontalLines.ToList(),
+ ColumnSpans = ColumnSpans.Select(row => row.ToList()).ToList(),
+ SpanAlignments = SpanAlignments.Select(row => row.ToList()).ToList(),
+ SpanSpecifications = SpanSpecifications.Select(row => row.ToList()).ToList(),
Alignments = Alignments.ToList(),
Cells = new List>(Cells.Select(list =>
new List(list.Select(sublist => sublist.Clone(finalize)))))
@@ -43,12 +61,17 @@ public Table() : this(null) { }
/// environment). Length NRows+1: index 0 = above row 0 … NRows = below the last
/// row. Empty for every non-array environment.
public List HorizontalLines { get; set; } = new List();
+ /// Cell column spans, parallel to ; unspecified cells span one column.
+ public List> ColumnSpans { get; set; } = new List>();
+ /// Explicit alignment for spanning cells, parallel to .
+ public List> SpanAlignments { get; set; } = new List>();
+ public List> SpanSpecifications { get; set; } = new List>();
/// The name of the environment that this table denotes
public string? Environment { get; set; }
/// Number of rows
public int NRows => Cells.Count;
/// Number of columns
- public int NColumns => NRows == 0 ? 0 : Cells.Max(row => row.Count);
+ public int NColumns => NRows == 0 ? 0 : Cells.Select((row, i) => row.Select((_, j) => GetColumnSpan(i, j)).Sum()).DefaultIfEmpty(0).Max();
public void SetCell(MathList list, int iRow, int iColumn) {
while (Cells.Count <= iRow) Cells.Add(new List());
while (Cells[iRow].Count <= iColumn) Cells[iRow].Add(new MathList());
@@ -69,14 +92,21 @@ public bool EqualsTable(Table otherTable) =>
CellStyle == otherTable.CellStyle &&
VerticalLines.SequenceEqual(otherTable.VerticalLines) &&
HorizontalLines.SequenceEqual(otherTable.HorizontalLines) &&
+ ColumnSpans.SequenceEqual(otherTable.ColumnSpans, (a, b) => a.SequenceEqual(b)) &&
+ SpanAlignments.SequenceEqual(otherTable.SpanAlignments, (a, b) => a.SequenceEqual(b)) &&
+ SpanSpecifications.SequenceEqual(otherTable.SpanSpecifications, (a, b) => a.SequenceEqual(b)) &&
Environment == otherTable.Environment;
public override bool Equals(object obj) => obj is Table t ? EqualsTable(t) : false;
public override int GetHashCode() =>
(base.GetHashCode(), NestedSequenceHash(Cells), SequenceHash(Alignments),
InterColumnSpacing, InterRowAdditionalSpacing, CellStyle,
- (SequenceHash(VerticalLines), SequenceHash(HorizontalLines), Environment)).GetHashCode();
+ (SequenceHash(VerticalLines), SequenceHash(HorizontalLines),
+ NestedSequenceHash(ColumnSpans), NestedSequenceHash(SpanAlignments),
+ NestedSequenceHash(SpanSpecifications), Environment)).GetHashCode();
private static int NestedSequenceHash(IEnumerable> rows) =>
SequenceHash(rows.Select(SequenceHash));
+ private static int NestedSequenceHash(IEnumerable> rows) =>
+ SequenceHash(rows.Select(SequenceHash));
private static int SequenceHash(IEnumerable items) {
unchecked {
var hash = 17;
@@ -84,5 +114,21 @@ private static int SequenceHash(IEnumerable items) {
return hash;
}
}
+ public int GetColumnSpan(int row, int column) => row >= 0 && column >= 0 && row < ColumnSpans.Count && column < ColumnSpans[row].Count
+ ? ColumnSpans[row][column] : 1;
+ public ColumnAlignment? GetSpanAlignment(int row, int column) => row >= 0 && column >= 0 && row < SpanAlignments.Count && column < SpanAlignments[row].Count
+ ? SpanAlignments[row][column] : null;
+ internal void SetColumnSpan(int row, int column, int span, ColumnAlignment alignment, string specification = "") {
+ if (span < 1) throw new System.ArgumentOutOfRangeException(nameof(span));
+ while (ColumnSpans.Count <= row) ColumnSpans.Add(new List());
+ while (ColumnSpans[row].Count <= column) ColumnSpans[row].Add(1);
+ ColumnSpans[row][column] = span;
+ while (SpanAlignments.Count <= row) SpanAlignments.Add(new List());
+ while (SpanAlignments[row].Count <= column) SpanAlignments[row].Add(null);
+ SpanAlignments[row][column] = alignment;
+ while (SpanSpecifications.Count <= row) SpanSpecifications.Add(new List());
+ while (SpanSpecifications[row].Count <= column) SpanSpecifications[row].Add(null);
+ SpanSpecifications[row][column] = specification;
+ }
}
}
diff --git a/CSharpMath/Atom/LaTeXParser.cs b/CSharpMath/Atom/LaTeXParser.cs
index 422ac411..bcf96a9c 100644
--- a/CSharpMath/Atom/LaTeXParser.cs
+++ b/CSharpMath/Atom/LaTeXParser.cs
@@ -812,6 +812,15 @@ void RecordHorizontalLine() {
case var _ when _matrixEnvironments.TryGetValue(name, out var delimiters):
table.Environment = "matrix"; // TableEnvironment is set to matrix as delimiters are converted to latex outside the table.
table.InterColumnSpacing = 18;
+ for (var r = 0; r < table.Cells.Count; r++) {
+ var logicalColumn = 0;
+ for (var c = 0; c < table.Cells[r].Count; c++, logicalColumn++)
+ if (table.Cells[r][c].Count == 1 && table.Cells[r][c][0] is MulticolumnAtom multi) {
+ table.Cells[r][c] = multi.Content;
+ table.SetColumnSpan(r, c, multi.Span, multi.Alignment, multi.Specification);
+ logicalColumn += multi.Span - 1;
+ }
+ }
// All the cells render in textstyle, stored on the table rather than per-cell.
table.CellStyle = LineStyle.Text;
return delimiters switch {
@@ -851,6 +860,21 @@ void RecordHorizontalLine() {
break;
}
}
+ var declaredColumns = table.Alignments.Count;
+ for (var r = 0; r < table.Cells.Count; r++) {
+ var logicalColumn = 0;
+ for (var c = 0; c < table.Cells[r].Count; c++, logicalColumn++) {
+ if (table.Cells[r][c].Count != 1 || table.Cells[r][c][0] is not MulticolumnAtom multi) continue;
+ if (multi.Span > declaredColumns - logicalColumn)
+ return @"\multicolumn span exceeds the array column count or overlaps another cell";
+ table.Cells[r][c] = multi.Content;
+ table.SetColumnSpan(r, c, multi.Span, multi.Alignment, multi.Specification);
+ logicalColumn += multi.Span - 1;
+ }
+ // Preserve the historical array behavior: ordinary cells beyond
+ // the declared specification are tolerated and dropped by the
+ // renderer. Multicolumn spans remain strictly validated above.
+ }
// Note: rows may declare fewer/more cells than the spec; extra cells are
// dropped and missing ones render empty, matching pre-port behavior.
while (vLines.Count < table.NColumns + 1) vLines.Add(0);
@@ -1275,6 +1299,13 @@ static string Wrap(MathList operand, Atoms.FractionStyle style) {
}
for (int j = 0; j < row.Count; j++) {
var cell = row[j];
+ var span = table.GetColumnSpan(i, j);
+ if (span > 1) {
+ var spec = table.SpanSpecifications.Count > i && table.SpanSpecifications[i].Count > j
+ ? table.SpanSpecifications[i][j] : null;
+ builder.Append(@"\multicolumn{").Append(span).Append("}{")
+ .Append(spec ?? table.GetAlignment(j).ToString().ToLowerInvariant()).Append("}{");
+ }
if (table.Environment == "matrix"
&& cell.Count >= 1
&& cell[0] is Style) {
@@ -1303,6 +1334,7 @@ static string Wrap(MathList operand, Atoms.FractionStyle style) {
cell = cell.Slice(1, cell.Count - 1);
}
MathListToLaTeX(cell, builder, currentFontStyle);
+ if (span > 1) builder.Append('}');
if (j < row.Count - 1) {
builder.Append('&');
}
diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs
index 2efa1eca..a34f2801 100644
--- a/CSharpMath/Atom/LaTeXSettings.cs
+++ b/CSharpMath/Atom/LaTeXSettings.cs
@@ -200,6 +200,23 @@ public static class LaTeXSettings {
{ @"\begin", (parser, accumulate, stopChar) =>
parser.ReadEnvironment().Bind(env =>
parser.ReadTable(env, null, false, stopChar)).Bind(Ok) },
+ { @"\multicolumn", (parser, accumulate, stopChar) => {
+ var environment = parser.Environments.PeekOrDefault() as LaTeXParser.TableEnvironment;
+ if (environment?.Name is not ("array" or "matrix" or "pmatrix" or "bmatrix" or "Bmatrix" or "vmatrix" or "Vmatrix"))
+ return @"\multicolumn is only valid inside an array or matrix";
+ var (countText, countError) = parser.ReadRawArgument();
+ if (countError != null || !int.TryParse(countText, out var count) || count < 1)
+ return @"\multicolumn requires a positive span count";
+ var (spec, specError) = parser.ReadRawArgument();
+ if (specError != null || string.IsNullOrWhiteSpace(spec) || spec.Any(c => c is not ('l' or 'c' or 'r' or '|')) || !spec.Any(c => c is 'l' or 'c' or 'r'))
+ return @"\multicolumn requires an l, c, or r alignment specification";
+ var alignment = spec.First(c => c is 'l' or 'c' or 'r') switch {
+ 'l' => ColumnAlignment.Left,
+ 'c' => ColumnAlignment.Center,
+ _ => ColumnAlignment.Right
+ };
+ return parser.ReadArgument().Bind(content => ((MathAtom?)new MulticolumnAtom(count, alignment, content, spec), (MathList?)null));
+ } },
// \color and its 2-argument alias \textcolor
{ @"\color", (parser, accumulate, stopChar) =>
parser.ReadColor().Bind(
diff --git a/CSharpMath/Display/Typesetter.cs b/CSharpMath/Display/Typesetter.cs
index 09bac458..f37d5d9f 100644
--- a/CSharpMath/Display/Typesetter.cs
+++ b/CSharpMath/Display/Typesetter.cs
@@ -1274,10 +1274,28 @@ private List>> TypesetCells(Table table, float[]
r.Add(colDispalys);
for (int i = 0; i < row.Count; i++) {
var disp = CreateLine(row[i], _font, _context, cellStyle, false);
- columnWidths[i] = Math.Max(disp.Width, columnWidths[i]);
+ var column = row.Take(i).Select((_, j) => table.GetColumnSpan(r.Count - 1, j)).Sum();
+ var span = table.GetColumnSpan(r.Count - 1, i);
+ if (span == 1) columnWidths[column] = Math.Max(disp.Width, columnWidths[column]);
colDispalys.Add(disp);
}
}
+ // A spanning cell may be the only contributor to one or more logical
+ // columns. Allocate its excess width across those columns so the frame
+ // remains wide enough and inter-column spacing is counted exactly once.
+ for (var rowIndex = 0; rowIndex < r.Count; rowIndex++) {
+ var column = 0;
+ for (var cell = 0; cell < r[rowIndex].Count; cell++) {
+ var span = table.GetColumnSpan(rowIndex, cell);
+ if (span > 1 && column + span <= columnWidths.Length) {
+ var spacing = (span - 1) * table.InterColumnSpacing * CellStyleFontSize(table) / 18f;
+ var excess = r[rowIndex][cell].Width - (Enumerable.Range(column, span).Sum(c => columnWidths[c]) + spacing);
+ if (excess > 0)
+ for (var c = column; c < column + span; c++) columnWidths[c] += excess / span;
+ }
+ column += span;
+ }
+ }
return r;
}
/// The line style the cells of this table actually render in. Some envs
@@ -1305,8 +1323,8 @@ private IDisplay MakeTable(Table table) {
List>? verticalRuleXs = null;
if (!hasRules) {
var rowDisplays = new List>();
- foreach (var row in displays) {
- rowDisplays.Add(MakeRowWithColumns(row, table, columnWidths));
+ for (var rowIndex = 0; rowIndex < displays.Count; rowIndex++) {
+ rowDisplays.Add(MakeRowWithColumns(displays[rowIndex], table, columnWidths, rowIndex));
}
// position all the rows
PositionRows(rowDisplays, table);
@@ -1324,6 +1342,21 @@ private IDisplay MakeTable(Table table) {
float padding = rulePaddingMultiplier * _styleFont.PointSize;
float ruleGap = ruleGapMultiplier * _styleFont.PointSize;
float cellStyleMuUnit = CellStyleFontSize(table) / 18f;
+ var suppressedBoundaries = new HashSet();
+ for (var row = 0; row < table.Cells.Count; row++) {
+ var column = 0;
+ for (var cell = 0; cell < table.Cells[row].Count; cell++) {
+ var span = table.GetColumnSpan(row, cell);
+ if (span > 1) {
+ var specification = table.SpanSpecifications.Count > row && table.SpanSpecifications[row].Count > cell
+ ? table.SpanSpecifications[row][cell] ?? "" : "";
+ for (var boundary = column + 1; boundary < column + span; boundary++) suppressedBoundaries.Add(boundary);
+ if (!specification.StartsWith("|", StringComparison.Ordinal)) suppressedBoundaries.Add(column);
+ if (!specification.EndsWith("|", StringComparison.Ordinal)) suppressedBoundaries.Add(column + span);
+ }
+ column += span;
+ }
+ }
columnOffsets = new float[nColumns];
verticalRuleXs = new List>();
@@ -1331,7 +1364,7 @@ private IDisplay MakeTable(Table table) {
for (int boundary = 0; boundary <= nColumns; boundary++) {
float gapBase = boundary == 0 || boundary == nColumns
? 0 : table.InterColumnSpacing * cellStyleMuUnit;
- int count = boundary < table.VerticalLines.Count ? table.VerticalLines[boundary] : 0;
+ int count = suppressedBoundaries.Contains(boundary) ? 0 : boundary < table.VerticalLines.Count ? table.VerticalLines[boundary] : 0;
var ruleXs = new List();
if (count > 0) {
// No padding outside the outermost rules so they sit flush at the box edges.
@@ -1355,8 +1388,8 @@ private IDisplay MakeTable(Table table) {
float contentWidth = x;
var ruledRowDisplays = new List>();
- foreach (var row in displays) {
- ruledRowDisplays.Add(MakeRuledRowWithColumns(row, table, columnWidths, columnOffsets));
+ for (var rowIndex = 0; rowIndex < displays.Count; rowIndex++) {
+ ruledRowDisplays.Add(MakeRuledRowWithColumns(displays[rowIndex], table, columnWidths, columnOffsets, rowIndex));
}
// position all the rows
@@ -1414,22 +1447,27 @@ private IDisplay MakeTable(Table table) {
/// Like MakeRowWithColumns but using precomputed shared column offsets.
private ListDisplay MakeRuledRowWithColumns(
- List> row, Table table, float[] columnWidths, float[] columnOffsets) {
+ List> row, Table table, float[] columnWidths, float[] columnOffsets, int rowIndex) {
Range rowRange = Range.NotFound;
+ var column = 0;
for (int i = 0; i < row.Count; i++) {
var entry = row[i];
- var alignment = table.GetAlignment(i);
- var cellPosition = columnOffsets[i];
+ var span = table.GetColumnSpan(rowIndex, i);
+ var width = Enumerable.Range(column, span).Sum(c => columnWidths[c])
+ + (span - 1) * table.InterColumnSpacing * CellStyleFontSize(table) / 18f;
+ var alignment = table.GetSpanAlignment(rowIndex, i) ?? table.GetAlignment(column);
+ var cellPosition = columnOffsets[column];
switch (alignment) {
case ColumnAlignment.Right:
- cellPosition += (columnWidths[i] - entry.Width);
+ cellPosition += width - entry.Width;
break;
case ColumnAlignment.Center:
- cellPosition += (columnWidths[i] - entry.Width) / 2;
+ cellPosition += (width - entry.Width) / 2;
break;
}
entry.Position = new PointF(cellPosition, 0);
rowRange += entry.Range;
+ column += span;
}
var ruled = new ListDisplay(row.ToArray());
if (rowRange != Range.NotFound) {
@@ -1439,14 +1477,17 @@ private ListDisplay MakeRuledRowWithColumns(
}
private ListDisplay MakeRowWithColumns
- (List> row, Table table, float[] columnWidths) {
+ (List> row, Table table, float[] columnWidths, int rowIndex) {
float columnStart = 0;
+ var column = 0;
Range rowRange = Range.NotFound;
float cellStyleMuUnit = CellStyleFontSize(table) / 18f;
for (int i = 0; i < row.Count; i++) {
var entry = row[i];
- float columnWidth = columnWidths[i];
- var alignment = table.GetAlignment(i);
+ var span = table.GetColumnSpan(rowIndex, i);
+ float columnWidth = Enumerable.Range(column, span).Sum(c => columnWidths[c])
+ + (span - 1) * table.InterColumnSpacing * cellStyleMuUnit;
+ var alignment = table.GetSpanAlignment(rowIndex, i) ?? table.GetAlignment(column);
var cellPosition = columnStart;
switch (alignment) {
case ColumnAlignment.Right:
@@ -1458,7 +1499,8 @@ private ListDisplay MakeRowWithColumns
}
entry.Position = new PointF(cellPosition, 0);
rowRange += entry.Range;
- columnStart += (columnWidth + table.InterColumnSpacing * cellStyleMuUnit);
+ columnStart += columnWidth + table.InterColumnSpacing * cellStyleMuUnit;
+ column += span;
}
return new ListDisplay(row.ToArray());
}
diff --git a/CSharpMath/PublicAPI.Unshipped.txt b/CSharpMath/PublicAPI.Unshipped.txt
index c035cc15..f6af1c02 100644
--- a/CSharpMath/PublicAPI.Unshipped.txt
+++ b/CSharpMath/PublicAPI.Unshipped.txt
@@ -212,3 +212,11 @@ virtual CSharpMath.Atom.MathAtom.Nucleus.set -> void
*REMOVED*CSharpMath.Atom.MathAtom.FontStyle.set -> void
*REMOVED*CSharpMath.Atom.MathAtom.Nucleus.get -> string!
*REMOVED*CSharpMath.Atom.MathAtom.Nucleus.set -> void
+CSharpMath.Atom.Atoms.Table.ColumnSpans.get -> System.Collections.Generic.List!>!
+CSharpMath.Atom.Atoms.Table.ColumnSpans.set -> void
+CSharpMath.Atom.Atoms.Table.SpanAlignments.get -> System.Collections.Generic.List!>!
+CSharpMath.Atom.Atoms.Table.SpanAlignments.set -> void
+CSharpMath.Atom.Atoms.Table.SpanSpecifications.get -> System.Collections.Generic.List!>!
+CSharpMath.Atom.Atoms.Table.SpanSpecifications.set -> void
+CSharpMath.Atom.Atoms.Table.GetColumnSpan(int row, int column) -> int
+CSharpMath.Atom.Atoms.Table.GetSpanAlignment(int row, int column) -> CSharpMath.Atom.ColumnAlignment?