Files
kolaytercih/src/lib/rehber.ts
bilalgursen 6bd210075f Rehbere mermaid şema desteği ve 4 yeni tercih dönemi yazısı ekle
Markdown'daki \`\`\`mermaid çitleri artık yazıyı parçalara bölüyor ve şemalar
üründeki ortak el çizimi bileşeniyle (ElCizimiSema) çiziliyor; çitte "%% aria:"
satırı zorunlu, "%% altyazi:" figcaption veriyor. Tercih dönemi aramalarına
dayanan dört yeni yazı: merkezi yerleştirme algoritması, OBP hesabı, üniversite
kayıt rehberi ve yerleşememe/istenmeyen bölüm karar ağacı — her biri risk renk
dilinde birer şemayla.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 04:25:58 +03:00

152 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from "node:fs";
import path from "node:path";
import { marked } from "marked";
/**
* Rehber makaleleri: content/rehber/*.md dosyaları build sırasında okunur ve
* statik HTML'e gömülür (sayfalar generateStaticParams + dynamicParams=false
* ile tamamen prerender edilir; runtime'da fs erişimi olmaz).
*
* Frontmatter elle parse edilir (yalnızca düz `anahtar: değer` satırları) —
* 10 makale için gray-matter bağımlılığına gerek yok.
*
* ```mermaid çitleri HTML'e gömülmez: makale parçalara bölünür ve şemalar
* sayfada ürünün ortak el çizimi bileşeniyle (ElCizimiSema) çizilir. Çitin
* içinde `%% aria:` satırı zorunludur (erişilebilirlik metni), `%% altyazi:`
* isteğe bağlı figcaption verir; iki satır da mermaid tanımından çıkarılır.
*/
export type RehberOzet = {
slug: string;
baslik: string;
aciklama: string;
/** ISO tarih — Article JSON-LD datePublished */
tarih: string;
};
export type RehberSema = {
/** Mermaid tanımı (aria/altyazi yönergeleri ayıklanmış hâli) */
tanim: string;
ariaLabel: string;
altyazi?: string;
};
export type RehberParca =
| { tip: "html"; html: string }
| { tip: "sema"; sema: RehberSema };
export type RehberYazi = RehberOzet & { parcalar: RehberParca[] };
const REHBER_DIZIN = path.join(process.cwd(), "content", "rehber");
function parseFrontmatter(raw: string): {
meta: Record<string, string>;
body: string;
} {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
if (!match) return { meta: {}, body: raw };
const meta: Record<string, string> = {};
for (const satir of match[1].split(/\r?\n/)) {
const ayrac = satir.indexOf(":");
if (ayrac === -1) continue;
const anahtar = satir.slice(0, ayrac).trim();
const deger = satir
.slice(ayrac + 1)
.trim()
.replace(/^["']|["']$/g, "");
if (anahtar) meta[anahtar] = deger;
}
return { meta, body: raw.slice(match[0].length) };
}
const SEMA_CITI = /^```mermaid[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gm;
function parseSema(slug: string, ham: string): RehberSema {
let ariaLabel = "";
let altyazi: string | undefined;
const satirlar: string[] = [];
for (const satir of ham.split(/\r?\n/)) {
const aria = satir.match(/^%%\s*aria:\s*(.+)$/);
const alt = satir.match(/^%%\s*altyazi:\s*(.+)$/);
if (aria) ariaLabel = aria[1].trim();
else if (alt) altyazi = alt[1].trim();
else satirlar.push(satir);
}
if (!ariaLabel) {
throw new Error(
`rehber: ${slug}.md içindeki mermaid şemasında "%% aria: ..." satırı zorunlu`,
);
}
return { tanim: satirlar.join("\n").trim(), ariaLabel, altyazi };
}
function parcala(slug: string, body: string): RehberParca[] {
const parcalar: RehberParca[] = [];
let son = 0;
for (const eslesme of body.matchAll(SEMA_CITI)) {
const md = body.slice(son, eslesme.index).trim();
if (md) {
parcalar.push({
tip: "html",
html: marked.parse(md, { gfm: true, async: false }),
});
}
parcalar.push({ tip: "sema", sema: parseSema(slug, eslesme[1]) });
son = eslesme.index + eslesme[0].length;
}
const kalan = body.slice(son).trim();
if (kalan) {
parcalar.push({
tip: "html",
html: marked.parse(kalan, { gfm: true, async: false }),
});
}
return parcalar;
}
function oku(slug: string): RehberYazi | null {
const dosya = path.join(REHBER_DIZIN, `${slug}.md`);
if (!fs.existsSync(dosya)) return null;
const { meta, body } = parseFrontmatter(fs.readFileSync(dosya, "utf8"));
if (!meta.baslik || !meta.aciklama || !meta.tarih) {
throw new Error(
`rehber: ${slug}.md frontmatter eksik (baslik/aciklama/tarih zorunlu)`,
);
}
return {
slug,
baslik: meta.baslik,
aciklama: meta.aciklama,
tarih: meta.tarih,
parcalar: parcala(slug, body),
};
}
export function getAllRehberSlugs(): string[] {
if (!fs.existsSync(REHBER_DIZIN)) return [];
return fs
.readdirSync(REHBER_DIZIN)
.filter((f) => f.endsWith(".md"))
.map((f) => f.replace(/\.md$/, ""))
.sort();
}
export function getRehber(slug: string): RehberYazi | null {
// path traversal koruması: slug yalnızca dosya adı olabilir
if (!/^[a-z0-9-]+$/.test(slug)) return null;
return oku(slug);
}
export function getAllRehberler(): RehberOzet[] {
return getAllRehberSlugs()
.map((slug) => oku(slug))
.filter((y): y is RehberYazi => y !== null)
.map(({ slug, baslik, aciklama, tarih }) => ({
slug,
baslik,
aciklama,
tarih,
}))
.sort((a, b) => b.tarih.localeCompare(a.tarih));
}