├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── apps └── weixin-md │ ├── .eslintrc.js │ ├── README.md │ ├── components │ ├── Editor │ │ ├── Editor.tsx │ │ └── index.ts │ ├── Footer │ │ ├── Footer.tsx │ │ └── index.ts │ ├── Header │ │ ├── Header.tsx │ │ └── index.ts │ ├── RednoteEditor │ │ ├── RednoteEditor.tsx │ │ └── index.ts │ ├── RednoteHeader │ │ ├── RednoteHeader.tsx │ │ └── index.ts │ └── Toast │ │ ├── Toast.tsx │ │ └── index.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── index.tsx │ └── rednote.tsx │ ├── postcss.config.js │ ├── public │ └── favicon.png │ ├── styles │ └── globals.css │ ├── tailwind.config.js │ ├── tsconfig.json │ └── utils │ ├── clipboard.ts │ └── export.ts ├── package.json ├── packages ├── config │ ├── package.json │ ├── postcss.config.js │ └── tailwind.config.js ├── eslint-config-custom │ ├── index.js │ └── package.json ├── md-converter │ ├── __tests__ │ │ ├── renderer.test.ts │ │ └── utils.test.ts │ ├── index.ts │ ├── package.json │ ├── renderer │ │ ├── BaseRenderer │ │ │ ├── BaseRenderer.ts │ │ │ └── index.ts │ │ ├── RednoteRenderer │ │ │ ├── RednoteRenderer.ts │ │ │ ├── converters │ │ │ │ ├── code.ts │ │ │ │ ├── codespan.ts │ │ │ │ ├── em.ts │ │ │ │ ├── heading.ts │ │ │ │ ├── hr.ts │ │ │ │ ├── index.ts │ │ │ │ ├── link.ts │ │ │ │ ├── list.ts │ │ │ │ ├── listItem.ts │ │ │ │ ├── paragraph.ts │ │ │ │ └── strong.ts │ │ │ └── index.ts │ │ ├── WXRenderer │ │ │ ├── WXRenderer.ts │ │ │ ├── converters │ │ │ │ ├── code.ts │ │ │ │ ├── codespan.ts │ │ │ │ ├── em.ts │ │ │ │ ├── heading.ts │ │ │ │ ├── hr.ts │ │ │ │ ├── index.ts │ │ │ │ ├── link.ts │ │ │ │ ├── list.ts │ │ │ │ ├── listItem.ts │ │ │ │ ├── paragraph.ts │ │ │ │ ├── quote.ts │ │ │ │ └── strong.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── themes │ │ ├── default.ts │ │ ├── index.ts │ │ ├── rednote.ts │ │ └── types.ts │ ├── tsconfig.json │ ├── types │ │ ├── Converter.ts │ │ ├── MarkdownElement.ts │ │ └── index.ts │ └── utils │ │ ├── index.ts │ │ └── styles.ts ├── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── index.tsx │ ├── md-editor │ ├── editor.tsx │ └── index.ts │ ├── package.json │ ├── preview │ ├── index.ts │ └── preview.tsx │ ├── rednote-preview │ ├── index.ts │ ├── preview.css │ └── preview.tsx │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*/"], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | timeout-minutes: 15 13 | runs-on: ubuntu-latest 14 | # To use Remote Caching, uncomment the next lines and follow the steps below. 15 | # env: 16 | # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 17 | # TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 2 24 | 25 | - uses: pnpm/action-setup@v2.0.1 26 | with: 27 | version: 9.15.1 28 | 29 | - name: Setup Node.js environment 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: 22 33 | cache: 'pnpm' 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Lint 39 | run: pnpm lint 40 | 41 | - name: Build 42 | run: pnpm build 43 | 44 | - name: Test 45 | run: pnpm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 yeshu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 公众号排版工具 2 | 3 | ![ci](https://github.com/xdlrt/mds/actions/workflows/ci.yml/badge.svg) 4 | 5 | 一款高度简洁的微信公众号 Markdown 编辑器! 🎉 6 | 7 | 在线体验:https://markdowns.yeshu.cloud 8 | 9 | ## 功能介绍 10 | 11 | 在尽可能简单的功能下将 markdown 内容转换为可直接发表的公众号格式。 12 | 13 | 当前能力: 14 | 15 | - [x] 常见 markdown 元素的转换 16 | - [x] 一键复制到剪贴板 17 | - [x] 实时转换渲染 18 | -------------------------------------------------------------------------------- /apps/weixin-md/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/weixin-md/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 12 | 13 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 14 | 15 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { MarkdownEditor, Preview } from "ui"; 3 | import { defaultTheme, WXRenderer } from "md-converter"; 4 | import { marked } from "marked"; 5 | 6 | export const Editor = () => { 7 | const renderer = new WXRenderer({ theme: defaultTheme }); 8 | const output = renderer.assemble(); 9 | marked.use({ renderer: output }); 10 | const [preview, setPreview] = useState(""); 11 | const handleChange = (value: string) => { 12 | const content = marked.parse(value); 13 | const suffix = renderer.buildSuffix(); 14 | setPreview(content + suffix); 15 | }; 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Editor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Editor"; 2 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | export const Footer = () => { 2 | return ( 3 | 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Footer"; 2 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { copy } from "../../utils/clipboard"; 2 | import { exportHTML } from "../../utils/export"; 3 | import { toast } from "../Toast"; 4 | 5 | export const Header = () => { 6 | const handleCopy = () => { 7 | let previewDOM = document.getElementById(`preview`); 8 | if (!previewDOM) return toast.error("现在没什么可以复制的"); 9 | copy(exportHTML(previewDOM)); 10 | toast.success("内容已复制,可到公众号后台粘贴"); 11 | }; 12 | 13 | return ( 14 |
15 |
16 |
17 |
18 |

19 | 公众号排版工具 20 |

21 |

22 | {`一款高度简洁的微信公众号 Markdown 编辑器! 🎉`} 23 |

24 |
25 |
26 | 33 | 39 | Github 40 | 48 | 53 | 54 | 55 |
56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Header"; 2 | -------------------------------------------------------------------------------- /apps/weixin-md/components/RednoteEditor/RednoteEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { MarkdownEditor, RednotePreview } from "ui"; 3 | import { RednoteRenderer, rednoteTheme } from "md-converter"; 4 | import { marked } from "marked"; 5 | 6 | export const RednoteEditor = () => { 7 | const renderer = new RednoteRenderer({ theme: rednoteTheme }); 8 | const output = renderer.assemble(); 9 | marked.use({ renderer: output }); 10 | const [previews, setPreviews] = useState([]); 11 | const handleChange = (value: string) => { 12 | const content = marked.parse(value); 13 | setPreviews(content.split("
")); 14 | }; 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/weixin-md/components/RednoteEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RednoteEditor"; 2 | -------------------------------------------------------------------------------- /apps/weixin-md/components/RednoteHeader/RednoteHeader.tsx: -------------------------------------------------------------------------------- 1 | import html2canvas from "html2canvas"; 2 | 3 | export const RednoteHeader = () => { 4 | const handleDownloadImages = async () => { 5 | const previewElements = document.querySelectorAll(".preview-card"); 6 | 7 | for (let i = 0; i < previewElements.length; i++) { 8 | const element = previewElements[i] as HTMLElement; 9 | 10 | const canvas = await html2canvas(element, { 11 | scale: 3, 12 | }); 13 | 14 | // document.body.appendChild(canvas); 15 | const link = document.createElement("a"); 16 | link.download = `preview-${i + 1}.png`; 17 | link.href = canvas.toDataURL(); 18 | link.click(); 19 | } 20 | }; 21 | 22 | return ( 23 |
24 |
25 |
26 |
27 |

28 | RedMark 29 |

30 |

31 | {`A highly concise Rednote Markdown converter! 🎉`} 32 |

33 |
34 |
35 | 42 | 48 | Github 49 | 56 | 62 | 63 | 64 |
65 |
66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /apps/weixin-md/components/RednoteHeader/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RednoteHeader"; 2 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Toast/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { ToastContainer, toast as toastify } from "react-toastify"; 2 | import "react-toastify/dist/ReactToastify.css"; 3 | 4 | export const ToastRoot = () => { 5 | return ( 6 | 17 | ); 18 | }; 19 | 20 | export const toast = toastify; 21 | -------------------------------------------------------------------------------- /apps/weixin-md/components/Toast/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Toast"; 2 | -------------------------------------------------------------------------------- /apps/weixin-md/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/weixin-md/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | experimental: { 4 | transpilePackages: ["ui"], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/weixin-md/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weixin-md", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "html2canvas": "^1.4.1", 13 | "marked": "4.2.3", 14 | "md-converter": "workspace:^", 15 | "next": "13.0.6", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "react-toastify": "^9.1.3", 19 | "ui": "workspace:*" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.22.15", 23 | "@types/marked": "^4.3.1", 24 | "@types/node": "18.11.10", 25 | "@types/react": "^18.2.21", 26 | "@types/react-dom": "^18.2.7", 27 | "config": "workspace:*", 28 | "eslint": "8.28.0", 29 | "eslint-config-custom": "workspace:*", 30 | "tsconfig": "workspace:*", 31 | "typescript": "^5.7.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/weixin-md/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { AppProps } from "next/app"; 3 | import "../styles/globals.css"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ; 7 | } 8 | 9 | export default MyApp; 10 | -------------------------------------------------------------------------------- /apps/weixin-md/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | import { Editor } from "../components/Editor"; 4 | import { Footer } from "../components/Footer"; 5 | import { Header } from "../components/Header"; 6 | import { ToastRoot } from "../components/Toast"; 7 | import Script from "next/script"; 8 | 9 | export default function Web() { 10 | return ( 11 |
12 | 13 | 公众号排版工具 | 一颗小树 14 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/weixin-md/pages/rednote.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | import { Footer } from "../components/Footer"; 4 | import { ToastRoot } from "../components/Toast"; 5 | import Script from "next/script"; 6 | import { RednoteEditor } from "../components/RednoteEditor"; 7 | import { RednoteHeader } from "../components/RednoteHeader"; 8 | 9 | export default function RedNote() { 10 | return ( 11 |
12 | 13 | RedMark 14 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 | 28 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/weixin-md/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("config/postcss.config"); 2 | -------------------------------------------------------------------------------- /apps/weixin-md/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdlrt/mds/8c3e5dc66f5a2582b3eb0c73424149b818ffebfc/apps/weixin-md/public/favicon.png -------------------------------------------------------------------------------- /apps/weixin-md/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* fix text shifting issue with https://github.com/niklasvh/html2canvas/issues/2775 */ 6 | @layer base { 7 | img { 8 | @apply inline-block; 9 | } 10 | } 11 | 12 | .md-editor { 13 | height: 100% !important; 14 | } 15 | 16 | .md-editor .cm-editor { 17 | height: 100% !important; 18 | } 19 | 20 | .md-editor .cm-editor.cm-focused { 21 | outline: none; 22 | } 23 | 24 | .md-editor .cm-scroller { 25 | display: block!important; 26 | overflow-x: hidden; 27 | padding: 20px; 28 | } 29 | 30 | .md-editor .cm-scroller::-webkit-scrollbar { 31 | display: none; 32 | } 33 | 34 | .md-editor .cm-line { 35 | word-wrap: break-word; 36 | white-space: pre-wrap; 37 | word-break: normal; 38 | } 39 | 40 | .preview-wrap::-webkit-scrollbar { 41 | display: none; 42 | } 43 | -------------------------------------------------------------------------------- /apps/weixin-md/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("config/tailwind.config"); 2 | -------------------------------------------------------------------------------- /apps/weixin-md/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /apps/weixin-md/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | export const copy = (text: string) => { 2 | let input = document.getElementById("copy-input") as HTMLInputElement; 3 | if (!input) { 4 | // input 必须在页面内存在。 5 | input = document.createElement("input"); 6 | input.id = "copy-input"; 7 | input.style.position = "absolute"; 8 | input.style.left = "-1000px"; 9 | input.style.zIndex = "-1000"; 10 | document.body.appendChild(input); 11 | } 12 | 13 | // 让 input 选中一个字符 14 | input.value = "NOTHING"; 15 | input.setSelectionRange(0, 1); 16 | input.focus(); 17 | 18 | const copyHandler = (e: ClipboardEvent) => { 19 | e.preventDefault(); 20 | if (e.clipboardData) { 21 | e.clipboardData.setData("text/html", text); 22 | e.clipboardData.setData("text/plain", text); 23 | } 24 | document.removeEventListener("copy", copyHandler); 25 | }; 26 | 27 | document.addEventListener("copy", copyHandler); 28 | document.execCommand("copy"); 29 | }; 30 | -------------------------------------------------------------------------------- /apps/weixin-md/utils/export.ts: -------------------------------------------------------------------------------- 1 | // 判断是否是包裹代码块的 pre 元素 2 | function isPre(element: HTMLElement) { 3 | return ( 4 | element.tagName === "PRE" && 5 | Array.from(element.classList).includes("code__pre") 6 | ); 7 | } 8 | 9 | // 判断是否是包裹代码块的 code 元素 10 | function isCode(element: HTMLElement) { 11 | return ( 12 | element.tagName === "CODE" && 13 | Array.from(element.classList).includes("prettyprint") 14 | ); 15 | } 16 | 17 | // 判断是否是包裹代码字符的 span 元素 18 | function isSpan(element: HTMLElement) { 19 | return ( 20 | element.tagName === "SPAN" && 21 | (isCode(element.parentElement as HTMLElement) || 22 | isCode( 23 | (element.parentElement as HTMLElement).parentElement as HTMLElement, 24 | ) || 25 | isCode( 26 | (element.parentElement as HTMLElement).parentElement 27 | ?.parentElement as HTMLElement, 28 | )) 29 | ); 30 | } 31 | 32 | function setStyles(element: HTMLElement) { 33 | function getElementStyles( 34 | element: HTMLElement, 35 | includes = ["color"], 36 | excludes = ["width", "height"], 37 | ) { 38 | const styles = getComputedStyle(element, null); 39 | return Object.entries(styles) 40 | .filter( 41 | ([key]) => 42 | styles.getPropertyValue(key) && 43 | includes.includes(key) && 44 | !excludes.includes(key), 45 | ) 46 | .map(([key, value]) => `${key}:${value};`) 47 | .join(""); 48 | } 49 | 50 | switch (true) { 51 | case isPre(element): 52 | case isCode(element): 53 | case isSpan(element): 54 | const existingStyles = element.getAttribute("style") || ""; 55 | const newStyles = getElementStyles(element); 56 | const combinedStyles = `${existingStyles}${ 57 | existingStyles && newStyles ? ";" : "" 58 | }${newStyles}`; 59 | element.setAttribute("style", combinedStyles); 60 | default: 61 | // do nothing 62 | } 63 | if (element.children.length) { 64 | Array.from(element.children).forEach((child) => 65 | setStyles(child as HTMLElement), 66 | ); 67 | } 68 | } 69 | 70 | export function exportHTML(element: HTMLElement) { 71 | setStyles(element); 72 | const htmlStr = element.innerHTML; 73 | return htmlStr; 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mds", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev --parallel", 12 | "lint": "turbo run lint", 13 | "test": "turbo run test", 14 | "prepare": "husky install", 15 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 16 | }, 17 | "lint-staged": { 18 | "apps/**/*.{js,ts,jsx,tsx}": [ 19 | "pnpm run format" 20 | ], 21 | "packages/**/*.{js,ts,jsx,tsx}": [ 22 | "pnpm run format" 23 | ], 24 | "*.json": [ 25 | "pnpm run format" 26 | ] 27 | }, 28 | "devDependencies": { 29 | "autoprefixer": "^10.4.14", 30 | "eslint-config-custom": "workspace:*", 31 | "husky": "^8.0.3", 32 | "lint-staged": "^13.2.3", 33 | "postcss": "^8.4.27", 34 | "prettier": "latest", 35 | "tailwindcss": "^3.3.3", 36 | "turbo": "^2.3.3" 37 | }, 38 | "engines": { 39 | "node": ">=18.0.0" 40 | }, 41 | "packageManager": "pnpm@8.6.10" 42 | } 43 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "postcss.config.js", 7 | "tailwind.config.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/config/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/config/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["../../packages/ui/**/*.{ts,tsx}", "./**/*.{ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "turbo", "prettier"], 3 | rules: { 4 | "@next/next/no-html-link-for-pages": "off", 5 | "react/jsx-key": "off", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eslint": "8.28.0", 8 | "eslint-config-next": "13.0.6", 9 | "eslint-config-prettier": "^8.10.0", 10 | "eslint-config-turbo": "latest", 11 | "eslint-plugin-react": "7.31.11" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.7.3" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/md-converter/__tests__/renderer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "vitest"; 2 | import { marked } from "marked"; 3 | import { BaseRenderer } from "../renderer/BaseRenderer"; 4 | import { WXRenderer } from "../renderer/WXRenderer"; 5 | import { defaultTheme } from "../themes"; 6 | 7 | describe("BaseRenderer check with default theme", () => { 8 | const renderer = new BaseRenderer({ theme: defaultTheme }); 9 | 10 | test("setTheme", () => { 11 | renderer.setTheme({ ...defaultTheme, hr: { test: "xxx" } }); 12 | renderer.theme.hr.test = "xxx"; 13 | }); 14 | 15 | test("assemble", () => { 16 | try { 17 | renderer.assemble(); 18 | expect(false).toEqual(true); 19 | } catch (error) { 20 | expect(false).toEqual(false); 21 | } 22 | }); 23 | }); 24 | 25 | describe("WXRenderer check with default theme", () => { 26 | const renderer = new WXRenderer({ theme: defaultTheme }); 27 | const output = renderer.assemble(); 28 | marked.use({ renderer: output }); 29 | 30 | test("valid converter number", () => { 31 | expect(Object.keys(output).length).toEqual(11); 32 | }); 33 | 34 | test("em", () => { 35 | const text = marked.parse("*xxx*"); 36 | expect(text).toMatch(""); 39 | }); 40 | 41 | test("h1", () => { 42 | const text = marked.parse("# heading1"); 43 | expect(text).toMatch(""); 46 | }); 47 | 48 | test("h2", () => { 49 | const text = marked.parse("## heading2"); 50 | expect(text).toMatch(""); 53 | }); 54 | 55 | test("h3", () => { 56 | const text = marked.parse("### heading3"); 57 | expect(text).toMatch(""); 60 | }); 61 | 62 | test("h4", () => { 63 | const text = marked.parse("#### heading4"); 64 | expect(text).toMatch(""); 67 | }); 68 | 69 | test("hr", () => { 70 | const text = marked.parse("---"); 71 | expect(text).toMatch(" { 75 | const renderer = new WXRenderer({ theme: defaultTheme }); 76 | const output = renderer.assemble(); 77 | marked.use({ renderer: output }); 78 | 79 | const weixinLink = marked.parse( 80 | "[一颗小树 #27 找到并坚持自己的热爱](https://mp.weixin.qq.com/s/-tF20PdAdMuqXakuBt7_wQ)", 81 | ); 82 | expect(weixinLink).toMatch(""); 84 | expect(weixinLink).toMatch("title"); 85 | expect(weixinLink).toMatch("一颗小树 #27 找到并坚持自己的热爱"); 86 | expect(weixinLink).toMatch( 87 | "https://mp.weixin.qq.com/s/-tF20PdAdMuqXakuBt7_wQ", 88 | ); 89 | expect(renderer.buildSuffix()).toEqual(""); 90 | 91 | const weixinLink2 = marked.parse( 92 | "[一颗小树 #27 找到并坚持自己的热爱](http://mp.weixin.qq.com/s/-tF20PdAdMuqXakuBt7_wQ)", 93 | ); 94 | expect(weixinLink2).toMatch( 95 | "http://mp.weixin.qq.com/s/-tF20PdAdMuqXakuBt7_wQ", 96 | ); 97 | 98 | const link = marked.parse( 99 | "[一颗小树 - 竹白](https://xiaoshu.zhubai.love)xxx[一颗小树 - 竹白](https://xiaoshu.zhubai.love)", 100 | ); 101 | expect(link).toMatch(""); 103 | expect(link).toMatch(""); 104 | expect(link).toMatch(""); 105 | expect(link).toMatch("一颗小树 - 竹白"); 106 | expect(renderer.buildSuffix()).toMatch(""); 112 | }); 113 | 114 | test("list - ordered", () => { 115 | const text = marked.parse("1. xxx\n\n2. yyy\n\n3. zzz"); 116 | expect(text).toMatch(""); 118 | expect(text).toMatch(""); 119 | expect(text).toMatch("xxx"); 120 | expect(text).toMatch("yyy"); 121 | expect(text).toMatch("zzz"); 122 | }); 123 | 124 | test("listItem", () => { 125 | const text = marked.parse("- xxx\n\n- yyy\n\n- zzz"); 126 | expect(text).toMatch(""); 128 | expect(text).toMatch(""); 129 | expect(text).toMatch("xxx"); 130 | expect(text).toMatch("yyy"); 131 | expect(text).toMatch("zzz"); 132 | }); 133 | 134 | test("paragraph", () => { 135 | const text = marked.parse("xxxyyyzzz"); 136 | expect(text).toMatch(""); 138 | }); 139 | 140 | test("quote", () => { 141 | const text = marked.parse("> xxx"); 142 | expect(text).toMatch(""); 144 | expect(text).toMatch(""); 146 | expect(text).toMatch("xxx"); 147 | }); 148 | 149 | test("strong", () => { 150 | const text = marked.parse("**xxx**"); 151 | expect(text).toMatch(""); 153 | expect(text).toMatch(""); 155 | expect(text).toMatch("xxx"); 156 | }); 157 | 158 | test("code", () => { 159 | const text = marked.parse("```typescript\nconst test = 1;```"); 160 | expect(text).toMatch(""); 162 | expect(text).toMatch(" { 5 | test("makeStyleText", () => { 6 | // @ts-ignore 7 | expect(makeStyleText()).toEqual(""); 8 | expect(makeStyleText({})).toEqual(""); 9 | expect( 10 | makeStyleText({ 11 | "font-style": "italic", 12 | "font-size": `15px`, 13 | "line-height": `1.75`, 14 | }), 15 | ).toEqual("font-style:italic;font-size:15px;line-height:1.75"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/md-converter/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./renderer"; 2 | export * from "./themes"; 3 | -------------------------------------------------------------------------------- /packages/md-converter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "md-converter", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "scripts": { 8 | "lint": "eslint *.ts*", 9 | "test:ui": "vitest --ui", 10 | "test": "vitest --run", 11 | "test:coverage": "vitest run --coverage" 12 | }, 13 | "devDependencies": { 14 | "@types/marked": "^4.3.1", 15 | "@vitest/coverage-c8": "^0.31.4", 16 | "@vitest/ui": "^0.31.4", 17 | "eslint": "8.28.0", 18 | "eslint-config-custom": "workspace:*", 19 | "tsconfig": "workspace:*", 20 | "typescript": "^5.7.3", 21 | "vitest": "^0.31.4" 22 | }, 23 | "dependencies": { 24 | "highlight.js": "^11.8.0", 25 | "marked": "^4.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/BaseRenderer/BaseRenderer.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "marked"; 2 | import { Theme } from "../../themes"; 3 | 4 | export class BaseRenderer { 5 | theme: Theme; 6 | 7 | constructor({ theme }: { theme: Theme }) { 8 | this.theme = theme; 9 | } 10 | 11 | setTheme(theme: Theme) { 12 | this.theme = theme; 13 | } 14 | 15 | assemble(): Partial { 16 | throw new Error("assemble function is not implement!"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/BaseRenderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BaseRenderer"; 2 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/RednoteRenderer.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "marked"; 2 | import { BaseRenderer } from "../BaseRenderer"; 3 | import { codeConverterFactory } from "./converters/code"; 4 | import { codespanConverterFactory } from "./converters/codespan"; 5 | import { emConverterFactory } from "./converters/em"; 6 | import { headingConverterFactory } from "./converters/heading"; 7 | import { hrConverterFactory } from "./converters/hr"; 8 | import { linkConverterFactory } from "./converters/link"; 9 | import { listConverterFactory } from "./converters/list"; 10 | import { listItemConverterFactory } from "./converters/listItem"; 11 | import { paragraphConverterFactory } from "./converters/paragraph"; 12 | import { strongConverterFactory } from "./converters"; 13 | 14 | export class RednoteRenderer extends BaseRenderer { 15 | assemble(): Partial { 16 | return { 17 | em: emConverterFactory(this.theme), 18 | heading: headingConverterFactory(this.theme), 19 | hr: hrConverterFactory(this.theme), 20 | link: linkConverterFactory(this.theme, {}), 21 | list: listConverterFactory(this.theme), 22 | listitem: listItemConverterFactory(this.theme), 23 | paragraph: paragraphConverterFactory(this.theme), 24 | code: codeConverterFactory(this.theme), 25 | codespan: codespanConverterFactory(this.theme), 26 | strong: strongConverterFactory(this.theme), 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/code.ts: -------------------------------------------------------------------------------- 1 | import hljs from "highlight.js"; 2 | import "highlight.js/styles/github.css"; 3 | import { Theme } from "../../../themes"; 4 | import { MarkdownElement, ConverterFunc } from "../../../types"; 5 | import { makeStyleText } from "../../../utils"; 6 | 7 | export const codeConverter: ConverterFunc = ( 8 | styles: Theme, 9 | text: string, 10 | lang: string, 11 | ) => { 12 | lang = hljs.getLanguage(lang) ? lang : "plaintext"; 13 | 14 | text = hljs.highlight(text, { language: lang }).value; 15 | 16 | text = text 17 | .replace(/\r\n/g, "
") 18 | .replace(/\n/g, "
") 19 | .replace(/(>[^<]+)|(^[^<]+)/g, (str) => { 20 | return str.replace(/\s/g, " "); 21 | }); 22 | 23 | return `
${text}
`; 28 | }; 29 | 30 | export const codeConverterFactory = (styles: Theme) => { 31 | return (text: string, lang: string) => codeConverter(styles, text, lang); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/codespan.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const codespanConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `${text}`; 12 | }; 13 | 14 | export const codespanConverterFactory = (styles: Theme) => { 15 | return (text: string) => codespanConverter(styles, text); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/em.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const emConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `${text}`; 12 | }; 13 | 14 | export const emConverterFactory = (styles: Theme) => { 15 | return (text: string) => emConverter(styles, text); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/heading.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const headingConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | level: number, 9 | ) => { 10 | const tag = 11 | `h${Math.min(Math.max(level, 1), 6)}` as keyof (typeof styles)[MarkdownElement.Heading]; 12 | return `<${tag} style="${makeStyleText( 13 | styles[MarkdownElement.Heading][tag], 14 | )}">${text}`; 15 | }; 16 | 17 | export const headingConverterFactory = (styles: Theme) => { 18 | return (text: string, level: number) => headingConverter(styles, text, level); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/hr.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | 4 | export const hrConverter: ConverterFunc = ( 5 | styles: Theme, 6 | ) => { 7 | return `
`; 8 | }; 9 | 10 | export const hrConverterFactory = (styles: Theme) => { 11 | return () => hrConverter(styles); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./code"; 2 | export * from "./codespan"; 3 | export * from "./em"; 4 | export * from "./heading"; 5 | export * from "./hr"; 6 | export * from "./link"; 7 | export * from "./list"; 8 | export * from "./listItem"; 9 | export * from "./paragraph"; 10 | export * from "./strong"; 11 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/link.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { 3 | MarkdownElement, 4 | ConverterFunc, 5 | LinkConverterOptions, 6 | } from "../../../types"; 7 | import { makeStyleText } from "../../../utils"; 8 | 9 | export const linkConverter: ConverterFunc = ( 10 | styles: Theme, 11 | options: LinkConverterOptions, 12 | href: string, 13 | title: string, 14 | text: string, 15 | ) => { 16 | const titleAttr = title ? ` title="${title}"` : ""; 17 | return `${text}`; 20 | }; 21 | 22 | export const linkConverterFactory = ( 23 | styles: Theme, 24 | options: LinkConverterOptions, 25 | ) => { 26 | return (href: string, title: string, text: string) => 27 | linkConverter(styles, options, href, title, text); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/list.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const listConverter: ConverterFunc = ( 6 | styles: Theme, 7 | body: string, 8 | ordered: boolean, 9 | _start: number, 10 | ) => { 11 | body = body.replace(/<\/*p.*?>/g, ""); 12 | // Split on li tags and filter out empty strings 13 | let segments = body.split(/<\/?li[^>]*>/g).filter((s) => s.trim()); 14 | if (!ordered) { 15 | // Wrap each segment in li tags 16 | body = segments 17 | .map( 18 | (s) => 19 | `
  • 20 | 21 | ${s} 22 |
  • `, 23 | ) 24 | .join(""); 25 | return `
      ${body}
    `; 26 | } 27 | body = segments 28 | .map( 29 | (s, i) => 30 | `
  • 31 | ${i + 1}. 32 | ${s} 33 |
  • `, 34 | ) 35 | .join(""); 36 | return `
      ${body}
    `; 37 | }; 38 | 39 | export const listConverterFactory = (styles: Theme) => { 40 | return (body: string, ordered: boolean, start: number) => 41 | listConverter(styles, body, ordered, start); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/listItem.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const listItemConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | task: boolean, 9 | checked: boolean, 10 | ) => { 11 | let checkbox = ""; 12 | if (task) { 13 | checkbox = ` `; 14 | } 15 | return `
  • ${checkbox}${text}
  • `; 18 | }; 19 | 20 | export const listItemConverterFactory = (styles: Theme) => { 21 | return (text: string, task: boolean, checked: boolean) => 22 | listItemConverter(styles, text, task, checked); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/paragraph.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const paragraphConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `

    ${text}

    `; 12 | }; 13 | 14 | export const paragraphConverterFactory = (styles: Theme) => { 15 | return (text: string) => paragraphConverter(styles, text); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/converters/strong.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const strongConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `${text}`; 10 | }; 11 | 12 | export const strongConverterFactory = (styles: Theme) => { 13 | return (text: string) => strongConverter(styles, text); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/RednoteRenderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./RednoteRenderer"; 2 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/WXRenderer.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from "marked"; 2 | import { makeStyleText } from "../../utils"; 3 | import { BaseRenderer } from "../BaseRenderer"; 4 | import { 5 | EMConverterFactory, 6 | headingConverterFactory, 7 | HRConverterFactory, 8 | linkConverterFactory, 9 | listConverterFactory, 10 | listItemConverterFactory, 11 | paragraphConverterFactory, 12 | quoteConverterFactory, 13 | strongConverterFactory, 14 | codeConverterFactory, 15 | codeSpanConverterFactory, 16 | } from "./converters"; 17 | 18 | interface FootNote { 19 | title: string; 20 | href: string; 21 | } 22 | 23 | export class WXRenderer extends BaseRenderer { 24 | private footNotes: FootNote[] = []; 25 | 26 | private addFootNote = (title: string, href: string) => { 27 | const length = this.footNotes.push({ title, href }); 28 | return length; 29 | }; 30 | 31 | private buildFootNotes = () => { 32 | let notes = this.footNotes.map(({ title, href }, index) => { 33 | if (title === href) { 34 | return `[${ 35 | index + 1 36 | }]: ${title}
    `; 39 | } 40 | return `[${ 41 | index + 1 42 | }]${title}: ${href}
    `; 45 | }); 46 | if (notes.length === 0) { 47 | return ""; 48 | } 49 | return `

    相关引用

    ${notes.join("\n")}

    `; 54 | }; 55 | 56 | buildSuffix() { 57 | const suffix = ""; 58 | const footNotes = this.buildFootNotes(); 59 | return suffix + footNotes; 60 | } 61 | 62 | assemble(): Partial { 63 | return { 64 | em: EMConverterFactory(this.theme), 65 | heading: headingConverterFactory(this.theme), 66 | hr: HRConverterFactory(this.theme), 67 | link: linkConverterFactory(this.theme, { 68 | addFootNote: this.addFootNote, 69 | enableFootNote: true, 70 | }), 71 | list: listConverterFactory(this.theme), 72 | listitem: listItemConverterFactory(this.theme), 73 | paragraph: paragraphConverterFactory(this.theme), 74 | blockquote: quoteConverterFactory(this.theme), 75 | strong: strongConverterFactory(this.theme), 76 | code: codeConverterFactory(this.theme), 77 | codespan: codeSpanConverterFactory(this.theme), 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/code.ts: -------------------------------------------------------------------------------- 1 | import hljs from "highlight.js"; 2 | import "highlight.js/styles/github.css"; 3 | import { Theme } from "../../../themes"; 4 | import { MarkdownElement, ConverterFunc } from "../../../types"; 5 | import { makeStyleText } from "../../../utils"; 6 | 7 | export const codeConverter: ConverterFunc = ( 8 | styles: Theme, 9 | text: string, 10 | lang: string, 11 | ) => { 12 | lang = hljs.getLanguage(lang) ? lang : "plaintext"; 13 | 14 | text = hljs.highlight(text, { language: lang }).value; 15 | 16 | text = text 17 | .replace(/\r\n/g, "
    ") 18 | .replace(/\n/g, "
    ") 19 | .replace(/(>[^<]+)|(^[^<]+)/g, (str) => { 20 | return str.replace(/\s/g, " "); 21 | }); 22 | 23 | return `
    ${text}
    `; 28 | }; 29 | 30 | export const codeConverterFactory = (styles: Theme) => { 31 | return (text: string, lang: string) => codeConverter(styles, text, lang); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/codespan.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const codeSpanConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `${text}`; 10 | }; 11 | 12 | export const codeSpanConverterFactory = (styles: Theme) => { 13 | return (text: string) => codeSpanConverter(styles, text); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/em.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const EMConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `${text}`; 10 | }; 11 | 12 | export const EMConverterFactory = (styles: Theme) => { 13 | return (text: string) => EMConverter(styles, text); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/heading.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const headingConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | level: number, 9 | ) => { 10 | switch (level) { 11 | case 1: 12 | return `

    ${text}

    `; 13 | case 2: 14 | return `

    ${text}

    `; 15 | case 3: 16 | return `

    ${text}

    `; 17 | default: 18 | return `

    ${text}

    `; 19 | } 20 | }; 21 | 22 | export const headingConverterFactory = (styles: Theme) => { 23 | return (text: string, level: number) => headingConverter(styles, text, level); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/hr.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const HRConverter: ConverterFunc = ( 6 | styles: Theme, 7 | ) => { 8 | return `
    `; 9 | }; 10 | 11 | export const HRConverterFactory = (styles: Theme) => { 12 | return () => HRConverter(styles); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./em"; 2 | export * from "./heading"; 3 | export * from "./hr"; 4 | export * from "./link"; 5 | export * from "./list"; 6 | export * from "./listItem"; 7 | export * from "./paragraph"; 8 | export * from "./quote"; 9 | export * from "./strong"; 10 | export * from "./code"; 11 | export * from "./codespan"; 12 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/link.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { 3 | MarkdownElement, 4 | ConverterFunc, 5 | LinkConverterOptions, 6 | } from "../../../types"; 7 | import { makeStyleText } from "../../../utils"; 8 | 9 | export const linkConverter: ConverterFunc = ( 10 | styles: Theme, 11 | options: LinkConverterOptions, 12 | href: string, 13 | title: string, 14 | text: string, 15 | ) => { 16 | if (href.includes("mp.weixin.qq.com")) { 17 | return `${text}`; 20 | } 21 | if (href === text) { 22 | return text; 23 | } 24 | const { enableFootNote, addFootNote } = options; 25 | if (enableFootNote && addFootNote) { 26 | let index = addFootNote(title || text, href); 27 | return `${text}[${index}]`; 30 | } 31 | return `${text}`; 32 | }; 33 | 34 | export const linkConverterFactory = ( 35 | styles: Theme, 36 | options: LinkConverterOptions, 37 | ) => { 38 | return (href: string, title: string, text: string) => 39 | linkConverter(styles, options, href, title, text); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/list.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const listConverter: ConverterFunc = ( 6 | styles: Theme, 7 | body: string, 8 | ordered: boolean, 9 | _start: number, 10 | ) => { 11 | body = body.replace(/<\/*p.*?>/g, ""); 12 | let segments = body.split(`<%s/>`); 13 | if (!ordered) { 14 | body = segments.join("• "); 15 | return `
      ${body}
    `; 16 | } 17 | body = segments[0]; 18 | for (let i = 1; i < segments.length; i++) { 19 | body = body + i + ". " + segments[i]; 20 | } 21 | return `
      ${body}
    `; 22 | }; 23 | 24 | export const listConverterFactory = (styles: Theme) => { 25 | return (body: string, ordered: boolean, start: number) => 26 | listConverter(styles, body, ordered, start); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/listItem.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const listItemConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `
  • ${text}
  • `; 10 | }; 11 | 12 | export const listItemConverterFactory = (styles: Theme) => { 13 | return (text: string, task: boolean, checked: boolean) => 14 | listItemConverter(styles, text, task, checked); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/paragraph.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const paragraphConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | if (text.indexOf("${text}

    ` 14 | : ""; 15 | }; 16 | 17 | export const paragraphConverterFactory = (styles: Theme) => { 18 | return (text: string) => paragraphConverter(styles, text); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/quote.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const quoteConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | text = text.replace( 10 | //g, 11 | `

    `, 12 | ); 13 | return `

    ${text}
    `; 16 | }; 17 | 18 | export const quoteConverterFactory = (styles: Theme) => { 19 | return (text: string) => quoteConverter(styles, text); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/converters/strong.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "../../../themes"; 2 | import { MarkdownElement, ConverterFunc } from "../../../types"; 3 | import { makeStyleText } from "../../../utils"; 4 | 5 | export const strongConverter: ConverterFunc = ( 6 | styles: Theme, 7 | text: string, 8 | ) => { 9 | return `${text}`; 10 | }; 11 | 12 | export const strongConverterFactory = (styles: Theme) => { 13 | return (text: string) => strongConverter(styles, text); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/WXRenderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./WXRenderer"; 2 | -------------------------------------------------------------------------------- /packages/md-converter/renderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./WXRenderer"; 2 | export * from "./RednoteRenderer"; 3 | -------------------------------------------------------------------------------- /packages/md-converter/themes/default.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "./types"; 2 | 3 | export const defaultTheme: Theme = { 4 | heading: { 5 | // 一级标题样式 6 | h1: { 7 | "font-size": `1.2em`, 8 | "text-align": `center`, 9 | "font-weight": `bold`, 10 | display: `table`, 11 | margin: `2em auto 1em`, 12 | padding: `0 1em`, 13 | "border-bottom": `2px solid rgba(250, 81, 81, 1)`, 14 | color: `rgba(250, 81, 81, 1)`, 15 | }, 16 | // 二级标题样式 17 | h2: { 18 | "font-size": `1.2em`, 19 | "text-align": `left`, 20 | "font-weight": `bold`, 21 | margin: `2em auto 1em`, 22 | padding: `0 0.2em`, 23 | color: `rgba(250, 81, 81, 1)`, 24 | }, 25 | // 三级标题样式 26 | h3: { 27 | "font-weight": `bold`, 28 | "font-size": `1.1em`, 29 | margin: `1.5em 8px 0.75em`, 30 | "line-height": `1.2`, 31 | "padding-left": `8px`, 32 | "border-left": `3px solid rgba(250, 81, 81, 1)`, 33 | color: `#3f3f3f`, 34 | }, 35 | // 四级标题样式 36 | h4: { 37 | "font-weight": `bold`, 38 | "font-size": `1em`, 39 | "text-align": `left`, 40 | margin: `1em 8px 0.5em`, 41 | color: `rgba(250, 81, 81, 1)`, 42 | }, 43 | }, 44 | paragraph: { 45 | margin: `1em 8px`, 46 | "letter-spacing": `0.1em`, 47 | color: `#3f3f3f`, 48 | "font-size": `15px`, 49 | "line-height": `1.75`, 50 | }, 51 | blockquoteParagraph: { 52 | "letter-spacing": `0.1em`, 53 | color: `rgb(80, 80, 80)`, 54 | display: `block`, 55 | "font-size": `15px`, 56 | "line-height": `1.75`, 57 | }, 58 | list: { 59 | ol: { 60 | "margin-left": `0`, 61 | "padding-left": `2.2em`, 62 | "list-style": "decimal", 63 | color: `#3f3f3f`, 64 | }, 65 | ul: { 66 | "margin-left": `0`, 67 | "padding-left": `2em`, 68 | "list-style": `disc`, 69 | color: `#3f3f3f`, 70 | }, 71 | }, 72 | listItem: { 73 | margin: `0.2em 8px 0.2em 0`, 74 | color: `#3f3f3f`, 75 | "font-size": `15px`, 76 | "line-height": `1.75`, 77 | }, 78 | quote: { 79 | "font-style": `normal`, 80 | "border-left": `none`, 81 | padding: `0.5em 0.75em`, 82 | "border-radius": `8px`, 83 | color: `rgba(0,0,0,0.5)`, 84 | background: `#f7f7f7`, 85 | margin: `0`, 86 | }, 87 | hr: { 88 | "border-style": `solid`, 89 | "border-width": `1px 0 0`, 90 | "border-color": `rgba(0,0,0,0.1)`, 91 | "-webkit-transform-origin": `0 0`, 92 | "-webkit-transform": `scale(1, 0.5)`, 93 | "transform-origin": `0 0`, 94 | transform: `scale(1, 0.5)`, 95 | }, 96 | link: { 97 | color: `#576b95`, 98 | "font-size": `15px`, 99 | "line-height": `1.75`, 100 | }, 101 | // 字体加粗样式 102 | strong: { 103 | color: `rgba(250, 81, 81, 1)`, 104 | "font-weight": `bold`, 105 | "font-size": `15px`, 106 | "line-height": `1.75`, 107 | }, 108 | em: { 109 | "font-style": "italic", 110 | "font-size": `15px`, 111 | "line-height": `1.75`, 112 | }, 113 | footNotes: { 114 | container: { 115 | margin: `0.5em 8px`, 116 | "font-size": `80%`, 117 | color: `#3f3f3f`, 118 | }, 119 | item: { 120 | "word-break": "break-all", 121 | "font-style": "normal", 122 | }, 123 | }, 124 | code: { 125 | pre: { 126 | "font-size": `14px`, 127 | "overflow-x": `auto`, 128 | "border-radius": `8px`, 129 | padding: `1em`, 130 | "line-height": `1.5`, 131 | margin: `10px 8px`, 132 | }, 133 | code: { 134 | margin: `0px`, 135 | "white-space": `nowrap`, 136 | "font-family": `Menlo, Operator Mono, Consolas, Monaco, monospace`, 137 | }, 138 | }, 139 | codeSpan: { 140 | "font-size": `90%`, 141 | "white-space": `pre`, 142 | color: `#d14`, 143 | background: `rgba(27,31,35,.05)`, 144 | padding: `3px 5px`, 145 | "border-radius": `4px`, 146 | }, 147 | }; 148 | -------------------------------------------------------------------------------- /packages/md-converter/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./default"; 2 | export * from "./rednote"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /packages/md-converter/themes/rednote.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "./types"; 2 | 3 | export const rednoteTheme: Theme = { 4 | heading: { 5 | // 一级标题样式 6 | h1: { 7 | "font-size": `21px`, 8 | "text-align": `left`, 9 | "font-weight": `bold`, 10 | margin: `15px 0 12px`, 11 | color: `#494E5E`, 12 | "font-family": `-apple-system`, 13 | }, 14 | // 二级标题样式 15 | h2: { 16 | "font-size": `19px`, 17 | "line-height": `19px`, 18 | "text-align": `left`, 19 | "font-weight": `bold`, 20 | margin: `12px 0 8px`, 21 | padding: `0 6px`, 22 | "border-left": `4px solid #494E5E`, 23 | color: `#494E5E`, 24 | "font-family": `-apple-system`, 25 | }, 26 | // 三级标题样式 27 | h3: { 28 | "font-weight": `bold`, 29 | "font-size": `17px`, 30 | margin: `8px 0 4px`, 31 | color: `#494E5E`, 32 | }, 33 | // 四级标题样式 34 | h4: { 35 | "font-weight": `bold`, 36 | "font-size": `15px`, 37 | "text-align": `left`, 38 | color: `#494E5E`, 39 | }, 40 | }, 41 | paragraph: { 42 | margin: `6px 0`, 43 | "letter-spacing": `0.5px`, 44 | color: `#3f3f3f`, 45 | "font-size": `14px`, 46 | "line-height": `1.5`, 47 | }, 48 | blockquoteParagraph: { 49 | "letter-spacing": `0.5px`, 50 | color: `rgb(80, 80, 80)`, 51 | display: `block`, 52 | "font-size": `15px`, 53 | "line-height": `1.75`, 54 | }, 55 | list: { 56 | ol: { 57 | "list-style": "none", 58 | color: `#3f3f3f`, 59 | }, 60 | ul: { 61 | "list-style": "none", 62 | color: `#3f3f3f`, 63 | }, 64 | }, 65 | listItem: { 66 | margin: `3px 8px 3px 0`, 67 | "letter-spacing": `0.5px`, 68 | color: `#3f3f3f`, 69 | "font-size": `14px`, 70 | "line-height": `1.5`, 71 | display: `flex`, 72 | "align-items": `start`, 73 | }, 74 | listItemSymbol: { 75 | ol: { 76 | "margin-right": `4px`, 77 | "line-height": `1.5`, 78 | }, 79 | ul: { 80 | "margin-right": `4px`, 81 | "line-height": `1`, 82 | }, 83 | }, 84 | quote: { 85 | "font-style": `normal`, 86 | "border-left": `none`, 87 | padding: `0.5em 0.75em`, 88 | "border-radius": `8px`, 89 | color: `rgba(0,0,0,0.5)`, 90 | background: `#f7f7f7`, 91 | margin: `0`, 92 | }, 93 | hr: { 94 | "border-style": `solid`, 95 | "border-width": `1px 0 0`, 96 | "border-color": `rgba(0,0,0,0.1)`, 97 | "-webkit-transform-origin": `0 0`, 98 | "-webkit-transform": `scale(1, 0.5)`, 99 | "transform-origin": `0 0`, 100 | transform: `scale(1, 0.5)`, 101 | }, 102 | link: { 103 | color: `#576b95`, 104 | "font-size": `15px`, 105 | "line-height": `1.75`, 106 | }, 107 | // 字体加粗样式 108 | strong: { 109 | color: `#494E5E`, 110 | "font-weight": `bold`, 111 | "font-size": `14px`, 112 | "line-height": `1.5`, 113 | }, 114 | em: { 115 | "font-style": "italic", 116 | "font-size": `14px`, 117 | "line-height": `1.5`, 118 | }, 119 | code: { 120 | pre: { 121 | "font-size": `14px`, 122 | "overflow-x": `auto`, 123 | "border-radius": `8px`, 124 | padding: `1em`, 125 | "line-height": `1.5`, 126 | margin: `10px 8px`, 127 | }, 128 | code: { 129 | margin: `0px`, 130 | "white-space": `nowrap`, 131 | "font-family": `Menlo, Operator Mono, Consolas, Monaco, monospace`, 132 | }, 133 | }, 134 | codeSpan: { 135 | "font-size": `90%`, 136 | "white-space": `pre`, 137 | color: `#d14`, 138 | background: `rgba(27,31,35,.05)`, 139 | padding: `3px 5px`, 140 | "border-radius": `4px`, 141 | }, 142 | }; 143 | -------------------------------------------------------------------------------- /packages/md-converter/themes/types.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownElement } from "../types"; 2 | 3 | export type styleObject = { 4 | [key: string]: string; 5 | }; 6 | 7 | export interface Theme { 8 | [MarkdownElement.Heading]: { 9 | h1: styleObject; 10 | h2: styleObject; 11 | h3: styleObject; 12 | h4: styleObject; 13 | }; 14 | [MarkdownElement.Paragraph]: styleObject; 15 | [MarkdownElement.BlockquoteParagraph]: styleObject; 16 | [MarkdownElement.Quote]: styleObject; 17 | [MarkdownElement.List]: { 18 | ol: styleObject; 19 | ul: styleObject; 20 | }; 21 | [MarkdownElement.ListItem]: styleObject; 22 | [MarkdownElement.ListItemSymbol]?: { 23 | ol: styleObject; 24 | ul: styleObject; 25 | }; 26 | [MarkdownElement.Link]: styleObject; 27 | [MarkdownElement.Strong]: styleObject; 28 | [MarkdownElement.HR]: styleObject; 29 | [MarkdownElement.EM]: styleObject; 30 | [MarkdownElement.FootNotes]?: { 31 | container: styleObject; 32 | item: styleObject; 33 | }; 34 | [MarkdownElement.Code]: { 35 | pre: styleObject; 36 | code: styleObject; 37 | }; 38 | [MarkdownElement.CodeSpan]: styleObject; 39 | } 40 | -------------------------------------------------------------------------------- /packages/md-converter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/md-converter/types/Converter.ts: -------------------------------------------------------------------------------- 1 | // https://marked.js.org/using_pro#renderer 2 | import { MarkdownElement } from "."; 3 | import { Theme } from "../themes"; 4 | 5 | export interface LinkConverterOptions { 6 | addFootNote?: (title: string, href: string) => number; 7 | enableFootNote?: boolean; 8 | } 9 | 10 | export interface ConverterMap { 11 | [MarkdownElement.Heading]: ( 12 | styles: Theme, 13 | text: string, 14 | level: number, 15 | ) => string; 16 | [MarkdownElement.Paragraph]: (styles: Theme, text: string) => string; 17 | [MarkdownElement.Quote]: (styles: Theme, text: string) => string; 18 | [MarkdownElement.List]: ( 19 | styles: Theme, 20 | body: string, 21 | ordered: boolean, 22 | start: number, 23 | ) => string; 24 | [MarkdownElement.ListItem]: ( 25 | styles: Theme, 26 | text: string, 27 | task: boolean, 28 | checked: boolean, 29 | ) => string; 30 | [MarkdownElement.Link]: ( 31 | styles: Theme, 32 | options: LinkConverterOptions, 33 | href: string, 34 | title: string, 35 | text: string, 36 | ) => string; 37 | [MarkdownElement.HR]: (styles: Theme) => string; 38 | [MarkdownElement.Strong]: (styles: Theme, text: string) => string; 39 | [MarkdownElement.EM]: (styles: Theme, text: string) => string; 40 | [MarkdownElement.Code]: (styles: Theme, text: string, lang: string) => string; 41 | [MarkdownElement.CodeSpan]: (styles: Theme, text: string) => string; 42 | } 43 | 44 | export type ConverterType = keyof ConverterMap; 45 | 46 | export type ConverterFunc = ConverterMap[T]; 47 | -------------------------------------------------------------------------------- /packages/md-converter/types/MarkdownElement.ts: -------------------------------------------------------------------------------- 1 | export enum MarkdownElement { 2 | Heading = "heading", 3 | Paragraph = "paragraph", 4 | BlockquoteParagraph = "blockquoteParagraph", 5 | Quote = "quote", 6 | List = "list", 7 | ListItem = "listItem", 8 | ListItemSymbol = "listItemSymbol", 9 | Link = "link", 10 | Strong = "strong", 11 | EM = "em", // 斜体 12 | HR = "hr", // 水平分割线 13 | FootNotes = "footNotes", // 脚注 14 | Code = "code", 15 | CodeSpan = "codeSpan", 16 | } 17 | -------------------------------------------------------------------------------- /packages/md-converter/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./MarkdownElement"; 2 | export * from "./Converter"; 3 | -------------------------------------------------------------------------------- /packages/md-converter/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./styles"; 2 | -------------------------------------------------------------------------------- /packages/md-converter/utils/styles.ts: -------------------------------------------------------------------------------- 1 | import { styleObject } from "../themes"; 2 | 3 | export const makeStyleText = (styles?: styleObject) => { 4 | if (!styles) return ""; 5 | const arr = []; 6 | for (const key in styles) { 7 | arr.push(key + ":" + styles[key]); 8 | } 9 | return arr.join(";"); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./md-editor"; 2 | export * from "./preview"; 3 | export * from "./rednote-preview"; 4 | -------------------------------------------------------------------------------- /packages/ui/md-editor/editor.tsx: -------------------------------------------------------------------------------- 1 | import CodeMirror from "@uiw/react-codemirror"; 2 | import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; 3 | import { languages } from "@codemirror/language-data"; 4 | 5 | export interface MarkdownEditorProps { 6 | onChange?: (value: string) => void; 7 | } 8 | 9 | export const MarkdownEditor = (props: MarkdownEditorProps) => { 10 | const { onChange } = props; 11 | const handleChange = (value: string) => { 12 | onChange && onChange(value); 13 | }; 14 | return ( 15 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ui/md-editor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./editor"; 2 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.0.0", 4 | "main": "./index.tsx", 5 | "types": "./index.tsx", 6 | "license": "MIT", 7 | "scripts": { 8 | "lint": "eslint *.ts*" 9 | }, 10 | "devDependencies": { 11 | "@codemirror/view": "^6.17.1", 12 | "@types/react": "^18.2.21", 13 | "@types/react-dom": "^18.2.7", 14 | "eslint": "8.28.0", 15 | "eslint-config-custom": "workspace:*", 16 | "react": "^18.2.0", 17 | "react-dom": "18.2.0", 18 | "tsconfig": "workspace:*", 19 | "typescript": "^5.7.3" 20 | }, 21 | "dependencies": { 22 | "@codemirror/lang-markdown": "^6.2.0", 23 | "@codemirror/language-data": "^6.3.1", 24 | "@codemirror/state": "6.2.1", 25 | "@lezer/common": "^1.0.4", 26 | "@uiw/react-codemirror": "^4.21.13", 27 | "md-converter": "workspace:^" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/ui/preview/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./preview"; 2 | -------------------------------------------------------------------------------- /packages/ui/preview/preview.tsx: -------------------------------------------------------------------------------- 1 | export const Preview = ({ preview }: { preview: string }) => { 2 | if (!preview) 3 | return ( 4 |
    5 | 这里空空如也 6 |
    7 | ); 8 | return ( 9 |
    10 |
    11 |
    17 |
    18 |
    19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/ui/rednote-preview/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./preview"; 2 | -------------------------------------------------------------------------------- /packages/ui/rednote-preview/preview.css: -------------------------------------------------------------------------------- 1 | .preview-card h3 { 2 | position: relative; 3 | display: inline-block; 4 | width: fit-content; 5 | } 6 | 7 | .preview-card h3::before { 8 | content: ''; 9 | position: absolute; 10 | left: -3px; 11 | right: 0; 12 | top: 50%; 13 | height: 8px; 14 | background-color: #494E5E; 15 | opacity: 0.2; 16 | width: calc(100% + 6px); 17 | transform: translateY(-50%) skewX(-15deg); 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/rednote-preview/preview.tsx: -------------------------------------------------------------------------------- 1 | import "./preview.css"; 2 | 3 | export const RednotePreview = ({ previews }: { previews: string[] }) => { 4 | if (!previews.length) 5 | return ( 6 |
    7 | There is nothing here. 8 |
    9 | ); 10 | return ( 11 |
    12 | {previews.map((preview, i) => ( 13 |
    14 |
    15 |
    20 |
    28 | 29 | {i + 1} 30 | / 31 | {previews.length} 32 | 33 | {i === 0 && 右滑看全部} 34 |
    35 |
    36 |
    37 | ))} 38 |
    39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/react-library.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", ".next/**"] 7 | }, 8 | "lint": { 9 | "outputs": [] 10 | }, 11 | "dev": { 12 | "cache": false 13 | }, 14 | "test": { 15 | "outputs": [] 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------