Skip to content

Konverter dua arah: konservatif saat push, toleran saat pull

Adityo Guni Waluyo

Bug garis horizontal yang hilang diam-diam mengungkap desain konverter dua arah yang sehat: push kanonik, pull toleran tapi tetap menolak keras.

Ringkasan

Gue kira Plane yang ngapus garis horizontal pas pull, ternyata parser pull gue sendiri yang diam-diam nelen <hr> karena cuma kenal <hr />. Sekarang yang nggak dikenal langsung dilempar error pakai nomor baris, nggak lagi hilang senyap. Dari situ desainnya dibikin push yang konservatif dan pull yang toleran tapi cuma di dialek sendiri, plus tes round-trip biar aman.

Saya jalankan pull di alat sinkronisasi dokumen, dan outputnya kelihatan bersih: nggak ada error, nggak ada peringatan. Pas saya buka halamannya di Plane, garis horizontal yang tadi saya tambahin manual di editor Plane lenyap dari file hasil pull. Elemen itu nggak berubah jadi teks aneh atau salah format; dia benar-benar hilang dari file markdown hasil pull.

Dugaan pertama saya ya editor Plane yang nge-strip elemen itu pas nyimpan. Saya bahkan sempat curiga ada script pembersih yang jalan di sisi server. Habis waktu nggak sedikit buat nyimak log, semuanya normal. Tebakan saya meleset jauh, dan pelaku paling terakhir yang saya duga: parser pull di konverter html markdown round trip milik saya sendiri.

Parser ini subclass dari HTMLParser bawaan Python, dan di sinilah kejutannya. Python memperlakukan <hr> dan <hr /> lewat callback yang beda: bentuk XHTML-style memicu handle_startendtag [1], sedangkan tag pembuka biasa masuk ke handle_starttag. Kode lama cuma mentolerir bentuk self-closing. Jadi garis horizontal yang saya tulis dengan bentuk yang justru paling umum itu ditelan diam-diam, tanpa exception, tanpa log.

Perbaikannya justru memperketat, bukan melonggarkan. Bentuk bare sekarang melempar UnsupportedMarkdownError lengkap dengan nomor baris HTML-nya, dan parser tetap cuma menerima bentuk self-closing untuk elemen void itu [2]. Bedanya sekarang: yang dulu hilang tanpa suara, sekarang menolak dengan keras. Aturannya sengaja dibegini: pull nggak boleh menebak.

Perbaikan ini buka mata saya ke desain dua arah yang lebih sehat secara keseluruhan. Alat ini memirror file markdown dari folder .docs ke Plane Pages dan balik lagi ke repo. Konverter html markdown round trip yang benar ternyata nggak simetris, dan justru di ketidak-simetrisan itu pelajarannya.

Sisi konservatif dan sisi toleran

Sisi push berperan sebagai serializer konservatif. Fungsi inline_to_html menyimpan code span dan link sebagai token yang dibungkus karakter NUL dulu, baru sisa teksnya di-escape. Hasilnya, karakter ampersand dan kurung sudut di dalam code span atau link di-escape tepat satu kali. Teks biasa hanya melewati escape untuk ampersand dan kurung sudut dengan quote=False, jadi tanda kutip tetap literal di dalam teks elemen; cuma nilai atribut href yang meng-escape kutip.

def inline_to_html(text):
    text = stash_code_spans(text)   # \x00c0\x00, dst
    text = stash_links(text)        # \x00l0\x00, dst
    text = escape(text, quote=False)
    return unstash(bold(italic(text)))

Emisinya juga dipatok. Code fence membawa atribut data-lang, sel tabel di-escape sebagai teks polos, dan gambar diubah jadi paragraf placeholder yang terbaca manusia, karena Plane Community Edition nggak punya attachment. Pull tinggal memetakan paragraf placeholder itu balik ke sintaks gambar.

Sisi pull beda sikap. Ia toleran, tapi cuma di dalam dialek yang dikenalnya. Tag div diperlakukan sebagai kontainer transparan, br di dalam paragraf dibaca sebagai line break, nested inline dan nested list di-un-nest balik ke markdown. Dialek yang sama itu diproduksi sisi push, plus hasil rapian editor bawaan. Segala hal di luar dialek itu ditolak dengan exception yang menyebut nomor baris HTML-nya.

Satu bug lagi yang kebagian perbaikan di commit yang sama, dan ini yang paling saya suka. Pemetaan placeholder gambar dulu aktif untuk paragraf apa pun yang baris pertamanya cocok dengan teks placeholder. Baris kedua dan seterusnya di paragraf itu dibuang diam-diam. Sekarang pemetaan itu cuma berlaku kalau paragrafnya tepat satu bagian teks; kalau lebih, paragrafnya di-round-trip utuh apa adanya.

Postel yang dibelah dua

Kasus-kasus di atas nyambung ke hukum Postel yang sering dikutip dari RFC 791: bersikap konservatif saat mengirim, liberal saat menerima [3]. Prinsipnya nggak salah; yang bikin bahaya itu tafsir bebasnya, yaitu liberal yang diartikan sebagai diam-diam membuang apa yang nggak dipahami. Toleransi tanpa suara baru kerasukan berminggu-minggu kemudian, biasanya pas ada yang nyadar kontennya berkurang.

Solusi di konverter ini membelah prinsip itu per sisi. Output push tetap kanonik dan konservatif. Sikap liberal di pull cuma berlaku di dalam dialek yang dikenal; di luarnya, penolakan keras dengan nomor baris. Jadi kalau suatu hari editor Plane mengubah cara dia merapikan HTML, yang muncul itu error yang menyebut barisnya, duluan sebelum kontennya berubah.

Jaring pengaman round-trip

Jaring pengamannya juga bukan unit test biasa. Ada invariant round-trip yang diuji sebagai property: markdown diubah ke HTML, balik lagi ke markdown, harus ternormalisasi ke teks awal. Konverter ini juga dijalankan di atas korpus dokumen asli, pola klasik property testing dengan korpus dan oracle, persis contoh formatter-over-corpus yang dibahas Hypothesis [4].

Dua bug kecil di satu commit fix ternyata melahirkan keputusan desain yang lebih besar: toleransi itu bukan berarti terima segalanya. Kalau konverter dua arah yang kamu pakai pernah "memakan" konten tanpa jejak, cek dulu callback mana yang sebenarnya menerima tag itu. Sering kali HTML-nya benar; asumsi soal callback parser itulah yang salah.

Sumber

Artikel terkait