feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
@ -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 }) => (
|
||||
<pre className="docCode"><code>{children}</code></pre>
|
||||
);
|
||||
// ── Код-блок с подсветкой синтаксиса ──────────────────────────────
|
||||
// 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 (
|
||||
<pre className="docCode" data-lang={resolved}>
|
||||
<code dangerouslySetInnerHTML={{ __html: highlightCode(text, resolved) }} />
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Плашка «куда писать скрипт» ───────────────────────────────────
|
||||
// kind="global" — глобальный скрипт (создаётся в категории «Скрипты»)
|
||||
|
||||
@ -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, '<').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 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() */
|
||||
`;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user