Finite-State Markdown Engine - O(N) single-pass Markdown to HTML converter using pure FST
Project description
FSTMD - Finite-State Markdown Engine
A pure Finite State Transducer (FST) Markdown to HTML converter for Python 3.13+.
Features
- โก O(N) Single-Pass Processing - No backtracking, no regex, no AST
- ๐ Security First - XSS prevention with proper HTML escaping
- ๐ฏ Zero Dependencies - Pure Python, no external packages required
- ๐ Type Safe - Full type annotations, mypy strict compatible
- ๐ Python 3.13+ Optimized - Uses latest Python features
Installation
pip install fstmd
Or install from source:
git clone https://github.com/fstmd/fstmd.git
cd fstmd
pip install -e .
Quick Start
from fstmd import Markdown
# Create a parser (safe mode by default)
md = Markdown(mode="safe")
# Render Markdown to HTML
html = md.render("**Hello** *world*")
print(html)
# Output: <p><strong>Hello</strong> <em>world</em></p>
Supported Markdown Features
| Feature | Syntax | Output |
|---|---|---|
| Bold | **text** |
<strong>text</strong> |
| Italic | *text* |
<em>text</em> |
| Bold+Italic | ***text*** |
<strong><em>text</em></strong> |
| Headings | # H1 to ###### H6 |
<h1> to <h6> |
| Unordered Lists | - item |
<ul><li>item</li></ul> |
| Paragraphs | Blank line separated | <p>...</p> |
API Reference
Markdown Class
from fstmd import Markdown
# Safe mode (default) - escapes all HTML
md = Markdown(mode="safe")
# Raw mode - passes through HTML (use with trusted input only!)
md = Markdown(mode="raw")
# Strict mode - raises exceptions on security issues
md = Markdown(mode="safe", strict=True)
# Render markdown
html = md.render("# Hello **World**")
# Always render safely, regardless of instance mode
safe_html = md.render_safe("<script>alert(1)</script>")
Convenience Functions
from fstmd.parser import render, render_unsafe
# Quick render with caching
html = render("**bold**")
# Render without escaping (dangerous!)
html = render_unsafe("<b>raw html</b>")
Security
FSTMD is designed with security as a primary concern:
Safe Mode (Default)
All HTML special characters are escaped:
<โ<>โ>&โ&"โ"'โ'
md = Markdown(mode="safe")
result = md.render("<script>alert('xss')</script>")
# Output: <p><script>alert('xss')</script></p>
Dangerous Pattern Detection
The library detects and can reject:
<script>tagsjavascript:URLsvbscript:URLsdata:URLs (except safe image types)
Architecture
Finite State Transducer Design
FSTMD uses a Mealy Machine (FST) where output is generated during state transitions.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INLINE FST STATES โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโ '*' โโโโโโโโโโโโ '*' โโโโโโโโโโโโ โ
โ โ TEXT โโโโโโโโโโโโโบโ STAR_ONE โโโโโโโโโโโโบโ STAR_TWO โ โ
โ โโโโโฌโโโโ โโโโโโฌโโโโโโ โโโโโโฌโโโโโโ โ
โ โ โ โ โ
โ โ other โ other โ other โ
โ โผ โผ โผ โ
โ output char start italic start bold โ
โ โ
โ โโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
โ โ IN_ITALIC โโโโโโโโโโ STAR_ONE โ (from TEXT with '*') โ
โ โโโโโโโฌโโโโโโ โโโโโโโโโโโโโโ โ
โ โ '*' โ
โ โผ โ
โ โโโโโโโโโโโโโโโโ โ
โ โ close italic โโโโโโบ output </em>, goto TEXT โ
โ โโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โ
โ โ IN_BOLD โโโโโโโโโโโ STAR_TWO โ (from STAR_ONE) โ
โ โโโโโโฌโโโโโโ โโโโโโโโโโโโโโโโโ โ
โ โ '**' โ
โ โผ โ
โ โโโโโโโโโโโโโโโ โ
โ โ close bold โโโโโโโโโโบ output </strong> โ
โ โโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Block-Level FST
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ BLOCK FST STATES โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโ โ
โ โ START โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโฌโโโโโโ โ โ
โ โ โ โ
โ โโโโโดโโโโ '#' โโโโโโโโโโโโ โ โ
โ โ LINE โโโโโโโโโโโโบโ HEADING โโโโบ count #'s, emit <hN> โ โ
โ โ START โ โโโโโโโโโโโโ โ โ
โ โโโโโฌโโโโ โ โ
โ โ โ โ
โ โ '-' โโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโบโ LIST_ITEM โโโโบ emit <li> โ โ
โ โ โโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ '\n' โโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโบโ BLANK_LINE โโโโบ close paragraph โ โ
โ โ โโโโโโโโโโโโโโโโโ โ โ
โ โ โ โ
โ โ other โโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโบโ PARAGRAPH โโโโบ emit <p> โ โ
โ โโโโโโโโโโโโโโโโโ โ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Design Decisions
- Two-Character Lookahead Maximum - Disambiguates
*vs**vs*** - No Backtracking - All decisions are final
- Output During Transitions - Mealy machine produces output as it processes
- Immutable State Definitions - States are enums, transitions are cached
Performance
Complexity Guarantees
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Parse | O(N) | O(N) |
| Per character | O(1) | O(1) |
Benchmarks
Tested on Python 3.13 with a medium-sized document (~500 chars):
| Library | Avg Time (ms) | Throughput | Relative |
|---|---|---|---|
| FSTMD | 0.05 | 10M chars/sec | 1.0x |
| markdown-it-py | 0.15 | 3.3M chars/sec | 0.33x |
| Python-Markdown | 0.30 | 1.7M chars/sec | 0.17x |
| CommonMark-Py | 0.35 | 1.4M chars/sec | 0.14x |
Note: Benchmarks vary by hardware and document structure.
Run benchmarks yourself:
from fstmd.benchmarks import run_benchmarks, print_benchmark_results
results = run_benchmarks()
print_benchmark_results(results)
Limitations
FSTMD focuses on speed and simplicity. It does not support:
- Code blocks (fenced or indented)
- Block quotes
- Ordered lists
- Links and images
- Tables
- Footnotes
- HTML pass-through in safe mode
For full CommonMark compliance, use markdown-it-py.
Development
Setup
# Clone the repository
git clone https://github.com/fstmd/fstmd.git
cd fstmd
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Type checking
mypy fstmd
# Linting
ruff check fstmd
Project Structure
fstmd/
โโโ __init__.py # Package exports
โโโ __main__.py # CLI entry point
โโโ parser.py # High-level Markdown class
โโโ exceptions.py # Custom exceptions
โโโ core/
โ โโโ __init__.py
โ โโโ fsm.py # Main FST engine
โ โโโ states.py # State definitions
โ โโโ transitions.py # Transition table
โ โโโ safe_html.py # HTML escaping
โโโ benchmarks/
โ โโโ __init__.py
โ โโโ runner.py # Benchmark utilities
โโโ tests/
โโโ conftest.py
โโโ test_inline.py
โโโ test_blocks.py
โโโ test_security.py
โโโ test_fsm.py
โโโ test_integration.py
Building and Publishing
Build
# Install build tools
pip install build twine
# Build distribution
python -m build
# Check the build
twine check dist/*
Publish to PyPI
# Upload to TestPyPI first
twine upload --repository testpypi dist/*
# Upload to PyPI
twine upload dist/*
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass (
pytest) - Ensure type checking passes (
mypy fstmd) - Ensure linting passes (
ruff check fstmd) - Submit a pull request
License
MIT License - see LICENSE file.
Acknowledgments
Inspired by:
- markdown-it - Architecture insights
- peg-markdown - PEG-based parsing ideas
- Automata theory and FST research
Made with โค๏ธ for fast, secure Markdown parsing.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fstmd-1.0.0.tar.gz.
File metadata
- Download URL: fstmd-1.0.0.tar.gz
- Upload date:
- Size: 29.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6c7fdb66dfdd36686084c7f37b80d729a4543f91a4f6f4fc277660227b0df7c
|
|
| MD5 |
b196420d7366925530128841637dcaf0
|
|
| BLAKE2b-256 |
39a54c4f86ef9ce0b1c710f7a33d3fabf2a163c1a86a2503a09aed2555a951f7
|
File details
Details for the file fstmd-1.0.0-py3-none-any.whl.
File metadata
- Download URL: fstmd-1.0.0-py3-none-any.whl
- Upload date:
- Size: 23.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1d3b27feb64afada8ef3cc93370c499792c1c2ed755030838a55e5b14c73901
|
|
| MD5 |
c8dffd46521f1e1af5de790d9e4cff47
|
|
| BLAKE2b-256 |
23904bd339cbdf9c7ee4c15841f81be2a6a43cb7117e0dc299ad3ee943d27681
|