前言
最近,在研究一个如何将我们git项目的MARKDOWN文档获取到,并且可以展示到界面通过检索查到,于是经过几天的摸索,成功的研究了出来
本次前端vue使用的是Markdown-it
Markdown-it 是一个用于解析和渲染 Markdown 标记语言的 JavaScript 库。
它采用模块化的设计,提供了灵活的配置选项和丰富的插件系统,使开发者可以根据自己的需要定制 Markdown 的解析和渲染过程。
使用 Markdown-it,你可以将 Markdown 文本解析为 HTML 输出,并且可以根据需要添加功能、扩展语法或修改解析行为
后端springboot使用JGit
JGit 是一个开源的 Java 实现的 Git 客户端库,它允许开发者在 Java 程序中直接操作 Git 仓库。
JGit 提供了一些核心的 API,使开发者可以使用 Java 代码来访问和操作 Git 仓库,例如创建仓库、提交变更、分支管理、标签管理、克隆远程仓库等。它提供了对 Git 分布式版本控制系统的完整支持,能够满足日常的代码版本管理需求。
但是我们这里单纯只是将其获取git的文件进行展示markdown,因此并用不上
准备工作
前端
在前端,
我使用了element-ui
前端框架写页面
使用Markdown-it
进行解析markdown
使用axios
连接了前后端
因此,需要安装如上依赖,指令如下:
npm i element-ui
npm i markdown-it
npm i axios
后端
因后端为springboot项目,需要安装springboot的依赖,这里不多赘述,主要是需要安装JGit
的依赖
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>5.9.0.202009080501-r</version>
</dependency>
效果演示
那么,在说如何做之前,先介绍一下我做出来的效果吧
首先,我建立了一个
git仓库
,专门用于获取到markdown文件
为了程序能够获取到文件,我在data文件夹下放了四个markdown文件,并且在程序指定只获取data下的markdown文件
- 无数据时,前端界面显示如下
当什么关键字都不输入,检索全部markdown文件
此时展示文件
此时随便点击列表的一个文件查看
切换另一个
以上,我们能够发现,它能够把我们的markdown的
表格
,图片
以及表情
正确的显示出来,并且样式排版也过得去,当然,这个是可以自己调整的
多提一句,我有做一个简单的
检索关键字的逻辑
,逻辑如下:
- 什么关键字不输入的时候,检索指定文件夹下所有markdown
- 当输入关键字,检索文件名,如果包含关键字,则把文件路径加入集合列表
- 当文件名不包含关键字,判断文件内容是否包含关键字,包含也把对应文件路径加入列表
前端代码逻辑
界面部分
<template>
<div>
<el-page-header content="MarkDown展示"/>
<el-input v-model="searchKey" placeholder="检索问题" style="position: relative;;width: 70%;left: 0%"></el-input>
<el-button @click="searchProblem" type="primary" plain style="margin: 10px;">检索</el-button>
<el-card>
<el-table :data="searchData" style="width: 100%;font-size: 20px; max-height: 500px; overflow-y: auto;background-color: white;">
<el-table-column type="index" label="序号">
</el-table-column>
<el-table-column label="列表项">
<template slot-scope="scope">
<span>{{ changePathName(scope.row )}}</span>
</template>
</el-table-column>
<el-table-column label="操作">
<template slot-scope="scope">
<div>
<el-button type="primary" size="medium" @click="findMarkDown(scope.row)" style="font-size: 24px;">查看</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 展示区 -->
<el-card style="position: relative;width: 100%;overflow-x: auto;" header="MarkDown处理文档">
<div style="position: relative;">
<el-card style="background-color:rgb(255, 253, 245);padding: 32px;" v-if="htmlContent">
<div v-html="htmlContent" class="v-md-header"></div>
</el-card>
<el-card style="background-color:rgb(255, 253, 245);padding: 32px;" v-else>
<div style="position: absolute;left:46.5%;text-align: center;line-height: 0px;font-weight: bold;">请检索并查看文档</div>
</el-card>
</div>
</el-card>
</div>
</template>
JavaScript逻辑
<script>
// 封装的axios调用后端的方法,如需要则按照自己的项目调用修改即可
import {searchProblem,findMarkDownBypath} from "../ajax/api"
import MarkdownIt from 'markdown-it';
export default {
name: "MarkDown",
data() {
return {
searchKey: "",
searchData: [], // 检索到的问题列表
markdownText: '', // 加载好图片的Markdown文本
markdownRenderer: null, // 解析markdown渲染器定义
htmlContent: '', // 解析为html
}
},
mounted() {
this.markdownRenderer = new MarkdownIt();
},
methods: {
// 检索文件
searchProblem() {
searchProblem(this.searchKey).then(res => {
console.log("检索数据:",res);
this.searchData = res.data.data; // 赋值检索数据,注意这里的res.data.data请根据自己实际回参更改获取参数
this.markdownText = ""; // 每次检索清空markdown显示文档内容
this.htmlContent = ""; // 每次检索清空markdown显示文档内容
})
},
// 根据文件路径查找markdown文件
findMarkDown(path) {
console.log("path:",path);
findMarkDownBypath(path).then(res => {
console.log("markdown内容:",res);
this.markdownText = res.data.data;
this.htmlContent = this.markdownRenderer.render(this.markdownText);
console.log(this.htmlContent);
})
},
// 处理字符串,回传的参数实际为:data/学生成绩系统前端.md,将字符串进行截取
changePathName(str) {
if (str) {
var lastIndex = str.lastIndexOf('/');
var result = str.substring(lastIndex + 1);
return result.replace('.md','');
}
return str;
}
}
}
</script>
在以上,后端传递的路径实际为:
[
"data/README.en.md",
"data/README.md",
"data/学生成绩系统前端.md",
"data/网上购药商城.md"
]
因此为了美观和直观展示,我是有做字符处理的,详情参考如何代码
此外,我后端获取到的markdown的内容实际数据为:
# search_markdown_data
#### Description
用于检索markdown的数据来源
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
我的代码中,使用
markdown-it
创建渲染器,将如上数据转换为:
<h1>search_markdown_data</h1>
<h4>Description</h4>
<p>用于检索markdown的数据来源</p>
<h4>Software Architecture</h4>
<p>Software architecture description</p>
<h4>Installation</h4>
<ol>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
</ol>
<h4>Instructions</h4>
<ol>
<li>xxxx</li>
<li>xxxx</li>
<li>xxxx</li>
</ol>
<h4>Contribution</h4>
<ol>
<li>Fork the repository</li>
<li>Create Feat_xxx branch</li>
<li>Commit your code</li>
<li>Create Pull Request</li>
</ol>
<h4>Gitee Feature</h4>
<ol>
<li>You can use Readme_XXX.md to support different languages, such as Readme_en.md, Readme_zh.md</li>
<li>Gitee blog <a href="https://blog.gitee.com">blog.gitee.com</a></li>
<li>Explore open source project <a href="https://gitee.com/explore">https://gitee.com/explore</a></li>
<li>The most valuable open source project <a href="https://gitee.com/gvp">GVP</a></li>
<li>The manual of Gitee <a href="https://gitee.com/help">https://gitee.com/help</a></li>
<li>The most popular members <a href="https://gitee.com/gitee-stars/">https://gitee.com/gitee-stars/</a></li>
</ol>
实际为html的数据,因此,我们就可以在界面使用vue的
v-html
展示markdown的内容
css样式
以上我们知道它会将数据转为html的数据,因此,就可以使用css样式调整,以下为我的css样式,供参考:
h1 {
color: #ff0000;
}
p {
font-size: 16px;
line-height: 1.5;
}
.v-md-header {
text-align: left !important;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid black;
padding: 8px;
}
th {
background-color: #f2f2f2; /* 设置表头的背景颜色 */
}
tr:nth-child(even) {
background-color: #dddddd; /* 设置偶数行的背景颜色 */
}
tr:hover {
background-color: #f5f5f5; /* 设置鼠标悬停时的背景颜色 */
}
h1,h2,h3,h4,h5{
border-bottom: 1px #d8d6d6 solid;
}
img{
width: 80%;
}
后端代码逻辑
先说下我的查询的方法,JGIT
我尝试了很久,都只能通过先克隆到本地
,再读取的方式,于是放弃了研究如何使用JGIT在线读取文件的方式,也许后面我可能研究的出来。
同样,为了保证每次都是最新的文档,我采用了判断是否已经克隆下来了,如果克隆了则更新代码,没有克隆则克隆,来保证每次都是最新代码
在正式执行前,我们需要先定义好需要的参数
// 解析markdown图片正则
private static final String MARKDOWN_IMAGE_PATTERN = "(!\\[[^\\]]*\\])\\(([^\\)]+)\\)";
// 需要克隆git到本机的路径
private final String LOCAL_PATH = "E:\\git\\markdown";
// 需要获取markdown的git链接
private final String GIT_PATH = "https://gitee.com/spring-in-huangxian-county/search_markdown_data.git";
// 需要获取的git分支
private final String GIT_BRANCH = "master";
// 需要抓取的Git内指定文件夹的markdown
private final String MARK_DOWN_PATH = "data";
// 当前后端项目的位置,该目的是为了能够找到正确的文件路径
private final String PROJECT_PATH = "F:\\gitee\\search_markdown_end";
查询
controller层
@GetMapping("/searchProblem")
public ResultVO<List<String>> searchMarkdown(@RequestParam("searchKey") String searchKey)
throws Exception {
// 获取Git仓库中的Markdown文档列表
try {
List<String> markdownFiles = new MarkDownService().getGitDataFilePath();
List<String> results = new ArrayList<>();
if (StringUtils.isEmpty(searchKey)) {
results.addAll(markdownFiles);
} else {
for (String path:markdownFiles) {
// 如果标题包含检索关键字加入列表
if (path.contains(searchKey)) {
results.add(path);
} else {
// 判断具体内容是否包含关键字,是则加入列表
if (new MarkDownService().isContainSearchKeyForContent(searchKey,path)) {
results.add(path);
}
}
}
}
return new ResultVO<>(0,"OK",results);
}catch (Exception e) {
return new ResultVO<>(1,e.getMessage());
}
}
ResultVO为封装的响应体,如有兴趣可参考我之前文章
MarkDownService为service层文件名
service层
克隆和拉取git的方式读取文件,获取文件路径
// 克隆和拉取git的方式读取文件
public List<String> getGitDataFilePath() throws Exception {
File localPath = new File(LOCAL_PATH);
String remoteUrl = GIT_PATH;
String branchName =GIT_BRANCH; // 或者其他分支名称
String folderPath = MARK_DOWN_PATH; // data文件夹的路径
List<String> markDownFilePathList = new ArrayList<>();
Repository repository;
if (localPath.exists()) {
repository = openLocalRepository(localPath);
pullLatestChanges(repository);
} else {
repository = cloneRepository(localPath, remoteUrl);
}
try (Git git = new Git(repository)) {
Iterable<RevCommit> commits = git.log().add(repository.resolve(branchName)).call();
RevCommit commit = commits.iterator().next();
try (RevWalk revWalk = new RevWalk(repository)) {
RevTree tree = revWalk.parseTree(commit.getTree());
try (TreeWalk treeWalk = new TreeWalk(repository)) {
treeWalk.addTree(tree);
treeWalk.setRecursive(true);
while (treeWalk.next()) {
if (treeWalk.getPathString().startsWith(folderPath) && treeWalk.getPathString().endsWith(".md")) {
System.out.println("Found markdown file: " + treeWalk.getPathString());
// 这里可以根据需要进行具体的处理,比如读取文件内容等
markDownFilePathList.add(treeWalk.getPathString());
}
}
}
}
} catch (IOException | GitAPIException e) {
e.printStackTrace();
}
return markDownFilePathList;
}
打开本地git
// 打开本地git项目
private Repository openLocalRepository(File localPath) throws IOException {
System.out.println("Opening existing repository...");
Git git = Git.open(localPath);
return git.getRepository();
}
克隆代码
// 克隆git
private Repository cloneRepository(File localPath, String remoteUrl) throws GitAPIException {
System.out.println("Cloning repository...");
Git git = Git.cloneRepository()
.setURI(remoteUrl)
.setDirectory(localPath)
.call();
return git.getRepository();
}
拉取最新代码
//拉取git最新代码
private void pullLatestChanges(Repository repository) throws GitAPIException {
System.out.println("Pulling latest changes...");
Git git = new Git(repository);
PullCommand pull = git.pull().setTimeout(30);
pull.call();
}
检查文件内容是否包含关键字
/**
* @param searchKey 检索关键字
* @param path markdown文本路径
* @desc 通过关键字和路径找到指定markdown文件是否内容包含关键字
* */
public Boolean isContainSearchKeyForContent(String searchKey,String path) {
Boolean containFlag = false;
String content ="";
try {
content = findMarkDownBypathNoWithImage(path);
}catch (Exception e) {
System.out.println("获取markdown文本失败:"+e.getMessage());
}
if (content.contains(searchKey)) {
containFlag = true;
}
return containFlag;
}
要判断文件内容是否包含关键字,不需要将文件图片进行解析,直接获取文件内容
public String findMarkDownBypathNoWithImage(String filePath) throws Exception{
String localPath = LOCAL_PATH;
String markDownContent = "";
if (filePath.endsWith(".md")) {
File markdownFile = new File(localPath, filePath);
try (Scanner scanner = new Scanner(markdownFile)) {
StringBuilder contentBuilder = new StringBuilder();
while (scanner.hasNextLine()) {
contentBuilder.append(scanner.nextLine()).append("\n");
}
markDownContent = contentBuilder.toString();
System.out.println("Markdown file content:\n" + markDownContent);
} catch (IOException e) {
throw new Exception(e.getMessage());
}
}
return markDownContent;
}
根据路径获取文件
以下为会解析图片的方式进行获取文件
controller层
@GetMapping("findMarkDownBypath")
public ResultVO<String> findMarkDownBypath(@RequestParam("path")String path) throws Exception {
try {
return new ResultVO<>(new MarkDownService().findMarkDownBypathWithImage(path));
}catch (Exception e) {
return new ResultVO<>(1,e.getMessage());
}
}
service层
public String findMarkDownBypathWithImage(String filePath) throws Exception{
String localPath = LOCAL_PATH;
String markDownContent = "";
if (filePath.endsWith(".md")) {
File markdownFile = new File(localPath, filePath);
try (Scanner scanner = new Scanner(markdownFile)) {
StringBuilder contentBuilder = new StringBuilder();
while (scanner.hasNextLine()) {
contentBuilder.append(scanner.nextLine()).append("\n");
}
String markdownContent = contentBuilder.toString();
markDownContent = loadImages(markdownContent,filePath);
// 在这里得到了具体的markdown文件内容
System.out.println("Markdown file content:\n" + markdownContent);
} catch (IOException e) {
throw new Exception(e.getMessage());
}
}
return markDownContent;
}
解析图片
public String loadImages(String markdownContent, String markdownFilePath) {
Pattern pattern = Pattern.compile(MARKDOWN_IMAGE_PATTERN);
Matcher matcher = pattern.matcher(markdownContent);
String localPath = LOCAL_PATH;
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String originalImageTag = matcher.group(0);
String altText = matcher.group(1);
String imagePath = matcher.group(2);
try {
String absoluteImagePath = getAbsoluteImagePath(imagePath, markdownFilePath);
absoluteImagePath = absoluteImagePath.replace(PROJECT_PATH,localPath);
String imageData = loadImage(absoluteImagePath);
String transformedImageTag = "![Image](" + imageData + ")";
matcher.appendReplacement(sb, transformedImageTag);
} catch (IOException e) {
// 图像加载出错,可以根据实际需求进行处理
e.printStackTrace();
}
}
matcher.appendTail(sb);
return sb.toString();
}
public static String loadImage(String imagePath) throws IOException {
File imageFile = new File(imagePath);
// 读取图像文件的字节数组
byte[] imageData = FileUtils.readFileToByteArray(imageFile);
// 将字节数组转换为Base64编码字符串
String base64ImageData = java.util.Base64.getEncoder().encodeToString(imageData);
return "data:image/png;base64," + base64ImageData;
}
public static String getAbsoluteImagePath(String imagePath, String markdownFilePath) {
File markdownFile = new File(markdownFilePath);
String markdownDirectory = markdownFile.getParent();
String absoluteImagePath = new File(markdownDirectory, imagePath).getAbsolutePath();
return absoluteImagePath;
}
依赖
为了防止出现依赖可能缺失的情况,可参考我的项目的maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.hxc.common</groupId>
<artifactId>CommonBack</artifactId>
<version>1.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<log4j2.version>2.17.0</log4j2.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.8</version>
</dependency>
<!-- swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<!-- swagger的ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.15.0</version>
</dependency>
<!-- 文件处理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!-- POI excel处理依赖 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.8</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.eclipse.jgit</groupId>-->
<!-- <artifactId>org.eclipse.jgit</artifactId>-->
<!-- <version>4.4.1.201607150455-r</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>5.9.0.202009080501-r</version>
</dependency>
</dependencies>
<build>
<finalName>common_end</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.hxc.common.MarkDownApplication</mainClass>
<classpathPrefix>libs/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!--跳过junit-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
git
以下为我实现读取git的markdown的项目,可供参考
前端
后端
结语
以上为我实现vue+springboot读取git的markdown文件并展示的过程