prpm-dbp 0.3.0 → 0.5.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 CHANGED
@@ -128,6 +128,7 @@ Pengguna kemudian menarik versi baharu dengan `npx skills update`.
128
128
  | `jawi <fail>` | tukar teks Rumi ke Jawi ikut ejaan DBP |
129
129
  | `rumi <fail>` | tukar teks Jawi ke Rumi dari cache |
130
130
  | `import-tsv <fail>` | import dataset rumi/jawi tambahan |
131
+ | `nama <rumi> <jawi>` | sahkan satu nama khas |
131
132
  | `banding <kata...>` | silang semak PRPM lawan KamusDBP |
132
133
  | `cache` | status cache, `--buang <kata>` untuk padam satu |
133
134
 
package/SKILL.md CHANGED
@@ -91,6 +91,59 @@ Sesetengah entri memulangkan **dua perkataan Jawi untuk satu perkataan Rumi**: `
91
91
 
92
92
  **Teks Arab yang dipetik dibiarkan sepenuhnya.** Jawi Melayu tidak menggunakan baris tanda vokal, jadi kehadiran fathah, kasrah, dammah, sukun atau tanwin bermakna potongan itu Arab, bukan Jawi. Ayat al-Quran dan hadis dalam dokumen Melayu lulus tanpa disentuh dan dilaporkan dalam medan `arab`.
93
93
 
94
+ ## Nama khas
95
+
96
+ Diuji terhadap PRPM: **nama tokoh dan nama surah tiada sumber berwibawa.** `muhammad`, `ibrahim`, `musa`, `umar`, `aisyah`, `khadijah` semuanya tiada entri. Nama surah seperti `fatihah`, `baqarah`, `yasin` ada entri tetapi tanpa ejaan Jawi. Dataset komuniti meliputi nama tempat (`mekah` مکة, `mesir` مسير, `pahang` ڤاهڠ) tetapi bukan nama orang.
97
+
98
+ Alat ini **tidak mereka ejaan nama**. Sebaliknya:
99
+
100
+ - Perkataan berhuruf besar di **tengah** ayat dikira nama khas dan dilaporkan dalam medan `namaKhas`, berasingan daripada `belumDisahkan`. Nama bukan salah eja.
101
+ - Sahkan sekali, guna selamanya:
102
+
103
+ ```bash
104
+ npx prpm-dbp nama Muhammad <ejaan-jawi>
105
+ npx prpm-dbp nama # senaraikan yang sudah disahkan
106
+ ```
107
+
108
+ Ejaan yang disimpan begini ditandakan datang daripada pengguna, bukan daripada DBP. Sahkan dengan guru atau rujukan bertulis dahulu.
109
+
110
+ **Jangan keliru antara dua benda:**
111
+
112
+ | | Layanan |
113
+ |---|---|
114
+ | Petikan bahasa Arab (ayat, doa, hadis) | kekal Arab, tidak disentuh |
115
+ | Nama khas dalam ayat Melayu | **ditulis dalam Jawi**, bukan dikekalkan |
116
+
117
+ Bagi nama berasal Arab, ejaan Jawinya memang ejaan Arab asal, jadi ia nampak seperti dikekalkan. Bagi nama bukan Arab ia ditransliterasi penuh: `Kuala Lumpur` menjadi `کوالا لومڤور`.
118
+
119
+ ## Peraturan Pedoman DBP
120
+
121
+ Satu fail sahaja dalam pakej ini **menjana** ejaan dan bukan mencarinya: `src/pedoman.js`. Ia dibenarkan kerana setiap peraturan di dalamnya datang daripada *Pedoman Umum Ejaan Jawi Bahasa Melayu* terbitan DBP, dan dirujuk pada seksyennya.
122
+
123
+ **§14.1 — kata sendi `di` dan `ke` ditulis sebagai satu kata dengan kata yang mengikutinya.**
124
+
125
+ ```
126
+ di Mekah -> دمکة bukan د مکة
127
+ ke sekolah -> کسکوله bukan ک سکوله
128
+ ```
129
+
130
+ Nota seksyen yang sama: jika kata berikutnya bermula dengan alif, hamzah dibubuh di atas alif itu. `di Asrama` menjadi `دأسراما`, bukan `داسراما`.
131
+
132
+ Kodpoint hamzah (U+0623) disahkan secara empirik daripada dataset, bukan diandaikan: `abai` bermula `ا` U+0627, `diabaikan` bermula `د` + `أ` U+0623. Corak sama pada `diambil`, `diajar`, `diurus`.
133
+
134
+ Bentuk bercantum tidak wujud sebagai entri kamus, jadi pembacaan songsang memisahkannya semula dan memulihkan hamzah kepada alif. Peraturan ini simetri.
135
+
136
+ Kata sendi **tidak** dicantum apabila kata berikutnya belum disahkan, kerana hasilnya akan menjadi Jawi bercantum dengan teks Rumi bertanda.
137
+
138
+ **§11 — kata serapan Arab, dua peringkat:**
139
+
140
+ | Seksyen | Jenis | Cara eja | Contoh |
141
+ |---|---|---|---|
142
+ | §11.1 | istilah khusus | ikut ejaan asal Arab | `wuduk`, `Qur'an`, `sunah`, `hadis`, `tayamum` |
143
+ | §11.2 | kata umum yang sudah terserap | ikut kaedah kata jati Melayu | `sabun` |
144
+
145
+ Pakej tidak perlu memilih antara dua ini. DBP sudah memilih, dan pilihan itu terkandung dalam ejaan yang dipulangkan PRPM. Ia didokumenkan supaya tiada siapa "membetulkan" `صلاة` kepada ejaan fonetik Melayu kemudian hari.
146
+
94
147
  ## Rantai kepercayaan
