Ders 04 / 25
Şablon ve İfade Sözdizimi
İşaretlemenin veriye bağlanması; şablonun bir kez derlenip çok kez çalıştırılması, sabit ve devingen parçaların ayrılması, ifadelerin kapsamı ile sınırı, metin kaçırma ve öznitelik–özellik bağlaması ayrımı.
İçindekiler
Önceki dersin bileşenleri görünüm tanımını iç içe işlev çağrılarıyla kurdu. Bir ölçüm tablosunun bütün yapısı bu biçimde yazıldığında işaretlemenin şekli kaybolur: hangi ögenin nerede kapandığı, hangi özniteliğin hangi ögeye ait olduğu çağrı parantezleri arasında okunur hâle gelmez.
Bu ders işaretlemenin kendi söz diziminde yazılıp veriye bağlanmasını ele alır. Kurulacak mekanizmanın adı şablon (template) dur: içinde ifade taşıyan bir işaretleme metni ve onu görünüm tanımı üreten bir işleve çeviren bir derleme adımı.
İki Yazım Ailesi
İşaretlemeyi programa sokmanın iki yolu vardır ve çerçeve aileleri bu ayrımla bölünür.
Birinci yolda işaretleme dilin içine gömülür: programlama dilinin söz dizimi genişletilir, işaretleme dilin bir ifadesi hâline gelir. İfade yeri diye ayrı bir kavram yoktur; işaretlemenin içindeki her şey zaten dilin ifadesidir. Kazanç, dilin bütün olanaklarının doğrudan kullanılabilmesidir.
İkinci yolda şablon ayrı bir metindir: işaretlemeye özel imler eklenir ve bu metin derlenerek işleve çevrilir. Şablon dili dilden küçüktür; yalnızca ifadelere ve birkaç yönergeye izin verir. Kazanç, küçük dilin çözümlenebilir olmasıdır: derleyici hangi parçanın sabit hangisinin devingen olduğunu bilir ve bu bilgiyle iş yapabilir.
İki yol da aynı yere varır: durumdan görünüm tanımı üreten bir işlev. Fark, bu işlevin ne zaman ve ne kadar bilgiyle üretildiğidir.
Şablonun Derlenmesi
Aşağıdaki derleyici ikinci yolun küçük bir örneğidir. Şablon bir kez çözümlenir, sonuç bir komut ağacına dönüşür; her oluşturmada yalnızca bu ağaç çalıştırılır.
// sablon-derleyici.mjs — sablon metnini gorunum ureten bir isleve derler const SABLON = ` <div class="rozet" :durum="deger >= esik ? 'asim' : 'normal'" @tikla="ac"> <span class="ad">{{ad}}</span> <span class="deger">{{deger}} {{birim}}</span> </div>`.trim(); // 1) Sozcuksel cozumleme: etiketler ve metin parcalari. // Etiket eslemesi tirnak icindeki ">" karakterini atlar. const ETIKET = /<\/[a-z][\w-]*>|<[a-z][\w-]*(?:\s+[:@]?[\w-]+(?:="[^"]*")?)*\s*>|[^<]+/g; const parcala = (metin) => metin.match(ETIKET) ?? []; // 2) Metni sabit dilim ve ifade olarak ayirir: "{{ad}}: {{deger}}" → 4 parca. function metniAyir(metin) { const parcalar = []; let son = 0; for (const eslesme of metin.matchAll(/\{\{([^}]+)\}\}/g)) { if (eslesme.index > son) parcalar.push({ tur: "sabit", deger: metin.slice(son, eslesme.index) }); parcalar.push({ tur: "ifade", kaynak: eslesme[1].trim() }); son = eslesme.index + eslesme[0].length; } if (son < metin.length) parcalar.push({ tur: "sabit", deger: metin.slice(son) }); return parcalar; } // 3) Oznitelikleri uce ayirir: sabit, bagli (:) ve olay (@). function oznitelikleriAyir(govde) { const sabit = {}, bagli = {}, olay = {}; for (const [, ad, deger] of govde.matchAll(/([:@]?[\w-]+)="([^"]*)"/g)) { if (ad.startsWith(":")) bagli[ad.slice(1)] = deger; else if (ad.startsWith("@")) olay[ad.slice(1)] = deger; else sabit[ad] = deger; } return { sabit, bagli, olay }; } // 4) Derleme: sablon bir kez cozumlenir, sonuc bir komut agacina donusur. function derle(sablon, kapsamAdlari) { const degerlendir = (kaynak) => new Function(...kapsamAdlari, `return (${kaynak});`); const yigit = [{ cocuklar: [] }]; let sabitSayisi = 0, devingenSayisi = 0; for (const parca of parcala(sablon)) { if (parca.startsWith("</")) { yigit.pop(); continue; } if (parca.startsWith("<")) { const ad = parca.match(/^<([a-z]+)/)[1]; const { sabit, bagli, olay } = oznitelikleriAyir(parca); sabitSayisi += Object.keys(sabit).length; devingenSayisi += Object.keys(bagli).length + Object.keys(olay).length; const dugum = { ad, sabit, cocuklar: [], bagli: Object.fromEntries(Object.entries(bagli) .map(([k, v]) => [k, degerlendir(v)])), olay: Object.fromEntries(Object.entries(olay) .map(([k, v]) => [k, degerlendir(v)])), }; yigit.at(-1).cocuklar.push(dugum); yigit.push(dugum); continue; } if (!parca.trim()) continue; for (const p of metniAyir(parca.trim())) { if (p.tur === "sabit") { sabitSayisi++; yigit.at(-1).cocuklar.push(p.deger); } else { devingenSayisi++; yigit.at(-1).cocuklar.push(degerlendir(p.kaynak)); } } } console.log(`derleme: ${sabitSayisi} sabit parca, ${devingenSayisi} devingen parca`); return { kok: yigit[0].cocuklar[0], kapsamAdlari }; } // 5) Calistirma: komut agaci + kapsam → gorunum tanimi. function uret({ kok, kapsamAdlari }, kapsam) { const argumanlar = kapsamAdlari.map((ad) => kapsam[ad]); const cik = (d) => { if (typeof d === "string") return d; if (typeof d === "function") return String(d(...argumanlar)); return { ad: d.ad, ozn: { ...d.sabit, ...Object.fromEntries(Object.entries(d.bagli) .map(([k, f]) => [k, f(...argumanlar)])), ...Object.fromEntries(Object.entries(d.olay) .map(([k, f]) => [`on:${k}`, f(...argumanlar).name || "islev"])) }, cocuklar: d.cocuklar.map(cik), }; }; return cik(kok); } const KAPSAM = ["ad", "deger", "birim", "esik", "ac"]; const derlenmis = derle(SABLON, KAPSAM); const yaz = (d, girinti = 0) => typeof d === "string" ? `${" ".repeat(girinti)}${JSON.stringify(d)}` : [`${" ".repeat(girinti)}<${d.ad}${Object.entries(d.ozn) .map(([k, v]) => ` ${k}=${JSON.stringify(v)}`).join("")}>`, ...d.cocuklar.map((c) => yaz(c, girinti + 2))].join("\n"); for (const olcum of [ { ad: "Sıcaklık", deger: -4.2, birim: "°C", esik: 30, ac: () => {} }, { ad: "Bağıl nem", deger: 94, birim: "%", esik: 90, ac: () => {} }, ]) console.log(`\n${yaz(uret(derlenmis, olcum))}`); // Ifade yalnizca bildirilen kapsam adlarini gorur. try { new Function(...KAPSAM, "return (esikAsimSayisi);")("x", 0, "", 0, null); } catch (hata) { console.log(`\nkapsamda olmayan ad: ${hata.constructor.name} — ${hata.message}`); }
derleme: 4 sabit parca, 5 devingen parca
<div class="rozet" durum="normal" on:tikla="ac">
<span class="ad">
"Sıcaklık"
<span class="deger">
"-4.2"
" "
"°C"
<div class="rozet" durum="asim" on:tikla="ac">
<span class="ad">
"Bağıl nem"
<span class="deger">
"94"
" "
"%"
kapsamda olmayan ad: ReferenceError — esikAsimSayisi is not defined
Çıktının dört sonucu vardır.
Derleme bir kez, çalıştırma çok kez. Çözümleme satırı yalnızca bir kez yazılmıştır; iki farklı ölçüm için görünüm üretilirken şablon metnine bir daha bakılmamıştır. On beş rozet için de aynıdır. Şablonun ayrıştırılması, oluşturma maliyetinin parçası değildir.
Sabit ve devingen parçalar ayrılmıştır. Derleyici dört parçanın hiçbir duruma bağlı olmadığını bilir. Bu bilgi bir sonraki derste maliyet düşürmek için kullanılır: karşılaştırılması gerekmeyen parçalar baştan bellidir.
İfade, bildirilen kapsamda değerlendirilir. Şablondaki deger adı, bileşenin
kapsamındaki deger değişkenidir; kapsamda bulunmayan bir ad çalışma zamanında hata
üretir. Şablonun gördüğü adlar bileşenin verdiği adlarla sınırlıdır.
Üç bağlama türü ayrı imlerle yazılmıştır. Ön ekli olmayan öznitelik sabittir; iki nokta ön eki değeri bir ifadeden alır; kuyruklu a ön eki bir olay bağlar. Ayrımın söz diziminde görünür olması, derleyicinin her birine farklı davranmasını sağlar.
İfadelerin Sınırı
Şablon ifadeleri, oluşturma sırasında ve her oluşturmada çalışır. Bundan iki kural çıkar.
İfade yan etki üretmemelidir. Önceki dersin saflık koşulu şablona da uygulanır: içinde durum değiştiren, istek başlatan ya da sayaç artıran bir ifade, oluşturma sayısına bağlı davranır.
İfade ucuz olmalıdır. Şablonun içinde bir listeyi sıralamak ya da süzmek, her oluşturmada yeniden hesaplanır. Bu hesabın bir kez yapılıp sonucunun saklanması, kursun türetilmiş değerler dersinin konusudur.
Şablon dilinin ifadeyle sınırlı tutulması bu kuralları söz dizimi düzeyinde de destekler: deyim yazılamayan bir dilde döngü kurmak, atama yapmak ya da hata yakalamak olanaksızdır. Bu iş bileşenin gövdesine ait olur.
Metin, Öznitelik ve Olay Bağlaması
Üç bağlama türünün davranışı birbirinden ayrıdır ve karıştırıldıklarında iki tanıdık sorun doğar.
// baglama-turleri.mjs — metin kacirma ile oznitelik/ozellik baglamasinin farki // 1) Metin yerlestirme kacirir; ham ekleme kacirmaz. const kacir = (s) => String(s) .replace(/&/g, "&").replace(/</g, "<") .replace(/>/g, ">").replace(/"/g, """); const not = 'Sensör <b>şüpheli</b> okuma'; // forma girilmis serbest metin console.log(`metin yerlestirme : <p>${kacir(not)}</p>`); console.log(`ham ekleme : <p>${not}</p>`); const etiket = 'kar" veri-yetki="tam'; // tirnak tasiyan deger console.log(`\noznitelik (kacirilmis): <li tur="${kacir(etiket)}">`); console.log(`oznitelik (kacirilmamis): <li tur="${etiket}">`); const oznitelikSayisi = (s) => [...s.matchAll(/[\w-]+="/g)].length; console.log(`uretilen oznitelik sayisi — kacirilmis: ` + `${oznitelikSayisi(`<li tur="${kacir(etiket)}">`)}, ` + `kacirilmamis: ${oznitelikSayisi(`<li tur="${etiket}">`)}`); // 2) Oznitelik belgedeki dizgi, ozellik yasayan degerdir; kullanici yazinca ayrisirlar. function alanKur() { const dugum = { ozn: {}, canliDeger: "", kullaniciDokundu: false }; return { dugum, oznitelikYaz(deger) { dugum.ozn.deger = String(deger); if (!dugum.kullaniciDokundu) dugum.canliDeger = String(deger); // ilk deger }, ozellikYaz(deger) { dugum.canliDeger = String(deger); }, kullaniciYazar(metin) { dugum.kullaniciDokundu = true; dugum.canliDeger = metin; }, }; } for (const tur of ["oznitelik", "ozellik"]) { const alan = alanKur(); const yaz = (v) => tur === "oznitelik" ? alan.oznitelikYaz(v) : alan.ozellikYaz(v); yaz("kar"); // ilk cizim: durumdaki arama metni alan.kullaniciYazar("kar de"); // kullanici yaziyor yaz("kar derinliği"); // durum guncellendi, yeniden cizim console.log(`\n${tur} baglamasi → belgedeki deger: ` + `${JSON.stringify(alan.dugum.ozn.deger ?? null)}, ` + `alanda gorunen: ${JSON.stringify(alan.dugum.canliDeger)}`); }
metin yerlestirme : <p>Sensör <b>şüpheli</b> okuma</p> ham ekleme : <p>Sensör <b>şüpheli</b> okuma</p> oznitelik (kacirilmis): <li tur="kar" veri-yetki="tam"> oznitelik (kacirilmamis): <li tur="kar" veri-yetki="tam"> uretilen oznitelik sayisi — kacirilmis: 1, kacirilmamis: 2 oznitelik baglamasi → belgedeki deger: "kar derinliği", alanda gorunen: "kar de" ozellik baglamasi → belgedeki deger: null, alanda gorunen: "kar derinliği"
Metin yerleştirme kaçırır. Kullanıcının girdiği metin metin olarak kalır; içindeki işaretleme karakterleri Web’in Temelleri ve HTML kursundaki karakter başvurularına çevrilir. Bu, şablon söz diziminin varsayılan davranışıdır ve seçilmiş bir güvenlik kararıdır. Aynı şablon dilinin sunduğu ham ekleme yolu bu korumayı kapatır: değer işaretleme olarak yorumlanır. Ham eklemenin girdisi hiçbir zaman kullanıcıdan gelmemelidir.
Öznitelik bağlaması da kaçırır. Kaçırılmamış bir değer tırnağı kapatıp yeni bir öznitelik açar: örnekte tek öznitelik yerine iki öznitelik üretilmiştir. Şablon derleyicisinin değeri koyduğu yeri bilmesi, doğru kaçırmayı seçebilmesinin nedenidir; aynı değer metin içinde, öznitelik içinde ve adres değeri olarak farklı kurallarla kaçırılır.
Öznitelik ile özellik aynı şey değildir. DOM API’si dersinde kurulan ayrım şablonda bir seçime dönüşür. Öznitelik yazmak belgedeki dizgiyi değiştirir; kullanıcı alana dokunduktan sonra alanın yaşayan değerini artık izlemez. Çıktının son iki satırı bunu gösterir: öznitelik yolu belgedeki değeri günceller ama alanda görünen metin kullanıcının yazdığı hâlde kalır. Özellik yolu yaşayan değeri günceller.
Kural buradan çıkar: görüntülenen değeri taşıyan bağlamalar özellik, belgedeki üstveriyi taşıyanlar öznitelik olarak yazılır. Mantıksal öznitelikler ayrı bir durumdur: belgede varlık–yokluk taşırlar, program tarafında ise doğru–yanlış; bir mantıksal özniteliği yanlış değerle yazmak onu kaldırmakla aynı şey değildir.
Olay bağlaması bir işlev bağlar. Şablondaki @tikla="ac" imi bir dizgi değil, kapsamda
bulunan bir işlev başvurusu üretir; çıktıda on:tikla="ac" olarak görünen şey odur.
Değerin dizgi olarak yorumlanması, işaretlemenin içine kod yazmak anlamına gelirdi.
Özet
- Şablon, işaretlemenin kendi söz diziminde yazılıp veriye bağlanmasıdır; derleme adımı onu görünüm tanımı üreten bir işleve çevirir.
- İşaretleme ya dilin içine gömülür ya da ayrı bir metin olarak derlenir; ikinci yolda derleyici sabit ve devingen parçaları ayırt edebilir.
- Şablon bir kez çözümlenir, üretilen komut ağacı her oluşturmada çalıştırılır; ayrıştırma oluşturma maliyetinin parçası değildir.
- Şablon ifadeleri bileşenin kapsamında değerlendirilir, yan etki üretmemeli ve ucuz olmalıdır; deyim yazılamaması bu kuralı söz dizimi düzeyinde destekler.
- Metin yerleştirme ve öznitelik bağlaması varsayılan olarak kaçırır; ham ekleme kaçırmaz ve girdisi kullanıcıdan gelmemelidir.
- Görüntülenen değeri taşıyan bağlamalar özellik, belgedeki üstveriyi taşıyanlar öznitelik olarak yazılır; olay bağlaması dizgi değil işlev başvurusu bağlar.
Sonraki Adım
Bu dersin şablonu tek bir rozeti betimledi ve yapısı her durumda aynı kaldı: aynı ögeler, aynı sırada. Gerçek bir pano ise değişken yapılıdır — süzgeç hiçbir ölçümle eşleşmediğinde tablo yerine bir boş durum iletisi görünür, ölçüm listesi büyüyüp küçülür, satırlar yeniden sıralanır. Sonraki ders şablona koşul ve yineleme yönergelerini ekler ve asıl soruyu sorar: liste değiştiğinde yeni tanımdaki hangi satırın ağaçtaki hangi düğüme karşılık geldiğini ne belirler?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.