Как создать красивую страницу с помощью NextJS, MDX

Если вы разработчик, то хотя бы один раз в своем путешествии вы натыкались на красивый блок кода с красивым цветом пользовательской темы, отображением нужной строки, цветом синтаксиса, отображением имени типа файла… И вы хотите сделать то же самое. В этом посте я покажу вам все, что знаю о том, как сделать пользовательский цифровой блог с помощью MDX.


Предварительные условия

  1. Вы должны быть немного знакомы с NextJS. Если вы еще не пробовали NextJS, я настоятельно рекомендую вам просмотреть учебник по NextJS с их официального сайта (так как там все достаточно понятно объясняется и помогает создать небольшой сайт).

  2. Что касается стилей, я использую ChakraUI для оформления своего сайта, но я не рекомендую вам следовать той же стратегии. Вместо этого я предлагаю вам использовать CSS-фреймворк (или даже чистый CSS), который вы хорошо знаете в настоящее время. Я постараюсь как можно лучше объяснить свойства каждого компонента ChakraUI, чтобы вы могли применить ту же идею.

  3. Что касается MDX, я настоятельно рекомендую вам просмотреть их страницу Getting started, там может быть много процессов интеграции с другими фреймворками, о которых вы не слышали, но давайте пока сосредоточимся на их разделе NextJS. Затем прочитайте страницу Using MDX, чтобы иметь некоторое представление о том, как они используют MDX, вы можете пойти дальше и попробовать MDX с NextJS, поскольку вы уже имеете некоторое представление о том, как генерировать страницы в NextJS из раздела 1.

Если что-то пойдет не так, пожалуйста, обратитесь к этому репозиторию для получения дополнительной информации, или вы можете создать проблему в моем основном репозитории сайта для получения дополнительных разъяснений, чтобы я мог улучшить содержание.

Установка и настройка

Есть некоторые пакеты, которые вам нужно будет установить заранее. Я объясню назначение каждого из них:

  • mdx-js/loader. Это webpack-версия MDX, которая поможет вам загрузить MDX (можно представить, что это как компилятор для перевода MDX в структуру HTML). Если вы собираетесь использовать MDX непосредственно в директории page NextJS, вам необходимо установить этот пакет, поскольку это требование для MDX. Есть и другой вариант, который я сейчас использую: я полностью отделяю содержимое из папки page и использую next-mdx-remote (который я представлю ниже) для получения содержимого для getStaticProps. Настройте ваш next.config.js (Если вы просто хотите поместить содержимое в папку page, чтобы nextjs автоматически отрисовал его):
module.exports = {
  reactStrictMode: true,

  // Prefer loading of ES Modules over CommonJS
  experimental: { esmExternals: true },
  // Support MDX files as pages:
  pageExtensions: ['md', 'mdx', 'tsx', 'ts', 'jsx', 'js'],
  // Support loading `.md`, `.mdx`:
  webpack(config, options) {
    config.module.rules.push({
      test: /.mdx?$/,
      use: [
        // The default `babel-loader` used by Next:
        options.defaultLoaders.babel,
        {
          loader: '@mdx-js/loader',
          /** @type {import('@mdx-js/loader').Options} */
          options: {
            /* jsxImportSource: …, otherOptions… */
          },
        },
      ],
    });

    return config;
  },
};
Войдите в полноэкранный режим Выход из полноэкранного режима
  • date-fns. Это совершенно необязательно, вам не нужно его устанавливать, так как это просто инструмент для форматирования даты в мета-данных.
  • gray-matter. Это также необязательно, это похоже на YAML ключ/значение, которое поможет вам иметь некоторые дополнительные данные (мета-данные) в вашем mdx. Пример (выделенные части — это мета-данные):
author: Van Nguyen Nguyen
date: "2022-02-05"
summary: "Something"

---

Your content go here
Войти в полноэкранный режим Выйти из полноэкранного режима
  • next-mdx-remote. Если вы не хотите использовать mdx-js/loader и хотите получать содержимое снаружи, это обязательное условие, поскольку этот пакет позволит вашему MDX быть загруженным в getStaticProps или getServerSideProps (вы уже должны знать эти вещи) из NextJS. Существует несколько альтернатив для этого: mdx-bundler и next-mdx от NextJS. Вы можете посмотреть сравнение здесь

  • prism-react-renderer. Это пакет, который поможет вам настроить ваш блок кода. Это рекомендация, потому что существует множество пакетов для выполнения одних и тех же задач. Я объясню логику позже.

  • mdx-js/react. Этот пакет предоставит MDXProvider для передачи пользовательских компонентов.

