From 858a474f07a0caa7fb44b21c740cc16b15fe5d96 Mon Sep 17 00:00:00 2001 From: barry0451 Date: Wed, 1 Apr 2026 22:43:05 +0000 Subject: [PATCH 1/2] fix: escape pipe chars in markdown table cells for extra field values (fixes #832) --- CHANGELOG.md | 1 + datacontract/export/markdown_exporter.py | 15 ++++++++++--- tests/test_export_markdown.py | 27 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22613649..7b129fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix SQL export generating multiple PRIMARY KEY constraints for composite keys (#1026) - Preserve parametrized physicalTypes for SQL export (#1086) - Fix incorrect SQL type mappings: SQL Server `double`/`jsonb`, MySQL bare `varchar`, missing Trino types (#1110) +- Fix markdown export breaking table structure when extra field values contain pipe characters (#832) ## [0.11.7] - 2026-03-24 diff --git a/datacontract/export/markdown_exporter.py b/datacontract/export/markdown_exporter.py index 443417f8..426b5285 100644 --- a/datacontract/export/markdown_exporter.py +++ b/datacontract/export/markdown_exporter.py @@ -366,11 +366,20 @@ def render_header(key: str) -> str: if isinstance(value_extra, list) and len(value_extra): if isinstance(value_extra[0], dict): - parts.append(array_of_dict_to_markdown(value_extra)) + raw = array_of_dict_to_markdown(value_extra) + if is_in_table_cell: + raw = raw.replace("|", "|").replace("\n", "
") + parts.append(raw) elif isinstance(value_extra[0], str): - parts.append(array_to_markdown(value_extra)) + raw = array_to_markdown(value_extra) + if is_in_table_cell: + raw = raw.replace("\n", "
") + parts.append(raw) elif isinstance(value_extra, dict): - parts.append(dict_to_markdown(value_extra)) + raw = dict_to_markdown(value_extra) + if is_in_table_cell: + raw = raw.replace("\n", "
") + parts.append(raw) else: parts.append(f"{str(value_extra)}{value_line_ending}") diff --git a/tests/test_export_markdown.py b/tests/test_export_markdown.py index f77036f7..4fd05b86 100644 --- a/tests/test_export_markdown.py +++ b/tests/test_export_markdown.py @@ -1,4 +1,6 @@ +import yaml from datacontract_specification.model import DataContractSpecification +from open_data_contract_standard.model import OpenDataContractStandard from typer.testing import CliRunner from datacontract.cli import app @@ -30,3 +32,28 @@ def test_to_markdown(): with open("fixtures/markdown/export/expected.md", "r") as file: assert result == file.read() + + +def test_pipe_chars_escaped_in_table_cells(): + """Regression test for #832: pipe chars in extra field values should be escaped when inside table cells.""" + contract_yaml = """ +id: test-pipe-escape +schema: + - name: orders + properties: + - name: order_id + logicalType: string + config: + mapping: "a | b | c" +""" + contract = OpenDataContractStandard.model_validate(yaml.safe_load(contract_yaml)) + result = to_markdown(contract) + + # Find the table row for order_id + lines = [line for line in result.split("\n") if "order_id" in line and line.startswith("|")] + assert lines, "order_id table row not found" + row = lines[0] + # The row must have exactly 4 pipe chars as table delimiters (| col1 | col2 | col3 |) + assert row.count("|") == 4, ( + f"Expected 4 pipe delimiters in row, got {row.count('|')}: {row!r}" + ) From 4f3e7df175546dcfd99bcb272f960d3c192810e4 Mon Sep 17 00:00:00 2001 From: barry0451 Date: Sun, 5 Apr 2026 01:40:35 +0000 Subject: [PATCH 2/2] fix: also escape pipe chars in array_to_markdown and dict_to_markdown within table cells --- datacontract/export/markdown_exporter.py | 4 ++-- .../markdown/export/multicolumn.odcs.yaml | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/markdown/export/multicolumn.odcs.yaml diff --git a/datacontract/export/markdown_exporter.py b/datacontract/export/markdown_exporter.py index 426b5285..9e8e6045 100644 --- a/datacontract/export/markdown_exporter.py +++ b/datacontract/export/markdown_exporter.py @@ -373,12 +373,12 @@ def render_header(key: str) -> str: elif isinstance(value_extra[0], str): raw = array_to_markdown(value_extra) if is_in_table_cell: - raw = raw.replace("\n", "
") + raw = raw.replace("|", "|").replace("\n", "
") parts.append(raw) elif isinstance(value_extra, dict): raw = dict_to_markdown(value_extra) if is_in_table_cell: - raw = raw.replace("\n", "
") + raw = raw.replace("|", "|").replace("\n", "
") parts.append(raw) else: parts.append(f"{str(value_extra)}{value_line_ending}") diff --git a/tests/fixtures/markdown/export/multicolumn.odcs.yaml b/tests/fixtures/markdown/export/multicolumn.odcs.yaml new file mode 100644 index 00000000..cbece142 --- /dev/null +++ b/tests/fixtures/markdown/export/multicolumn.odcs.yaml @@ -0,0 +1,23 @@ +id: test-multicolumn-export +name: Multicolumn Test Contract +version: 1.0.0 +schema: + - name: orders + description: Order records + properties: + - name: order_id + physicalType: string + quality: + - type: sql + description: Row count must be positive + query: SELECT count(*) FROM orders + mustBeGreaterThan: 0 + - type: text + description: Non-null check + - name: amount + physicalType: double + quality: + - type: sql + description: Amount must be positive + query: SELECT count(*) FROM orders WHERE amount <= 0 + mustBe: 0