prpm-dbp 0.2.2 → 0.3.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/SKILL.md CHANGED
@@ -73,6 +73,24 @@ Jawi Melayu menulis vokal dengan alif, wau dan ya, tidak seperti Arab. Kerana it
73
73
 
74
74
  `rumi` bekerja dari cache sahaja dan tidak menyentuh rangkaian. Perkataan yang belum pernah disemak dalam arah Rumi ke Jawi tidak akan dikenali. Jalankan `warm` atau `audit` dahulu pada kosa kata yang berkaitan.
75
75
 
76
+ ## Istilah Arab
77
+
78
+ Kebanyakan istilah agama ialah **kata Melayu**, bukan kata asing. Diuji terhadap PRPM: 29 daripada 30 istilah lazim ada entri, termasuk `yasin`, `fatihah`, `baqarah`, `rahman`, `rasulullah`. Jangan andaikan sesuatu itu Arab semata-mata kerana ia berbunyi Arab; tanya DBP dahulu.
79
+
80
+ Tiga keadaan berbeza, dilaporkan berasingan:
81
+
82
+ | Keadaan | Contoh | Maksud |
83
+ |---|---|---|
84
+ | Ada entri dan ejaan Jawi | `solat` صلاة, `zakat` زکاة | guna terus |
85
+ | `adaEntriTanpaJawi` | `yasin`, `fatihah`, `baqarah`, `wuduk` | DBP iktiraf sebagai kata Melayu, cuma tiada ejaan Jawi disimpan |
86
+ | `belumDisahkan` | `ahzab` | bukan kata Melayu; nama khas Arab |
87
+
88
+ Ejaan Jawi bagi pinjaman Arab **mengekalkan ejaan Arab asal**, bukan dieja secara fonetik: `صلاة` bukan `سولت`, `زکاة` bukan `زکات`. Ta marbutah dan alef maksura dikekalkan. Sebab itu ta marbutah tidak pernah dilipat semasa perbandingan.
89
+
90
+ Sesetengah entri memulangkan **dua perkataan Jawi untuk satu perkataan Rumi**: `rasulullah` → `رسول الله`. Pembacaan songsang memadan jujukan, jadi ia tidak pecah menjadi `rasul allah`.
91
+
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
+
76
94
  ## Rantai kepercayaan
77
95
 
78
96
  Setiap ejaan Jawi yang dikeluarkan membawa asal-usulnya. Jangan runtuhkan lapisan ini.
