Engineering Journal
Table Formatter
Table Formatter

Text Split: The Two-Keystroke Function That Turns a Blob Into a Table

2026-05-11

The job tracker assignment in TAFNE's documentation starts like this: paste a block of raw text into a single cell.

Stripe|Senior Engineer|Applied|Dec 3|No response
Notion|Product Designer|Interviewing|Dec 5|Round 2 scheduled
Linear|Frontend Dev|Applied|Dec 7|No response

Seven lines. Pipe-delimited. Each line has five fields. It looks like a table but it's sitting in one cell.

The instruction is: Alt+Shift+T to open Text Split, configure the delimiter, Alt+Shift+X to apply. Two keystrokes. The blob becomes a 7-row, 5-column structured table.

Here's how that works.

What Text Split Does

The core function is applyTextSplit() in tableManipulation.js. It reads the content of the selected cell, splits it on two delimiters, one for rows, one for columns, and reconstructs the table structure around it.

The split logic handles three directions:

if (splitDirection === 'rows') {
    const rows = splitText(text, rowDelimiter).filter(row => row.trim() !== '');
    tableData = rows.map(row => [row.trim()]);
} else if (splitDirection === 'columns') {
    const columns = splitText(text, colDelimiter).filter(col => col.trim() !== '');
    tableData = [columns];
} else {
    // Both
    const rows = splitText(text, rowDelimiter).filter(row => row.trim() !== '');
    tableData = rows.map(row =>
        splitText(row, colDelimiter).map(cell => cell.trim()).filter(cell => cell !== '')
    );
}

"Rows only" splits on the row delimiter and puts each piece in a single-column row. "Columns only" keeps the content on one row but splits into multiple columns. "Both", the default, splits on the row delimiter first, then splits each row on the column delimiter. The result is a 2D array of cell values.

The Space Delimiter Edge Case

The inner splitText helper handles one edge case explicitly:

const splitText = (str, delimiter) => {
    if (delimiter === ' ') {
        return str.trim().split(/\s+/);
    }
    if (delimiter === '') {
        return [str];
    }
    return str.split(delimiter);
};

A single space as delimiter uses \s+: splitting on one or more consecutive whitespace characters. This is the correct behavior for space-delimited text where multiple spaces might appear between fields. A literal ' '.split(' ') would create empty strings between consecutive spaces.

An empty string as delimiter is a no-op. Without this guard, 'hello'.split('') would split every character, which is never what a user means when they leave the delimiter blank.

Rebuilding the Table

The tableData array becomes new DOM nodes. The first row replaces the content of the original cell's row. Subsequent rows are inserted as new <tr> elements:

tableData.forEach((rowData, rowIndex) => {
    if (rowIndex === 0) {
        const newCellsHtml = rowData.map(cellText => &lt;td&gt;${cellText}&lt;/td&gt;).join('');
        $cell.before(newCellsHtml);
    } else {
        let newRowHtml = '<tr>' + '<td></td>'.repeat(cellIndex);
        newRowHtml += rowData.map(cellText => &lt;td&gt;${cellText}&lt;/td&gt;).join('');
        newRowHtml += '</tr>';
        $lastRow.after(newRowHtml);
        $lastRow = $lastRow.next();
    }
});

$cell.remove();

For the first row: new cells are inserted before the original cell in its existing row. The original cell is removed afterward. This preserves the surrounding row structure and any other cells in that row.

For subsequent rows: a new <tr> is constructed. The '<td></td>'.repeat(cellIndex) padding is interesting: it fills the columns to the left of where the original cell sat with empty cells, so the split content lands in the correct column position within the table.

State and Undo

The function saves state before and after:

function applyTextSplit() {
    window.saveCurrentState(); // snapshot before
    // ... split logic ...
    setupTableInteraction();
    window.saveCurrentState(); // snapshot after
}

Two saves: one before (so Ctrl+Z can undo back to the original single-cell state) and one after (so Ctrl+Y can redo the split). Standard bracketing pattern.

Why It Works on Multiple Cells

The function iterates over selectedCells, not just the first one:

selectedCells.forEach((cell, cellIndex) => {
    const $cell = $(cell);
    const text = $cell.text();
    // ... perform split on this cell ...
});

If you've selected five cells, each one gets split independently using the same delimiter configuration. This lets you batch-split a column of blob values into structured data in one pass.

The job tracker scenario, paste a whole text dump into one cell and split it into a real table, demonstrates the intended use case well. But the multi-cell iteration makes it useful for repeated operations on structured columns too: a column of "Firstname Lastname" values, for example, can all be split to separate columns at once.

Source: github.com/carnworkstudios/TAFNE

Read this post in the full Engineering Journal →