Skip to content

Commit 83291cb

Browse files
feat: academic rendering design and zero-config bibliography (v0.1.2)
1 parent c6c7431 commit 83291cb

7 files changed

Lines changed: 298 additions & 41 deletions

File tree

.gitignore

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,204 @@ cython_debug/
204204
.cursorignore
205205
.cursorindexingignore
206206

207+
# Distribution / packaging
208+
.Python
209+
build/
210+
develop-eggs/
211+
dist/
212+
downloads/
213+
eggs/
214+
.eggs/
215+
lib/
216+
lib64/
217+
parts/
218+
sdist/
219+
var/
220+
wheels/
221+
share/python-wheels/
222+
*.egg-info/
223+
.installed.cfg
224+
*.egg
225+
MANIFEST
226+
227+
# PyInstaller
228+
# Usually these files are written by a python script from a template
229+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
230+
*.manifest
231+
*.spec
232+
233+
# Installer logs
234+
pip-log.txt
235+
pip-delete-this-directory.txt
236+
237+
# Unit test / coverage reports
238+
htmlcov/
239+
.tox/
240+
.nox/
241+
.coverage
242+
.coverage.*
243+
.cache
244+
nosetests.xml
245+
coverage.xml
246+
*.cover
247+
*.py.cover
248+
.hypothesis/
249+
.pytest_cache/
250+
cover/
251+
252+
# Translations
253+
*.mo
254+
*.pot
255+
256+
# Django stuff:
257+
*.log
258+
local_settings.py
259+
db.sqlite3
260+
db.sqlite3-journal
261+
262+
# Flask stuff:
263+
instance/
264+
.webassets-cache
265+
266+
# Scrapy stuff:
267+
.scrapy
268+
269+
# Sphinx documentation
270+
docs/_build/
271+
272+
# PyBuilder
273+
.pybuilder/
274+
target/
275+
276+
# Jupyter Notebook
277+
.ipynb_checkpoints
278+
279+
# IPython
280+
profile_default/
281+
ipython_config.py
282+
283+
# pyenv
284+
# For a library or package, you might want to ignore these files since the code is
285+
# intended to run in multiple environments; otherwise, check them in:
286+
# .python-version
287+
288+
# pipenv
289+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
290+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
291+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
292+
# install all needed dependencies.
293+
#Pipfile.lock
294+
295+
# UV
296+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
297+
# This is especially recommended for binary packages to ensure reproducibility, and is more
298+
# commonly ignored for libraries.
299+
#uv.lock
300+
301+
# poetry
302+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
303+
# This is especially recommended for binary packages to ensure reproducibility, and is more
304+
# commonly ignored for libraries.
305+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
306+
#poetry.lock
307+
#poetry.toml
308+
309+
# pdm
310+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
311+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
312+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
313+
#pdm.lock
314+
#pdm.toml
315+
.pdm-python
316+
.pdm-build/
317+
318+
# pixi
319+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
320+
#pixi.lock
321+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
322+
# in the .venv directory. It is recommended not to include this directory in version control.
323+
.pixi
324+
325+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
326+
__pypackages__/
327+
328+
# Celery stuff
329+
celerybeat-schedule
330+
celerybeat.pid
331+
332+
# SageMath parsed files
333+
*.sage.py
334+
335+
# Environments
336+
.env
337+
.envrc
338+
.venv
339+
env/
340+
venv/
341+
ENV/
342+
env.bak/
343+
venv.bak/
344+
345+
# Spyder project settings
346+
.spyderproject
347+
.spyproject
348+
349+
# Rope project settings
350+
.ropeproject
351+
352+
# mkdocs documentation
353+
/site
354+
355+
# mypy
356+
.mypy_cache/
357+
.dmypy.json
358+
dmypy.json
359+
360+
# Pyre type checker
361+
.pyre/
362+
363+
# pytype static type analyzer
364+
.pytype/
365+
366+
# Cython debug symbols
367+
cython_debug/
368+
369+
# PyCharm
370+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
371+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
372+
# and can be added to the global gitignore or merged into this file. For a more nuclear
373+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
374+
#.idea/
375+
376+
# Abstra
377+
# Abstra is an AI-powered process automation framework.
378+
# Ignore directories containing user credentials, local state, and settings.
379+
# Learn more at https://abstra.io/docs
380+
.abstra/
381+
382+
# Visual Studio Code
383+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
384+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
385+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
386+
# you could uncomment the following to ignore the entire vscode folder
387+
# .vscode/
388+
389+
# Ruff stuff:
390+
.ruff_cache/
391+
392+
# PyPI configuration file
393+
.pypirc
394+
395+
# Cursor
396+
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
397+
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
398+
# refer to https://docs.cursor.com/context/ignore-files
399+
.cursorignore
400+
.cursorindexingignore
401+
207402
# Marimo
208403
marimo/_static/
209404
marimo/_lsp/
210405
__marimo__/
406+
407+
refs.bib

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ That's it. Zero configuration required.
5151

5252
---
5353

54+
## 🎓 Academic Features (v0.1.2+)
55+
56+
`doc-engine-cli` ships with a premium scientific layout (using Linux Libertine and Inter font-families) and **Zero-Config Bibliography** handling.
57+
58+
To add an IEEE-styled bibliography to your PDF:
59+
1. Create a `refs.bib`, `references.bib`, or `bibliography.bib` file in your repository.
60+
2. In your `README.md`, cite using standard syntax: `[@citation-key]`.
61+
62+
When you run `doc-engine build`, the CLI will automatically detect your `.bib` file, securely bind it to the sandbox, and inject a formatted References page at the end of the document.
63+
64+
---
65+
5466
## Quick Start
5567

5668
### Installation

doc_engine/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
__version__ = "0.1.1"
1+
"""doc-engine-cli: Modern Markdown to Typst compiler."""
2+
__version__ = "0.1.2"

doc_engine/cli.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
console = Console()
1515

1616
_README_CANDIDATES = ("README.md", "readme.md", "Readme.md", "README.MD")
17+
_BIB_CANDIDATES = ("refs.bib", "references.bib", "bibliography.bib")
1718

1819

1920
def _detect_git_user() -> str:
@@ -30,8 +31,8 @@ def _detect_git_user() -> str:
3031
return "Anonymous"
3132

3233

33-
def _find_readme(directory: Path) -> Path | None:
34-
for name in _README_CANDIDATES:
34+
def _find_file(directory: Path, candidates: tuple[str, ...]) -> Path | None:
35+
for name in candidates:
3536
candidate = directory / name
3637
if candidate.exists():
3738
return candidate
@@ -51,12 +52,14 @@ def cli(ctx: click.Context) -> None:
5152
@click.option("-o", "--output", default=None, help="Output PDF file path.")
5253
@click.option("-t", "--title", default=None, help="Document title override.")
5354
@click.option("-a", "--author", default=None, help="Author name override.")
55+
@click.option("--bib", default=None, help="Path to custom .bib file.")
5456
@click.option("--open", "open_pdf", is_flag=True, help="Open PDF after generation.")
5557
def build(
5658
input_file: str | None,
5759
output: str | None,
5860
title: str | None,
5961
author: str | None,
62+
bib: str | None,
6063
open_pdf: bool,
6164
) -> None:
6265
console.print(
@@ -67,10 +70,12 @@ def build(
6770
)
6871
)
6972