package/bin/cli.js CHANGED
@@ -154,6 +154,12 @@ async function main() {
154
154
  if (h.belumDisahkan.length) {
155
155
  console.error(`\nbelum disahkan (${h.belumDisahkan.length}): ${h.belumDisahkan.join(', ')}`)
156
156
  }
157
+ if (h.adaEntriTanpaJawi.length) {
158
+ console.error(
159
+ `DBP ada entri tetapi tiada ejaan Jawi (${h.adaEntriTanpaJawi.length}): ` +
160
+ h.adaEntriTanpaJawi.join(', '),
161
+ )
162
+ }
157
163
  if (h.terbitanSah.length) {
158
164
  console.error(
159
165
  `terbitan sah tanpa ejaan Jawi (${h.terbitanSah.length}): ` +
@@ -165,13 +171,14 @@ async function main() {
165
171
 
166
172
  case 'rumi': {
167
173
  const teks = await readFile(baki[0], 'utf8')
168
- const h = keRumi(teks, cache)
174
+ const h = keRumi(teks, cache, { dataset: await muatDataset() })
169
175
  if (pilihan.json) { keluar(h); break }
170
176
  console.log(h.teks)
171
177
  console.error(`\npeta songsang: ${h.saizPeta} ejaan`)
172
178
  if (h.tidakDikenali.length) {
173
179
  console.error(`tidak dikenali (${h.tidakDikenali.length}): ${h.tidakDikenali.join(', ')}`)
174
180
  }
181
+ if (h.arab.length) console.error(`teks Arab dibiarkan (${h.arab.length}): ${h.arab.join(' ')}`)
175
182
  for (const k of h.kabur) console.error(`kabur: ${k.jawi} -> ${k.calon.join(', ')}`)
176
183
  break
177
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prpm-dbp",
3
- "version": "0.2.2",
3
+ "version": "0.3.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/tukar.js CHANGED
@@ -10,7 +10,13 @@
10
10
 
11
11
  import { lookup } from './prpm.js'
12
12
  import { normalisasiJawi } from './kamus.js'
13
- import { cariDataset } from './dataset.js'
13
+ import { cariDataset, muatDataset } from './dataset.js'
14
+
15
+ // Baris tanda vokal Arab (fathah, kasrah, dammah, sukun, syaddah, tanwin,
16
+ // alef superscript). Jawi Melayu TIDAK menggunakannya; teks al-Quran dan
17
+ // hadis yang dipetik dalam dokumen Melayu menggunakannya. Kehadirannya
18
+ // bermakna potongan itu bahasa Arab, bukan Jawi, dan tidak boleh ditukar.
19
+ const HARAKAT = /[\u064B-\u0652\u0670\u0653-\u0655]/
14
20
 
15
21
  const JAWI_JULAT = '؀-ۿݐ-ݿﭐ-﷿ﹰ-'
16
22
  // Token dipecah supaya pemisah dikekalkan, jadi tanda baca, baris baru dan
@@ -80,15 +86,36 @@ export async function cariDasar(kata, cache, opts = {}) {
80
86
  return null
81
87
  }
82
88
 
83
- /** Bina peta Jawi -> Rumi daripada segala yang sudah disahkan dalam cache. */
84
- export function petaSongsang(cache) {
89
+ /**
90
+ * Bina peta Jawi -> Rumi. Menggabungkan cache DBP dengan dataset yang
91
+ * diimport, jika ada: tanpa dataset, apa-apa yang ditukar melalui lapisan
92
+ * ketiga tidak boleh dipusingkan balik langsung.
93
+ *
94
+ * Sesetengah nilai mengandungi RUANG (rasulullah -> رسول الله). Peta menjejaki
95
+ * bilangan perkataan terpanjang supaya pembacaan songsang boleh memadan
96
+ * jujukan, bukan token tunggal sahaja.
97
+ */
98
+ export function petaSongsang(cache, dataset = null) {
85
99
  const peta = new Map()
86
- for (const rec of Object.values(cache)) {
87
- if (!rec?.jawi) continue
88
- const kunci = normalisasiJawi(rec.jawi)
100
+ let maxKata = 1
101
+
102
+ const tambah = (jawi, rumi) => {
103
+ const kunci = normalisasiJawi(jawi)
104
+ if (!kunci) return
105
+ const bilangan = kunci.split(/\s+/).length
106
+ if (bilangan > maxKata) maxKata = bilangan
89
107
  if (!peta.has(kunci)) peta.set(kunci, [])
90
- if (!peta.get(kunci).includes(rec.kata)) peta.get(kunci).push(rec.kata)
108
+ if (!peta.get(kunci).includes(rumi)) peta.get(kunci).push(rumi)
91
109
  }
110
+
111
+ for (const rec of Object.values(cache)) {
112
+ if (rec?.jawi) tambah(rec.jawi, rec.kata)
113
+ }
114
+ for (const [rumi, ejaan] of Object.entries(dataset?.peta ?? {})) {
115
+ for (const j of ejaan) tambah(j, rumi)
116
+ }
117
+
118
+ peta.maxKata = maxKata
92
119
  return peta
93
120
  }
94
121
 
@@ -102,6 +129,7 @@ export async function keJawi(teks, cache, opts = {}) {
102
129
  const terbitanSah = []
103
130
  const dariDataset = []
104
131
  const dbp = []
132
+ const adaEntriTanpaJawi = []
105
133
  const bahagian = teks.split(PECAH_RUMI)
106
134
  const keluar = []
107
135
 
@@ -111,6 +139,11 @@ export async function keJawi(teks, cache, opts = {}) {
111
139
  const rec = await lookup(cebis.toLowerCase(), cache, opts)
112
140
  if (rec?.jawi) { keluar.push(rec.jawi); dbp.push(cebis); continue }
113
141
 
142
+ // Keadaan berbeza daripada tiada entri: DBP mengiktiraf perkataan ini
143
+ // sebagai Melayu, cuma tidak menyimpan ejaan Jawinya. Lazim bagi nama
144
+ // surah dan istilah agama seperti yasin, fatihah, baqarah.
145
+ if (rec?.dijumpai) adaEntriTanpaJawi.push(cebis)
146
+
114
147
  // Lapisan ketiga: dataset yang diimport pengguna. Berlabel, tidak pernah
115
148
  // menyamar sebagai ejaan DBP.
116
149
  if (gunaDataset) {
@@ -141,6 +174,7 @@ export async function keJawi(teks, cache, opts = {}) {
141
174
  teks: keluar.join(''),
142
175
  disahkanDbp: dbp.length,
143
176
  dariDataset,
177
+ adaEntriTanpaJawi: [...new Set(adaEntriTanpaJawi)],
144
178
  belumDisahkan: [...new Set(belumDisahkan)],
145
179
  terbitanSah,
146
180
  lengkap: belumDisahkan.length === 0 && terbitanSah.length === 0,
@@ -156,32 +190,66 @@ export async function keJawi(teks, cache, opts = {}) {
156
190
  * menggugurkannya. Kerana itu perlanggaran jauh lebih jarang daripada yang
157
191
  * disangka. Bila ia berlaku, SEMUA calon dilaporkan, bukan dipilih satu.
158
192
  */
159
- export function keRumi(teksJawi, cache, { penanda = ['«', '»'] } = {}) {
160
- const peta = petaSongsang(cache)
193
+ export function keRumi(teksJawi, cache, { penanda = ['«', '»'], dataset = null } = {}) {
194
+ const peta = petaSongsang(cache, dataset)
161
195
  const tidakDikenali = []
162
196
  const kabur = []
197
+ const arab = []
163
198
  const bahagian = teksJawi.split(PECAH_JAWI)
164
199
  const keluar = []
165
200
 
166
- for (const [i, cebis] of bahagian.entries()) {
167
- if (i % 2 === 0) { keluar.push(cebis); continue }
201
+ for (let i = 0; i < bahagian.length; i++) {
202
+ if (i % 2 === 0) { keluar.push(bahagian[i]); continue }
203
+
204
+ // Teks Arab bertanda baris dibiarkan seadanya. Cuba menukarnya akan
205
+ // merosakkan petikan al-Quran dan hadis dalam dokumen Melayu.
206
+ if (HARAKAT.test(bahagian[i])) {
207
+ arab.push(bahagian[i])
208
+ keluar.push(bahagian[i])
209
+ continue
210
+ }
211
+
212
+ // Padanan jujukan terpanjang dahulu, supaya رسول الله ditemui sebagai
213
+ // satu perkataan dan bukan dipecah jadi rasul + allah.
214
+ let padanan = null
215
+ let hujung = i
216
+ // Had atas MESTI integer. Nilai pecahan menyebabkan n melompat 3.5, 2.5,
217
+ // 1.5 dan tidak pernah menyentuh bilangan perkataan sebenar, lalu hujung
218
+ // jatuh pada pemisah dan bukan token.
219
+ const maxN = Math.min(peta.maxKata, Math.floor((bahagian.length - i + 1) / 2))
220
+ for (let n = maxN; n >= 1; n--) {
221
+ const akhir = i + (n - 1) * 2
222
+ if (akhir >= bahagian.length || akhir % 2 === 0) continue
223
+ // Hanya gabung apabila pemisah antara token benar-benar ruang.
224
+ let bolehGabung = true
225
+ for (let k = i + 1; k < akhir; k += 2) {
226
+ if (!/^\s+$/.test(bahagian[k])) { bolehGabung = false; break }
227
+ }
228
+ if (!bolehGabung) continue
229
+ const frasa = bahagian.slice(i, akhir + 1).join(' ').replace(/\s+/g, ' ')
230
+ const jumpa = peta.get(normalisasiJawi(frasa))
231
+ if (jumpa) { padanan = jumpa; hujung = akhir; break }
232
+ }
168
233
 
169
- const padanan = peta.get(normalisasiJawi(cebis))
170
234
  if (!padanan) {
171
- tidakDikenali.push(cebis)
172
- keluar.push(penanda[0] + cebis + penanda[1])
173
- } else if (padanan.length === 1) {
174
- keluar.push(padanan[0])
175
- } else {
176
- kabur.push({ jawi: cebis, calon: padanan })
235
+ tidakDikenali.push(bahagian[i])
236
+ keluar.push(penanda[0] + bahagian[i] + penanda[1])
237
+ continue
238
+ }
239
+
240
+ if (padanan.length === 1) keluar.push(padanan[0])
241
+ else {
242
+ kabur.push({ jawi: bahagian.slice(i, hujung + 1).join(''), calon: padanan })
177
243
  keluar.push(padanan.join('|'))
178
244
  }
245
+ i = hujung
179
246
  }
180
247
 
181
248
  return {
182
249
  teks: keluar.join(''),
183
250
  tidakDikenali: [...new Set(tidakDikenali)],
184
251
  kabur,
252
+ arab: [...new Set(arab)],
185
253
  saizPeta: peta.size,
186
254
  lengkap: tidakDikenali.length === 0,
187
255
  }