23 Commits

Author SHA1 Message Date
jaywcjlove
d806339c26 released v2.1.0 2022-09-03 15:08:45 +08:00
jaywcjlove
6a2279d7b2 feat: add theme url parameter. 2022-09-03 15:07:58 +08:00
jaywcjlove
ba0b158a47 doc: Update README.md 2022-09-03 13:40:40 +08:00
jaywcjlove
3bf313ffc3 style: modify theme style. 2022-09-03 13:33:25 +08:00
jaywcjlove
f245c33973 feat: add base theme. 2022-09-03 12:50:18 +08:00
jaywcjlove
1c7167bcf9 feat: add theme editor. 2022-09-03 12:19:13 +08:00
jaywcjlove
0580011f7b feat: add preview themes. 2022-09-03 01:11:22 +08:00
jaywcjlove
d007e11f20 chore: format code & add format tools. 2022-09-02 22:45:28 +08:00
jaywcjlove
6da12fcb10 style: modify select style. 2022-09-02 21:41:09 +08:00
jaywcjlove
e0fed4783a style: modify image style. 2022-09-02 17:36:05 +08:00
jaywcjlove
e9b3b60f6d feat: add editor theme switch. 2022-09-02 17:06:30 +08:00
jaywcjlove
cbaad4e0fc style: update footnotes style. 2022-09-02 11:58:45 +08:00
jaywcjlove
3abeaf355c feat: support custom style & ignore content syntax. 2022-09-02 11:44:25 +08:00
jaywcjlove
709849e048 feat: support docker deplay. 2022-09-02 01:46:24 +08:00
jaywcjlove
9b08674a5a released v2.0.0 2022-09-02 01:34:27 +08:00
jaywcjlove
61649a29da chore: update renovate.json 2022-09-02 01:32:48 +08:00
jaywcjlove
92db8dd11a style: update preview styles. 2022-09-02 01:31:20 +08:00
jaywcjlove
3d11eb8479 doc: Update README.md 2022-09-02 01:26:59 +08:00
jaywcjlove
cce03c115b chore: udpate workflows config. 2022-09-02 01:26:25 +08:00
jaywcjlove
bebe6a549c doc: Update README.md 2022-09-02 01:26:25 +08:00
小弟调调™
cd23add2ce Create LICENSE 2022-09-02 01:19:05 +08:00
renovate[bot]
79f9d0d9b4 chore(deps): add renovate.json (#1) 2022-09-02 01:18:26 +08:00
jaywcjlove
d0364a0bee feat: init web app. 2022-09-02 01:16:22 +08:00
44 changed files with 2261 additions and 130 deletions

99
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,99 @@
name: CI
on:
push:
branches:
- master
jobs:
build-deploy:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
- run: npm run build
- name: Generate Contributors Images
uses: jaywcjlove/github-action-contributors@main
with:
filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\])
output: build/CONTRIBUTORS.svg
avatarSize: 42
- name: Create Tag
id: create_tag
uses: jaywcjlove/create-tag-action@main
with:
package-path: ./package.json
- name: get tag version
id: tag_version
uses: jaywcjlove/changelog-generator@main
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
commit_message: ${{ github.event.head_commit.message }} ${{steps.tag_version.outputs.tag}}
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
- name: Generate Changelog
id: changelog
uses: jaywcjlove/changelog-generator@main
with:
head-ref: ${{steps.create_tag.outputs.version}}
filter-author: (renovate-bot|Renovate Bot)
filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}'
- name: Create Release
uses: ncipollo/release-action@v1
if: steps.create_tag.outputs.successful
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.create_tag.outputs.version }}
tag: ${{ steps.create_tag.outputs.version }}
body: |
Documentation ${{ steps.changelog.outputs.tag }}: https://raw.githack.com/jaywcjlove/wxmp/${{ steps.changelog.outputs.gh-pages-short-hash }}/index.html
Comparing Changes: ${{ steps.changelog.outputs.compareurl }}
${{ steps.changelog.outputs.changelog }}
# Create Docker Image
- name: Docker login
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
- name: Build Awesome Mac image
run: docker image build -t wxmp .
- name: Tags & Push image (latest)
run: |
echo "outputs.tag - ${{ steps.changelog.outputs.version }}"
docker tag wxmp ${{ secrets.DOCKER_USER }}/wxmp:latest
docker push ${{ secrets.DOCKER_USER }}/wxmp:latest
- name: Tags & Push image
if: steps.create_tag.outputs.successful
run: |
echo "outputs.tag - ${{ steps.changelog.outputs.version }}"
docker tag wxmp ${{ secrets.DOCKER_USER }}/wxmp:${{steps.changelog.outputs.version}}
docker push ${{ secrets.DOCKER_USER }}/wxmp:${{steps.changelog.outputs.version}}
# Create Docker Image in GitHub
- name: Login to GitHub registry
run: echo ${{ github.token }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build docker image
run: docker build -t ghcr.io/jaywcjlove/wxmp:latest .
- name: Publish to GitHub registry
run: docker push ghcr.io/jaywcjlove/wxmp:latest
- name: Tag docker image (beta) and publish to GitHub registry
if: steps.create_tag.outputs.successful
run: |
echo "version: v${{ steps.changelog.outputs.version }}"
docker tag ghcr.io/jaywcjlove/wxmp:latest ghcr.io/jaywcjlove/wxmp:${{steps.changelog.outputs.version}}
docker push ghcr.io/jaywcjlove/wxmp:${{steps.changelog.outputs.version}}

22
.gitignore vendored
View File

@@ -1,11 +1,31 @@
dist
build
lib
cjs
esm
node_modules
npm-debug.log*
lerna-debug.log
yarn-error.log
package-lock.json
.DS_Store
.cache
.vscode
.idea
.env
*.mpassword
*.bak
*.tem
*.temp
#.swp
*.*~
~*.*
*.crx
# IDEA
*.iml
*.ipr
*.iws
.idea/

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx pretty-quick --staged

50
.kktrc.ts Normal file
View File

@@ -0,0 +1,50 @@
import path from 'path';
import webpack, { Configuration } from 'webpack';
import lessModules from '@kkt/less-modules';
import { mdCodeModulesLoader } from 'markdown-react-code-preview-loader';
import scopePluginOptions from '@kkt/scope-plugin-options';
import { LoaderConfOptions } from 'kkt';
import raw from '@kkt/raw-modules';
import pkg from './package.json';
export default (conf: Configuration, env: 'development' | 'production', options: LoaderConfOptions) => {
conf = lessModules(conf, env, options);
conf = mdCodeModulesLoader(conf);
conf = raw(conf, env, {
...options,
test: /\.(md.css)$/i,
});
conf = scopePluginOptions(conf, env, {
...options,
allowedFiles: [path.resolve(process.cwd(), 'README.md'), path.resolve(process.cwd(), 'src')],
});
conf.plugins!.push(
new webpack.DefinePlugin({
VERSION: JSON.stringify(pkg.version),
}),
);
conf.module!.exprContextCritical = false;
if (env === 'production') {
conf.output = { ...conf.output, publicPath: './' };
conf.optimization = {
...conf.optimization,
splitChunks: {
cacheGroups: {
reactvendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
chunks: 'all',
},
refractor: {
test: /[\\/]node_modules[\\/](refractor)[\\/]/,
name: 'refractor-prismjs-vendor',
chunks: 'all',
},
},
},
};
}
return conf;
};

