|
| 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! |
0 commit comments