+        for (r = 0; r < nr; ++r) {
+            const spans = multicolumns[r];
+            for (const start in spans) {
+                if (spans.hasOwnProperty(start)) {
+                    const colStart = Number(start);
+                    if (colStart < k && k < colStart + spans[colStart].n) {
+                        const rw = body[r];
+                        (gaps = gaps || []).push(
+                            [rw.pos - rw.height, rw.pos + rw.depth]);
+                    }
+                }
+            }
+        }
+        if (gaps) {
+            gaps.sort((a, b) => a[0] - b[0]);
+            ruleGaps[k] = gaps;
+        }
+    }
+
     const colDescriptions = group.cols || [];
     const cols: HtmlDomNode[] = [];
     let colSep;
@@ -437,17 +583,44 @@ const htmlBuilder: HtmlBuilder<"array"> = function(group, options) {
 
             if (colDescr.separator === "|" || colDescr.separator === ":") {
                 const lineType = colDescr.separator === "|" ? "solid" : "dashed";
-                const separator = makeSpan(["vertical-separator"], [], options);
-                separator.style.height = makeEm(totalHeight);
-                separator.style.borderRightWidth = makeEm(ruleThickness);
-                separator.style.borderRightStyle = lineType;
-                separator.style.margin = `0 ${makeEm(-ruleThickness / 2)}`;
-                const shift = totalHeight - offset;
-                if (shift) {
-                    separator.style.verticalAlign = makeEm(-shift);
+                // Rows in which a \multicolumn spans across this boundary
+                // suppress the rule for the extent of the row, so draw the
+                // rule as segments covering the remaining intervals.
+                const gaps = ruleGaps[c];
+                const segments: Array<[number, number]> = [];
+                let segStart = 0;
+                if (gaps) {
+                    for (let i = 0; i < gaps.length; i++) {
+                        const gapTop =
+                            Math.max(0, Math.min(gaps[i][0], totalHeight));
+                        const gapBottom =
+                            Math.max(0, Math.min(gaps[i][1], totalHeight));
+                        if (gapTop > segStart) {
+                            segments.push([segStart, gapTop]);
+                        }
+                        segStart = Math.max(segStart, gapBottom);
+                    }
+                }
+                if (segStart < totalHeight) {
+                    segments.push([segStart, totalHeight]);
                 }
 
-                cols.push(separator);
+                for (let i = 0; i < segments.length; i++) {
+                    const segTop = segments[i][0];
+                    const segBottom = segments[i][1];
+                    const separator =
+                        makeSpan(["vertical-separator"], [], options);
+                    separator.style.height = makeEm(segBottom - segTop);
+                    separator.style.borderRightWidth = makeEm(ruleThickness);
+                    separator.style.borderRightStyle = lineType;
+                    separator.style.margin = `0 ${makeEm(-ruleThickness / 2)}`;
+                    const shift = segBottom - offset;
+                    if (shift) {
+                        separator.style.verticalAlign = makeEm(-shift);
+                    }
+
+                    cols.push(separator);
+                }
             } else {
                 throw new ParseError(
                     "Invalid separator type: " + colDescr.separator);
@@ -476,6 +649,7 @@ const htmlBuilder: HtmlBuilder<"array"> = function(group, options) {
             type: "elem";
             elem: HtmlDomNode;
             shift: number;
+            wrapperClasses?: string[];
         }> = [];
         for (r = 0; r < nr; ++r) {
             const row = body[r];
@@ -486,13 +660,25 @@ const htmlBuilder: HtmlBuilder<"array"> = function(group, options) {
             const shift = row.pos - offset;
             elem.depth = row.depth;
             elem.height = row.height;
-            colElems.push({type: "elem", elem: elem, shift: shift});
+            const span = multicolumns[r][c];
+            if (span) {
+                // The \multicolumn alignment overrides the column's
+                // declared alignment for this cell.
+                colElems.push({type: "elem", elem: elem, shift: shift,
+                    wrapperClasses: ["multicolumn-align-" + span.align]});
+            } else {
+                colElems.push({type: "elem", elem: elem, shift: shift});
+            }
         }
 
-        const colVList = makeVList({
-            positionType: "individualShift",
-            children: colElems,
-        }, options);
+        // A column may have no cells of its own if \multicolumn cells span
+        // across it in every row.
+        const colVList = colElems.length > 0
+            ? makeVList({
+                positionType: "individualShift",
+                children: colElems,
+            }, options)
+            : makeSpan([], []);
         const colSpan = makeSpan(
             ["col-align-" + (colDescr?.align || "c")],
             [colVList],
@@ -558,8 +744,17 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
         const rw = group.body[i];
         const row = [];
         for (let j = 0; j < rw.length; j++) {
-            row.push(new MathNode("mtd",
-                [mml.buildGroup(rw[j], options)]));
+            const cell = rw[j];
+            if (cell.type === "multicolumn") {
+                const mtd = new MathNode("mtd",
+                    [mml.buildGroup(cell, options)]);
+                mtd.setAttribute("columnspan", String(cell.n));
+                mtd.setAttribute("columnalign", alignMap[cell.align].trim());
+                row.push(mtd);
+            } else {
+                row.push(new MathNode("mtd",
+                    [mml.buildGroup(cell, options)]));
+            }
         }
         if (group.tags && group.tags[i]) {
             row.unshift(glue);
@@ -707,6 +902,7 @@ const alignedHandler = function(context: EnvContextLike, args: AnyParseNode[]) {
             colSeparationType: separationType,
             maxNumCols: isSplit ? 2 : undefined,
             leqno: context.parser.settings.leqno,
+            envName: context.envName,
         },
         "display"
     );
@@ -739,6 +935,11 @@ const alignedHandler = function(context: EnvContextLike, args: AnyParseNode[]) {
     const isAligned = !numCols;
     res.body.forEach(function(row) {
         for (let i = 1; i < row.length; i += 2) {
+            if (row[i].type === "multicolumn") {
+                // A \multicolumn cell provides its own content; skip the
+                // empty group insertion.
+                continue;
+            }
             // Modify ordgroup node within styling node
             const styling = assertNodeType(row[i], "styling");
             const ordgroup = assertNodeType(styling.body[0], "ordgroup");
@@ -752,8 +953,8 @@ const alignedHandler = function(context: EnvContextLike, args: AnyParseNode[]) {
                     `expected ${numMaths}, but got ${curMaths}`,
                     row[0]);
             }
-        } else if (numCols < row.length) { // Case 2
-            numCols = row.length;
+        } else if (numCols < rowColumnCount(row)) { // Case 2
+            numCols = rowColumnCount(row);
         }
     });
 
@@ -822,6 +1023,11 @@ defineEnvironment({
             cols,
             hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
             maxNumCols: cols.length,
+            envName: context.envName,
