13 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
25 changed files with 1514 additions and 279 deletions

4
.husky/pre-commit Executable file
View File

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

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

103
README.md
View File

@@ -1,10 +1,10 @@
<div align="center"> <div align="center">
<h1 align="center">微信公众号 Markdown 编辑器</h1>
<h1 align="center">微信公众号 Markdown 编辑器</h1>
</div> </div>
微信公众号文章 Markdown 编辑器,使用 markdown 语法创建一篇简介美观大方的微信公众号图文。由于发版本麻烦,和一些功能无法扩展停滞开发了,未来不再开发 Chrome 的工具(暂存在 chrome 分支),通过 web 版本定制更丰富的功能。 ![微信公众号 Markdown 编辑器](https://user-images.githubusercontent.com/1680273/188257498-529e42aa-44b0-441f-bcdd-049efa174b78.png)
微信公众号文章 Markdown 在线编辑器,使用 markdown 语法创建一篇简介美观大方的微信公众号图文。由于发版本麻烦,和一些功能无法扩展停滞开发了,未来不再开发 Chrome 的插件(暂存在 chrome 分支),通过 web 版本定制更丰富的功能。
## 功能特性 ## 功能特性
@@ -12,9 +12,11 @@
- [x] 支持 Markdown 所有基础语法 - [x] 支持 Markdown 所有基础语法
- [x] 支持自定义 CSS 样式 - [x] 支持自定义 CSS 样式
- [ ] 支持主题选择 & 配置 - [x] 支持主题选择 & 编辑预览
- [x] 支持明暗两种主题预览。 - [x] 支持明暗两种主题预览。
- [ ] 支持色盘取色,快速替换文章整体色调 - [ ] 支持色盘取色,快速替换文章整体色调
- [ ] 支持 URL 参数加载 Markdown 内容。
- [x] 支持 URL 参数选择预览主题。
### 支持代码块样式 ### 支持代码块样式
@@ -56,10 +58,11 @@ Inline Code `{code: 0}`
### 支持 GFM 脚注 ### 支持 GFM 脚注
这是一个简单的脚注[^1]。 页面最后有一些额外的文字描述。注意这不是完整的注脚[^2] 这是一个简单的 Markdown[^1] 语法的脚注[^2]。 页面最后有一些额外的文字描述。注意这不是完整的注脚[^3]特性
[^1]: https://github.github.com/gfm/ [^1]: GitHub 风格的 Markdown 规范 https://github.github.com/gfm/
[^2]: 微信文章不支持锚点跳转和打开第三方 URL 超链接,所以不支持完整的注脚 [^2]: 脚注 https://github.blog/changelog/2021-09-30-footnotes-now-supported-in-markdown-fields/
[^3]: 微信文章不支持锚点跳转和打开第三方 URL 超链接,所以不支持完整的注脚特性。
### 支持注释 ### 支持注释
@@ -72,6 +75,79 @@ Inline Code `{code: 0}`
音 <rp></rp><rt>yin</rt><rp></rp> 音 <rp></rp><rt>yin</rt><rp></rp>
</ruby> </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;` 设置文字红色。
### 标记忽略内容
此特性利用 HTML 注释在 markdown 中被忽略的特性,标记需要忽略的内容,标记开始 `<!--rehype:ignore:start-->`,标记结束 `<!--rehype:ignore:end-->`,被标记的内容在微信 Markdown 编辑器预览中不显示。在其它预览工具中展示内容,比如 GitHub 中能展示。
```markdown
# 注释忽略
<!--rehype:ignore:start-->内容在微信 Markdown 编辑器预览中不显示。在其它预览工具中展示内容。<!--rehype:ignore:end-->
```
## 主题定制
在目录 `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 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)
@@ -98,16 +174,19 @@ docker run --name wxmp -itd -p 96611:3000 ghcr.io/jaywcjlove/wxmp:latest
http://localhost:96611/ http://localhost:96611/
``` ```
## Contributors ## 贡献者
As always, thanks to our amazing contributors! 一如既往,感谢我们出色的贡献者!
<a href="https://github.com/jaywcjlove/wxmp/graphs/contributors"> <a href="https://github.com/jaywcjlove/wxmp/graphs/contributors">
<img src="https://jaywcjlove.github.io/wxmp/CONTRIBUTORS.svg" /> <img src="https://jaywcjlove.github.io/wxmp/CONTRIBUTORS.svg" />
</a> </a>
Made with [github-action-contributors](https://github.com/jaywcjlove/github-action-contributors). 上图贡献者列表,由 [action-contributors](https://github.com/jaywcjlove/github-action-contributors)[^4] 自动生成贡献者图片。
[^4]: Action Contributors https://github.com/jaywcjlove/github-action-contributors
## License ## License
Licensed under the MIT License. 根据 MIT 许可证获得许可。

View File

@@ -1,18 +1,33 @@
{ {
"name": "website", "name": "website",
"version": "2.0.0", "version": "2.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "kkt start", "start": "kkt start",
"build": "kkt build" "build": "kkt build",
"prepare": "husky install",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"pretty-quick": "pretty-quick --staged"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.9", "@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-back-to-top": "^1.2.0",
"@uiw/react-github-corners": "^1.5.15", "@uiw/react-github-corners": "^1.5.15",
"@uiw/react-markdown-editor": "^5.3.2", "@uiw/react-markdown-editor": "^5.6.0",
"@uiw/react-markdown-preview": "^4.1.0",
"@wcj/dark-mode": "^1.0.15", "@wcj/dark-mode": "^1.0.15",
"css-tree": "^2.2.1", "css-tree": "^2.2.1",
"react": "^18.2.0", "react": "^18.2.0",
@@ -20,6 +35,8 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.3.0", "react-hot-toast": "^2.3.0",
"react-router-dom": "^6.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-prism-plus": "^1.5.0",
"rehype-raw": "^6.1.1", "rehype-raw": "^6.1.1",
"rehype-stringify": "^9.0.3", "rehype-stringify": "^9.0.3",
@@ -37,6 +54,9 @@
"@types/react": "^18.0.17", "@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.25",
"husky": "^8.0.1",
"prettier": "^2.7.1",
"pretty-quick": "~3.1.3",
"kkt": "^7.2.0", "kkt": "^7.2.0",
"markdown-react-code-preview-loader": "^2.1.2" "markdown-react-code-preview-loader": "^2.1.2"
}, },

View File

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

View File

@@ -24,7 +24,7 @@ const CopyView: React.FC<{ command: ICommand; editorProps: IMarkdownEditor & Too
document.execCommand(`copy`); document.execCommand(`copy`);
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();
toast.success(<div></div>); toast.success(<div></div>);
} };
return ( return (
<Button type="button" onClick={handleClick}> <Button type="button" onClick={handleClick}>
{props.command.icon} {props.command.icon}
@@ -38,8 +38,8 @@ export const copy: ICommand = {
button: (command, props, opts) => <CopyView command={command} editorProps={{ ...props, ...opts }} />, button: (command, props, opts) => <CopyView command={command} editorProps={{ ...props, ...opts }} />,
icon: ( icon: (
<svg fill="currentColor" viewBox="0 0 24 24" height="16" width="16"> <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="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"/> <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> </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>
),
};

View File

@@ -1,18 +1,17 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Outlet } from 'react-router-dom'; import { Outlet, NavLink } from 'react-router-dom';
import '@wcj/dark-mode'; import '@wcj/dark-mode';
import { ReactComponent as LogoIcon } from '../assets/logo.svg'; import { ReactComponent as LogoIcon } from '../assets/logo.svg';
import { ReactComponent as GithubIcon } from '../assets/github.svg'; import { ReactComponent as GithubIcon } from '../assets/github.svg';
const Warpper = styled.div` const Warpper = styled.div``;
`;
const Header = styled.header` const Header = styled.header`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--color-border-muted); border-bottom: 1px solid var(--color-border-muted);
padding: 0.5rem 1rem 0.5rem 1rem; padding: 0.5rem 0.6rem 0.5rem 1rem;
`; `;
const Article = styled.article` const Article = styled.article`
@@ -27,10 +26,11 @@ const Logo = styled(LogoIcon)`
`; `;
const Title = styled.h1` const Title = styled.h1`
font-size: 1.0rem; font-size: 1rem;
margin: 0; margin: 0;
display: flex; display: flex;
align-items: center; align-items: center;
user-select: none;
sup { sup {
color: var(--color-fg-subtle); color: var(--color-fg-subtle);
margin-left: 0.4rem; margin-left: 0.4rem;
@@ -38,19 +38,40 @@ const Title = styled.h1`
border-radius: 0.1rem; border-radius: 0.1rem;
padding: 0 0.2rem; padding: 0 0.2rem;
font-weight: normal; font-weight: normal;
font-size: 0.1rem;
letter-spacing: -0.1rem;
} }
`; `;
const Section = styled.section` const Section = styled.section`
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.8rem; gap: 0.5rem;
dark-mode { dark-mode {
font-size: 1.4rem; font-size: 1.05rem;
display: block;
line-height: 12px;
margin-left: 0.6rem;
} }
a svg { a svg {
display: block; 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() { export function Layout() {
@@ -65,9 +86,11 @@ export function Layout() {
</Title> </Title>
</Article> </Article>
<Section> <Section>
<NavLink to="/"></NavLink>
<NavLink to="/editor/theme"></NavLink>
<dark-mode permanent dark="Dark" light="Light" /> <dark-mode permanent dark="Dark" light="Light" />
<a href="https://github.com/jaywcjlove/wxmp" target="__blank"> <a href="https://github.com/jaywcjlove/wxmp" target="__blank">
<GithubIcon width={28} height={28} /> <GithubIcon width={23} height={23} />
</a> </a>
</Section> </Section>
</Header> </Header>

View File

@@ -1,177 +0,0 @@
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:0.1rem 1rem;
border-radius:4px;
background:rgba(27,31,35,.05);
margin: 1rem 0;
}
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; }

View File

@@ -5,6 +5,7 @@ import BackToUp from '@uiw/react-back-to-top';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
import { createGlobalStyle } from 'styled-components'; import { createGlobalStyle } from 'styled-components';
import App from './App'; import App from './App';
import { Provider } from './store/context';
export const GlobalStyle = createGlobalStyle` export const GlobalStyle = createGlobalStyle`
[data-color-mode*='dark'], [data-color-mode*='dark'] body { [data-color-mode*='dark'], [data-color-mode*='dark'] body {
@@ -44,7 +45,9 @@ export const GlobalStyle = createGlobalStyle`
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
* { *,
*::before,
*::after {
box-sizing: border-box; box-sizing: border-box;
} }
`; `;
@@ -56,6 +59,8 @@ root.render(
<Toaster /> <Toaster />
<BackToUp>Top</BackToUp> <BackToUp>Top</BackToUp>
<GlobalStyle /> <GlobalStyle />
<App /> <Provider>
<App />
</Provider>
</HashRouter>, </HashRouter>,
); );

View File

@@ -1,6 +1,7 @@
import { MarkdownPreviewProps } from '@uiw/react-markdown-preview'; import { MarkdownPreviewProps } from '@uiw/react-markdown-preview';
import styled from 'styled-components'; import styled from 'styled-components';
import def from '../../conf/default.md.css'; import { useContext } from 'react';
import { Context } from '../../store/context';
import { markdownToHTML } from '../../utils/markdownToHTML'; import { markdownToHTML } from '../../utils/markdownToHTML';
@@ -11,7 +12,8 @@ const Warpper = styled.div`
min-height: 100%; min-height: 100%;
`; `;
export const Preview = (props: MarkdownPreviewProps, visible: boolean) => { export const Preview = (props: MarkdownPreviewProps) => {
const html = markdownToHTML(props.source || '', def); const { css } = useContext(Context);
return <Warpper dangerouslySetInnerHTML={{ __html: html }} />; const html = markdownToHTML(props.source || '', css);
} return <Warpper contentEditable spellCheck={false} dangerouslySetInnerHTML={{ __html: html }} />;
};

View File

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

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

View File

@@ -1,28 +1,28 @@
import { RootContent, Element, Text } from 'hast'; import { RootContent, Element, Text, Root } from 'hast';
export const getBlock = (data: any, str: string = '') => { export const getBlock = (data: any, str: string = '') => {
if (data && data.data && data.data.type === 'Declaration') { if (data && data.data && data.data.type === 'Declaration') {
str = `${data.data.property}: ${data.data.value.value}${data.data.important ? ' !important' : ''};`; str = `${data.data.property}: ${data.data.value.value}${data.data.important ? ' !important' : ''};`;
if (data.next) { if (data.next) {
str += getBlock(data.next) str += getBlock(data.next);
} }
} }
return str; return str;
} };
export const cssdata = (list: any, result: Record<string, string> = {}) => { export const cssdata = (list: any, result: Record<string, string> = {}) => {
if (list.data && list.data.type === 'Rule') { if (list.data && list.data.type === 'Rule') {
result[list.data.prelude.value] = getBlock(list.data.block.children.head); result[list.data.prelude.value] = getBlock(list.data.block.children.head);
if (list.next) { if (list.next) {
result = cssdata(list.next, {...result}) result = cssdata(list.next, { ...result });
} }
} }
return result; return result;
} };
export const spaceEscape = (node: RootContent) => { export const spaceEscape = (node: RootContent) => {
if (node.type === 'element' && node.children) { if (node.type === 'element' && node.children) {
const className = (node.properties?.className as string[]); const className = node.properties?.className as string[];
if (className) { if (className) {
if (!node.properties) { if (!node.properties) {
node.properties = {}; node.properties = {};
@@ -30,17 +30,17 @@ export const spaceEscape = (node: RootContent) => {
node.properties.className = className.filter((str: string) => !/(token|control-flow)/.test(str)); node.properties.className = className.filter((str: string) => !/(token|control-flow)/.test(str));
} }
node.children.map(elm => { node.children.map((elm) => {
if (elm.type === 'element' && elm.children) { if (elm.type === 'element' && elm.children) {
spaceEscape(elm) spaceEscape(elm);
} }
if (elm.type === 'text') { if (elm.type === 'text') {
elm.value = elm.value.replace(/\s/g, '\u00A0') elm.value = elm.value.replace(/\s/g, '\u00A0');
} }
return elm return elm;
}) });
} }
} };
type ChildContent = Element | Text; type ChildContent = Element | Text;
const getNodeText = (node: ChildContent[]) => { const getNodeText = (node: ChildContent[]) => {
@@ -50,41 +50,64 @@ const getNodeText = (node: ChildContent[]) => {
else if (item.type === 'element') { else if (item.type === 'element') {
str += getNodeText(item.children as ChildContent[]); str += getNodeText(item.children as ChildContent[]);
} }
}) });
return str.replace(/↩/, ''); return str.replace(/↩/, '');
} };
export const footnotes = (node: Element) => { export const footnotes = (node: Element) => {
node.children.map((item) => { node.children.map((item) => {
if (item.type === 'element' && item.tagName === 'h2') { if (item.type === 'element' && item.tagName === 'h2') {
if (!item.properties) item.properties = {}; if (!item.properties) item.properties = {};
item.properties.className = ['footnotes-title']; item.properties.className = ['footnotes-title'];
item.children = [{ item.children = [
type: 'text', {
value: '参考' type: 'text',
}]; value: '参考',
},
];
} }
if (item.type === 'element' && item.tagName === 'ol') { if (item.type === 'element' && item.tagName === 'ol') {
item.children.map((li) => { item.children.map((li) => {
if (li.type === 'element' && li.tagName === 'li') { if (li.type === 'element' && li.tagName === 'li') {
if (!li.properties) li.properties = {}; if (!li.properties) li.properties = {};
li.properties.className = ['footnotes-list']; li.properties.className = ['footnotes-list'];
li.children = [{ li.children = [
type: 'text', {
value: getNodeText(li.children as ChildContent[]) type: 'text',
}] value: getNodeText(li.children as ChildContent[]),
},
];
} }
return li; return li;
}) });
} }
return item; return item;
}) });
} };
export const footnotesLabel = (node: Element) => { export const footnotesLabel = (node: Element) => {
const label = getNodeText(node.children as ChildContent[]); const label = getNodeText(node.children as ChildContent[]);
node.children = [{ node.children = [
type: 'text', {
value: `[${label}]` 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'];
}
};

View File

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