@yashau/thaana 0.0.0-stage → 2026.1023.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/LICENSE +24 -0
- package/README.md +68 -2
- package/dist/auto.d.ts +1 -0
- package/dist/auto.js +13 -0
- package/dist/cdn/thaana.js +7 -0
- package/dist/index.d.ts +180 -0
- package/dist/index.js +187 -0
- package/dist/keyboard-C7ryTCJR.js +338 -0
- package/dist/thaana.iife.js +7 -0
- package/package.json +60 -3
- package/thaana.css +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ibrahim Yashau
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
This license covers the code in this repository. The fonts in fonts/sources and
|
|
24
|
+
apps/web/static/fonts keep their own licenses; see each family's README.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @yashau/thaana
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Everything to make Thaana (Dhivehi) work on the web, in ~3 KB. No dependencies.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i @yashau/thaana
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
React, Svelte or Vue? Use [`@yashau/thaana-react`](https://www.npmjs.com/package/@yashau/thaana-react), [`@yashau/thaana-svelte`](https://www.npmjs.com/package/@yashau/thaana-svelte) or [`@yashau/thaana-vue`](https://www.npmjs.com/package/@yashau/thaana-vue). They re-export everything below.
|
|
10
|
+
|
|
11
|
+
## Keyboard: type Thaana on any keyboard
|
|
12
|
+
|
|
13
|
+
Most people have no Dhivehi keyboard installed. This rewrites Latin keystrokes to Thaana with the standard **Divehi Phonetic** layout (Microsoft's kbddiv1, the one Maldivian sites use): `h`→ހ, `a`→ަ, `q`→ް, `w`→އ, `,`→،, `?`→؟.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { attachKeyboard } from '@yashau/thaana';
|
|
17
|
+
|
|
18
|
+
let dv = true; // e.g. bound to a DV/EN toggle
|
|
19
|
+
const detach = attachKeyboard(document.querySelector('#name'), { enabled: () => dv });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or with no JS of your own:
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<script type="module">
|
|
26
|
+
import '@yashau/thaana/auto';
|
|
27
|
+
</script>
|
|
28
|
+
<input data-thaana />
|
|
29
|
+
<textarea data-thaana></textarea>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or with no build step at all:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script src="https://dvfonts.yashau.com/thaana.iife.js"></script>
|
|
36
|
+
<!-- window.Thaana -->
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Only single typed characters are rewritten. Paste, IME composition, autocorrect, deletion and real Dhivehi keyboards pass through.
|
|
40
|
+
- Inserts through the browser's editing command, so `input` events fire (controlled inputs keep working) and undo works.
|
|
41
|
+
- Sets `dir="rtl"`, `lang="dv"` and turns off autocorrect/autocapitalize/spellcheck unless you set them, then keeps `dir` in step with the text (RTL if it contains any Thaana, LTR only for pure Latin; `autoDirection: false` to opt out).
|
|
42
|
+
- Bracket pairs are swapped like Microsoft's layout: press the key that looks right in RTL (`)` to open on the right) and the stored text keeps `(` before the enclosed text, which the browser mirrors.
|
|
43
|
+
- Rich-text editors (Lexical, TipTap, ProseMirror): call `thaanaForInputEvent(event)` in the editor's `beforeinput` hook and insert the result through the editor's API.
|
|
44
|
+
|
|
45
|
+
## Text helpers
|
|
46
|
+
|
|
47
|
+
| Function | What it's for |
|
|
48
|
+
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `transliterate('divehi')` | `"ދިވެހި"`: replays keystrokes in the layout. |
|
|
50
|
+
| `convertLegacy(text, { layout })` | Converts text typed in old pre-Unicode A_ fonts (it looks like `ihevid`) to Unicode. |
|
|
51
|
+
| `repairPdfText(text)` | Fixes Thaana copied out of PDFs: reversed dates/times, stray spaces, jumped list markers. |
|
|
52
|
+
| `normalizePunctuation(text)` | `"ބަސް ، ބުދަ ?"` → `"ބަސް، ބުދަ؟"`, tightens brackets. |
|
|
53
|
+
| `isolateNumbers(text)` | Keeps `-5`, `+960 333-1234`, `9:00`, `1,000`, `50%` readable inside RTL text. Display only; the framework packages wrap it in `<ThaanaText>`. |
|
|
54
|
+
| `isolate(value)` | Wraps a URL, email or name so it can't scramble the sentence around it. |
|
|
55
|
+
| `stripBidiControls(text)` | Removes bidi controls before saving or comparing. |
|
|
56
|
+
| `hasThaana`, `hasRtl`, `isMixedScript`, `detectDirection`, `resolveDirection` | Script and direction detection. |
|
|
57
|
+
| `fontStack('Faruma')` | `'Faruma', system-ui, 'Noto Sans Thaana', 'MV Boli', sans-serif` |
|
|
58
|
+
| `loadFonts({ family: 'Faruma' })` | Loads a font from [dvfonts](https://dvfonts.yashau.com) and resolves when it can be drawn. |
|
|
59
|
+
| `PANGRAMS`, `LETTERS`, `FILI`, … | Sample text and the alphabet. |
|
|
60
|
+
|
|
61
|
+
## CSS defaults
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import '@yashau/thaana/thaana.css';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Applies to `:lang(dv)`: RTL, line height that doesn't clip fili, no synthetic italics/bold, `unicode-bidi: plaintext` for user content, LTR for code/URLs/emails.
|
|
68
|
+
|
|
69
|
+
Guide: https://dvfonts.yashau.com/docs · Agent guide: https://dvfonts.yashau.com/llms.txt
|
package/dist/auto.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startAutoAttach(): void;
|
package/dist/auto.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { n as autoAttach } from "./keyboard-C7ryTCJR.js";
|
|
2
|
+
// src/auto.ts
|
|
3
|
+
/** Attach to [data-thaana] fields once the DOM is ready. Called explicitly by
|
|
4
|
+
* the CDN/IIFE builds so a bundler can't tree-shake it away. */
|
|
5
|
+
let started = false;
|
|
6
|
+
function startAutoAttach() {
|
|
7
|
+
if (started || typeof document === "undefined") return;
|
|
8
|
+
started = true;
|
|
9
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => autoAttach(), { once: true });
|
|
10
|
+
else autoAttach();
|
|
11
|
+
}
|
|
12
|
+
startAutoAttach();
|
|
13
|
+
export { startAutoAttach };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const e=Object.freeze({q:`ް`,w:`އ`,e:`ެ`,r:`ރ`,t:`ތ`,y:`ޔ`,u:`ު`,i:`ި`,o:`ޮ`,p:`ޕ`,a:`ަ`,s:`ސ`,d:`ދ`,f:`ފ`,g:`ގ`,h:`ހ`,j:`ޖ`,k:`ކ`,l:`ލ`,z:`ޒ`,x:`×`,c:`ޗ`,v:`ވ`,b:`ބ`,n:`ނ`,m:`މ`,Q:`ޤ`,W:`ޢ`,E:`ޭ`,R:`ޜ`,T:`ޓ`,Y:`ޠ`,U:`ޫ`,I:`ީ`,O:`ޯ`,P:`÷`,A:`ާ`,S:`ށ`,D:`ޑ`,F:`ﷲ`,G:`ޣ`,H:`ޙ`,J:`ޛ`,K:`ޚ`,L:`ޅ`,Z:`ޡ`,X:`ޘ`,C:`ޝ`,V:`ޥ`,B:`ޞ`,N:`ޏ`,M:`ޟ`,",":`،`,";":`؛`,"?":`؟`,"(":`)`,")":`(`,"[":`]`,"]":`[`,"{":`}`,"}":`{`,"<":`>`,">":`<`});function t(t){let n=``;for(let r of t)n+=e[r]??r;return n}function n(e){return e>=1424&&e<=2303||e>=64285&&e<=65023||e>=65136&&e<=65278||e>=67584&&e<=69631||e>=124928&&e<=126975}function r(e){return e>=65&&e<=90||e>=97&&e<=122||e>=192&&e<=687&&e!==215&&e!==247||e>=880&&e<=1023||e>=1024&&e<=1279}const i=/[ހ-]/u;function a(e){return i.test(e)}function o(e){for(let t of e)if(n(t.codePointAt(0)))return!0;return!1}function s(e){let t=!1,i=!1;for(let a of e){let e=a.codePointAt(0);if(n(e)?t=!0:r(e)&&(i=!0),t&&i)return!0}return!1}function c(e){for(let t of e){let e=t.codePointAt(0);if(n(e))return`rtl`;if(r(e))return`ltr`}return`neutral`}function l(e,t,n=`ltr`){return t||(o(e)?`rtl`:c(e)===`ltr`?`ltr`:n)}const u=``,d=``,f=``,p=``;function m(e,t=`ltr`){return(t===`ltr`?``:t===`rtl`?``:``)+e+``}const h=/(^|[\s\p{P}\p{S}])(\+\d[\d -]*\d|[+\-\u2212]?\d(?:[\d.,:/-]*\d)?%?)(?=$|[\s\p{P}\p{S}])/gu;function g(e,t=o(e)){return t?e.replace(h,(e,t,n)=>`${t}${n}`):e}const _=/[--]/gu;function v(e){return e.replace(_,``)}const y=`[\\d\\u0590-\\u08FF\\uFB1D-\\uFDFF\\uFE70-\\uFEFE]`,b=/^[\p{Ps}\p{Pi}]$/u,x=RegExp(`(\\p{P})\\s+(?=${y})`,`gu`),S=RegExp(`(${y})\\s+(\\p{P})`,`gu`);function C(e,t={}){let{arabicPunctuation:n=!0}=t,r=e;return n&&(r=r.replace(/([ހ-])(\s*),/gu,`$1$2،`).replace(/([ހ-])(\s*);/gu,`$1$2؛`).replace(/([ހ-])(\s*)\?/gu,`$1$2؟`)),r.split(`
|
|
2
|
+
`).map(e=>e.replace(x,(e,t)=>b.test(t)?t:e).replace(S,(e,t,n)=>b.test(n)?e:`${t}${n}`)).join(`
|
|
3
|
+
`)}function w(e){return e.split(`
|
|
4
|
+
`).map(e=>{if(!a(e))return e;let t=e.replace(/([/:])\s+(?=\d)/gu,`$1`);t=t.replace(/\d+(?:[/:]\d+)+/gu,e=>{let t=new Set(e.match(/[/:]/gu));if(t.size!==1)return e;let[n]=t;return e.split(n).toReversed().join(n)}),t=C(t,{arabicPunctuation:!1}),t=t.replace(/^(\s*)\.(\d)(\d)(\s+)/u,(e,t,n,r,i)=>`${t}${r}-${n}${i}`);let n=/^(\s*)(\d+)(\s+)([\s\S]*?)(\s*)\.$/u.exec(t);if(n&&a(n[4])){let[,e,r,i,a,o]=n;t=`${e}.${r}${i}${a}${o}`}return t}).join(`
|
|
5
|
+
`)}const T=[`Noto Sans Thaana`,`MV Boli`];function E(e,{latin:t=`system-ui`}={}){return[e,t,...T,`sans-serif`].map(D).join(`, `)}function D(e){return/^[a-z-]+$/.test(e)?e:`'${e}'`}function O(t){if(t.inputType!==`insertText`||t.isComposing)return null;let n=t.data&&t.data.length===1?t.data:void 0;return n?e[n]??null:null}function k(e,t={}){let{enabled:n=()=>!0,setAttributes:r=!0,autoDirection:i=!0}=t;if(r)for(let[t,n]of[[`dir`,`rtl`],[`lang`,`dv`],[`autocomplete`,`off`],[`autocorrect`,`off`],[`autocapitalize`,`none`],[`spellcheck`,`false`]])e.hasAttribute(t)||e.setAttribute(t,n);let a=!1,o=t=>{let r=t;if(a||!n())return;let i=O(r);if(i){r.preventDefault(),a=!0;try{if(typeof document.execCommand==`function`&&document.execCommand(`insertText`,!1,i))return;if(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement){let t=e.selectionStart??e.value.length;e.setRangeText(i,t,e.selectionEnd??t,`end`),e.dispatchEvent(new Event(`input`,{bubbles:!0}))}}finally{a=!1}}},s=()=>e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement?e.value:e.textContent??``,c=()=>{e.dir=l(s(),null,`rtl`)};return e.addEventListener(`beforeinput`,o),i&&(e.addEventListener(`input`,c),c()),()=>{e.removeEventListener(`beforeinput`,o),e.removeEventListener(`input`,c)}}const A=/* @__PURE__ */ new WeakMap;function j(e=document){let t=()=>{for(let t of e.querySelectorAll(`[data-thaana]`))A.has(t)||t.dataset.thaana===`off`||A.set(t,k(t))};t();let n=new MutationObserver(t);return n.observe(e instanceof Document?e.documentElement:e,{childList:!0,subtree:!0}),()=>n.disconnect()}let M=!1;function N(){M||typeof document>`u`||(M=!0,document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>j(),{once:!0}):j())}N();const P={typewriter:{a:`އ`,b:`ބ`,c:`ް`,d:`ދ`,e:`ެ`,f:`ފ`,g:`ގ`,h:`ހ`,i:`ި`,j:`ޖ`,k:`ކ`,l:`ލ`,m:`މ`,n:`ނ`,o:`ޮ`,p:`ޕ`,q:`ޤ`,r:`ރ`,s:`ސ`,t:`ތ`,u:`ު`,v:`ވ`,w:`ަ`,x:`ޝ`,y:`ޔ`,z:`ޒ`,A:`ޢ`,B:`ޞ`,C:`ޗ`,D:`ޑ`,E:`ޭ`,F:`ޟ`,G:`ޣ`,H:`ޙ`,I:`ީ`,J:`ޛ`,K:`ޚ`,L:`ޅ`,N:`ޏ`,O:`ޯ`,R:`ޜ`,S:`ށ`,T:`ޓ`,U:`ޫ`,V:`ޥ`,W:`ާ`,X:`ޘ`,Y:`ޠ`,Z:`ޡ`,Q:`ﷲ`,",":`،`,";":`؛`,"?":`؟`},phonetic:{q:`ް`,w:`އ`,e:`ެ`,r:`ރ`,t:`ތ`,y:`ޔ`,u:`ު`,i:`ި`,o:`ޮ`,p:`ޕ`,a:`ަ`,s:`ސ`,d:`ދ`,f:`ފ`,g:`ގ`,h:`ހ`,j:`ޖ`,k:`ކ`,l:`ލ`,z:`ޒ`,c:`ޗ`,v:`ވ`,b:`ބ`,n:`ނ`,m:`މ`,Q:`ޤ`,W:`ޢ`,E:`ޭ`,R:`ޜ`,T:`ޓ`,Y:`ޠ`,U:`ޫ`,I:`ީ`,O:`ޯ`,A:`ާ`,S:`ށ`,D:`ޑ`,F:`ﷲ`,G:`ޣ`,H:`ޙ`,J:`ޛ`,K:`ޚ`,L:`ޅ`,Z:`ޡ`,X:`ޘ`,C:`ޝ`,V:`ޥ`,B:`ޞ`,N:`ޏ`,M:`ޟ`,",":`،`,";":`؛`,"?":`؟`}};function F(e,t={}){let{layout:n=`typewriter`,reverse:r=!0}=t,i=P[n];return e.split(/(\r?\n)/).map(e=>{if(e===`
|
|
6
|
+
`||e===`\r
|
|
7
|
+
`)return e;let t=(r?[...e].toReversed():[...e]).map(e=>i[e]??e).join(``);return r&&(t=t.replace(/\d[\d.,:/-]*\d|\d/g,e=>[...e].toReversed().join(``))),t}).join(``)}const I=`https://dvfonts.yashau.com`;function L(e,{display:t=`swap`,origin:n=I}={}){return`${n}/css2?${(Array.isArray(e)?e:[e]).map(({family:e,weights:t})=>`family=${encodeURIComponent(e).replace(/%20/g,`+`)}${t?.length?`:wght@${t.toSorted((e,t)=>e-t).join(`;`)}`:``}`).join(`&`)}&display=${t}`}function R(e,t){let n=L(e,t),r=document.querySelector(`link[data-dvfonts][href="${CSS.escape(n)}"]`);r||(r=document.createElement(`link`),r.rel=`stylesheet`,r.href=n,r.dataset.dvfonts=``,document.head.append(r));let i=Array.isArray(e)?e:[e];return new Promise((e,t)=>{if(r.sheet)return e();r.addEventListener(`load`,()=>e(),{once:!0}),r.addEventListener(`error`,()=>t(/* @__PURE__ */ Error(`Could not load ${n}`)),{once:!0})}).then(()=>Promise.all(i.flatMap(({family:e,weights:t=[400]})=>t.map(t=>document.fonts.load(`${t} 16px "${e}"`,`ދ`)))).then(()=>void 0))}const z=[`ޗާލު ޒުވާން ކުއްޖާ، ފޮށިން ޕެކެޓެއް ކަނޑައި، ޏަމްޏަމް ގަހުގައި އިން ބުޅަލަށް މަސްދީ، ރީތި ލަވައެއް ކިޔައިދިނެވެ.`,`ކުޑަ ޗުއްޓީއެއްގައި ޏ. އަތޮޅަށް ފުރަން ޓިކެޓާއި ބޮޑު ފޮށި ހިފައިގެން، ޒުވާން ޕައިލެޓް މަޖާ ލަވައެއް ސަކަރާތާއެކު ކިޔަމުން ދިޔައެވެ.`,`ޕާކުގައި ހުރި މޭޒުމަތީގައި، ކުޑަ ކުއްޖާ ފޮނި ޏަމްޏަމް މޭވާއާއި ޗޮކްލެޓް ބަހައްޓައި، ޅަ އެކުވެރިޔާއަށް ތަށިން ސައި ދިނެވެ.`,`ސުކޫލު ކުލާހުގައި ހުރި ބޯޑުގައި، މޮޅު ކުއްޖާ ތާނަ އަކުރު 'ޏަވިޔަނި' ވަރަށް ޗާލުކޮށް ޕެންސިލުން ފޮތުގައި ލިޔެ، ޒުވާން ޓީޗަރަށް ދެއްކިއެވެ.`,`ބާޒާރުން ތާޒާ ޏަމްޏަމް މޭވާއާއި، ޗީޒް، ޓޮމާޓޯ އަދި ފޮނި ޖޭމު ޑަބިޔަލެއް ގަނެގެން، ކުއްޖާ ޕްލާސްޓިކް ކޮތަޅަށް އަޅައިގެން އަވަހަށް ހިނގައިގަތެވެ.`,`ރީތި ޗާލު ހަނދުވަރުގައި، ޏ. އަތޮޅުގެ ގޮނޑުދޮށުގައި ޒުވާނުން ބޮޑުބެރު ޖަހައި، ޕާޓީއެއް ހަދައި، ފާނޫޒު ދިއްލައިގެން މަޖާ ލަވަތައް ކިޔައި ސަކަރާތްޖެހިއެވެ.`,`ޗަސްބިމުގައި ހުރި ޏަމްޏަމް ގަހުގެ ގޮފިތަކަށް ޖެހި، ޅަ ލޯބި ދޫނި، ކުޑަ ޕިކަޕެއްގައި އައި ޒުވާނުން ޖެހި ޓެންޓު މަތިން އުދުހިގެން ދިޔައެވެ.`,`މީރު ޗޮކްލެޓާއި، ބަޓަރު، ފުށް، ޗީޒް އަދި ޏަމްޏަމް އެއްކޮށް، ބޮޑު ތައްޓަކަށް ސަމުސަލުން ހަކުރު އަޅައިގެން، ޒުވާން ކުއްޖާ ޕޭސްޓްރީ ތައްޔާރުކުރިއެވެ.`],B=`ރަމަޟާން މަހުގެ ޚާއްޞަ ޘަޤާފަތާއި ޝަރީޢަތުގެ ޙަޤީޤަތް ޙިފްޡުކޮށް ޛިކުރުކުރުމަކީ ބަޣާވާތެއް ނޫނެވެ.`,V=(e,t)=>Array.from({length:t-e},(t,n)=>String.fromCodePoint(e+n)),H=V(1920,1944),U=V(1944,1958),W=V(1958,1969);N();export{U as ARABIC_LETTERS,B as ARABIC_LETTERS_SAMPLE,I as DVFONTS_ORIGIN,W as FILI,f as FSI,e as KEYMAP,P as LEGACY_LAYOUTS,H as LETTERS,u as LRI,z as PANGRAMS,p as PDI,d as RLI,T as SYSTEM_THAANA_FONTS,k as attachKeyboard,j as autoAttach,F as convertLegacy,c as detectDirection,L as fontCssUrl,E as fontStack,o as hasRtl,a as hasThaana,s as isMixedScript,m as isolate,g as isolateNumbers,R as loadFonts,C as normalizePunctuation,w as repairPdfText,l as resolveDirection,v as stripBidiControls,O as thaanaForInputEvent,t as transliterate};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Latin key -> Thaana: Microsoft's "Divehi Phonetic" keyboard layout (kbddiv1),
|
|
3
|
+
* the layout Maldivian websites use via aharen/thaana-keyboard
|
|
4
|
+
* (h→ހ, a→ަ, q→ް sukun, w→އ alifu …).
|
|
5
|
+
*
|
|
6
|
+
* Bracket pairs are swapped, as in Microsoft's layout and rihaPDF. Browsers
|
|
7
|
+
* mirror ( ) [ ] { } < > in RTL text, so a Dhivehi typist opens with the key
|
|
8
|
+
* that looks right on screen (")"), which stores U+0028: the glyph matches the
|
|
9
|
+
* key pressed and the stored text stays in logical order.
|
|
10
|
+
*/
|
|
11
|
+
export declare const KEYMAP: Readonly<Record<string, string>>;
|
|
12
|
+
/**
|
|
13
|
+
* Replay keystrokes in the Divehi Phonetic layout: maps character by character.
|
|
14
|
+
* `transliterate('divehi')` → "ދިވެހި". This is not a romanisation converter;
|
|
15
|
+
* pass what a user would type on the layout, not arbitrary English spellings.
|
|
16
|
+
*/
|
|
17
|
+
export declare function transliterate(text: string): string;
|
|
18
|
+
interface KeyboardOptions {
|
|
19
|
+
/** Return false to let keystrokes through untouched (e.g. a DV/EN toggle). Default: always on. */
|
|
20
|
+
enabled?: () => boolean;
|
|
21
|
+
/** Set dir="rtl", lang="dv" and disable autocorrect/autocapitalize/spellcheck
|
|
22
|
+
* unless the element already sets them. Default: true. */
|
|
23
|
+
setAttributes?: boolean;
|
|
24
|
+
/** Keep `dir` in step with the text: RTL when it contains Thaana (or is
|
|
25
|
+
* empty / has no letters), LTR only for pure Latin. Default: true. */
|
|
26
|
+
autoDirection?: boolean;
|
|
27
|
+
}
|
|
28
|
+
type Editable = HTMLInputElement | HTMLTextAreaElement | HTMLElement;
|
|
29
|
+
/**
|
|
30
|
+
* The Thaana character a `beforeinput` event should insert instead, or null to
|
|
31
|
+
* let it through. For rich-text editors (Lexical, TipTap/ProseMirror, Slate…)
|
|
32
|
+
* that own their own insertion: call this in the editor's beforeinput hook,
|
|
33
|
+
* preventDefault, and insert the result through the editor's API.
|
|
34
|
+
* Only single typed characters are mapped; IME composition, paste,
|
|
35
|
+
* autocorrect and deletes return null.
|
|
36
|
+
*/
|
|
37
|
+
export declare function thaanaForInputEvent(event: InputEvent): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Make an <input>, <textarea> or contenteditable element type Thaana on any
|
|
40
|
+
* keyboard. Only single typed characters are rewritten; paste, IME
|
|
41
|
+
* composition, autocorrect, deletion and text from a real Dhivehi keyboard
|
|
42
|
+
* pass through. Inserts via the browser's own editing command, so `input`
|
|
43
|
+
* events fire (React/Vue/Svelte bindings keep working) and undo works.
|
|
44
|
+
*
|
|
45
|
+
* @returns a function that detaches the keyboard.
|
|
46
|
+
*/
|
|
47
|
+
export declare function attachKeyboard(el: Editable, options?: KeyboardOptions): () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Attach the keyboard to every `[data-thaana]` element under `root`, now and as
|
|
50
|
+
* elements are added later. `data-thaana="off"` opts an element out.
|
|
51
|
+
* @returns a function that stops watching (already-attached fields stay attached).
|
|
52
|
+
*/
|
|
53
|
+
export declare function autoAttach(root?: ParentNode): () => void;
|
|
54
|
+
type LegacyLayout = "typewriter" | "phonetic";
|
|
55
|
+
/** ASCII -> Unicode maps for pre-Unicode Thaana fonts.
|
|
56
|
+
* typewriter: the "A_" fonts (A_Waheed, A_Faseyha, A_Randhoo, A_Haleem …).
|
|
57
|
+
* phonetic: a few older casual fonts (Thaana, Akko, Athu Casual, Avas Thaana …). */
|
|
58
|
+
export declare const LEGACY_LAYOUTS: Readonly<Record<LegacyLayout, Readonly<Record<string, string>>>>;
|
|
59
|
+
interface ConvertLegacyOptions {
|
|
60
|
+
/** Which legacy font family the text was typed for. Default: 'typewriter' (A_ fonts). */
|
|
61
|
+
layout?: LegacyLayout;
|
|
62
|
+
/** Legacy documents store each line in visual (left-to-right) order. Reverse
|
|
63
|
+
* lines back to logical order. Default: true. */
|
|
64
|
+
reverse?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Convert text typed in a pre-Unicode Thaana font (it looks like Latin
|
|
68
|
+
* gibberish such as "ihevid" once the font is gone) into Unicode Thaana.
|
|
69
|
+
* Each line is reversed into logical order, characters are mapped, and digit
|
|
70
|
+
* groups are put back in reading order.
|
|
71
|
+
*/
|
|
72
|
+
export declare function convertLegacy(text: string, options?: ConvertLegacyOptions): string;
|
|
73
|
+
/** True if the string contains any Thaana. */
|
|
74
|
+
export declare function hasThaana(text: string): boolean;
|
|
75
|
+
/** True if the string contains any right-to-left letter (Thaana, Arabic, Hebrew…). */
|
|
76
|
+
export declare function hasRtl(text: string): boolean;
|
|
77
|
+
/** True if the string mixes right-to-left letters with Latin/Greek/Cyrillic
|
|
78
|
+
* letters. Digits, spaces and punctuation don't count, so "ދިވެހި 123" is not mixed. */
|
|
79
|
+
export declare function isMixedScript(text: string): boolean;
|
|
80
|
+
/** Direction of the first strong character, the same rule as dir="auto". */
|
|
81
|
+
export declare function detectDirection(text: string): "rtl" | "ltr" | "neutral";
|
|
82
|
+
/**
|
|
83
|
+
* Direction to render a piece of text with (rihaPDF's rule). An explicit
|
|
84
|
+
* choice always wins; otherwise any right-to-left letter makes it RTL. This
|
|
85
|
+
* suits Dhivehi UIs better than dir="auto", which turns "Apple ގެ ފޯނު"
|
|
86
|
+
* left-to-right because the first letter is Latin. Text with no letters at all
|
|
87
|
+
* (digits, punctuation) gets `fallback`: pass 'rtl' in Thaana contexts.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveDirection(text: string, explicit?: "rtl" | "ltr" | null, fallback?: "rtl" | "ltr"): "rtl" | "ltr";
|
|
90
|
+
/** Unicode bidi isolates. Prefer the HTML <bdi> element when you control markup. */
|
|
91
|
+
export declare const LRI = "";
|
|
92
|
+
export declare const RLI = "";
|
|
93
|
+
export declare const FSI = "";
|
|
94
|
+
export declare const PDI = "";
|
|
95
|
+
/**
|
|
96
|
+
* Wrap a value in a directional isolate so it can't scramble the sentence
|
|
97
|
+
* around it: phone numbers, URLs, emails, code or user names inside Thaana
|
|
98
|
+
* text. For plain strings (titles, notifications, canvas, SVG) where you can't
|
|
99
|
+
* use <bdi> or dir="ltr".
|
|
100
|
+
*/
|
|
101
|
+
export declare function isolate(value: string, direction?: "ltr" | "rtl" | "auto"): string;
|
|
102
|
+
/**
|
|
103
|
+
* Wrap numbers in right-to-left text in left-to-right isolates, so "-5",
|
|
104
|
+
* "+960 333-1234", "9:00", "1,000" and "50%" keep their order inside Thaana.
|
|
105
|
+
* Brackets around a number stay outside the isolate (putting them inside makes
|
|
106
|
+
* the closing bracket jump). No-op for text without right-to-left letters.
|
|
107
|
+
* Strip the controls again with stripBidiControls() before saving.
|
|
108
|
+
*/
|
|
109
|
+
export declare function isolateNumbers(text: string, rtl?: boolean): string;
|
|
110
|
+
/** Remove bidi control characters (before saving, searching or comparing text). */
|
|
111
|
+
export declare function stripBidiControls(text: string): string;
|
|
112
|
+
interface NormalizePunctuationOptions {
|
|
113
|
+
/** Turn Latin , ; ? that follow Thaana into ، ؛ ؟. Default: true. */
|
|
114
|
+
arabicPunctuation?: boolean;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Tidy spacing and punctuation in Thaana text:
|
|
118
|
+
* "ބަސް ، ބުދަ ؛ އެއް ؟ ނޫން !" → "ބަސް، ބުދަ؛ އެއް؟ ނޫން!"
|
|
119
|
+
* "ދުވަސް ( ބުރުނު ) [ 8 ]" → "ދުވަސް (ބުރުނު) [8]"
|
|
120
|
+
* Spaces before punctuation go, spaces just inside opening brackets/quotes go,
|
|
121
|
+
* and the space before an opening bracket is kept.
|
|
122
|
+
*/
|
|
123
|
+
export declare function normalizePunctuation(text: string, options?: NormalizePunctuationOptions): string;
|
|
124
|
+
/**
|
|
125
|
+
* Repair Thaana text copied out of a PDF. PDFs store right-to-left text in
|
|
126
|
+
* visual order, so after copy/paste dates and times come out reversed
|
|
127
|
+
* ("2026/1/14" for 14/1/2026, "00:9" for 9:00), punctuation picks up stray
|
|
128
|
+
* spaces, numbered-list markers jump to the end ("1 ބައި." for ".1 ބައި") and
|
|
129
|
+
* "1.2" section markers arrive as ".12". Only lines containing Thaana are
|
|
130
|
+
* touched. Do NOT run this on text people typed: it would reverse their dates.
|
|
131
|
+
*/
|
|
132
|
+
export declare function repairPdfText(text: string): string;
|
|
133
|
+
/** Thaana fonts installed on common systems, used as a fallback for glyphs a
|
|
134
|
+
* web font lacks: MV Boli ships with Windows, Noto Sans Thaana with Android
|
|
135
|
+
* and most Linux desktops. */
|
|
136
|
+
export declare const SYSTEM_THAANA_FONTS: readonly string[];
|
|
137
|
+
/**
|
|
138
|
+
* A font-family value that keeps both scripts readable: most Dhivehi fonts have
|
|
139
|
+
* no Latin letters, so Latin falls through to the system UI font, and missing
|
|
140
|
+
* Thaana falls through to an installed Thaana font.
|
|
141
|
+
*
|
|
142
|
+
* fontStack('Faruma') → "'Faruma', system-ui, 'Noto Sans Thaana', 'MV Boli', sans-serif"
|
|
143
|
+
*/
|
|
144
|
+
export declare function fontStack(family: string, { latin }?: {
|
|
145
|
+
latin?: string;
|
|
146
|
+
}): string;
|
|
147
|
+
/** Load Dhivehi fonts from dvfonts (Google-Fonts-compatible CSS API). */
|
|
148
|
+
export declare const DVFONTS_ORIGIN = "https://dvfonts.yashau.com";
|
|
149
|
+
interface FontRequest {
|
|
150
|
+
/** Family name, e.g. "Faruma" or "MV Faseyha" (old names like "A_Waheed" work too). */
|
|
151
|
+
family: string;
|
|
152
|
+
/** Weights to load. Default: the family's default weight. */
|
|
153
|
+
weights?: number[];
|
|
154
|
+
}
|
|
155
|
+
/** URL of the CSS for one or more families. */
|
|
156
|
+
export declare function fontCssUrl(fonts: FontRequest | FontRequest[], { display, origin }?: {
|
|
157
|
+
display?: string;
|
|
158
|
+
origin?: string;
|
|
159
|
+
}): string;
|
|
160
|
+
/**
|
|
161
|
+
* Add the stylesheet for the given families to <head> (once per URL) and
|
|
162
|
+
* resolve when the fonts are ready to draw with. Use the family name in CSS
|
|
163
|
+
* afterwards: font-family: 'Faruma', system-ui, sans-serif.
|
|
164
|
+
*/
|
|
165
|
+
export declare function loadFonts(fonts: FontRequest | FontRequest[], options?: {
|
|
166
|
+
display?: string;
|
|
167
|
+
origin?: string;
|
|
168
|
+
}): Promise<void>;
|
|
169
|
+
/** Sample sentences that each use all 24 native Thaana letters. Shown at
|
|
170
|
+
* random on dvfonts. */
|
|
171
|
+
export declare const PANGRAMS: readonly string[];
|
|
172
|
+
/** Uses 12 of the 14 Arabic-derived letters (ޜ and ޥ have no common words). */
|
|
173
|
+
export declare const ARABIC_LETTERS_SAMPLE = "ރަމަޟާން މަހުގެ ޚާއްޞަ ޘަޤާފަތާއި ޝަރީޢަތުގެ ޙަޤީޤަތް ޙިފްޡުކޮށް ޛިކުރުކުރުމަކީ ބަޣާވާތެއް ނޫނެވެ.";
|
|
174
|
+
/** The 24 native letters, in alphabet order. */
|
|
175
|
+
export declare const LETTERS: string[];
|
|
176
|
+
/** The 14 letters added for Arabic loanwords. */
|
|
177
|
+
export declare const ARABIC_LETTERS: string[];
|
|
178
|
+
/** Vowel marks (fili), including sukun. */
|
|
179
|
+
export declare const FILI: string[];
|
|
180
|
+
export type { ConvertLegacyOptions, FontRequest, KeyboardOptions, LegacyLayout, NormalizePunctuationOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { _ as repairPdfText, a as LRI, b as KEYMAP, c as SYSTEM_THAANA_FONTS, d as hasRtl, f as hasThaana, g as normalizePunctuation, h as isolateNumbers, i as FSI, l as detectDirection, m as isolate, n as autoAttach, o as PDI, p as isMixedScript, r as thaanaForInputEvent, s as RLI, t as attachKeyboard, u as fontStack, v as resolveDirection, x as transliterate, y as stripBidiControls } from "./keyboard-C7ryTCJR.js";
|
|
2
|
+
// src/legacy.ts
|
|
3
|
+
/** ASCII -> Unicode maps for pre-Unicode Thaana fonts.
|
|
4
|
+
* typewriter: the "A_" fonts (A_Waheed, A_Faseyha, A_Randhoo, A_Haleem …).
|
|
5
|
+
* phonetic: a few older casual fonts (Thaana, Akko, Athu Casual, Avas Thaana …). */
|
|
6
|
+
const LEGACY_LAYOUTS = {
|
|
7
|
+
typewriter: {
|
|
8
|
+
"a": "އ",
|
|
9
|
+
"b": "ބ",
|
|
10
|
+
"c": "ް",
|
|
11
|
+
"d": "ދ",
|
|
12
|
+
"e": "ެ",
|
|
13
|
+
"f": "ފ",
|
|
14
|
+
"g": "ގ",
|
|
15
|
+
"h": "ހ",
|
|
16
|
+
"i": "ި",
|
|
17
|
+
"j": "ޖ",
|
|
18
|
+
"k": "ކ",
|
|
19
|
+
"l": "ލ",
|
|
20
|
+
"m": "މ",
|
|
21
|
+
"n": "ނ",
|
|
22
|
+
"o": "ޮ",
|
|
23
|
+
"p": "ޕ",
|
|
24
|
+
"q": "ޤ",
|
|
25
|
+
"r": "ރ",
|
|
26
|
+
"s": "ސ",
|
|
27
|
+
"t": "ތ",
|
|
28
|
+
"u": "ު",
|
|
29
|
+
"v": "ވ",
|
|
30
|
+
"w": "ަ",
|
|
31
|
+
"x": "ޝ",
|
|
32
|
+
"y": "ޔ",
|
|
33
|
+
"z": "ޒ",
|
|
34
|
+
"A": "ޢ",
|
|
35
|
+
"B": "ޞ",
|
|
36
|
+
"C": "ޗ",
|
|
37
|
+
"D": "ޑ",
|
|
38
|
+
"E": "ޭ",
|
|
39
|
+
"F": "ޟ",
|
|
40
|
+
"G": "ޣ",
|
|
41
|
+
"H": "ޙ",
|
|
42
|
+
"I": "ީ",
|
|
43
|
+
"J": "ޛ",
|
|
44
|
+
"K": "ޚ",
|
|
45
|
+
"L": "ޅ",
|
|
46
|
+
"N": "ޏ",
|
|
47
|
+
"O": "ޯ",
|
|
48
|
+
"R": "ޜ",
|
|
49
|
+
"S": "ށ",
|
|
50
|
+
"T": "ޓ",
|
|
51
|
+
"U": "ޫ",
|
|
52
|
+
"V": "ޥ",
|
|
53
|
+
"W": "ާ",
|
|
54
|
+
"X": "ޘ",
|
|
55
|
+
"Y": "ޠ",
|
|
56
|
+
"Z": "ޡ",
|
|
57
|
+
"Q": "ﷲ",
|
|
58
|
+
",": "،",
|
|
59
|
+
";": "؛",
|
|
60
|
+
"?": "؟"
|
|
61
|
+
},
|
|
62
|
+
phonetic: {
|
|
63
|
+
"q": "ް",
|
|
64
|
+
"w": "އ",
|
|
65
|
+
"e": "ެ",
|
|
66
|
+
"r": "ރ",
|
|
67
|
+
"t": "ތ",
|
|
68
|
+
"y": "ޔ",
|
|
69
|
+
"u": "ު",
|
|
70
|
+
"i": "ި",
|
|
71
|
+
"o": "ޮ",
|
|
72
|
+
"p": "ޕ",
|
|
73
|
+
"a": "ަ",
|
|
74
|
+
"s": "ސ",
|
|
75
|
+
"d": "ދ",
|
|
76
|
+
"f": "ފ",
|
|
77
|
+
"g": "ގ",
|
|
78
|
+
"h": "ހ",
|
|
79
|
+
"j": "ޖ",
|
|
80
|
+
"k": "ކ",
|
|
81
|
+
"l": "ލ",
|
|
82
|
+
"z": "ޒ",
|
|
83
|
+
"c": "ޗ",
|
|
84
|
+
"v": "ވ",
|
|
85
|
+
"b": "ބ",
|
|
86
|
+
"n": "ނ",
|
|
87
|
+
"m": "މ",
|
|
88
|
+
"Q": "ޤ",
|
|
89
|
+
"W": "ޢ",
|
|
90
|
+
"E": "ޭ",
|
|
91
|
+
"R": "ޜ",
|
|
92
|
+
"T": "ޓ",
|
|
93
|
+
"Y": "ޠ",
|
|
94
|
+
"U": "ޫ",
|
|
95
|
+
"I": "ީ",
|
|
96
|
+
"O": "ޯ",
|
|
97
|
+
"A": "ާ",
|
|
98
|
+
"S": "ށ",
|
|
99
|
+
"D": "ޑ",
|
|
100
|
+
"F": "ﷲ",
|
|
101
|
+
"G": "ޣ",
|
|
102
|
+
"H": "ޙ",
|
|
103
|
+
"J": "ޛ",
|
|
104
|
+
"K": "ޚ",
|
|
105
|
+
"L": "ޅ",
|
|
106
|
+
"Z": "ޡ",
|
|
107
|
+
"X": "ޘ",
|
|
108
|
+
"C": "ޝ",
|
|
109
|
+
"V": "ޥ",
|
|
110
|
+
"B": "ޞ",
|
|
111
|
+
"N": "ޏ",
|
|
112
|
+
"M": "ޟ",
|
|
113
|
+
",": "،",
|
|
114
|
+
";": "؛",
|
|
115
|
+
"?": "؟"
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Convert text typed in a pre-Unicode Thaana font (it looks like Latin
|
|
120
|
+
* gibberish such as "ihevid" once the font is gone) into Unicode Thaana.
|
|
121
|
+
* Each line is reversed into logical order, characters are mapped, and digit
|
|
122
|
+
* groups are put back in reading order.
|
|
123
|
+
*/
|
|
124
|
+
function convertLegacy(text, options = {}) {
|
|
125
|
+
const { layout = "typewriter", reverse = true } = options;
|
|
126
|
+
const map = LEGACY_LAYOUTS[layout];
|
|
127
|
+
return text.split(/(\r?\n)/).map((line) => {
|
|
128
|
+
if (line === "\n" || line === "\r\n") return line;
|
|
129
|
+
let out = (reverse ? [...line].toReversed() : [...line]).map((c) => map[c] ?? c).join("");
|
|
130
|
+
if (reverse) out = out.replace(/\d[\d.,:/-]*\d|\d/g, (m) => [...m].toReversed().join(""));
|
|
131
|
+
return out;
|
|
132
|
+
}).join("");
|
|
133
|
+
}
|
|
134
|
+
// src/fonts.ts
|
|
135
|
+
/** Load Dhivehi fonts from dvfonts (Google-Fonts-compatible CSS API). */
|
|
136
|
+
const DVFONTS_ORIGIN = "https://dvfonts.yashau.com";
|
|
137
|
+
/** URL of the CSS for one or more families. */
|
|
138
|
+
function fontCssUrl(fonts, { display = "swap", origin = DVFONTS_ORIGIN } = {}) {
|
|
139
|
+
return `${origin}/css2?${(Array.isArray(fonts) ? fonts : [fonts]).map(({ family, weights }) => {
|
|
140
|
+
return `family=${encodeURIComponent(family).replace(/%20/g, "+")}${weights?.length ? `:wght@${weights.toSorted((a, b) => a - b).join(";")}` : ""}`;
|
|
141
|
+
}).join("&")}&display=${display}`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Add the stylesheet for the given families to <head> (once per URL) and
|
|
145
|
+
* resolve when the fonts are ready to draw with. Use the family name in CSS
|
|
146
|
+
* afterwards: font-family: 'Faruma', system-ui, sans-serif.
|
|
147
|
+
*/
|
|
148
|
+
function loadFonts(fonts, options) {
|
|
149
|
+
const href = fontCssUrl(fonts, options);
|
|
150
|
+
let link = document.querySelector(`link[data-dvfonts][href="${CSS.escape(href)}"]`);
|
|
151
|
+
if (!link) {
|
|
152
|
+
link = document.createElement("link");
|
|
153
|
+
link.rel = "stylesheet";
|
|
154
|
+
link.href = href;
|
|
155
|
+
link.dataset.dvfonts = "";
|
|
156
|
+
document.head.append(link);
|
|
157
|
+
}
|
|
158
|
+
const list = Array.isArray(fonts) ? fonts : [fonts];
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
if (link.sheet) return resolve();
|
|
161
|
+
link.addEventListener("load", () => resolve(), { once: true });
|
|
162
|
+
link.addEventListener("error", () => reject(/* @__PURE__ */ new Error(`Could not load ${href}`)), { once: true });
|
|
163
|
+
}).then(() => Promise.all(list.flatMap(({ family, weights = [400] }) => weights.map((w) => document.fonts.load(`${w} 16px "${family}"`, "ދ")))).then(() => void 0));
|
|
164
|
+
}
|
|
165
|
+
// src/samples.ts
|
|
166
|
+
/** Sample sentences that each use all 24 native Thaana letters. Shown at
|
|
167
|
+
* random on dvfonts. */
|
|
168
|
+
const PANGRAMS = [
|
|
169
|
+
"ޗާލު ޒުވާން ކުއްޖާ، ފޮށިން ޕެކެޓެއް ކަނޑައި، ޏަމްޏަމް ގަހުގައި އިން ބުޅަލަށް މަސްދީ، ރީތި ލަވައެއް ކިޔައިދިނެވެ.",
|
|
170
|
+
"ކުޑަ ޗުއްޓީއެއްގައި ޏ. އަތޮޅަށް ފުރަން ޓިކެޓާއި ބޮޑު ފޮށި ހިފައިގެން، ޒުވާން ޕައިލެޓް މަޖާ ލަވައެއް ސަކަރާތާއެކު ކިޔަމުން ދިޔައެވެ.",
|
|
171
|
+
"ޕާކުގައި ހުރި މޭޒުމަތީގައި، ކުޑަ ކުއްޖާ ފޮނި ޏަމްޏަމް މޭވާއާއި ޗޮކްލެޓް ބަހައްޓައި، ޅަ އެކުވެރިޔާއަށް ތަށިން ސައި ދިނެވެ.",
|
|
172
|
+
"ސުކޫލު ކުލާހުގައި ހުރި ބޯޑުގައި، މޮޅު ކުއްޖާ ތާނަ އަކުރު 'ޏަވިޔަނި' ވަރަށް ޗާލުކޮށް ޕެންސިލުން ފޮތުގައި ލިޔެ، ޒުވާން ޓީޗަރަށް ދެއްކިއެވެ.",
|
|
173
|
+
"ބާޒާރުން ތާޒާ ޏަމްޏަމް މޭވާއާއި، ޗީޒް، ޓޮމާޓޯ އަދި ފޮނި ޖޭމު ޑަބިޔަލެއް ގަނެގެން، ކުއްޖާ ޕްލާސްޓިކް ކޮތަޅަށް އަޅައިގެން އަވަހަށް ހިނގައިގަތެވެ.",
|
|
174
|
+
"ރީތި ޗާލު ހަނދުވަރުގައި، ޏ. އަތޮޅުގެ ގޮނޑުދޮށުގައި ޒުވާނުން ބޮޑުބެރު ޖަހައި، ޕާޓީއެއް ހަދައި، ފާނޫޒު ދިއްލައިގެން މަޖާ ލަވަތައް ކިޔައި ސަކަރާތްޖެހިއެވެ.",
|
|
175
|
+
"ޗަސްބިމުގައި ހުރި ޏަމްޏަމް ގަހުގެ ގޮފިތަކަށް ޖެހި، ޅަ ލޯބި ދޫނި، ކުޑަ ޕިކަޕެއްގައި އައި ޒުވާނުން ޖެހި ޓެންޓު މަތިން އުދުހިގެން ދިޔައެވެ.",
|
|
176
|
+
"މީރު ޗޮކްލެޓާއި، ބަޓަރު، ފުށް، ޗީޒް އަދި ޏަމްޏަމް އެއްކޮށް، ބޮޑު ތައްޓަކަށް ސަމުސަލުން ހަކުރު އަޅައިގެން، ޒުވާން ކުއްޖާ ޕޭސްޓްރީ ތައްޔާރުކުރިއެވެ."
|
|
177
|
+
];
|
|
178
|
+
/** Uses 12 of the 14 Arabic-derived letters (ޜ and ޥ have no common words). */
|
|
179
|
+
const ARABIC_LETTERS_SAMPLE = "ރަމަޟާން މަހުގެ ޚާއްޞަ ޘަޤާފަތާއި ޝަރީޢަތުގެ ޙަޤީޤަތް ޙިފްޡުކޮށް ޛިކުރުކުރުމަކީ ބަޣާވާތެއް ނޫނެވެ.";
|
|
180
|
+
const range = (from, to) => Array.from({ length: to - from }, (_, i) => String.fromCodePoint(from + i));
|
|
181
|
+
/** The 24 native letters, in alphabet order. */
|
|
182
|
+
const LETTERS = range(1920, 1944);
|
|
183
|
+
/** The 14 letters added for Arabic loanwords. */
|
|
184
|
+
const ARABIC_LETTERS = range(1944, 1958);
|
|
185
|
+
/** Vowel marks (fili), including sukun. */
|
|
186
|
+
const FILI = range(1958, 1969);
|
|
187
|
+
export { ARABIC_LETTERS, ARABIC_LETTERS_SAMPLE, DVFONTS_ORIGIN, FILI, FSI, KEYMAP, LEGACY_LAYOUTS, LETTERS, LRI, PANGRAMS, PDI, RLI, SYSTEM_THAANA_FONTS, attachKeyboard, autoAttach, convertLegacy, detectDirection, fontCssUrl, fontStack, hasRtl, hasThaana, isMixedScript, isolate, isolateNumbers, loadFonts, normalizePunctuation, repairPdfText, resolveDirection, stripBidiControls, thaanaForInputEvent, transliterate };
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// src/keymap.ts
|
|
2
|
+
/**
|
|
3
|
+
* Latin key -> Thaana: Microsoft's "Divehi Phonetic" keyboard layout (kbddiv1),
|
|
4
|
+
* the layout Maldivian websites use via aharen/thaana-keyboard
|
|
5
|
+
* (h→ހ, a→ަ, q→ް sukun, w→އ alifu …).
|
|
6
|
+
*
|
|
7
|
+
* Bracket pairs are swapped, as in Microsoft's layout and rihaPDF. Browsers
|
|
8
|
+
* mirror ( ) [ ] { } < > in RTL text, so a Dhivehi typist opens with the key
|
|
9
|
+
* that looks right on screen (")"), which stores U+0028: the glyph matches the
|
|
10
|
+
* key pressed and the stored text stays in logical order.
|
|
11
|
+
*/
|
|
12
|
+
const KEYMAP = Object.freeze({
|
|
13
|
+
q: "ް",
|
|
14
|
+
w: "އ",
|
|
15
|
+
e: "ެ",
|
|
16
|
+
r: "ރ",
|
|
17
|
+
t: "ތ",
|
|
18
|
+
y: "ޔ",
|
|
19
|
+
u: "ު",
|
|
20
|
+
i: "ި",
|
|
21
|
+
o: "ޮ",
|
|
22
|
+
p: "ޕ",
|
|
23
|
+
a: "ަ",
|
|
24
|
+
s: "ސ",
|
|
25
|
+
d: "ދ",
|
|
26
|
+
f: "ފ",
|
|
27
|
+
g: "ގ",
|
|
28
|
+
h: "ހ",
|
|
29
|
+
j: "ޖ",
|
|
30
|
+
k: "ކ",
|
|
31
|
+
l: "ލ",
|
|
32
|
+
z: "ޒ",
|
|
33
|
+
x: "×",
|
|
34
|
+
c: "ޗ",
|
|
35
|
+
v: "ވ",
|
|
36
|
+
b: "ބ",
|
|
37
|
+
n: "ނ",
|
|
38
|
+
m: "މ",
|
|
39
|
+
Q: "ޤ",
|
|
40
|
+
W: "ޢ",
|
|
41
|
+
E: "ޭ",
|
|
42
|
+
R: "ޜ",
|
|
43
|
+
T: "ޓ",
|
|
44
|
+
Y: "ޠ",
|
|
45
|
+
U: "ޫ",
|
|
46
|
+
I: "ީ",
|
|
47
|
+
O: "ޯ",
|
|
48
|
+
P: "÷",
|
|
49
|
+
A: "ާ",
|
|
50
|
+
S: "ށ",
|
|
51
|
+
D: "ޑ",
|
|
52
|
+
F: "ﷲ",
|
|
53
|
+
G: "ޣ",
|
|
54
|
+
H: "ޙ",
|
|
55
|
+
J: "ޛ",
|
|
56
|
+
K: "ޚ",
|
|
57
|
+
L: "ޅ",
|
|
58
|
+
Z: "ޡ",
|
|
59
|
+
X: "ޘ",
|
|
60
|
+
C: "ޝ",
|
|
61
|
+
V: "ޥ",
|
|
62
|
+
B: "ޞ",
|
|
63
|
+
N: "ޏ",
|
|
64
|
+
M: "ޟ",
|
|
65
|
+
",": "،",
|
|
66
|
+
";": "؛",
|
|
67
|
+
"?": "؟",
|
|
68
|
+
"(": ")",
|
|
69
|
+
")": "(",
|
|
70
|
+
"[": "]",
|
|
71
|
+
"]": "[",
|
|
72
|
+
"{": "}",
|
|
73
|
+
"}": "{",
|
|
74
|
+
"<": ">",
|
|
75
|
+
">": "<"
|
|
76
|
+
});
|
|
77
|
+
/**
|
|
78
|
+
* Replay keystrokes in the Divehi Phonetic layout: maps character by character.
|
|
79
|
+
* `transliterate('divehi')` → "ދިވެހި". This is not a romanisation converter;
|
|
80
|
+
* pass what a user would type on the layout, not arbitrary English spellings.
|
|
81
|
+
*/
|
|
82
|
+
function transliterate(text) {
|
|
83
|
+
let out = "";
|
|
84
|
+
for (const ch of text) out += KEYMAP[ch] ?? ch;
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
// src/text.ts
|
|
88
|
+
/**
|
|
89
|
+
* Putting Thaana on screen correctly: script detection, direction, bidi
|
|
90
|
+
* isolation, punctuation clean-up and repair of text copied out of PDFs.
|
|
91
|
+
*
|
|
92
|
+
* Much of this is ported from rihaPDF (github.com/yashau/rihaPDF), where each
|
|
93
|
+
* rule was added for a real document; the tests pin the same cases.
|
|
94
|
+
*/
|
|
95
|
+
/** Strong right-to-left code points: Hebrew, Arabic, Syriac, Thaana, NKo, Arabic
|
|
96
|
+
* presentation forms and the historic RTL scripts. */
|
|
97
|
+
function isRtlCodePoint(cp) {
|
|
98
|
+
return cp >= 1424 && cp <= 2303 || cp >= 64285 && cp <= 65023 || cp >= 65136 && cp <= 65278 || cp >= 67584 && cp <= 69631 || cp >= 124928 && cp <= 126975;
|
|
99
|
+
}
|
|
100
|
+
/** Strong left-to-right letters (Latin, Greek, Cyrillic). Digits and
|
|
101
|
+
* punctuation are direction-neutral and deliberately not included. */
|
|
102
|
+
function isLtrLetter(cp) {
|
|
103
|
+
return cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp >= 192 && cp <= 687 && cp !== 215 && cp !== 247 || cp >= 880 && cp <= 1023 || cp >= 1024 && cp <= 1279;
|
|
104
|
+
}
|
|
105
|
+
const THAANA_RE = /[ހ-]/u;
|
|
106
|
+
/** True if the string contains any Thaana. */
|
|
107
|
+
function hasThaana(text) {
|
|
108
|
+
return THAANA_RE.test(text);
|
|
109
|
+
}
|
|
110
|
+
/** True if the string contains any right-to-left letter (Thaana, Arabic, Hebrew…). */
|
|
111
|
+
function hasRtl(text) {
|
|
112
|
+
for (const ch of text) if (isRtlCodePoint(ch.codePointAt(0))) return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/** True if the string mixes right-to-left letters with Latin/Greek/Cyrillic
|
|
116
|
+
* letters. Digits, spaces and punctuation don't count, so "ދިވެހި 123" is not mixed. */
|
|
117
|
+
function isMixedScript(text) {
|
|
118
|
+
let rtl = false;
|
|
119
|
+
let ltr = false;
|
|
120
|
+
for (const ch of text) {
|
|
121
|
+
const cp = ch.codePointAt(0);
|
|
122
|
+
if (isRtlCodePoint(cp)) rtl = true;
|
|
123
|
+
else if (isLtrLetter(cp)) ltr = true;
|
|
124
|
+
if (rtl && ltr) return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
/** Direction of the first strong character, the same rule as dir="auto". */
|
|
129
|
+
function detectDirection(text) {
|
|
130
|
+
for (const ch of text) {
|
|
131
|
+
const cp = ch.codePointAt(0);
|
|
132
|
+
if (isRtlCodePoint(cp)) return "rtl";
|
|
133
|
+
if (isLtrLetter(cp)) return "ltr";
|
|
134
|
+
}
|
|
135
|
+
return "neutral";
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Direction to render a piece of text with (rihaPDF's rule). An explicit
|
|
139
|
+
* choice always wins; otherwise any right-to-left letter makes it RTL. This
|
|
140
|
+
* suits Dhivehi UIs better than dir="auto", which turns "Apple ގެ ފޯނު"
|
|
141
|
+
* left-to-right because the first letter is Latin. Text with no letters at all
|
|
142
|
+
* (digits, punctuation) gets `fallback`: pass 'rtl' in Thaana contexts.
|
|
143
|
+
*/
|
|
144
|
+
function resolveDirection(text, explicit, fallback = "ltr") {
|
|
145
|
+
if (explicit) return explicit;
|
|
146
|
+
if (hasRtl(text)) return "rtl";
|
|
147
|
+
return detectDirection(text) === "ltr" ? "ltr" : fallback;
|
|
148
|
+
}
|
|
149
|
+
/** Unicode bidi isolates. Prefer the HTML <bdi> element when you control markup. */
|
|
150
|
+
const LRI = "";
|
|
151
|
+
const RLI = "";
|
|
152
|
+
const FSI = "";
|
|
153
|
+
const PDI = "";
|
|
154
|
+
/**
|
|
155
|
+
* Wrap a value in a directional isolate so it can't scramble the sentence
|
|
156
|
+
* around it: phone numbers, URLs, emails, code or user names inside Thaana
|
|
157
|
+
* text. For plain strings (titles, notifications, canvas, SVG) where you can't
|
|
158
|
+
* use <bdi> or dir="ltr".
|
|
159
|
+
*/
|
|
160
|
+
function isolate(value, direction = "ltr") {
|
|
161
|
+
return (direction === "ltr" ? "" : direction === "rtl" ? "" : "") + value + "";
|
|
162
|
+
}
|
|
163
|
+
const NUMBER_TOKEN_RE = /(^|[\s\p{P}\p{S}])(\+\d[\d -]*\d|[+\-\u2212]?\d(?:[\d.,:/-]*\d)?%?)(?=$|[\s\p{P}\p{S}])/gu;
|
|
164
|
+
/**
|
|
165
|
+
* Wrap numbers in right-to-left text in left-to-right isolates, so "-5",
|
|
166
|
+
* "+960 333-1234", "9:00", "1,000" and "50%" keep their order inside Thaana.
|
|
167
|
+
* Brackets around a number stay outside the isolate (putting them inside makes
|
|
168
|
+
* the closing bracket jump). No-op for text without right-to-left letters.
|
|
169
|
+
* Strip the controls again with stripBidiControls() before saving.
|
|
170
|
+
*/
|
|
171
|
+
function isolateNumbers(text, rtl = hasRtl(text)) {
|
|
172
|
+
if (!rtl) return text;
|
|
173
|
+
return text.replace(NUMBER_TOKEN_RE, (_m, prefix, token) => `${prefix}${token}`);
|
|
174
|
+
}
|
|
175
|
+
const BIDI_CONTROLS = /[--]/gu;
|
|
176
|
+
/** Remove bidi control characters (before saving, searching or comparing text). */
|
|
177
|
+
function stripBidiControls(text) {
|
|
178
|
+
return text.replace(BIDI_CONTROLS, "");
|
|
179
|
+
}
|
|
180
|
+
const RTL_OR_DIGIT = "[\\d\\u0590-\\u08FF\\uFB1D-\\uFDFF\\uFE70-\\uFEFE]";
|
|
181
|
+
const OPENER_RE = /^[\p{Ps}\p{Pi}]$/u;
|
|
182
|
+
const OPENER_GAP_RE = new RegExp(`(\\p{P})\\s+(?=${RTL_OR_DIGIT})`, "gu");
|
|
183
|
+
const BEFORE_PUNCT_GAP_RE = new RegExp(`(${RTL_OR_DIGIT})\\s+(\\p{P})`, "gu");
|
|
184
|
+
/**
|
|
185
|
+
* Tidy spacing and punctuation in Thaana text:
|
|
186
|
+
* "ބަސް ، ބުދަ ؛ އެއް ؟ ނޫން !" → "ބަސް، ބުދަ؛ އެއް؟ ނޫން!"
|
|
187
|
+
* "ދުވަސް ( ބުރުނު ) [ 8 ]" → "ދުވަސް (ބުރުނު) [8]"
|
|
188
|
+
* Spaces before punctuation go, spaces just inside opening brackets/quotes go,
|
|
189
|
+
* and the space before an opening bracket is kept.
|
|
190
|
+
*/
|
|
191
|
+
function normalizePunctuation(text, options = {}) {
|
|
192
|
+
const { arabicPunctuation = true } = options;
|
|
193
|
+
let out = text;
|
|
194
|
+
if (arabicPunctuation) out = out.replace(/([ހ-])(\s*),/gu, "$1$2،").replace(/([ހ-])(\s*);/gu, "$1$2؛").replace(/([ހ-])(\s*)\?/gu, "$1$2؟");
|
|
195
|
+
return out.split("\n").map((line) => line.replace(OPENER_GAP_RE, (match, punct) => OPENER_RE.test(punct) ? punct : match).replace(BEFORE_PUNCT_GAP_RE, (match, lead, punct) => OPENER_RE.test(punct) ? match : `${lead}${punct}`)).join("\n");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Repair Thaana text copied out of a PDF. PDFs store right-to-left text in
|
|
199
|
+
* visual order, so after copy/paste dates and times come out reversed
|
|
200
|
+
* ("2026/1/14" for 14/1/2026, "00:9" for 9:00), punctuation picks up stray
|
|
201
|
+
* spaces, numbered-list markers jump to the end ("1 ބައި." for ".1 ބައި") and
|
|
202
|
+
* "1.2" section markers arrive as ".12". Only lines containing Thaana are
|
|
203
|
+
* touched. Do NOT run this on text people typed: it would reverse their dates.
|
|
204
|
+
*/
|
|
205
|
+
function repairPdfText(text) {
|
|
206
|
+
return text.split("\n").map((line) => {
|
|
207
|
+
if (!hasThaana(line)) return line;
|
|
208
|
+
let out = line.replace(/([/:])\s+(?=\d)/gu, "$1");
|
|
209
|
+
out = out.replace(/\d+(?:[/:]\d+)+/gu, (group) => {
|
|
210
|
+
const seps = new Set(group.match(/[/:]/gu));
|
|
211
|
+
if (seps.size !== 1) return group;
|
|
212
|
+
const [sep] = seps;
|
|
213
|
+
return group.split(sep).toReversed().join(sep);
|
|
214
|
+
});
|
|
215
|
+
out = normalizePunctuation(out, { arabicPunctuation: false });
|
|
216
|
+
out = out.replace(/^(\s*)\.(\d)(\d)(\s+)/u, (_m, lead, major, minor, gap) => `${lead}${minor}-${major}${gap}`);
|
|
217
|
+
const list = /^(\s*)(\d+)(\s+)([\s\S]*?)(\s*)\.$/u.exec(out);
|
|
218
|
+
if (list && hasThaana(list[4])) {
|
|
219
|
+
const [, lead, marker, gap, body, tail] = list;
|
|
220
|
+
out = `${lead}.${marker}${gap}${body}${tail}`;
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}).join("\n");
|
|
224
|
+
}
|
|
225
|
+
/** Thaana fonts installed on common systems, used as a fallback for glyphs a
|
|
226
|
+
* web font lacks: MV Boli ships with Windows, Noto Sans Thaana with Android
|
|
227
|
+
* and most Linux desktops. */
|
|
228
|
+
const SYSTEM_THAANA_FONTS = ["Noto Sans Thaana", "MV Boli"];
|
|
229
|
+
/**
|
|
230
|
+
* A font-family value that keeps both scripts readable: most Dhivehi fonts have
|
|
231
|
+
* no Latin letters, so Latin falls through to the system UI font, and missing
|
|
232
|
+
* Thaana falls through to an installed Thaana font.
|
|
233
|
+
*
|
|
234
|
+
* fontStack('Faruma') → "'Faruma', system-ui, 'Noto Sans Thaana', 'MV Boli', sans-serif"
|
|
235
|
+
*/
|
|
236
|
+
function fontStack(family, { latin = "system-ui" } = {}) {
|
|
237
|
+
return [
|
|
238
|
+
family,
|
|
239
|
+
latin,
|
|
240
|
+
...SYSTEM_THAANA_FONTS,
|
|
241
|
+
"sans-serif"
|
|
242
|
+
].map(quoteFamily).join(", ");
|
|
243
|
+
}
|
|
244
|
+
/** Generic families (system-ui, sans-serif) stay bare; names get quoted. */
|
|
245
|
+
function quoteFamily(f) {
|
|
246
|
+
return /^[a-z-]+$/.test(f) ? f : `'${f}'`;
|
|
247
|
+
}
|
|
248
|
+
// src/keyboard.ts
|
|
249
|
+
/**
|
|
250
|
+
* The Thaana character a `beforeinput` event should insert instead, or null to
|
|
251
|
+
* let it through. For rich-text editors (Lexical, TipTap/ProseMirror, Slate…)
|
|
252
|
+
* that own their own insertion: call this in the editor's beforeinput hook,
|
|
253
|
+
* preventDefault, and insert the result through the editor's API.
|
|
254
|
+
* Only single typed characters are mapped; IME composition, paste,
|
|
255
|
+
* autocorrect and deletes return null.
|
|
256
|
+
*/
|
|
257
|
+
function thaanaForInputEvent(event) {
|
|
258
|
+
if (event.inputType !== "insertText" || event.isComposing) return null;
|
|
259
|
+
const key = event.data && event.data.length === 1 ? event.data : void 0;
|
|
260
|
+
if (!key) return null;
|
|
261
|
+
return KEYMAP[key] ?? null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Make an <input>, <textarea> or contenteditable element type Thaana on any
|
|
265
|
+
* keyboard. Only single typed characters are rewritten; paste, IME
|
|
266
|
+
* composition, autocorrect, deletion and text from a real Dhivehi keyboard
|
|
267
|
+
* pass through. Inserts via the browser's own editing command, so `input`
|
|
268
|
+
* events fire (React/Vue/Svelte bindings keep working) and undo works.
|
|
269
|
+
*
|
|
270
|
+
* @returns a function that detaches the keyboard.
|
|
271
|
+
*/
|
|
272
|
+
function attachKeyboard(el, options = {}) {
|
|
273
|
+
const { enabled = () => true, setAttributes = true, autoDirection = true } = options;
|
|
274
|
+
if (setAttributes) {
|
|
275
|
+
for (const [attr, value] of [
|
|
276
|
+
["dir", "rtl"],
|
|
277
|
+
["lang", "dv"],
|
|
278
|
+
["autocomplete", "off"],
|
|
279
|
+
["autocorrect", "off"],
|
|
280
|
+
["autocapitalize", "none"],
|
|
281
|
+
["spellcheck", "false"]
|
|
282
|
+
]) if (!el.hasAttribute(attr)) el.setAttribute(attr, value);
|
|
283
|
+
}
|
|
284
|
+
let inserting = false;
|
|
285
|
+
const onBeforeInput = (e) => {
|
|
286
|
+
const ev = e;
|
|
287
|
+
if (inserting || !enabled()) return;
|
|
288
|
+
const mapped = thaanaForInputEvent(ev);
|
|
289
|
+
if (!mapped) return;
|
|
290
|
+
ev.preventDefault();
|
|
291
|
+
inserting = true;
|
|
292
|
+
try {
|
|
293
|
+
if (typeof document.execCommand === "function" && document.execCommand("insertText", false, mapped)) return;
|
|
294
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
295
|
+
const start = el.selectionStart ?? el.value.length;
|
|
296
|
+
el.setRangeText(mapped, start, el.selectionEnd ?? start, "end");
|
|
297
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
inserting = false;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const fieldText = () => el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : el.textContent ?? "";
|
|
304
|
+
const onInput = () => {
|
|
305
|
+
el.dir = resolveDirection(fieldText(), null, "rtl");
|
|
306
|
+
};
|
|
307
|
+
el.addEventListener("beforeinput", onBeforeInput);
|
|
308
|
+
if (autoDirection) {
|
|
309
|
+
el.addEventListener("input", onInput);
|
|
310
|
+
onInput();
|
|
311
|
+
}
|
|
312
|
+
return () => {
|
|
313
|
+
el.removeEventListener("beforeinput", onBeforeInput);
|
|
314
|
+
el.removeEventListener("input", onInput);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const attached = /* @__PURE__ */ new WeakMap();
|
|
318
|
+
/**
|
|
319
|
+
* Attach the keyboard to every `[data-thaana]` element under `root`, now and as
|
|
320
|
+
* elements are added later. `data-thaana="off"` opts an element out.
|
|
321
|
+
* @returns a function that stops watching (already-attached fields stay attached).
|
|
322
|
+
*/
|
|
323
|
+
function autoAttach(root = document) {
|
|
324
|
+
const scan = () => {
|
|
325
|
+
for (const el of root.querySelectorAll("[data-thaana]")) {
|
|
326
|
+
if (attached.has(el) || el.dataset.thaana === "off") continue;
|
|
327
|
+
attached.set(el, attachKeyboard(el));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
scan();
|
|
331
|
+
const observer = new MutationObserver(scan);
|
|
332
|
+
observer.observe(root instanceof Document ? root.documentElement : root, {
|
|
333
|
+
childList: true,
|
|
334
|
+
subtree: true
|
|
335
|
+
});
|
|
336
|
+
return () => observer.disconnect();
|
|
337
|
+
}
|
|
338
|
+
export { repairPdfText as _, LRI as a, KEYMAP as b, SYSTEM_THAANA_FONTS as c, hasRtl as d, hasThaana as f, normalizePunctuation as g, isolateNumbers as h, FSI as i, detectDirection as l, isolate as m, autoAttach as n, PDI as o, isMixedScript as p, thaanaForInputEvent as r, RLI as s, attachKeyboard as t, fontStack as u, resolveDirection as v, transliterate as x, stripBidiControls as y };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var Thaana=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});let t=Object.freeze({q:`ް`,w:`އ`,e:`ެ`,r:`ރ`,t:`ތ`,y:`ޔ`,u:`ު`,i:`ި`,o:`ޮ`,p:`ޕ`,a:`ަ`,s:`ސ`,d:`ދ`,f:`ފ`,g:`ގ`,h:`ހ`,j:`ޖ`,k:`ކ`,l:`ލ`,z:`ޒ`,x:`×`,c:`ޗ`,v:`ވ`,b:`ބ`,n:`ނ`,m:`މ`,Q:`ޤ`,W:`ޢ`,E:`ޭ`,R:`ޜ`,T:`ޓ`,Y:`ޠ`,U:`ޫ`,I:`ީ`,O:`ޯ`,P:`÷`,A:`ާ`,S:`ށ`,D:`ޑ`,F:`ﷲ`,G:`ޣ`,H:`ޙ`,J:`ޛ`,K:`ޚ`,L:`ޅ`,Z:`ޡ`,X:`ޘ`,C:`ޝ`,V:`ޥ`,B:`ޞ`,N:`ޏ`,M:`ޟ`,",":`،`,";":`؛`,"?":`؟`,"(":`)`,")":`(`,"[":`]`,"]":`[`,"{":`}`,"}":`{`,"<":`>`,">":`<`});function n(e){let n=``;for(let r of e)n+=t[r]??r;return n}function r(e){return e>=1424&&e<=2303||e>=64285&&e<=65023||e>=65136&&e<=65278||e>=67584&&e<=69631||e>=124928&&e<=126975}function i(e){return e>=65&&e<=90||e>=97&&e<=122||e>=192&&e<=687&&e!==215&&e!==247||e>=880&&e<=1023||e>=1024&&e<=1279}let a=/[ހ-]/u;function o(e){return a.test(e)}function s(e){for(let t of e)if(r(t.codePointAt(0)))return!0;return!1}function c(e){let t=!1,n=!1;for(let a of e){let e=a.codePointAt(0);if(r(e)?t=!0:i(e)&&(n=!0),t&&n)return!0}return!1}function l(e){for(let t of e){let e=t.codePointAt(0);if(r(e))return`rtl`;if(i(e))return`ltr`}return`neutral`}function u(e,t,n=`ltr`){return t||(s(e)?`rtl`:l(e)===`ltr`?`ltr`:n)}function d(e,t=`ltr`){return(t===`ltr`?``:t===`rtl`?``:``)+e+``}let f=/(^|[\s\p{P}\p{S}])(\+\d[\d -]*\d|[+\-\u2212]?\d(?:[\d.,:/-]*\d)?%?)(?=$|[\s\p{P}\p{S}])/gu;function p(e,t=s(e)){return t?e.replace(f,(e,t,n)=>`${t}${n}`):e}let m=/[--]/gu;function h(e){return e.replace(m,``)}let g=`[\\d\\u0590-\\u08FF\\uFB1D-\\uFDFF\\uFE70-\\uFEFE]`,_=/^[\p{Ps}\p{Pi}]$/u,v=RegExp(`(\\p{P})\\s+(?=${g})`,`gu`),y=RegExp(`(${g})\\s+(\\p{P})`,`gu`);function b(e,t={}){let{arabicPunctuation:n=!0}=t,r=e;return n&&(r=r.replace(/([ހ-])(\s*),/gu,`$1$2،`).replace(/([ހ-])(\s*);/gu,`$1$2؛`).replace(/([ހ-])(\s*)\?/gu,`$1$2؟`)),r.split(`
|
|
2
|
+
`).map(e=>e.replace(v,(e,t)=>_.test(t)?t:e).replace(y,(e,t,n)=>_.test(n)?e:`${t}${n}`)).join(`
|
|
3
|
+
`)}function x(e){return e.split(`
|
|
4
|
+
`).map(e=>{if(!o(e))return e;let t=e.replace(/([/:])\s+(?=\d)/gu,`$1`);t=t.replace(/\d+(?:[/:]\d+)+/gu,e=>{let t=new Set(e.match(/[/:]/gu));if(t.size!==1)return e;let[n]=t;return e.split(n).toReversed().join(n)}),t=b(t,{arabicPunctuation:!1}),t=t.replace(/^(\s*)\.(\d)(\d)(\s+)/u,(e,t,n,r,i)=>`${t}${r}-${n}${i}`);let n=/^(\s*)(\d+)(\s+)([\s\S]*?)(\s*)\.$/u.exec(t);if(n&&o(n[4])){let[,e,r,i,a,o]=n;t=`${e}.${r}${i}${a}${o}`}return t}).join(`
|
|
5
|
+
`)}let S=[`Noto Sans Thaana`,`MV Boli`];function C(e,{latin:t=`system-ui`}={}){return[e,t,...S,`sans-serif`].map(w).join(`, `)}function w(e){return/^[a-z-]+$/.test(e)?e:`'${e}'`}function T(e){if(e.inputType!==`insertText`||e.isComposing)return null;let n=e.data&&e.data.length===1?e.data:void 0;return n?t[n]??null:null}function E(e,t={}){let{enabled:n=()=>!0,setAttributes:r=!0,autoDirection:i=!0}=t;if(r)for(let[t,n]of[[`dir`,`rtl`],[`lang`,`dv`],[`autocomplete`,`off`],[`autocorrect`,`off`],[`autocapitalize`,`none`],[`spellcheck`,`false`]])e.hasAttribute(t)||e.setAttribute(t,n);let a=!1,o=t=>{let r=t;if(a||!n())return;let i=T(r);if(i){r.preventDefault(),a=!0;try{if(typeof document.execCommand==`function`&&document.execCommand(`insertText`,!1,i))return;if(e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement){let t=e.selectionStart??e.value.length;e.setRangeText(i,t,e.selectionEnd??t,`end`),e.dispatchEvent(new Event(`input`,{bubbles:!0}))}}finally{a=!1}}},s=()=>e instanceof HTMLInputElement||e instanceof HTMLTextAreaElement?e.value:e.textContent??``,c=()=>{e.dir=u(s(),null,`rtl`)};return e.addEventListener(`beforeinput`,o),i&&(e.addEventListener(`input`,c),c()),()=>{e.removeEventListener(`beforeinput`,o),e.removeEventListener(`input`,c)}}let D=/* @__PURE__ */ new WeakMap;function O(e=document){let t=()=>{for(let t of e.querySelectorAll(`[data-thaana]`))D.has(t)||t.dataset.thaana===`off`||D.set(t,E(t))};t();let n=new MutationObserver(t);return n.observe(e instanceof Document?e.documentElement:e,{childList:!0,subtree:!0}),()=>n.disconnect()}let k=!1;function A(){k||typeof document>`u`||(k=!0,document.readyState===`loading`?document.addEventListener(`DOMContentLoaded`,()=>O(),{once:!0}):O())}A();let j={typewriter:{a:`އ`,b:`ބ`,c:`ް`,d:`ދ`,e:`ެ`,f:`ފ`,g:`ގ`,h:`ހ`,i:`ި`,j:`ޖ`,k:`ކ`,l:`ލ`,m:`މ`,n:`ނ`,o:`ޮ`,p:`ޕ`,q:`ޤ`,r:`ރ`,s:`ސ`,t:`ތ`,u:`ު`,v:`ވ`,w:`ަ`,x:`ޝ`,y:`ޔ`,z:`ޒ`,A:`ޢ`,B:`ޞ`,C:`ޗ`,D:`ޑ`,E:`ޭ`,F:`ޟ`,G:`ޣ`,H:`ޙ`,I:`ީ`,J:`ޛ`,K:`ޚ`,L:`ޅ`,N:`ޏ`,O:`ޯ`,R:`ޜ`,S:`ށ`,T:`ޓ`,U:`ޫ`,V:`ޥ`,W:`ާ`,X:`ޘ`,Y:`ޠ`,Z:`ޡ`,Q:`ﷲ`,",":`،`,";":`؛`,"?":`؟`},phonetic:{q:`ް`,w:`އ`,e:`ެ`,r:`ރ`,t:`ތ`,y:`ޔ`,u:`ު`,i:`ި`,o:`ޮ`,p:`ޕ`,a:`ަ`,s:`ސ`,d:`ދ`,f:`ފ`,g:`ގ`,h:`ހ`,j:`ޖ`,k:`ކ`,l:`ލ`,z:`ޒ`,c:`ޗ`,v:`ވ`,b:`ބ`,n:`ނ`,m:`މ`,Q:`ޤ`,W:`ޢ`,E:`ޭ`,R:`ޜ`,T:`ޓ`,Y:`ޠ`,U:`ޫ`,I:`ީ`,O:`ޯ`,A:`ާ`,S:`ށ`,D:`ޑ`,F:`ﷲ`,G:`ޣ`,H:`ޙ`,J:`ޛ`,K:`ޚ`,L:`ޅ`,Z:`ޡ`,X:`ޘ`,C:`ޝ`,V:`ޥ`,B:`ޞ`,N:`ޏ`,M:`ޟ`,",":`،`,";":`؛`,"?":`؟`}};function M(e,t={}){let{layout:n=`typewriter`,reverse:r=!0}=t,i=j[n];return e.split(/(\r?\n)/).map(e=>{if(e===`
|
|
6
|
+
`||e===`\r
|
|
7
|
+
`)return e;let t=(r?[...e].toReversed():[...e]).map(e=>i[e]??e).join(``);return r&&(t=t.replace(/\d[\d.,:/-]*\d|\d/g,e=>[...e].toReversed().join(``))),t}).join(``)}let N=`https://dvfonts.yashau.com`;function P(e,{display:t=`swap`,origin:n=N}={}){return`${n}/css2?${(Array.isArray(e)?e:[e]).map(({family:e,weights:t})=>`family=${encodeURIComponent(e).replace(/%20/g,`+`)}${t?.length?`:wght@${t.toSorted((e,t)=>e-t).join(`;`)}`:``}`).join(`&`)}&display=${t}`}function F(e,t){let n=P(e,t),r=document.querySelector(`link[data-dvfonts][href="${CSS.escape(n)}"]`);r||(r=document.createElement(`link`),r.rel=`stylesheet`,r.href=n,r.dataset.dvfonts=``,document.head.append(r));let i=Array.isArray(e)?e:[e];return new Promise((e,t)=>{if(r.sheet)return e();r.addEventListener(`load`,()=>e(),{once:!0}),r.addEventListener(`error`,()=>t(/* @__PURE__ */ Error(`Could not load ${n}`)),{once:!0})}).then(()=>Promise.all(i.flatMap(({family:e,weights:t=[400]})=>t.map(t=>document.fonts.load(`${t} 16px "${e}"`,`ދ`)))).then(()=>void 0))}let I=[`ޗާލު ޒުވާން ކުއްޖާ، ފޮށިން ޕެކެޓެއް ކަނޑައި، ޏަމްޏަމް ގަހުގައި އިން ބުޅަލަށް މަސްދީ، ރީތި ލަވައެއް ކިޔައިދިނެވެ.`,`ކުޑަ ޗުއްޓީއެއްގައި ޏ. އަތޮޅަށް ފުރަން ޓިކެޓާއި ބޮޑު ފޮށި ހިފައިގެން، ޒުވާން ޕައިލެޓް މަޖާ ލަވައެއް ސަކަރާތާއެކު ކިޔަމުން ދިޔައެވެ.`,`ޕާކުގައި ހުރި މޭޒުމަތީގައި، ކުޑަ ކުއްޖާ ފޮނި ޏަމްޏަމް މޭވާއާއި ޗޮކްލެޓް ބަހައްޓައި، ޅަ އެކުވެރިޔާއަށް ތަށިން ސައި ދިނެވެ.`,`ސުކޫލު ކުލާހުގައި ހުރި ބޯޑުގައި، މޮޅު ކުއްޖާ ތާނަ އަކުރު 'ޏަވިޔަނި' ވަރަށް ޗާލުކޮށް ޕެންސިލުން ފޮތުގައި ލިޔެ، ޒުވާން ޓީޗަރަށް ދެއްކިއެވެ.`,`ބާޒާރުން ތާޒާ ޏަމްޏަމް މޭވާއާއި، ޗީޒް، ޓޮމާޓޯ އަދި ފޮނި ޖޭމު ޑަބިޔަލެއް ގަނެގެން، ކުއްޖާ ޕްލާސްޓިކް ކޮތަޅަށް އަޅައިގެން އަވަހަށް ހިނގައިގަތެވެ.`,`ރީތި ޗާލު ހަނދުވަރުގައި، ޏ. އަތޮޅުގެ ގޮނޑުދޮށުގައި ޒުވާނުން ބޮޑުބެރު ޖަހައި، ޕާޓީއެއް ހަދައި، ފާނޫޒު ދިއްލައިގެން މަޖާ ލަވަތައް ކިޔައި ސަކަރާތްޖެހިއެވެ.`,`ޗަސްބިމުގައި ހުރި ޏަމްޏަމް ގަހުގެ ގޮފިތަކަށް ޖެހި، ޅަ ލޯބި ދޫނި، ކުޑަ ޕިކަޕެއްގައި އައި ޒުވާނުން ޖެހި ޓެންޓު މަތިން އުދުހިގެން ދިޔައެވެ.`,`މީރު ޗޮކްލެޓާއި، ބަޓަރު، ފުށް، ޗީޒް އަދި ޏަމްޏަމް އެއްކޮށް، ބޮޑު ތައްޓަކަށް ސަމުސަލުން ހަކުރު އަޅައިގެން، ޒުވާން ކުއްޖާ ޕޭސްޓްރީ ތައްޔާރުކުރިއެވެ.`],L=(e,t)=>Array.from({length:t-e},(t,n)=>String.fromCodePoint(e+n)),R=L(1920,1944),z=L(1944,1958),B=L(1958,1969);return A(),e.ARABIC_LETTERS=z,e.ARABIC_LETTERS_SAMPLE=`ރަމަޟާން މަހުގެ ޚާއްޞަ ޘަޤާފަތާއި ޝަރީޢަތުގެ ޙަޤީޤަތް ޙިފްޡުކޮށް ޛިކުރުކުރުމަކީ ބަޣާވާތެއް ނޫނެވެ.`,e.DVFONTS_ORIGIN=N,e.FILI=B,e.FSI=``,e.KEYMAP=t,e.LEGACY_LAYOUTS=j,e.LETTERS=R,e.LRI=``,e.PANGRAMS=I,e.PDI=``,e.RLI=``,e.SYSTEM_THAANA_FONTS=S,e.attachKeyboard=E,e.autoAttach=O,e.convertLegacy=M,e.detectDirection=l,e.fontCssUrl=P,e.fontStack=C,e.hasRtl=s,e.hasThaana=o,e.isMixedScript=c,e.isolate=d,e.isolateNumbers=p,e.loadFonts=F,e.normalizePunctuation=b,e.repairPdfText=x,e.resolveDirection=u,e.stripBidiControls=h,e.thaanaForInputEvent=T,e.transliterate=n,e})({});
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yashau/thaana",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2026.1023.0",
|
|
4
|
+
"description": "Everything to make Thaana (Dhivehi) work on the web: phonetic keyboard for any input, legacy A_-font text converter, bidi helpers, font loading and sane CSS defaults.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bidi",
|
|
7
|
+
"dhivehi",
|
|
8
|
+
"divehi",
|
|
9
|
+
"fonts",
|
|
10
|
+
"keyboard",
|
|
11
|
+
"maldives",
|
|
12
|
+
"rtl",
|
|
13
|
+
"thaana"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://dvfonts.yashau.com/docs",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Ibrahim Yashau",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/yashau/dvfonts.git",
|
|
21
|
+
"directory": "packages/thaana"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"thaana.css",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": [
|
|
31
|
+
"./dist/auto.js",
|
|
32
|
+
"./dist/thaana.iife.js",
|
|
33
|
+
"*.css"
|
|
34
|
+
],
|
|
35
|
+
"unpkg": "./dist/thaana.iife.js",
|
|
36
|
+
"jsdelivr": "./dist/thaana.iife.js",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./auto": {
|
|
43
|
+
"types": "./dist/auto.d.ts",
|
|
44
|
+
"default": "./dist/auto.js"
|
|
45
|
+
},
|
|
46
|
+
"./thaana.css": "./thaana.css",
|
|
47
|
+
"./cdn": "./dist/cdn/thaana.js",
|
|
48
|
+
"./iife": "./dist/thaana.iife.js",
|
|
49
|
+
"./package.json": "./package.json"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"jsdom": "^30.1.3",
|
|
53
|
+
"publint": "^0.3.25",
|
|
54
|
+
"tsdown": "^0.23.1",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vitest": "^5.0.5"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "rm -rf dist && tsdown && node scripts/check-build.mjs",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"lint": "publint"
|
|
62
|
+
}
|
|
6
63
|
}
|
package/thaana.css
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @yashau/thaana — sane defaults for Thaana (Dhivehi) text.
|
|
3
|
+
* Applies only to elements marked lang="dv" (put it on <html> for Dhivehi
|
|
4
|
+
* sites). https://dvfonts.yashau.com/guide
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* Right-to-left by default. dir="rtl" in the HTML is still the most robust
|
|
8
|
+
(it also flips layout before CSS loads); this is the safety net. */
|
|
9
|
+
:where(:lang(dv)) {
|
|
10
|
+
direction: rtl;
|
|
11
|
+
/* Fili sit above and below the letters: tight line-heights clip them. */
|
|
12
|
+
line-height: 1.75;
|
|
13
|
+
/* Thaana fonts have no italics; synthesized slant and fake bold look broken. */
|
|
14
|
+
font-synthesis: none;
|
|
15
|
+
/* No hyphenation dictionaries exist for Dhivehi. */
|
|
16
|
+
hyphens: manual;
|
|
17
|
+
/* Thaana is not cursive, but tracking breaks fili alignment in many fonts. */
|
|
18
|
+
letter-spacing: normal;
|
|
19
|
+
text-align: start;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* User-generated content: each paragraph takes its direction from its first
|
|
23
|
+
letter, so a Latin-first line is LTR and a Thaana-first line is RTL. */
|
|
24
|
+
:where(:lang(dv))
|
|
25
|
+
:where(p, li, td, th, dd, blockquote, figcaption, h1, h2, h3, h4, h5, h6, input, textarea, [contenteditable]) {
|
|
26
|
+
unicode-bidi: plaintext;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Inputs: keep the caret and placeholder on the right for Thaana. */
|
|
30
|
+
:where(input, textarea):where(:lang(dv), [data-thaana]) {
|
|
31
|
+
direction: rtl;
|
|
32
|
+
text-align: start;
|
|
33
|
+
unicode-bidi: plaintext;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Things that are always left-to-right inside Thaana text. */
|
|
37
|
+
:where(:lang(dv)) :where(code, kbd, samp, pre, var, [type='email'], [type='url'], [type='tel'], [type='number']) {
|
|
38
|
+
direction: ltr;
|
|
39
|
+
unicode-bidi: isolate;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Never italicise Thaana (emphasis = bold or colour instead). */
|
|
43
|
+
:where(:lang(dv)) :where(em, i, cite, dfn, address) {
|
|
44
|
+
font-style: normal;
|
|
45
|
+
}
|