13
.prettierignore Normal file
View File

@@ -0,0 +1,13 @@
**/*.md
**/*.svg
**/*.ejs
**/*.html
**/*.yml
package.json
node_modules
dist
build
coverage
lib
esm
test

11
.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}

7
Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
# https://lipanski.com/posts/smallest-docker-image-static-website
# https://github.com/lipanski/docker-static-website
FROM lipanski/docker-static-website:latest
# Copy the static website
# Use the .dockerignore file to control what ends up inside the image!
COPY ./build .

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 小弟调调™
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

205
README.md
View File

@@ -1,41 +1,192 @@
# Wxmp
<div align="center">
<h1 align="center">微信公众号 Markdown 编辑器</h1>
</div>
chrome 小插件,优化在微信公众账号中发文章,因复制粘贴带过去的`font-family` CSS 样式导致被微信过滤样式全无。使用此插件删除提交文章上所有HTML节点上的`font-family`,让复制过去的样式保持一致。
![微信公众号 Markdown 编辑器](https://user-images.githubusercontent.com/1680273/188257498-529e42aa-44b0-441f-bcdd-049efa174b78.png)
目前删除这些标签上的`font-family`样式
微信公众号文章 Markdown 在线编辑器,使用 markdown 语法创建一篇简介美观大方的微信公众号图文。由于发版本麻烦,和一些功能无法扩展停滞开发了,未来不再开发 Chrome 的插件(暂存在 chrome 分支),通过 web 版本定制更丰富的功能。
> `code`,`pre`,`h1`,`h2`,`h3`,`h4`,`h5`,`h6`,`p`,`div`,`span`
## 功能特性
![界面预览](https://raw.githubusercontent.com/jaywcjlove/wxmp/master/wxmq.png)
开发计划和一些功能介绍,有需求可以在 issue 中提,使得工具变得更加完善。下面示例用于 web 应用中效果展示。
- [x] 支持 Markdown 所有基础语法
- [x] 支持自定义 CSS 样式
- [x] 支持主题选择 & 编辑预览。
- [x] 支持明暗两种主题预览。
- [ ] 支持色盘取色,快速替换文章整体色调
- [ ] 支持 URL 参数加载 Markdown 内容。
- [x] 支持 URL 参数选择预览主题。
### 支持代码块样式
下面是 `jsx` 代码块展示示例,并高亮代码,用于 web 应用中效果展示。
```jsx
function Demo() {
return <div className="demo">Hello World!</div>
}
```
下面是 `css` 代码块展示示例,并高亮代码,用于 web 应用中效果展示。
```css
li {
font-size: 16px;
margin: 0;
line-height: 26px;
color: rgb(30 41 59);
font-family:-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif;
}
```
### 支持内联代码
> 用于 web 应用中效果展示。
Inline Code `{code: 0}`
### 支持表格
表格无法使用自定义样式,暂时没找到解决途径
| Header 1 | Header 2 |
| --- | --- |
| Key 1 | Value 1 |
| Key 2 | Value 2 |
| Key 3 | Value 3 |
### 支持 GFM 脚注
这是一个简单的 Markdown[^1] 语法的脚注[^2]。 页面最后有一些额外的文字描述。注意这不是完整的注脚[^3]特性。
[^1]: GitHub 风格的 Markdown 规范 https://github.github.com/gfm/
[^2]: 脚注 https://github.blog/changelog/2021-09-30-footnotes-now-supported-in-markdown-fields/
[^3]: 微信文章不支持锚点跳转和打开第三方 URL 超链接,所以不支持完整的注脚特性。
### 支持注释
<ruby>
汉 <rp></rp><rt>Han</rt><rp></rp>
字 <rp></rp><rt>zi</rt><rp></rp>
拼 <rp></rp><rt>pin</rt><rp></rp>
音 <rp></rp><rt>yin</rt><rp></rp>
注 <rp></rp><rt>zhu</rt><rp></rp>
音 <rp></rp><rt>yin</rt><rp></rp>
</ruby>
### 支持自定义样式
<!--rehype:style=color: red;-->
在 Markdown 中 HTML 注释也可以用在 markdown 中,利用这一特点,为一些内容自定一样式。使用 HTML 注释 `<!--rehype:xxx-->`<!--rehype:style=color: red;background: #ff000033;--> 让 Markdown 支持样式自定义。
```markdown
## 定义标题样式
<!--rehype:style=display: flex; height: 230px; align-items: center; justify-content: center; font-size: 38px;-->
支持对某些文字变更样式如_文字颜色_<!--rehype:style=color: red;-->,文字颜色将被设置为红色(red)。
```
⚠️ 注意:这一特性可能适用于有一定 css 前端基础知识的用户,不过它也非常简单,使用 `<!--rehype:style=` 开始,`-->` 结束,中间包裹 css 样式,如 `color: red;` 设置文字红色。
## 已经实现功能
### 标记忽略内容
- [x] 过滤 `font-family`
- [x] 代码高亮区域有背景颜色;
- [x] 代码高亮区域有横向滚动条强制不换行;
- [x] 增加iOS滚动滚动弹性
- [ ] 添加设置标题工具;
- [ ] 添加字段高亮工具;
- [ ] 添加删除线工具,如:<del>删除线</del>
此特性利用 HTML 注释在 markdown 中被忽略的特性,标记需要忽略的内容,标记开始 `<!--rehype:ignore:start-->`,标记结束 `<!--rehype:ignore:end-->`,被标记的内容在微信 Markdown 编辑器预览中不显示。在其它预览工具中展示内容,比如 GitHub 中能展示。
# 直接安装
```markdown
# 注释忽略
1. 下载扩展程序[Wxmp.crx](https://github.com/jaywcjlove/wxmp/releases) 文件
2. 在chrome里面器地址输入`chrome://extensions/` 打开插件界面
3.`Wxmp.crx`文件拖入chrome浏览器的扩展程序列表中
<!--rehype:ignore:start-->内容在微信 Markdown 编辑器预览中不显示。在其它预览工具中展示内容。<!--rehype:ignore:end-->
```
# 开发模式插件安装
## 主题定制
1. 下载文件压缩包解压
2. 在chrome里面器地址输入`chrome://extensions/` 打开插件界面
3. 点击`加载已解压的扩展程序...`
4. 选择插件所在的目录
在目录 `src/themes` 中存放默认主题,在 `src/store/context.tsx` 中配置主题,主题使用 css 定义样式,不支持复杂的选择器。提供在线主题编辑器,欢迎修改并 PR 进仓库供大家使用。
```css
/* 1~6 标题样式定义 */
h1 {} h2 {} h3 {} h4 {} h5 {} h6 {}
a { color: red; } /* 超链接样式定义 */
strong {} /* 加粗样式定义 */
del {} /* 删除线样式定义 */
em {} /* 下划线样式定义 */
u {} /* 下划线样式定义 */
p {} /* 段落样式定义 */
ul {} /* 无序列表样式定义 */
ol {} /* 有序列表样式定义 */
li {} /* 列表条目样式定义 */
blockquote {} /* 块级引用样式定义 */
table {}
td {}
th {}
pre {} /* 样式定义 */
.code-highlight {} /* 代码块样式定义 */
.code-line {} /* 代码块行样式定义 */
.code-spans {} /* 代码块行样式定义 */
sup {} /* GFM 脚注样式定义 */
.footnotes-title {} /* GFM 脚注,参考标题样式定义 */
.footnotes-list {} /* GFM 脚注,参考列表样式定义 */
.image-warpper {} /* 图片父节点样式定义 */
.image {} /* 图片样式定义 */
/* 部分代码高亮样式 */
.comment {}
.property {}
.function {}
.keyword {}
.punctuation {}
.unit {}
.tag {}
.color {}
.selector {}
.quote {}
.number {}
.attr-name {}
.attr-value {}
```
## 部署
[![Docker Image Version (latest by date)](https://img.shields.io/docker/v/wcjiang/wxmp?logo=docker)](https://hub.docker.com/r/wcjiang/wxmp) [![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/wcjiang/wxmp?logo=docker)](https://hub.docker.com/r/wcjiang/wxmp) [![Docker Pulls](https://img.shields.io/docker/pulls/wcjiang/wxmp?logo=docker)](https://hub.docker.com/r/wcjiang/wxmp)
轻松通过 docker 部署《微信公众号 Markdown 编辑器》网站应用。
```bash
docker pull wcjiang/wxmp
# Or
docker pull ghcr.io/jaywcjlove/wxmp:latest
```
```bash
docker run --name wxmp --rm -d -p 96611:3000 wcjiang/wxmp:latest
# Or
docker run --name wxmp -itd -p 96611:3000 wcjiang/wxmp:latest
# Or
docker run --name wxmp -itd -p 96611:3000 ghcr.io/jaywcjlove/wxmp:latest
```
在浏览器中访问以下 URL
```
http://localhost:96611/
```
## 贡献者
一如既往,感谢我们出色的贡献者!
<a href="https://github.com/jaywcjlove/wxmp/graphs/contributors">
<img src="https://jaywcjlove.github.io/wxmp/CONTRIBUTORS.svg" />
</a>
上图贡献者列表,由 [action-contributors](https://github.com/jaywcjlove/github-action-contributors)[^4] 自动生成贡献者图片。
# 使用方法
[^4]: Action Contributors https://github.com/jaywcjlove/github-action-contributors
1. 打开微信公众平台,新建图文消息,复制文章到编辑器中
2. 在右上角点击微信图标
3. 点击弹出的模态框上的删除按钮
4. 如果成功会在按钮后面提示`更改成功!!`
## License
根据 MIT 许可证获得许可。

View File

@@ -1,7 +0,0 @@
ul,li{margin: 0;padding: 0;}
.warpper{width: 200px; min-height: 23px;}
.warpper ul {}
.warpper ul li{}
.warpper ul li a{display: block;box-shadow: 0 1px 1px 0 #D0D0D0;line-height: 23px;border:1px solid #D0D0D0;border-radius: 4px;padding: 0 5px;color:#333;}
.warpper ul li a:hover{box-shadow: 0 1px 1px 0 #4A90E2;border: 1px solid #4A90E2;color:#4A90E2;}
.warpper ul li a:active{box-shadow: 0 1px 1px 0 #ABABAB;border: 1px solid #ABABAB;color:#ABABAB;}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -1,37 +0,0 @@
chrome.extension.onRequest.addListener(
function (request, sender, sendResponse) {
if (request.hello == "btn_del_family") {
var bodys = document.querySelectorAll('iframe#ueditor_0')
if(bodys&&bodys.length>0&&bodys[0].contentDocument){
var body = bodys[0].contentDocument;
if(body){
var elms = body.querySelectorAll('pre code,pre,h1,h2,h3,h4,h5,h6,p,div,span');
for (var i = 0; i < elms.length; i++) {
var styl = elms[i].getAttribute('style');
if(elms[i].tagName === 'PRE'){
elms[i].setAttribute('style','box-sizing: border-box; overflow: auto;font-size: 0.93em; padding: 1em; margin-top: 1.5em; margin-bottom: 1.5em; line-height: 1.3; word-break: break-all; word-wrap: break-word; color: rgb(51, 51, 51); border: none; border-radius: 3px; max-height: 35em; position: relative;background-color:#EDEDED;word-wrap: initial!important;-webkit-overflow-scrolling: touch;')
}else if(elms[i].tagName === 'CODE'){
elms[i].setAttribute('style','box-sizing: border-box;font-size: 1em; color: inherit; border-radius: 0px; white-space: inherit; overflow-wrap: normal; background: none;word-wrap:normal!important;')
}else{
if(styl){
styl = styl.replace(/font-family\:[\s\S]*?\;/g,'');
if(elms[i].tagName === 'SPAN') styl += 'word-wrap:normal!important;';
elms[i].setAttribute('style',styl);
}
}
}
var elms = body.querySelectorAll('p code');
for (var i = 0; i < elms.length; i++) {
if(elms[i].tagName === 'CODE'){
elms[i].setAttribute('style','color: #c7254e;background-color: #f9f2f4;padding: 2px 4px;border-radius: 3px;')
}
}
}
}
sendResponse({ msg: '更改完毕!!' });
}
}
);

4
js/jq.3.0.0.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
$('.warpper a.btn_del_family').on('click',function(){
var self = this
chrome.tabs.getSelected(null, function (tab) {
chrome.tabs.sendRequest(tab.id, {"hello": "btn_del_family"}, function (response) {
$(self).find('span').html(response.msg);
setTimeout(function(){
$(self).find('span').html('');
}, 2000);
});
});
})
$('.warpper a.btn_del_fonts').on('click',function(){
console.log("btn_del_fonts");
chrome.tabs.getSelected(null, function (tab) {
chrome.tabs.sendRequest(tab.id, {"hello": "btn_del_fonts"}, function (response) {
console.log("response",response);
});
});
})

View File

@@ -1,16 +0,0 @@
{
"name": "Wxmp",
"version": "1.0",
"description": "微信公众账号发文章优化文章插件",
"icons": { "128": "img/icon.png" },
"permissions": ["*://*/*","declarativeContent","tabs", "unlimitedStorage"],
"browser_action": {
"default_title": "",
"default_icon": "img/icon.png",
"default_popup": "popup.html"
},
"manifest_version": 2,
"content_scripts": [{"matches": ["*://*/*"],"js": ["js/bg.js"]}],
"permissions": ["*://*/*","tabs"]
}

81
package.json Normal file
View File

@@ -0,0 +1,81 @@
{
"name": "website",
"version": "2.1.0",
"private": true,
"scripts": {
"start": "kkt start",
"build": "kkt build",
"prepare": "husky install",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"pretty-quick": "pretty-quick --staged"
},
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.9",
"@uiw/codemirror-theme-abcdef": "^4.11.6",
"@uiw/codemirror-theme-androidstudio": "^4.11.6",
"@uiw/codemirror-theme-atomone": "^4.11.6",
"@uiw/codemirror-theme-bbedit": "^4.11.6",
"@uiw/codemirror-theme-bespin": "^4.11.6",
"@uiw/codemirror-theme-darcula": "^4.11.6",
"@uiw/codemirror-theme-dracula": "^4.11.6",
"@uiw/codemirror-theme-duotone": "^4.11.6",
"@uiw/codemirror-theme-eclipse": "^4.11.6",
"@uiw/codemirror-theme-github": "^4.11.6",
"@uiw/codemirror-theme-okaidia": "^4.11.6",
"@uiw/codemirror-theme-sublime": "^4.11.6",
"@uiw/codemirror-theme-xcode": "^4.11.6",
"@uiw/react-back-to-top": "^1.2.0",
"@uiw/react-github-corners": "^1.5.15",
"@uiw/react-markdown-editor": "^5.6.0",
"@wcj/dark-mode": "^1.0.15",
"css-tree": "^2.2.1",
"react": "^18.2.0",
"react-code-preview-layout": "^2.0.4",
"react-dom": "^18.2.0",
"react-hot-toast": "^2.3.0",
"react-router-dom": "^6.3.0",
"rehype-attr": "^2.0.8",
"rehype-ignore": "^1.0.1",
"rehype-prism-plus": "^1.5.0",
"rehype-raw": "^6.1.1",
"rehype-stringify": "^9.0.3",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"styled-components": "^5.3.5",
"unified": "^10.1.2"
},
"devDependencies": {
"@kkt/less-modules": "^7.2.0",
"@kkt/raw-modules": "^7.2.0",
"@kkt/scope-plugin-options": "^7.2.0",
"@types/css-tree": "^1.0.7",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.25",
"husky": "^8.0.1",
"prettier": "^2.7.1",
"pretty-quick": "~3.1.3",
"kkt": "^7.2.0",
"markdown-react-code-preview-loader": "^2.1.2"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -1,18 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>弹出层</title>
<link rel="stylesheet" type="text/css" href="css/popup.css">
</head>
<body>
<div class="warpper">
<ul>
<li><a class="btn_del_family" href="#">优化文章样式<span></span></a></li>
<!-- <li><a class="btn_del_fonts" href="#">删除family样式</a></li> -->
</ul>
</div>
<script type="text/javascript" src="js/jq.3.0.0.min.js"></script>
<script type="text/javascript" src="js/popup.js"></script>
</body>
</html>

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

24
public/index.html Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<meta name="theme-color" content="#000000" />
<title>微信公众号 Markdown 编辑器</title>
<meta name="keywords" content="react,simple,monorepo,template,component,project,package,development" />
<meta name="description" content="Simple React package development project example template." />
<link rel="icon" href="%PUBLIC_URL%favicon.ico" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

10
renovate.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"packageRules": [
{
"matchPackagePatterns": ["*"],
"rangeStrategy": "replace"
}
]
}

15
src/App.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { Routes, Route } from 'react-router-dom';
import { Layout } from './components/Layout';
import { HomePage } from './pages/home';
import { EditorPage } from './pages/theme/editor';
export default function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<HomePage />} />
<Route path="/editor/theme" element={<EditorPage />} />
</Route>
</Routes>
);
}

7
src/assets/color.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g id="Layer_2">
<g id="color-palette">
<path d="M19.54 5.08A10.61 10.61 0 0 0 11.91 2a10 10 0 0 0-.05 20 2.58 2.58 0 0 0 2.53-1.89 2.52 2.52 0 0 0-.57-2.28.5.5 0 0 1 .37-.83h1.65A6.15 6.15 0 0 0 22 11.33a8.48 8.48 0 0 0-2.46-6.25Zm-12.7 9.66a1.5 1.5 0 1 1 .4-2.08 1.49 1.49 0 0 1-.4 2.08ZM8.3 9.25a1.5 1.5 0 1 1-.55-2 1.5 1.5 0 0 1 .55 2ZM11 7a1.5 1.5 0 1 1 1.5-1.5A1.5 1.5 0 0 1 11 7Zm5.75.8a1.5 1.5 0 1 1 .55-2 1.5 1.5 0 0 1-.55 2Z" style="fill:#231f20" id="color-palette-2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 576 B

3
src/assets/github.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 1024 1024" height="1em" width="1em">
<path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0 1 38.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 784 B

3
src/assets/logo.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1aad1a" viewBox="0 0 1024 1024" height="1em" width="1em">
<path d="M690.1 377.4c5.9 0 11.8.2 17.6.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6a21.5 21.5 0 0 1 9.1 17.6c0 2.4-.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-.1 17.8-.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8zm-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1zm-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1zm586.8 415.6c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7a9 9 0 0 0 6.4-2.6 9 9 0 0 0 2.6-6.4c0-2.2-.9-4.4-1.4-6.6-.3-1.2-7.6-28.3-12.2-45.3-.5-1.9-.9-3.8-.9-5.7.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9zm179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9a36.08 36.08 0 0 1-36 35.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

45
src/commands/copy.tsx Normal file
View File

@@ -0,0 +1,45 @@
import React from 'react';
import { ICommand, IMarkdownEditor, ToolBarProps } from '@uiw/react-markdown-editor';
import toast from 'react-hot-toast';
import styled from 'styled-components';
const Button = styled.button`
white-space: nowrap;
width: initial !important;
display: flex;
align-items: center;
padding: 0 0.4rem !important;
`;
const CopyView: React.FC<{ command: ICommand; editorProps: IMarkdownEditor & ToolBarProps }> = (props) => {
const { editorProps } = props;
const handleClick = () => {
const dom = editorProps.preview.current;
dom?.focus();
window.getSelection()?.removeAllRanges();
let range = document.createRange();
range.setStartBefore(dom?.firstChild!);
range.setEndAfter(dom?.lastChild!);
window.getSelection()?.addRange(range);
document.execCommand(`copy`);
window.getSelection()?.removeAllRanges();
toast.success(<div></div>);
};
return (
<Button type="button" onClick={handleClick}>
{props.command.icon}
</Button>
);
};
export const copy: ICommand = {
name: 'copy',
keyCommand: 'copy',
button: (command, props, opts) => <CopyView command={command} editorProps={{ ...props, ...opts }} />,
icon: (
<svg fill="currentColor" viewBox="0 0 24 24" height="16" width="16">
<path d="M20 2H10a2 2 0 0 0-2 2v2h8a2 2 0 0 1 2 2v8h2a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" />
<path d="M4 22h10c1.103 0 2-.897 2-2V10c0-1.103-.897-2-2-2H4c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2zm2-10h6v2H6v-2zm0 4h6v2H6v-2z" />
</svg>
),
};

27
src/commands/css.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { NavLink } from 'react-router-dom';
import { ICommand } from '@uiw/react-markdown-editor';
import styled from 'styled-components';
const Link = styled(NavLink)`
font-size: 0.8rem;
line-height: 0.8rem;
text-decoration: none;
padding: 0.18rem 0.3rem;
&:hover {
color: var(--color-accent-fg);
background-color: var(--color-neutral-muted);
border-radius: 0.2rem;
}
`;
export const cssCommand: ICommand = {
name: 'previewTtheme',
keyCommand: 'previewTtheme',
button: () => <Link to="/editor/theme"></Link>,
};
export const previousCommand: ICommand = {
name: 'previous',
keyCommand: 'previous',
button: () => <Link to="/"></Link>,
};

73
src/commands/theme.tsx Normal file
View File

@@ -0,0 +1,73 @@
import React, { useContext } from 'react';
import { ICommand, IMarkdownEditor, ToolBarProps } from '@uiw/react-markdown-editor';
import styled from 'styled-components';
import { Context, previewThemes, PreviewThemeValue, themes as editorThemes, ThemeValue } from '../store/context';
const Select = styled.select`
max-width: 4rem;
padding: 0;
appearance: none;
background-color: var(--color-border-muted);
border: none;
padding: 0 0.2rem 0 0.2rem;
margin: 0;
font-family: inherit;
font-size: 0.3rem;
outline: none;
height: 1.15rem;
cursor: inherit;
line-height: inherit;
border-radius: 0.2rem;
&::-ms-expand {
display: none;
}
`;
const ThemeView: React.FC<{ command: ICommand; editorProps: IMarkdownEditor & ToolBarProps }> = (props) => {
const { theme, setTheme } = useContext(Context);
const handleChange = (ev: React.ChangeEvent<HTMLSelectElement>) => setTheme(ev.target.value as any);
return (
<Select value={theme} onChange={handleChange}>
{(Object.keys(editorThemes) as Array<ThemeValue>).map((name, key) => {
return (
<option key={key} value={name}>
{editorThemes[name].label}
</option>
);
})}
</Select>
);
};
export const theme: ICommand = {
name: 'theme',
keyCommand: 'theme',
button: (command, props, opts) => <ThemeView command={command} editorProps={{ ...props, ...opts }} />,
};
const ThemePreviewView: React.FC<{}> = () => {
const { setCss, previewTheme, setPreviewTheme } = useContext(Context);
const handleChange = (ev: React.ChangeEvent<HTMLSelectElement>) => {
const value = ev.target.value as PreviewThemeValue;
console.log('vvvv');
setPreviewTheme(value);
setCss(previewThemes[value].value);
};
return (
<Select value={previewTheme} onChange={handleChange}>
{(Object.keys(previewThemes) as Array<PreviewThemeValue>).map((name, key) => {
return (
<option value={name} key={key}>
{previewThemes[name].label}
</option>
);
})}
</Select>
);
};
export const previeTheme: ICommand = {
name: 'previewTtheme',
keyCommand: 'previewTtheme',
button: () => <ThemePreviewView />,
};

25
src/commands/title.tsx Normal file
View File

@@ -0,0 +1,25 @@
import React from 'react';
import { ICommand } from '@uiw/react-markdown-editor';
import styled from 'styled-components';
import { ReactComponent as ColorIcon } from '../assets/color.svg';
const Title = styled.div`
font-size: 0.9rem;
font-weight: bold;
display: flex;
align-items: center;
line-height: 1;
padding-right: 0.5rem;
padding-left: 0.2rem;
`;
export const themeTitle: ICommand = {
name: 'themeTitle',
keyCommand: 'themeTitle',
button: () => (
<Title>
<ColorIcon width={16} height={16} />
</Title>
),
};

100
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,100 @@
import styled from 'styled-components';
import { Outlet, NavLink } from 'react-router-dom';
import '@wcj/dark-mode';
import { ReactComponent as LogoIcon } from '../assets/logo.svg';
import { ReactComponent as GithubIcon } from '../assets/github.svg';
const Warpper = styled.div``;
const Header = styled.header`
display: flex;
flex-direction: row;
justify-content: space-between;
border-bottom: 1px solid var(--color-border-muted);
padding: 0.5rem 0.6rem 0.5rem 1rem;
`;
const Article = styled.article`
display: flex;
flex-direction: row;
align-items: center;
gap: 0.6rem;
`;
const Logo = styled(LogoIcon)`
max-width: 3.6rem;
`;
const Title = styled.h1`
font-size: 1rem;
margin: 0;
display: flex;
align-items: center;
user-select: none;
sup {
color: var(--color-fg-subtle);
margin-left: 0.4rem;
background-color: var(--color-border-muted);
border-radius: 0.1rem;
padding: 0 0.2rem;
font-weight: normal;
font-size: 0.1rem;
letter-spacing: -0.1rem;
}
`;
const Section = styled.section`
display: flex;
align-items: center;
gap: 0.5rem;
dark-mode {
font-size: 1.05rem;
display: block;
line-height: 12px;
margin-left: 0.6rem;
}
a svg {
display: block;
}
a {
text-decoration: none;
color: var(--color-theme-text);
padding: 0.1rem 0.3rem;
box-shadow: inset 0 0 0 var(--color-accent-fg);
transition: all 0.3s;
font-size: 0.9rem;
&.active {
box-shadow: inset 0 -0.3rem 0 var(--color-accent-fg);
}
&:hover:not(.active):not(:last-child) {
box-shadow: inset 0 -1.5rem 0 var(--color-accent-fg);
color: #fff;
border-radius: 0.2rem;
}
}
`;
export function Layout() {
return (
<Warpper className="wmde-markdown-color">
<Header>
<Article>
<Logo width={28} height={28} />
<Title>
<sup> v{VERSION} </sup>
</Title>
</Article>
<Section>
<NavLink to="/"></NavLink>
<NavLink to="/editor/theme"></NavLink>
<dark-mode permanent dark="Dark" light="Light" />
<a href="https://github.com/jaywcjlove/wxmp" target="__blank">
<GithubIcon width={23} height={23} />
</a>
</Section>
</Header>
<Outlet />
</Warpper>
);
}

66
src/index.tsx Normal file
View File

@@ -0,0 +1,66 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import BackToUp from '@uiw/react-back-to-top';
import { Toaster } from 'react-hot-toast';
import { createGlobalStyle } from 'styled-components';
import App from './App';
import { Provider } from './store/context';
export const GlobalStyle = createGlobalStyle`
[data-color-mode*='dark'], [data-color-mode*='dark'] body {
--color-fg-default: #c9d1d9;
--color-fg-muted: #8b949e;
--color-fg-subtle: #484f58;
--color-canvas-default: #0d1117;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgba(110,118,129,0.4);
--color-accent-fg: #58a6ff;
--color-accent-emphasis: #1f6feb;
--color-attention-subtle: rgba(187,128,9,0.15);
--color-danger-fg: #f85149;
}
[data-color-mode*='light'], [data-color-mode*='light'] body {
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210,18%,87%,1);
--color-neutral-muted: rgba(175,184,193,0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
`;
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<HashRouter>
<Toaster />
<BackToUp>Top</BackToUp>
<GlobalStyle />
<Provider>
<App />
</Provider>
</HashRouter>,
);

View File

@@ -0,0 +1,19 @@
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import styled from 'styled-components';
import { useContext } from 'react';
import { Context } from '../../store/context';
import { markdownToHTML } from '../../utils/markdownToHTML';
const Warpper = styled.div`
width: 375px;
padding: 20px;
box-shadow: 0 0 60px rgb(0 0 0 / 10%);
min-height: 100%;
`;
export const Preview = (props: MarkdownPreviewProps) => {
const { css } = useContext(Context);
const html = markdownToHTML(props.source || '', css);
return <Warpper contentEditable spellCheck={false} dangerouslySetInnerHTML={{ __html: html }} />;
};

35
src/pages/home/index.tsx Normal file
View File

@@ -0,0 +1,35 @@
import MarkdownEditor, { getCommands } from '@uiw/react-markdown-editor';
import { useContext } from 'react';
import { EditorView } from '@codemirror/view';
import styled from 'styled-components';
import { Preview } from './Preview';
import { copy } from '../../commands/copy';
import { theme as themeCommand, previeTheme } from '../../commands/theme';
import { cssCommand } from '../../commands/css';
import { Context, themes } from '../../store/context';
const Warpper = styled.div`
height: calc(100vh - 2.9rem);
`;
export const HomePage = () => {
const commands = [...getCommands(), themeCommand];
const { theme, markdown, setMarkdown } = useContext(Context);
const themeValue = themes[theme].value;
const handleChange = (value: string) => setMarkdown(value);
return (
<Warpper>
<MarkdownEditor
value={markdown}
toolbars={commands}
theme={themeValue}
toolbarsMode={[cssCommand, previeTheme, copy, 'preview', 'fullscreen']}
extensions={[EditorView.lineWrapping]}
renderPreview={Preview}
onChange={handleChange}
visible={true}
height="calc(100vh - 4.92rem)"
/>
</Warpper>
);
};

View File

@@ -0,0 +1,19 @@
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import styled from 'styled-components';
import { useContext } from 'react';
import { Context } from '../../store/context';
import { markdownToHTML } from '../../utils/markdownToHTML';
const Warpper = styled.div`
width: 375px;
padding: 20px;
box-shadow: 0 0 60px rgb(0 0 0 / 10%);
min-height: 100%;
`;
export const Preview = (props: MarkdownPreviewProps) => {
const { css, markdown } = useContext(Context);
const html = markdownToHTML(markdown, css);
return <Warpper contentEditable spellCheck={false} dangerouslySetInnerHTML={{ __html: html }} />;
};

View File

@@ -0,0 +1,38 @@
import MarkdownEditor, { IMarkdownEditor } from '@uiw/react-markdown-editor';
import { useContext } from 'react';
import { EditorView } from '@codemirror/view';
import styled from 'styled-components';
import { css as cssLang } from '@codemirror/lang-css';
import { Preview } from './Preview';
import { copy } from '../../commands/copy';
import { previousCommand } from '../../commands/css';
import { themeTitle } from '../../commands/title';
import { theme as themeCommand, previeTheme } from '../../commands/theme';
import { Context, themes } from '../../store/context';
const Warpper = styled.div`
height: calc(100vh - 2.9rem);
`;
export const EditorPage = () => {
const commands = [themeTitle, themeCommand, previousCommand];
const toolbarsMode: IMarkdownEditor['toolbarsMode'] = [previeTheme, copy, 'preview', 'fullscreen'];
const { theme, css, setCss } = useContext(Context);
const value = themes[theme].value;
const handleChange = (value: string) => setCss(value);
return (
<Warpper>
<MarkdownEditor
value={css}
theme={value}
toolbars={commands}
toolbarsMode={toolbarsMode}
reExtensions={[EditorView.lineWrapping, cssLang()]}
renderPreview={Preview}
onChange={handleChange}
visible={true}
height="calc(100vh - 4.92rem)"
/>
</Warpper>
);
};

19
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
/// <reference types="react-scripts" />
declare module '*.less' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare var VERSION: string;
declare module '*.md' {
import { CodeBlockData } from 'markdown-react-code-preview-loader';
const src: CodeBlockData;
export default src;
}
declare module '*.md.css' {
const src: string;
export default src;
}

169
src/store/context.tsx Normal file
View File

@@ -0,0 +1,169 @@
import React, { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { defaultTheme } from '@uiw/react-markdown-editor';
import { abcdef } from '@uiw/codemirror-theme-abcdef';
import { androidstudio } from '@uiw/codemirror-theme-androidstudio';
import { atomone } from '@uiw/codemirror-theme-atomone';
import { bbedit } from '@uiw/codemirror-theme-bbedit';
import { bespin } from '@uiw/codemirror-theme-bespin';
import { darcula } from '@uiw/codemirror-theme-darcula';
import { dracula } from '@uiw/codemirror-theme-dracula';
import { duotoneLight, duotoneDark } from '@uiw/codemirror-theme-duotone';
import { eclipse } from '@uiw/codemirror-theme-eclipse';
import { githubLight, githubDark } from '@uiw/codemirror-theme-github';
import { okaidia } from '@uiw/codemirror-theme-okaidia';
import { sublime } from '@uiw/codemirror-theme-sublime';
import { xcodeLight, xcodeDark } from '@uiw/codemirror-theme-xcode';
import defStyle from '../themes/default.md.css';
import simpleStyle from '../themes/simple.md.css';
import underscoreStyle from '../themes/underscore.md.css';
import baseStyle from '../themes/base.md.css';
import data from '../../README.md';
export const themes = {
default: {
label: '默认主题',
value: defaultTheme,
},
abcdef: {
label: 'Abcdef Theme',
value: abcdef,
},
androidstudio: {
label: 'Android Studio Theme',
value: androidstudio,
},
atomone: {
label: 'Atomone Theme',
value: atomone,
},
bbedit: {
label: 'Bbedit Theme',
value: bbedit,
},
bespin: {
label: 'Bespin Theme',
value: bespin,
},
darcula: {
label: 'Darcula Theme',
value: darcula,
},
dracula: {
label: 'Dracula Theme',
value: dracula,
},
duotoneLight: {
label: 'Duotone Light Theme',
value: duotoneLight,
},
duotoneDark: {
label: 'Duotone Dark Theme',
value: duotoneDark,
},
eclipse: {
label: 'Eclipse Theme',
value: eclipse,
},
githubLight: {
label: 'Github Light Theme',
value: githubLight,
},
githubDark: {
label: 'Github Dark Theme',
value: githubDark,
},
okaidia: {
label: 'Okaidia Theme',
value: okaidia,
},
sublime: {
label: 'Sublime Theme',
value: sublime,
},
xcodeLight: {
label: 'Xcode Light Theme',
value: xcodeLight,
},
xcodeDark: {
label: 'Xcode Dark Theme',
value: xcodeDark,
},
};
export const previewThemes = {
default: {
label: '翡翠绿',
value: defStyle,
},
simple: {
label: '简洁蓝',
value: simpleStyle,
},
underscore: {
label: '下划线黄',
value: underscoreStyle,
},
base: {
label: '简洁',
value: baseStyle,
},
};
export type ThemeValue = keyof typeof themes;
export type PreviewThemeValue = keyof typeof previewThemes;
export interface CreateContext {
markdown: string;
setMarkdown: React.Dispatch<React.SetStateAction<string>>;
css: string;
setCss: React.Dispatch<React.SetStateAction<string>>;
previewTheme: PreviewThemeValue;
setPreviewTheme: React.Dispatch<React.SetStateAction<PreviewThemeValue>>;
theme: ThemeValue;
setTheme: React.Dispatch<React.SetStateAction<ThemeValue>>;
}
export const Context = React.createContext<CreateContext>({
markdown: data.source,
setMarkdown: () => {},
css: previewThemes['underscore'].value,
setCss: () => {},
previewTheme: 'underscore',
setPreviewTheme: () => {},
theme: 'default',
setTheme: () => {},
});
export const Provider: React.FC<React.PropsWithChildren> = ({ children }) => {
const [searchParams, setSearchParams] = useSearchParams();
const paramPreviewTheme = searchParams.get('theme') as PreviewThemeValue;
const initPreviewTheme = paramPreviewTheme || 'underscore';
const [markdown, setMarkdown] = React.useState<string>(data.source);
const [css, setCss] = React.useState<string>(previewThemes[initPreviewTheme].value);
const [previewTheme, setPreviewTheme] = React.useState<PreviewThemeValue>(initPreviewTheme);
const [theme, setTheme] = React.useState<ThemeValue>('default');
useEffect(() => {
if (paramPreviewTheme !== previewTheme) {
searchParams.set('theme', previewTheme);
setSearchParams(searchParams);
}
}, [paramPreviewTheme, previewTheme, searchParams, setSearchParams]);
return (
<Context.Provider
value={{
markdown,
setMarkdown,
css,
setCss,
previewTheme,
setPreviewTheme,
theme,
setTheme,
}}
>
{children}
</Context.Provider>
);
};

181
src/themes/base.md.css Normal file
View File

@@ -0,0 +1,181 @@
a {
color: inherit;
text-decoration: none;
}
h1 {
color: inherit;
font-size: 1.5rem;
font-weight: bold;
}
h2 {
color: inherit;
margin: 2.5rem 0 1rem 0;
font-size: 1.3em;
font-weight: bold;
}
h3 {
color: inherit;
margin: 1em 0 1em 0;
font-weight: bold;
font-size: 1em;
}
h4 {
color: inherit;
margin: 0.6em 0 0.6em 0;
font-weight: bold;
font-size: 0.9em;
}
p {
color: initial;
font-size: 0.85em;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
font-size: 0.85em;
}
blockquote {
font-style: normal;
border-left: none;
margin: 1em 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-size: 0.85em;
font-weight: 400;
letter-spacing: normal;
word-spacing: 0px;
border-radius: 5px;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: pre;
background: rgba(27, 31, 35, 0.05);
padding: 0.2em 0.6em;
border-radius: 0.6em;
font-weight: bold;
font-size: 0.45em;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3rem 0 0.6rem 0;
padding-left: 0.2rem;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

221
src/themes/default.md.css Normal file
View File

@@ -0,0 +1,221 @@
a {
color: #576b95;
text-decoration: none;
font-size: 0.85em;
}
h1 {
display: table;
text-align: center;
color: #3f3f3f;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 2em auto 1em;
padding: 0 1em;
border-bottom: 2px solid #009874;
margin-top: 0;
}
h2 {
display: table;
text-align: center;
color: #fff;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.3em;
font-weight: bold;
margin: 4em auto 2em;
padding: 0 0.3em;
border-radius: 0.3em;
background: #009874;
}
p {
font-size: 0.85em;
}
h3 {
text-align: left;
color: #3f3f3f;
line-height: 1.2;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
margin: 2em 8px 0.75em 0;
padding-left: 8px;
border-left: 3px solid #009874;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
line-height: 1.5em;
color: rgb(30 41 59);
font-size: 0.85em;
}
blockquote {
text-align: left;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 0.85em;
font-style: normal;
border-left: none;
padding: 0.1rem 1rem;
border-radius: 4px;
background: rgba(27, 31, 35, 0.05);
margin: 1rem 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-indent: 0px;
text-transform: none;
widows: 2;
word-spacing: 0px;
text-decoration-style: initial;
text-decoration-color: initial;
text-align: left;
line-height: 1.5;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
border-radius: 0.3em;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
line-height: 1.75;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: pre;
color: #009874;
background: rgba(27, 31, 35, 0.05);
padding: 0.2em 0.6em;
border-radius: 0.6em;
font-weight: bold;
font-size: 0.45em;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3em 0 0.6em 0;
padding-left: 0.2em;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

221
src/themes/simple.md.css Normal file
View File

@@ -0,0 +1,221 @@
a {
color: #576b95;
text-decoration: none;
font-size: 0.85em;
}
h1 {
display: table;
text-align: center;
color: #3f3f3f;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.2em;
font-weight: bold;
margin: 2em auto 1em;
padding: 0 1em;
border-bottom: 2px solid #0f4c81;
margin-top: 0;
}
h2 {
display: table;
text-align: center;
color: #fff;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.2em;
font-weight: bold;
margin: 4em auto 2em;
padding: 0 0.3em;
border-radius: 0.3rem;
background: #0f4c81;
}
p {
font-size: 0.85em;
}
h3 {
text-align: left;
color: #3f3f3f;
line-height: 1.2;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
margin: 2em 8px 0.75em 0;
padding-left: 8px;
border-left: 3px solid #0f4c81;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
line-height: 1.5em;
color: rgb(30 41 59);
font-size: 0.85em;
}
blockquote {
text-align: left;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 0.85em;
font-style: normal;
border-left: none;
padding: 0.1rem 1rem;
border-radius: 4px;
background: rgba(27, 31, 35, 0.05);
margin: 1rem 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-indent: 0px;
text-transform: none;
widows: 2;
word-spacing: 0px;
text-decoration-style: initial;
text-decoration-color: initial;
text-align: left;
line-height: 1.5;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
border-radius: 0.3em;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
line-height: 1.75;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: pre;
color: #0f4c81;
background: rgba(27, 31, 35, 0.05);
padding: 0.2em 0.6em;
border-radius: 0.6em;
font-weight: bold;
font-size: 0.45em;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3em 0 0.6em 0;
padding-left: 0.2em;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

View File

@@ -0,0 +1,218 @@
a {
color: #576b95;
text-decoration: none;
font-size: 0.85em;
}
h1 {
display: table;
text-align: center;
color: #3f3f3f;
line-height: 1.15;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.3em;
font-weight: bold;
margin: 2em auto 1em;
padding: 0 1em 0.3em 1em;
margin-top: 0;
box-shadow: inset 0 -0.9rem 0 0 #ffb11b;
}
h2 {
display: table;
line-height: 1.35;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.2em;
font-weight: bold;
padding: 0 0.3em;
margin: 2em 0 1em 0;
box-shadow: inset 0 -0.7rem 0 0 #ffb11b;
}
p {
font-size: 0.85em;
}
h3 {
text-align: left;
color: #3f3f3f;
line-height: 1.2;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1.1em;
font-weight: bold;
margin: 2em 8px 0.75em 0;
padding-left: 8px;
border-left: 5px solid #ffb11b;
}
ul {
padding-left: 1.2em;
}
ol {
padding-left: 1.2em;
}
li {
margin: 0;
line-height: 1.5em;
color: rgb(30 41 59);
font-size: 0.85em;
}
blockquote {
text-align: left;
line-height: 1.75;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 0.85em;
font-style: normal;
border-left: none;
padding: 0.1rem 1rem;
border-radius: 4px;
background: rgba(27, 31, 35, 0.05);
margin: 1rem 0;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-style: normal;
font-variant-ligatures: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: 2;
text-indent: 0px;
text-transform: none;
widows: 2;
word-spacing: 0px;
text-decoration-style: initial;
text-decoration-color: initial;
text-align: left;
line-height: 1.5;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
border-radius: 0.3em;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 0.85em;
}
td {
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
th {
background: rgb(0 0 0 / 5%);
border: 1px solid #ddd;
padding: 0.25em 0.5em;
}
.code-highlight {
text-align: left;
line-height: 1.75;
font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;
font-size: 0.85em;
margin: 0px;
white-space: nowrap;
}
.code-line {
display: block;
line-height: 1.3;
}
.code-spans {
text-align: left;
line-height: 1;
white-space: pre;
color: #ffb11b;
background: rgba(27, 31, 35, 0.05);
padding: 0.2em 0.6em;
border-radius: 0.6em;
font-weight: bold;
font-size: 0.45em;
}
.footnotes-title {
display: table;
font-family: -apple-system-font, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;
font-size: 1em;
font-weight: bold;
margin: 3em 0 0.6em 0;
padding-left: 0.2em;
}
.footnotes-list {
font-size: 0.75em;
font-style: italic;
line-height: 1.2;
margin: 0.4rem 0;
}
figure {
margin: 0;
}
.image-warpper {
text-align: center;
margin-bottom: 0rem;
visibility: visible;
}
.image {
display: initial;
max-width: 100%;
}
.comment {
color: #6a737d;
}
.property {
color: #6f42c1;
}
.function {
color: #6f42c1;
}
.keyword {
color: #d73a49;
}
.punctuation {
color: #0550ae;
}
.unit {
color: #0550ae;
}
.tag {
color: #22863a;
}
.selector {
color: #22863a;
}
.quote {
color: #22863a;
}
.number {
color: #005cc5;
}
.attr-name {
color: #005cc5;
}
.attr-value {
color: #005cc5;
}

113
src/utils/css.ts Normal file
View File

@@ -0,0 +1,113 @@
import { RootContent, Element, Text, Root } from 'hast';
export const getBlock = (data: any, str: string = '') => {
if (data && data.data && data.data.type === 'Declaration') {
str = `${data.data.property}: ${data.data.value.value}${data.data.important ? ' !important' : ''};`;
if (data.next) {
str += getBlock(data.next);
}
}
return str;
};
export const cssdata = (list: any, result: Record<string, string> = {}) => {
if (list.data && list.data.type === 'Rule') {
result[list.data.prelude.value] = getBlock(list.data.block.children.head);
if (list.next) {
result = cssdata(list.next, { ...result });
}
}
return result;
};
export const spaceEscape = (node: RootContent) => {
if (node.type === 'element' && node.children) {
const className = node.properties?.className as string[];
if (className) {
if (!node.properties) {
node.properties = {};
}
node.properties.className = className.filter((str: string) => !/(token|control-flow)/.test(str));
}
node.children.map((elm) => {
if (elm.type === 'element' && elm.children) {
spaceEscape(elm);
}
if (elm.type === 'text') {
elm.value = elm.value.replace(/\s/g, '\u00A0');
}
return elm;
});
}
};
type ChildContent = Element | Text;
const getNodeText = (node: ChildContent[]) => {
let str = '';
node.forEach((item) => {
if (item.type === 'text') str += item.value;
else if (item.type === 'element') {
str += getNodeText(item.children as ChildContent[]);
}
});
return str.replace(/↩/, '');
};
export const footnotes = (node: Element) => {
node.children.map((item) => {
if (item.type === 'element' && item.tagName === 'h2') {
if (!item.properties) item.properties = {};
item.properties.className = ['footnotes-title'];
item.children = [
{
type: 'text',
value: '参考',
},
];
}
if (item.type === 'element' && item.tagName === 'ol') {
item.children.map((li) => {
if (li.type === 'element' && li.tagName === 'li') {
if (!li.properties) li.properties = {};
li.properties.className = ['footnotes-list'];
li.children = [
{
type: 'text',
value: getNodeText(li.children as ChildContent[]),
},
];
}
return li;
});
}
return item;
});
};
export const footnotesLabel = (node: Element) => {
const label = getNodeText(node.children as ChildContent[]);
node.children = [
{
type: 'text',
value: `[${label}]`,
},
];
};
export const imagesStyle = (node: Element, parent: Root | Element | null) => {
if (
parent?.type === 'element' &&
/(p|a)/.test(parent.tagName) &&
node?.type === 'element' &&
node.tagName === 'img'
) {
if (parent.tagName === 'p') {
parent.tagName = 'figure';
}
if (!parent.properties) parent.properties = {};
parent.properties.className = ['image-warpper'];
if (!node.properties) node.properties = {};
node.properties.className = ['image'];
}
};

103
src/utils/markdownToHTML.ts Normal file
View File

@@ -0,0 +1,103 @@
import { VFile } from 'vfile';
import { unified } from 'unified';
import * as csstree from 'css-tree';
import { Element } from 'hast';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypePrism from 'rehype-prism-plus';
import rehypeRaw from 'rehype-raw';
import rehypeAttrs from 'rehype-attr';
import rehypeIgnore from 'rehype-ignore';
import rehypeRewrite from 'rehype-rewrite';
import stringify from 'rehype-stringify';
import { cssdata, spaceEscape, footnotes, footnotesLabel, imagesStyle } from './css';
export type MarkdownToHTMLOptions = {};
export function markdownToHTML(md: string, css: string, options: MarkdownToHTMLOptions = {}) {
const ast = csstree.parse(css, {
parseAtrulePrelude: false,
parseRulePrelude: false,
parseValue: false,
parseCustomProperty: false,
positions: false,
});
// @ts-ignore
const data = cssdata(ast.children.head);
const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypePrism)
.use(rehypeRaw)
.use(rehypeIgnore, {})
.use(rehypeAttrs, { properties: 'attr' })
.use(rehypeRewrite, {
rewrite: (node, index, parent) => {
// @ts-ignore
if (
node?.type === 'element' &&
node?.tagName === 'code' &&
parent?.type === 'element' &&
parent?.tagName === 'pre'
) {
spaceEscape(node);
}
if (
node?.type === 'element' &&
node.tagName === 'section' &&
(node?.properties?.className as string[]).includes('footnotes')
) {
footnotes(node);
}
if (node?.type === 'element' && node.tagName === 'sup') {
footnotesLabel(node);
}
if (node?.type === 'element' && node.tagName === 'img') {
imagesStyle(node, parent);
}
// Code Spans style
if (
node?.type === 'element' &&
node?.tagName === 'code' &&
parent?.type === 'element' &&
parent?.tagName !== 'pre'
) {
if (!node.properties) node.properties = {};
node.properties!.className = ['code-spans'];
}
// List TODO style
if (parent?.type === 'element' && node?.type === 'element' && node?.tagName === 'input') {
if (parent && parent.type === 'element') {
parent.children = parent?.children.filter((elm) => (elm as Element).tagName !== 'input');
}
return;
}
// Support *.md.css
if (node?.type === 'element') {
if (!node.properties) {
node.properties = {};
}
const className = node.properties?.className as string[];
let style = '';
if (className) {
className.forEach((name) => {
if (data[`.${name}`]) {
style = data[`.${name}`];
}
});
}
if (!style) style = data[node.tagName];
if (style) {
node.properties.style = style + (node.properties.style || '');
}
}
},
})
.use(stringify);
const file = new VFile();
file.value = md;
const hastNode = processor.runSync(processor.parse(file), file);
return String(processor.stringify(hastNode, file));
}

22
tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"include": [".kktrc.ts", "src"],
"compilerOptions": {
"jsx": "react-jsx",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"baseUrl": "./src",
"noFallthroughCasesInSwitch": true,
"noEmit": true
}
}

BIN
wxmq.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB