IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA9/32-dars18 daqiqa
Mundarija (28)

Sintaksis yoritish React'da: Prism, Shiki, highlight.js, nusxalash tugmasi va qator raqamlari

Qisqacha: Sintaksis yoritish — kodni tokenlarga bo'lib (kalit so'z, son, satr, izoh), har biriga rang berish. Kutubxonalar ikki xil natija beradi. HTML satr (highlight.js, Shiki codeToHtml) — innerHTML talab qiladi va vazifalar ning Trusted Types siyosatida yiqildi. React elementlari (prism-react-renderer, Shiki codeToHast + React) — CSP 0. Eng yaxshisi — yoritishni build vaqtida qilish (rehype-pretty-code, Shiki): brauzerga 0 KB JS. Kanon o'lchovi: highlight.js +10.0 KB, Prism +26.2, Shiki +70.5.

Bu darsda

  • Tokenlash va yoritish qanday ishlashini kutubxonasiz modelda ko'rasiz.
  • prism-react-renderer, Shiki va highlight.js ni ulaysiz va natija turini (satr yoki element) ajratasiz.
  • Markdown ichidagi kod bloklarini components.code orqali yoritasiz.
  • Nusxalash tugmasi, qator raqamlari va ajratilgan qatorlarni a11y bilan qo'shasiz.
  • Yoritishni brauzerda yoki build vaqtida qilishni narx bo'yicha tanlaysiz.

Oldin bilishingiz kerak: Markdown render qilish, RegExp asoslari va metodlari, Clipboard API, Brauzer xavfsizlik modeli.

1. Nega bu kerak?

O'tgan darsda vazifalar ga Markdown izoh qo'shdik. Sardor dasturchi-stajyor — izohlarga kod parchalarini ham yozadi:

text
Narxni shunday hisoblaymiz:
~~~js
const jami = osh * 2 + choy;
~~~

