termaui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -0
- package/dist/terma.esm.js +139 -0
- package/dist/terma.js +159 -0
- package/dist/termaui.css +1347 -0
- package/dist/termaui.min.css +1 -0
- package/fonts/babelstone-tibetan-slim.woff2 +0 -0
- package/fonts/babelstone-tibetan.woff2 +0 -0
- package/fonts/ddc-rinzin.woff2 +0 -0
- package/fonts/ddc-uchen.woff2 +0 -0
- package/fonts/gangjie-drutsa.woff2 +0 -0
- package/fonts/gangjie-uchen.woff2 +0 -0
- package/fonts/jamyang-monlam-uchen.woff2 +0 -0
- package/fonts/jomolhari-regular.woff2 +0 -0
- package/fonts/joyig.woff2 +0 -0
- package/fonts/khampa-dedri-bechu.woff2 +0 -0
- package/fonts/khampa-dedri-chuyig.woff2 +0 -0
- package/fonts/khampa-dedri-drutsa.woff2 +0 -0
- package/fonts/misans-tibetan.woff2 +0 -0
- package/fonts/monlam-bodyig.woff2 +0 -0
- package/fonts/monlam-uni-dutsa1.woff2 +0 -0
- package/fonts/monlam-uni-dutsa2.woff2 +0 -0
- package/fonts/monlam-uni-ouchan1.woff2 +0 -0
- package/fonts/monlam-uni-ouchan2.woff2 +0 -0
- package/fonts/monlam-uni-ouchan3.woff2 +0 -0
- package/fonts/monlam-uni-ouchan4.woff2 +0 -0
- package/fonts/monlam-uni-ouchan5.woff2 +0 -0
- package/fonts/monlam-uni-paytsik.woff2 +0 -0
- package/fonts/monlam-uni-sans.woff2 +0 -0
- package/fonts/monlam-uni-tikrang.woff2 +0 -0
- package/fonts/monlam-uni-tiktong.woff2 +0 -0
- package/fonts/noto-sans-tibetan.woff2 +0 -0
- package/fonts/noto-serif-tibetan-black.woff2 +0 -0
- package/fonts/noto-serif-tibetan-bold.woff2 +0 -0
- package/fonts/noto-serif-tibetan-extrabold.woff2 +0 -0
- package/fonts/noto-serif-tibetan-extralight.woff2 +0 -0
- package/fonts/noto-serif-tibetan-light.woff2 +0 -0
- package/fonts/noto-serif-tibetan-medium.woff2 +0 -0
- package/fonts/noto-serif-tibetan-regular.woff2 +0 -0
- package/fonts/noto-serif-tibetan-semibold.woff2 +0 -0
- package/fonts/noto-serif-tibetan-thin.woff2 +0 -0
- package/fonts/panchen-tsukring.woff2 +0 -0
- package/fonts/qomolangma-betsu.woff2 +0 -0
- package/fonts/qomolangma-drutsa.woff2 +0 -0
- package/fonts/qomolangma-tsutong.woff2 +0 -0
- package/fonts/qomolangma-uchen-sarchen.woff2 +0 -0
- package/fonts/riwoche-dhodri-yigchen.woff2 +0 -0
- package/fonts/sadri-drutsa.woff2 +0 -0
- package/fonts/sadri-yigchen.woff2 +0 -0
- package/fonts/tibetan-machine-uni.woff2 +0 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# termaUI
|
|
2
|
+
|
|
3
|
+
**Tibetan typography CSS framework** — fonts, utility classes, and rendering fixes for Tibetan script on the web.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/termaui)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
[Documentation](https://termafoundry.com/termaui/docs/) · [Demo](https://termafoundry.com/termaui/) · [GitHub](https://github.com/termafoundry/termafoundry)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What it includes
|
|
13
|
+
|
|
14
|
+
- **30+ Tibetan fonts** as WOFF2 — Jomolhari, Noto Serif Tibetan, Monlam, Qomolangma, BabelStone, Khampa Dedri, and many more
|
|
15
|
+
- **Utility classes** for font selection, sizes, line heights, text effects, and pecha manuscript layout
|
|
16
|
+
- **terma.js** — JavaScript utilities that fix Tibetan rendering problems CSS alone cannot solve (line breaking, punctuation protection, Unicode normalization)
|
|
17
|
+
- **Zero config** — load the CSS and start using `tr-` classes
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### CDN (no build step)
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/termaui/dist/termaui.min.css">
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/termaui/dist/terma.js"></script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### npm
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install termaui
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<!-- 1. Load the stylesheet -->
|
|
42
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/termaui/dist/termaui.min.css">
|
|
43
|
+
|
|
44
|
+
<!-- 2. Use lang="bo" + termaUI classes -->
|
|
45
|
+
<p class="tr-jomolhari tr-guard" lang="bo">
|
|
46
|
+
བཀྲ་ཤིས་བདེ་ལེགས།
|
|
47
|
+
</p>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For correct line breaking, add terma.js and call `terma.prepareAll()` after the DOM is ready:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<script src="https://cdn.jsdelivr.net/npm/termaui/dist/terma.js"></script>
|
|
54
|
+
<script>
|
|
55
|
+
document.addEventListener('DOMContentLoaded', () => terma.prepareAll());
|
|
56
|
+
</script>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Framework Guides
|
|
62
|
+
|
|
63
|
+
### Vanilla HTML
|
|
64
|
+
|
|
65
|
+
No build step needed. Use the CDN links above.
|
|
66
|
+
|
|
67
|
+
### Next.js (App Router)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install termaui
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// app/layout.tsx
|
|
75
|
+
import 'termaui/dist/termaui.css';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// app/TibetanInit.tsx
|
|
80
|
+
'use client';
|
|
81
|
+
import { useEffect } from 'react';
|
|
82
|
+
import terma from 'termaui';
|
|
83
|
+
|
|
84
|
+
export function TibetanInit() {
|
|
85
|
+
useEffect(() => { terma.prepareAll(); }, []);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// Add <TibetanInit /> inside <body> in your root layout
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Next.js (Pages Router)
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// pages/_app.tsx
|
|
95
|
+
import 'termaui/dist/termaui.css';
|
|
96
|
+
import { useEffect } from 'react';
|
|
97
|
+
import { useRouter } from 'next/router';
|
|
98
|
+
import terma from 'termaui';
|
|
99
|
+
|
|
100
|
+
export default function App({ Component, pageProps }) {
|
|
101
|
+
const router = useRouter();
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
terma.prepareAll();
|
|
104
|
+
router.events.on('routeChangeComplete', () => terma.prepareAll());
|
|
105
|
+
return () => router.events.off('routeChangeComplete', () => terma.prepareAll());
|
|
106
|
+
}, [router]);
|
|
107
|
+
return <Component {...pageProps} />;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Astro
|
|
112
|
+
|
|
113
|
+
```astro
|
|
114
|
+
---
|
|
115
|
+
// src/layouts/BaseLayout.astro
|
|
116
|
+
import 'termaui/dist/termaui.css';
|
|
117
|
+
---
|
|
118
|
+
<html><body>
|
|
119
|
+
<slot />
|
|
120
|
+
<script>
|
|
121
|
+
import terma from 'termaui';
|
|
122
|
+
terma.prepareAll();
|
|
123
|
+
</script>
|
|
124
|
+
</body></html>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Astro + View Transitions**: Use `astro:page-load` instead of `DOMContentLoaded` when View Transitions are enabled — it fires after every client-side navigation.
|
|
128
|
+
|
|
129
|
+
### Vite + React
|
|
130
|
+
|
|
131
|
+
```jsx
|
|
132
|
+
// src/main.jsx
|
|
133
|
+
import 'termaui/dist/termaui.css';
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```jsx
|
|
137
|
+
// Root component
|
|
138
|
+
import { useEffect } from 'react';
|
|
139
|
+
import terma from 'termaui';
|
|
140
|
+
|
|
141
|
+
export default function App() {
|
|
142
|
+
useEffect(() => { terma.prepareAll(); }, []);
|
|
143
|
+
// ...
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Vite + Vue
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// src/main.ts
|
|
151
|
+
import 'termaui/dist/termaui.css';
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```vue
|
|
155
|
+
<!-- Root component -->
|
|
156
|
+
<script setup>
|
|
157
|
+
import { onMounted } from 'vue';
|
|
158
|
+
import terma from 'termaui';
|
|
159
|
+
onMounted(() => terma.prepareAll());
|
|
160
|
+
</script>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### SvelteKit
|
|
164
|
+
|
|
165
|
+
```svelte
|
|
166
|
+
<!-- src/routes/+layout.svelte -->
|
|
167
|
+
<script>
|
|
168
|
+
import 'termaui/dist/termaui.css';
|
|
169
|
+
import { onMount } from 'svelte';
|
|
170
|
+
import terma from 'termaui';
|
|
171
|
+
onMount(() => terma.prepareAll());
|
|
172
|
+
</script>
|
|
173
|
+
<slot />
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Tailwind CSS Compatibility
|
|
177
|
+
|
|
178
|
+
termaUI uses a `tr-` prefix on every class. There are no naming conflicts with Tailwind, Bootstrap, or any other CSS framework. Combine classes freely:
|
|
179
|
+
|
|
180
|
+
```html
|
|
181
|
+
<div class="p-4 rounded-xl bg-slate-900 tr-glass">
|
|
182
|
+
<p class="text-sm tr-jomolhari tr-guard tr-text-rainbow" lang="bo">
|
|
183
|
+
བཀྲ་ཤིས་བདེ་ལེགས།
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## CSS Classes Reference
|
|
191
|
+
|
|
192
|
+
### Font Stacks
|
|
193
|
+
|
|
194
|
+
| Class | Font | Style |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `.tr-jomolhari` | Jomolhari | Traditional Uchen |
|
|
197
|
+
| `.tr-noto` | Noto Serif Tibetan | Modern screen serif |
|
|
198
|
+
| `.tr-noto-bold` | Noto Serif Tibetan 700 | Bold |
|
|
199
|
+
| `.tr-machine-uni` | Tibetan Machine Uni | Display/headline |
|
|
200
|
+
| `.tr-monlam` | Monlam Bodyig | Elegant Uchen |
|
|
201
|
+
| `.tr-drutsa` | Qomolangma-Drutsa | Cursive drutsa |
|
|
202
|
+
| `.tr-noto-sans` | Noto Sans Tibetan | Sans-serif |
|
|
203
|
+
| `.tr-misans` | MiSans Tibetan | Geometric sans |
|
|
204
|
+
|
|
205
|
+
Full font list: [termafoundry.com/termaui/docs](https://termafoundry.com/termaui/docs/#tr-jomolhari)
|
|
206
|
+
|
|
207
|
+
### Typography
|
|
208
|
+
|
|
209
|
+
| Class | Effect |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `.tr-text-sm` `.tr-text-base` `.tr-text-lg` `.tr-text-xl` `.tr-text-2xl` | Font sizes |
|
|
212
|
+
| `.tr-leading-tight` `.tr-leading-normal` `.tr-leading-relaxed` `.tr-leading-loose` | Line heights |
|
|
213
|
+
| `.tr-text-center` `.tr-text-right` | Alignment |
|
|
214
|
+
| `.tr-justify-bo` | Tibetan justification (requires terma.js) |
|
|
215
|
+
| `.tr-indent` | Traditional indent (2em) |
|
|
216
|
+
|
|
217
|
+
### Visual Effects
|
|
218
|
+
|
|
219
|
+
| Class | Effect |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `.tr-text-rainbow` | Multi-color gradient text |
|
|
222
|
+
| `.tr-text-saffron` | Saffron-to-gold gradient |
|
|
223
|
+
| `.tr-text-fire` | Animated flame gradient |
|
|
224
|
+
| `.tr-shimmer` | Animated gold metallic sweep |
|
|
225
|
+
| `.tr-glow-aurora` | Pulsing teal aurora glow |
|
|
226
|
+
| `.tr-text-lapis` | Deep ultramarine gradient |
|
|
227
|
+
| `.tr-emboss` | Carved-stone relief effect |
|
|
228
|
+
| `.tr-outline-gold` | Gold outline, transparent fill |
|
|
229
|
+
| `.tr-glass` | Frosted glass panel |
|
|
230
|
+
| `.tr-glass-dark` | Dark frosted glass panel |
|
|
231
|
+
|
|
232
|
+
### Guardian Utilities
|
|
233
|
+
|
|
234
|
+
| Class | Effect |
|
|
235
|
+
|---|---|
|
|
236
|
+
| `.tr-guard` | Adds `padding-block: 0.25em` — prevents vowel mark clipping |
|
|
237
|
+
| `.tr-guard-lg` | Larger clip guard (`0.5em`) |
|
|
238
|
+
| `.tr-overflow-safe` | Last-resort overflow protection |
|
|
239
|
+
| `.tr-scale-up` | Scale Tibetan to 120% to match English visual weight |
|
|
240
|
+
| `.tr-scale-match` | Scale to 150% |
|
|
241
|
+
| `.tr-baseline` | Vertical alignment fix for inline Tibetan in English text |
|
|
242
|
+
| `.tr-shad-pair` | Prevent double-shad from splitting across lines |
|
|
243
|
+
|
|
244
|
+
### Stacking
|
|
245
|
+
|
|
246
|
+
| Class | Effect |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `.tr-stack-safe` | Extra line height for complex consonant stacks (mantras, dharanis) |
|
|
249
|
+
| `.tr-ligatures` | Enable OpenType stacking features (`liga`, `blws`, `abvs`) |
|
|
250
|
+
| `.tr-yig-chung` | Commentary text at 75% size |
|
|
251
|
+
|
|
252
|
+
### Pecha Layout
|
|
253
|
+
|
|
254
|
+
| Class | Effect |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `.tr-pecha` | Traditional pecha manuscript container (6:1 landscape, three columns) |
|
|
257
|
+
| `.tr-pecha-dark` | Dark variant (gold on indigo) |
|
|
258
|
+
| `.tr-pecha-title` | Left margin column |
|
|
259
|
+
| `.tr-pecha-body` | Main text block |
|
|
260
|
+
| `.tr-pecha-folio` | Right folio number column |
|
|
261
|
+
| `.tr-pecha-title-rotated` | Rotated title label |
|
|
262
|
+
| `.tr-yig-mgo` | Adds ༄༅། opening mark via `::before` |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## terma.js API
|
|
267
|
+
|
|
268
|
+
### `terma.prepare(element)`
|
|
269
|
+
|
|
270
|
+
Processes Tibetan text inside a single element:
|
|
271
|
+
- Inserts zero-width spaces after tsheg (་) for proper line-break opportunities
|
|
272
|
+
- Replaces tsheg-before-shad (་།) with non-breaking tsheg (༌།) to prevent splitting
|
|
273
|
+
- Wraps double-shad (། །) with word-joiners
|
|
274
|
+
|
|
275
|
+
Safe to call multiple times — processed elements are skipped.
|
|
276
|
+
|
|
277
|
+
```js
|
|
278
|
+
terma.prepare(document.getElementById('prayer'));
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### `terma.prepareAll(selector?)`
|
|
282
|
+
|
|
283
|
+
Processes all `[lang="bo"]` elements on the page, or all elements matching the given CSS selector.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
terma.prepareAll(); // all [lang="bo"]
|
|
287
|
+
terma.prepareAll('.tibetan-content'); // custom selector
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### `terma.normalize(element)` / `terma.normalizeAll(selector?)`
|
|
291
|
+
|
|
292
|
+
Applies Unicode NFC normalization to text nodes. Fixes invisible search failures caused by equivalent-looking but differently encoded Tibetan sequences.
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
terma.normalizeAll();
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### CSS-only vs. Requires terma.js
|
|
299
|
+
|
|
300
|
+
**CSS only (no terma.js needed):**
|
|
301
|
+
- All font classes
|
|
302
|
+
- All visual effects
|
|
303
|
+
- Typography utilities (sizes, line heights, alignment)
|
|
304
|
+
- Guardian utilities
|
|
305
|
+
- Pecha layout
|
|
306
|
+
|
|
307
|
+
**Requires terma.js:**
|
|
308
|
+
- Correct Tibetan line breaking
|
|
309
|
+
- `.tr-justify-bo` (does nothing without `terma.prepare()`)
|
|
310
|
+
- Auto-protection of double-shad and tsheg-before-shad
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Package Contents
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
termaui/
|
|
318
|
+
dist/
|
|
319
|
+
termaui.css — Full stylesheet (unminified)
|
|
320
|
+
termaui.min.css — Minified stylesheet (recommended for production)
|
|
321
|
+
terma.js — UMD/CJS build (browser script tag, Node require)
|
|
322
|
+
terma.esm.js — ESM build (bundler import)
|
|
323
|
+
fonts/
|
|
324
|
+
*.woff2 — All Tibetan font files
|
|
325
|
+
README.md
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## License
|
|
331
|
+
|
|
332
|
+
MIT — free for personal and commercial use.
|
|
333
|
+
|
|
334
|
+
Fonts are bundled under their respective open licenses (SIL OFL, GPL v2, open license). See [termafoundry.com/termaui](https://termafoundry.com/termaui/) for per-font license details.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
terma.js v0.1.0 — ESM Module
|
|
3
|
+
Tibetan text processing utilities for termaUI.
|
|
4
|
+
Handles line-breaking, justification, and punctuation protection
|
|
5
|
+
that CSS alone cannot solve.
|
|
6
|
+
|
|
7
|
+
Usage (bundler / import statement):
|
|
8
|
+
import terma from 'termaui';
|
|
9
|
+
terma.prepareAll();
|
|
10
|
+
|
|
11
|
+
Named imports also work:
|
|
12
|
+
import { prepare, prepareAll, normalize, normalizeAll } from 'termaui';
|
|
13
|
+
========================================================================== */
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// Tibetan Unicode constants
|
|
18
|
+
const TSHEG = '\u0F0B'; // ་ intersyllabic separator
|
|
19
|
+
const TSHEG_NONBREAK = '\u0F0C'; // ༌ non-breaking tsheg
|
|
20
|
+
const SHAD = '\u0F0D'; // ། clause/sentence marker
|
|
21
|
+
const NYIS_SHAD = '\u0F0E'; // ༎ double shad (single char)
|
|
22
|
+
const GTER_MA = '\u0F14'; // ༔ terma sign
|
|
23
|
+
const VISARGA = '\u0F7F'; // ཿ visarga
|
|
24
|
+
const ZWS = '\u200B'; // zero-width space (break opportunity)
|
|
25
|
+
const WJ = '\u2060'; // word joiner (prevents break)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Walk all text nodes inside an element.
|
|
29
|
+
*/
|
|
30
|
+
function walkTextNodes(el, callback) {
|
|
31
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
32
|
+
const nodes = [];
|
|
33
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
34
|
+
// Process in reverse to avoid offset issues from mutations
|
|
35
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
36
|
+
callback(nodes[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* prepare(element)
|
|
42
|
+
*
|
|
43
|
+
* Processes Tibetan text inside the given element:
|
|
44
|
+
*
|
|
45
|
+
* 1. Replaces tsheg before any clause-ending mark with non-breaking tsheg (༌)
|
|
46
|
+
* to prevent bad breaks. Covers:
|
|
47
|
+
* - shad (། U+0F0D) — standard clause marker
|
|
48
|
+
* - nyis-shad (༎ U+0F0E) — double clause marker
|
|
49
|
+
* - gter-ma (༔ U+0F14) — terma / treasure-text sign
|
|
50
|
+
* Example: ང་། → ང༌། | ར་༎ → ར༌༎ | ར་༔ → ར༌༔
|
|
51
|
+
*
|
|
52
|
+
* 2. Wraps double-shad sequences (། །) with word-joiners so they
|
|
53
|
+
* never split across lines.
|
|
54
|
+
*
|
|
55
|
+
* 3. Inserts zero-width spaces after tsheg (་) to give browsers
|
|
56
|
+
* proper line-break opportunities — the #1 Tibetan rendering fix.
|
|
57
|
+
*
|
|
58
|
+
* Call this after the DOM is ready. Safe to call multiple times —
|
|
59
|
+
* already-processed elements are skipped.
|
|
60
|
+
*/
|
|
61
|
+
export function prepare(el) {
|
|
62
|
+
if (!el) return;
|
|
63
|
+
if (el.dataset.termaPrepared) return;
|
|
64
|
+
|
|
65
|
+
walkTextNodes(el, (node) => {
|
|
66
|
+
let text = node.textContent;
|
|
67
|
+
|
|
68
|
+
// Step 1: Replace tsheg before clause-ending marks with non-breaking tsheg.
|
|
69
|
+
// Covers shad (།), nyis-shad (༎), and gter-ma (༔).
|
|
70
|
+
// The captured group ($1) preserves whichever mark was present.
|
|
71
|
+
text = text.replace(
|
|
72
|
+
new RegExp(TSHEG + '([' + SHAD + NYIS_SHAD + GTER_MA + '])', 'g'),
|
|
73
|
+
TSHEG_NONBREAK + '$1'
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Step 2: Protect double-shad from splitting
|
|
77
|
+
text = text.replace(
|
|
78
|
+
new RegExp(SHAD + ' ' + SHAD, 'g'),
|
|
79
|
+
SHAD + WJ + ' ' + WJ + SHAD
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Step 3: Insert zero-width space after tsheg for line-break opportunities
|
|
83
|
+
text = text.replace(
|
|
84
|
+
new RegExp(TSHEG + '(?!' + ZWS + ')', 'g'),
|
|
85
|
+
TSHEG + ZWS
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (text !== node.textContent) {
|
|
89
|
+
node.textContent = text;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
el.dataset.termaPrepared = 'true';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* prepareAll(selector?)
|
|
98
|
+
*
|
|
99
|
+
* Convenience: prepare all [lang="bo"] elements on the page,
|
|
100
|
+
* or all elements matching the given selector.
|
|
101
|
+
*/
|
|
102
|
+
export function prepareAll(selector) {
|
|
103
|
+
const sel = selector || '[lang="bo"]';
|
|
104
|
+
document.querySelectorAll(sel).forEach(prepare);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* normalize(element)
|
|
109
|
+
*
|
|
110
|
+
* Applies Unicode NFC normalization to all text nodes inside the element.
|
|
111
|
+
*/
|
|
112
|
+
export function normalize(el) {
|
|
113
|
+
if (!el) return;
|
|
114
|
+
if (el.dataset.termaNormalized) return;
|
|
115
|
+
|
|
116
|
+
walkTextNodes(el, (node) => {
|
|
117
|
+
const normalized = node.textContent.normalize('NFC');
|
|
118
|
+
if (normalized !== node.textContent) {
|
|
119
|
+
node.textContent = normalized;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
el.dataset.termaNormalized = 'true';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* normalizeAll(selector?)
|
|
128
|
+
*
|
|
129
|
+
* Convenience: normalize all [lang="bo"] elements on the page,
|
|
130
|
+
* or all elements matching the given selector.
|
|
131
|
+
*/
|
|
132
|
+
export function normalizeAll(selector) {
|
|
133
|
+
const sel = selector || '[lang="bo"]';
|
|
134
|
+
document.querySelectorAll(sel).forEach(normalize);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const terma = { prepare, prepareAll, normalize, normalizeAll };
|
|
138
|
+
|
|
139
|
+
export default terma;
|
package/dist/terma.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
terma.js v0.1.0
|
|
3
|
+
Tibetan text processing utilities for termaUI.
|
|
4
|
+
Handles line-breaking, justification, and punctuation protection
|
|
5
|
+
that CSS alone cannot solve. Required for correct Tibetan line breaking.
|
|
6
|
+
|
|
7
|
+
Usage (browser script tag — sets window.terma global):
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/termaui/dist/terma.js"></script>
|
|
9
|
+
<script>terma.prepareAll();</script>
|
|
10
|
+
|
|
11
|
+
Usage (bundler — import from package):
|
|
12
|
+
See terma.esm.js for ESM named/default exports.
|
|
13
|
+
========================================================================== */
|
|
14
|
+
|
|
15
|
+
const terma = (() => {
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
// Tibetan Unicode constants
|
|
19
|
+
const TSHEG = '\u0F0B'; // ་ intersyllabic separator
|
|
20
|
+
const TSHEG_NONBREAK = '\u0F0C'; // ༌ non-breaking tsheg
|
|
21
|
+
const SHAD = '\u0F0D'; // ། clause/sentence marker
|
|
22
|
+
const NYIS_SHAD = '\u0F0E'; // ༎ double shad (single char)
|
|
23
|
+
const GTER_MA = '\u0F14'; // ༔ terma sign
|
|
24
|
+
const VISARGA = '\u0F7F'; // ཿ visarga
|
|
25
|
+
const ZWS = '\u200B'; // zero-width space (break opportunity)
|
|
26
|
+
const WJ = '\u2060'; // word joiner (prevents break)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Walk all text nodes inside an element.
|
|
30
|
+
*/
|
|
31
|
+
function walkTextNodes(el, callback) {
|
|
32
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
33
|
+
const nodes = [];
|
|
34
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
35
|
+
// Process in reverse to avoid offset issues from mutations
|
|
36
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
37
|
+
callback(nodes[i]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* prepare(element)
|
|
43
|
+
*
|
|
44
|
+
* Processes Tibetan text inside the given element:
|
|
45
|
+
*
|
|
46
|
+
* 1. Replaces tsheg before any clause-ending mark with non-breaking tsheg (༌)
|
|
47
|
+
* to prevent bad breaks. Covers:
|
|
48
|
+
* - shad (། U+0F0D) — standard clause marker
|
|
49
|
+
* - nyis-shad (༎ U+0F0E) — double clause marker
|
|
50
|
+
* - gter-ma (༔ U+0F14) — terma / treasure-text sign
|
|
51
|
+
* Example: ང་། → ང༌། | ར་༎ → ར༌༎ | ར་༔ → ར༌༔
|
|
52
|
+
*
|
|
53
|
+
* 2. Wraps double-shad sequences (། །) with word-joiners so they
|
|
54
|
+
* never split across lines.
|
|
55
|
+
*
|
|
56
|
+
* 3. Inserts zero-width spaces after tsheg (་) to give browsers
|
|
57
|
+
* proper line-break opportunities — the #1 Tibetan rendering fix.
|
|
58
|
+
*
|
|
59
|
+
* Call this after the DOM is ready. Safe to call multiple times —
|
|
60
|
+
* already-processed elements are skipped.
|
|
61
|
+
*/
|
|
62
|
+
function prepare(el) {
|
|
63
|
+
if (!el) return;
|
|
64
|
+
if (el.dataset.termaPrepared) return;
|
|
65
|
+
|
|
66
|
+
walkTextNodes(el, (node) => {
|
|
67
|
+
let text = node.textContent;
|
|
68
|
+
|
|
69
|
+
// Step 1: Replace tsheg before clause-ending marks with non-breaking tsheg.
|
|
70
|
+
// Covers shad (།), nyis-shad (༎), and gter-ma (༔).
|
|
71
|
+
// The captured group ($1) preserves whichever mark was present.
|
|
72
|
+
text = text.replace(
|
|
73
|
+
new RegExp(TSHEG + '([' + SHAD + NYIS_SHAD + GTER_MA + '])', 'g'),
|
|
74
|
+
TSHEG_NONBREAK + '$1'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Step 2: Protect double-shad from splitting
|
|
78
|
+
// ། ། → ། ། (word-joiners around the space)
|
|
79
|
+
text = text.replace(
|
|
80
|
+
new RegExp(SHAD + ' ' + SHAD, 'g'),
|
|
81
|
+
SHAD + WJ + ' ' + WJ + SHAD
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Step 3: Insert zero-width space after tsheg for line-break opportunities
|
|
85
|
+
// But NOT after non-breaking tsheg (already protected above)
|
|
86
|
+
text = text.replace(
|
|
87
|
+
new RegExp(TSHEG + '(?!' + ZWS + ')', 'g'),
|
|
88
|
+
TSHEG + ZWS
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (text !== node.textContent) {
|
|
92
|
+
node.textContent = text;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
el.dataset.termaPrepared = 'true';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* prepareAll(selector?)
|
|
101
|
+
*
|
|
102
|
+
* Convenience: prepare all [lang="bo"] elements on the page,
|
|
103
|
+
* or all elements matching the given selector.
|
|
104
|
+
*/
|
|
105
|
+
function prepareAll(selector) {
|
|
106
|
+
const sel = selector || '[lang="bo"]';
|
|
107
|
+
document.querySelectorAll(sel).forEach(prepare);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* normalize(element)
|
|
112
|
+
*
|
|
113
|
+
* Applies Unicode NFC normalization to all text nodes inside the element.
|
|
114
|
+
*
|
|
115
|
+
* Why this matters: The same Tibetan glyph can be encoded as different
|
|
116
|
+
* Unicode sequences that look identical but are technically different strings.
|
|
117
|
+
* For example, a composed "OM" character vs. built from individual parts.
|
|
118
|
+
* If your database stores NFC and a user types NFD (or vice versa), searches
|
|
119
|
+
* will silently fail. Normalizing to NFC at render time ensures consistent
|
|
120
|
+
* string comparison.
|
|
121
|
+
*
|
|
122
|
+
* Note: NFC covers most cases but does not reorder characters that were typed
|
|
123
|
+
* in an incorrect logical order (a font/input-method problem). For wrong-order
|
|
124
|
+
* stacks that show dotted circles, fix the source data or input method.
|
|
125
|
+
*
|
|
126
|
+
* Safe to call multiple times — skips already-normalized elements.
|
|
127
|
+
*/
|
|
128
|
+
function normalize(el) {
|
|
129
|
+
if (!el) return;
|
|
130
|
+
if (el.dataset.termaNormalized) return;
|
|
131
|
+
|
|
132
|
+
walkTextNodes(el, (node) => {
|
|
133
|
+
const normalized = node.textContent.normalize('NFC');
|
|
134
|
+
if (normalized !== node.textContent) {
|
|
135
|
+
node.textContent = normalized;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
el.dataset.termaNormalized = 'true';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* normalizeAll(selector?)
|
|
144
|
+
*
|
|
145
|
+
* Convenience: normalize all [lang="bo"] elements on the page,
|
|
146
|
+
* or all elements matching the given selector.
|
|
147
|
+
*/
|
|
148
|
+
function normalizeAll(selector) {
|
|
149
|
+
const sel = selector || '[lang="bo"]';
|
|
150
|
+
document.querySelectorAll(sel).forEach(normalize);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { prepare, prepareAll, normalize, normalizeAll };
|
|
154
|
+
})();
|
|
155
|
+
|
|
156
|
+
// Export for module environments
|
|
157
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
158
|
+
module.exports = terma;
|
|
159
|
+
}
|