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>
152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
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));
|
||
}
|