texflow-mcp 1.0.0__tar.gz

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 (49) hide show
  1. texflow_mcp-1.0.0/LICENSE +21 -0
  2. texflow_mcp-1.0.0/MANIFEST.in +4 -0
  3. texflow_mcp-1.0.0/PKG-INFO +138 -0
  4. texflow_mcp-1.0.0/README.md +116 -0
  5. texflow_mcp-1.0.0/pyproject.toml +50 -0
  6. texflow_mcp-1.0.0/server.py +226 -0
  7. texflow_mcp-1.0.0/setup.cfg +4 -0
  8. texflow_mcp-1.0.0/tests/test_compiler.py +99 -0
  9. texflow_mcp-1.0.0/tests/test_formatters.py +232 -0
  10. texflow_mcp-1.0.0/tests/test_ingestion.py +219 -0
  11. texflow_mcp-1.0.0/tests/test_model.py +206 -0
  12. texflow_mcp-1.0.0/tests/test_queue.py +179 -0
  13. texflow_mcp-1.0.0/tests/test_serializer.py +380 -0
  14. texflow_mcp-1.0.0/tests/test_tools.py +400 -0
  15. texflow_mcp-1.0.0/tests/test_workflow.py +245 -0
  16. texflow_mcp-1.0.0/texflow/__init__.py +3 -0
  17. texflow_mcp-1.0.0/texflow/compiler.py +181 -0
  18. texflow_mcp-1.0.0/texflow/data/class_defaults.json +44 -0
  19. texflow_mcp-1.0.0/texflow/data/font_map.json +21 -0
  20. texflow_mcp-1.0.0/texflow/data/latex_reference/README.md +58 -0
  21. texflow_mcp-1.0.0/texflow/data/latex_reference/build_reference_data.py +119 -0
  22. texflow_mcp-1.0.0/texflow/data/latex_reference/commands/math.json +105 -0
  23. texflow_mcp-1.0.0/texflow/data/latex_reference/commands/package-mappings.json +73 -0
  24. texflow_mcp-1.0.0/texflow/data/latex_reference/commands/sectioning.json +76 -0
  25. texflow_mcp-1.0.0/texflow/data/latex_reference/errors/patterns.json +113 -0
  26. texflow_mcp-1.0.0/texflow/data/latex_reference/metadata.json +22 -0
  27. texflow_mcp-1.0.0/texflow/data/latex_reference/packages/amsmath.json +89 -0
  28. texflow_mcp-1.0.0/texflow/data/latex_reference/packages/common-packages.json +80 -0
  29. texflow_mcp-1.0.0/texflow/data/latex_reference/symbols/greek.json +149 -0
  30. texflow_mcp-1.0.0/texflow/data/latex_reference/symbols/math.json +184 -0
  31. texflow_mcp-1.0.0/texflow/formatters.py +178 -0
  32. texflow_mcp-1.0.0/texflow/ingestion.py +310 -0
  33. texflow_mcp-1.0.0/texflow/model.py +337 -0
  34. texflow_mcp-1.0.0/texflow/serializer.py +452 -0
  35. texflow_mcp-1.0.0/texflow/tools/__init__.py +1 -0
  36. texflow_mcp-1.0.0/texflow/tools/document.py +122 -0
  37. texflow_mcp-1.0.0/texflow/tools/edit.py +239 -0
  38. texflow_mcp-1.0.0/texflow/tools/layout.py +169 -0
  39. texflow_mcp-1.0.0/texflow/tools/queue.py +137 -0
  40. texflow_mcp-1.0.0/texflow/tools/reference.py +399 -0
  41. texflow_mcp-1.0.0/texflow/tools/render.py +74 -0
  42. texflow_mcp-1.0.0/texflow/tools/state.py +76 -0
  43. texflow_mcp-1.0.0/texflow/tools/workflow.py +123 -0
  44. texflow_mcp-1.0.0/texflow_mcp.egg-info/PKG-INFO +138 -0
  45. texflow_mcp-1.0.0/texflow_mcp.egg-info/SOURCES.txt +47 -0
  46. texflow_mcp-1.0.0/texflow_mcp.egg-info/dependency_links.txt +1 -0
  47. texflow_mcp-1.0.0/texflow_mcp.egg-info/entry_points.txt +2 -0
  48. texflow_mcp-1.0.0/texflow_mcp.egg-info/requires.txt +2 -0
  49. texflow_mcp-1.0.0/texflow_mcp.egg-info/top_level.txt +2 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aaron Bockelie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include README.md