95
148
 
96
149
  Setiap ejaan Jawi yang dikeluarkan membawa asal-usulnya. Jangan runtuhkan lapisan ini.
package/bin/cli.js CHANGED
@@ -6,6 +6,7 @@ import { pasangSkill } from '../src/install.js'
6
6
  import { banding, cariKamus } from '../src/kamus.js'
7
7
  import { keJawi, keRumi } from '../src/tukar.js'
8
8
  import { huraiTsv, muatDataset, simpanDataset } from '../src/dataset.js'
9
+ import { NAMA_PATH, muatNama, tambahNama } from '../src/nama.js'
9
10
  import {
10
11
  CACHE_PATH,
11
12
  auditJawi,
@@ -26,6 +27,8 @@ const BANTUAN = `prpm-dbp — semak Bahasa Melayu dan Jawi terhadap PRPM DBP
26
27
  npx prpm-dbp jawi <fail.txt> tukar teks Rumi ke Jawi ikut ejaan DBP
27
28
  npx prpm-dbp rumi <fail.txt> tukar teks Jawi ke Rumi dari cache
28
29
  npx prpm-dbp import-tsv <fail.tsv> import dataset rumi<TAB>jawi (dilabel bukan DBP)
30
+ npx prpm-dbp nama <rumi> <jawi> sahkan satu nama khas (tokoh, surah, tempat)
31
+ npx prpm-dbp nama senaraikan nama khas yang disahkan
29
32
  npx prpm-dbp banding <kata...> silang semak PRPM lawan KamusDBP
30
33
  npx prpm-dbp cache [--buang <kata>] status cache
31
34
  npx prpm-dbp install [--link] pasang sebagai Claude Code skill
@@ -160,6 +163,12 @@ async function main() {
160
163
  h.adaEntriTanpaJawi.join(', '),
161
164
  )
162
165
  }
166
+ if (h.namaKhas.length) {
167
+ console.error(
168
+ `nama khas belum disahkan (${h.namaKhas.length}): ${h.namaKhas.join(', ')}\n` +
169
+ ` sahkan dengan: npx prpm-dbp nama <rumi> <jawi>`,
170
+ )
171
+ }
163
172
  if (h.terbitanSah.length) {
164
173
  console.error(
165
174
  `terbitan sah tanpa ejaan Jawi (${h.terbitanSah.length}): ` +
@@ -183,6 +192,17 @@ async function main() {
183
192
  break
184
193
  }
185
194
 
195
+ case 'nama': {
196
+ if (baki.length >= 2) {
197
+ const rec = await tambahNama(baki[0], baki[1], { jenis: pilihan.jenis ?? null })
198
+ keluar({ disimpan: rec, fail: NAMA_PATH, nota: 'Ejaan ini datang dari anda, bukan dari DBP.' })
199
+ break
200
+ }
201
+ const peta = await muatNama()
202
+ keluar({ fail: NAMA_PATH, jumlah: Object.keys(peta).length, nama: peta })
203
+ break
204
+ }
205
+
186
206
  case 'import-tsv': {
187
207
  if (!baki[0]) throw new Error('perlu fail TSV: rumi<TAB>jawi setiap baris')
188
208
  const { peta, dilangkau, jumlah } = huraiTsv(await readFile(baki[0], 'utf8'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Semak ejaan Bahasa Melayu dan Jawi terhadap PRPM Dewan Bahasa dan Pustaka. CLI dan skill untuk AI agent.",
5
5
  "keywords": [
6
6
  "bahasa-melayu",
package/src/nama.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Simpanan nama khas yang disahkan pengguna.
3
+ *
4
+ * Kenapa ini wujud: nama tokoh dan nama surah tiada sumber berwibawa yang
5
+ * boleh dicapai. Diuji terhadap PRPM, muhammad, ibrahim, musa, umar, aisyah
6
+ * dan khadijah semuanya TIADA entri. Dataset komuniti pula meliputi nama
7
+ * tempat (مکة, مسير, ڤاهڠ) tetapi bukan nama orang atau nama surah.
8
+ *
9
+ * Alat ini TIDAK mereka ejaan nama. Ia menyimpan apa yang pengguna sahkan
10
+ * sendiri, dengan sumber dicatat, supaya kerja pengesahan itu dibuat sekali
11
+ * sahaja dan boleh diaudit kemudian.
12
+ */
13
+
14
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
15
+ import { dirname, join } from 'node:path'
16
+
17
+ import { CACHE_PATH } from './prpm.js'
18
+
19
+ export const NAMA_PATH = process.env.PRPM_NAMA ?? join(dirname(CACHE_PATH), 'nama-khas.json')
20
+
21
+ let dimuat = null
22
+
23
+ export async function muatNama() {
24
+ if (dimuat !== null) return dimuat
25
+ try {
26
+ dimuat = JSON.parse(await readFile(NAMA_PATH, 'utf8'))
27
+ } catch {
28
+ dimuat = {}
29
+ }
30
+ return dimuat
31
+ }
32
+
33
+ export async function simpanNama(peta) {
34
+ await mkdir(dirname(NAMA_PATH), { recursive: true })
35
+ const tmp = `${NAMA_PATH}.tmp`
36
+ await writeFile(tmp, JSON.stringify(peta, null, 1), 'utf8')
37
+ await rename(tmp, NAMA_PATH)
38
+ dimuat = peta
39
+ return NAMA_PATH
40
+ }
41
+
42
+ export async function tambahNama(rumi, jawi, { sumber = 'pengguna', jenis = null } = {}) {
43
+ const peta = { ...(await muatNama()) }
44
+ const kunci = String(rumi).trim().toLowerCase()
45
+ if (!kunci || !jawi) throw new Error('perlu rumi dan jawi')
46
+ peta[kunci] = { rumi: kunci, jawi: String(jawi).trim(), sumber, jenis }
47
+ await simpanNama(peta)
48
+ return peta[kunci]
49
+ }
50
+
51
+ export async function cariNama(kata) {
52
+ const peta = await muatNama()
53
+ const rec = peta[String(kata).trim().toLowerCase()]
54
+ if (!rec) return null
55
+ return { ...rec, dijumpai: true, disahkanDbp: false }
56
+ }
57
+
58
+ /**
59
+ * Teka sama ada perkataan itu nama khas.
60
+ *
61
+ * Huruf besar di TENGAH ayat ialah isyarat yang munasabah dalam teks Melayu.
62
+ * Ia sengaja tidak digunakan pada perkataan pertama ayat, kerana di situ
63
+ * huruf besar tidak bermakna apa-apa.
64
+ */
65
+ export function mungkinNamaKhas(kata, sebelumnya) {
66
+ if (!/^[A-Z]/.test(kata)) return false
67
+ // Awal teks, atau selepas noktah, tanda soal, tanda seru, atau baris baru.
68
+ const awalAyat = !sebelumnya || /[.!?:\n]\s*$/.test(sebelumnya)
69
+ return !awalAyat
70
+ }
package/src/pedoman.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Peraturan daripada Pedoman Umum Ejaan Jawi Bahasa Melayu (DBP).
3
+ *
4
+ * Ini satu-satunya tempat dalam pakej yang MENJANA ejaan dan bukan sekadar
5
+ * mencarinya. Ia dibenarkan kerana setiap peraturan di sini datang daripada
6
+ * dokumen rasmi DBP dan dirujuk pada seksyennya, bukan direka.
7
+ *
8
+ * Sumber: Pedoman Umum Ejaan Jawi Bahasa Melayu, Dewan Bahasa dan Pustaka.
9
+ * Berasaskan Pedoman Ejaan Jawi yang Disempurnakan (1986, ed. 3 1993),
10
+ * hasil Konvensyen Tulisan Jawi Peringkat Kebangsaan 1984, diselaraskan
11
+ * 1991 dan 1993, disahkan Jawatankuasa Teknikal Ejaan Jawi 1993-1994.
12
+ */
13
+
14
+ const ALIF = 'ا' // ا
15
+ const ALIF_HAMZAH = 'أ' // أ
16
+
17
+ /**
18
+ * §14.1 — Kata sendi nama `di` dan `ke` ditulis sebagai SATU KATA dengan
19
+ * kata yang mengikutinya.
20
+ *
21
+ * Nota §14.1: jika kata yang mengikutinya bermula dengan huruf alif, huruf
22
+ * hamzah dibubuh di atas alif itu.
23
+ *
24
+ * Kodpoint hamzah disahkan secara empirik daripada dataset, bukan diandaikan:
25
+ * abai ا U+0627 ...
26
+ * diabaikan د + أ U+0623 ...
27
+ * Corak sama pada diambil, diajar, diurus.
28
+ */
29
+ export const KATA_SENDI = new Map([
30
+ ['di', 'د'], // د
31
+ ['ke', 'ک'], // ک
32
+ ])
33
+
34
+ export function bolehCantumSendi(kata) {
35
+ return KATA_SENDI.has(String(kata).toLowerCase())
36
+ }
37
+
38
+ /**
39
+ * Cantumkan kata sendi dengan ejaan Jawi kata yang mengikutinya.
40
+ * Pulangkan null jika kata itu bukan kata sendi yang dicakupi.
41
+ */
42
+ export function cantumSendi(kataSendi, jawiSelepas) {
43
+ const awalan = KATA_SENDI.get(String(kataSendi).toLowerCase())
44
+ if (!awalan || !jawiSelepas) return null
45
+
46
+ const selepas = jawiSelepas.startsWith(ALIF)
47
+ ? ALIF_HAMZAH + jawiSelepas.slice(ALIF.length)
48
+ : jawiSelepas
49
+
50
+ return { jawi: awalan + selepas, rujukan: 'Pedoman DBP §14.1' }
51
+ }
52
+
53
+ /**
54
+ * §11 — Kata serapan Arab dieja dalam dua cara berbeza:
55
+ * §11.1 istilah khusus -> ikut ejaan asal bahasa Arab (wuduk, Qur'an,
56
+ * sunah, hadis, tayamum, wuquf, reda)
57
+ * §11.2 kata umum yang -> ikut cara mengeja kata jati Melayu (sabun)
58
+ * sudah terserap
59
+ *
60
+ * Pakej ini tidak perlu memilih antara dua ini: DBP sudah memilih, dan
61
+ * pilihan itu terkandung dalam ejaan yang dipulangkan PRPM. Peraturan ini
62
+ * didokumenkan di sini supaya tiada siapa "membetulkan" صلاة kepada ejaan
63
+ * fonetik Melayu kemudian hari.
64
+ */
65
+ export const NOTA_SERAPAN_ARAB = 'Pedoman DBP §11.1 dan §11.2'
66
+
67
+ /**
68
+ * Songsangan §14.1: pisahkan kata sendi yang bercantum.
69
+ *
70
+ * Bentuk bercantum seperti دمکة tidak wujud sebagai entri kamus, jadi
71
+ * pembacaan songsang mesti mencubanya sendiri. Hamzah dipulihkan kepada
72
+ * alif kerana hamzah itu ditambah oleh peraturan, bukan sebahagian ejaan
73
+ * kata dasar.
74
+ */
75
+ export function pisahSendi(jawi) {
76
+ const calon = []
77
+ for (const [rumi, awalan] of KATA_SENDI) {
78
+ if (!jawi.startsWith(awalan) || jawi.length <= awalan.length) continue
79
+ let baki = jawi.slice(awalan.length)
80
+ if (baki.startsWith(ALIF_HAMZAH)) baki = ALIF + baki.slice(ALIF_HAMZAH.length)
81
+ calon.push({ sendi: rumi, baki, rujukan: 'Pedoman DBP §14.1' })
82
+ }
83
+ return calon
84
+ }
package/src/tukar.js CHANGED
@@ -11,6 +11,8 @@
11
11
  import { lookup } from './prpm.js'
12
12
  import { normalisasiJawi } from './kamus.js'
13
13
  import { cariDataset, muatDataset } from './dataset.js'
14
+ import { cariNama, mungkinNamaKhas } from './nama.js'
15
+ import { bolehCantumSendi, cantumSendi, pisahSendi } from './pedoman.js'
14
16
 
15
17
  // Baris tanda vokal Arab (fathah, kasrah, dammah, sukun, syaddah, tanwin,
16
18
  // alef superscript). Jawi Melayu TIDAK menggunakannya; teks al-Quran dan
@@ -130,6 +132,8 @@ export async function keJawi(teks, cache, opts = {}) {
130
132
  const dariDataset = []
131
133
  const dbp = []
132
134
  const adaEntriTanpaJawi = []
135
+ const dariNama = []
136
+ const namaKhas = []
133
137
  const bahagian = teks.split(PECAH_RUMI)
134
138
  const keluar = []
135
139
 
@@ -155,6 +159,14 @@ export async function keJawi(teks, cache, opts = {}) {
155
159
  }
156
160
  }
157
161
 
162
+ // Nama khas yang pengguna sudah sahkan sendiri.
163
+ const nm = await cariNama(cebis)
164
+ if (nm?.jawi) {
165
+ dariNama.push({ kata: cebis, jawi: nm.jawi })
166
+ keluar.push(nm.jawi)
167
+ continue
168
+ }
169
+
158
170
  if (cubaDasar) {
159
171
  const dasar = await cariDasar(cebis.toLowerCase(), cache, opts)
160
172
  if (dasar) {
@@ -166,18 +178,43 @@ export async function keJawi(teks, cache, opts = {}) {
166
178
  }
167
179
  }
168
180
 
169
- belumDisahkan.push(cebis)
181
+ // Nama khas yang belum disahkan bukan salah eja. Ia dilaporkan
182
+ // berasingan supaya pengguna tahu apa yang perlu disahkan sekali, dan
183
+ // bukan mengulang menyemak ejaan yang memang betul.
184
+ if (mungkinNamaKhas(cebis, bahagian[i - 1])) namaKhas.push(cebis)
185
+ else belumDisahkan.push(cebis)
170
186
  keluar.push(penanda[0] + cebis + penanda[1])
171
187
  }
172
188
 
189
+ // Pedoman DBP §14.1: kata sendi di dan ke bercantum dengan kata berikutnya.
190
+ // Dibuat sebagai lulusan kedua supaya logik carian di atas kekal mudah.
191
+ const sendiDicantum = []
192
+ for (let k = 1; k < keluar.length - 2; k += 2) {
193
+ const asal = bahagian[k]
194
+ if (!bolehCantumSendi(asal)) continue
195
+ if (!/^\s+$/.test(keluar[k + 1])) continue
196
+ const seterusnya = keluar[k + 2]
197
+ // Hanya cantum bila kata berikutnya benar-benar bertukar, bukan ditanda.
198
+ if (!seterusnya || seterusnya.startsWith(penanda[0])) continue
199
+ const gabung = cantumSendi(asal, seterusnya)
200
+ if (!gabung) continue
201
+ sendiDicantum.push({ sendi: asal, jawi: gabung.jawi, rujukan: gabung.rujukan })
202
+ keluar[k] = gabung.jawi
203
+ keluar[k + 1] = ''
204
+ keluar[k + 2] = ''
205
+ }
206
+
173
207
  return {
174
208
  teks: keluar.join(''),
209
+ sendiDicantum,
175
210
  disahkanDbp: dbp.length,
176
211
  dariDataset,
212
+ dariNama,
213
+ namaKhas: [...new Set(namaKhas)],
177
214
  adaEntriTanpaJawi: [...new Set(adaEntriTanpaJawi)],
178
215
  belumDisahkan: [...new Set(belumDisahkan)],
179
216
  terbitanSah,
180
- lengkap: belumDisahkan.length === 0 && terbitanSah.length === 0,
217
+ lengkap: belumDisahkan.length === 0 && terbitanSah.length === 0 && namaKhas.length === 0,
181
218
  // Sepenuhnya DBP bermakna setiap perkataan datang dari sumber rasmi.
182
219
  semuanyaDbp: dariDataset.length === 0 && belumDisahkan.length === 0 && terbitanSah.length === 0,
183
220
  }
@@ -195,6 +232,7 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
195
232
  const tidakDikenali = []
196
233
  const kabur = []
197
234
  const arab = []
235
+ const sendiDipisah = []
198
236
  const bahagian = teksJawi.split(PECAH_JAWI)
199
237
  const keluar = []
200
238
 
@@ -232,6 +270,15 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
232
270
  }
233
271
 
234
272
  if (!padanan) {
273
+ // Pedoman DBP §14.1 songsang: bentuk bercantum seperti دمکة tiada
274
+ // dalam kamus, jadi cuba pisahkan kata sendi di hadapan.
275
+ let dipisah = null
276
+ for (const calon of pisahSendi(bahagian[i])) {
277
+ const jumpa = peta.get(normalisasiJawi(calon.baki))
278
+ if (jumpa?.length === 1) { dipisah = calon.sendi + ' ' + jumpa[0]; break }
279
+ }
280
+ if (dipisah) { keluar.push(dipisah); sendiDipisah.push(bahagian[i]); continue }
281
+
235
282
  tidakDikenali.push(bahagian[i])
236
283
  keluar.push(penanda[0] + bahagian[i] + penanda[1])
237
284
  continue
@@ -250,6 +297,7 @@ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null
250
297
  tidakDikenali: [...new Set(tidakDikenali)],
251
298
  kabur,
252
299
  arab: [...new Set(arab)],
300
+ sendiDipisah,
253
301
  saizPeta: peta.size,
254
302
  lengkap: tidakDikenali.length === 0,
255
303
  }