Skip to content

Commit d1271f7

Browse files
committed
feat: add math expression support with LaTeX → Unicode fallback
- Add pymdownx.arithmatex extension for parsing $...$ and \(...\) math - Auto-inject MathJax 3 loader when math expressions detected in output - Add _normalize_split_math_expressions to merge split math like $4.99 \rightarrow $14.99 into single valid expression - Add _replace_common_latex_commands_in_text as server-side fallback: replaces \rightarrow → →, \times → ×, \text{M} → M, \$ → $, etc. so math renders correctly in environments where JS is blocked - Fix: tighten merge regex to avoid swallowing *italic* text after a standalone $\cmd$ expression - Add HTML_HANDLING.md with integration guide for Django/FastAPI - Update README with math support and HTML output sections - Add pymdown-extensions>=10.16.1 dependency
1 parent 0cd13f1 commit d1271f7

5 files changed

Lines changed: 553 additions & 4 deletions

File tree

HTML_HANDLING.md

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
# Hướng dẫn xử lý HTML output từ md2html-tailwind4
2+
3+
## 1. Tổng quan
4+
5+
`Converter` sinh ra HTML hoàn chỉnh với:
6+
- **Tailwind CSS 4** classes (không cần build step)
7+
- **MathJax 3** auto-injected khi có công thức toán
8+
- **Dark mode support** via `dark:` variants
9+
- **Responsive design** cho tất cả elements
10+
11+
## 2. Sử dụng HTML output
12+
13+
### 2.1 HTML có sẵn MathJax
14+
15+
HTML được sinh từ converter **đã bao gồm** MathJax loader:
16+
17+
```html
18+
<!-- Tự động chèn vào cuối nếu có toán học -->
19+
<script>
20+
window.MathJax = window.MathJax || {
21+
tex: {inlineMath: [['\\(', '\\)'], ['$', '$']]},
22+
svg: {fontCache: 'global'}
23+
};
24+
</script>
25+
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
26+
```
27+
28+
**Khi nào được inject:**
29+
- Nếu markdown chứa `$...$` hay `\(...\)` → MathJax được inject
30+
- Nếu không có toán học → MathJax không được thêm (tiết kiệm bandwidth)
31+
32+
### 2.2 Cách sử dụng với Django/FastAPI
33+
34+
```python
35+
from md2html_tailwind4 import Converter
36+
37+
# Trong view/handler
38+
converter = Converter(font_size='base')
39+
html = converter.convert_md_to_html(markdown_content)
40+
41+
# Truyền vào template
42+
return render(request, 'template.html', {'content': html | safe})
43+
```
44+
45+
**Template (Django):**
46+
```html
47+
{% extends "base.html" %}
48+
{% block content %}
49+
<div class="bg-white dark:bg-neutral-900 p-4">
50+
{{ content | safe }}
51+
</div>
52+
{% endblock %}
53+
```
54+
55+
## 3. Công thức toán học (Math expressions)
56+
57+
### 3.1 Format hỗ trợ
58+
59+
| Format | Ví dụ | Kết quả |
60+
|--------|-------|--------|
61+
| Inline $ | `$\rightarrow$` ||
62+
| Inline \(\) | `\(\rightarrow\)` ||
63+
| Display $$ | `$$\sum x$$` | ∑ x (trên dòng riêng) |
64+
| Display \[\] | `\[E=mc^2\]` | E=mc² (trên dòng riêng) |
65+
66+
### 3.2 Pattern hỗ trợ đặc biệt
67+
68+
Converter tự động gom các math fragment thành 1 expression:
69+
70+
```markdown
71+
# Input
72+
Price: $4.99 \rightarrow $14.99
73+
74+
# Converter xử lý thành
75+
Price: $4.99 \rightarrow 14.99$
76+
77+
# HTML output
78+
Price: <span class="arithmatex">\(4.99 \rightarrow 14.99\)</span>
79+
```
80+
81+
**Hỗ trợ patterns:**
82+
- `$num1 \cmd $num2``$num1 \cmd num2$`
83+
- `$num1$ \cmd $num2$``$num1 \cmd num2$`
84+
- `$num1 $num2` → No merge (must have LaTeX command)
85+
86+
### 3.3 Best practices
87+
88+
**Tốt:**
89+
```markdown
90+
- Tiền tệ: $4.99 \rightarrow $14.99
91+
- Công thức: $E=mc^2$
92+
- Khoa học: $\alpha + \beta = \gamma$
93+
```
94+
95+
**Tránh:**
96+
```markdown
97+
- Tiền tệ viết liên tục: $4.99$14.99 (không parse)
98+
- Mix ké: $4.99 text $text (confusing)
99+
- Không escape khi cần: $abc$def (sẽ parse $abc$ riêng)
100+
```
101+
102+
## 4. CSS & Tailwind classes
103+
104+
### 4.1 Các class được áp dụng tự động
105+
106+
**Headings:**
107+
```
108+
h1: text-2xl sm:text-3xl lg:text-4xl font-bold
109+
h2: text-xl sm:text-2xl lg:text-3xl font-bold
110+
h3: text-lg sm:text-xl lg:text-2xl font-semibold
111+
```
112+
113+
**Paragraphs:**
114+
```
115+
text-base leading-7 text-justify text-neutral-800 dark:text-neutral-200
116+
```
117+
118+
**Code blocks:**
119+
```
120+
rounded-xl border bg-neutral-950 p-4 dark:border-neutral-700
121+
```
122+
123+
**Admonitions:**
124+
```
125+
note/info: border-blue-400 bg-blue-50
126+
warning: border-amber-400 bg-amber-50
127+
danger/error: border-red-400 bg-red-50
128+
```
129+
130+
### 4.2 Font size scales
131+
132+
```python
133+
converter = Converter(font_size='sm') # Nhỏ
134+
converter = Converter(font_size='base') # Mặc định
135+
converter = Converter(font_size='lg') # Lớn
136+
```
137+
138+
## 5. Dark mode
139+
140+
HTML được sinh **tự động hỗ trợ dark mode**. Để kích hoạt:
141+
142+
```html
143+
<!-- HTML đã có dark: classes -->
144+
<!-- Kích hoạt bằng JS: -->
145+
<script>
146+
document.documentElement.classList.add('dark');
147+
// Hoặc toggle
148+
document.documentElement.classList.toggle('dark');
149+
</script>
150+
151+
<!-- Hoặc CSS -->
152+
@media (prefers-color-scheme: dark) {
153+
html { @apply dark; }
154+
}
155+
```
156+
157+
## 6. Tích hợp với frameworks
158+
159+
### 6.1 Django
160+
161+
```python
162+
# settings.py
163+
TEMPLATES = [
164+
{
165+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
166+
'OPTIONS': {
167+
'builtins': ['django.template.defaulttags'],
168+
},
169+
},
170+
]
171+
172+
# views.py
173+
from md2html_tailwind4 import Converter
174+
from django.utils.safestring import mark_safe
175+
176+
def article_view(request, slug):
177+
article = Article.objects.get(slug=slug)
178+
converter = Converter()
179+
html_content = converter.convert_md_to_html(article.markdown_text)
180+
return render(request, 'article.html', {
181+
'content': mark_safe(html_content)
182+
})
183+
184+
# template.html
185+
<div class="prose prose-sm md:prose-base max-w-4xl">
186+
{{ content }}
187+
</div>
188+
```
189+
190+
### 6.2 FastAPI
191+
192+
```python
193+
from fastapi import FastAPI, Response
194+
from md2html_tailwind4 import Converter
195+
196+
app = FastAPI()
197+
198+
@app.get("/article/{article_id}")
199+
def get_article(article_id: int):
200+
# Fetch article from DB
201+
converter = Converter()
202+
html = converter.convert_md_to_html(article.content)
203+
return Response(content=html, media_type="text/html")
204+
```
205+
206+
### 6.3 Static site generators
207+
208+
```python
209+
# build.py
210+
from pathlib import Path
211+
from md2html_tailwind4 import Converter
212+
213+
converter = Converter(font_size='base')
214+
md_dir = Path('content')
215+
html_dir = Path('output')
216+
217+
for md_file in md_dir.glob('*.md'):
218+
with open(md_file) as f:
219+
content = f.read()
220+
221+
html = converter.convert_md_to_html(content)
222+
223+
output_file = html_dir / md_file.stem / 'index.html'
224+
output_file.parent.mkdir(exist_ok=True, parents=True)
225+
output_file.write_text(html)
226+
```
227+
228+
## 7. Performance & optimization
229+
230+
### 7.1 MathJax loading
231+
232+
- **MathJax được inject từ CDN** (618 kB gzipped)
233+
- **Auto-inject chỉ khi cần**: Không có toán → không load MathJax
234+
- **Async loading**: `<script async>` không block page render
235+
236+
### 7.2 Caching
237+
238+
```python
239+
from functools import lru_cache
240+
241+
@lru_cache(maxsize=1000)
242+
def cached_convert(markdown_text):
243+
converter = Converter()
244+
return converter.convert_md_to_html(markdown_text)
245+
```
246+
247+
### 7.3 Production notes
248+
249+
- HTML đã bao gồm Tailwind classes → **không cần JIT compiler**
250+
- MathJax từ CDN → đảm bảo **CORS allowed** trên production
251+
- Django/FastAPI xử lý xây dựng HTML nhanh → **no build step**
252+
253+
## 8. Xử lý lỗi & edge cases
254+
255+
### 8.1 Django template delimiters
256+
257+
```markdown
258+
# Input
259+
{% if condition %}...{% endif %}
260+
261+
# Converter tự động escape
262+
{% templatetag openblock %} if condition {% templatetag closeblock %}...
263+
```
264+
265+
**Lý do:** Tránh Django parse lại khi render
266+
267+
### 8.2 External links
268+
269+
```markdown
270+
# Input
271+
[Link](https://example.com)
272+
273+
# Output
274+
<a href="..." target="_blank" rel="noopener noreferrer">Link</a>
275+
```
276+
277+
**Tự động thêm:**
278+
- `target="_blank"` (mở tab mới)
279+
- `rel="noopener noreferrer"` (security)
280+
281+
### 8.3 Tables
282+
283+
**Audio tables** (3 cột, cột 3 = media URL):
284+
```
285+
| Tên | URL | Audio |
286+
|-----|-----|-------|
287+
| Song 1 | /path/song.mp3 | [play] |
288+
```
289+
→ Chuyển thành interactive audio player
290+
291+
**Bảng thường:**
292+
```
293+
| Col1 | Col2 |
294+
|------|------|
295+
| Data | Data |
296+
```
297+
→ Responsive table với scroll trên mobile
298+
299+
## 9. Changelog toán học
300+
301+
- **v1.4.2+**: Thêm pymdownx.arithmatex để parse `$...$` expressions
302+
- **v1.4.3+**: Auto-inject MathJax 3 khi phát hiện toán học
303+
- **v1.5.0+**: Smart merge của math fragments (e.g., `$4.99 \rightarrow $14.99`)
304+
305+
## 10. Troubleshooting
306+
307+
### Toán học không hiển thị
308+
309+
**Nguyên nhân 1: CSP (Content Security Policy)**
310+
```html
311+
<!-- Header cần allow CDN -->
312+
<meta http-equiv="Content-Security-Policy"
313+
content="script-src 'unsafe-inline' https://cdn.jsdelivr.net">
314+
```
315+
316+
**Nguyên nhân 2: Format không đúng**
317+
```markdown
318+
# ❌ Lỗi
319+
$4.99 \rightarrow $14.99
320+
→ Phải có LaTeX command giữa hai $
321+
322+
# ✅ Đúng
323+
$4.99 \rightarrow 14.99$
324+
```
325+
326+
**Nguyên nhân 3: JS không chạy**
327+
```python
328+
# Đảm bảo output được mark as safe
329+
from django.utils.safestring import mark_safe
330+
html = mark_safe(converter.convert_md_to_html(content))
331+
```
332+
333+
### HTML không style
334+
335+
- Đảm bảo Tailwind CSS 4 được load trên page
336+
- HTML đã bao gồm class names, không phải inline styles
337+
- Framework phải compile Tailwind hoặc dùng CDN
338+
339+
### Performance chậm
340+
341+
- Kiểm tra MathJax loading time (DevTools → Network)
342+
- Disable MathJax nếu không cần toán: tự remove `<script>` tags
343+
- Dùng `font_size='sm'` cho mobile
344+
345+
---
346+
347+
**Hỏi thêm?** Tham khảo README.md hoặc file này!

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Designed for projects that render HTML inside a Tailwind-powered frontend (Djang
1212
- Definition lists (`dl`/`dt`/`dd`)
1313
- Footnotes (`[^1]` syntax)
1414
- Admonition callout boxes (`!!! note`, `!!! warning`, `!!! danger`, etc.) with color-coded Tailwind styling
15+
- Inline/block math parsing via `pymdownx.arithmatex` (e.g. `$\\rightarrow$`) with automatic MathJax loader injection
1516
- Heading anchor IDs auto-generated via `toc` extension
1617
- List items immediately following a paragraph (no blank line) render correctly
1718
- Responsive tables with overflow scrolling
@@ -57,6 +58,32 @@ md2html input.md output.html --font-size sm
5758
| `base` *(default)* | `text-2xl``text-4xl` | `text-base` | `text-sm` |
5859
| `lg` | `text-3xl``text-5xl` | `text-lg` | `text-base` |
5960

61+
## HTML Output & Integration
62+
63+
The generated HTML is **production-ready** with:
64+
65+
- **Tailwind CSS 4** classes already applied (no build step needed)
66+
- **MathJax 3** automatically injected when math expressions detected
67+
- **Dark mode support** via `dark:` variants
68+
- **Responsive design** for all elements
69+
- **Security hardening** (Django template escaping, external link attributes)
70+
71+
See [HTML_HANDLING.md](HTML_HANDLING.md) for detailed integration guides with Django, FastAPI, and more.
72+
73+
## Math Support
74+
75+
Format math expressions with `$...$` or `\(...\)`:
76+
77+
```markdown
78+
Pricing: $4.99 \rightarrow $14.99
79+
Physics: $E=mc^2$
80+
Formula: \[\sum_{i=1}^{n} x_i\]
81+
```
82+
83+
Smart normalization handles split expressions:
84+
- `$4.99 \rightarrow $14.99``$4.99 \rightarrow 14.99$`
85+
- MathJax loaded automatically when needed
86+
6087
## Requirements
6188

6289
- Python >= 3.11

0 commit comments

Comments
 (0)