2
+ include pyproject.toml
3
+ include server.py
4
+ recursive-include texflow/data *
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: texflow-mcp
3
+ Version: 1.0.0
4
+ Summary: LaTeX document compiler with MCP interface
5
+ Author-email: Aaron Bockelie <aaronsb@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/aaronsb/texflow-mcp
8
+ Project-URL: Repository, https://github.com/aaronsb/texflow-mcp
9
+ Project-URL: Issues, https://github.com/aaronsb/texflow-mcp/issues
10
+ Keywords: mcp,latex,document,typesetting,pdf
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Text Processing :: Markup :: LaTeX
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: fastmcp>=3.0.0
20
+ Requires-Dist: mistune>=3.2.0
21
+ Dynamic: license-file
22
+
23
+ # TeXFlow MCP
24
+
25
+ ![License](https://img.shields.io/github/license/aaronsb/texflow-mcp)
26
+ ![PyPI](https://img.shields.io/pypi/v/texflow-mcp)
27
+ ![Python](https://img.shields.io/pypi/pyversions/texflow-mcp)
28
+
29
+ A LaTeX document compiler with an [MCP](https://modelcontextprotocol.io) interface. AI agents operate on a structured document model — sections, paragraphs, figures, tables — while TeXFlow handles all LaTeX mechanics: packages, preamble, fonts, and compilation.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install texflow-mcp
35
+ ```
36
+
37
+ Or run without installing:
38
+
39
+ ```bash
40
+ uvx texflow-mcp
41
+ ```
42
+
43
+ ### System dependencies (optional)
44
+
45
+ TeXFlow compiles documents to PDF using XeLaTeX. Without it, you can still build and export `.tex` files.
46
+
47
+ ```bash
48
+ # Arch
49
+ pacman -S texlive-xetex texlive-fontsrecommended texlive-fontsextra
50
+
51
+ # Debian/Ubuntu
52
+ apt install texlive-xetex texlive-fonts-recommended texlive-fonts-extra
53
+
54
+ # Fedora
55
+ dnf install texlive-xetex texlive-collection-fontsrecommended texlive-collection-fontsextra
56
+ ```
57
+
58
+ For page preview (PNG), install `poppler-utils` (provides `pdftoppm`).
59
+
60
+ ## Configure with Claude Code
61
+
62
+ ```bash
63
+ claude mcp add texflow -- uvx texflow-mcp
64
+ ```
65
+
66
+ That's it. Restart Claude Code and the tools are available.
67
+
68
+ To set a workspace directory (where documents are saved):
69
+
70
+ ```bash
71
+ claude mcp add texflow -- uvx texflow-mcp ~/Documents/TeXFlow
72
+ ```
73
+
74
+ ## Configure with Claude Desktop
75
+
76
+ Add to your `claude_desktop_config.json`:
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "texflow": {
82
+ "command": "uvx",
83
+ "args": ["texflow-mcp"]
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## How it works
90
+
91
+ TeXFlow has two entry flows, both converging on the same in-memory document model:
92
+
93
+ 1. **Scaffold** — `document(action="create")` builds an empty document skeleton. Add content with `edit(action="insert")`.
94
+ 2. **Markdown ingest** — `document(action="ingest", source="paper.md")` parses markdown into the model. Refine layout from there.
95
+
96
+ The model auto-saves to disk as JSON. LaTeX is only ever an output artifact — you never edit `.tex` directly.
97
+
98
+ ## Tools
99
+
100
+ | Tool | Purpose |
101
+ |------|---------|
102
+ | `document` | Create, ingest markdown, show outline, read sections |
103
+ | `layout` | Columns, fonts, paper, margins, headers/footers, TOC |
104
+ | `edit` | Insert, replace, delete, move blocks (section, paragraph, figure, table, code, equation, list, raw) |
105
+ | `render` | Compile to PDF, preview page as PNG, export `.tex` |
106
+ | `reference` | Search LaTeX commands, symbols, packages, error help |
107
+ | `queue` | Batch multiple operations in one call |
108
+
109
+ Every response includes a workflow state hint showing where you are and what to do next.
110
+
111
+ ## Example session
112
+
113
+ ```
114
+ > document(action="create", title="My Paper", document_class="article")
115
+
116
+ > queue(operations=[
117
+ {"tool": "edit", "action": "insert", "block_type": "section", "title": "Introduction", "level": 1},
118
+ {"tool": "edit", "action": "insert", "content": "This paper explores...", "section": "Introduction"},
119
+ {"tool": "edit", "action": "insert", "block_type": "section", "title": "Methods", "level": 1},
120
+ {"tool": "layout", "font": "palatino", "columns": 2}
121
+ ])
122
+
123
+ > render(action="compile")
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ git clone https://github.com/aaronsb/texflow-mcp
130
+ cd texflow-mcp
131
+ uv sync
132
+ uv run pytest tests/ -v # 222 tests
133
+ uv run texflow # Start MCP server
134
+ ```
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,116 @@
1
+ # TeXFlow MCP
2
+
3
+ ![License](https://img.shields.io/github/license/aaronsb/texflow-mcp)
4
+ ![PyPI](https://img.shields.io/pypi/v/texflow-mcp)
5
+ ![Python](https://img.shields.io/pypi/pyversions/texflow-mcp)
6
+
7
+ A LaTeX document compiler with an [MCP](https://modelcontextprotocol.io) interface. AI agents operate on a structured document model — sections, paragraphs, figures, tables — while TeXFlow handles all LaTeX mechanics: packages, preamble, fonts, and compilation.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install texflow-mcp
13
+ ```
14
+
15
+ Or run without installing:
16
+
17
+ ```bash
18
+ uvx texflow-mcp
19
+ ```
20
+
21
+ ### System dependencies (optional)
22
+
23
+ TeXFlow compiles documents to PDF using XeLaTeX. Without it, you can still build and export `.tex` files.
24
+
25
+ ```bash
26
+ # Arch
27
+ pacman -S texlive-xetex texlive-fontsrecommended texlive-fontsextra
28
+
29
+ # Debian/Ubuntu
30
+ apt install texlive-xetex texlive-fonts-recommended texlive-fonts-extra
31
+
32
+ # Fedora
33
+ dnf install texlive-xetex texlive-collection-fontsrecommended texlive-collection-fontsextra
34
+ ```
35
+
36
+ For page preview (PNG), install `poppler-utils` (provides `pdftoppm`).
37
+
38
+ ## Configure with Claude Code
39
+
40
+ ```bash
41
+ claude mcp add texflow -- uvx texflow-mcp
42
+ ```
43
+
44
+ That's it. Restart Claude Code and the tools are available.
45
+
46
+ To set a workspace directory (where documents are saved):
47
+
48
+ ```bash
49
+ claude mcp add texflow -- uvx texflow-mcp ~/Documents/TeXFlow
50
+ ```
51
+
52
+ ## Configure with Claude Desktop
53
+
54
+ Add to your `claude_desktop_config.json`:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "texflow": {
60
+ "command": "uvx",
61
+ "args": ["texflow-mcp"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## How it works
68
+
69
+ TeXFlow has two entry flows, both converging on the same in-memory document model:
70
+
71
+ 1. **Scaffold** — `document(action="create")` builds an empty document skeleton. Add content with `edit(action="insert")`.
72
+ 2. **Markdown ingest** — `document(action="ingest", source="paper.md")` parses markdown into the model. Refine layout from there.
73
+
74
+ The model auto-saves to disk as JSON. LaTeX is only ever an output artifact — you never edit `.tex` directly.
75
+
76
+ ## Tools
77
+
78
+ | Tool | Purpose |
79
+ |------|---------|
80
+ | `document` | Create, ingest markdown, show outline, read sections |
81
+ | `layout` | Columns, fonts, paper, margins, headers/footers, TOC |
82
+ | `edit` | Insert, replace, delete, move blocks (section, paragraph, figure, table, code, equation, list, raw) |
83
+ | `render` | Compile to PDF, preview page as PNG, export `.tex` |
84
+ | `reference` | Search LaTeX commands, symbols, packages, error help |
85
+ | `queue` | Batch multiple operations in one call |
86
+
87
+ Every response includes a workflow state hint showing where you are and what to do next.
88
+
89
+ ## Example session
90
+
91
+ ```
92
+ > document(action="create", title="My Paper", document_class="article")
93
+
94
+ > queue(operations=[
95
+ {"tool": "edit", "action": "insert", "block_type": "section", "title": "Introduction", "level": 1},
96
+ {"tool": "edit", "action": "insert", "content": "This paper explores...", "section": "Introduction"},
97
+ {"tool": "edit", "action": "insert", "block_type": "section", "title": "Methods", "level": 1},
98
+ {"tool": "layout", "font": "palatino", "columns": 2}
99
+ ])
100
+
101
+ > render(action="compile")
102
+ ```
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ git clone https://github.com/aaronsb/texflow-mcp
108
+ cd texflow-mcp
109
+ uv sync
110
+ uv run pytest tests/ -v # 222 tests
111
+ uv run texflow # Start MCP server
112
+ ```
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "texflow-mcp"
3
+ version = "1.0.0"
4
+ description = "LaTeX document compiler with MCP interface"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ authors = [
9
+ {name = "Aaron Bockelie", email = "aaronsb@users.noreply.github.com"},
10
+ ]
11
+ keywords = ["mcp", "latex", "document", "typesetting", "pdf"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Topic :: Text Processing :: Markup :: LaTeX",
18
+ ]
19
+ dependencies = [
20
+ "fastmcp>=3.0.0",
21
+ "mistune>=3.2.0",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/aaronsb/texflow-mcp"
26
+ Repository = "https://github.com/aaronsb/texflow-mcp"
27
+ Issues = "https://github.com/aaronsb/texflow-mcp/issues"
28
+
29
+ [project.scripts]
30
+ texflow = "server:main"
31
+
32
+ [build-system]
33
+ requires = ["setuptools", "wheel"]
34
+ build-backend = "setuptools.build_meta"
35
+
36
+ [tool.setuptools]
37
+ packages = {find = {where = ["."], include = ["texflow*"]}}
38
+ py-modules = ["server"]
39
+ include-package-data = true
40
+
41
+ [tool.setuptools.package-data]
42
+ texflow = ["data/**/*"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "pytest>=9.0.2",
50
+ ]
@@ -0,0 +1,226 @@
1
+ """TeXFlow MCP server — LaTeX document compiler with MCP interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from fastmcp import FastMCP
9
+
10
+ from texflow.tools.state import set_output_dir
11
+ from texflow.tools.document import document_tool
12
+ from texflow.tools.layout import layout_tool
13
+ from texflow.tools.edit import edit_tool
14
+ from texflow.tools.render import render_tool
15
+ from texflow.tools.reference import reference_tool
16
+ from texflow.tools.queue import queue_tool
17
+ from texflow.tools.workflow import state_hint, error_hint, WORKFLOW_MAP
18
+
19
+ mcp = FastMCP(
20
+ "texflow",
21
+ instructions=(
22
+ "TeXFlow is a LaTeX document compiler. "
23
+ "Use 'document' to create or ingest content, 'layout' to configure typesetting, "
24
+ "'edit' to manipulate content structurally, 'render' to compile to PDF, "
25
+ "'reference' for LaTeX documentation search, and "
26
+ "'queue' to batch multiple operations in one call. "
27
+ "Every response includes a workflow state hint showing where you are and what to do next."
28
+ ),
29
+ )
30
+
31
+
32
+ def _with_hints(result: str) -> str:
33
+ """Append workflow state hint and error guidance to a tool result."""
34
+ extra = error_hint(result)
35
+ return result + extra + state_hint()
36
+
37
+
38
+ # --- Workflow resource ---
39
+
40
+ @mcp.resource("texflow://workflow")
41
+ def workflow_map() -> str:
42
+ """TeXFlow workflow map showing states and transitions."""
43
+ return WORKFLOW_MAP + "\n" + state_hint()
44
+
45
+
46
+ # --- Document tool ---
47
+
48
+ @mcp.tool()
49
+ def document(
50
+ action: str,
51
+ document_class: str | None = None,
52
+ title: str | None = None,
53
+ author: str | None = None,
54
+ source: str | None = None,
55
+ section: str | None = None,
56
+ ) -> str:
57
+ """Create, ingest, and inspect documents.
58
+
59
+ Actions:
60
+ - create: Scaffold a new empty document. Optionally set class, title, author.
61
+ - ingest: Parse markdown text or file path into the document model.
62
+ - outline: Show document structure (sections, block counts).
63
+ - read: Read content of a specific section as prose text.
64
+ """
65
+ return _with_hints(document_tool(action, document_class, title, author, source, section))
66
+
67
+
68
+ # --- Layout tool ---
69
+
70
+ @mcp.tool()
71
+ def layout(
72
+ columns: int | None = None,
73
+ font: str | None = None,
74
+ font_sans: str | None = None,
75
+ font_mono: str | None = None,
76
+ font_size: str | None = None,
77
+ paper: str | None = None,
78
+ margins: str | None = None,
79
+ header_left: str | None = None,
80
+ header_center: str | None = None,
81
+ header_right: str | None = None,
82
+ footer_left: str | None = None,
83
+ footer_center: str | None = None,
84
+ footer_right: str | None = None,
85
+ toc: bool | None = None,
86
+ lof: bool | None = None,
87
+ lot: bool | None = None,
88
+ line_spacing: float | None = None,
89
+ ) -> str:
90
+ """Configure document typesetting and layout.
91
+
92
+ Only provided parameters are changed; others are left as-is.
93
+ Returns the current full layout configuration after changes.
94
+ """
95
+ return _with_hints(layout_tool(
96
+ columns, font, font_sans, font_mono, font_size, paper, margins,
97
+ header_left, header_center, header_right,
98
+ footer_left, footer_center, footer_right,
99
+ toc, lof, lot, line_spacing,
100
+ ))
101
+
102
+
103
+ # --- Edit tool ---
104
+
105
+ @mcp.tool()
106
+ def edit(
107
+ action: str,
108
+ block_type: str | None = None,
109
+ section: str | None = None,
110
+ position: int | None = None,
111
+ content: str | None = None,
112
+ title: str | None = None,
113
+ level: int | None = None,
114
+ language: str | None = None,
115
+ path: str | None = None,
116
+ caption: str | None = None,
117
+ headers: list[str] | None = None,
118
+ rows: list[list[str]] | None = None,
119
+ target_section: str | None = None,
120
+ target_position: int | None = None,
121
+ ) -> str:
122
+ """Manipulate document content structurally.
123
+
124
+ Actions:
125
+ - insert: Add a new block at a position within a section.
126
+ - replace: Replace a block at a position with new content.
127
+ - delete: Remove a block at a position.
128
+ - move: Move a block from one location to another.
129
+
130
+ Sections are addressed by title path (e.g., 'Methods/Data Collection').
131
+ Blocks within a section are addressed by 0-based index.
132
+ Use document(action='outline') to see current structure and indices.
133
+ """
134
+ return _with_hints(edit_tool(
135
+ action, block_type, section, position, content, title, level,
136
+ language, path, caption, headers, rows, target_section, target_position,
137
+ ))
138
+
139
+
140
+ # --- Render tool ---
141
+
142
+ @mcp.tool()
143
+ def render(
144
+ action: str,
145
+ output_path: str | None = None,
146
+ page: int | None = None,
147
+ dpi: int | None = None,
148
+ ) -> str:
149
+ """Compile and export the document.
150
+
151
+ Actions:
152
+ - compile: Serialize model to .tex, compile to PDF. Returns PDF path.
153
+ - preview: Render a specific page as base64 PNG image.
154
+ - tex: Export the raw .tex source. Returns the LaTeX content.
155
+ """
156
+ return _with_hints(render_tool(action, output_path, page, dpi))
157
+
158
+
159
+ # --- Reference tool ---
160
+
161
+ @mcp.tool()
162
+ def reference(
163
+ action: str,
164
+ query: str | None = None,
165
+ description: str | None = None,
166
+ name: str | None = None,
167
+ error: str | None = None,
168
+ topic: str | None = None,
169
+ path: str | None = None,
170
+ ) -> str:
171
+ """Search LaTeX documentation, symbols, packages, and error solutions.
172
+
173
+ Actions:
174
+ - search: Search for LaTeX commands or general topics.
175
+ - symbol: Find symbols by description (e.g., "approximately equal").
176
+ - package: Get information about a LaTeX package.
177
+ - check_style: Analyze a .tex file for best practices.
178
+ - error_help: Get help for LaTeX error messages.
179
+ - example: Get working examples for a topic (table, equation, figure, list, code).
180
+ """
181
+ return _with_hints(reference_tool(action, query, description, name, error, topic, path))
182
+
183
+
184
+ # --- Queue tool ---
185
+
186
+ @mcp.tool()
187
+ def queue(
188
+ operations: list[dict],
189
+ continue_on_error: bool = False,
190
+ ) -> str:
191
+ """Execute multiple operations in a single call.
192
+
193
+ Each operation is a dict with 'tool' (document, layout, edit, render, reference)
194
+ plus the arguments for that tool. Operations run sequentially; disk is written
195
+ once at the end.
196
+
197
+ Example:
198
+ queue(operations=[
199
+ {"tool": "document", "action": "create", "title": "My Doc"},
200
+ {"tool": "edit", "action": "insert", "block_type": "section", "title": "Intro", "level": 1},
201
+ {"tool": "edit", "action": "insert", "content": "Hello world.", "section": "Intro"},
202
+ {"tool": "layout", "columns": 2, "font": "palatino"}
203
+ ])
204
+
205
+ Args:
206
+ operations: List of operation dicts.
207
+ continue_on_error: If False (default), stop on first error.
208
+ """
209
+ # Queue gets summary + state hint, no per-operation hints
210
+ result = queue_tool(operations, continue_on_error)
211
+ return result + state_hint()
212
+
213
+
214
+ def main():
215
+ """Entry point for the texflow CLI."""
216
+ # Optional workspace dir argument
217
+ if len(sys.argv) > 1:
218
+ workspace = Path(sys.argv[1])
219
+ workspace.mkdir(parents=True, exist_ok=True)
220
+ set_output_dir(workspace)
221
+
222
+ mcp.run()
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,99 @@
1
+ """Tests for the LaTeX compiler."""
2
+
3
+ import shutil
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from texflow.compiler import CompileError, CompileResult, compile_tex, preview_page, _parse_errors
10
+ from texflow.model import Document, Metadata, Paragraph, Section
11
+ from texflow.serializer import serialize
12
+
13
+ HAS_XELATEX = shutil.which("xelatex") is not None
14
+ HAS_PDFLATEX = shutil.which("pdflatex") is not None
15
+ HAS_LATEX = HAS_XELATEX or HAS_PDFLATEX
16
+ HAS_PDFTOPPM = shutil.which("pdftoppm") is not None
17
+
18
+
19
+ def _minimal_tex() -> str:
20
+ doc = Document(
21
+ metadata=Metadata(title="Test"),
22
+ content=[Section(title="Hello", level=1, content=[Paragraph(text="World")])],
23
+ )
24
+ return serialize(doc)
25
+
26
+
27
+ def test_compile_result_dataclass():
28
+ r = CompileResult(success=True, pdf_path=Path("/tmp/test.pdf"))
29
+ assert r.success is True
30
+ assert r.errors == []
31
+
32
+
33
+ def test_parse_errors_from_log():
34
+ log = """This is some log output
35
+ ! Undefined control sequence.
36
+ l.42 \\badcommand
37
+
38
+ ! Missing $ inserted.
39
+ l.55 some_text_
40
+ """
41
+ errors = _parse_errors(log)
42
+ assert len(errors) == 2
43
+ assert errors[0].message == "Undefined control sequence."
44
+ assert errors[0].line == 42
45
+ assert errors[1].message == "Missing $ inserted."
46
+ assert errors[1].line == 55
47
+
48
+
49
+ def test_parse_errors_empty_log():
50
+ assert _parse_errors("All good, no errors here.") == []
51
+
52
+
53
+ @pytest.mark.skipif(not HAS_LATEX, reason="No LaTeX engine installed")
54
+ def test_compile_minimal_document():
55
+ tex = _minimal_tex()
56
+ with tempfile.TemporaryDirectory() as tmpdir:
57
+ result = compile_tex(tex, output_dir=Path(tmpdir), filename="test")
58
+ assert result.tex_path is not None
59
+ assert result.tex_path.exists()
60
+ if result.success:
61
+ assert result.pdf_path is not None
62
+ assert result.pdf_path.exists()
63
+ assert result.pdf_path.stat().st_size > 0
64
+
65
+
66
+ @pytest.mark.skipif(not HAS_LATEX, reason="No LaTeX engine installed")
67
+ def test_compile_with_errors():
68
+ bad_tex = "\\documentclass{article}\n\\begin{document}\n\\badcommand\n\\end{document}"
69
+ with tempfile.TemporaryDirectory() as tmpdir:
70
+ result = compile_tex(bad_tex, output_dir=Path(tmpdir), filename="bad")
71
+ assert result.tex_path is not None
72
+ assert len(result.errors) > 0
73
+
74
+
75
+ @pytest.mark.skipif(not (HAS_LATEX and HAS_PDFTOPPM), reason="Requires LaTeX + pdftoppm")
76
+ def test_preview_page():
77
+ tex = _minimal_tex()
78
+ with tempfile.TemporaryDirectory() as tmpdir:
79
+ result = compile_tex(tex, output_dir=Path(tmpdir))
80
+ if result.success and result.pdf_path:
81
+ b64 = preview_page(result.pdf_path, page=1, dpi=72)
82
+ assert b64 is not None
83
+ assert len(b64) > 100 # Should be a non-trivial PNG
84
+
85
+
86
+ def test_preview_nonexistent_file():
87
+ result = preview_page(Path("/tmp/nonexistent.pdf"))
88
+ assert result is None
89
+
90
+
91
+ @pytest.mark.skipif(HAS_LATEX, reason="Only test when no LaTeX engine available")
92
+ def test_compile_without_engine():
93
+ tex = _minimal_tex()
94
+ with tempfile.TemporaryDirectory() as tmpdir:
95
+ result = compile_tex(tex, output_dir=Path(tmpdir))
96
+ assert result.success is False
97
+ assert result.tex_path is not None
98
+ assert result.tex_path.exists()
99
+ assert any("No LaTeX engine" in e.message for e in result.errors)