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.
Files changed (50) hide show
  1. package/README.md +334 -0
  2. package/dist/terma.esm.js +139 -0
  3. package/dist/terma.js +159 -0
  4. package/dist/termaui.css +1347 -0
  5. package/dist/termaui.min.css +1 -0
  6. package/fonts/babelstone-tibetan-slim.woff2 +0 -0
  7. package/fonts/babelstone-tibetan.woff2 +0 -0
  8. package/fonts/ddc-rinzin.woff2 +0 -0
  9. package/fonts/ddc-uchen.woff2 +0 -0
  10. package/fonts/gangjie-drutsa.woff2 +0 -0
  11. package/fonts/gangjie-uchen.woff2 +0 -0
  12. package/fonts/jamyang-monlam-uchen.woff2 +0 -0
  13. package/fonts/jomolhari-regular.woff2 +0 -0
  14. package/fonts/joyig.woff2 +0 -0
  15. package/fonts/khampa-dedri-bechu.woff2 +0 -0
  16. package/fonts/khampa-dedri-chuyig.woff2 +0 -0
  17. package/fonts/khampa-dedri-drutsa.woff2 +0 -0
  18. package/fonts/misans-tibetan.woff2 +0 -0
  19. package/fonts/monlam-bodyig.woff2 +0 -0
  20. package/fonts/monlam-uni-dutsa1.woff2 +0 -0
  21. package/fonts/monlam-uni-dutsa2.woff2 +0 -0
  22. package/fonts/monlam-uni-ouchan1.woff2 +0 -0
  23. package/fonts/monlam-uni-ouchan2.woff2 +0 -0
  24. package/fonts/monlam-uni-ouchan3.woff2 +0 -0
  25. package/fonts/monlam-uni-ouchan4.woff2 +0 -0
  26. package/fonts/monlam-uni-ouchan5.woff2 +0 -0
  27. package/fonts/monlam-uni-paytsik.woff2 +0 -0
  28. package/fonts/monlam-uni-sans.woff2 +0 -0
  29. package/fonts/monlam-uni-tikrang.woff2 +0 -0
  30. package/fonts/monlam-uni-tiktong.woff2 +0 -0
  31. package/fonts/noto-sans-tibetan.woff2 +0 -0
  32. package/fonts/noto-serif-tibetan-black.woff2 +0 -0
  33. package/fonts/noto-serif-tibetan-bold.woff2 +0 -0
  34. package/fonts/noto-serif-tibetan-extrabold.woff2 +0 -0
  35. package/fonts/noto-serif-tibetan-extralight.woff2 +0 -0
  36. package/fonts/noto-serif-tibetan-light.woff2 +0 -0
  37. package/fonts/noto-serif-tibetan-medium.woff2 +0 -0
  38. package/fonts/noto-serif-tibetan-regular.woff2 +0 -0
  39. package/fonts/noto-serif-tibetan-semibold.woff2 +0 -0
  40. package/fonts/noto-serif-tibetan-thin.woff2 +0 -0
  41. package/fonts/panchen-tsukring.woff2 +0 -0
  42. package/fonts/qomolangma-betsu.woff2 +0 -0
  43. package/fonts/qomolangma-drutsa.woff2 +0 -0
  44. package/fonts/qomolangma-tsutong.woff2 +0 -0
  45. package/fonts/qomolangma-uchen-sarchen.woff2 +0 -0
  46. package/fonts/riwoche-dhodri-yigchen.woff2 +0 -0
  47. package/fonts/sadri-drutsa.woff2 +0 -0
  48. package/fonts/sadri-yigchen.woff2 +0 -0
  49. package/fonts/tibetan-machine-uni.woff2 +0 -0
  50. 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
+ [![npm](https://img.shields.io/npm/v/termaui)](https://www.npmjs.com/package/termaui)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ }