311 lines
8.8 KiB
TypeScript
311 lines
8.8 KiB
TypeScript
import { FC, useEffect, useState } from "react";
|
||
import ReactMarkdown from "react-markdown";
|
||
import remarkGfm from "remark-gfm";
|
||
import rehypeHighlight from "rehype-highlight";
|
||
import rehypeRaw from "rehype-raw";
|
||
import rehypeSanitize from "rehype-sanitize";
|
||
import axios from "../../axios";
|
||
import "highlight.js/styles/github-dark.css";
|
||
import Header from "../mission/statement/Header";
|
||
|
||
import { defaultSchema } from "hast-util-sanitize";
|
||
|
||
const schema = {
|
||
...defaultSchema,
|
||
attributes: {
|
||
...defaultSchema.attributes,
|
||
div: [
|
||
...(defaultSchema.attributes?.div || []),
|
||
["style"] // разрешаем атрибут style на div
|
||
]
|
||
}
|
||
};
|
||
|
||
|
||
interface MarkdownEditorProps {
|
||
defaultValue?: string;
|
||
onChange: (value: string) => void;
|
||
}
|
||
|
||
const MarkdownEditor: FC<MarkdownEditorProps> = ({ defaultValue, onChange }) => {
|
||
const [markdown, setMarkdown] = useState<string>(defaultValue || `# 🌙 Добро пожаловать в Markdown-редактор
|
||
|
||
Добро пожаловать в **Markdown-редактор**!
|
||
Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇
|
||
|
||
---
|
||
|
||
## 🧱 1. Форматирование текста
|
||
|
||
Вот примеры базового форматирования:
|
||
|
||
- **Жирный текст**
|
||
- *Курсивный текст*
|
||
- ***Жирный курсив***
|
||
- ~~Зачёркнутый~~
|
||
|
||
> 💬 _Цитаты_ можно использовать для выделения текста, заметок или описаний.
|
||
|
||
---
|
||
|
||
## 🧩 2. Списки
|
||
|
||
### 🔹 Маркированный список
|
||
|
||
- Один
|
||
- Два
|
||
- Вложенный уровень
|
||
- Ещё глубже
|
||
- Три
|
||
|
||
### 🔸 Нумерованный список
|
||
|
||
1. Первый
|
||
2. Второй
|
||
3. Третий
|
||
1. Вложенный
|
||
2. Ещё один
|
||
|
||
---
|
||
|
||
## ✅ 3. Чеклисты (GFM)
|
||
|
||
- [x] Поддержка Markdown
|
||
- [x] Подсветка кода
|
||
- [x] Таблицы
|
||
- [x] Эмодзи 😎
|
||
- [ ] Экспорт в PDF (в будущем)
|
||
|
||
---
|
||
|
||
## 💻 4. Код и подсветка
|
||
|
||
Пример **TypeScript**:
|
||
|
||
\`\`\`tsx
|
||
type User = {
|
||
name: string;
|
||
role: "Разработчик" | "Помощник";
|
||
};
|
||
|
||
function greet(user: User) {
|
||
return \`Привет, \${user.name}! 👋 Роль: \${user.role}\`;
|
||
}
|
||
|
||
console.log(greet({ name: "Ты", role: "Разработчик" }));
|
||
\`\`\`
|
||
|
||
Пример **JavaScript**:
|
||
|
||
\`\`\`js
|
||
const sum = (a, b) => a + b;
|
||
console.log(sum(2, 3)); // 5
|
||
\`\`\`
|
||
|
||
Пример **Python**:
|
||
|
||
\`\`\`python
|
||
def greet(name):
|
||
return f"Привет, {name}! 👋"
|
||
|
||
print(greet("Мир"))
|
||
\`\`\`
|
||
|
||
---
|
||
|
||
## 📊 5. Таблицы (GFM)
|
||
|
||
| Имя | Роль | Активен | Эмодзи |
|
||
|-------------|----------------|----------|--------|
|
||
| ChatGPT | Помощник 🤖 | ✅ | 🤓 |
|
||
| Ты | Разработчик 💻 | ✅ | 🚀 |
|
||
| TailwindCSS | Стилизация 🎨 | 🟢 | 💅 |
|
||
|
||
> Таблицы поддерживают **жирный текст**, _курсив_ и даже \`инлайн-код\` внутри ячеек.
|
||
|
||
---
|
||
|
||
## 🔗 6. Ссылки
|
||
|
||
- [Документация Markdown](https://www.markdownguide.org/)
|
||
- [React Markdown на GitHub](https://github.com/remarkjs/react-markdown)
|
||
- Автоматическая ссылка: https://github.com
|
||
|
||
---
|
||
|
||
## 🖼️ 7. Изображения
|
||
|
||
### Markdown-логотип:
|
||
|
||

|
||
|
||
или
|
||
|
||
<img src=\"https://upload.wikimedia.org/wikipedia/commons/4/48/Markdown-mark.svg\" alt=\"img\"/>
|
||
|
||
или если нужно выравнивание по центру
|
||
|
||
<div style=\"display: flex; items-align: center; justify-content: center; background: gray;\">
|
||
<img src=\"https://upload.wikimedia.org/wikipedia/commons/4/48/Markdown-mark.svg\" alt=\"img\"/>
|
||
</div>
|
||
|
||
|
||
---
|
||
|
||
## 🧠 8. Цитаты и вложенность
|
||
|
||
> 💭 Это обычная цитата.
|
||
>
|
||
> > А это — **вложенная цитата**.
|
||
> >
|
||
> > > Можно вкладывать сколько угодно уровней!
|
||
|
||
---
|
||
|
||
## ⚙️ 9. Горизонтальные линии
|
||
|
||
---
|
||
|
||
***
|
||
|
||
---
|
||
|
||
## 🧮 10. Таблица внутри цитаты
|
||
|
||
> Вот таблица прямо внутри блока цитаты:
|
||
>
|
||
> | Язык | Назначение |
|
||
> |-------|-------------|
|
||
> | JS | Web-разработка |
|
||
> | TS | Строгая типизация |
|
||
> | PY | Скрипты и AI |
|
||
|
||
---
|
||
|
||
## 🧩 11. Встроенный HTML
|
||
|
||
<details>
|
||
<summary>📂 Раскрывающийся блок</summary>
|
||
Этот текст виден только после раскрытия!
|
||
<ul>
|
||
<li>HTML списки работают</li>
|
||
<li>И даже <b>жирный текст</b></li>
|
||
</ul>
|
||
</details>
|
||
|
||
---
|
||
## 🎨 12. Вложенные списки с кодом
|
||
|
||
- Этапы:
|
||
1. Создай проект
|
||
2. Добавь зависимости:
|
||
\`\`\`bash
|
||
npm install react-markdown remark-gfm rehype-highlight highlight.js
|
||
\`\`\`
|
||
3. Импортируй стили:
|
||
\`\`\`tsx
|
||
import "highlight.js/styles/github-dark.css";
|
||
\`\`\`
|
||
4. Готово!
|
||
|
||
---
|
||
|
||
## 🚀 13. Финал
|
||
|
||
Поздравляю! 🎉
|
||
Ты только что увидел все ключевые возможности **Markdown + GFM** в действии.
|
||
|
||
> ✨ Используй этот текст как шаблон для тестирования рендерера.
|
||
> 💡 Совет: попробуй поменять тему \`highlight.js\` (например \`monokai.css\` или \`atom-one-dark.css\`).
|
||
|
||
---
|
||
|
||
**🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!**
|
||
|
||
`);
|
||
|
||
useEffect(() => {
|
||
onChange(markdown);
|
||
}, [markdown]);
|
||
|
||
// Обработчик вставки
|
||
const handlePaste = async (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||
const items = e.clipboardData.items;
|
||
|
||
for (const item of items) {
|
||
if (item.type.startsWith("image/")) {
|
||
e.preventDefault(); // предотвращаем вставку картинки как текста
|
||
|
||
const file = item.getAsFile();
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
|
||
try {
|
||
const response = await axios.post("/media/upload", formData, {
|
||
headers: { "Content-Type": "multipart/form-data" },
|
||
});
|
||
|
||
const imageUrl = response.data.url;
|
||
// Вставляем ссылку на картинку в текст
|
||
const cursorPos = (e.target as HTMLTextAreaElement).selectionStart;
|
||
const newText =
|
||
markdown.slice(0, cursorPos) +
|
||
`<img src=\"${imageUrl}\" alt=\"img\"/>` +
|
||
markdown.slice(cursorPos);
|
||
|
||
setMarkdown(newText);
|
||
} catch (err) {
|
||
console.error("Ошибка загрузки изображения:", err);
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||
<div>
|
||
<Header missionId={1} />
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 h-full min-h-0">
|
||
{/* Предпросмотр */}
|
||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||
<h2 className="text-lg font-semibold mb-3 text-gray-100">👀 Предпросмотр</h2>
|
||
<div className="flex-1 bg-[#161b22] rounded-lg shadow-lg p-6 h-[calc(100%-40px)]">
|
||
<div className="prose prose-invert max-w-none h-full overflow-auto pr-4 medium-scrollbar">
|
||
<ReactMarkdown
|
||
remarkPlugins={[remarkGfm]}
|
||
rehypePlugins={[rehypeRaw, [rehypeSanitize, schema], rehypeHighlight]}
|
||
>
|
||
{markdown}
|
||
</ReactMarkdown>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Редактор */}
|
||
<div className="overflow-y-auto min-h-0 overflow-hidden">
|
||
<div className="p-4 border-r border-gray-700 flex flex-col h-full">
|
||
<h2 className="text-lg font-semibold mb-3 text-gray-100">📝 Редактор</h2>
|
||
<textarea
|
||
value={markdown}
|
||
onChange={(e) => setMarkdown(e.target.value)}
|
||
onPaste={handlePaste} // <-- вот сюда обработчик вставки
|
||
className="flex-1 w-full bg-[#0d1117] text-gray-200 border border-gray-700
|
||
rounded-lg p-5 font-mono text-sm resize-none focus:outline-none focus:ring-2
|
||
medium-scrollbar"
|
||
placeholder="Пиши в формате Markdown..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default MarkdownEditor;
|