Создавать пользовательские теги для страницы

Настройка фундаментальной логики для рендеринга MDX

Во-первых, нам нужен контент для сайта. Я настоятельно рекомендую вам использовать уже готовый учебный веб-проект из NextJS. Затем мы можем создать папку с MDX-файлом на корневом уровне:

//try-mdx/test.mdx
---
title: "This is for Trying MDX"
date: "2020-01-02"
summary: "This is the summary testing for MDX"
---

# Ahihi this is a custome Heading

<Test>
    <Something>Hello World </Something>
</Test> 

a [link](https://example.com), an ![image](./image.png), some *emphasis*,
something **strong**, and finally a little `<div/>`.  
**strong**

// Remove the sign '' from codeblock since DEV editor does not accept it
```javascript file=testing.js highlights=1,2
const test= 1;
const funnyThing = () => {
    console.log(test);
}
funnyThing()```

Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь нам нужно найти способ получить содержимое MDX-файла. Если вы уже прошли учебник NextJS, то знаете, что можно получить путь и содержимое, применив некоторую логику, но вместо файла с .md, вы получите файл с .mdx.

// lib/posts.js
import fs from 'fs';
import path from 'path';
// Using gray matter for getting metadata
import matter from 'gray-matter';

const postsDirectory = path.join(process.cwd(), '/try-mdx');

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory);
  const allPostsData = fileNames.map(fileName => {
    const ext = fileName.split('.')[1];
    // Remove ".mdx" from file name to get id
    const id = fileName.replace(/.mdx$/, '');

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName);
    const fileContents = fs.readFileSync(fullPath, 'utf8');

    // Use gray-matter to parse the post metadata section
    const matterResult = matter(fileContents);
    // Combine the data with the id
    return {
      id,
      ...matterResult.data,
    };
  });
  // Sort posts by date
  return allPostsData.sort(({ date: a }, { date: b }) => {
    if (a < b) {
      return 1;
    } else if (a > b) {
      return -1;
    } else {
      return 0;
    }
  });
}

export function getAllPostIds() {
  // Read all the filename in the directory path
  const fileNames = fs.readdirSync(postsDirectory);

  // Filter out the ext, only need to get the name of the file
  return fileNames.map(fileName => { return {
      // Following routing rule of NextJS
      params: {
        id: fileName.replace(/.mdx$/, ''),
      },
    };
  });
}

export async function getPostData(id) {
  // For each file name provided, we gonna file the path of the file
  const fullPath = path.join(postsDirectory, `${id}.mdx`);
  // Read the content in utf8 format
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  //  Using gray-matter to get the content and that data
  const { content, data } = matter(fileContents);

  // provide what need to be rendered for static-file-generation
  return {
    id,
    content,
    ...data,
  };
}

Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь я предполагаю, что вы понимаете, что такое статическая генерация, а также динамическая маршрутизация (поскольку это фундаментальные темы, которые были рассмотрены в учебном курсе NextJS), как использовать getStaticPaths и getStaticProps.

Если вы следуете подходу mdx-js/loader, вы можете просто создать некоторый [имя файла].mdx и увидеть, как
произойдет волшебство, содержимое, которое вы напишите в MDX-файле, будет переведено в формат HTML. Делайте
не забудьте настроить ваш next.config.js и установить mdx-js/loader.

Если вы следуете next-md-remote, вы должны выделить содержимое вашего блога из папки page/, чтобы NextJS не отобразил его. Затем используйте динамический маршрут для их получения.

pages/
...
├── posts
│   └── [id].js  // Dynamic Routing
...

Вход в полноэкранный режим Выход из полноэкранного режима

Внутри файла [id].js:

// pages/posts/[id].js

// Getting component from NextJS tutorial
// Layout is just the wrapper with the styling width to move page to the center with 
// some extra metadata
import Layout from '../../components/layout';
// Head component is add the title for the page
import Head from 'next/head';
// Date component from NextJS tutorial, basically it will format the date for you 
// but you could just print a raw date string
import Date from '../../components/date';

// Function to get path and contents of the .mdx file (already mentioned above) 
import { getAllPostIds, getPostData } from '../../lib/posts';

