emboss-pdf 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. emboss/__init__.py +77 -0
  2. emboss/__main__.py +311 -0
  3. emboss/adapters/__init__.py +7 -0
  4. emboss/adapters/docx_export.py +221 -0
  5. emboss/adapters/html_export.py +342 -0
  6. emboss/adapters/markdown_export.py +224 -0
  7. emboss/adapters/pydantic_schema.py +999 -0
  8. emboss/bibliography.py +208 -0
  9. emboss/charts.py +375 -0
  10. emboss/code_highlight.py +292 -0
  11. emboss/colors.py +298 -0
  12. emboss/constraints.py +272 -0
  13. emboss/crossref.py +174 -0
  14. emboss/generate.py +367 -0
  15. emboss/images.py +289 -0
  16. emboss/intelligence.py +843 -0
  17. emboss/layout/__init__.py +5 -0
  18. emboss/layout/engine.py +1269 -0
  19. emboss/markdown.py +290 -0
  20. emboss/math_render.py +772 -0
  21. emboss/numbering.py +37 -0
  22. emboss/pdf/__init__.py +12 -0
  23. emboss/pdf/assembler.py +153 -0
  24. emboss/pdf/fonts.py +233 -0
  25. emboss/pdf/objects.py +200 -0
  26. emboss/pdf/streams.py +275 -0
  27. emboss/pdf/tags.py +151 -0
  28. emboss/pdf/verify.py +136 -0
  29. emboss/pdfa.py +291 -0
  30. emboss/py.typed +0 -0
  31. emboss/redaction.py +99 -0
  32. emboss/signing.py +263 -0
  33. emboss/slides.py +223 -0
  34. emboss/spec.py +646 -0
  35. emboss/styles.py +295 -0
  36. emboss/svg.py +332 -0
  37. emboss/templates.py +133 -0
  38. emboss/toc.py +152 -0
  39. emboss/typography/__init__.py +10 -0
  40. emboss/typography/font_metrics.py +456 -0
  41. emboss/typography/hyphenation.py +324 -0
  42. emboss/typography/ligatures.py +42 -0
  43. emboss/typography/line_breaking.py +570 -0
  44. emboss/typography/protrusion.py +84 -0
  45. emboss/writer.py +1391 -0
  46. emboss_pdf-0.1.0.dist-info/METADATA +835 -0
  47. emboss_pdf-0.1.0.dist-info/RECORD +52 -0
  48. emboss_pdf-0.1.0.dist-info/WHEEL +5 -0
  49. emboss_pdf-0.1.0.dist-info/entry_points.txt +2 -0
  50. emboss_pdf-0.1.0.dist-info/licenses/LICENSE +191 -0
  51. emboss_pdf-0.1.0.dist-info/licenses/NOTICE +9 -0
  52. emboss_pdf-0.1.0.dist-info/top_level.txt +1 -0
emboss/__init__.py ADDED
@@ -0,0 +1,77 @@
1
+ """Emboss - constraint-driven PDF generation.
2
+
3
+ Describe a document; the engine handles layout, typography, and the
4
+ PDF/UA structure tree. Output is deterministic: identical input always
5
+ produces identical bytes.
6
+
7
+ from emboss import Document
8
+
9
+ doc = Document(title="Quarterly Report", style="finance")
10
+ doc.heading("Revenue Analysis", level=1)
11
+ doc.paragraph("Revenue increased 12% year over year.")
12
+ doc.table(headers=["Region", "Q3"], rows=[["North", "$2.4M"]])
13
+ doc.save("report.pdf")
14
+ """
15
+
16
+ from .constraints import (
17
+ ConstraintValidator, Issue, ValidationError, ValidationResult,
18
+ )
19
+ from .spec import (
20
+ BibliographyBlock, BulletList, Callout, Chart, Citation, CodeBlock,
21
+ Document, Footnote, HeaderFooter, Heading, HorizontalRule, Image,
22
+ LegalFeatures, MathBlock, NumberedList, PageBreak, PageSpec, Paragraph,
23
+ SvgBlock, Table, TableCell, TextRun,
24
+ )
25
+ from .bibliography import format_citation, format_bibliography
26
+ from .code_highlight import tokenize, colorize, THEMES as CODE_THEMES, LANGUAGES
27
+ from .math_render import parse_math, MathExpression, render_math
28
+ from .colors import (
29
+ ColorTheme, resolve_color, PALETTES,
30
+ CmykColor, SpotColor, rgb_to_cmyk, parse_cmyk,
31
+ )
32
+ from .crossref import CrossReferenceIndex
33
+ from .svg import SvgImage, parse_svg, render_svg
34
+ from .numbering import NumberingContext
35
+ from .styles import PRESETS, Style, StyleSheet, resolve_preset
36
+ from .typography.font_metrics import FontMetrics, FontRegistry
37
+ from .typography.hyphenation import Hyphenator
38
+ from .typography.line_breaking import LineBreaker
39
+ from .intelligence import (
40
+ ContentAnalyzer, QualityScorer, SmartTypography, TableIntelligence,
41
+ DocumentTypeDetector,
42
+ )
43
+ from .slides import slide_document, SlideConfig, SLIDE_16_9, SLIDE_4_3
44
+ from .templates import (
45
+ memo, report, letter, invoice, academic_paper, legal_brief,
46
+ slide_deck, data_sheet,
47
+ )
48
+ from .markdown import parse_markdown
49
+ from .generate import spec_prompt, generate, parse_spec_json
50
+ from .writer import RenderResult, render_document
51
+
52
+ __version__ = "0.1.0"
53
+
54
+ __all__ = [
55
+ "Document", "PageSpec", "Heading", "Paragraph", "BulletList",
56
+ "NumberedList", "Table", "TableCell", "TextRun", "Image", "Chart",
57
+ "Footnote", "Callout", "CodeBlock", "MathBlock", "BibliographyBlock",
58
+ "Citation", "SvgBlock", "PageBreak", "HorizontalRule", "LegalFeatures",
59
+ "HeaderFooter", "SvgImage", "parse_svg", "render_svg", "NumberingContext",
60
+ "format_citation", "format_bibliography",
61
+ "tokenize", "colorize", "CODE_THEMES", "LANGUAGES",
62
+ "parse_math", "MathExpression", "render_math",
63
+ "ColorTheme", "resolve_color", "PALETTES",
64
+ "CmykColor", "SpotColor", "rgb_to_cmyk", "parse_cmyk",
65
+ "CrossReferenceIndex",
66
+ "Style", "StyleSheet", "PRESETS", "resolve_preset",
67
+ "FontRegistry", "FontMetrics", "Hyphenator", "LineBreaker",
68
+ "ConstraintValidator", "ValidationResult", "ValidationError", "Issue",
69
+ "slide_document", "SlideConfig", "SLIDE_16_9", "SLIDE_4_3",
70
+ "memo", "report", "letter", "invoice", "academic_paper",
71
+ "legal_brief", "slide_deck", "data_sheet",
72
+ "render_document", "RenderResult",
73
+ "parse_markdown", "spec_prompt", "generate", "parse_spec_json",
74
+ "ContentAnalyzer", "QualityScorer", "SmartTypography",
75
+ "TableIntelligence", "DocumentTypeDetector",
76
+ "__version__",
77
+ ]
emboss/__main__.py ADDED
@@ -0,0 +1,311 @@
1
+ """CLI entry point: emboss render spec.json -o output.pdf
2
+
3
+ Supports JSON input from files or stdin, so an LLM can pipe its output
4
+ directly into PDF generation:
5
+
6
+ llm "Generate a financial report" --json | emboss render - -o report.pdf
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def _render(args: argparse.Namespace) -> int:
18
+ try:
19
+ from .adapters.pydantic_schema import DocumentSpec
20
+ except ImportError:
21
+ print(
22
+ "error: pydantic is required for JSON-to-PDF conversion.\n"
23
+ " pip install emboss-pdf[llm]",
24
+ file=sys.stderr,
25
+ )
26
+ return 1
27
+
28
+ if args.input == "-":
29
+ raw = sys.stdin.read()
30
+ else:
31
+ path = Path(args.input)
32
+ if not path.exists():
33
+ print(f"error: file not found: {path}", file=sys.stderr)
34
+ return 1
35
+ raw = path.read_text(encoding="utf-8")
36
+
37
+ try:
38
+ data = json.loads(raw)
39
+ except json.JSONDecodeError as exc:
40
+ print(f"error: invalid JSON: {exc}", file=sys.stderr)
41
+ return 1
42
+
43
+ try:
44
+ spec = DocumentSpec.model_validate(data)
45
+ except Exception as exc:
46
+ print(f"error: invalid document spec:\n {exc}", file=sys.stderr)
47
+ return 1
48
+
49
+ try:
50
+ doc = spec.to_document()
51
+ pdf_bytes = doc.render()
52
+ except Exception as exc:
53
+ print(f"error: rendering failed: {exc}", file=sys.stderr)
54
+ return 1
55
+
56
+ output = Path(args.output)
57
+ output.write_bytes(pdf_bytes)
58
+
59
+ if not args.quiet:
60
+ from .pdf.verify import verify_pdf
61
+
62
+ report = verify_pdf(pdf_bytes)
63
+ print(
64
+ f"{output} — {len(pdf_bytes):,} bytes, "
65
+ f"{report.page_count} page{'s' if report.page_count != 1 else ''}, "
66
+ f"tagged={report.has_struct_tree}",
67
+ )
68
+ if not report.ok:
69
+ for problem in report.problems:
70
+ print(f" warning: {problem}", file=sys.stderr)
71
+ return 0
72
+
73
+
74
+ def _schema(args: argparse.Namespace) -> int:
75
+ try:
76
+ from .adapters.pydantic_schema import generate_json_schema
77
+ except ImportError:
78
+ print(
79
+ "error: pydantic is required for schema generation.\n"
80
+ " pip install emboss-pdf[llm]",
81
+ file=sys.stderr,
82
+ )
83
+ return 1
84
+
85
+ output = generate_json_schema(indent=2)
86
+
87
+ if args.output:
88
+ Path(args.output).write_text(output, encoding="utf-8")
89
+ if not args.quiet:
90
+ print(f"Schema written to {args.output}")
91
+ else:
92
+ print(output)
93
+ return 0
94
+
95
+
96
+ def _verify(args: argparse.Namespace) -> int:
97
+ from .pdf.verify import verify_pdf
98
+
99
+ path = Path(args.input)
100
+ if not path.exists():
101
+ print(f"error: file not found: {path}", file=sys.stderr)
102
+ return 1
103
+
104
+ data = path.read_bytes()
105
+ report = verify_pdf(data)
106
+ print(report)
107
+ return 0 if report.ok else 1
108
+
109
+
110
+ def _export(args: argparse.Namespace) -> int:
111
+ try:
112
+ from .adapters.pydantic_schema import DocumentSpec
113
+ except ImportError:
114
+ print(
115
+ "error: pydantic is required.\n pip install emboss-pdf[llm]",
116
+ file=sys.stderr,
117
+ )
118
+ return 1
119
+
120
+ if args.input == "-":
121
+ raw = sys.stdin.read()
122
+ else:
123
+ path = Path(args.input)
124
+ if not path.exists():
125
+ print(f"error: file not found: {path}", file=sys.stderr)
126
+ return 1
127
+ raw = path.read_text(encoding="utf-8")
128
+
129
+ try:
130
+ spec = DocumentSpec.model_validate_json(raw)
131
+ except Exception as exc:
132
+ print(f"error: invalid document spec:\n {exc}", file=sys.stderr)
133
+ return 1
134
+
135
+ doc = spec.to_document()
136
+ fmt = args.format
137
+
138
+ if fmt == "html":
139
+ from .adapters.html_export import to_html
140
+
141
+ output = to_html(doc, standalone=True)
142
+ elif fmt == "markdown":
143
+ from .adapters.markdown_export import to_markdown
144
+
145
+ output = to_markdown(doc)
146
+ elif fmt == "office-json":
147
+ from .adapters.docx_export import to_office_dict
148
+
149
+ output = json.dumps(to_office_dict(doc), indent=2, ensure_ascii=False)
150
+ else:
151
+ print(f"error: unknown format: {fmt}", file=sys.stderr)
152
+ return 1
153
+
154
+ if args.output:
155
+ Path(args.output).write_text(output, encoding="utf-8")
156
+ if not args.quiet:
157
+ print(f"Exported {fmt} to {args.output}")
158
+ else:
159
+ print(output)
160
+ return 0
161
+
162
+
163
+ def _analyze(args: argparse.Namespace) -> int:
164
+ from .intelligence import ContentAnalyzer
165
+
166
+ if args.input == "-":
167
+ raw = sys.stdin.read()
168
+ else:
169
+ path = Path(args.input)
170
+ if not path.exists():
171
+ print(f"error: file not found: {path}", file=sys.stderr)
172
+ return 1
173
+ raw = path.read_text(encoding="utf-8")
174
+
175
+ try:
176
+ data = json.loads(raw)
177
+ except json.JSONDecodeError as exc:
178
+ print(f"error: invalid JSON: {exc}", file=sys.stderr)
179
+ return 1
180
+
181
+ analyzer = ContentAnalyzer()
182
+ analysis = analyzer.analyze_spec(data)
183
+ print(analysis.summary)
184
+ return 0
185
+
186
+
187
+ def _validate(args: argparse.Namespace) -> int:
188
+ try:
189
+ from .adapters.pydantic_schema import DocumentSpec
190
+ except ImportError:
191
+ print(
192
+ "error: pydantic is required.\n pip install emboss-pdf[llm]",
193
+ file=sys.stderr,
194
+ )
195
+ return 1
196
+
197
+ if args.input == "-":
198
+ raw = sys.stdin.read()
199
+ else:
200
+ raw = Path(args.input).read_text(encoding="utf-8")
201
+
202
+ try:
203
+ data = json.loads(raw)
204
+ except json.JSONDecodeError as exc:
205
+ print(f"error: invalid JSON: {exc}", file=sys.stderr)
206
+ return 1
207
+
208
+ try:
209
+ spec = DocumentSpec.model_validate(data)
210
+ except Exception as exc:
211
+ print(f"invalid: {exc}", file=sys.stderr)
212
+ return 1
213
+
214
+ print(
215
+ f"valid: {spec.title!r}, "
216
+ f"{len(spec.content)} blocks, "
217
+ f"style={spec.style}"
218
+ )
219
+ return 0
220
+
221
+
222
+ def main(argv: list[str] | None = None) -> int:
223
+ parser = argparse.ArgumentParser(
224
+ prog="emboss",
225
+ description="Emboss — constraint-driven PDF generation for LLM pipelines",
226
+ )
227
+ parser.add_argument(
228
+ "--version", action="version",
229
+ version=f"%(prog)s {_get_version()}",
230
+ )
231
+ sub = parser.add_subparsers(dest="command")
232
+
233
+ render_p = sub.add_parser(
234
+ "render",
235
+ help="Render a JSON document spec to PDF",
236
+ description="Convert a JSON document specification to a professionally typeset PDF.",
237
+ )
238
+ render_p.add_argument("input", help="JSON spec file path, or '-' for stdin")
239
+ render_p.add_argument("-o", "--output", default="output.pdf", help="Output PDF path (default: output.pdf)")
240
+ render_p.add_argument("-q", "--quiet", action="store_true", help="Suppress status output")
241
+
242
+ schema_p = sub.add_parser(
243
+ "schema",
244
+ help="Export the JSON Schema for LLM prompt engineering",
245
+ description="Generate the JSON Schema that LLMs use to produce valid document specs.",
246
+ )
247
+ schema_p.add_argument("-o", "--output", default=None, help="Write to file instead of stdout")
248
+ schema_p.add_argument("-q", "--quiet", action="store_true")
249
+
250
+ verify_p = sub.add_parser(
251
+ "verify",
252
+ help="Verify an existing PDF's structural integrity",
253
+ )
254
+ verify_p.add_argument("input", help="PDF file path")
255
+
256
+ export_p = sub.add_parser(
257
+ "export",
258
+ help="Export a JSON spec to HTML, Markdown, or Office-ready JSON",
259
+ description="Convert a document spec to alternative formats for preview, editing, or PPTX/DOCX conversion.",
260
+ )
261
+ export_p.add_argument("input", help="JSON spec file path, or '-' for stdin")
262
+ export_p.add_argument(
263
+ "-f", "--format",
264
+ choices=["html", "markdown", "office-json"],
265
+ default="html",
266
+ help="Output format (default: html)",
267
+ )
268
+ export_p.add_argument("-o", "--output", default=None, help="Write to file instead of stdout")
269
+ export_p.add_argument("-q", "--quiet", action="store_true")
270
+
271
+ analyze_p = sub.add_parser(
272
+ "analyze",
273
+ help="Run content intelligence analysis on a document spec",
274
+ description="Detect document type, classify table columns, and report content intelligence.",
275
+ )
276
+ analyze_p.add_argument("input", help="JSON spec file, or '-' for stdin")
277
+
278
+ validate_p = sub.add_parser(
279
+ "validate",
280
+ help="Validate a JSON document spec without rendering",
281
+ )
282
+ validate_p.add_argument("input", help="JSON spec file, or '-' for stdin")
283
+
284
+ args = parser.parse_args(argv)
285
+
286
+ if args.command is None:
287
+ parser.print_help()
288
+ return 0
289
+
290
+ handlers = {
291
+ "render": _render,
292
+ "schema": _schema,
293
+ "verify": _verify,
294
+ "export": _export,
295
+ "analyze": _analyze,
296
+ "validate": _validate,
297
+ }
298
+ return handlers[args.command](args)
299
+
300
+
301
+ def _get_version() -> str:
302
+ try:
303
+ from . import __version__
304
+
305
+ return __version__
306
+ except Exception:
307
+ return "0.1.0"
308
+
309
+
310
+ if __name__ == "__main__":
311
+ sys.exit(main())
@@ -0,0 +1,7 @@
1
+ """Adapters for external systems and format conversion.
2
+
3
+ pydantic_schema - Pydantic v2 models for LLM structured output
4
+ html_export - Semantic HTML for preview and interchange
5
+ markdown_export - Markdown for LLM round-trips and pandoc conversion
6
+ docx_export - Structured dict for DOCX/PPTX conversion
7
+ """
@@ -0,0 +1,221 @@
1
+ """Export a Document to a structured representation for DOCX/PPTX conversion.
2
+
3
+ Rather than depending on python-docx or python-pptx directly (which would
4
+ add heavy dependencies), this module exports a clean intermediate dict
5
+ that any conversion tool can consume:
6
+
7
+ from emboss.adapters.docx_export import to_office_dict
8
+ data = to_office_dict(document)
9
+ # Feed to python-docx, pandoc, or any converter
10
+
11
+ The dict mirrors the Open XML structure closely enough that conversion
12
+ is mechanical, but uses plain Python types so no Office libraries are
13
+ required at export time.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from ..spec import Document
22
+
23
+ __all__ = ["to_office_dict"]
24
+
25
+
26
+ def to_office_dict(document: "Document") -> dict:
27
+ """Convert a Document to a structured dict for Office format conversion.
28
+
29
+ Returns a dict with:
30
+ - metadata: title, author, subject, keywords, language
31
+ - styles: resolved style presets as CSS-like properties
32
+ - content: ordered list of typed blocks with full formatting
33
+ """
34
+ from ..spec import (
35
+ BibliographyBlock,
36
+ BulletList,
37
+ Callout,
38
+ Chart,
39
+ CodeBlock,
40
+ Footnote,
41
+ Heading,
42
+ HorizontalRule,
43
+ Image,
44
+ MathBlock,
45
+ PageBreak,
46
+ Paragraph,
47
+ Table,
48
+ )
49
+
50
+ sheet = document.stylesheet
51
+ blocks = []
52
+
53
+ for element in document.content:
54
+ if isinstance(element, Heading):
55
+ blocks.append({
56
+ "type": "heading",
57
+ "level": element.level,
58
+ "text": element.text,
59
+ "numbering": element.numbering,
60
+ "runs": _serialize_runs(element.runs),
61
+ })
62
+
63
+ elif isinstance(element, Paragraph):
64
+ style = sheet.resolved(sheet.body, element.style)
65
+ blocks.append({
66
+ "type": "paragraph",
67
+ "runs": _serialize_runs(element.runs),
68
+ "align": style.require("align"),
69
+ "indent_first": style.indent_first or 0,
70
+ })
71
+
72
+ elif isinstance(element, BulletList):
73
+ items = []
74
+ for item_runs in element.item_runs:
75
+ items.append({"runs": _serialize_runs(item_runs)})
76
+ blocks.append({
77
+ "type": "list",
78
+ "bullet": element.bullet,
79
+ "items": items,
80
+ })
81
+
82
+ elif isinstance(element, Table):
83
+ blocks.append(_serialize_table(element))
84
+
85
+ elif isinstance(element, Image):
86
+ blocks.append({
87
+ "type": "image",
88
+ "source": element.source if isinstance(element.source, str) else "<bytes>",
89
+ "alt_text": element.alt_text,
90
+ "width": element.width,
91
+ "height": element.height,
92
+ "caption": element.caption,
93
+ "align": element.align,
94
+ })
95
+
96
+ elif isinstance(element, Chart):
97
+ blocks.append({
98
+ "type": "chart",
99
+ "chart_type": element.chart_type,
100
+ "labels": list(element.labels),
101
+ "values": list(element.values),
102
+ "title": element.title,
103
+ "width": element.width,
104
+ "height": element.height,
105
+ })
106
+
107
+ elif isinstance(element, Footnote):
108
+ blocks.append({
109
+ "type": "footnote",
110
+ "marker": element.marker,
111
+ "runs": _serialize_runs(element.runs),
112
+ })
113
+
114
+ elif isinstance(element, Callout):
115
+ blocks.append({
116
+ "type": "callout",
117
+ "variant": element.variant,
118
+ "title": element.title,
119
+ "runs": _serialize_runs(element.runs),
120
+ "background": element.background,
121
+ "border_color": element.border_color,
122
+ })
123
+
124
+ elif isinstance(element, CodeBlock):
125
+ blocks.append({
126
+ "type": "code_block",
127
+ "code": element.code,
128
+ "language": element.language,
129
+ "line_numbers": element.line_numbers,
130
+ "caption": element.caption,
131
+ })
132
+
133
+ elif isinstance(element, MathBlock):
134
+ blocks.append({
135
+ "type": "math",
136
+ "source": element.source,
137
+ "display": element.display,
138
+ "caption": element.caption,
139
+ })
140
+
141
+ elif isinstance(element, BibliographyBlock):
142
+ from ..bibliography import format_bibliography
143
+ blocks.append({
144
+ "type": "bibliography",
145
+ "title": element.title,
146
+ "heading_level": element.heading_level,
147
+ "entries": format_bibliography(
148
+ element.citations, element.bib_style
149
+ ),
150
+ })
151
+
152
+ elif isinstance(element, HorizontalRule):
153
+ blocks.append({"type": "horizontal_rule"})
154
+
155
+ elif isinstance(element, PageBreak):
156
+ blocks.append({"type": "page_break"})
157
+
158
+ page = document.page
159
+ return {
160
+ "metadata": {
161
+ "title": document.title,
162
+ "author": document.author,
163
+ "subject": document.subject,
164
+ "keywords": document.keywords,
165
+ "language": document.language,
166
+ },
167
+ "page": {
168
+ "width_pt": page.width,
169
+ "height_pt": page.height,
170
+ "margin_top_pt": page.margin_top,
171
+ "margin_right_pt": page.margin_right,
172
+ "margin_bottom_pt": page.margin_bottom,
173
+ "margin_left_pt": page.margin_left,
174
+ },
175
+ "content": blocks,
176
+ }
177
+
178
+
179
+ def _serialize_runs(runs: list) -> list[dict]:
180
+ return [
181
+ {
182
+ "text": run.text,
183
+ "bold": run.bold,
184
+ "italic": run.italic,
185
+ "font_size": run.font_size,
186
+ "font_family": run.font_family,
187
+ "color": run.color,
188
+ "link": run.link,
189
+ }
190
+ for run in runs
191
+ ]
192
+
193
+
194
+ def _serialize_table(table) -> dict:
195
+ headers = []
196
+ for cell in table.header_cells:
197
+ headers.append({
198
+ "text": cell.plain_text,
199
+ "align": cell.align,
200
+ "bold": cell.bold,
201
+ })
202
+
203
+ rows = []
204
+ for row in table.body_rows:
205
+ cells = []
206
+ for cell in row:
207
+ cells.append({
208
+ "text": cell.plain_text,
209
+ "align": cell.align,
210
+ "bold": cell.bold,
211
+ "background": cell.background,
212
+ })
213
+ rows.append(cells)
214
+
215
+ return {
216
+ "type": "table",
217
+ "headers": headers,
218
+ "rows": rows,
219
+ "caption": table.caption,
220
+ "stripe": table.stripe,
221
+ }