Baru-baru ini saya menjadi tertarik pada bagaimana penyorotan kode diatur dari dalam. Pada awalnya kelihatannya semuanya sangat rumit di sana - pohon sintaksis, rekursi, dan itu saja. Namun, setelah diperiksa lebih dekat, ternyata tidak ada yang sulit di sini. Semua pekerjaan dapat dilakukan dalam satu siklus dengan mengintip bolak-balik, apalagi, ekspresi reguler hampir tidak pernah digunakan dalam skrip yang dihasilkan.
Halaman Demo:
Highlighter Kode JavascriptIde utama
Kami mendeklarasikan variabel
status , yang akan menyimpan informasi tentang bagian kode mana yang kita gunakan. Jika, misalnya,
negara sama dengan satu, maka ini berarti bahwa kita berada di dalam string dengan tanda kutip tunggal. Script akan menunggu kutipan penutup dan mengabaikan yang lainnya. Hal yang sama dengan menyoroti komentar, regexp, dan elemen lainnya, masing-masing memiliki nilai statusnya sendiri. Dengan demikian, karakter pembuka dan penutup yang berbeda tidak akan bertentangan; dengan kata lain, kode seperti ini:
let a = '"\'"';
akan disorot dengan benar, yaitu, kasus-kasus seperti itu menyebabkan kesulitan paling besar.
Memulai
Kami menentukan nilai yang mungkin dari variabel keadaan, serta warna di mana ini atau itu bagian dari kode akan dicat, serta daftar kata kunci Javascript (yang juga akan disorot):
negara const = {... const states = { NONE : 0, SINGLE_QUOTE : 1,
Selanjutnya, kita membuat fungsi yang akan mengambil garis dengan kode dan mengembalikan HTML yang sudah selesai dengan kode yang disorot. Untuk menyorot, karakter akan dibungkus SPAN dengan warna yang ditentukan dalam variabel
warna .
Fungsi hanya akan memiliki satu siklus, yang menganalisis setiap karakter dan menambahkan pembukaan / penutupan SPAN bila perlu.
function highlight(code) { let output = ''; let state = states.NONE; for (let i = 0; i < code.length; i++) { let char = code[i], prev = code[i-1], next = code[i+1];
Pertama, sorot komentar: single-line dan multi-line. Jika karakter saat ini dan selanjutnya adalah garis miring, dan mereka tidak berada di dalam garis (
negara adalah 0, yaitu,
menyatakan .
TIDAK ADA ), maka ini adalah awal dari komentar. Ubah
status dan buka SPAN dengan warna yang diinginkan:
if (state == states.NONE && char == '/' && next == '/') { state = states.SL_COMMENT; output += '<span style="color: ' + colors.SL_COMMENT + '">' + char; continue; }
diperlukan
terus sehingga pemeriksaan berikut tidak berfungsi dan konflik tidak terjadi.
Selanjutnya, kita menunggu akhir baris: jika karakter saat ini adalah satu baris dan dalam
keadaan komentar satu baris, tutup SPAN dan ubah
status menjadi nol:
if (state == states.SL_COMMENT && char == '\n') { state = states.NONE; output += char + '</span>'; continue; }
Demikian pula, kami mencari komentar multi-baris, algoritmenya persis sama, hanya karakter yang Anda cari berbeda:
if (state == states.NONE && char == '/' && next == '*') { state = states.ML_COMMENT; output += '<span style="color: ' + colors.ML_COMMENT + '">' + char; continue; } if (state == states.ML_COMMENT && char == '/' && prev == '*') { state = states.NONE; output += char + '</span>'; continue; }
Penandaan string terjadi dengan cara yang sama, hanya saja harus diperhitungkan bahwa tanda kutip penutup dapat diloloskan dengan backslash, dan dengan demikian, sudah tidak lagi menjadi slash penutup.
if (state == states.NONE && char == '\'') { state = states.SINGLE_QUOTE; output += '<span style="color: ' + colors.SINGLE_QUOTE + '">' + char; continue; } if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
Kode mirip dengan apa yang sudah di atas, hanya sekarang kita tidak mendaftarkan akhir baris jika ada backslash sebelum kutipan.
Definisi string yang dikutip ganda terjadi dengan cara yang persis sama, dan tidak masuk akal untuk menguraikannya secara detail. Untuk melengkapi gambar, saya akan menempatkannya di bawah spoiler.
if (state == States.NONE && char == '' '') {... if (state == states.NONE && char == '"') { state = states.DOUBLE_QUOTE; output += '<span style="color: ' + colors.DOUBLE_QUOTE + '">' + char; continue; } if (state == states.DOUBLE_QUOTE && char == '"' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; } if (state == states.NONE && char == '`') { state = states.ML_QUOTE; output += '<span style="color: ' + colors.ML_QUOTE + '">' + char; continue; } if (state == states.ML_QUOTE && char == '`' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
Literatur Regexp, yang mudah dikacaukan dengan tanda pembagian, patut dipertimbangkan secara terpisah. Kami akan kembali ke masalah ini di akhir artikel, tetapi untuk saat ini kami melakukan hal yang sama dengan regexps dengan string.
if (state == states.NONE && char == '/') { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; } if (state == states.REGEX_LITERAL && char == '/' && prev != '\\') { state = states.NONE; output += char + '</span>'; continue; }
Ini mengakhiri kasus sederhana ketika awal dan akhir literal dapat ditentukan oleh 1-2 karakter. Mari kita mulai menyoroti angka: seperti yang Anda tahu, mereka selalu mulai dengan angka, tetapi dapat memiliki huruf dalam komposisi (
0xFF ,
123n ).
if (state == states.NONE && /[0-9]/.test(char) && !/[0-9a-z$_]/i.test(prev)) { state = states.NUMBER_LITERAL; output += '<span style="color: ' + colors.NUMBER_LITERAL + '">' + char; continue; } if (state == states.NUMBER_LITERAL && !/[0-9a-fnx]/i.test(char)) { state = states.NONE; output += '</span>' }
Di sini kita mencari awal angka: karakter sebelumnya tidak boleh berupa angka atau huruf, jika tidak angka-angka dalam nama variabel akan disorot. Segera setelah karakter saat ini bukan angka atau huruf yang dapat terkandung dalam literal angka, tutup SPAN dan atur
status ke nol.
Semua jenis literal yang mungkin disorot, pencarian kata kunci tetap ada. Untuk melakukan ini, Anda memerlukan loop bersarang yang melihat ke depan dan menentukan apakah karakter saat ini adalah awal kata kunci.
if (state == states.NONE && !/[a-z0-9$_]/i.test(prev)) { let word = '', j = 0; while (code[i + j] && /[az]/i.test(code[i + j])) { word += code[i + j]; j++; } if (keywords.includes(word)) { state = states.KEYWORD; output += '<span style="color: ' + colors.KEYWORD + '">'; } }
Di sini kita melihat, karakter sebelumnya tidak boleh dalam nama variabel, jika tidak
biarkan kata kunci akan disorot di
outlet kata. Kemudian loop bersarang mengumpulkan kata terpanjang yang mungkin sampai karakter non-alfabet ditemukan. Jika kata yang diterima ada dalam susunan
kata kunci , buka SPAN dan mulailah menyorot kata tersebut. Segera setelah karakter non-alfabet ditemukan, ini berarti akhir kata - karenanya, tutup SPAN:
if (state == states.KEYWORD && !/[az]/i.test(char)) { state = states.NONE; output += '</span>'; }
Hal yang paling sederhana tetap ada - penyorotan operator, di sini Anda dapat membandingkan dengan serangkaian karakter yang dapat terjadi di operator:
if (state == states.NONE && '+-/*=&|%!<>?:'.indexOf(char) != -1) { output += '<span style="color: ' + colors.OPERATOR + '">' + char + '</span>'; continue; }
Pada akhir loop, jika tidak ada kondisi yang
melanjutkan penyebab yang dipicu, kami cukup menambahkan karakter saat ini ke variabel yang dihasilkan. Ketika awal atau akhir literal atau kata kunci terjadi, kami membuka / menutup SPAN dengan warna; dalam semua kasus lain - misalnya, ketika garis sudah terbuka, kita hanya membuang satu karakter sekaligus. Anda juga perlu melindungi kurung sudut bukaan, jika tidak mereka dapat merusak tata letak.
output += char.replace('<', '&' + 'lt;');
Perbaikan bug
Semuanya tampak entah bagaimana terlalu sederhana, dan tidak sia-sia: dengan pengujian yang lebih teliti, ada kasus di mana lampu latar tidak bekerja dengan benar.
Divisi ini diakui sebagai regexp, untuk membedakan satu dari yang lain, perlu mengubah cara regexp ditentukan. Kami mendeklarasikan variabel
isRegex = true , setelah itu kami akan mencoba untuk βmembuktikanβ bahwa ini bukan regexp, tetapi sebuah tanda pembagian. Tidak boleh ada kata kunci atau tanda kurung buka sebelum operasi pembagian - oleh karena itu, kami membuat loop bersarang dan melihat apa yang garis miring hadapi.
Seperti sebelumnya if (state == states.NONE && char == '/') { state = states.REGEX_LITERAL; output += '<span style="color: ' + colors.REGEX_LITERAL + '">' + char; continue; }
if (state == states.NONE && char == '/') { let word = '', j = 0, isRegex = true; while (i + j >= 0) { j--;
Meskipun pendekatan ini menyelesaikan masalah, itu masih bukan tanpa cacat. Anda dapat menyesuaikannya sehingga algoritme ini juga menyoroti secara tidak benar, misalnya:
jika (a) / regex / atau lebih:
1 / / regex / / 2 . Mengapa seseorang yang membagi angka menjadi regexp perlu menyoroti kode - ini adalah pertanyaan lain; desainnya secara sintaksis benar, meskipun tidak terjadi dalam kehidupan nyata.
Ada masalah dengan pewarnaan regexp dalam banyak karya, misalnya di
prism.js . Tampaknya, untuk menyoroti regexps yang benar, Anda harus sepenuhnya memahami sintaksisnya, seperti halnya peramban.
Bug kedua yang harus saya tangani terkait dengan backslash. Tanda kutip penutup tidak dikenali dalam string bentuk
'tes \\' karena adanya backslash di depannya. Kembali ke kondisi yang menangkap ujung garis:
if (state == states.SINGLE_QUOTE && char == '\'' && prev != '\\')
Bagian terakhir dari kondisi ini perlu diubah: jika garis miring terbalik (yaitu, ada garis miring terbalik yang lain sebelum itu), kemudian daftarkan ujung baris.
const closingCharNotEscaped = prev != '\\' || prev == '\\' && code[i-2] == '\\';
Penggantian yang sama harus dilakukan dalam mencari string dengan tanda kutip ganda dan terbalik, serta dalam pencarian regexp.
Itu saja, Anda dapat menguji sorot dengan tautan di awal artikel.