Table Extraction
Every call to convert() populates result.tables with one entry per <table> found in the input. Each entry has both a rendered Markdown string and a structured cell grid, so you can embed the Markdown in downstream documents or walk the grid for analysis without re-parsing.
Table extraction runs on every call. There is no opt-in flag. Use JSON output with --no-content in the CLI, or ignore result.content in library code, when you only want table data.
TableData
Section titled “TableData”result.tables is a Vec<TableData> (or the equivalent list in each binding).
| Field | Type | Description |
|---|---|---|
grid |
TableGrid |
Structured cell grid. |
markdown |
String |
The Markdown rendering of this table, identical to what appears in result.content. |
TableGrid
Section titled “TableGrid”| Field | Type | Description |
|---|---|---|
rows |
u32 |
Number of rows in the table. |
cols |
u32 |
Number of columns in the table. |
cells |
Vec<GridCell> |
Flat list of cells. May be shorter than rows * cols when cells span multiple rows or columns. |
GridCell
Section titled “GridCell”| Field | Type | Description |
|---|---|---|
content |
String |
Cell text. Inline formatting is flattened to plain text. |
row |
u32 |
0-indexed row position. |
col |
u32 |
0-indexed column position. |
row_span |
u32 |
How many rows the cell occupies. Defaults to 1. |
col_span |
u32 |
How many columns the cell occupies. Defaults to 1. |
is_header |
bool |
true for <th>, false for <td>. |
Basic Extraction
Section titled “Basic Extraction”use html_to_markdown_rs::{ConversionOptions, convert};
fn main() -> Result<(), Box<dyn std::error::Error>> { let html = r#"<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>"#;
// `tables` is collected alongside the document tree, so it must be enabled. let options = ConversionOptions::builder().include_document_structure(true).build(); let result = convert(html, Some(options))?;
for table in &result.tables { for cell in &table.grid.cells { let kind = if cell.is_header { "Header" } else { "Cell" }; println!(" {kind} (r{},c{}): {}", cell.row, cell.col, cell.content); } } Ok(())}from html_to_markdown import ConversionOptions, convert
html = """<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>"""
# `tables` is collected alongside the document tree, so it must be enabled.result = convert(html, ConversionOptions(include_document_structure=True))
for table in result.tables: for cell in table.grid.cells: prefix = "Header" if cell.is_header else "Cell" print(f" {prefix} (r{cell.row},c{cell.col}): {cell.content}")import { convert } from "@xberg-io/html-to-markdown";
const html = `<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>`;
const result = convert(html, { includeDocumentStructure: true });
for (const table of result.tables ?? []) { for (const cell of table.grid.cells ?? []) { const kind = cell.isHeader ? "Header" : "Cell"; console.log(` ${kind} (r${cell.row},c${cell.col}): ${cell.content}`); }}import ( "fmt" "log"
htmltomarkdown "github.com/xberg-io/html-to-markdown/packages/go/v3")
html := `<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>`
result, err := htmltomarkdown.Convert(html, &htmltomarkdown.ConversionOptions{ IncludeDocumentStructure: true,})if err != nil { log.Fatal(err)}
for _, table := range result.Tables { for _, cell := range table.Grid.Cells { kind := "Cell" if cell.IsHeader { kind = "Header" } fmt.Printf(" %s (r%d,c%d): %s\n", kind, cell.Row, cell.Col, cell.Content) }}require 'html_to_markdown'
html = <<~HTML <table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr> </table>HTML
# Tables are only populated when `include_document_structure` is enabled.result = HtmlToMarkdown.convert(html, include_document_structure: true)
result.tables.each do |table| table.grid.cells.group_by(&:row).each do |_row, cells| prefix = cells.first.is_header ? "Header" : "Row" puts " #{prefix}: #{cells.map(&:content).join(', ')}" endenduse HtmlToMarkdown\HtmlToMarkdownApi;use HtmlToMarkdown\ConversionOptions;
$html = <<<HTML<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>HTML;
// tables are populated only when includeDocumentStructure is enabled.$options = ConversionOptions::from_json(json_encode(['includeDocumentStructure' => true]));$result = HtmlToMarkdownApi::convert($html, $options);
foreach ($result->getTables() as $table) { foreach ($table->getGrid()->getCells() as $cell) { $kind = $cell->isHeader ? 'Header' : 'Cell'; echo " {$kind} (r{$cell->row},c{$cell->col}): {$cell->content}\n"; }}import io.xberg.htmltomarkdown.HtmlToMarkdown;import io.xberg.htmltomarkdown.ConversionOptions;import io.xberg.htmltomarkdown.ConversionResult;import io.xberg.htmltomarkdown.HtmlToMarkdownRsException;
public class TableExample { public static void main(String[] args) throws HtmlToMarkdownRsException { String html = """ <table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr> </table> """;
ConversionOptions options = ConversionOptions.builder() .withIncludeDocumentStructure(true) .build(); ConversionResult result = HtmlToMarkdown.convert(html, options);
for (var table : result.tables()) { for (var cell : table.grid().cells()) { String prefix = Boolean.TRUE.equals(cell.isHeader()) ? "Header" : "Cell"; System.out.printf(" %s (r%d,c%d): %s%n", prefix, cell.row(), cell.col(), cell.content()); } } }}using HtmlToMarkdown;
var html = @"<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>";
var options = new ConversionOptions { IncludeDocumentStructure = true };var result = HtmlToMarkdownConverter.Convert(html, options);
foreach (var table in result.Tables){ foreach (var cell in table.Grid.Cells) { var kind = cell.IsHeader ? "Header" : "Cell"; Console.WriteLine($" {kind} (r{cell.Row},c{cell.Col}): {cell.Content}"); }}html = """<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>"""
opts = %HtmlToMarkdown.ConversionOptions{include_document_structure: true}{:ok, result} = HtmlToMarkdown.convert(html, opts)
for %HtmlToMarkdown.TableData{grid: grid} <- result.tables do grid.cells |> Enum.group_by(& &1.row) |> Enum.sort_by(fn {row, _cells} -> row end) |> Enum.each(fn {_row, cells} -> cells = Enum.sort_by(cells, & &1.col) prefix = if hd(cells).is_header, do: "Header", else: "Row" values = Enum.map(cells, & &1.content) IO.puts(" #{prefix}: #{Enum.join(values, ", ")}") end)endlibrary(htmltomarkdown)
html <- "<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>"
# `tables` is collected alongside the document tree, so it must be enabled.opts <- conversion_options(include_document_structure = TRUE)result <- convert(html, opts)
for (table in result$tables) { for (cell in table$grid$cells) { prefix <- if (cell$is_header) "Header" else "Cell" cat(sprintf(" %s (r%d,c%d): %s\n", prefix, cell$row, cell$col, cell$content)) }}#include "html_to_markdown.h"#include <stdio.h>
int main(void) { const char *html = "<table>" "<tr><th>Name</th><th>Age</th></tr>" "<tr><td>Alice</td><td>30</td></tr>" "<tr><td>Bob</td><td>25</td></tr>" "</table>";
/* include_document_structure must be enabled to populate result.tables; * with the default options the "tables" array in the JSON is empty. */ HTMAlefHandle options = htm_conversion_options_from_json("{\"include_document_structure\":true}"); if (options == 0) { fprintf(stderr, "options failed: %s\n", htm_last_error_context()); return 1; }
HTMAlefHandle result = htm_convert(html, options); htm_conversion_options_free(options); if (result == 0) { fprintf(stderr, "convert failed: %s\n", htm_last_error_context()); return 1; }
char *json = htm_conversion_result_to_json(result); if (json != NULL) { printf("%s\n", json); /* contains a populated "tables" array */ htm_free_string(json); }
htm_conversion_result_free(result); return 0;}import { convert, WasmConversionOptions } from "@xberg-io/html-to-markdown-wasm";
const html = `<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>`;
const options = WasmConversionOptions.default();options.includeDocumentStructure = true;
const result = convert(html, options);
for (const table of result.tables) { for (const cell of table.grid.cells) { const kind = cell.isHeader ? "Header" : "Cell"; console.log(` ${kind} (r${cell.row},c${cell.col}): ${cell.content}`); }}import HtmlToMarkdown
let options = try conversionOptionsFromJson( "{\"include_document_structure\":true}")
let html = """<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>"""
let result = try convert(html: html, options: options)
for table in result.tables() { print("Markdown:", table.markdown().toString()) let grid = table.grid() print("Grid: \(grid.rows()) rows x \(grid.cols()) cols") for cellJson in grid.cells() { let cell = try gridCellFromJson(cellJson.as_str().toString()) let kind = cell.isHeader ? "Header" : "Cell" print(" \(kind) (r\(cell.row),c\(cell.col)): \(cell.content)") }}import 'package:h2m/h2m.dart';import 'package:h2m/src/html_to_markdown_rs_bridge_generated/frb_generated.dart' show RustLib;
Future<void> main() async { await RustLib.init();
const html = '''<table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr></table>''';
final options = await createConversionOptionsFromJson( json: '{"include_document_structure":true}', ); final result = await H2mBridge.convert(html, options: options);
for (final table in result.tables) { for (final cell in table.grid.cells) { final kind = cell.isHeader ? 'Header' : 'Cell'; print(' $kind (r${cell.row},c${cell.col}): ${cell.content}'); } }}import io.xberg.android.ConversionOptionsimport io.xberg.android.HtmlToMarkdown
fun main() { val html = """ <table> <tr><th>Name</th><th>Age</th></tr> <tr><td>Alice</td><td>30</td></tr> <tr><td>Bob</td><td>25</td></tr> </table> """.trimIndent()
// Table extraction requires `includeDocumentStructure = true` — without it, // `result.tables` is always empty even for table-heavy HTML. val options = ConversionOptions(includeDocumentStructure = true) val result = HtmlToMarkdown.convert(html, options)
for (table in result.tables) { println(table.markdown) for (cell in table.grid.cells) { val kind = if (cell.isHeader) "Header" else "Cell" println(" $kind (r${cell.row},c${cell.col}): ${cell.content}") } }}const std = @import("std");const html_to_markdown = @import("html_to_markdown_rs");
pub fn main() !void { var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); const allocator = gpa.allocator();
const html = \\<table> \\ <tr><th>Name</th><th>Age</th></tr> \\ <tr><td>Alice</td><td>30</td></tr> \\ <tr><td>Bob</td><td>25</td></tr> \\</table> ;
// Tables are only populated in `result.tables` when // `include_document_structure` is enabled. const result_json = try html_to_markdown.convert(html, "{\"include_document_structure\":true}"); defer std.heap.c_allocator.free(result_json);
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, result_json, .{}); defer parsed.deinit();
const tables = parsed.value.object.get("tables").?.array; std.debug.print("Extracted {d} table(s)\n", .{tables.items.len});
for (tables.items, 0..) |table, i| { const grid = table.object.get("grid").?.object; const rows = grid.get("rows").?.integer; const cols = grid.get("cols").?.integer; const markdown = table.object.get("markdown").?.string; std.debug.print("Table {d}: {d}x{d}\n{s}\n", .{ i, rows, cols, markdown });
for (grid.get("cells").?.array.items) |cell| { const content = cell.object.get("content").?.string; const row = cell.object.get("row").?.integer; const col = cell.object.get("col").?.integer; const is_header = cell.object.get("is_header").?.bool; std.debug.print(" [{d},{d}] header={} '{s}'\n", .{ row, col, is_header, content }); } }}Relationship to result.content
Section titled “Relationship to result.content”The Markdown in TableData.markdown is the same Markdown that appears inline inside result.content. The grid exists for code that needs cell-level access: headers vs body rows, span detection, or programmatic lookup by (row, col).
If the input has no tables, result.tables is an empty list. If the output format is "plain", tables are still extracted and their grids are still populated; only the rendering in result.content changes.
A cell with row_span > 1 or col_span > 1 appears once in cells, positioned at its top-left coordinates. Downstream code that iterates by (row, col) should respect the span or use the spans to reconstruct a dense grid.
Found a bug or mistake on this page?
If something here is wrong or out of date, open an issue on GitHub or contribute a fix via pull request.