Konsol: fraktal (paralel şeritli 24-bit Mandelbrot/Julia + yakınlaşma), sudoku (geri izlemeli, canlı animasyon), fatura (JSON → terminal tablosu, PDF ve Excel). Masaüstü: hesap makinesi (işlem önceliği, canlı bağlama), odak (Pomodoro), parola atölyesi (entropi ölçer, SHA-256). Web: pano (Kanban REST API + SQLite), kisalt (302 yönlendirme, SVG istatistik), anket (adres başına tek oy, sunucuda çizilen grafik). Hepsi `kabalang calistir` ile derlenip çalışır; web uygulamaları yalnız 127.0.0.1'i dinler, SQL parametre bağlamalı, HTML çıktısı kaçırılır.
286 lines
16 KiB
Plaintext
286 lines
16 KiB
Plaintext
# ╔══════════════════════════════════════════════════════════════════╗
|
|
# ║ KISALT — KabaLang ile bağlantı kısaltıcı ║
|
|
# ║ ║
|
|
# ║ · Sayfalar sunucuda KabaLang ile üretilir (şablon motoru yok) ║
|
|
# ║ · /k/{kod} → gerçek HTTP 302 yönlendirmesi + tıklama kaydı ║
|
|
# ║ · İstatistik sayfasındaki grafik: sunucuda üretilen SVG ║
|
|
# ╚══════════════════════════════════════════════════════════════════╝
|
|
#
|
|
# Çalıştır: kabalang calistir web/kisalt/kisalt.kbl
|
|
# Aç: http://127.0.0.1:8472
|
|
|
|
kullan kaba.web
|
|
kullan kaba.json
|
|
kullan kaba.veritabani
|
|
kullan kaba.sistem
|
|
|
|
# Karıştırılabilecek harfler (0/o, 1/l/i) bilerek yok.
|
|
ALFABE = "abcdefghjkmnpqrstuvwxyz23456789"
|
|
KOD_UZUNLUGU = 6
|
|
PORT = 8472
|
|
|
|
db = cevre.al("KISALT_DB")
|
|
eger db == "":
|
|
db = "kisalt.db"
|
|
|
|
# ── Stil: tüm sayfalar aynı görünümü paylaşır ───────────────────────
|
|
|
|
STIL = """
|
|
:root { --zemin:#0a0d15; --yuzey:#121826; --yuzey2:#182033; --cizgi:#232c42;
|
|
--metin:#e6ebf5; --soluk:#8a94ab; --mavi:#377ef4; --cam:#22d3ee; --yesil:#34d399; }
|
|
* { box-sizing: border-box; }
|
|
body { margin:0; min-height:100vh; color:var(--metin);
|
|
font:15px/1.55 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
background: radial-gradient(900px 480px at 50% -12%, #377ef433, transparent 65%),
|
|
radial-gradient(600px 360px at 95% 10%, #22d3ee18, transparent 60%), var(--zemin); }
|
|
a { color: var(--cam); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
.kap { max-width: 980px; margin: 0 auto; padding: 0 24px 56px; }
|
|
nav { display:flex; align-items:center; gap:12px; padding: 22px 0; }
|
|
.logo { width:36px; height:36px; border-radius:11px; display:grid; place-items:center; font-weight:800;
|
|
background:linear-gradient(135deg,var(--mavi),var(--cam)); box-shadow:0 6px 22px #377ef455; }
|
|
nav b { font-size: 18px; letter-spacing: -.01em; }
|
|
nav span { margin-left:auto; color:var(--soluk); font-size:13px; }
|
|
.kahraman { text-align:center; padding: 46px 0 30px; }
|
|
.kahraman h1 { font-size: clamp(30px, 5vw, 48px); line-height:1.1; margin:0 0 12px; letter-spacing:-.03em; }
|
|
.kahraman h1 em { font-style: normal; background: linear-gradient(90deg, var(--mavi), var(--cam));
|
|
-webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
.kahraman p { color: var(--soluk); margin: 0 0 28px; }
|
|
form { display:flex; gap:10px; max-width: 720px; margin: 0 auto; background: var(--yuzey);
|
|
border:1px solid var(--cizgi); border-radius: 16px; padding: 8px; box-shadow: 0 20px 60px #0007; }
|
|
input { flex:1; min-width:0; font:inherit; color:var(--metin); background:transparent; border:0; padding: 12px 14px; outline:none; }
|
|
button { font:inherit; font-weight:600; color:#fff; border:0; border-radius: 11px; padding: 12px 22px; cursor:pointer;
|
|
background: linear-gradient(135deg, var(--mavi), #2563eb); box-shadow: 0 6px 18px #377ef450; }
|
|
#sonuc { max-width:720px; margin: 14px auto 0; min-height: 24px; font-size: 14px; color: var(--soluk); }
|
|
#sonuc .kisa { color: var(--yesil); font-weight: 600; font-family: ui-monospace, monospace; font-size: 15px; }
|
|
.kutular { display:grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 34px 0 18px; }
|
|
.kutu { background: var(--yuzey); border:1px solid var(--cizgi); border-radius: 16px; padding: 16px 18px; }
|
|
.kutu small { color: var(--soluk); font-size: 12px; text-transform: uppercase; letter-spacing: .07em; }
|
|
.kutu div { font-size: 26px; font-weight: 700; letter-spacing: -.02em; }
|
|
table { width:100%; border-collapse: collapse; background: var(--yuzey); border:1px solid var(--cizgi);
|
|
border-radius: 16px; overflow:hidden; }
|
|
th, td { text-align:left; padding: 12px 16px; border-bottom: 1px solid var(--cizgi); font-size: 14px; }
|
|
th { color: var(--soluk); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .07em; background: var(--yuzey2); }
|
|
tr:last-child td { border-bottom: 0; }
|
|
td.hedef { color: var(--soluk); max-width: 360px; overflow:hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
td.kod { font-family: ui-monospace, monospace; }
|
|
.sayac { font-weight: 700; color: var(--metin); }
|
|
.grafik { background: var(--yuzey); border:1px solid var(--cizgi); border-radius: 16px; padding: 18px; margin-top: 14px; }
|
|
.grafik svg { width: 100%; height: auto; display:block; }
|
|
.adres { word-break: break-all; color: var(--soluk); }
|
|
footer { text-align:center; color: var(--soluk); font-size: 12px; margin-top: 30px; }
|
|
@media (max-width: 680px) {
|
|
.kap { padding: 0 16px 40px; }
|
|
form { flex-direction: column; }
|
|
.kutular { grid-template-columns: 1fr 1fr; }
|
|
.kutular .kutu:last-child { grid-column: 1 / -1; }
|
|
td.hedef, th.hedef { display: none; }
|
|
}
|
|
"""
|
|
|
|
fonksiyon sayfa(baslik, govde):
|
|
don "<!doctype html><html lang='tr'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'><title>" + web.kacir(baslik) + "</title><style>" + STIL + "</style></head><body><div class='kap'><nav><div class='logo'>K</div><b>Kısalt</b><span>KabaLang · SQLite · 302</span></nav>" + govde + "<footer>Bu sayfa sunucuda KabaLang ile üretildi.</footer></div></body></html>"
|
|
|
|
# ── Veritabanı ──────────────────────────────────────────────────────
|
|
|
|
fonksiyon hazirla(db):
|
|
veritabani.sqlite(db, "CREATE TABLE IF NOT EXISTS baglanti(kod TEXT PRIMARY KEY, adres TEXT NOT NULL, olusturma TEXT, tiklama INTEGER DEFAULT 0); CREATE TABLE IF NOT EXISTS tiklama(kod TEXT, zaman TEXT);")
|
|
|
|
# Satırları `|` ile ayrılmış sütunlara böler. Adres son sütundur ve
|
|
# kendi içinde `|` taşıyabilir → kalan parçalar geri birleştirilir.
|
|
fonksiyon satirlar(ham):
|
|
sonuc = []
|
|
eger ham == "":
|
|
don sonuc
|
|
dongu s icinde ham.bol("\n"):
|
|
sonuc.ekle(s)
|
|
don sonuc
|
|
|
|
fonksiyon son_sutun(parcalar, bas):
|
|
don parcalar.dilim(bas, parcalar.uzunluk() - bas).birlestir("|")
|
|
|
|
# ── Kısa kod ────────────────────────────────────────────────────────
|
|
|
|
fonksiyon yeni_kod():
|
|
kod = ""
|
|
dongu _ icinde 1..KOD_UZUNLUGU:
|
|
kod = kod + ALFABE.dilim(rastgele.sayi(0, ALFABE.uzunluk() - 1), 1)
|
|
don kod
|
|
|
|
fonksiyon kod_gecerli(k):
|
|
eger k.uzunluk() != KOD_UZUNLUGU:
|
|
don yanlis
|
|
dongu i icinde 0..k.uzunluk() - 1:
|
|
eger degil ALFABE.icerir(k.dilim(i, 1)):
|
|
don yanlis
|
|
don dogru
|
|
|
|
# Hedef adres yalnız http(s) olabilir: `javascript:` ya da `file:` ile
|
|
# başlayan bir "bağlantı" kısaltıcıyı saldırı aracına çevirirdi.
|
|
fonksiyon adres_dogrula(a):
|
|
eger a.uzunluk() > 2000:
|
|
don "adres çok uzun"
|
|
eger degil (a.ile_baslar("https://") veya a.ile_baslar("http://")):
|
|
don "adres http:// ya da https:// ile başlamalı"
|
|
eger a.icerir(" ") veya a.icerir("\n") veya a.icerir("\r") veya a.icerir("\t"):
|
|
don "adres boşluk içeremez"
|
|
eger a.uzunluk() < 12:
|
|
don "adres eksik görünüyor"
|
|
don ""
|
|
|
|
fonksiyon kaydet(db, adres):
|
|
kod = yeni_kod()
|
|
# Çakışma olasılığı çok düşük ama sıfır değil: boş kod bulana dek dene.
|
|
iken veritabani.sqlite(db, "SELECT count(*) FROM baglanti WHERE kod = ?", [kod]) != "0":
|
|
kod = yeni_kod()
|
|
veritabani.sqlite(db, "INSERT INTO baglanti(kod, adres, olusturma) VALUES (?, ?, ?)", [kod, adres, zaman.simdi()])
|
|
don kod
|
|
|
|
# Tanıtım verisi: ilk açılışta birkaç bağlantı ve geçmiş tıklamalar.
|
|
fonksiyon ornek_veri(db):
|
|
eger veritabani.sqlite(db, "SELECT count(*) FROM baglanti") != "0":
|
|
don 0
|
|
adresler = ["https://kabaforce.com", "https://dev.kabaforce.com/kabalang/", "https://forge.kabaforce.com/KabaLang/ornekler", "https://maps.kabaforce.com/?q=Galata+Kulesi", "https://dev.kabaforce.com/kabalang/kilavuz/12-web-uygulamasi"]
|
|
agirlik = [7, 11, 5, 3, 2]
|
|
dongu i icinde 0..adresler.uzunluk() - 1:
|
|
kod = kaydet(db, adresler[i])
|
|
veritabani.sqlite(db, "UPDATE baglanti SET olusturma = datetime('now', 'localtime', ?) WHERE kod = ?", ["-" + metin(7 + i) + " days", kod])
|
|
# Son yedi güne yayılmış tıklamalar.
|
|
dongu gun icinde 0..6:
|
|
adet = rastgele.sayi(0, agirlik[i])
|
|
dongu _ icinde 1..adet:
|
|
veritabani.sqlite(db, "INSERT INTO tiklama(kod, zaman) VALUES (?, datetime('now', 'localtime', ?))", [kod, "-" + metin(gun) + " days"])
|
|
veritabani.sqlite(db, "UPDATE baglanti SET tiklama = (SELECT count(*) FROM tiklama WHERE tiklama.kod = baglanti.kod) WHERE kod = ?", [kod])
|
|
don 1
|
|
|
|
hazirla(db)
|
|
ornek_veri(db)
|
|
|
|
# ── Sayfalar ────────────────────────────────────────────────────────
|
|
|
|
fonksiyon ana_sayfa(_istek):
|
|
toplam = veritabani.sqlite(db, "SELECT count(*) FROM baglanti")
|
|
tik = veritabani.sqlite(db, "SELECT coalesce(sum(tiklama), 0) FROM baglanti")
|
|
bugun = veritabani.sqlite(db, "SELECT count(*) FROM tiklama WHERE date(zaman) = date('now', 'localtime')")
|
|
|
|
g = "<section class='kahraman'><h1>Uzun bağlantılar,<br><em>kısa ve ölçülebilir.</em></h1>"
|
|
g = g + "<p>Bir adres yapıştırın; KabaLang ona kısa bir kod versin ve her tıklamayı saysın.</p>"
|
|
g = g + "<form id='f'><input id='adres' placeholder='https://…' autocomplete='off' required><button>Kısalt</button></form>"
|
|
g = g + "<div id='sonuc'></div></section>"
|
|
|
|
g = g + "<div class='kutular'>"
|
|
g = g + "<div class='kutu'><small>Bağlantı</small><div>" + toplam + "</div></div>"
|
|
g = g + "<div class='kutu'><small>Toplam tıklama</small><div>" + tik + "</div></div>"
|
|
g = g + "<div class='kutu'><small>Bugün</small><div>" + bugun + "</div></div>"
|
|
g = g + "</div>"
|
|
|
|
g = g + "<table><tr><th>Kısa kod</th><th class='hedef'>Hedef</th><th>Tıklama</th><th></th></tr>"
|
|
dongu s icinde satirlar(veritabani.sqlite(db, "SELECT kod, tiklama, adres FROM baglanti ORDER BY olusturma DESC LIMIT 12")):
|
|
p = s.bol("|")
|
|
kod = p[0]
|
|
# Kod yalnız ALFABE harflerinden oluşur → href içinde güvenli.
|
|
# Hedef adres kullanıcıdan geldi → yalnız METİN olarak, kaçırılarak basılır.
|
|
g = g + "<tr><td class='kod'><a href='/k/" + kod + "'>/k/" + kod + "</a></td>"
|
|
g = g + "<td class='hedef'>" + web.kacir(son_sutun(p, 2)) + "</td>"
|
|
g = g + "<td class='sayac'>" + p[1] + "</td>"
|
|
g = g + "<td><a href='/istatistik/" + kod + "'>istatistik →</a></td></tr>"
|
|
g = g + "</table>"
|
|
|
|
# Küçük bir istemci betiği: formu JSON API'ye gönderir.
|
|
g = g + "<script>document.getElementById('f').onsubmit=async e=>{e.preventDefault();const s=document.getElementById('sonuc');"
|
|
g = g + "const y=await fetch('/api/kisalt',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({adres:document.getElementById('adres').value})});"
|
|
g = g + "const d=await y.json();s.replaceChildren();if(!y.ok){s.textContent='⚠ '+d.hata;return;}"
|
|
g = g + "const a=document.createElement('a');a.className='kisa';a.href=d.kisa;a.textContent=location.host+d.kisa;s.append('✓ Hazır: ',a);setTimeout(()=>location.reload(),1600);};</script>"
|
|
don web.html(sayfa("Kısalt · KabaLang", g))
|
|
|
|
fonksiyon kisalt_api(istek):
|
|
govde = istek.govde()
|
|
eger degil json.gecerli_mi(govde):
|
|
don web.json(json.uret({"hata": "gövde geçerli JSON değil"}), 400)
|
|
adres = json.al(govde, "adres").kirp()
|
|
sorun = adres_dogrula(adres)
|
|
eger sorun != "":
|
|
don web.json(json.uret({"hata": sorun}), 422)
|
|
kod = kaydet(db, adres)
|
|
don web.json(json.uret({"kod": kod, "kisa": "/k/" + kod, "adres": adres}), 201)
|
|
|
|
fonksiyon bulunamadi_sayfasi():
|
|
g = "<section class='kahraman'><h1>Bu kod <em>bulunamadı</em></h1><p>Bağlantı silinmiş ya da hiç var olmamış olabilir.</p><p><a href='/'>← ana sayfa</a></p></section>"
|
|
don web.html(sayfa("Bulunamadı · Kısalt", g), 404)
|
|
|
|
fonksiyon yonlendir(istek):
|
|
kod = istek.parametre("kod")
|
|
eger degil kod_gecerli(kod):
|
|
don bulunamadi_sayfasi()
|
|
adres = veritabani.sqlite(db, "SELECT adres FROM baglanti WHERE kod = ?", [kod])
|
|
eger adres == "":
|
|
don bulunamadi_sayfasi()
|
|
veritabani.sqlite(db, "INSERT INTO tiklama(kod, zaman) VALUES (?, datetime('now', 'localtime'))", [kod])
|
|
veritabani.sqlite(db, "UPDATE baglanti SET tiklama = tiklama + 1 WHERE kod = ?", [kod])
|
|
# Gerçek HTTP yönlendirmesi: 302 + Location başlığı.
|
|
don web.yanit("", 302).baslik("Location", adres)
|
|
|
|
# Son 7 günün tıklamalarını SVG çubuk grafiğe çevirir — tamamen sunucuda.
|
|
fonksiyon yedi_gun_svg(db, kod):
|
|
gunler = []
|
|
adetler = []
|
|
en_cok = 1
|
|
dongu i icinde 0..6:
|
|
geri = 6 - i
|
|
gun = veritabani.sqlite(db, "SELECT strftime('%d.%m', 'now', 'localtime', ?)", ["-" + metin(geri) + " days"])
|
|
adet = sayi(veritabani.sqlite(db, "SELECT count(*) FROM tiklama WHERE kod = ? AND date(zaman) = date('now', 'localtime', ?)", [kod, "-" + metin(geri) + " days"]))
|
|
gunler.ekle(gun)
|
|
adetler.ekle(adet)
|
|
eger adet > en_cok:
|
|
en_cok = adet
|
|
|
|
gen = 700
|
|
yuk = 220
|
|
cubuk = 64
|
|
aralik = 36
|
|
svg = "<svg viewBox='0 0 " + metin(gen) + " " + metin(yuk + 40) + "' xmlns='http://www.w3.org/2000/svg'>"
|
|
svg = svg + "<defs><linearGradient id='g' x1='0' y1='1' x2='0' y2='0'><stop offset='0' stop-color='#377ef4'/><stop offset='1' stop-color='#22d3ee'/></linearGradient></defs>"
|
|
dongu i icinde 0..6:
|
|
h = adetler[i] * (yuk - 30) // en_cok
|
|
eger h < 3:
|
|
h = 3
|
|
x = 30 + i * (cubuk + aralik)
|
|
y = yuk - h
|
|
svg = svg + "<rect x='" + metin(x) + "' y='" + metin(y) + "' width='" + metin(cubuk) + "' height='" + metin(h) + "' rx='10' fill='url(#g)'/>"
|
|
svg = svg + "<text x='" + metin(x + cubuk // 2) + "' y='" + metin(y - 8) + "' fill='#e6ebf5' font-size='15' font-weight='700' text-anchor='middle' font-family='system-ui'>" + metin(adetler[i]) + "</text>"
|
|
svg = svg + "<text x='" + metin(x + cubuk // 2) + "' y='" + metin(yuk + 26) + "' fill='#8a94ab' font-size='13' text-anchor='middle' font-family='system-ui'>" + gunler[i] + "</text>"
|
|
don svg + "</svg>"
|
|
|
|
fonksiyon istatistik(istek):
|
|
kod = istek.parametre("kod")
|
|
eger degil kod_gecerli(kod):
|
|
don bulunamadi_sayfasi()
|
|
satir = veritabani.sqlite(db, "SELECT tiklama, olusturma, adres FROM baglanti WHERE kod = ?", [kod])
|
|
eger satir == "":
|
|
don bulunamadi_sayfasi()
|
|
p = satir.bol("|")
|
|
son = veritabani.sqlite(db, "SELECT coalesce(max(zaman), '—') FROM tiklama WHERE kod = ?", [kod])
|
|
|
|
g = "<section class='kahraman' style='padding-bottom:10px'><h1><em>/k/" + kod + "</em></h1>"
|
|
g = g + "<p class='adres'>→ " + web.kacir(son_sutun(p, 2)) + "</p></section>"
|
|
g = g + "<div class='kutular'>"
|
|
g = g + "<div class='kutu'><small>Toplam tıklama</small><div>" + p[0] + "</div></div>"
|
|
g = g + "<div class='kutu'><small>Oluşturuldu</small><div style='font-size:17px;padding-top:7px'>" + p[1] + "</div></div>"
|
|
g = g + "<div class='kutu'><small>Son tıklama</small><div style='font-size:17px;padding-top:7px'>" + son + "</div></div>"
|
|
g = g + "</div>"
|
|
g = g + "<div class='grafik'><small style='color:#8a94ab;text-transform:uppercase;letter-spacing:.07em;font-size:12px'>Son 7 gün</small>" + yedi_gun_svg(db, kod) + "</div>"
|
|
g = g + "<p style='text-align:center;margin-top:22px'><a href='/'>← tüm bağlantılar</a></p>"
|
|
don web.html(sayfa("/k/" + kod + " · istatistik", g))
|
|
|
|
# Tek iş parçacığı: tıklama sayacı yarış koşuluna girmesin.
|
|
sunucu = web.sunucu(PORT, "127.0.0.1", 1)
|
|
sunucu.govde_siniri(8192)
|
|
sunucu.al("/", ana_sayfa)
|
|
sunucu.gonder("/api/kisalt", kisalt_api)
|
|
sunucu.al("/k/{kod}", yonlendir)
|
|
sunucu.al("/istatistik/{kod}", istatistik)
|
|
sunucu.bulunamadi((_istek) => bulunamadi_sayfasi())
|
|
|
|
yazdir("🔗 Kısalt hazır — http://127.0.0.1:" + metin(PORT))
|
|
sunucu.basla()
|