Skip to content

Commit cc62444

Browse files
kostubclaude
andauthored
Add LaTeX array environment (PR 1/3): model fields + factory constructor (#251)
* [item 1] Add verticalLines/horizontalLines fields to MTMathTable Adds inert NSArray<NSNumber*>* verticalLines/horizontalLines properties to MTMathTable, defaulting to empty arrays and deep-copied in -copyWithZone:. No behavior change for existing environments (fields are unused until the array environment factory constructor consumes them). * [item 2] Add +arrayTableWithAlignments factory constructor Adds MTMathAtomFactory +arrayTableWithAlignments:verticalLines:horizontalLines:rows:error:, which builds a bare textstyle MTMathTable for the array environment from already-validated structured input (alignments/vertical-line counts computed by the future column-spec parser). horizontalLines is normalized to numRows+1 entries so the renderer can index every row boundary. * [item 3] Validate array row cell counts (too-many-cells / short-row) Pins the too-many-cells and short-row contract of +arrayTableWithAlignments:verticalLines:horizontalLines:rows:error: with explicit tests. The validation guard itself was already introduced in item 2; no production code changes here. * Declare verticalLines/horizontalLines copy for snapshot safety The new NSArray properties were strong (default), so the setter aliased the caller's array by reference. The factory itself assigns a mutable hLines behind the NSArray*-typed horizontalLines, so a later in-place mutation would leak into the stored table. Declare both properties copy (the standard Cocoa idiom for an immutable-snapshot NSArray, O(1) when the source is already immutable). With the copy setters, copyWithZone: no longer needs arrayWithArray: — a plain assignment snapshots. Adds a test that mutating a mutable array after assignment does not change the stored value. * Pad short array rows to numCols; normalize verticalLines (review) Address gemini review on #251: - Pad short rows out to numCols so a trailing all-empty column keeps its alignment and vertical rule (numColumns is derived from the widest row). - Normalize verticalLines to numCols+1 (mirrors horizontalLines) and guard nil for both nonnull line params. Not adopted: the reviewer's out-of-bounds crash framing and the truncation of over-long line arrays. -columnOffsetsForTable: already bounds-guards every access (i < vLines.count ? ... : 0) and the rule loops iterate the arrays' own counts, so a short/over-long array cannot crash; truncating would also silently swallow caller mistakes. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c27737a commit cc62444

5 files changed

Lines changed: 207 additions & 0 deletions

File tree

iosMath/lib/MTMathAtomFactory.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,21 @@ FOUNDATION_EXPORT NSString *const MTSymbolDegree;
210210
matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms.
211211
*/
212212
+ (nullable MTMathAtom*) tableWithEnvironment:(nullable NSString*) env rows:(NSArray<NSArray<MTMathList*>*>*) rows error:(NSError**) error;
213+
214+
/**
215+
Construct a bare `array`-environment table from already-validated structured input.
216+
The parser owns all column-spec syntax validation; this method only validates row
217+
cell counts against `columnAlignments.count` and assembles the table.
218+
@param columnAlignments one MTColumnAlignment per declared column.
219+
@param verticalLines `|` counts per column boundary (length columnAlignments.count+1).
220+
@param horizontalLines `\hline` counts per row boundary (normalized here to numRows+1).
221+
@returns a bare MTMathTable (no delimiters), or nil + error on too-many-cells.
222+
*/
223+
+ (nullable MTMathAtom*) arrayTableWithAlignments:(NSArray<NSNumber*>*) columnAlignments
224+
verticalLines:(NSArray<NSNumber*>*) verticalLines
225+
horizontalLines:(NSArray<NSNumber*>*) horizontalLines
226+
rows:(NSArray<NSArray<MTMathList*>*>*) rows
227+
error:(NSError**) error;
213228
@end
214229

215230
NS_ASSUME_NONNULL_END

iosMath/lib/MTMathAtomFactory.m

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,59 @@ + (nullable MTMathAtom *)tableWithEnvironment:(NSString *)env rows:(NSArray<NSAr
527527
return nil;
528528
}
529529

530+
+ (nullable MTMathAtom*) arrayTableWithAlignments:(NSArray<NSNumber*>*) columnAlignments
531+
verticalLines:(NSArray<NSNumber*>*) verticalLines
532+
horizontalLines:(NSArray<NSNumber*>*) horizontalLines
533+
rows:(NSArray<NSArray<MTMathList*>*>*) rows
534+
error:(NSError**) error
535+
{
536+
MTMathTable* table = [[MTMathTable alloc] initWithEnvironment:@"array"];
537+
NSInteger numCols = columnAlignments.count;
538+
for (int i = 0; i < rows.count; i++) {
539+
NSArray<MTMathList*>* row = rows[i];
540+
if ((NSInteger) row.count > numCols) {
541+
if (error) {
542+
NSString* message = [NSString stringWithFormat:
543+
@"array row has %ld cells but column specification declares %ld columns",
544+
(long) row.count, (long) numCols];
545+
*error = [NSError errorWithDomain:MTParseError
546+
code:MTParseErrorInvalidNumColumns
547+
userInfo:@{ NSLocalizedDescriptionKey : message }];
548+
}
549+
return nil;
550+
}
551+
for (int j = 0; j < row.count; j++) {
552+
[table setCell:row[j] forRow:i column:j];
553+
}
554+
// Pad short rows out to numCols. numColumns is derived from the widest row,
555+
// so without this a trailing all-empty column (its alignment + vertical rule)
556+
// would be silently dropped. setCell backfills the intermediate cells.
557+
if ((NSInteger) row.count < numCols) {
558+
[table setCell:[MTMathList new] forRow:i column:numCols - 1];
559+
}
560+
}
561+
for (int j = 0; j < numCols; j++) {
562+
[table setAlignment:columnAlignments[j].integerValue forColumn:j];
563+
}
564+
// Normalize verticalLines to length numCols+1 so every declared column boundary is
565+
// represented (mirrors horizontalLines below). Guard nil for the nonnull property.
566+
NSMutableArray<NSNumber*>* vLines = [NSMutableArray arrayWithArray:verticalLines ?: @[]];
567+
while ((NSInteger) vLines.count < numCols + 1) {
568+
[vLines addObject:@0];
569+
}
570+
table.verticalLines = vLines;
571+
// Normalize horizontalLines to length numRows+1 so the renderer can index boundaries.
572+
NSMutableArray<NSNumber*>* hLines = [NSMutableArray arrayWithArray:horizontalLines ?: @[]];
573+
while (hLines.count < table.numRows + 1) {
574+
[hLines addObject:@0];
575+
}
576+
table.horizontalLines = hLines;
577+
table.interRowAdditionalSpacing = 0;
578+
table.interColumnSpacing = 18; // ≈ 2·\arraycolsep at the default size; matches matrix.
579+
table.cellStyle = kMTLineStyleText; // post-#245: cells render textstyle via the table.
580+
return table;
581+
}
582+
530583
+ (NSMutableDictionary<NSString*, MTMathAtom*>*) supportedLatexSymbols
531584
{
532585
static NSMutableDictionary<NSString*, MTMathAtom*>* commands = nil;

iosMath/lib/MTMathList.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,16 @@ typedef NS_ENUM(NSInteger, MTColumnAlignment) {
728728
/// measures the inter-column/row glue in this style.
729729
@property (nonatomic) MTLineStyle cellStyle;
730730

731+
/// The number of `|` vertical rules at each column boundary (array environment).
732+
/// Length numColumns+1: index 0 = before column 0 … numColumns = after the last column.
733+
/// Empty for every non-array environment (inert default — no layout effect).
734+
@property (nonatomic, copy, nonnull) NSArray<NSNumber*>* verticalLines;
735+
736+
/// The number of `\hline` horizontal rules at each row boundary (array environment).
737+
/// Length numRows+1: index 0 = above row 0 … numRows = below the last row.
738+
/// Empty for every non-array environment (inert default — no layout effect).
739+
@property (nonatomic, copy, nonnull) NSArray<NSNumber*>* horizontalLines;
740+
731741
/// Set the value of a given cell. The table is automatically resized to contain this cell.
732742
- (void) setCell:(MTMathList*) list forRow:(NSInteger) row column:(NSInteger) column;
733743

iosMath/lib/MTMathList.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,8 @@ - (instancetype)initWithEnvironment:(NSString *)env
11781178
self.interRowAdditionalSpacing = 0;
11791179
self.interColumnSpacing = 0;
11801180
self.cellStyle = kMTLineStyleInherit;
1181+
self.verticalLines = [NSArray array];
1182+
self.horizontalLines = [NSArray array];
11811183
_environment = env;
11821184
}
11831185
return self;
@@ -1206,6 +1208,9 @@ - (id)copyWithZone:(NSZone *)zone
12061208
op.cellStyle = self.cellStyle;
12071209
op->_environment = self.environment;
12081210
op.alignments = [NSMutableArray arrayWithArray:self.alignments];
1211+
// The copy setters take an immutable snapshot, so a plain assignment suffices.
1212+
op.verticalLines = self.verticalLines;
1213+
op.horizontalLines = self.horizontalLines;
12091214
// Perform a deep copy of the cells.
12101215
NSMutableArray* cellCopy = [NSMutableArray arrayWithCapacity:self.cells.count];
12111216
for (NSMutableArray* row in self.cells) {

iosMathTests/MTMathListTest.m

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,130 @@ - (void) testCopy
302302
[MTMathListTest checkListCopy:list2 original:list forTest:self];
303303
}
304304

305+
- (void)testMathTableVerticalHorizontalLinesDefaultAndCopy
306+
{
307+
MTMathTable* table = [[MTMathTable alloc] init];
308+
// Default: both empty (inert — existing envs unaffected).
309+
XCTAssertNotNil(table.verticalLines);
310+
XCTAssertNotNil(table.horizontalLines);
311+
XCTAssertEqual(table.verticalLines.count, 0);
312+
XCTAssertEqual(table.horizontalLines.count, 0);
313+
314+
table.verticalLines = @[ @1, @0, @2 ];
315+
table.horizontalLines = @[ @1, @0 ];
316+
MTMathTable* copy = [table copy];
317+
XCTAssertEqualObjects(copy.verticalLines, (@[ @1, @0, @2 ]));
318+
XCTAssertEqualObjects(copy.horizontalLines, (@[ @1, @0 ]));
319+
// Mutating the original's field must not affect the copy.
320+
table.verticalLines = @[ @9 ];
321+
XCTAssertEqualObjects(copy.verticalLines, (@[ @1, @0, @2 ]));
322+
323+
// The properties take an immutable snapshot: mutating a mutable array
324+
// *after* assigning it must not change the stored value (copy semantics).
325+
NSMutableArray<NSNumber*>* mutV = [@[ @1, @2 ] mutableCopy];
326+
NSMutableArray<NSNumber*>* mutH = [@[ @3 ] mutableCopy];
327+
table.verticalLines = mutV;
328+
table.horizontalLines = mutH;
329+
[mutV addObject:@99];
330+
[mutH addObject:@99];
331+
XCTAssertEqualObjects(table.verticalLines, (@[ @1, @2 ]));
332+
XCTAssertEqualObjects(table.horizontalLines, (@[ @3 ]));
333+
}
334+
335+
- (void)testArrayTableFactoryBuildsBareTextstyleTable
336+
{
337+
MTMathList* a = [MTMathListBuilder buildFromString:@"a"];
338+
MTMathList* b = [MTMathListBuilder buildFromString:@"b"];
339+
MTMathList* c = [MTMathListBuilder buildFromString:@"c"];
340+
MTMathList* d = [MTMathListBuilder buildFromString:@"d"];
341+
NSArray* rows = @[ @[ a, b ], @[ c, d ] ];
342+
343+
NSError* error = nil;
344+
MTMathAtom* atom = [MTMathAtomFactory
345+
arrayTableWithAlignments:@[ @(kMTColumnAlignmentRight), @(kMTColumnAlignmentLeft) ]
346+
verticalLines:@[ @1, @0, @1 ]
347+
horizontalLines:@[ @1 ]
348+
rows:rows
349+
error:&error];
350+
XCTAssertNil(error);
351+
XCTAssertNotNil(atom);
352+
// Bare table — no MTInner / delimiters wrapping.
353+
XCTAssertEqual(atom.type, kMTMathAtomTable);
354+
MTMathTable* table = (MTMathTable*) atom;
355+
XCTAssertEqualObjects(table.environment, @"array");
356+
XCTAssertEqual(table.numRows, 2);
357+
XCTAssertEqual(table.numColumns, 2);
358+
XCTAssertEqual([table getAlignmentForColumn:0], kMTColumnAlignmentRight);
359+
XCTAssertEqual([table getAlignmentForColumn:1], kMTColumnAlignmentLeft);
360+
XCTAssertEqualObjects(table.verticalLines, (@[ @1, @0, @1 ]));
361+
// horizontalLines normalized to numRows+1 == 3.
362+
XCTAssertEqualObjects(table.horizontalLines, (@[ @1, @0, @0 ]));
363+
XCTAssertEqual(table.interColumnSpacing, 18);
364+
XCTAssertEqual(table.interRowAdditionalSpacing, 0);
365+
XCTAssertEqual(table.cellStyle, kMTLineStyleText);
366+
}
367+
368+
- (void)testArrayTableFactoryRejectsTooManyCells
369+
{
370+
MTMathList* a = [MTMathListBuilder buildFromString:@"a"];
371+
MTMathList* b = [MTMathListBuilder buildFromString:@"b"];
372+
// Spec declares 1 column but the row has 2 cells.
373+
NSError* error = nil;
374+
MTMathAtom* atom = [MTMathAtomFactory
375+
arrayTableWithAlignments:@[ @(kMTColumnAlignmentCenter) ]
376+
verticalLines:@[ @0, @0 ]
377+
horizontalLines:@[]
378+
rows:@[ @[ a, b ] ]
379+
error:&error];
380+
XCTAssertNil(atom);
381+
XCTAssertNotNil(error);
382+
XCTAssertEqual(error.code, MTParseErrorInvalidNumColumns);
383+
XCTAssertEqualObjects(error.localizedDescription,
384+
@"array row has 2 cells but column specification declares 1 columns");
385+
}
386+
387+
- (void)testArrayTableFactoryAcceptsShortRows
388+
{
389+
MTMathList* a = [MTMathListBuilder buildFromString:@"a"];
390+
// Spec declares 3 columns; row has 1 cell — allowed. The short row is padded out
391+
// to 3 cells so the declared trailing columns (alignment + vertical rule) survive.
392+
NSError* error = nil;
393+
MTMathAtom* atom = [MTMathAtomFactory
394+
arrayTableWithAlignments:@[ @(kMTColumnAlignmentCenter),
395+
@(kMTColumnAlignmentCenter),
396+
@(kMTColumnAlignmentCenter) ]
397+
verticalLines:@[ @0, @0, @0, @0 ]
398+
horizontalLines:@[]
399+
rows:@[ @[ a ] ]
400+
error:&error];
401+
XCTAssertNil(error);
402+
XCTAssertNotNil(atom);
403+
MTMathTable* table = (MTMathTable*) atom;
404+
XCTAssertEqual(table.numColumns, 3);
405+
XCTAssertEqual([table.cells[0] count], 3);
406+
}
407+
408+
- (void)testArrayTableFactoryNormalizesVerticalLines
409+
{
410+
MTMathList* a = [MTMathListBuilder buildFromString:@"a"];
411+
MTMathList* b = [MTMathListBuilder buildFromString:@"b"];
412+
// Caller passes a too-short verticalLines (and nil horizontalLines); the factory
413+
// pads verticalLines to numCols+1 and tolerates nil rather than crashing.
414+
NSError* error = nil;
415+
MTMathAtom* atom = [MTMathAtomFactory
416+
arrayTableWithAlignments:@[ @(kMTColumnAlignmentCenter),
417+
@(kMTColumnAlignmentCenter) ]
418+
verticalLines:@[ @1 ]
419+
horizontalLines:nil
420+
rows:@[ @[ a, b ] ]
421+
error:&error];
422+
XCTAssertNil(error);
423+
XCTAssertNotNil(atom);
424+
MTMathTable* table = (MTMathTable*) atom;
425+
XCTAssertEqual(table.verticalLines.count, 3); // numCols(2) + 1
426+
XCTAssertEqualObjects(table.verticalLines, (@[ @1, @0, @0 ]));
427+
}
428+
305429
@end
306430

307431
@interface MTMathAtomTest : XCTestCase

0 commit comments

Comments
 (0)