Untuk mengubah teks bertanda kurung bersarang menjadi pohon array, pindai dari kiri ke kanan, buat array untuk setiap tanda kurung buka, lalu kembali ke induknya pada setiap tanda kurung tutup. Panduan ini menyediakan parser JavaScript lengkap yang dapat Anda jalankan secara lokal di Safari pada Mac.
Tugas di sini adalah mengonversi kerangka kecil berisi label yang disusun sendiri menjadi array bersarang, sambil mempertahankan urutan, teks dalam tanda kutip, dan grup kosongnya. Semua contoh menggunakan string sintetis. Parser ini menyertakan aturan tata bahasa dan pemeriksaan eksplisit, sehingga Anda dapat memastikan apakah struktur yang dihasilkan sesuai dengan yang Anda maksud.
Tentukan arti tanda kurung dan label dalam tanda kutip
Sebelum mengonversi apa pun, bedakan tanda kurung struktural dari tanda kurung yang menjadi bagian dari label. Dalam contoh ini, tanda kurung terluar membentuk grup, sedangkan tanda kurung di dalam label bertanda kutip ganda tetap menjadi teks:
(desk ("blue (round)" "say \"hello\"") () cards)
Struktur array yang dimaksud adalah:
[
[
"desk",
["blue (round)", "say \"hello\""],
[],
"cards"
]
]
Array terluar tambahan itu disengaja: array tersebut mewakili keseluruhan dokumen. Setiap grup bertanda kurung menjadi satu array di dalamnya. Dengan demikian, tipe nilai yang dikembalikan tetap konsisten, baik masukan berisi satu grup, beberapa grup, maupun label tanpa tanda kurung yang mengapitnya.
Aturan berikut menetapkan format masukan artikel ini. Ini adalah pilihan desain untuk parser ini, bukan format teks bertanda kurung bawaan JavaScript.
| Fitur masukan | Arti dalam pohon yang dihasilkan |
|---|---|
( dan ) di luar tanda kutip ganda |
Memulai dan mengakhiri array bersarang |
| Spasi di luar tanda kutip ganda | Memisahkan label; pemisahnya tidak disimpan |
Token tanpa tanda kutip seperti cards |
Menghasilkan sebuah string |
String bertanda kutip ganda seperti "blue tile" |
Menghasilkan satu string, termasuk spasi di dalamnya |
() |
Menghasilkan array kosong |
"" |
Menghasilkan string kosong |
12, true, atau null di luar tanda kutip |
Mempertahankan ejaannya sebagai string |
| Koma dan tanda kutip tunggal | Memperlakukannya sebagai karakter biasa dalam token tanpa tanda kutip |
Tanda kurung juga memisahkan token, jadi a(b)c menjadi ["a", ["b"], "c"]. Dua label yang bersebelahan memerlukan spasi: "a"b akan ditolak. Jika label berisi spasi, tanda kutip ganda, atau tanda kurung literal, gunakan bentuk bertanda kutip ganda.
Token bertanda kutip ganda mengikuti sintaks string JSON. Parser meneruskan setiap token bertanda kutip yang lengkap ke JSON.parse(), yang menerima teks JSON dan melempar SyntaxError jika sintaksnya tidak valid. Dengan begitu, validasi escape ditangani oleh parser yang terdokumentasi. Spesifikasi ECMAScript: JSON.parse
Buka ruang kerja JavaScript lokal di Safari
Web Inspector Safari menyediakan prompt JavaScript yang diperlukan untuk transformasi desktop ini. Anda tidak memerlukan instalasi paket atau proyek terpisah.
- Di Safari, pilih
Safari→Settings…→Advanced. - Aktifkan
Show features for web developers. - Buka tab dan masukkan
about:blankdi bilah alamat. - Pilih
Develop→Show Web Inspector, atau tekanOption–Command–I. - Pilih tab
Consoledan gunakan prompt inputnya.
WebKit mendokumentasikan pengaturan pengembang dan pintasan inspector di Mengaktifkan Web Inspector. Referensi Antarmuka Web Inspector menjelaskan bahwa prompt konsol menjalankan JavaScript dan menampilkan hasilnya.
Tempel seluruh program di bawah ini ke prompt tersebut, lalu jalankan. Program ini mencakup parser, contoh yang disusun sendiri, enam pemeriksaan masukan valid, dan tujuh pemeriksaan masukan tidak valid. Tidak diperlukan impor. Program ini berhasil dijalankan di Node.js 20.19.2; langkah antarmuka Safari didokumentasikan, tetapi tidak diuji secara langsung untuk artikel ini.
Jalankan parser teks bertanda kurung lengkap
Parser ini memelihara tumpukan array yang terbuka. Tanda kurung buka membuat array anak dan menempatkannya di tumpukan. Tanda kurung tutup mengeluarkan array yang sedang aktif dari tumpukan. Label ditambahkan ke array yang sedang terbuka.
function parenthesizedToTree(text) {
if (typeof text !== "string") {
throw new TypeError("Expected a string");
}
const root = [];
const stack = [{ items: root, start: -1 }];
const separator = c => /\s|[()]/u.test(c);
let i = 0;
function fail(message, at = i) {
throw new SyntaxError(message + " at UTF-16 offset " + at);
}
while (i < text.length) {
const c = text[i];
const current = stack[stack.length - 1].items;
if (/\s/u.test(c)) {
i++;
} else if (c === "(") {
const child = [];
current.push(child);
stack.push({ items: child, start: i });
i++;
} else if (c === ")") {
if (stack.length === 1) fail("Unexpected closing parenthesis");
stack.pop();
i++;
} else if (c === '"') {
const start = i++;
let closed = false;
while (i < text.length) {
if (text[i] === "\\") {
i += 2;
} else if (text[i] === '"') {
i++;
closed = true;
break;
} else {
i++;
}
}
if (!closed) fail("Unclosed quoted string", start);
let value;
try {
value = JSON.parse(text.slice(start, i));
} catch {
fail("Invalid JSON string", start);
}
if (i < text.length && !separator(text[i])) {
fail("Expected whitespace or a parenthesis");
}
current.push(value);
} else {
const start = i;
while (i < text.length && !separator(text[i])) {
if (text[i] === '"') fail("Quote inside an unquoted token");
i++;
}
current.push(text.slice(start, i));
}
}
if (stack.length !== 1) {
fail("Unclosed opening parenthesis", stack[stack.length - 1].start);
}
return root;
}
const input = String.raw`(desk ("blue (round)" "say \"hello\"") () cards)`;
console.log(JSON.stringify(parenthesizedToTree(input), null, 2));
const validCases = [
["", []],
["()", [[]]],
["a(b)c", ["a", ["b"], "c"]],
['("a b" "")', [["a b", ""]]],
["(12 true)", [["12", "true"]]],
[String.raw`("a\\b" "\u0061" "$&")`, [["a\\b", "a", "$&"]]]
];
for (const [source, expected] of validCases) {
const actual = parenthesizedToTree(source);
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error("Unexpected tree for " + source);
}
}
const invalidCases = [
"(a", "a)", '("a)', String.raw`("\q")`,
'"a"b', 'a"b"', '("line\nbreak")'
];
for (const source of invalidCases) {
let rejected = false;
try {
parenthesizedToTree(source);
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
rejected = true;
}
if (!rejected) throw new Error("Expected rejection for " + source);
}
console.log("All 13 checks passed.");
Hasil cetak pertama adalah struktur bersarang yang ditampilkan sebelumnya, diformat dalam beberapa baris. Pesan terakhir seharusnya berbunyi All 13 checks passed. Fungsi ini mengembalikan array JavaScript yang sebenarnya; JSON.stringify() mengubah nilai itu menjadi teks yang dapat dicetak, dan argumen ketiganya menentukan indentasi. Spesifikasi ECMAScript: JSON.stringify
Untuk mengonversi kerangka sintetis lain, ubah hanya sampel yang ditetapkan ke input, lalu jalankan ulang program. Biarkan pemeriksaannya tetap agar terus memverifikasi perilaku parser yang dimaksud. Salin teks array yang dicetak ke dokumen kerja jika Anda memerlukan representasi tekstual.
Pertahankan semantik tanda kutip dan escape
Ada dua lapisan yang perlu dipertimbangkan: sumber JavaScript yang memuat sampel dan teks bertanda kurung yang sedang diurai. Sampel menggunakan templat bertag String.raw agar garis miring terbaliknya diteruskan ke parser tanpa terlebih dahulu diurai sebagai escape string biasa pada lapisan sumber JavaScript. Operasi yang terdokumentasi membaca segmen mentah templat tersebut. Spesifikasi ECMAScript: String.raw
Pertahankan pembatas backtick di sekitar sampel yang disediakan. Untuk alur penyuntingan sederhana ini, susun teks sampel tanpa backtick atau pembuka interpolasi templat ${, karena keduanya memiliki arti dalam sintaks templat JavaScript.
Di dalam token masukan bertanda kutip ganda, \" mewakili tanda kutip ganda literal, \\ mewakili satu garis miring terbalik, dan \u0061 mewakili a. Nilai string ini telah diurai: pohon mempertahankan makna label, sedangkan ejaan escape asli dan tanda kutip yang mengapitnya dibuang. Di luar tanda kutip ganda, parser khusus ini memperlakukan garis miring terbalik sebagai karakter token biasa.
Sebagai contoh, pemeriksaan masukan valid terakhir memastikan bahwa token masukan "a\\b", "\u0061", dan "$&" menjadi label yang berisi satu garis miring terbalik, label a, dan label literal $&. Jika Anda perlu mereproduksi spasi dan ejaan escape asli secara persis, array berisi string yang telah diurai tidaklah cukup; representasinya juga perlu menyimpan teks sumber atau posisi token.
Mengapa penggantian menyeluruh mengubah hasil
Mengganti setiap tanda kurung buka dengan kurung siku buka dan setiap tanda kurung tutup dengan kurung siku tutup lainnya tidak menerapkan tata bahasa ini. Cara itu juga akan mengubah tanda kurung di dalam "blue (round)", serta membiarkan label tanpa tanda kutip dan pemisah item yang hilang tetap tidak terselesaikan. Kesimpulan itu langsung terlihat dari sampel yang disusun sendiri.
Ada detail lain terkait penggantian: argumen string untuk penggantian menafsirkan urutan khusus seperti $&, yang menyisipkan teks yang cocok. Fungsi pengganti memberikan teks hasilnya tanpa langkah substitusi tersebut. Spesifikasi ECMAScript: replaceAll dan substitusi pengganti
Contoh terpisah ini menunjukkan perbedaannya:
const label = "tile";
console.log(label.replaceAll("tile", "$&")); // tile
console.log(label.replaceAll("tile", () => "$&")); // $&
Kedua hasil diverifikasi selama penyusunan. Parser pohon sepenuhnya menghindari semantik penggantian: parser menambahkan string yang telah diurai secara langsung ke array. Jika kelak Anda menambahkan langkah untuk mengganti nama label, lakukan penggantian pada label yang telah diurai dan tentukan secara eksplisit apakah teks pengganti perlu ditafsirkan atau disisipkan secara literal.
Cari penyebab keluaran yang tidak diharapkan sebelum menggunakan ulang pohon
Pemeriksaan yang disertakan berfungsi sebagai panduan keputusan ringkas. Pemeriksaan tersebut mencakup masukan kosong, grup kosong, grup dan label yang bersebelahan, spasi dalam tanda kutip, nilai yang hanya berupa string, escape, serta sintaks tidak valid. Gunakan gejala berikut untuk menemukan ketidakcocokan antara kerangka yang Anda maksud dan tata bahasa yang telah ditetapkan.
| Gejala | Penjelasan dan langkah berikutnya |
|---|---|
| Hasilnya memiliki satu array terluar tambahan | Akar mewakili dokumen. Pertahankan array itu kecuali langkah berikutnya secara eksplisit memerlukan tepat satu grup tingkat atas. |
| Sebuah frasa menjadi beberapa string | Spasi memisahkan label tanpa tanda kutip. Letakkan frasa di dalam tanda kutip ganda. |
| Koma tetap ada di dalam label | Koma adalah karakter biasa di sini. Gunakan spasi di antara label yang terpisah. |
| Tanda kurung tiba-tiba menjadi array bersarang | Letakkan tanda kurung literal di dalam label bertanda kutip ganda. |
Unexpected closing parenthesis |
Tanda kurung tutup tidak memiliki grup buka yang cocok. Periksa posisi yang dilaporkan. |
Unclosed opening parenthesis |
Ada grup yang masih terbuka di akhir. Posisi tersebut menunjukkan tanda kurung buka terdalam yang belum ditutup. |
Invalid JSON string |
Periksa apakah token bertanda kutip memiliki escape yang tidak valid atau karakter kontrol yang tidak di-escape. |
| Kesalahan pemisah terkait tanda kutip | Pisahkan label yang bersebelahan dengan spasi, atau apit seluruh label yang dimaksud dalam satu string bertanda kutip. |
Offset dalam implementasi ini dimulai dari nol dan menghitung unit kode UTF-16, sesuai dengan pengindeksan string JavaScript. Offset bukan nomor baris atau jumlah karakter yang terlihat. Untuk kesalahan validasi token bertanda kutip, posisi yang dilaporkan adalah awal token, jadi periksa seluruh label bertanda kutip.
Masukan tidak valid melempar pengecualian sebelum fungsi mengembalikan hasil. Meskipun array dirangkai secara internal selama pemindaian berlangsung, pemanggil tidak menerima pohon parsial. Parser ini menggunakan tumpukan eksplisit alih-alih pemanggilan fungsi rekursif, tetapi contoh ini tidak menetapkan ukuran dokumen atau kedalaman bersarang maksimum yang didukung.
Untuk kerangka kecil di desktop, urutan praktisnya adalah menyepakati tata bahasanya, menjalankan sampel dan pemeriksaan tanpa perubahan, lalu menggantinya dengan teks yang Anda susun. Pastikan label bertanda kutip, grup kosong, dan akar dokumen sudah benar sebelum menggunakan ulang hasilnya; ketiga detail itu menentukan apakah pohon array mempertahankan struktur yang Anda maksud.
Sumber
- Mengaktifkan Web Inspector — WebKit.
- Antarmuka Web Inspector — WebKit.
- Spesifikasi Bahasa ECMAScript® 2027: Data Terstruktur — Ecma International / TC39, draf yang terus diperbarui.
- Spesifikasi Bahasa ECMAScript® 2027: Pemrosesan Teks — Ecma International / TC39, draf yang terus diperbarui.