Создание редактора Markdown, используя Next.js и TailwindCss
Присоединяйтесь ко мне в этом проекте, где мы создаем онлайн-редактор Markdown, используя последнюю версию Nextjs.
1. Создайте целевую страницу
Мне нужен простой макет, поэтому я разделил экран на две части; слева — редактор, а справа мы видим рендеринг Markdown.
const Homepage = () => {
return (
<div className='h-screen flex justify-between'>
// Input the markdown
<section className='w-full pt-5 h-full'>
<textarea
className='w-full ... placeholder:opacity-80'
placeholder='Feed me some Markdown 🍕'
autoFocus
/>
</section>
<div className='fixed ... border-dashed' />
// Render the markdown
<article className='w-full pt-5 pl-6'>
Markdown lies here
</article>
</div>
)
}
return Homepage
2. Добавьте состояния для хранения данных
Теперь давайте изменим его на клиентский компонент и добавим хук useState
.
"use client"
import { useState } from "react"
const Homepage = () => {
const [source, setSource] = useState('');
return (
...
<textarea
className='w-full ... placeholder:opacity-80'
placeholder='Feed me some Markdown 🍕'
value={source}
onChange={(e) => setSource(e.target.value)}
autoFocus
/>
...
)
}
3. Настройте React Markdown и @tailwindcss/typography
Мы используем react-markdown
для рендеринга уценки и @tailwindcss/typography
стилизации уценки. Установите их, выполнив следующие команды.
npm install react-markdown
npm install -D @tailwindcss/typography
Теперь импортируйте и добавьте компонент Markdown и передайте его source
как дочерний. Не забудьте добавить prose
имя класса в компонент Markdown.
import Markdown from 'react-markdown'
const Homepage = () => {
return (
...
<div className='fixed ... border-dashed' />
// Render the markdown
<article className='w-full pt-5 pl-6'>
<Markdown
className='prose prose-invert min-w-full'
>
{source}
</Markdown>
</article>
...
)
}
Теперь, если вы введете любую уценку, вы все равно не найдете никаких изменений. Это потому, что мы забыли добавить @tailwindcss/typography
плагин в конфигурацию Tailwindcss.
Измените свой tailwind.config.ts
на следующее:
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
// Add the plugin here
plugins: [require('@tailwindcss/typography')]
}
export default config
Теперь напишите уценку и вы увидите изменения вживую.
4. Подсветка кода и пользовательские компоненты
Теперь нам нужно установить react-syntax-highlighter
пакет, чтобы добавить подсветку кода в наш проект.
npm i react-syntax-highlighter
npm i --save @types/react-syntax-highlighter
Теперь мы собираемся создать пользовательский компонент для средства выделения кода.
Создайте папку с именем components
внутри src
папки. Теперь создайте файл с именем Code.tsx
внутри папки компонентов.
Добавьте следующий код из документации реакции-синтаксиса-выделителя:
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { materialOceanic } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export const CodeBlock = ({ ...props }) => {
return (
<SyntaxHighlighter
language={props.className?.replace(/(?:lang(?:uage)?-)/, '')}
style={materialOceanic}
wrapLines={true}
className='not-prose rounded-md'
>
{props.children}
</SyntaxHighlighter>
)
}
Здесь реквизит содержит имя класса с языком кода в формате: lang-typescript
или иногда language-typescript
мы используем какое-нибудь регулярное выражение, чтобы удалить все, кроме названия языка. Имя not-prose
класса удалит стили оформления по умолчанию.
Теперь вернитесь к основному page.tsx
файлу, импортируйте CodeBlock
компонент и передайте его исходному <Markdown />
компоненту.
import Markdown from 'react-markdown'
import { CodeBlock } from '@/components/Code'
const Homepage = () => {
const options = { code: CodeBlock }
return (
...
<Markdown
className='prose prose-invert min-w-full'
components={options}
>
{source}
</Markdown>
...
)
}
Это заменит каждое появление code
нашего пользовательского CodeBlock
компонента.
Необязательно
ОШИБКА: вокруг компонента кода может возникнуть странная темная рамка, вызванная тегом pre
и стилями tailwind.
Чтобы исправить это, вернитесь к своему файлу Code.tsx
и добавьте следующий код, который удаляет стили tailwind из тега pre
.
export const Pre = ({ ...props }) => {
return (
<div className='not-prose'>
{props.children}
</div>
)
}
Импортируйте это в свой page.tsx
и добавьте в options
переменную:
const Homepage = () => {
const options = {
code: CodeBlock,
// Add here
pre: Pre,
}
return ( ... )
}
Это удалит границу.
5. Добавление плагинов Rehype и Remark
Rehype и Remark — это плагины, используемые для преобразования и управления содержимым HTML и Markdown веб-сайта, помогая улучшить его функциональность и внешний вид.
Мы собираемся использовать следующее:
rehype-sanitize
: Очистит уценку.rehype-external-links
: Добавит значок 🔗 к ссылкам.remark-gfm
: Плагин для поддержки GFM (Поддержка таблиц, сносок и т. д.)
Установите плагины:
npm i remark-gfm rehype-external-links rehype-sanitize
Вернемся к нашему page.tsx
import remarkGfm from 'remark-gfm'
import rehypeSanitize from 'rehype-sanitize'
import rehypeExternalLinks from 'rehype-external-links'
...
<Markdown
...
remarkPlugins={[remarkGfm]}
rehypePlugins={[
rehypeSanitize,
[rehypeExternalLinks,
{ content: { type: 'text', value: '🔗' } }
],
]}
>{source}</Markdown>
Передайте плагины примечаний remarkPlugins
и переделайте плагины rehypePlugins
(я знаю, это очень удивительно).
Если какой-либо плагин требует какой-либо настройки, поместите его в квадратные скобки, за которыми следует имя плагина и параметры в фигурных скобках в следующем синтаксисе: [veryCoolPlugin, { { options } }]
6. Заголовок с кнопками Markdown
Затем мы добавляем компонент заголовка с кнопками, при нажатии на которые вставляются определенные элементы Markdown.
Сначала создайте Header.tsx
в папке components
и напишите следующий код:
const Header = () => {
const btns = [
{ name: 'B', syntax: '**Bold**' },
{ name: 'I', syntax: '*Italic*' },
{ name: 'S', syntax: '~Strikethrough~' },
{ name: 'H1', syntax: '# ' },
]
return (
<header className="flex ... bg-[#253237]">
{btns.map(btn => (
<button
key={btn.syntax}
className="flex ...rounded-md"
>
{btn.name}
</button>
))}
</header>
)
}
export default Header
Импортируйте его в основной page.tsx
import Header from '@/components/Header'
const Homepage = () => {
const options = { code: CodeBlock }
return (
<>
<Header /> // Should be on top
<div className='h-screen flex justify-between'>
...
</div>
</>
)
}
Вот в чем загвоздка. Наши состояния лежат в родительском компоненте, а Header
компонент — в дочернем.
Как мы работаем с состояниями в дочернем компоненте? Лучшее решение — создать функцию для изменения состояния родительского компонента и передать эту функцию дочернему компоненту.
const Homepage = () => {
const [source, setSource] = useState('');
const feedElement = (syntax: string) => {
return setSource(source + syntax)
}
return (
<>
<Header />
...
)
}
В Header.tsx
нужно принять функцию в качестве параметра и добавить ее к кнопке с помощью onClick
атрибута:
const Header = (
{ feedElement }:
{ feedElement: (syntax: string) => void }
) => {
const btns = [ ... ]
return (
...
<button
key={btn.syntax}
className="flex ...rounded-md"
onClick={() => feedElement(btn.syntax)}
>
{btn.name}
</button>
)
}
Возвращаемся к тому, что page.tsx
мы передаем feedElement
функцию в Header
.
const feedElement = (syntax: string) => {
return setSource(source + syntax)
}
return (
<>
<Header feedElement={feedElement} />
...
)
Теперь каждый раз, когда вы нажимаете на кнопку, вы должны получать следующий элемент Markdown.
Подведение итогов
Вот и все. Теперь у нас есть полнофункциональный редактор Markdown, созданный с использованием Nextjs.