diff --git a/src/community/docsData.jsx b/src/community/docsData.jsx index d000e02..000a264 100644 --- a/src/community/docsData.jsx +++ b/src/community/docsData.jsx @@ -1,6 +1,6 @@ import React from 'react'; import DocIcon from './docsIcons'; -import { LangTabs } from './docsLang'; +import { LangTabs, highlightCode } from './docsLang'; /** * docsData.jsx — контент вики редактора Рублокс (разделы A-J). @@ -22,10 +22,20 @@ import { LangTabs } from './docsLang'; * только SVG-иконки (см. docsIcons.jsx). */ -// ── Код-блок ────────────────────────────────────────────────────── -export const Code = ({ children }) => ( -
{children}
-);
+// ── Код-блок с подсветкой синтаксиса ──────────────────────────────
+// lang='js' (default) | 'lua'. Если не указан — автодетект по содержимому.
+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 (
+
+
+
+ );
+};
// ── Плашка «куда писать скрипт» ───────────────────────────────────
// kind="global" — глобальный скрипт (создаётся в категории «Скрипты»)
diff --git a/src/community/docsLang.jsx b/src/community/docsLang.jsx
index b78ecbb..394a4b3 100644
--- a/src/community/docsLang.jsx
+++ b/src/community/docsLang.jsx
@@ -14,6 +14,105 @@
*/
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, '>');
+}
+
+/** Возвращает 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 `${safe}`;
+ }).join('');
+}
+
+
const LS_KEY = 'rublox.docs.lang';
const DEFAULT_LANG = 'js';
@@ -310,4 +409,14 @@ export const DOCS_LANG_STYLES = `
cursor: pointer;
}
.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() */
`;