73+
cwd = Path.cwd()
74+
7075
if input_file:
7176
input_path = Path(input_file)
7277
else:
73-
input_path = _find_readme(Path.cwd())
78+
input_path = _find_file(cwd, _README_CANDIDATES)
7479
if not input_path:
7580
console.print(
7681
"[bold red]Error:[/bold red] No README.md found in current directory.\n"
@@ -83,15 +88,27 @@ def build(
8388
console.print(f"[bold red]Error:[/bold red] File not found — {input_path}")
8489
raise SystemExit(1)
8590

91+
# Detect Bibliography
92+
resolved_bib = None
93+
if bib:
94+
resolved_bib = Path(bib)
95+
if not resolved_bib.exists():
96+
console.print(f"[bold yellow]Warning:[/bold yellow] Bibliography file not found — {bib}")
97+
resolved_bib = None
98+
else:
99+
resolved_bib = _find_file(cwd, _BIB_CANDIDATES)
100+
if resolved_bib:
101+
console.print(f" [dim]Auto-detected bib:[/dim] [cyan]{resolved_bib.name}[/cyan]")
102+
86103
markdown_content = input_path.read_text(encoding="utf-8")
87104

88105
resolved_title = title or extract_title(markdown_content)
89106
resolved_author = author or _detect_git_user()
90107
resolved_output = output or f"{input_path.stem}_doc.pdf"
91108

92-
console.print(f" [dim]Title:[/dim] [white]{resolved_title}[/white]")
93-
console.print(f" [dim]Author:[/dim] [white]{resolved_author}[/white]")
94-
console.print(f" [dim]Output:[/dim] [cyan]{resolved_output}[/cyan]")
109+
console.print(f" [dim]Title:[/dim] [white]{resolved_title}[/white]")
110+
console.print(f" [dim]Author:[/dim] [white]{resolved_author}[/white]")
111+
console.print(f" [dim]Output:[/dim] [cyan]{resolved_output}[/cyan]")
95112
console.print()
96113

97114
with console.status("[bold blue]Converting Markdown → Typst…[/bold blue]"):
@@ -105,6 +122,7 @@ def build(
105122
title=resolved_title,
106123
author=resolved_author,
107124
output_path=resolved_output,
125+
bib_file=str(resolved_bib.resolve()) if resolved_bib else None,
108126
)
109127
except Exception as exc:
110128
console.print(f"\n[bold red]Compilation failed:[/bold red] {exc}")

doc_engine/compiler.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import shutil
12
import tempfile
23
from pathlib import Path
34

@@ -11,28 +12,38 @@ def compile_pdf(
1112
title: str,
1213
author: str,
1314
output_path: str,
15+
bib_file: str | None = None,
1416
) -> None:
15-
template_src = _TEMPLATE_PATH.read_text(encoding="utf-8")
16-
main_src = _build_main(typst_body, title, author)
1717
resolved_output = str(Path(output_path).resolve())
1818

1919
with tempfile.TemporaryDirectory() as tmpdir:
2020
tmp = Path(tmpdir)
21-
(tmp / "report.typ").write_text(template_src, encoding="utf-8")
21+
(tmp / "report.typ").write_text(_TEMPLATE_PATH.read_text(encoding="utf-8"), encoding="utf-8")
22+
2223
main_file = tmp / "main.typ"
24+
bib_inject = "none"
25+
26+
if bib_file:
27+
bib_path = Path(bib_file)
28+
if bib_path.exists():
29+
shutil.copy(bib_path, tmp / bib_path.name)
30+
bib_inject = f'"{bib_path.name}"'
31+
32+
main_src = _build_main(typst_body, title, author, bib_inject)
2333
main_file.write_text(main_src, encoding="utf-8")
2434

2535
typst.compile(str(main_file), output=resolved_output)
2636

2737

28-
def _build_main(body: str, title: str, author: str) -> str:
38+
def _build_main(body: str, title: str, author: str, bib_inject: str) -> str:
2939
safe_title = title.replace('"', '\\"')
3040
safe_author = author.replace('"', '\\"')
3141
return (
3242
'#import "report.typ": setup_doc\n\n'
3343
"#show: setup_doc.with(\n"
3444
f' title: "{safe_title}",\n'
3545
f' author: "{safe_author}",\n'
46+
f" bibliography_file: {bib_inject},\n"
3647
")\n\n"
3748
f"{body}"
3849
)

doc_engine/converter.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ def convert(markdown: str) -> str:
184184
renderer=renderer,
185185
plugins=["table", "strikethrough"],
186186
)
187-
return md(markdown)
187+
typst_str = md(markdown)
188+
# Translate Pandoc syntax [@citation_key] back to native Typst @citation_key
189+
typst_str = re.sub(r'\[\\@([a-zA-Z0-9_\-]+)\]', r'@\1', typst_str)
190+
return typst_str
188191

189192

190193
def extract_title(markdown: str) -> str:

0 commit comments

Comments
 (0)