feat: init web app.

This commit is contained in:
jaywcjlove
2022-09-02 01:16:22 +08:00
parent ef01ce998e
commit d0364a0bee
28 changed files with 888 additions and 130 deletions

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/

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;
};

View File

@@ -1,41 +1,75 @@
# Wxmp
<div align="center">
chrome 小插件,优化在微信公众账号中发文章,因复制粘贴带过去的`font-family` CSS 样式导致被微信过滤样式全无。使用此插件删除提交文章上所有HTML节点上的`font-family`,让复制过去的样式保持一致。
<h1 align="center">微信公众号 Markdown 编辑器</h1>
目前删除这些标签上的`font-family`样式
</div>
> `code`,`pre`,`h1`,`h2`,`h3`,`h4`,`h5`,`h6`,`p`,`div`,`span`
用于微信公众号文章使用 markdown 语法做一篇简介美观大方的微信公众号图文的工具。原先是一个 Chrome 插件来解决排版问题,由于发版本麻烦,和一些功能拓展开发停滞了,最近写了一个 Web 版本供自己使用。
![界面预览](https://raw.githubusercontent.com/jaywcjlove/wxmp/master/wxmq.png)
## 功能特性
开发计划和一些功能介绍,有新需求可以在 issue 中提。
## 已经实现功能
- [x] 支持 Markdown 所有基础语法
- [x] 支持自定义 CSS 样式
- [ ] 支持主题选择 & 配置。
- [x] 支持明暗两种主题预览。
- [ ] 支持色盘取色,快速替换文章整体色调
- [x] 过滤 `font-family`
- [x] 代码高亮区域有背景颜色;
- [x] 代码高亮区域有横向滚动条强制不换行;
- [x] 增加iOS滚动滚动弹性
- [ ] 添加设置标题工具;
- [ ] 添加字段高亮工具;
- [ ] 添加删除线工具,如:<del>删除线</del>
### 支持代码块样式
# 直接安装
下面是 `jsx` 代码块展示示例,并高亮代码
1. 下载扩展程序[Wxmp.crx](https://github.com/jaywcjlove/wxmp/releases) 文件
2. 在chrome里面器地址输入`chrome://extensions/` 打开插件界面
3.`Wxmp.crx`文件拖入chrome浏览器的扩展程序列表中
```jsx
function Demo() {
return <div className="demo">Hello World!</div>
}
```
# 开发模式插件安装
下面是 `css` 代码块展示示例,并高亮代码
1. 下载文件压缩包解压
2. 在chrome里面器地址输入`chrome://extensions/` 打开插件界面
3. 点击`加载已解压的扩展程序...`
4. 选择插件所在的目录
```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;
}
```
### 支持内联代码
# 使用方法
Inline Code `{code: 0}`
1. 打开微信公众平台,新建图文消息,复制文章到编辑器中
2. 在右上角点击微信图标
3. 点击弹出的模态框上的删除按钮
4. 如果成功会在按钮后面提示`更改成功!!`
### 支持表格
表格无法使用自定义样式,暂时没找到解决途径
| Header 1 | Header 2 |
| --- | --- |
| Key 1 | Value 1 |
| Key 2 | Value 2 |
| Key 3 | Value 3 |
### 支持 GFM 脚注
这是一个简单的脚注[^1]。 页面最后有一些额外的文字描述。注意这不是完整的注脚[^2]。
[^1]: https://github.github.com/gfm/
[^2]: 微信文章不支持锚点跳转和打开第三方 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>
## License
Licensed under the MIT License.

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"]
}

61
package.json Normal file
View File

@@ -0,0 +1,61 @@
{
"name": "website",
"version": "1.1.0",
"private": true,
"scripts": {
"start": "kkt start",
"build": "kkt build"
},
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.9",
"@uiw/react-back-to-top": "^1.2.0",
"@uiw/react-github-corners": "^1.5.15",
"@uiw/react-markdown-editor": "^5.3.2",
"@uiw/react-markdown-preview": "^4.1.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-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",
"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>

37
re.md Normal file
View File

@@ -0,0 +1,37 @@
<div align="center">
<h1 align="center">微信公众号 Markdown 编辑器</h1>
</div>
用于微信公众号文章使用 markdown 语法做一篇简介美观大方的微信公众号图文的工具。原先是一个 Chrome 插件来解决排版问题,由于发版本麻烦,和一些功能拓展开发停滞了,最近写了一个 Web 版本供自己使用。
## 功能特性
- [x] 支持 Markdown 所有基础语法
- [x] 支持自定义 CSS 样式
- [ ] 支持主题选择 & 配置。
- [x] 支持明暗两种主题预览。
- [ ] 支持色盘取色,快速替换文章整体色调
### 支持代码块样式
```jsx
function Demo() {
return <div>Hello World!</div>
}
```
```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;
}
```
## License
Licensed under the MIT License.

13
src/App.tsx Normal file
View File

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

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

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

@@ -0,0 +1,77 @@
import styled from 'styled-components';
import { Outlet } 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 1rem 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: 1.0rem;
margin: 0;
display: flex;
align-items: center;
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;
}
`;
const Section = styled.section`
display: flex;
align-items: center;
gap: 0.8rem;
dark-mode {
font-size: 1.4rem;
}
a svg {
display: block;
}
`;
export function Layout() {
return (
<Warpper className="wmde-markdown-color">
<Header>
<Article>
<Logo width={28} height={28} />
<Title>
<sup> v{VERSION} </sup>
</Title>
</Article>
<Section>
<dark-mode permanent dark="Dark" light="Light" />
<a href="https://github.com/jaywcjlove/wxmp" target="__blank">
<GithubIcon width={28} height={28} />
</a>
</Section>
</Header>
<Outlet />
</Warpper>
);
}

177
src/conf/default.md.css Normal file
View File

@@ -0,0 +1,177 @@
a {
color: #576b95;
text-decoration: none;
font-size: 14px;
}
h1 {
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;
display:table;
margin:2em auto 1em;
padding:0 1em;
border-bottom:2px solid #009874;
margin-top: 0;
}
h2 {
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;
display:table;
margin:4em auto 2em;
padding:0 0.3em;
border-radius: 0.3rem;
background:#009874;
}
p {
font-size: 14px;
}
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;
font-size: 14px;
}
ol {
padding-left: 1.2em;
font-size: 14px;
}
li {
font-size: 16px;
margin: 0;
line-height: 26px;
color: rgb(30 41 59);
font-size: 14px;
}
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:14px;
font-style:normal;
border-left:none;
padding:1em;
border-radius:4px;
background:rgba(27,31,35,.05);
margin:2em 8px;
}
pre {
display: block;
overflow-x: auto;
padding: 1em;
color: rgb(51, 51, 51);
background: rgb(248, 248, 248);
font-size: 14px;
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: 5px;
margin: 0.9rem 0;
white-space: pre;
}
table {
width: 100% !important;
border-collapse: collapse;
line-height: 1.35;
font-size: 90%;
}
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: 14px;
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,.05);
padding: 0.2rem 0.3rem;
border-radius:4px;
font-weight: bold;
font-size: 14px;
}
.footnotes-title {
font-family:-apple-system-font,BlinkMacSystemFont,"Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei UI","Microsoft YaHei",Arial,sans-serif;
font-size:1.0rem;
font-weight:bold;
display:table;
margin:3rem 0 0.6rem 0;
padding-left: 0.2rem;
}
.footnotes-list {
font-size: 12px;
}
.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; }

61
src/index.tsx Normal file
View File

@@ -0,0 +1,61 @@
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';
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;
}
* {
box-sizing: border-box;
}
`;
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<HashRouter>
<Toaster />
<BackToUp>Top</BackToUp>
<GlobalStyle />
<App />
</HashRouter>,
);

View File

@@ -0,0 +1,17 @@
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import styled from 'styled-components';
import def from '../../conf/default.md.css';
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, visible: boolean) => {
const html = markdownToHTML(props.source || '', def);
return <Warpper dangerouslySetInnerHTML={{ __html: html }} />;
}

45
src/pages/home/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/pages/home/index.tsx Normal file
View File

@@ -0,0 +1,27 @@
import MarkdownEditor, { getCommands } from '@uiw/react-markdown-editor';
import { EditorView } from "@codemirror/view";
import styled from 'styled-components';
import data from '../../../README.md';
import { Preview } from './Preview';
import { copy } from './copy'
const Warpper = styled.div`
height: calc(100vh - 2.9rem);
`;
export const HomePage = () => {
const commands = getCommands();
return (
<Warpper>
<MarkdownEditor
value={data.source}
toolbars={commands}
toolbarsMode={[copy, 'preview', 'fullscreen']}
extensions={[EditorView.lineWrapping]}
renderPreview={Preview}
visible={true}
height="calc(100vh - 5.0rem)"
/>
</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;
}

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

@@ -0,0 +1,90 @@
import { RootContent, Element, Text } 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}]`
}];
}

View File

@@ -0,0 +1,80 @@
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 rehypeRewrite from 'rehype-rewrite';
import stringify from 'rehype-stringify';
import { cssdata, spaceEscape, footnotes, footnotesLabel } 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(remarkRehype, { allowDangerousHtml: true })
.use(rehypePrism)
.use(remarkGfm)
.use(rehypeRaw)
.use(rehypeRewrite, {
rewrite: (node, index, parent) => {
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 === 'code' && parent?.type === 'element' && parent?.tagName !== 'pre') {
if (!node.properties) node.properties = {}
node.properties!.className = ['code-spans'];
}
if (node?.type === 'element') {
if (node.tagName === 'input' && parent?.type === 'element') {
if (parent && parent.type === 'element') {
parent.children = parent?.children.filter(elm => (elm as Element).tagName !== 'input')
}
return;
}
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;
}
}
}
})
.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