(~~~ — kod blokining ikkinchi yozilishi, ``` bilan bir xil.) Hozir u bir xil rangli matn. Kod rangsiz bo'lsa, o'qish qiyin: qayerda kalit so'z, qayerda satr, qayerda izoh — ko'z ajratmaydi. Bu kursdagi har bir kod bloki rangli ekanini sezgandirsiz — bu ham sintaksis yoritish.

Ikkinchi ehtiyoj — nusxalash. Telefonda kodni barmoq bilan belgilash azob. "Nusxalash" tugmasi bitta bosishda ishni bitiradi.

Hayotiy o'xshatish: maktab daftaridagi grammatik tahlil. O'qituvchi ega ostiga bitta, kesim ostiga ikkita chiziq tortadi. Gap o'sha-o'sha, lekin tuzilishi ko'rinib qoladi. Yoritgich ham shunday: kod o'zgarmaydi, har "so'z turi" o'z rangini oladi.

2. Yoritish ichkaridan: tokenlar

2.1 O'quv modeli: o'z tokenizer'imiz

Yoritgich ikki ish qiladi. Avval kodni tokenlarga bo'ladi (tokenlash, tokenization): har bo'lakka tur beradi. Keyin har turga rang yoki CSS sinfi qo'yadi. Buni bir nechta muntazam ifoda bilan o'zimiz yozamiz:

jsx
import { useEffect } from "react";

// har tur — bitta regex; tartib muhim: avval izoh va satr
const RULES = [
  ["comment", /^\/\/.*/],
  ["string", /^"[^"]*"/],
  ["keyword", /^(?:const|let|return|function)\b/],
  ["number", /^\d+/],
  ["plain", /^[\s\S]/], // qolgan har bitta belgi
];

function tokenize(code) {
  const tokens = [];
  let rest = code;
  while (rest) {
    for (const [type, re] of RULES) {
      const m = rest.match(re);
      if (m) {
        tokens.push({ type, text: m[0] });
        rest = rest.slice(m[0].length);
        break;
      }
    }
  }
  return tokens;
}

const COLORS = {
  keyword: "#00009f",
  number: "#36acaa",
  string: "#e3116c",
  comment: "#6a737d",
};

const code = 'const osh = 35000; // "narx"';

export default function App() {
  const tokens = tokenize(code);
  useEffect(() => {
    const typed = tokens.filter((t) => t.type !== "plain");
    console.log(typed.map((t) => `${t.type}:${t.text}`).join(" | "));
  }, [tokens]);
  return (
    <pre>
      <code>
        {tokens.map((t, i) => (
          <span key={i} style={{ color: COLORS[t.type] }}>
            {t.text}
          </span>
        ))}
      </code>
    </pre>
  );
}

Konsolda:

text
keyword:const | number:35000 | comment:// "narx"

Ekranda:

text
const osh = 35000; // "narx"

Ekrandagi matn kod bilan bir xil — yoritish faqat rang qo'shadi. Konsolga qarang: izoh ichidagi "narx" satr emas, izohning bir qismi. Chunki comment qoidasi birinchi turadi va butun qatorni yeb qo'yadi. Qoidalar tartibini almashtirsangiz, // izoh bo'lib, "narx" — satr bo'lib chiqadi. Haqiqiy yoritgichlar shunday chalkashliklarni yuzlab qoidalar bilan hal qiladi.

Rang style orqali, React elementida berildi — innerHTML yo'q. Bu muhim nuqta: keyingi bo'limda kutubxonalar aynan shu joyda ikkiga bo'linadi.

2.2 Grammatika va mavzu

Haqiqiy yoritgichda ikki qism bor:

  • Grammatika (grammar) — tilning tokenlash qoidalari. JavaScript, Python, CSS uchun alohida.
  • Mavzu (theme) — token turi → rang. github-light, dracula, one-dark.

Ular alohida bo'lgani uchun bitta grammatika bilan yuzlab mavzuni almashtirish mumkin. Shiki VS Code'ning o'zidagi TextMate grammatikalari va mavzularini ishlatadi — kod VS Code'dagi bilan bir xil ranglanadi. Prism va highlight.js — o'z grammatikalari bilan, sodda va tezroq.

2.3 Yorug' va qorong'i mavzu

Sayt qorong'i temaga o'tsa, kod bloki ham o'tishi kerak. Yorug' fon uchun tanlangan ko'k kalit so'z qora fonda ko'rinmay qoladi. Uch yo'l bor:

  1. CSS sinflari + ikki fayl. highlight.js va Prism (sinf rejimida) tokenlarga sinf beradi (hljs-keyword, token keyword). Ranglar CSS'da, @media (prefers-color-scheme: dark) yoki [data-tema="qorongi"] ichida boshqa qiymat. Eng arzon yo'l: JS o'zgarmaydi, faqat CSS.
  2. CSS o'zgaruvchilari. Shiki'da ikki mavzuni birdan berish mumkin: themes: { light: "github-light", dark: "github-dark" }. Natijada har tokenda yorug' rang va --shiki-dark o'zgaruvchisi bo'ladi. Qorong'i temada CSS color: var(--shiki-dark) qo'yadi.
  3. Qayta yoritish. Tema o'zgarganda kodni boshqa mavzu bilan qayta hisoblash. Eng qimmati: har o'tishda hamma bloklar qayta tokenlanadi.

Qaysi biri bo'lmasin, kontrastni tekshiring: kod matni ham oddiy matn kabi WCAG 1.4.3 (kamida 4.5:1) talabiga bo'ysunadi. Ko'p tayyor mavzular izohlarni (// …) juda och kulrangda beradi — kontrast tekshiruvchisi buni darhol ushlaydi.

Tekshirib ko'ring: Nega mavzuni CSS sinflari bilan berish "qayta yoritish" dan arzon?

Javob

Tokenlash — eng og'ir ish: matnni grammatika qoidalari bilan bo'lish. Sinflar bilan tokenlar bir marta hisoblanadi, tema almashganda brauzer faqat CSS qoidasini qayta qo'llaydi — JS ishlamaydi. Qayta yoritishda esa har blok yana tokenlanadi va React elementlari qayta yaratiladi.

3. Uchta kutubxona: satr yoki element

3.1 highlight.js — HTML satr

Eng eski va eng keng tarqalgani (190+ til). Natija — HTML satr. Biz highlight.js 11.12.0 ni Node 24 da ishga tushirdik:

js
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";

hljs.registerLanguage("javascript", javascript);
const html = hljs.highlight("const osh = 35000; // narx", {
  language: "javascript",
}).value;

html qiymati:

text
<span class="hljs-keyword">const</span> osh = <span class="hljs-number">35000</span>; <span class="hljs-comment">// narx</span>

Ranglar — CSS sinflari (hljs-keyword), mavzu alohida CSS fayl. React'da bu satr dangerouslySetInnerHTML bilan qo'yiladi — kanon tajribasida vazifalar CSP'si ostida TrustedHTML talabi bilan yiqildi.

Satrning o'zi xavfli emas: highlight.js kod matnini ekranlaydi (< → &lt;). Muammo — innerHTML yo'li va Trusted Types. Ko'p saytlar yoritishni serverda qiladi va tayyor HTML'ni yuboradi — bu kurs sayti ham darslarni serverda marked + highlight.js bilan HTML'ga aylantiradi. Shunda brauzerga yoritgich kodi umuman kelmaydi.

hljs.highlightAuto(code) — tilni o'zi taxmin qiladi. Qulay, lekin har grammatikani sinab ko'radi (sekin) va qisqa parchada ko'p adashadi. Tilni har doim aniq bering.

3.2 prism-react-renderer — React elementlari

prism-react-renderer — Prism grammatikalari asosida, lekin HTML satr emas, tokenlar beradi. Siz ularni o'zingiz chizasiz (render prop). Kanon tajribasidagi kod:

tsx
import { Highlight, themes } from "prism-react-renderer";

<Highlight theme={themes.github} code={code} language="js">
  {({ tokens, getLineProps, getTokenProps }) => (
    <pre>
      {tokens.map((line, i) => (
        <div key={i} {...getLineProps({ line })}>
          {line.map((token, j) => (
            <span key={j} {...getTokenProps({ token })} />
          ))}
        </div>
      ))}
    </pre>
  )}
</Highlight>;

tokens — qatorlar massivi, har qator — tokenlar massivi. Biz const osh = 35000; // narx ning tokenlarini Node'da chiqardik:

text
[["keyword","const"],["plain"," osh "],["operator","="],["plain"," "],["number","35000"],["punctuation",";"],["plain"," "],["comment","// narx"]]

O'quv modelimizdagi g'oyaning o'zi, faqat operator va punctuation turlari ham bor. getTokenProps har tokenga className (token keyword) va style ({ color: "#00009f" }) beradi. style React orqali qo'yiladi — CSP 0, kanon o'lchovi ham, bizning Chrome 155 dagi qayta sinovimiz ham (kalit so'z rangi rgb(0, 0, 159)).

Narx — +26.2 KB. Paket Prism'ning tanlangan tillari bilan keladi; boshqa til kerak bo'lsa, grammatikani alohida qo'shasiz.

3.3 Shiki — VS Code sifati

Shiki — eng aniq yoritgich: TextMate grammatikalari, VS Code mavzulari. Sukut bo'yicha Oniguruma regex dvigatelini WebAssembly sifatida yuklaydi. Kanon WASM'siz, JavaScript regex dvigateli bilan o'lchadi — +70.5 KB (faqat JS va bitta mavzu).

codeToHtml — HTML satr, ranglar style atributlarida:

text
<pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">const</span>…

Bu satr innerHTML bilan qo'yilsa — kanonda TT buzilishi. Hatto Trusted Types bo'lmasa ham, CSP qoidasiga ko'ra style-src 'self' HTML ichidagi style="…" atributlarini to'sadi — kod rangsiz qoladi (bu holatni alohida o'lchamadik — hujjat bo'yicha).

Lekin Shiki ikkinchi yo'lni ham beradi: codeToHast — HTML daraxti (hast), o'tgan darsdagi unified ekotizimining o'zi. Daraxtni hast-util-to-jsx-runtime React elementlariga aylantiradi:

tsx
import { Fragment } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";

const hast = highlighter.codeToHast(code, {
  lang: "javascript",
  theme: "github-light",
});
const element = toJsxRuntime(hast, { Fragment, jsx, jsxs });

Biz shu variantni vazifalar CSP'si bilan Chrome 155 da sinadik: buzilish 0, kalit so'z rangi rgb(215, 58, 73) (#D73A49) — ranglar qo'yildi. Sabab: React style ni atribut sifatida emas, DOM xususiyati (element.style.color = …) orqali qo'yadi, CSP esa buni to'smaydi.

Tekshirib ko'ring: Nega highlight.js satri xavfsiz (kod ekranlangan), lekin baribir Trusted Types'da yiqiladi?

Javob

Trusted Types satrning ichiga qaramaydi — u qaysi yo'l bilan kelganini tekshiradi. innerHTML ga oddiy satr berish — taqiq, satr qanchalik toza bo'lmasin. Faqat siyosatdan o'tgan TrustedHTML obyekti qabul qilinadi. Bu qattiq tuyuladi, lekin aynan shu qoida "bu satrni kimdir tekshirgandir" degan taxminni yo'q qiladi.

4. Markdown ichidagi kod bloklari

Izohdagi ```js bloki react-markdown'da <pre><code className="language-js"> bo'lib chiqadi. components.code ni almashtirsak, yoritishni o'zimiz qo'shamiz. Natija oynasida Prism yo'q, shuning uchun o'quv modelimizdagi tokenize ni ishlatamiz — g'oya bir xil:

jsx
import Markdown from "react-markdown";

const RULES = [
  ["comment", /^\/\/.*/],
  ["keyword", /^(?:const|let|return)\b/],
  ["number", /^\d+/],
  ["plain", /^[\s\S]/],
];
const COLORS = {
  keyword: "#00009f",
  number: "#36acaa",
  comment: "#6a737d",
};

function tokenize(code) {
  const out = [];
  let rest = code;
  while (rest) {
    const [type, re] = RULES.find(([, r]) => r.test(rest));
    const text = rest.match(re)[0];
    out.push({ type, text });
    rest = rest.slice(text.length);
  }
  return out;
}

function Code({ className, children }) {
  const lang = /language-(\w+)/.exec(className ?? "")?.[1];
  if (!lang) return <code>{children}</code>; // `inline` kod
  const tokens = tokenize(String(children).replace(/\n$/, ""));
  return (
    <code data-lang={lang}>
      {tokens.map((t, i) => (
        <span key={i} style={{ color: COLORS[t.type] }}>
          {t.text}
        </span>
      ))}
    </code>
  );
}

const note =
  "Hisob: `jami`\n\n```js\nconst jami = 35000 * 2; // 2 osh\n```";

export default function App() {
  return <Markdown components={{ code: Code }}>{note}</Markdown>;
}

Ekranda:

text
Hisob: jami
const jami = 35000 * 2; // 2 osh

Ikki narsaga qarang. Til — className dan (language-js): bo'lmasa, bu qator ichidagi `jami` — yoritmaymiz. Oxirgi \n — Markdown blok matnining oxirida yangi qator bor, uni kesamiz, aks holda pastda bo'sh qator chiqadi.

Prism bilan xuddi shunday: Code ichida tokenize o'rniga <Highlight code={…} language={lang}>. Tayyor plaginlar ham bor: rehype-highlight (highlight.js grammatikalari, lekin natija — hast daraxti, satr emas) va Shiki'ning @shikijs/rehype plagini.

5. Nusxalash, qator raqamlari va ajratish

5.1 Qator oralig'ini o'qish

Hujjat saytlarida qatorlarni ajratish odatda blok sarlavhasida yoziladi: ```js {2,4-5}. Bu — meta satr. Uni o'qish — oddiy JS:

js
function parseLines(meta) {
  const m = /\{([\d,\s-]+)\}/.exec(meta);
  const lines = new Set();
  if (!m) return lines;
  for (const part of m[1].split(",")) {
    const [from, to = from] = part.trim().split("-").map(Number);
    for (let n = from; n <= to; n++) lines.add(n);
  }
  return lines;
}

console.log(parseLines('js {2,4-5} title="narx.js"'));
console.log(parseLines("js"));

Konsolda:

text
Set(3) { 2, 4, 5 }
Set(0) {}

to = from — standart qiymat: "2" bo'lsa, oraliq 2 dan 2 gacha. Set — tez tekshirish uchun (lines.has(n)).

5.2 Kod bloki: raqamlar, ajratish, nusxalash

Endi hammasini bitta komponentga yig'amiz. Nusxalash funksiyasini prop qilib beramiz: haqiqiy ilovada u navigator.clipboard.writeText (Clipboard API), natija oynasida esa — konsolga yozadigan soxta funksiya:

jsx
import { useState } from "react";

function CodeBlock({ code, highlight = new Set(), copy }) {
  const [status, setStatus] = useState("");
  async function handleCopy() {
    try {
      await copy(code);
      setStatus("Nusxalandi");
    } catch {
      setStatus("Nusxalab bo'lmadi — qo'lda belgilang");
    }
  }
  return (
    <figure>
      <pre>
        {code.split("\n").map((line, i) => (
          <div
            key={i}
            style={{ background: highlight.has(i + 1) ? "#ff6" : "" }}
          >
            <span aria-hidden="true" style={{ userSelect: "none" }}>
              {String(i + 1).padStart(2)}{" "}
            </span>
            {line}
          </div>
        ))}
      </pre>
      <button type="button" onClick={handleCopy}>
        Nusxalash
      </button>{" "}
      <span role="status">{status}</span>
    </figure>
  );
}

const fakeCopy = async (text) =>
  console.log("clipboard:", text.length, "belgi");

export default function App() {
  return (
    <CodeBlock
      code={[
        "const osh = 35000;",
        "const choy = 5000;",
        "console.log(osh + choy);",
      ].join("\n")}
      highlight={new Set([3])}
      copy={fakeCopy}
    />
  );
}

Konsolda:

text
clipboard: 62 belgi

Ekranda:

text
1 const osh = 35000;
2 const choy = 5000;
3 console.log(osh + choy);
Nusxalash Nusxalandi

Uchta a11y qarori:

  1. Qator raqamlari aria-hidden — ekran o'quvchi "bir, const osh…" demaydi. user-select: none — sichqoncha bilan belgilaganda raqamlar nusxaga tushmaydi. Nusxalash tugmasi esa code ning o'zini beradi (62 belgi — raqamsiz).
  2. "Nusxalandi" role="status" ichida — ekran o'quvchi uni o'qiydi. Faqat ikonka o'zgarsa, Malika tugma ishladimi-yo'qmi bilmaydi.
  3. Xato holati — writeText HTTPS'siz yoki ruxsat bo'lmasa rad etadi. Jim qolmang: "qo'lda belgilang" deng.

Ajratilgan qator — fon rangi. Rang yolg'iz emas: ko'p saytlar chap chetga chiziq ham qo'shadi (kontrast va rang darsidagi qoida).

6. Build vaqtida yoritish: brauzerga 0 KB

Hujjat sayti, blog, kurs — kod bloklari oldindan ma'lum. Ularni har mehmon brauzerida qayta yoritish — isrof. Build vaqtida yoritish: sayt yig'ilayotganda kod rangli HTML'ga aylanadi, brauzerga faqat HTML va CSS boradi.

flowchart LR
  subgraph B["Build (bir marta)"]
    M["Markdown / MDX"] --> P["rehype-pretty-code (Shiki)"]
    P --> H["rangli HTML"]
  end
  subgraph R["Brauzer (har mehmon)"]
    H --> V["ko'rinadi: 0 KB yoritgich JS"]
  end

Diagrammaning ikkinchi qismiga qarang: brauzer hech narsa hisoblamaydi. rehype-pretty-code — Shiki ustidagi rehype plagini: qator raqamlari, {2,4-5} ajratish, title="…" sarlavha — hammasi meta satrdan. Next.js'da server komponentlari bilan Shiki serverda ishlaydi va natija HTML bo'lib keladi — 23-qismda.

Qachon brauzerda kerak? Foydalanuvchi kodni o'zi yozsa — izoh, chat, onlayn muharrir. Unda yengil yoritgich (highlight.js yoki Prism) va kech yuklash.

7. Biz sinab ko'rdik: hajm va CSP

7.1 Kanon o'lchovi

Kod yoritgichlar: JS (gzip, React'dan tashqari)
  • build vaqtida (rehype-pretty-code)hisob — build'da0 KB
  • highlight.js 11.12 (core + js)10 KB
  • prism-react-renderer 2.4.126,2 KB
  • Shiki 4.5.0 (JS dvigatel, 1 til)70,5 KB

Manba: O'lchandi: kanon tajribasi, Vite 8.3.3, React 19.3.0, gzip -9, Chrome 155, natijalar/tajriba/kutubxonalar-21.txt, 2026-10-17; build vaqtidagi 0 — brauzer yoritgich JS yuklamaydi (ta'rif bo'yicha)

Xom natija (xato matnlari qisqartirilgan):

text
kod-hljs   +10.0 KB | CSP require-trusted-types-for×2 | … 'TrustedHTML' …
kod-prism  +26.2 KB | CSP 0
kod-shiki  +70.5 KB | CSP require-trusted-types-for×2 | … 'TrustedHTML' …

CSP ostida highlight.js va Shiki misollari ekranga hech narsa chizmadi (0 element) — xato React daraxtini yiqitdi. CSP'siz ikkalasi ham const osh = 35000; ni chizdi.

7.2 Bizning qo'shimcha sinovimiz

Kanon Shiki'ni codeToHtml + dangerouslySetInnerHTML bilan sinagan. Biz codeToHast + toJsxRuntime variantini qo'shdik (esbuild, Chrome 155, kanon CSP'si): 0 buzilish, ranglar joyida. Demak, Shiki'ning o'zi emas, ulash usuli CSP'ni buzadi.

7.3 Nega vazifalar o'zgarmaydi

Kanon izohda kod bloklarini yoritmadi. Izohlar qisqa, kod parchasi kam uchraydi. Bitta funksiya uchun +26 KB (Prism) yoki +70 KB (Shiki) — o'tgan darsdagi 46 KB Markdown chunk'iga yana bir yarmi. Kerak bo'lsa — Prism, faqat components.code da va kech yuklanadigan chunk ichida.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
kod rangsiz, konsolda TrustedHTML HTML satr innerHTML bilan React elementlari (Prism, Shiki hast)
Shiki ranglari yo'q, TT yo'q style="…" atributini CSP to'sdi codeToHast + toJsxRuntime
til noto'g'ri aniqlandi highlightAuto tilni aniq bering
Python bloki yoritilmadi grammatika yuklanmagan tilni ro'yxatdan o'tkazing
nusxada qator raqamlari ham bor raqamlar matn ichida user-select: none + tugma code ni beradi
ekran o'quvchi raqamlarni o'qiydi aria-hidden yo'q raqam span iga aria-hidden
"Nusxalash" jim ishlamaydi HTTP yoki ruxsat yo'q catch + foydalanuvchiga xabar
kod blok ostida bo'sh qator Markdown blok oxiridagi \n .replace(/\n$/, "")
izohdagi qator ichidagi kod ham blok bo'ldi language- tekshirilmagan sinf bo'lmasa — oddiy <code>

9. Mashqlar

Mashqlar kurs/mashqlar/21/09-kod-yoritish/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Tokenlash qoidalari to'plami:
  • Token turi → rang xaritasi:
  • Shiki'ning React elementlariga yo'l ochadigan metodi:
  • Qator raqamlari nusxaga tushmasligi uchun CSS: user-select:
  • Yoritgich JS'i brauzerga umuman kelmaydigan usul: vaqtida yoritish
Yechim
  • grammatika — tilning tokenlash qoidalari (TextMate, Prism grammatikasi).
  • mavzu — github-light, dracula.
  • codeToHast — HTML daraxti; toJsxRuntime bilan React elementlari.
  • none — sichqoncha bilan belgilanmaydi.
  • build — rehype-pretty-code yoki serverda Shiki.

2-mashq (o'rta): tokenlar statistikasi

O'quv modelimizdagi RULES va tokenize dan foydalanib, komponent yozing: kod const narx = 35000;\nconst soni = 3;\n// jami\nreturn narx * soni;. Ekranda har tur uchun bitta <li>: keyword: 3, number: 2, comment: 1 (faqat plain dan boshqalari, birinchi uchrash tartibida).

Ishora: sanash — Map yoki Object.groupBy (guruhlash).

Yechim
jsx
const RULES = [
  ["comment", /^\/\/.*/],
  ["string", /^"[^"]*"/],
  ["keyword", /^(?:const|let|return|function)\b/],
  ["number", /^\d+/],
  ["plain", /^[\s\S]/],
];

function tokenize(code) {
  const tokens = [];
  let rest = code;
  while (rest) {
    const [type, re] = RULES.find(([, r]) => r.test(rest));
    const text = rest.match(re)[0];
    tokens.push({ type, text });
    rest = rest.slice(text.length);
  }
  return tokens;
}

const code = [
  "const narx = 35000;",
  "const soni = 3;",
  "// jami",
  "return narx * soni;",
].join("\n");

export default function App() {
  const counts = new Map();
  for (const t of tokenize(code)) {
    if (t.type === "plain") continue;
    counts.set(t.type, (counts.get(t.type) ?? 0) + 1);
  }
  return (
    <ul>
      {[...counts].map(([type, n]) => (
        <li key={type}>
          {type}: {n}
        </li>
      ))}
    </ul>
  );
}

Ekranda:

text
keyword: 3
number: 2
comment: 1

Map kalitlarni qo'shilish tartibida saqlaydi — birinchi const (keyword), keyin 35000 (number), keyin izoh. return ham kalit so'z — shuning uchun 3 ta. narx va soni — plain: modelimizda "o'zgaruvchi" turi yo'q. Haqiqiy grammatikalarda bor.

3-mashq (qiyin): meta satrdan sarlavha va til

parseMeta(info) funksiyasini yozing: ```ts {1,3} title="narx.ts" ning info satridan (ts {1,3} title="narx.ts") obyekt qaytarsin: { lang: "ts", lines: [1, 3], title: "narx.ts" }. Sarlavha yoki qatorlar bo'lmasa — title: null, lines: [].

Ishora: til — birinchi so'z; sarlavha — /title="([^"]*)"/; qatorlar — «Qator oralig'ini o'qish» bo'limidagi parseLines.

Yechim
js
function parseLines(meta) {
  const m = /\{([\d,\s-]+)\}/.exec(meta);
  const lines = new Set();
  if (!m) return lines;
  for (const part of m[1].split(",")) {
    const [from, to = from] = part.trim().split("-").map(Number);
    for (let n = from; n <= to; n++) lines.add(n);
  }
  return lines;
}

function parseMeta(info) {
  const lang = info.trim().split(/\s+/)[0] || null;
  const title = /title="([^"]*)"/.exec(info)?.[1] ?? null;
  return { lang, lines: [...parseLines(info)], title };
}

console.log(parseMeta('ts {1,3} title="narx.ts"'));
console.log(parseMeta("js"));

Konsolda:

text
{ lang: 'ts', lines: [ 1, 3 ], title: 'narx.ts' }
{ lang: 'js', lines: [], title: null }

?.[1] ?? null — moslik bo'lmasa exec null qaytaradi, ?. xatoni oldini oladi, ?? esa undefined o'rniga null beradi. rehype-pretty-code ham meta satrni shunday o'qiydi — faqat ko'proq imkoniyat bilan (so'z ajratish, diff).

4-mashq: tajriba qadami (ixtiyoriy)

vazifalar ga tegmang. Alohida Vite loyihada (kurs/tajriba/kod/) react-markdown + components.code + prism-react-renderer bilan izoh chizuvchisini yasang. Keyin xuddi shuni Shiki bilan — avval codeToHtml + dangerouslySetInnerHTML, keyin codeToHast + toJsxRuntime. Har variantni vazifalar CSP <meta> i bilan oching.

Yechim

Tekshiruv ro'yxati:

  • Prism: npm i -E prism-react-renderer@2.4.1; CSP bilan konsol toza, ranglar bor.
  • Shiki codeToHtml + dangerouslySetInnerHTML: This document requires 'TrustedHTML' assignment.
  • Shiki codeToHast + toJsxRuntime (npm i -E shiki@4.5.0 hast-util-to-jsx-runtime): konsol toza, ranglar bor.
  • npm run build: kanonda Prism +26.2 KB, Shiki (JS dvigatel, 1 til) +70.5 KB; sizning raqamingiz tillar va mavzular soniga bog'liq.

10. Real ishda

  • Hujjat saytlari (React, Vite, Next.js hujjatlari) — build vaqtida Shiki, ko'pincha rehype-pretty-code yoki shunga o'xshash plagin.
  • Bloglar va kurslar — serverda yoki build'da; bu kurs sayti — serverda highlight.js.
  • Chat va forum — foydalanuvchi kodi, brauzerda: Prism yoki highlight.js, kech yuklanadi.
  • Kod muharrirlari — yoritish muharrirning o'zida (Monaco, CodeMirror) — keyingi darslar.
  • Intervyu savollari: "Sintaksis yoritish qanday ishlaydi?", "nega dangerouslySetInnerHTML siz yoritish yaxshiroq?", "build vaqtida va runtime'da yoritish farqi?", "nusxalash tugmasini qanday a11y qilasiz?".

Xulosa

  • Yoritish = tokenlash (grammatika) + rang (mavzu); kod matni o'zgarmaydi.
  • highlight.js va Shiki codeToHtml — HTML satr, innerHTML → vazifalar TT'da yiqildi; prism-react-renderer va Shiki codeToHast + toJsxRuntime — React elementlari, CSP 0.
  • Markdown'da — components.code: til language-… sinfidan, oxirgi \n kesiladi.
  • Kod bloki: raqamlar aria-hidden va user-select: none, nusxa — code ning o'zi, "Nusxalandi" — role="status", xato holati ham.
  • Oldindan ma'lum kod — build vaqtida (0 KB JS); foydalanuvchi kodi — brauzerda, yengil va kech.
  • Kanon: highlight.js +10.0, Prism +26.2, Shiki +70.5 KB; vazifalar da yoritish yo'q — narxi foydasidan katta.

Keyingi dars: Rich text muharrir I: Tiptap — Markdown belgilarisiz, "Word kabi" tahrir: toolbar, qalin tugmasi, JSON va HTML'da saqlash va nega bu vazifalar ga kirmadi.

Manbalar

  • prism-react-renderer 2: README (Highlight, themes, getTokenProps) — github.com/FormidableLabs/prism-react-renderer
  • Shiki: "Installation", "Bundles", "JavaScript RegExp Engine", codeToHast — shiki.style
  • highlight.js 11: "Usage", "Importing the library" (core + tillar) — highlightjs.org
  • rehype-pretty-code — rehype-pretty.pages.dev; rehype-highlight — github.com/rehypejs/rehype-highlight
  • hast-util-to-jsx-runtime — github.com/syntax-tree/hast-util-to-jsx-runtime
  • MDN: Clipboard API writeText, CSP style-src — developer.mozilla.org
  • Kanon: natijalar/tajriba/kutubxonalar-21.txt, tajriba/kutubxonalar/src/k/kod-*.tsx (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

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