├── .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 |  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 |
22 | {`一款高度简洁的微信公众号 Markdown 编辑器! 🎉`} 23 |
24 |31 | {`A highly concise Rednote Markdown converter! 🎉`} 32 |
33 |"); 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("
` 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"); 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 ``; 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${text}
= ( 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}${tag}>`; 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 ``; 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${text}
= ( 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} = ( 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 |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 |11 | 17 |18 |7 | There is nothing here. 8 |9 | ); 10 | return ( 11 |12 | {previews.map((preview, i) => ( 13 |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 | --------------------------------------------------------------------------------14 |37 | ))} 38 |15 | 20 |36 |28 | 29 | {i + 1} 30 | / 31 | {previews.length} 32 | 33 | {i === 0 && 右滑看全部} 34 |35 |