// This is just come basic class for styling some tags 
import utilStyles from '../../components/utils.module.css';

// Two important function from next-mdx-remote that make the magic happens
// serialize will help us to convert raw MDX file into object that will be passed
to MDXRemote for rendering HTML on the page 
import { serialize } from 'next-mdx-remote/serialize';
// MDXRemote is the component for rendering data that get from serialize
import { MDXRemote } from 'next-mdx-remote';

export async function getStaticPaths() {

  // Get all the unique path that we need( the name of the folder)
  const paths = getAllPostIds();
  return {
    // Return the path
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  // Get the raw data of the MDX file according to the path that we get
  // Including the metadata and the raw content
  const postData = await getPostData(params.id);

  // Translating the raw content into readable object by serialize
  // I recommend you to console.log the value to see how they look like
  const mdxSource = await serialize(postData.content, {
    // next-mdx-remote also allow us to use remark and rehype plugin, reading MDX docs for more information
    // I am currently not using any plugin, so the array will be empty.
    mdxOptions: {
      remarkPlugins: [],
      rehypePlugins: [],
    },
  });
  return {
    // we only need 2 things from the props
    // postData (we dont care about the content since that one we will get from the mdxSource)
    // We care about getting the metadata here so that is why we still need to get postData
    props: {
      postData,
      mdxSource,
    },
  };
}

export default function Post({ postData, mdxSource }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        // MDXRemote is the components to render the actual content, other components above is just for 
        // metadata
        <MDXRemote {...mdxSource} />
      </article>
    </Layout>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вы можете спросить: «Хм, почему я должен использовать next-remote-mdx для настройки всего этого? Вместо этого я могу просто использовать mdx-js/loader и позволить NextJS автоматически отрисовывать мою страницу». Ну, я решил пойти этим путем, потому что хочу легко добавить больше настроек на мою страницу, например, иметь больше компонентов в моем <Post/>. «Но разве MDX не позволяет уже импортировать новые компоненты?». Да, но управление через JSX всегда проще и лучше. Например, вы можете иметь некоторую логику прямо в компоненте <Post/>, что раздражает в MDX.

Ваша страница, вероятно, будет выглядеть следующим образом.

Стилизация ваших тегов

MDX Docs фактически показывает вам способ стилизации ваших компонентов через

NextJS позволяет вам настраивать App, что это дает вам в данном случае:

  • Ввод дополнительных данных на страницы (что позволяет нам обернуть каждый новый компонент и импортировать новые данные, и эти данные будут добавлены на весь сайт на нескольких страницах).
  • Сохранение макета между изменениями страниц (это означает, что вы можете обернуть все приложение пользовательским компонентом, и этот новый компонент будет применяться глобально).
  • Добавление глобального CSS (что позволит вам применить цветовую тему для вашего блока кода).

Создайте customHeading.js в папке components.

components/
├── customHeading.js
├── ... 
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Внутри customHeading.js

//components/customHeading.js

//This is custom h1 tag = '#'
const MyH1 = props => <h1 style={{ color: 'tomato' }} {...props} />;

//This is custom h2 tag = '##'
const MyH2 = props => <h2 style={{ color: 'yellow' }} {...props} />;


//This is custom link tag = '[<name>](<url>)'
const MyLink = props => {
  console.log(props); // Will comeback to this line
  let content = props.children;
  let href = props.href;
  return (
    <a style={{ color: 'blue' }} href={href}>
      {content}
    </a>
  );
};

const BoringComponent = () => {
    return <p>I am so bored</p>
}

export { MyH1, MyH2, MyLink, BoringComponent };

Войдите в полноэкранный режим Выход из полноэкранного режима

Посмотрев на код, вы зададитесь вопросом: «Хорошо, но что за переменная props там находится?». Я объясню эту идею позже. А сейчас давайте сначала разберемся с работой пользовательских компонентов.

Создайте _app.js в папке с вашей страницей или, если у вас уже был такой файл, вам больше не нужно создавать новый.

pages/
...
├── _app.js 
...

Войдите в полноэкранный режим Выйти из полноэкранного режима

Внутри _app.js

// pages/_app.js

// You do not need to worry about these things
// it just give you some extra global style for the page
import '../styles/global.css';
import '../src/theme/style.css';
import { ChakraProvider } from '@chakra-ui/react';
import theme from '../src/theme/test';

// These are important line
import { MyH1, MyH2, MyLink, BoringComponent } from '../components/CustomHeading';
import { MDXProvider } from '@mdx-js/react';

// MDXProvider accept object only
const components = { h1: MyH1, h2: MyH2, a: MyLink, BoringComponent };

export default function App({ Component, pageProps }) {
  return (
    // Do not worry about the <ChakraProvider/>, it just give you the global style
    <ChakraProvider theme={theme}>
        // Wrapping the <Component/> by <MDXProvider/> so everypage will get applied 
        //the same thing
      <MDXProvider components={components}>
        // <Component/> is the feature of NextJS which identify the content of your 
        // current page. <Component/> will change its pageProps to new page when you change to new
        // page
        <Component {...pageProps} />;
      </MDXProvider>
    </ChakraProvider>
  );
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь вы можете видеть, что заголовок станет красным, потому что мы используем h1, если вы знакомы с markdown, а link станет синим.

Теперь давайте вернемся к переменной props. Если вы прокрутите страницу вверх, то увидите, что я сделал console.log(props).
Давайте посмотрим, что это такое из консоли

Если вы знаете о ReactJS (я предполагаю, что знаете), то если вы передаете компоненту любое ключевое значение, вы можете получить его значение через props. Так MDX под капотом уже разобрал весь файл, чтобы узнать, какой из них является ссылкой, изображением, заголовком, кодовым блоком… Поэтому вы можете получить значение оттуда.

К этому моменту вы знаете, как MDX взаимодействует со своими пользовательскими компонентами, просто получая информацию из реквизитов и передавая ее в новые пользовательские компоненты, можете пропустить следующее объяснение.

Простое объяснение MDXProvider


import Random from 'somewhere'

# Heading 

<Random/>

I feel bored
Вход в полноэкранный режим Выйти из полноэкранного режима

Вот что мы получаем, когда MDX переводит файл в JSX

import React from 'react'
import { MDXTag } from '@mdx-js/tag'
import MyComponent from './my-component'

export default ({ components }) => (
  <MDXTag name="wrapper" components={components}>
    <MDXTag name="h1" components={components}>
        Heading 
    </MDXTag>
    <Random />
    <MDXTag name="p" components={components}>
        I feel bored 
    </MDXTag>
  </MDXTag>
)
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы видим, что экспорт по умолчанию принимает components из реквизитов. Реквизит name реквизита MDXTag будет сопоставлен с компонентом, определенным в реквизите components. Поэтому, когда мы создаем переменную components, мы должны указать, с каким тегом сопоставлен этот компонент. Если же вы не хотите ничего сопоставлять, а просто используете его в MDX-файле, нам не нужно указывать тег имени.

Стилизация вашего блока кода

Это, вероятно, то, чего ждали многие. Давайте пройдемся по нему вместе.

Выбор темы подсветки синтаксиса очень важен, так как она сделает ваш блок кода более читабельным. Я лично использую свою любимую тему GruvBox Dark. Или вы можете найти больше красивых тем в этом репозитории.

Мой подход заключается в том, что я буду применять эту тему подсветки синтаксиса глобально, я не хочу менять ее динамически
и я знаю, что цель моего сайта — это просто небольшой блог, поэтому нет необходимости использовать несколько цветов подсветки синтаксиса.

Сначала поместите куда-нибудь css для подсветки кода. Я рекомендую создать папку styles/ в корне сайта.

styles/
└── gruvBox.css
...
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Перейдите к вашему _app.js и добавьте стилизацию


import '../styles/global.css';
import '../src/theme/style.css';
import { ChakraProvider } from '@chakra-ui/react';
import theme from '../src/theme/test';

import { MyH1, MyH2, MyLink, BoringComponent } from '../components/CustomHeading';
import { MDXProvider } from '@mdx-js/react';

// When you put the styling in _app.js the style will be applied across the whole website
import '../styles/gruvBox.css';

const components = { h1: MyH1, h2: MyH2, a: MyLink, BoringComponent };

export default function App({ Component, pageProps }) {
  return (
    <ChakraProvider theme={theme}>
      <MDXProvider components={components}>
        <Component {...pageProps} />;
      </MDXProvider>
    </ChakraProvider>
  );
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Вау, цвет изменился!!! На самом деле не совсем, если вы посмотрите на свою страницу прямо сейчас, цвет будет очень странным. Позвольте
объяснить почему. Во-первых, это то, что вы получаете из структуры HTML на вашей странице (вы можете просто проинспектировать из вашего
браузера, чтобы проверить разметку и стиль). Целая строка кода закрыта тегом <code/>.

<pre><code class="language-javascript" metastring="file=testing.js highlights=1,3-9" file="testing.js" highlights="1,3-9">
"const ahihi = 1;
export async function getStaticProps({ params }) {
    const postData = await getPostData(params.id);
    const mdxSource = await serialize(postData.content);
    console.log(postData);
    console.log(mdxSource);
    return {
        props: {
            postData,
            mdxSource,
        },
    };
}"
</code></pre>
Вход в полноэкранный режим Выйти из полноэкранного режима

И это единственная стилизация, которая была применена к этой разметке выше


code[class*="language-"], pre[class*="language-"] {
    color: #ebdbb2;
    font-family: Consolas, Monaco, "Andale Mono", monospace;
    direction: ltr;
    text-align: left;
    white-space: pre;
    word-spacing: normal;
    word-break: normal;
    line-height: 1.5;
    -moz-tab-size: 4;
    -o-tab-size: 4;
    tab-size: 4;
    -webkit-hyphens: none;
    -ms-hyphens: none;
    hyphens: none;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Но если вы посмотрите свой любимый лист синтаксических стилей, у нас есть много разных вещей, таких как: token, comment, delimiter, operator,… Так откуда же берутся все эти вещи? Они возникают в процессе токенизации кода. Поэтому вы должны найти способ токенизировать эту строку, чтобы
Вы сможете применить эти стилизации. prism-react-renderer будет отличным инструментом для этого.

Если вы обратитесь к их примеру использования, вы сможете ясно увидеть, как мы будем его использовать. Поскольку они уже предоставили нам пример обертки, нам просто нужно передать наши данные о содержимом.

Создайте customCodeblock.js в папке components/.

// components/customCodeblock.js

// I'm using styled components here since they also recommend using it but you can 
// just create some custom class or applied style directly into the components like the 
// React way.
import styled from '@emotion/styled';
// This is their provided components
import Highlight, { defaultProps } from 'prism-react-renderer';

// Custom <pre/> tag
const Pre = styled.pre`
  text-align: left;
  margin: 1em 0;
  padding: 0.5em;
  overflow: scroll;
  font-size: 14px;
`;

// Cutom <div/> (this is arrangement of the line)
const Line = styled.div`
  display: table-row;
`;

// Custom <span/> (this is for the Line number)
const LineNo = styled.span`
  display: table-cell;
  text-align: right;
  padding-right: 1em;
  user-select: none;
  opacity: 0.5;
`;

// Custom <span/> (this is for the content of the line)
const LineContent = styled.span`
  display: table-cell;
`;


const CustomCode = props => {
  // Pay attention the console.log() when we applied this custom codeBlock into the
  //_app.js. what metadata you are getting, is there anything you did not expect that actually
  // appear. Can you try out some extra features by changing the MDX codeblock content
  console.log(props);

  // From the console.log() you will be able to guess what are these things.
  const className = props.children.props.className || '';
  const code = props.children.props.children.trim();
  const language = className.replace(/language-/, '');

  return (
    <Highlight
      {...defaultProps}
      theme={undefined}
      code={code}
      language={language}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <Pre className={className} style={style}>
          {tokens.map((line, i) => (
            <Line key={i} {...getLineProps({ line, key: i })}>
              <LineNo>{i + 1}</LineNo>
              <LineContent>
                {line.map((token, key) => (
                  <span key={key} {...getTokenProps({ token, key })} />
                ))}
              </LineContent>
            </Line>
          ))}
        </Pre>
      )}
    </Highlight>
  );
};

export default CustomCode;
Войдите в полноэкранный режим Выйти из полноэкранного режима

Давайте применим этот CustomCode в вашем MDXProvider

import '../styles/global.css';
import { ChakraProvider } from '@chakra-ui/react';
import theme from '../src/theme/test';
import '../src/theme/style.css';
import { MyH1, MyH2, MyLink } from '../components/CustomHeading';
import { MDXProvider } from '@mdx-js/react';
import CustomCode from '../components/customCode';
import '../styles/gruvBox.css';

const components = { 
    h1: MyH1, 
    h2: MyH2, 
    a: MyLink, 
    pre: CustomCode };

export default function App({ Component, pageProps }) {
  return (
    <ChakraProvider theme={theme}>
      <MDXProvider components={components}>
        <Component {...pageProps} />;
      </MDXProvider>
    </ChakraProvider>
  );
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Надеюсь, вы получите то, что хотите, цвет должен быть таким, как вы ожидаете. Если что-то не так, пожалуйста, обратитесь к этому репозиторию

prism-react-renderer действительно предоставляет вам цветовую тему, они показали, как применить ее в своей документации, но у них нет GruvBox, поэтому я должен найти GruvBox.
стиль для глобального стиля, чтобы переопределить их цвет по умолчанию. Если вы сможете найти свою любимую тему в их
то нет необходимости добавлять глобальный стиль, вы можете удалить его.

Создание имени файла для вашего блока кода

Я надеюсь, что вы проверили console.log(props) из вашего пользовательского блока кода. Вот что мы видим в консоли:

Здесь есть несколько интересных реквизитов: file, highlights, metastring. Если вы вернетесь к содержанию, которое я уже привел в начале, там есть несколько дополнительных ключевых значений, которые я поместил в кодовый блок и которые для обычного синтаксиса markdown вроде как бесполезны. Но это MDX, MDX фактически анализирует блок кода и дает нам некоторые метаданные.

Из этих данных мы сможем сделать некоторые дополнительные функции. Давайте добавим имя файла/путь к нему:


import styled from '@emotion/styled';
import Highlight, { defaultProps } from 'prism-react-renderer';

const Pre = styled.pre`
...
`;

const Line = styled.div`
...
`;

const LineNo = styled.span`
...
`;

const LineContent = styled.span`
...
`;

const CustomCode = props => {
  console.log(props);
  const className = props.children.props.className || '';
  const code = props.children.props.children.trim();
  const language = className.replace(/language-/, '');
  const file = props.children.props.file;

  return (
    <Highlight
      {...defaultProps}
      theme={undefined}
      code={code}
      language={language}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <>
          <h2>{file}</h2>
          <Pre className={className} style={style}>
            {tokens.map((line, i) => (
              <Line key={i} {...getLineProps({ line, key: i })}>
                <LineNo>{i + 1}</LineNo>
                <LineContent>
                  {line.map((token, key) => (
                    <span key={key} {...getTokenProps({ token, key })} />
                  ))}
                </LineContent>
              </Line>
            ))}
          </Pre>
        </>
      )}
    </Highlight>
  );
};

export default CustomCode;

Вход в полноэкранный режим Выход из полноэкранного режима

Ваше домашнее задание — стилизовать это имя файла для вашего блока кода.

Создание выделения для вашего блока кода

Теперь, если вы посмотрите на метаданные highlights, вам, вероятно, станет интересно, чего я пытаюсь добиться. Моя идея проста:

if my highlights = 1,3-5
I want the value I parse from this string to be like this [1, 3, 4, 5]

if my highlights = 1,2,3 or 1-3
I want the value I parse from this string to be like this [1, 2, 3]

You get it right? the '-' will detect the range that I want to loop through.
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку теперь мы можем получить значение highlights, нам нужно найти способ разобрать эту строку.
Создадим lib/parseRange.js.

// lib/parseRange.js
function parsePart(string) {
  // Array that contain the range result
  let res = [];

  // we split the ',' and looping through every elemenet
  for (let str of string.split(',').map(str => str.trim())) {
    // Using regex to detect whether it is a number or a range
    if (/^-?d+$/.test(str)) {
      res.push(parseInt(str, 10));
    } else {
       // If it is a range, we have to contruct that range
      let split = str.split('-');
      let start = split[0] - '0';
      let end = split[1] - '0';
      for (let i = start; i <= end; i++) {
        res.push(i);
      }
    }
  }
  return res;
}

export default parsePart;
Войдите в полноэкранный режим Выйти из полноэкранного режима

Давайте используем эту штуку для вашего customCodeblock.js:


import styled from '@emotion/styled';
import Highlight, { defaultProps } from 'prism-react-renderer';
// import your function
import parsePart from '../lib/parseRange';

const Pre = styled.pre`
...
`;

const Line = styled.div`
...
`;

const LineNo = styled.span`
...
`;

const LineContent = styled.span`
...
`;

// shouldHighlight will return a function to be called later
// that function will return true or false depend on whether the index will appear
// inside our parsed array
const shouldHighlight = raw => {
  const parsedRange = parsePart(raw);
  if (parsedRange) {
    return index => parsedRange.includes(index);
  } else {
    return () => false;
  }
};

const CustomCode = props => {
  console.log(props);
  const className = props.children.props.className || '';
  const code = props.children.props.children.trim();
  const language = className.replace(/language-/, '');
  const file = props.children.props.file;

  // Getting the raw range
  const rawRange = props.children.props.highlights || '';
  // assign the checking function
  const highlights = shouldHighlight(rawRange);

  return (
    <Highlight
      {...defaultProps}
      theme={undefined}
      code={code}
      language={language}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <>
          <h2>{file}</h2>
          <Pre className={className} style={style}>
            // Getting the index from the mapping line
            {tokens.map((line, i) => (
              <Line key={i} {...getLineProps({ line, key: i })}>
                <LineNo>{i + 1}</LineNo>
                <LineContent
                  style={{
                    background: highlights(i + 1) ? 'gray' : 'transparent',
                  }}
                >
                  {line.map((token, key) => (
                    <span key={key} {...getTokenProps({ token, key })} />
                  ))}
                </LineContent>
              </Line>
            ))}
          </Pre>
        </>
      )}
    </Highlight>
  );
};

