Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class FileType(str, Enum):
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
OFFICE_EXTENSIONS = {'.docx', '.xlsx'}
OFFICE_EXTENSIONS = {'.docx', '.xlsx', '.pptx'}
VIDEO_EXTENSIONS = {'.mp4', '.mov', '.webm', '.mkv', '.avi', '.m4v', '.mp3', '.wav', '.m4a', '.ogg'}

CORPUS_WARN_THRESHOLD = 50_000 # words - below this, warn "you may not need a graph"
Expand Down Expand Up @@ -621,8 +621,30 @@ def _edge(src: str, tgt: str, relation: str) -> None:
return {"nodes": nodes, "edges": edges}


def pptx_to_markdown(path: Path) -> str:
"""Convert a .pptx file to markdown text using python-pptx."""
if not _zip_within_caps(path):
return ""
try:
from pptx import Presentation
prs = Presentation(str(path))
lines = []
for i, slide in enumerate(prs.slides, start=1):
lines.append(f"## Slide {i}")
for shape in slide.shapes:
if shape.has_text_frame:
text = shape.text_frame.text.strip()
if text:
lines.append(text)
return "\n".join(lines)
except ImportError:
return ""
except Exception:
return ""


def convert_office_file(path: Path, out_dir: Path) -> Path | None:
"""Convert a .docx or .xlsx to a markdown sidecar in out_dir.
"""Convert a .docx, .xlsx, or .pptx to a markdown sidecar in out_dir.

Returns the path of the converted .md file, or None if conversion failed
or the required library is not installed.
Expand All @@ -632,6 +654,8 @@ def convert_office_file(path: Path, out_dir: Path) -> Path | None:
text = docx_to_markdown(path)
elif ext == ".xlsx":
text = xlsx_to_markdown(path)
elif ext == ".pptx":
text = pptx_to_markdown(path)
else:
return None

Expand Down Expand Up @@ -679,6 +703,8 @@ def count_words(path: Path) -> int:
return len(docx_to_markdown(path).split())
if ext == ".xlsx":
return len(xlsx_to_markdown(path).split())
if ext == ".pptx":
return len(pptx_to_markdown(path).split())
with open(_os_path(path), encoding="utf-8", errors="ignore") as f:
return len(f.read().split())
except Exception:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pdf = ["pypdf>=6.12.0", "markdownify"]
watch = ["watchdog"]
svg = ["matplotlib", "numpy>=2.0; python_version >= '3.13'"]
leiden = ["graspologic; python_version < '3.13'"]
office = ["python-docx", "openpyxl"]
office = ["python-docx", "openpyxl", "python-pptx"]
google = ["openpyxl"]
postgres = ["psycopg[binary]"]
video = ["faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9"]
Expand All @@ -80,7 +80,7 @@ pascal = ["tree-sitter-pascal"]
# avoids breaking the default `uv tool install graphifyy` for everyone (#1104).
dm = ["tree-sitter-dm"]
terraform = ["tree-sitter-hcl"]
all = ["mcp", "starlette>=1.3.1", "neo4j", "falkordb", "pypdf>=6.12.0", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal"]
all = ["mcp", "starlette>=1.3.1", "neo4j", "falkordb", "pypdf>=6.12.0", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "python-pptx", "faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9", "matplotlib", "numpy>=2.0; python_version >= '3.13'", "openai", "tiktoken", "boto3", "anthropic", "tree-sitter-sql", "jieba", "tree-sitter-dm", "tree-sitter-hcl", "tree-sitter-pascal"]

[project.scripts]
graphify = "graphify.__main__:main"
Expand Down
70 changes: 70 additions & 0 deletions tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1632,3 +1632,73 @@ def test_detect_surfaces_unreadable_dir_instead_of_silent_skip(tmp_path, capsys)
assert any(f.endswith("a.py") for f in code) # rest of tree still enumerated
assert len(res["walk_errors"]) >= 1
assert "could not scan" in capsys.readouterr().err

def test_classify_docx():
assert classify_file(Path("report.docx")) == FileType.DOCUMENT

def test_classify_xlsx():
assert classify_file(Path("data.xlsx")) == FileType.DOCUMENT

def test_classify_pptx():
assert classify_file(Path("slides.pptx")) == FileType.DOCUMENT

import pytest

def test_docx_to_markdown(tmp_path):
pytest.importorskip("docx")
from docx import Document
from graphify.detect import docx_to_markdown
doc = Document()
doc.add_heading("Title", level=1)
doc.add_paragraph("Hello world content here.")
path = tmp_path / "test.docx"
doc.save(path)
text = docx_to_markdown(path)
assert "Hello" in text or "Title" in text

def test_xlsx_to_markdown(tmp_path):
pytest.importorskip("openpyxl")
import openpyxl
from graphify.detect import xlsx_to_markdown
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Revenue"
ws.append(["Month", "Amount", "Region"])
ws.append(["Jan", 1000, "North"])
path = tmp_path / "data.xlsx"
wb.save(path)
text = xlsx_to_markdown(path)
assert "Revenue" in text
assert "Month" in text
assert "Amount" in text

def test_pptx_to_markdown(tmp_path):
pytest.importorskip("pptx")
from pptx import Presentation
from graphify.detect import pptx_to_markdown
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = "Q4 Strategy"
path = tmp_path / "deck.pptx"
prs.save(path)
text = pptx_to_markdown(path)
assert "Q4 Strategy" in text

def test_pptx_to_markdown_returns_empty_on_error(tmp_path):
from graphify.detect import pptx_to_markdown
fake = tmp_path / "corrupt.pptx"
fake.write_bytes(b"not a real pptx")
result = pptx_to_markdown(fake)
assert isinstance(result, str)
assert result == ""

def test_count_words_docx(tmp_path):
pytest.importorskip("docx")
from docx import Document
doc = Document()
doc.add_heading("Title", level=1)
doc.add_paragraph("This document has several words in it.")
docx = tmp_path / "test.docx"
doc.save(docx)
from graphify.detect import count_words
assert count_words(docx) > 5
30 changes: 29 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.