doc-table-extract
v0.3.0
Published
Extract tables from HTML, Markdown, CSV, and plain text as structured JSON
Readme
doc-table-extract
Extract tables from HTML, Markdown, CSV, TSV, and plain text as structured JSON. Zero runtime dependencies.
Description
doc-table-extract is a lightweight, dependency-free TypeScript library that detects and extracts tabular data from multiple document formats. It auto-detects the input format, parses tables into a uniform ExtractedTable structure with headers, data rows, cell-level metadata, and column type inference, then optionally exports the result to JSON, CSV, Markdown, or HTML.
Key design decisions:
- Zero runtime dependencies. The package has no production dependencies. All parsing -- HTML tag matching, CSV field splitting, Markdown pipe parsing, ASCII border detection -- is implemented from scratch using regex and string processing.
- Format-agnostic output. Regardless of whether the input is an HTML
<table>, a GFM pipe table, a comma-separated file, or a space-aligned text table, the output is always the sameExtractedTableinterface withheaders,rows,cells,metadata, andcolumnTypes. - Auto-detection. The
extract()function inspects the input string and determines the format automatically. You can override this with an explicitformatoption. - Cell-level detail. Every cell carries its row index, column index, row span, column span, and header flag, enabling faithful round-trip export back to HTML with merged cells intact.
Installation
npm install doc-table-extractRequires Node.js >= 18.
Quick Start
import { extract } from 'doc-table-extract';
const tables = extract(`
| Name | Age | City |
|-------|-----|------|
| Alice | 30 | NYC |
| Bob | 25 | LA |
`);
console.log(tables[0].headers); // ['Name', 'Age', 'City']
console.log(tables[0].rows); // [['Alice', '30', 'NYC'], ['Bob', '25', 'LA']]
console.log(tables[0].columnTypes); // ['string', 'number', 'string']Features
- HTML extraction -- Parses
<table>elements including<thead>,<tbody>,<th>,<td>, with fullcolspanandrowspansupport. Handles nested tables (extracts outermost), multiple tables per document, HTML entity decoding (&,<,>,",', , numeric entities), and inline tag stripping (<b>,<i>,<a>,<span>). - Markdown extraction -- Parses GFM pipe tables with alignment indicators (
:---,:---:,---:). Handles escaped pipes (\|), multiple tables per document, empty cells, and rows with fewer columns than the header. - CSV/TSV extraction -- RFC 4180 compliant parsing with quoted fields, escaped double quotes, and embedded newlines. Auto-detects delimiter from comma, tab, semicolon, and pipe. Handles CRLF and CR line endings, uneven row lengths (short rows are padded), and multi-level headers.
- Plain text extraction -- Detects ASCII bordered tables (
+---+---+), box-drawing character tables (Unicode---,|, corner characters), and space/tab-aligned columnar data. Handles multiple tables separated by blank lines. - Format auto-detection -- The
detectFormat()function inspects the input and returns the detected format:html,markdown,csv,tsv,text, orunknown. - Column type inference -- After extraction, the normalizer analyzes each column's values and classifies them as
string,number,date, ormixed. Recognizes currency ($1,000), percentages (50%), and common date formats (YYYY-MM-DD,MM/DD/YYYY,Jan 15, 2024,Q1 2024). - Normalization -- Trims and collapses whitespace in all cells. Optionally preserves newlines within cells via the
preserveCellNewlinesoption. - Confidence scoring -- Each extracted table includes a confidence score (0.0--1.0). HTML tables receive 0.95, Markdown tables 0.9, CSV/TSV 0.85, and plain text 0.7. Filter low-confidence results with the
confidenceThresholdoption. - Multi-format export -- Export any
ExtractedTableto CSV (RFC 4180), GFM Markdown (with optional column alignment), HTML (<table>withrowspan/colspanattributes), or JSON ({ headers, rows }). - Extraction timing -- Every result includes
metadata.extractionMsreporting the extraction duration in milliseconds. - TypeScript strict mode -- Full type exports, declaration maps, and source maps. Compiled with
strict: true.
API Reference
extract(input, options?)
The primary entry point. Auto-detects the input format and extracts all tables.
function extract(input: string, options?: ExtractOptions): ExtractedTable[]Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| input | string | The document content to parse |
| options | ExtractOptions | Optional extraction configuration |
Returns: An array of ExtractedTable objects. Returns an empty array if no tables are found or all tables fall below the confidence threshold.
Behavior:
- Detects the input format (or uses
options.formatif provided). - Delegates to the format-specific extractor.
- Normalizes all tables (whitespace cleaning, column type detection).
- Filters tables below
options.confidenceThreshold. - Records extraction timing in
metadata.extractionMs.
For unknown format, all extractors are tried in sequence (HTML, Markdown, CSV, text) until one produces results.
import { extract } from 'doc-table-extract';
// Auto-detect
const tables = extract(inputString);
// Explicit format with options
const tables = extract(csvString, {
format: 'csv',
headerRows: 1,
confidenceThreshold: 0.5,
});detectFormat(input)
Detect the input format without extracting tables.
function detectFormat(input: string): InputFormatReturns: One of 'html', 'markdown', 'csv', 'tsv', 'text', or 'unknown'.
Detection rules (evaluated in order):
- HTML -- Contains
<tablefollowed by>or whitespace. - Markdown -- Contains pipe-delimited rows with a separator line matching
|---|. - TSV -- At least 80% of non-empty lines contain tab characters.
- CSV -- At least 2 lines with a consistent comma count (>= 1 comma per line).
- Text -- At least 2 horizontal separator lines (
+---+or box-drawing), or at least 60% of lines have multi-space gaps between content. - Unknown -- None of the above matched.
import { detectFormat } from 'doc-table-extract';
detectFormat('<table><tr><td>x</td></tr></table>'); // 'html'
detectFormat('| A | B |\n|---|---|\n| 1 | 2 |'); // 'markdown'
detectFormat('a,b,c\n1,2,3'); // 'csv'
detectFormat('a\tb\tc\n1\t2\t3'); // 'tsv'extractFromHtml(html, options?)
Extract tables from HTML content.
function extractFromHtml(html: string, options?: ExtractOptions): ExtractedTable[]Finds all top-level <table> elements (nested tables are treated as part of the outer table's cell content). Supports:
<thead>/<tbody>sections<th>and<td>cellscolspanandrowspanattributes- HTML entity decoding (
&,<,>,",', ,&#NNN;,&#xHHH;) - Inline tag stripping (preserves text content)
- Multi-level header flattening (with
headerRows> 1)
Confidence: 0.95
import { extractFromHtml } from 'doc-table-extract';
const tables = extractFromHtml(`
<table>
<thead><tr><th>Name</th><th>Age</th></tr></thead>
<tbody>
<tr><td>Alice</td><td>30</td></tr>
<tr><td>Bob</td><td>25</td></tr>
</tbody>
</table>
`);extractFromMarkdown(markdown, options?)
Extract tables from Markdown content (GFM pipe tables).
function extractFromMarkdown(markdown: string, options?: ExtractOptions): ExtractedTable[]Parses GFM pipe table syntax. A valid table requires a header row, a separator row (e.g., |---|), and zero or more data rows. Supports:
- Alignment indicators (
:---,:---:,---:) - Escaped pipes (
\|) in cell content - Multiple tables in one document
- Rows with fewer columns than the header (padded with empty strings)
Confidence: 0.9
import { extractFromMarkdown } from 'doc-table-extract';
const tables = extractFromMarkdown(`
| Product | Price |
|---------|-------:|
| Widget | $9.99 |
| Gadget | $19.99 |
`);extractFromCsv(text, options?)
Extract a table from CSV or TSV content.
function extractFromCsv(text: string, options?: ExtractOptions): ExtractedTable[]RFC 4180 compliant parser. Supports:
- Auto-detection of delimiter (comma, tab, semicolon, pipe)
- Quoted fields with embedded commas, newlines, and double quotes
- Escaped double quotes (
"") - CRLF and CR line endings
- Uneven row lengths (short rows padded with empty strings)
- Header row auto-detection (compares first row's numeric content against subsequent rows)
Confidence: 0.85
import { extractFromCsv } from 'doc-table-extract';
const tables = extractFromCsv('Name,Age\nAlice,30\nBob,25');
// Explicit delimiter
const tables = extractFromCsv(data, { delimiter: ';' });
// Tab-separated
const tables = extractFromCsv(tsvData, { delimiter: '\t' });extractFromText(text, options?)
Extract tables from plain text content.
function extractFromText(text: string, options?: ExtractOptions): ExtractedTable[]Detects two types of plain text tables:
ASCII/box-drawing bordered tables -- Tables delimited by
+,-,=,|, or Unicode box-drawing characters. Requires at least 2 separator lines. Column boundaries are inferred from separator character positions.Space-aligned columnar data -- Text where columns are separated by 2+ consecutive spaces. Requires at least 3 content lines and at least 2 detected columns. At least 50% of rows must have content in 2+ columns.
ASCII tables are tried first. Space-aligned detection is used as a fallback.
Confidence: 0.7
import { extractFromText } from 'doc-table-extract';
// ASCII bordered table
const tables = extractFromText(`
+-------+-----+------+
| Name | Age | City |
+-------+-----+------+
| Alice | 30 | NYC |
| Bob | 25 | LA |
+-------+-----+------+
`);
// Space-aligned columnar data
const tables = extractFromText(`
Name Age City
Alice 30 NYC
Bob 25 LA
Charlie 35 Chicago
`);normalizeTable(table, preserveNewlines?)
Normalize an extracted table: clean whitespace and detect column types.
function normalizeTable(table: ExtractedTable, preserveNewlines?: boolean): ExtractedTableParameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| table | ExtractedTable | -- | The table to normalize |
| preserveNewlines | boolean | false | When true, newlines within cells are preserved; when false, all whitespace is collapsed to single spaces |
Behavior:
- Trims leading/trailing whitespace from all headers, row values, and cell text.
- Collapses internal whitespace (multiple spaces/tabs become a single space).
- Detects column types from data row values (see Column Types below).
- Returns a new
ExtractedTable(does not mutate the input).
Note: extract() calls normalizeTable automatically. Use this function directly only when working with format-specific extractors.
normalizeTables(tables, preserveNewlines?)
Normalize an array of extracted tables.
function normalizeTables(tables: ExtractedTable[], preserveNewlines?: boolean): ExtractedTable[]Convenience wrapper that calls normalizeTable on each table in the array.
toCSV(table)
Export a table to RFC 4180 CSV format.
function toCSV(table: ExtractedTable): stringFields containing commas, double quotes, or newlines are quoted. Internal double quotes are escaped by doubling ("").
import { toCSV } from 'doc-table-extract';
const csv = toCSV(table);
// "Name,Age,City\nAlice,30,NYC\nBob,25,LA"toMarkdown(table, alignment?)
Export a table to GFM Markdown pipe table format.
function toMarkdown(table: ExtractedTable, alignment?: boolean): stringParameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| table | ExtractedTable | -- | The table to export |
| alignment | boolean | false | When true, numeric columns use right-alignment (---:) in the separator row |
Pipe characters in cell content are escaped with backslash. Newlines in cells are replaced with spaces.
import { toMarkdown } from 'doc-table-extract';
toMarkdown(table);
// | Name | Age | City |
// | --- | --- | --- |
// | Alice | 30 | NYC |
toMarkdown(table, true);
// | Name | Age | City |
// | --- | ---: | --- |
// | Alice | 30 | NYC |toHTML(table)
Export a table to an HTML <table> element string.
function toHTML(table: ExtractedTable): stringProduces a complete <table> with <thead> and <tbody> sections. Includes rowspan and colspan attributes on cells where the source cells array specifies spans greater than 1. HTML-escapes all content (&, <, >, ", ').
import { toHTML } from 'doc-table-extract';
const html = toHTML(table);
// <table>
// <thead>
// <tr>
// <th>Name</th>
// <th>Age</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>Alice</td>
// <td>30</td>
// </tr>
// </tbody>
// </table>toJSON(table)
Export a table to a plain JSON-serializable object.
function toJSON(table: ExtractedTable): { headers: string[]; rows: string[][] }Returns a simple { headers, rows } object without cell-level metadata. Useful for serialization or passing to APIs that expect a flat table format.
import { toJSON } from 'doc-table-extract';
const json = toJSON(table);
// { headers: ['Name', 'Age'], rows: [['Alice', '30'], ['Bob', '25']] }exportTable(table, format)
Export a table to a specified format by name.
function exportTable(table: ExtractedTable, format: ExportFormat): stringParameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| table | ExtractedTable | The table to export |
| format | ExportFormat | One of 'json', 'csv', 'markdown', 'html' |
Returns: A string in the requested format. For 'json', returns a pretty-printed JSON string (2-space indent).
Throws: Error with message Unsupported export format: <format> if the format is not recognized.
import { exportTable } from 'doc-table-extract';
const csvString = exportTable(table, 'csv');
const mdString = exportTable(table, 'markdown');
const htmlString = exportTable(table, 'html');
const jsonString = exportTable(table, 'json');Configuration
ExtractOptions
Options passed to extract() and all format-specific extractors.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| format | InputFormat | auto-detect | Input format hint. One of 'html', 'markdown', 'csv', 'tsv', 'text', 'unknown'. When omitted, detectFormat() is called automatically. |
| confidenceThreshold | number | 0 | Minimum confidence score (0.0--1.0) to include a table in results. Tables below this threshold are filtered out. |
| headerRows | number \| 'auto' | 'auto' | Number of header rows. 0 = no headers (generates Column 1, Column 2, etc.). A positive integer = treat that many rows as headers. 'auto' = detect automatically. |
| delimiter | string | auto-detect | Delimiter for CSV/TSV parsing. When omitted, the delimiter is auto-detected from comma, tab, semicolon, and pipe. |
| preserveCellNewlines | boolean | false | When true, newlines within cells are preserved during normalization. When false, all whitespace including newlines is collapsed to single spaces. |
Types
ExtractedTable
The primary output type. Every extraction function returns an array of these.
interface ExtractedTable {
headers: string[]; // Column header labels
rows: string[][]; // Data rows (each row is an array of cell values)
cells: TableCell[]; // Cell-level detail with position and span info
metadata: TableMetadata; // Source format, dimensions, confidence, timing
columnTypes: ColumnType[]; // Inferred type for each column
}TableCell
Cell-level detail for an individual table cell.
interface TableCell {
text: string; // Text content of the cell
rowIndex: number; // Zero-based row index
colIndex: number; // Zero-based column index
rowSpan: number; // Number of rows this cell spans (default: 1)
colSpan: number; // Number of columns this cell spans (default: 1)
isHeader: boolean; // Whether this cell is a header cell
}TableMetadata
Metadata about an extracted table.
interface TableMetadata {
format: InputFormat; // Source format that was detected
rowCount: number; // Number of data rows (excluding headers)
columnCount: number; // Number of columns
confidence: number; // Confidence score 0.0-1.0
pageNumber?: number; // Page number (1-based) if applicable
extractionMs?: number; // Extraction time in milliseconds
}TableRegion
A rectangular region on a page where a table was detected.
interface TableRegion {
x: number; // X coordinate of the top-left corner
y: number; // Y coordinate of the top-left corner
width: number; // Width of the region
height: number; // Height of the region
pageNumber: number; // Page number (1-based)
confidence: number; // Confidence score 0.0-1.0
}InputFormat
type InputFormat = 'html' | 'markdown' | 'csv' | 'tsv' | 'text' | 'unknown';ExportFormat
type ExportFormat = 'json' | 'csv' | 'markdown' | 'html';ColumnType
type ColumnType = 'string' | 'number' | 'date' | 'mixed';Detection rules:
- number -- At least 80% of non-empty values parse as numeric after stripping
$,,,%, and whitespace. - date -- At least 80% of non-empty values match a recognized date pattern (
YYYY-MM-DD,MM/DD/YYYY,DD/MM/YYYY,15 Jan 2024,Jan 15, 2024,2024,Q1 2024). - mixed -- Column contains a mixture of numeric and non-numeric values that does not meet the 80% threshold for either.
- string -- Default when no numeric or date patterns are detected.
Error Handling
doc-table-extract is designed to be fault-tolerant. Extraction functions do not throw on malformed input; they return an empty array instead.
extract(''); // [] -- empty input
extract('hello world'); // [] -- no detectable table
extract('<table></table>'); // [] -- empty table elementThe only function that throws is exportTable when given an unsupported format:
exportTable(table, 'xml' as any);
// throws Error: "Unsupported export format: xml"Advanced Usage
Extracting multiple tables
HTML and Markdown documents can contain multiple tables. All are returned in document order.
const html = `
<table><tr><th>A</th></tr><tr><td>1</td></tr></table>
<table><tr><th>B</th></tr><tr><td>2</td></tr></table>
`;
const tables = extract(html);
// tables.length === 2Filtering by confidence
Use confidenceThreshold to exclude low-confidence extractions.
const tables = extract(ambiguousInput, { confidenceThreshold: 0.8 });
// Only tables with confidence >= 0.8 are returnedHandling tables without headers
Set headerRows: 0 to treat all rows as data. Generated headers (Column 1, Column 2, ...) are assigned automatically.
const tables = extract(csvData, { headerRows: 0 });
// tables[0].headers === ['Column 1', 'Column 2', 'Column 3']Multi-level headers in HTML
For HTML tables with two header rows (e.g., a category row spanning columns and a sub-category row beneath it), use headerRows: 2. Headers are flattened by concatenating unique values per column.
const tables = extractFromHtml(html, { headerRows: 2 });
// A column under "Sales" > "Q1" gets header "Sales Q1"Round-trip extraction and export
Extract from one format and export to another.
import { extract, toMarkdown, toCSV, toHTML } from 'doc-table-extract';
const tables = extract(htmlString);
const table = tables[0];
const markdown = toMarkdown(table, true); // with column alignment
const csv = toCSV(table);
const html = toHTML(table); // preserves rowspan/colspanPreserving newlines in cells
By default, newlines within cells are collapsed to spaces. To preserve them:
const tables = extract(csvWithMultilineCells, { preserveCellNewlines: true });
// Cell text retains \n charactersWorking with column types
After extraction, use columnTypes to drive formatting or validation logic.
const table = extract(csvData)[0];
table.columnTypes.forEach((type, i) => {
if (type === 'number') {
console.log(`Column "${table.headers[i]}" is numeric`);
}
});TypeScript
The package ships with full TypeScript declarations (dist/index.d.ts) and declaration maps. All types are exported from the package root.
import type {
ExtractedTable,
TableCell,
TableMetadata,
TableRegion,
InputFormat,
ExportFormat,
ExtractOptions,
ColumnType,
} from 'doc-table-extract';Compiled with:
target: ES2022module: commonjsstrict: truedeclaration: truedeclarationMap: truesourceMap: true
License
MIT
