feat(wiki): подсветка синтаксиса JS и Lua в код-блоках
Раньше код был монотонным — все белое на чёрном.
Сейчас цветной syntax highlighting в стиле Dracula:
- розовый (#ff79c6) — ключевые слова (let/const/function/local/end/then)
- голубой (#8be9fd) — встроенные (game/workspace/Math/Vector3)
- жёлтый (#f1fa8c) — строки
- фиолетовый (#bd93f9) — числа
- серый (#6272a4) italic — комментарии
- зелёный (#50fa7b) — имена функций (id с () после)
Реализация: docsLang.highlightCode() — простой regex-токенизатор.
<Code> компонент авто-детектит lang ('js'/'lua') по содержимому
(паттерны local/then/--/:Connect), либо принимает явный prop lang=.
Без внешних библиотек — ~80 строк регулярки, легко поддерживать.
This commit is contained in:
parent
35cd304b0e
commit
d6ba23ae8d
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocIcon from './docsIcons';
|
import DocIcon from './docsIcons';
|
||||||
import { LangTabs } from './docsLang';
|
import { LangTabs, highlightCode } from './docsLang';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* docsData.jsx — контент вики редактора Рублокс (разделы A-J).
|
* docsData.jsx — контент вики редактора Рублокс (разделы A-J).
|
||||||
@ -22,10 +22,20 @@ import { LangTabs } from './docsLang';
|
|||||||
* только SVG-иконки (см. docsIcons.jsx).
|
* только SVG-иконки (см. docsIcons.jsx).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ── Код-блок ──────────────────────────────────────────────────────
|
// ── Код-блок с подсветкой синтаксиса ──────────────────────────────
|
||||||
export const Code = ({ children }) => (
|
// lang='js' (default) | 'lua'. Если не указан — автодетект по содержимому.
|
||||||
<pre className="docCode"><code>{children}</code></pre>
|
export const Code = ({ children, lang }) => {
|
||||||
);
|
const text = typeof children === 'string' ? children
|
||||||
|
: Array.isArray(children) ? children.join('') : String(children);
|
||||||
|
const resolved = lang || (
|
||||||
|
/\blocal\b|\bthen\b|\bend\b|\b:Connect\b|\bfunction\(|--\s/.test(text) ? 'lua' : 'js'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<pre className="docCode" data-lang={resolved}>
|
||||||
|
<code dangerouslySetInnerHTML={{ __html: highlightCode(text, resolved) }} />
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ── Плашка «куда писать скрипт» ───────────────────────────────────
|
// ── Плашка «куда писать скрипт» ───────────────────────────────────
|
||||||
// kind="global" — глобальный скрипт (создаётся в категории «Скрипты»)
|
// kind="global" — глобальный скрипт (создаётся в категории «Скрипты»)
|
||||||
|
|||||||
@ -14,6 +14,105 @@
|
|||||||
*/
|
*/
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// Простая подсветка синтаксиса для JS и Lua
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const JS_KEYWORDS = new Set([
|
||||||
|
'let', 'const', 'var', 'function', 'return', 'if', 'else', 'for', 'while',
|
||||||
|
'do', 'switch', 'case', 'break', 'continue', 'new', 'this', 'class',
|
||||||
|
'extends', 'super', 'true', 'false', 'null', 'undefined', 'try', 'catch',
|
||||||
|
'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'async', 'await',
|
||||||
|
'import', 'export', 'from', 'default', 'delete', 'void',
|
||||||
|
]);
|
||||||
|
const JS_BUILTINS = new Set([
|
||||||
|
'game', 'Math', 'Object', 'Array', 'String', 'Number', 'Boolean', 'JSON',
|
||||||
|
'console', 'setTimeout', 'setInterval', 'Promise', 'document', 'window',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const LUA_KEYWORDS = new Set([
|
||||||
|
'local', 'function', 'end', 'if', 'then', 'else', 'elseif', 'for', 'while',
|
||||||
|
'do', 'repeat', 'until', 'return', 'break', 'and', 'or', 'not', 'true',
|
||||||
|
'false', 'nil', 'in', 'goto',
|
||||||
|
]);
|
||||||
|
const LUA_BUILTINS = new Set([
|
||||||
|
'game', 'workspace', 'script', 'Instance', 'Vector3', 'Vector2', 'Color3',
|
||||||
|
'CFrame', 'UDim2', 'UDim', 'BrickColor', 'Enum', 'math', 'string', 'table',
|
||||||
|
'task', 'print', 'warn', 'pairs', 'ipairs', 'pcall', 'tostring', 'tonumber',
|
||||||
|
'TweenInfo', 'wait', 'tick', 'type', 'require', 'next', 'setmetatable',
|
||||||
|
'getmetatable', 'rawget', 'rawset',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Возвращает HTML-строку с раскрашенным кодом. lang: 'js' | 'lua'. */
|
||||||
|
export function highlightCode(text, lang) {
|
||||||
|
if (typeof text !== 'string') return escapeHtml(String(text || ''));
|
||||||
|
const isLua = lang === 'lua';
|
||||||
|
const keywords = isLua ? LUA_KEYWORDS : JS_KEYWORDS;
|
||||||
|
const builtins = isLua ? LUA_BUILTINS : JS_BUILTINS;
|
||||||
|
// Регулярки для токенов — порядок важен: сначала комменты и строки,
|
||||||
|
// потом числа, потом identifier'ы.
|
||||||
|
// JS: //... и /*...*/. Lua: --... и --[[...]].
|
||||||
|
const commentRe = isLua
|
||||||
|
? /--\[\[[\s\S]*?\]\]|--[^\n]*/g
|
||||||
|
: /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
|
||||||
|
// Строки: одинарные, двойные, в JS ещё бэктики.
|
||||||
|
const stringRe = isLua
|
||||||
|
? /"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|\[\[[\s\S]*?\]\]/g
|
||||||
|
: /"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g;
|
||||||
|
const numRe = /\b\d+(?:\.\d+)?\b/g;
|
||||||
|
const idRe = /[A-Za-zА-Яа-я_$][A-Za-zА-Яа-я0-9_$]*/g;
|
||||||
|
|
||||||
|
// Берём весь текст, делим на токены через одну общую регулярку.
|
||||||
|
const tokens = [];
|
||||||
|
const combined = new RegExp(
|
||||||
|
commentRe.source + '|' + stringRe.source + '|' + numRe.source + '|' + idRe.source,
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
while ((match = combined.exec(text)) !== null) {
|
||||||
|
const start = match.index;
|
||||||
|
const tok = match[0];
|
||||||
|
if (start > lastIndex) {
|
||||||
|
tokens.push({ type: 'raw', text: text.slice(lastIndex, start) });
|
||||||
|
}
|
||||||
|
// Классифицируем
|
||||||
|
if (tok.startsWith('--') || tok.startsWith('//') || tok.startsWith('/*')) {
|
||||||
|
tokens.push({ type: 'comment', text: tok });
|
||||||
|
} else if (/^["'`\[]/.test(tok)) {
|
||||||
|
tokens.push({ type: 'string', text: tok });
|
||||||
|
} else if (/^\d/.test(tok)) {
|
||||||
|
tokens.push({ type: 'number', text: tok });
|
||||||
|
} else if (keywords.has(tok)) {
|
||||||
|
tokens.push({ type: 'keyword', text: tok });
|
||||||
|
} else if (builtins.has(tok)) {
|
||||||
|
tokens.push({ type: 'builtin', text: tok });
|
||||||
|
} else {
|
||||||
|
// Идентификатор — проверим, идёт ли за ним ( → функция
|
||||||
|
const rest = text.slice(start + tok.length);
|
||||||
|
if (/^\s*\(/.test(rest)) {
|
||||||
|
tokens.push({ type: 'fn', text: tok });
|
||||||
|
} else {
|
||||||
|
tokens.push({ type: 'ident', text: tok });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex = start + tok.length;
|
||||||
|
}
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
tokens.push({ type: 'raw', text: text.slice(lastIndex) });
|
||||||
|
}
|
||||||
|
return tokens.map(t => {
|
||||||
|
const safe = escapeHtml(t.text);
|
||||||
|
if (t.type === 'raw' || t.type === 'ident') return safe;
|
||||||
|
return `<span class="hl-${t.type}">${safe}</span>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const LS_KEY = 'rublox.docs.lang';
|
const LS_KEY = 'rublox.docs.lang';
|
||||||
const DEFAULT_LANG = 'js';
|
const DEFAULT_LANG = 'js';
|
||||||
|
|
||||||
@ -310,4 +409,14 @@ export const DOCS_LANG_STYLES = `
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.langChoiceCancel:hover { background: #232842; color: #fff; }
|
.langChoiceCancel:hover { background: #232842; color: #fff; }
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════
|
||||||
|
Подсветка синтаксиса в код-блоках
|
||||||
|
══════════════════════════════════════════════════════════════════ */
|
||||||
|
.docCode .hl-keyword { color: #ff79c6; font-weight: 600; } /* let/const/local/function */
|
||||||
|
.docCode .hl-builtin { color: #8be9fd; } /* game / workspace / Math */
|
||||||
|
.docCode .hl-string { color: #f1fa8c; } /* 'строки' "строки" */
|
||||||
|
.docCode .hl-number { color: #bd93f9; } /* 42, 3.14 */
|
||||||
|
.docCode .hl-comment { color: #6272a4; font-style: italic; } /* // или -- */
|
||||||
|
.docCode .hl-fn { color: #50fa7b; } /* myFunc() */
|
||||||
`;
|
`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user