torm 0.3.0

A Tokio-based async ORM for Rust with GORM-like API, supporting SQLite, MySQL and PostgreSQL via native wire protocols
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TORM · Tokio Async ORM for Rust</title>
<meta name="description" content="A Tokio-based async ORM for Rust with GORM-like API, supporting SQLite, MySQL and PostgreSQL via native wire protocols.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🦀</text></svg>">
<style>
  :root {
    --bg: #0d1117;
    --bg-sub: #161b22;
    --bg-card: #1c2129;
    --border: #30363d;
    --border-light: #21262d;
    --fg: #e6edf3;
    --fg-muted: #8b949e;
    --fg-subtle: #6e7681;
    --accent: #58a6ff;
    --accent-strong: #79c0ff;
    --green: #3fb950;
    --orange: #d29922;
    --purple: #bc8cff;
    --red: #f85149;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html { scroll-behavior: smooth; }
  body {
    font-family: var(--font-sans);
    background: var(--bg);
    color: var(--fg);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
  }
  a { color: var(--accent); text-decoration: none; }
  a:hover { text-decoration: underline; }

  /* ── Header / Nav ─────────────────────────────── */
  header {
    position: sticky; top: 0; z-index: 100;
    background: var(--bg-sub);
    border-bottom: 1px solid var(--border-light);
    backdrop-filter: blur(8px);
  }
  .nav {
    max-width: 1280px; margin: 0 auto; padding: 0 24px;
    height: 60px; display: flex; align-items: center; gap: 24px;
  }
  .nav .brand {
    display: flex; align-items: center; gap: 10px;
    font-weight: 700; font-size: 17px; color: var(--fg); cursor: pointer;
  }
  .nav .brand:hover { text-decoration: none; }
  .brand .logo {
    width: 30px; height: 30px; border-radius: 6px;
    background: linear-gradient(135deg, var(--orange), var(--red));
    display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 15px;
  }
  .nav .links { display: flex; gap: 20px; margin-left: auto; align-items: center; }
  .nav .links a { color: var(--fg-muted); font-size: 14px; font-weight: 500; }
  .nav .links a:hover { color: var(--fg); text-decoration: none; }
  .nav .github-btn {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 14px; border: 1px solid var(--border);
    border-radius: 6px; color: var(--fg); font-size: 13px; font-weight: 500;
    background: var(--bg-sub);
  }
  .nav .github-btn:hover { background: var(--bg-card); text-decoration: none; }

  /* ── Hero ─────────────────────────────────────── */
  .hero {
    max-width: 1280px; margin: 0 auto; padding: 96px 24px 72px;
    text-align: center;
  }
  .hero .badge-row {
    display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-bottom: 24px;
  }
  .badge {
    font-family: var(--font-mono); font-size: 12px;
    padding: 4px 12px; border-radius: 20px;
    border: 1px solid var(--border); background: var(--bg-sub); color: var(--fg-muted);
  }
  .badge.green { color: var(--green); border-color: rgba(63,185,80,.3); }
  .badge.blue  { color: var(--accent-strong); border-color: rgba(88,166,255,.3); }
  .badge.purple{ color: var(--purple); border-color: rgba(188,140,255,.3); }
  .hero h1 {
    font-size: clamp(38px, 6vw, 64px); font-weight: 800; letter-spacing: -1.5px; line-height: 1.1;
    margin-bottom: 20px;
    background: linear-gradient(180deg, #f0f6fc 30%, #9da7b3 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }
  .hero h1 .accent { background: none; color: var(--accent-strong); -webkit-text-fill-color: currentColor; }
  .hero p.tagline {
    max-width: 720px; margin: 0 auto 36px;
    font-size: 19px; color: var(--fg-muted);
  }
  .hero .cta { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; }
  .btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 12px 26px; border-radius: 8px; font-size: 15px; font-weight: 600;
    border: 1px solid var(--border); cursor: pointer; transition: all .15s;
  }
  .btn.primary { background: #238636; border-color: #2ea043; color: #fff; }
  .btn.primary:hover { background: #2ea043; text-decoration: none; }
  .btn.secondary { background: var(--bg-sub); color: var(--fg); }
  .btn.secondary:hover { background: var(--bg-card); text-decoration: none; }

  /* ── Sections ─────────────────────────────────── */
  .section { max-width: 1280px; margin: 0 auto; padding: 64px 24px; }
  .section h2 {
    font-size: 28px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 8px;
  }
  .section .sub { color: var(--fg-muted); margin-bottom: 32px; font-size: 16px; }
  .section .anchor {
    display: flex; align-items: center; gap: 10px;
  }
  .section .anchor::before {
    content: ""; width: 4px; height: 26px; border-radius: 2px;
    background: linear-gradient(180deg, var(--accent), var(--green));
  }

  /* ── Feature cards ────────────────────────────── */
  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; }
  .card {
    background: var(--bg-sub); border: 1px solid var(--border-light); border-radius: 10px;
    padding: 24px; transition: transform .15s, border-color .15s;
  }
  .card:hover { transform: translateY(-3px); border-color: var(--border); }
  .card .icon { width: 40px; height: 40px; border-radius: 8px; display: grid; place-items: center; font-size: 20px; margin-bottom: 14px; }
  .card h3 { font-size: 17px; margin-bottom: 8px; }
  .card p { font-size: 14px; color: var(--fg-muted); }

  /* ── DB support ───────────────────────────────── */
  .db-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 18px; }
  .db-card {
    background: var(--bg-sub); border: 1px solid var(--border-light); border-radius: 10px;
    padding: 28px 24px; text-align: center; transition: border-color .15s;
  }
  .db-card:hover { border-color: var(--border); }
  .db-card .emoji { font-size: 40px; display: block; margin-bottom: 10px; }
  .db-card .name { font-weight: 700; font-size: 16px; }
  .db-card .tag {
    display: inline-block; margin-top: 8px; font-family: var(--font-mono); font-size: 11px;
    padding: 2px 10px; border-radius: 12px; background: rgba(63,185,80,.12); color: var(--green);
  }

  /* ── Code blocks ──────────────────────────────── */
  pre {
    background: var(--bg-sub); border: 1px solid var(--border-light); border-radius: 10px;
    padding: 22px 24px; overflow-x: auto; font-family: var(--font-mono); font-size: 13.5px; line-height: 1.7;
  }
  pre code { color: var(--fg); }
  .code-header {
    display: flex; align-items: center; gap: 8px; padding: 10px 16px;
    background: #0d1117; border: 1px solid var(--border-light); border-bottom: none;
    border-radius: 10px 10px 0 0; font-size: 12px; color: var(--fg-muted); font-family: var(--font-mono);
  }
  .code-header .dot { width: 11px; height: 11px; border-radius: 50%; }
  .code-header .dot:nth-child(1){ background:#ff5f57; } .code-header .dot:nth-child(2){ background:#febc2e; } .code-header .dot:nth-child(3){ background:#28c840; }
  .code-block { margin: 18px 0; }
  .code-block pre { border-radius: 0 0 10px 10px; }

  .tok-kw  { color: #ff7b72; }
  .tok-fn  { color: #d2a8ff; }
  .tok-str { color: #a5d6ff; }
  .tok-com { color: #8b949e; font-style: italic; }
  .tok-ty  { color: #79c0ff; }
  .tok-num { color: #79c0ff; }
  .tok-p   { color: #e6edf3; }

  /* ── Migration table ──────────────────────────── */
  .table-wrap { overflow-x: auto; border: 1px solid var(--border-light); border-radius: 10px; }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  thead { background: var(--bg-sub); }
  th, td { text-align: left; padding: 12px 18px; border-bottom: 1px solid var(--border-light); }
  th { color: var(--fg-muted); font-weight: 600; }
  tr:last-child td { border-bottom: none; }
  .arrow { color: var(--accent); font-weight: 700; margin: 0 6px; }
  td code, .tag-inline { font-family: var(--font-mono); font-size: 12px; background: var(--bg-card); padding: 2px 8px; border-radius: 5px; color: var(--accent-strong); }

  /* ── Footer ───────────────────────────────────── */
  footer {
    border-top: 1px solid var(--border-light); background: var(--bg-sub);
    padding: 40px 24px; text-align: center; color: var(--fg-subtle); font-size: 13px;
  }
  footer a { color: var(--fg-muted); }

  /* ── Responsive ───────────────────────────────── */
  @media (max-width: 720px) {
    .nav .links { display: none; }
    .hero { padding: 64px 24px 48px; }
    .section { padding: 48px 20px; }
  }
</style>
</head>
<body>

<!-- ═══ Header ═══ -->
<header>
  <div class="nav">
    <div class="brand" onclick="scrollToTop()">
      <div class="logo">T</div>
      <span>TORM</span>
    </div>
    <div class="links">
      <a href="#features">特性</a>
      <a href="#databases">数据库</a>
      <a href="#quickstart">快速开始</a>
      <a href="#migration">数据迁移</a>
      <a href="#api">API</a>
      <a class="github-btn" href="https://github.com/WLmutou/torm" target="_blank" rel="noopener">
        <svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
        GitHub
      </a>
    </div>
  </div>
</header>

<!-- ═══ Hero ═══ -->
<section class="hero">
  <div class="badge-row">
    <span class="badge blue">v0.2.0</span>
    <span class="badge green">100% Rust</span>
    <span class="badge purple">Async / Tokio</span>
    <span class="badge">Native Wire Protocols</span>
    <span class="badge">MIT License</span>
  </div>
  <h1>基于 Tokio 的 Rust 异步 <span class="accent">ORM</span></h1>
  <p class="tagline">
    面向 Rust 的异步 ORM,采用 GORM 风格 API,通过原生协议驱动
    直连 <strong>SQLite · MySQL · PostgreSQL</strong>,零额外数据库驱动依赖。
  </p>
  <div class="cta">
    <a class="btn primary" href="#quickstart">
      <svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm0 2.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11Zm0 1.5a1 1 0 0 1 .7.3l2 2a1 1 0 0 1-1.4 1.4L9 7.41V10a1 1 0 0 1-2 0V7.41L6.7 8.7a1 1 0 0 1-1.4-1.4l2-2A1 1 0 0 1 8 4Z"/></svg>
      快速开始
    </a>
    <a class="btn secondary" href="#features">了解更多</a>
  </div>
</section>

<!-- ═══ Features ═══ -->
<section class="section" id="features">
  <div class="anchor"><h2>核心特性</h2></div>
  <p class="sub">为生产级应用打造的现代 ORM 能力。</p>
  <div class="grid">
    <div class="card">
      <div class="icon" style="background:rgba(88,166,255,.15);">⚡</div>
      <h3>原生协议直连</h3>
      <p>通过数据库原生 wire protocol 通信,无需 libmysql / libpq 等外部驱动,纯 Rust 实现,部署零负担。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(63,185,80,.15);">🌊</div>
      <h3>Async / Tokio</h3>
      <p>基于 Tokio 运行时的高并发异步访问,天然支持大规模并发场景。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(188,140,255,.15);">🧩</div>
      <h3>GORM 风格 API</h3>
      <p>链式查询构建器、Where/Order/Join/分页/聚合等,语法直观、上手快。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(210,168,255,.15);">📦</div>
      <h3>派生宏模型</h3>
      <p>通过 <code>#[derive(Model)]</code> 定义模型,自动生成表结构、字段映射与关联关系。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(242,151,24,.15);">💾</div>
      <h3>三库驱动一体</h3>
      <p>统一 API 同时支持 SQLite、MySQL、PostgreSQL,一套代码多库切换。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(248,81,73,.15);">🔒</div>
      <h3>SQL 安全</h3>
      <p>参数化查询与标识符安全检查,有效防止 SQL 注入,保障数据安全。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(63,185,80,.15);">🛠️</div>
      <h3>迁移工具</h3>
      <p>内置 MySQL ↔ PostgreSQL、SQLite ↔ PostgreSQL 等数据库间迁移 CLI 工具。</p>
    </div>
    <div class="card">
      <div class="icon" style="background:rgba(121,192,255,.15);">📊</div>
      <h3>监控与性能</h3>
      <p>内置日志、SQL 监控与性能统计模块,助力定位瓶颈。</p>
    </div>
  </div>
</section>

<!-- ═══ Databases ═══ -->
<section class="section" id="databases">
  <div class="anchor"><h2>支持的数据库</h2></div>
  <p class="sub">通过原生协议驱动,无需安装任何外部数据库驱动。</p>
  <div class="db-grid">
    <div class="db-card">
      <span class="emoji">🟠</span>
      <div class="name">MySQL</div>
      <span class="tag">原生 wire protocol</span>
    </div>
    <div class="db-card">
      <span class="emoji">🐘</span>
      <div class="name">PostgreSQL</div>
      <span class="tag">原生 wire protocol</span>
    </div>
    <div class="db-card">
      <span class="emoji">🗄️</span>
      <div class="name">SQLite</div>
      <span class="tag">bundled · 零外部依赖</span>
    </div>
  </div>
</section>

<!-- ═══ Quickstart ═══ -->
<section class="section" id="quickstart">
  <div class="anchor"><h2>快速开始</h2></div>
  <p class="sub">在你的 <code>Cargo.toml</code> 中添加依赖并开始。</p>

  <div class="code-block">
    <div class="code-header"><span class="dot"></span><span class="dot"></span><span class="dot"></span>&nbsp; Cargo.toml</div>
    <pre><code><span class="tok-kw">[dependencies]</span>
torm = <span class="tok-str">"0.2"</span>
tokio = { version = <span class="tok-str">"1"</span>, features = [<span class="tok-str">"full"</span>] }</code></pre>
  </div>

  <div class="code-block">
    <div class="code-header"><span class="dot"></span><span class="dot"></span><span class="dot"></span>&nbsp; src/main.rs</div>
    <pre><code><span class="tok-kw">use</span> torm::db::database::Database;
<span class="tok-kw">use</span> torm::orm::model::Model;
<span class="tok-kw">use</span> torm_derive::Model;

<span class="tok-com">// 定义模型</span>
<span class="tok-p">#[</span><span class="tok-kw">derive</span><span class="tok-p">(</span>Model<span class="tok-p">)]</span>
<span class="tok-kw">struct</span> <span class="tok-ty">User</span> {
    <span class="tok-kw">#[id</span> <span class="tok-kw">(auto_increment)]</span>
    id: i64,
    name: <span class="tok-ty">String</span>,
    age: <span class="tok-ty">Option</span>&lt;i32&gt;,
}

<span class="tok-p">#[</span><span class="tok-kw">tokio</span>::<span class="tok-kw">main</span><span class="tok-p">]</span>
<span class="tok-kw">async fn</span> <span class="tok-fn">main</span>() -&gt; torm::db::error::<span class="tok-ty">Result</span>&lt;()&gt; {
    <span class="tok-com">// 连接数据库(MySQL / PostgreSQL / SQLite 通用)</span>
    <span class="tok-kw">let</span> db = Database::mysql(<span class="tok-str">"127.0.0.1"</span>, <span class="tok-num">3306</span>, <span class="tok-str">"mydb"</span>, <span class="tok-str">"root"</span>, <span class="tok-str">"pass"</span>).await?;

    <span class="tok-com">// 插入</span>
    <span class="tok-kw">let</span> user = User { id: <span class="tok-num">0</span>, name: <span class="tok-str">"Alice"</span>.into(), age: <span class="tok-ty">Some</span>(<span class="tok-num">25</span>) };
    user.insert(&db).await?;

    <span class="tok-com">// 查询</span>
    <span class="tok-kw">let</span> users = User::query(&db).await?;
    <span class="tok-kw">for</span> u <span class="tok-kw">in</span> users { println!(<span class="tok-str">"{:?}"</span>, u.name); }
    <span class="tok-ty">Ok</span>(())
}</code></pre>
  </div>
</section>

<!-- ═══ Migration ═══ -->
<section class="section" id="migration">
  <div class="anchor"><h2>数据库迁移工具</h2></div>
  <p class="sub">内置 CLI 工具,在数据库之间迁移表结构与数据。</p>
  <div class="table-wrap">
    <table>
      <thead>
        <tr><th>工具</th><th>迁移方向</th><th>示例</th></tr>
      </thead>
      <tbody>
        <tr>
          <td><code>mysql2postgresql</code></td>
          <td>MySQL <span class="arrow">→</span> PostgreSQL</td>
          <td><span class="tag-inline">--mhost 127.0.0.1 --mport 3306 --mdb mydb ...</span></td>
        </tr>
        <tr>
          <td><code>postgresql2mysql</code></td>
          <td>PostgreSQL <span class="arrow">→</span> MySQL</td>
          <td><span class="tag-inline">--phost 127.0.0.1 --pport 5432 --pdb mydb ...</span></td>
        </tr>
        <tr>
          <td><code>sqlite2postgres</code></td>
          <td>SQLite <span class="arrow">→</span> PostgreSQL</td>
          <td><span class="tag-inline">./sqlite2postgres data.db --phost ...</span></td>
        </tr>
        <tr>
          <td><code>postgres2sqlite</code></td>
          <td>PostgreSQL <span class="arrow">→</span> SQLite</td>
          <td><span class="tag-inline">./postgres2sqlite out.db --phost ...</span></td>
        </tr>
      </tbody>
    </table>
  </div>
  <div class="code-block" style="margin-top:20px;">
    <div class="code-header"><span class="dot"></span><span class="dot"></span><span class="dot"></span>&nbsp; 迁移示例</div>
    <pre><code><span class="tok-com"># MySQL → PostgreSQL(不指定 --tables 默认迁移全部表)</span>
./target/release/mysql2postgresql \
  --mhost 127.0.0.1 --mport 3306 --mdb mydb --muser root --mpass pw \
  --phost 127.0.0.1 --pport 5432 --pdb mydb --puser postgres --ppass pw

<span class="tok-com"># 可选参数:--tables t1,t2 / --batch 1000 / --create-only / --data-only</span></code></pre>
  </div>
</section>

<!-- ═══ API ═══ -->
<section class="section" id="api">
  <div class="anchor"><h2>核心 API</h2></div>
  <p class="sub">统一的抽象层,一套代码访问多种数据库。</p>
  <div class="table-wrap">
    <table>
      <thead>
        <tr><th>模块</th><th>说明</th></tr>
      </thead>
      <tbody>
        <tr><td><code>db::database</code></td><td>统一 <code>Database</code> / <code>Transaction</code> 抽象,MySQL / PostgreSQL / SQLite 通用</td></tr>
        <tr><td><code>orm::model</code></td><td>模型 trait 与 <code>#[derive(Model)]</code> 派生宏</td></tr>
        <tr><td><code>orm::query</code></td><td>链式查询构建器:Where / Order / Pagination</td></tr>
        <tr><td><code>orm::advanced_query</code></td><td>高级查询:Join / Aggregation / Union / Subquery</td></tr>
        <tr><td><code>orm::relations</code></td><td>关联关系:belongs_to / has_many 等</td></tr>
        <tr><td><code>orm::migration</code></td><td>Schema 迁移工具</td></tr>
        <tr><td><code>db::pool</code></td><td>连接池</td></tr>
        <tr><td><code>monitoring</code></td><td>日志、SQL 监控与性能统计</td></tr>
      </tbody>
    </table>
  </div>
</section>

<!-- ═══ Footer ═══ -->
<footer>
  <p><strong>TORM</strong> · A Tokio-based async ORM for Rust</p>
  <p style="margin-top:8px;">
    <a href="https://github.com/WLmutou/torm" target="_blank" rel="noopener">GitHub</a>
    <span style="margin:0 8px;color:var(--border)">·</span>
    <a href="https://docs.rs/torm" target="_blank" rel="noopener">docs.rs</a>
    <span style="margin:0 8px;color:var(--border)">·</span>
    <span>MIT License</span>
  </p>
</footer>

<script>
  function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); }
</script>
</body>
</html>