export default CustomCode;
Войти в полноэкранный режим Выход из полноэкранного режима

Надеюсь, вы получите желаемый стиль выделения. Теперь вы получили основное представление о том, как
выделить строку. Доработка этого способа будет вашим домашним заданием.

Создание функциональности копирования для вашего блока кода

Для этого мы воспользуемся веб-интерфейсом API под названием Clipboard API.
Я не буду объяснять его механизм, поскольку главный сайт делает это гораздо лучше меня. Вы можете ознакомиться с их объяснением здесь

Давайте изменим наш customCodeblock.js.

// useState to change the text of copy button 
import { useState } from 'react';
import styled from '@emotion/styled';
import Highlight, { defaultProps } from 'prism-react-renderer';
import parsePart from '../lib/parseRange';

const Pre = styled.pre`
...
`;

const Line = styled.div`
...
`;

const LineNo = styled.span`
...
`;

const LineContent = styled.span`
...
`;

const shouldHighlight = raw => {
    ...
};

const CustomCode = props => {

  const [currLabel, setCurrLabel] = useState('Copy');

  const copyToClibBoard = copyText => {
    let data = [
      new ClipboardItem({
        'text/plain': new Blob([copyText], { type: 'text/plain' }),
      }),
    ];
    navigator.clipboard.write(data).then(
      function () {
        setCurrLabel('Copied');
        setTimeout(() => {
          setCurrLabel('Copy');
        }, 1000);
      },
      function () {
        setCurrLabel(
          'There are errors'
        );
      }
    );
  };

  const className = props.children.props.className || '';
  const code = props.children.props.children.trim();
  const language = className.replace(/language-/, '');
  const file = props.children.props.file;

  const rawRange = props.children.props.highlights || '';
  const highlights = shouldHighlight(rawRange);

  return (
    <Highlight
      {...defaultProps}
      theme={undefined}
      code={code}
      language={language}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <>
          <h2>{file}</h2>
          <button
            onClick={() => copyToClibBoard(props.children.props.children)}
          >
            {currLabel}
          </button>
          <Pre className={className} style={style}>
            {tokens.map((line, i) => (
              <Line key={i} {...getLineProps({ line, key: i })}>
                <LineNo>{i + 1}</LineNo>
                <LineContent
                  style={{
                    background: highlights(i + 1) ? 'gray' : 'transparent',
                  }}
                >
                  {line.map((token, key) => (
                    <span key={key} {...getTokenProps({ token, key })} />
                  ))}
                </LineContent>
              </Line>
            ))}
          </Pre>
        </>
      )}
    </Highlight>
  );
};

export default CustomCode;

Вход в полноэкранный режим Выход из полноэкранного режима

Резюме

Я надеюсь, что вы достигли того, что искали, читая мой пост. Это всего лишь базовая логика для автоматизации пользовательских тегов для вашего сайта. Создавайте как можно больше пользовательских компонентов, чтобы удовлетворить ваши потребности, стилизуйте их под ваш любимый цвет. И с этого момента вы можете просто сосредоточиться на своем контенте. Удачи на вашем пути разработчика.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *