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() */ `;