纯前端基于react实现在线预览markdown,支持下载markdown、下载转化后的html。附完整代码。

204 阅读4分钟

技术栈:react18,ant design,marked-highlight,highlight.js,marked。

highlight主要负责样式高亮,marked负责格式转换。效果预览:

转化对比:

image.png

markdown预览

image.png

html预览

image.png

首先编写通用的样式代码。新建文件markdown.css:

/* Markdown通用样式 */

/* 设置全局字体样式 */
body {
  font-family: Arial, sans-serif;
  font-size: 16px;
  line-height: 1.6;
  color: #333;
}

/* 设置标题样式 */
h1,
h2,
h3,
h4,
h5,
h6 {
  margin-top: 1.3em;
  margin-bottom: 0.6em;
  font-weight: bold;
}

h1 {
  font-size: 2.2em;
}

h2 {
  font-size: 1.8em;
}

h3 {
  font-size: 1.6em;
}

h4 {
  font-size: 1.4em;
}

h5 {
  font-size: 1.2em;
}

h6 {
  font-size: 1em;
}

/* 设置段落样式 */
p {
  margin-bottom: 1.3em;
}

/* 设置链接样式 */
a {
  color: #337ab7;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* 设置列表样式 */
ul,
ol {
  margin-top: 0;
  margin-bottom: 1.3em;
  padding-left: 2em;
}

/* 设置代码块样式 */
pre {
  background-color: #f7f7f7;
  padding: 1em;
  border-radius: 4px;
  overflow: auto;
}

code {
  font-family: Consolas, Monaco, Courier, monospace;
  font-size: 0.9em;
  background-color: #f7f7f7;
  padding: 0.2em 0.4em;
  border-radius: 4px;
}

/* 设置引用样式 */
blockquote {
  margin: 0;
  padding-left: 1em;
  border-left: 4px solid #ddd;
  color: #777;
}

/* 设置表格样式 */
table {
  border-collapse: collapse;
  width: 100%;
  margin-bottom: 1.3em;
}

table th,
table td {
  padding: 0.5em;
  border: 1px solid #ccc;
}

/* 添加一些额外的样式,如图片居中显示 */
img {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  height: auto;
}

/* 设置代码行号样式 */
pre code .line-numbers {
  display: inline-block;
  width: 2em;
  padding-right: 1em;
  color: #999;
  text-align: right;
  user-select: none;
  pointer-events: none;
  border-right: 1px solid #ddd;
  margin-right: 0.5em;
}

/* 设置代码行样式 */
pre code .line {
  display: block;
  padding-left: 1.5em;
}

/* 设置代码高亮样式 */
pre code .line.highlighted {
  background-color: #f7f7f7;
}

/* 添加一些响应式样式,适应移动设备 */
@media only screen and (max-width: 768px) {
  body {
    font-size: 14px;
    line-height: 1.5;
  }
  
  h1 {
    font-size: 1.8em;
  }
  
  h2 {
    font-size: 1.5em;
  }
  
  h3 {
    font-size: 1.3em;
  }
  
  h4 {
    font-size: 1.1em;
  }
  
  h5 {
    font-size: 1em;
  }
  
  h6 {
    font-size: 0.9em;
  }
  
  table {
    font-size: 14px;
  }
}    

准备工作

  • 安装所需依赖:markedhighlight.jsant designmarked-highlight

  • 然后编写tsx文件。新建MarkdownTransfer.tsx文件,引入react核心方法,引入ui组件库ant design的一些组件,引入markedhighlight.jsmarked-highlight,引入刚才编写好的markdown.css文件。

import { useState } from 'react';
import { Col, Row, Card, Button, Input, Modal, Space, message } from 'antd';
import { markedHighlight } from "marked-highlight"
import 'highlight.js/styles/atom-one-dark.css'; // 引入合适的样式
import {Marked} from 'marked'; 
import hljs from 'highlight.js'
import './markdown.css'

思路分析:

  1. 预览markdown:基本的思路是左边的输入框改变时,拿到输入框内的值,用marked转换为html格式,动态修改右侧预览区域的html。
  2. 下载markdown:拿到左侧输入框的值,创建一个具有download属性的a标签,模拟点击下载。
  3. 下载html:和上面类似,只不过下载的字符串变为转化后的html字符串。注意下载时要拼接上markdown.css中的css,才能正确显示样式。

代码实现

  • 由于没有复杂的交互,这次没有划分子组件,直接在一个组件里完成了所有的功能。下面是组件中的完整代码。配合同级目录下的在前文创建的markdown.css,即可实现所有功能。可以无痛移植到其他现有的react项目中。
import { useState } from 'react';
import { Col, Row, Card, Button, Input, Modal, Space, message } from 'antd';
import { markedHighlight } from "marked-highlight"
import 'highlight.js/styles/atom-one-dark.css'; // 引入合适的样式
import {Marked} from 'marked'; 
import hljs from 'highlight.js'
import './markdown.css'

let cssStr = ''
import('./markdown.css').then((res) => {
  console.log('cssStr',res)
  cssStr = res.default
})

const { TextArea } = Input;
const marked=new Marked(
  markedHighlight({
    langPrefix: 'hljs language-',
    highlight(code, lang) {
      const language = hljs.getLanguage(lang) ? lang : 'shell'
      return hljs.highlight(code, { language}).value
    }
  })
)
const transfer = function () {
  let cardStyle = {
    width: 'auto',
    height: '100%',
  }
  let resultStyle = {
    width: 'auto',
    height: '550px',
    overflowY: 'scroll',
    backgroundColor: 'rgb(255,255,255,0.5)',
    borderRadius: '6px',
  }
  let [markdown, setMarkdown] = useState('')
  let [myHtml, setMyHtml] = useState('')
  let [isMarkdownOpen, setIsMarkdownOpen] = useState(false)
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [messageApi, contextHolder] = message.useMessage();
  /**
   * 文本域变化回调,动态修改预览区html
   * @param e 
   */
  const handleChange = (e: any) => {
    let md = e.target.value
    setMarkdown(md)
    let htmlStr = marked.parse(md) as string
    setMyHtml(htmlStr)
  }
  const preview = () => {
    setIsModalOpen(true)
  }
  const handleCancel = () => {
    setIsModalOpen(false)
  }
  const showMarkdown = () => {
    setIsMarkdownOpen(true)
  }
  /**
   * @param name 下载的文件名
   * @param text 下载的文本字符串
   * @param type 下载的类型
   * @returns void
   */
  const downText = (text: string, type: string = 'text/plain', name: string = 'down.txt') => {
    if (!text) {
      messageApi.warning('内容为空,无法下载')
      return
    }
    const link = document.createElement('a')
    link.download = name
    let href = URL.createObjectURL(new Blob([text], { type }))
    link.href = href
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
  }
  const downMarkdown = () => {
    downText(markdown, 'text/plain', 'markdown.md')
  }
  const downHtml = () => {
    let wholeHtml = myHtml + `<style>${cssStr}</style>`//完整的html需要加上css部分。
    console.log('wholeHtml', wholeHtml)
    downText(wholeHtml, 'text/html', 'myHtml.html')
  }
  return (
    <>
      <Row gutter={6}>
        {contextHolder}
        <Col span={12}>
          <Card title="markdown编写" extra={
            <Space>
              <Button onClick={showMarkdown} type="primary">查看markdown</Button>
              <Button onClick={preview} type="primary">预览Html</Button>
            </Space>
          }
            style={cardStyle}>
            <TextArea rows={23} placeholder="最多输入9999字" maxLength={9999}
              onChange={handleChange} />
          </Card>
        </Col>
        <Col span={12}>
          <Card title="markdown预览" extra={<a href="#">More</a>} style={cardStyle}>
            <div id="resultContainer" style={resultStyle}>
              <div dangerouslySetInnerHTML={{ __html: myHtml }} ></div>
            </div>
          </Card>
        </Col>
      </Row>
      <Modal title="HTML预览" width={1000} bodyStyle={{ height: '500px', overflowY: 'scroll' }} open={isModalOpen}
        footer={<Button type="primary" onClick={downHtml}>下载Html至本地</Button>} onCancel={handleCancel}>
        <div dangerouslySetInnerHTML={{ __html: myHtml }} ></div>
      </Modal>
      <Modal title="markdown预览" width={1000}  open={isMarkdownOpen}
        footer={<Button type="primary" onClick={downMarkdown}>下载Markdown至本地</Button>} onCancel={markdown => setIsMarkdownOpen(false)}>
        <div>
        <TextArea rows={28} placeholder="最多输入9999字" maxLength={9999} disabled value={markdown}/>
        </div>
      </Modal>
    </>

  )
}
export default transfer