-
-
Notifications
You must be signed in to change notification settings - Fork 925
feat: render smart chips, tabs, and all paragraph elements in markdown #649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
d432c64
6e59220
1073501
b1f095f
08ded0a
361f529
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,11 +7,15 @@ | |||||||||||||||||||||||||||||||
| - Ordered and unordered lists with nesting | ||||||||||||||||||||||||||||||||
| - Checklists with checked/unchecked state | ||||||||||||||||||||||||||||||||
| - Tables with header row separators | ||||||||||||||||||||||||||||||||
| - Smart chips: person (@mentions), rich links, dates, inline images, | ||||||||||||||||||||||||||||||||
| footnotes, horizontal rules, auto-text (page numbers), equations | ||||||||||||||||||||||||||||||||
| - Document tabs (multi-tab and nested child tabs) | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||
|
|
@@ -33,24 +37,80 @@ | |||||||||||||||||||||||||||||||
| def convert_doc_to_markdown(doc: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a Google Docs API document response to markdown. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Supports both legacy (top-level body) and tab-aware responses | ||||||||||||||||||||||||||||||||
| (includeTabsContent=True). For multi-tab docs, each tab gets a | ||||||||||||||||||||||||||||||||
| heading separator. Single-tab docs render without a tab heading. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||
| doc: The document JSON from docs.documents.get() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||
| Markdown string | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| tabs = doc.get("tabs", []) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if tabs: | ||||||||||||||||||||||||||||||||
| return _convert_tabs_to_markdown(tabs) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # Legacy: no tabs, use top-level body/lists/footnotes/inlineObjects | ||||||||||||||||||||||||||||||||
| return _convert_body_to_markdown(doc) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_tabs_to_markdown(tabs: list[dict[str, Any]]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a list of document tabs to markdown, recursing into child tabs.""" | ||||||||||||||||||||||||||||||||
| all_tab_docs: list[tuple[str, dict[str, Any]]] = [] | ||||||||||||||||||||||||||||||||
| _collect_tabs(tabs, all_tab_docs) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if len(all_tab_docs) == 1: | ||||||||||||||||||||||||||||||||
| _, tab_doc = all_tab_docs[0] | ||||||||||||||||||||||||||||||||
| return _convert_body_to_markdown(tab_doc) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| sections: list[str] = [] | ||||||||||||||||||||||||||||||||
| for title, tab_doc in all_tab_docs: | ||||||||||||||||||||||||||||||||
| tab_md = _convert_body_to_markdown(tab_doc) | ||||||||||||||||||||||||||||||||
| if tab_md.strip(): | ||||||||||||||||||||||||||||||||
| sections.append(f"# {title}\n\n{tab_md}") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return "\n".join(sections).rstrip("\n") + "\n" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _collect_tabs( | ||||||||||||||||||||||||||||||||
| tabs: list[dict[str, Any]], | ||||||||||||||||||||||||||||||||
| result: list[tuple[str, dict[str, Any]]], | ||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||
| """Flatten tab hierarchy into (title, documentTab) pairs.""" | ||||||||||||||||||||||||||||||||
| for tab in tabs: | ||||||||||||||||||||||||||||||||
| props = tab.get("tabProperties", {}) | ||||||||||||||||||||||||||||||||
| title = props.get("title", "Untitled Tab") | ||||||||||||||||||||||||||||||||
| doc_tab = tab.get("documentTab", {}) | ||||||||||||||||||||||||||||||||
| if doc_tab: | ||||||||||||||||||||||||||||||||
| result.append((title, doc_tab)) | ||||||||||||||||||||||||||||||||
| for child in tab.get("childTabs", []): | ||||||||||||||||||||||||||||||||
| _collect_tabs([child], result) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_body_to_markdown(doc: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a single document body (or documentTab) to markdown.""" | ||||||||||||||||||||||||||||||||
| body = doc.get("body", {}) | ||||||||||||||||||||||||||||||||
| content = body.get("content", []) | ||||||||||||||||||||||||||||||||
| lists_meta = doc.get("lists", {}) | ||||||||||||||||||||||||||||||||
| footnotes_meta = doc.get("footnotes", {}) | ||||||||||||||||||||||||||||||||
| inline_objects = doc.get("inlineObjects", {}) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| lines: list[str] = [] | ||||||||||||||||||||||||||||||||
| ordered_counters: dict[tuple[str, int], int] = {} | ||||||||||||||||||||||||||||||||
| prev_was_list = False | ||||||||||||||||||||||||||||||||
| footnote_defs: list[tuple[str, str]] = [] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| for element in content: | ||||||||||||||||||||||||||||||||
| if "paragraph" in element: | ||||||||||||||||||||||||||||||||
| para = element["paragraph"] | ||||||||||||||||||||||||||||||||
| text = _convert_paragraph_text(para) | ||||||||||||||||||||||||||||||||
| text = _convert_paragraph_text( | ||||||||||||||||||||||||||||||||
| para, | ||||||||||||||||||||||||||||||||
| footnotes_meta=footnotes_meta, | ||||||||||||||||||||||||||||||||
| inline_objects=inline_objects, | ||||||||||||||||||||||||||||||||
| footnote_defs=footnote_defs, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if not text.strip(): | ||||||||||||||||||||||||||||||||
| if prev_was_list: | ||||||||||||||||||||||||||||||||
|
|
@@ -66,10 +126,14 @@ def convert_doc_to_markdown(doc: dict[str, Any]) -> str: | |||||||||||||||||||||||||||||||
| checked = _is_checked(para) | ||||||||||||||||||||||||||||||||
| checkbox = "[x]" if checked else "[ ]" | ||||||||||||||||||||||||||||||||
| indent = " " * nesting | ||||||||||||||||||||||||||||||||
| # Re-render text without strikethrough for checked items | ||||||||||||||||||||||||||||||||
| # to avoid redundant ~~text~~ alongside [x] | ||||||||||||||||||||||||||||||||
| cb_text = ( | ||||||||||||||||||||||||||||||||
| _convert_paragraph_text(para, skip_strikethrough=True) | ||||||||||||||||||||||||||||||||
| _convert_paragraph_text( | ||||||||||||||||||||||||||||||||
| para, | ||||||||||||||||||||||||||||||||
| skip_strikethrough=True, | ||||||||||||||||||||||||||||||||
| footnotes_meta=footnotes_meta, | ||||||||||||||||||||||||||||||||
| inline_objects=inline_objects, | ||||||||||||||||||||||||||||||||
| footnote_defs=footnote_defs, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| if checked | ||||||||||||||||||||||||||||||||
| else text | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
@@ -106,22 +170,60 @@ def convert_doc_to_markdown(doc: dict[str, Any]) -> str: | |||||||||||||||||||||||||||||||
| ordered_counters.clear() | ||||||||||||||||||||||||||||||||
| lines.append("") | ||||||||||||||||||||||||||||||||
| prev_was_list = False | ||||||||||||||||||||||||||||||||
| table_md = _convert_table(element["table"]) | ||||||||||||||||||||||||||||||||
| table_md = _convert_table( | ||||||||||||||||||||||||||||||||
| element["table"], | ||||||||||||||||||||||||||||||||
| footnotes_meta=footnotes_meta, | ||||||||||||||||||||||||||||||||
| inline_objects=inline_objects, | ||||||||||||||||||||||||||||||||
| footnote_defs=footnote_defs, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| lines.append(table_md) | ||||||||||||||||||||||||||||||||
| lines.append("") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if footnote_defs: | ||||||||||||||||||||||||||||||||
| lines.append("") | ||||||||||||||||||||||||||||||||
| for fn_id, fn_text in footnote_defs: | ||||||||||||||||||||||||||||||||
| lines.append(f"[^{fn_id}]: {fn_text}") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| result = "\n".join(lines).rstrip("\n") + "\n" | ||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_paragraph_text( | ||||||||||||||||||||||||||||||||
| para: dict[str, Any], skip_strikethrough: bool = False | ||||||||||||||||||||||||||||||||
| para: dict[str, Any], | ||||||||||||||||||||||||||||||||
| skip_strikethrough: bool = False, | ||||||||||||||||||||||||||||||||
| footnotes_meta: dict[str, Any] | None = None, | ||||||||||||||||||||||||||||||||
| inline_objects: dict[str, Any] | None = None, | ||||||||||||||||||||||||||||||||
| footnote_defs: list[tuple[str, str]] | None = None, | ||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||
| """Convert paragraph elements to inline markdown text.""" | ||||||||||||||||||||||||||||||||
| parts: list[str] = [] | ||||||||||||||||||||||||||||||||
| for elem in para.get("elements", []): | ||||||||||||||||||||||||||||||||
| if "textRun" in elem: | ||||||||||||||||||||||||||||||||
| parts.append(_convert_text_run(elem["textRun"], skip_strikethrough)) | ||||||||||||||||||||||||||||||||
| elif "person" in elem: | ||||||||||||||||||||||||||||||||
| parts.append(_convert_person_chip(elem["person"])) | ||||||||||||||||||||||||||||||||
| elif "richLink" in elem: | ||||||||||||||||||||||||||||||||
| parts.append(_convert_rich_link_chip(elem["richLink"])) | ||||||||||||||||||||||||||||||||
| elif "dateElement" in elem: | ||||||||||||||||||||||||||||||||
| parts.append(_convert_date_chip(elem["dateElement"])) | ||||||||||||||||||||||||||||||||
| elif "inlineObjectElement" in elem: | ||||||||||||||||||||||||||||||||
| parts.append( | ||||||||||||||||||||||||||||||||
| _convert_inline_object(elem["inlineObjectElement"], inline_objects) | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| elif "footnoteReference" in elem: | ||||||||||||||||||||||||||||||||
| parts.append( | ||||||||||||||||||||||||||||||||
| _convert_footnote_reference( | ||||||||||||||||||||||||||||||||
| elem["footnoteReference"], footnotes_meta, footnote_defs | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| elif "horizontalRule" in elem: | ||||||||||||||||||||||||||||||||
| parts.append("\n---\n") | ||||||||||||||||||||||||||||||||
| elif "autoText" in elem: | ||||||||||||||||||||||||||||||||
| parts.append(_convert_auto_text(elem["autoText"])) | ||||||||||||||||||||||||||||||||
| elif "pageBreak" in elem or "columnBreak" in elem: | ||||||||||||||||||||||||||||||||
| pass # No meaningful markdown representation | ||||||||||||||||||||||||||||||||
| elif "equation" in elem: | ||||||||||||||||||||||||||||||||
| parts.append(_convert_equation(elem["equation"])) | ||||||||||||||||||||||||||||||||
| return "".join(parts).strip() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -133,12 +235,134 @@ def _convert_text_run( | |||||||||||||||||||||||||||||||
| style = text_run.get("textStyle", {}) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| text = content.rstrip("\n") | ||||||||||||||||||||||||||||||||
| # Replace Google Docs Private Use Area chip placeholders (e.g. \ue907) | ||||||||||||||||||||||||||||||||
| # that appear for unsupported chip types like vote, stopwatch, timer | ||||||||||||||||||||||||||||||||
| text = text.replace("\ue907", "[Smart Chip]") | ||||||||||||||||||||||||||||||||
| if not text: | ||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return _apply_text_style(text, style, skip_strikethrough) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_person_chip(person: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a person smart chip to a mailto link.""" | ||||||||||||||||||||||||||||||||
| props = person.get("personProperties", {}) | ||||||||||||||||||||||||||||||||
| name = props.get("name", "") | ||||||||||||||||||||||||||||||||
| email = props.get("email", "") | ||||||||||||||||||||||||||||||||
| if email: | ||||||||||||||||||||||||||||||||
| label = name or email | ||||||||||||||||||||||||||||||||
| return f"[{label}](mailto:{email})" | ||||||||||||||||||||||||||||||||
| return name or "" | ||||||||||||||||||||||||||||||||
|
Comment on lines
+251
to
+259
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not expand person chips into raw email addresses by default. This now embeds the underlying email in every person chip via 🛡️ Proposed safe default def _convert_person_chip(person: dict[str, Any]) -> str:
- """Convert a person smart chip to a mailto link."""
+ """Convert a person smart chip to display text."""
props = person.get("personProperties", {})
name = props.get("name", "")
email = props.get("email", "")
- if email:
- label = name or email
- return f"[{label}](mailto:{email})"
- return name or ""
+ return name or email or ""As per coding guidelines, "Never log or leak secrets, refresh tokens, or PII in exceptions or event streams" and "Tool response payloads should be concise; redact verbose HTML/email bodies by default and offer links or IDs to fetch full content." 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bad feedback, it should read the contents as they are.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
(´・ω・`) 🧠 Learnings used |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_rich_link_chip(rich_link: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a rich link smart chip to markdown. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Rich link chips contain a richLinkProperties dict with title and uri. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| props = rich_link.get("richLinkProperties", {}) | ||||||||||||||||||||||||||||||||
| title = props.get("title", "") | ||||||||||||||||||||||||||||||||
| uri = props.get("uri", "") | ||||||||||||||||||||||||||||||||
| if title and uri: | ||||||||||||||||||||||||||||||||
| return f"[{title}]({uri})" | ||||||||||||||||||||||||||||||||
| if uri: | ||||||||||||||||||||||||||||||||
| return uri | ||||||||||||||||||||||||||||||||
| return title or "" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_date_chip(date_elem: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a date smart chip (dateElement) to markdown. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Date elements contain dateElementProperties with displayText (the | ||||||||||||||||||||||||||||||||
| locale-formatted string) and a timestamp. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| props = date_elem.get("dateElementProperties", {}) | ||||||||||||||||||||||||||||||||
| display_text = props.get("displayText", "") | ||||||||||||||||||||||||||||||||
| if display_text: | ||||||||||||||||||||||||||||||||
| return display_text | ||||||||||||||||||||||||||||||||
| # Fallback: parse the timestamp into a date string | ||||||||||||||||||||||||||||||||
| timestamp = props.get("timestamp", "") | ||||||||||||||||||||||||||||||||
| if timestamp: | ||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||
| dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) | ||||||||||||||||||||||||||||||||
| return dt.strftime("%Y-%m-%d") | ||||||||||||||||||||||||||||||||
| except (ValueError, TypeError): | ||||||||||||||||||||||||||||||||
| return timestamp | ||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_inline_object( | ||||||||||||||||||||||||||||||||
| element: dict[str, Any], | ||||||||||||||||||||||||||||||||
| inline_objects: dict[str, Any] | None, | ||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||
| """Convert an inline object (image/drawing) to markdown.""" | ||||||||||||||||||||||||||||||||
| obj_id = element.get("inlineObjectId", "") | ||||||||||||||||||||||||||||||||
| if not inline_objects or obj_id not in inline_objects: | ||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||
| obj = inline_objects[obj_id] | ||||||||||||||||||||||||||||||||
| props = ( | ||||||||||||||||||||||||||||||||
| obj.get("inlineObjectProperties", {}) | ||||||||||||||||||||||||||||||||
| .get("embeddedObject", {}) | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| title = props.get("title", "") or props.get("description", "") | ||||||||||||||||||||||||||||||||
| uri = props.get("imageProperties", {}).get("contentUri", "") | ||||||||||||||||||||||||||||||||
| if uri: | ||||||||||||||||||||||||||||||||
| return f"" | ||||||||||||||||||||||||||||||||
| return f"[Image: {title}]" if title else "[Image]" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_footnote_reference( | ||||||||||||||||||||||||||||||||
| ref: dict[str, Any], | ||||||||||||||||||||||||||||||||
| footnotes_meta: dict[str, Any] | None, | ||||||||||||||||||||||||||||||||
| footnote_defs: list[tuple[str, str]] | None, | ||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a footnote reference to a markdown footnote marker. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Also collects the footnote definition text for appending at the end. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| fn_id = ref.get("footnoteId", "") | ||||||||||||||||||||||||||||||||
| if not fn_id: | ||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||
| if footnotes_meta and footnote_defs is not None and fn_id in footnotes_meta: | ||||||||||||||||||||||||||||||||
| fn_content = footnotes_meta[fn_id].get("content", []) | ||||||||||||||||||||||||||||||||
| fn_text = _extract_footnote_text(fn_content) | ||||||||||||||||||||||||||||||||
| # Avoid duplicates if the same footnote is referenced multiple times | ||||||||||||||||||||||||||||||||
| existing_ids = {fid for fid, _ in footnote_defs} | ||||||||||||||||||||||||||||||||
| if fn_id not in existing_ids: | ||||||||||||||||||||||||||||||||
| footnote_defs.append((fn_id, fn_text)) | ||||||||||||||||||||||||||||||||
| return f"[^{fn_id}]" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _extract_footnote_text(content: list[dict[str, Any]]) -> str: | ||||||||||||||||||||||||||||||||
| """Extract plain text from footnote content elements.""" | ||||||||||||||||||||||||||||||||
| parts: list[str] = [] | ||||||||||||||||||||||||||||||||
| for element in content: | ||||||||||||||||||||||||||||||||
| if "paragraph" in element: | ||||||||||||||||||||||||||||||||
| for elem in element["paragraph"].get("elements", []): | ||||||||||||||||||||||||||||||||
| if "textRun" in elem: | ||||||||||||||||||||||||||||||||
| text = elem["textRun"].get("content", "").strip() | ||||||||||||||||||||||||||||||||
| if text: | ||||||||||||||||||||||||||||||||
| parts.append(text) | ||||||||||||||||||||||||||||||||
| return " ".join(parts) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_auto_text(auto_text: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert an autoText element (e.g. page number) to a placeholder.""" | ||||||||||||||||||||||||||||||||
| text_type = auto_text.get("type", "") | ||||||||||||||||||||||||||||||||
| if text_type == "PAGE_NUMBER": | ||||||||||||||||||||||||||||||||
| return "[Page #]" | ||||||||||||||||||||||||||||||||
| if text_type == "PAGE_COUNT": | ||||||||||||||||||||||||||||||||
| return "[Page Count]" | ||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_equation(equation: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| """Convert an equation element to markdown.""" | ||||||||||||||||||||||||||||||||
| # The Docs API does not expose equation content as text — only the | ||||||||||||||||||||||||||||||||
| # suggestedInsertionIds / suggestedDeletionIds are available. | ||||||||||||||||||||||||||||||||
| return "[Equation]" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _apply_text_style( | ||||||||||||||||||||||||||||||||
| text: str, style: dict[str, Any], skip_strikethrough: bool = False | ||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||
|
|
@@ -212,7 +436,13 @@ def _is_checked(para: dict[str, Any]) -> bool: | |||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _convert_table(table: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| def _convert_table( | ||||||||||||||||||||||||||||||||
| table: dict[str, Any], | ||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||
| footnotes_meta: dict[str, Any] | None = None, | ||||||||||||||||||||||||||||||||
| inline_objects: dict[str, Any] | None = None, | ||||||||||||||||||||||||||||||||
| footnote_defs: list[tuple[str, str]] | None = None, | ||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||
| """Convert a table element to markdown.""" | ||||||||||||||||||||||||||||||||
| rows = table.get("tableRows", []) | ||||||||||||||||||||||||||||||||
| if not rows: | ||||||||||||||||||||||||||||||||
|
|
@@ -222,7 +452,12 @@ def _convert_table(table: dict[str, Any]) -> str: | |||||||||||||||||||||||||||||||
| for i, row in enumerate(rows): | ||||||||||||||||||||||||||||||||
| cells: list[str] = [] | ||||||||||||||||||||||||||||||||
| for cell in row.get("tableCells", []): | ||||||||||||||||||||||||||||||||
| cell_text = _extract_cell_text(cell) | ||||||||||||||||||||||||||||||||
| cell_text = _extract_cell_text( | ||||||||||||||||||||||||||||||||
| cell, | ||||||||||||||||||||||||||||||||
| footnotes_meta=footnotes_meta, | ||||||||||||||||||||||||||||||||
| inline_objects=inline_objects, | ||||||||||||||||||||||||||||||||
| footnote_defs=footnote_defs, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| cells.append(cell_text) | ||||||||||||||||||||||||||||||||
| md_rows.append("| " + " | ".join(cells) + " |") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -233,12 +468,23 @@ def _convert_table(table: dict[str, Any]) -> str: | |||||||||||||||||||||||||||||||
| return "\n".join(md_rows) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def _extract_cell_text(cell: dict[str, Any]) -> str: | ||||||||||||||||||||||||||||||||
| def _extract_cell_text( | ||||||||||||||||||||||||||||||||
| cell: dict[str, Any], | ||||||||||||||||||||||||||||||||
| *, | ||||||||||||||||||||||||||||||||
| footnotes_meta: dict[str, Any] | None = None, | ||||||||||||||||||||||||||||||||
| inline_objects: dict[str, Any] | None = None, | ||||||||||||||||||||||||||||||||
| footnote_defs: list[tuple[str, str]] | None = None, | ||||||||||||||||||||||||||||||||
| ) -> str: | ||||||||||||||||||||||||||||||||
| """Extract text from a table cell.""" | ||||||||||||||||||||||||||||||||
| parts: list[str] = [] | ||||||||||||||||||||||||||||||||
| for content_elem in cell.get("content", []): | ||||||||||||||||||||||||||||||||
| if "paragraph" in content_elem: | ||||||||||||||||||||||||||||||||
| text = _convert_paragraph_text(content_elem["paragraph"]) | ||||||||||||||||||||||||||||||||
| text = _convert_paragraph_text( | ||||||||||||||||||||||||||||||||
| content_elem["paragraph"], | ||||||||||||||||||||||||||||||||
| footnotes_meta=footnotes_meta, | ||||||||||||||||||||||||||||||||
| inline_objects=inline_objects, | ||||||||||||||||||||||||||||||||
| footnote_defs=footnote_defs, | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| if text.strip(): | ||||||||||||||||||||||||||||||||
| parts.append(text.strip()) | ||||||||||||||||||||||||||||||||
| cell_text = " ".join(parts) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.