├── .ackrc ├── .gitignore ├── LICENSE ├── README.md ├── compile.ts ├── demo ├── app.tsx ├── index.html ├── index.scss ├── index.tsx ├── preferences-modal.tsx ├── public │ ├── icon.svg │ └── sample.md └── store.ts ├── deno.json ├── dev-notes.md ├── icon.iconbuilder ├── package.json ├── src ├── components │ ├── editor.tsx │ ├── layout.tsx │ ├── modals │ │ ├── about.tsx │ │ ├── index.tsx │ │ └── prompt.tsx │ └── toolbar.tsx ├── css │ ├── github-markdown.scss │ ├── highlightjs.scss │ └── index.scss ├── index.tsx ├── store.ts ├── sync-scroll.ts └── utils.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=lib 2 | --ignore-file=match:/^yarn\.lock$/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | lib/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Tyler Liu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MarkPlus 2 | 3 | icon 4 | 5 | A React markdown editor and previewer. 6 | 7 | ## Demos 8 | 9 | - [Demo for Markdown authors](https://markpluslabs.github.io/react-markplus/) 10 | - [Demo for React developers](https://markpluslabs.github.io/react-markplus-demo/) 11 | - [source code](https://github.com/markpluslabs/react-markplus-demo/blob/main/src/app.tsx) 12 | 13 | ## Installation 14 | 15 | ``` 16 | yarn add react-markplus 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```tsx 22 | import MarkPlus from "react-markplus"; 23 | 24 | ; 25 | ``` 26 | 27 | ## CSS 28 | 29 | You will need to import the following CSS: 30 | 31 | - "katex/dist/katex.css"; 32 | - "@fortawesome/fontawesome-free/css/all.css"; 33 | - "react-markplus/src/css/index.scss"; 34 | 35 | ## markdown 36 | 37 | Initial markdown text to load into the editor. 38 | 39 | ```tsx 40 | ; 41 | ``` 42 | 43 | Default value is `''`. 44 | 45 | ## onChange 46 | 47 | A callback function to be invoked automatically when markdown text changes. 48 | 49 | ```tsx 50 | { 52 | console.log("markdown text changed to:", markdown); 53 | }} 54 | />; 55 | ``` 56 | 57 | Default value is `() => {}`. 58 | 59 | ## ❌ onPreviewChange 60 | 61 | A callback function to be invoked automatidally when preview html changes. 62 | 63 | This has been **removed** from the library. Because you are supposed to generate 64 | preview using 65 | [markplus-engine](https://github.com/markpluslabs/markplus-engine). 66 | 67 | ## toolbar 68 | 69 | Show, hide or remove toolbar. 70 | 71 | ```tsx 72 | ; 73 | ``` 74 | 75 | 3 possible values: 76 | 77 | - `show`: show toolbar, show a gutter below toolbar. Click the gutter to hide 78 | toolbar. 79 | - `hide`: hide toolbar, show a gutter on top. Click the gutter to show toolbar. 80 | - `none`: no toolbar, no gutter. 81 | 82 | Default value: `show`. 83 | 84 | ## mode 85 | 86 | Display editor, preview or both. 87 | 88 | ```tsx 89 | ; 90 | ``` 91 | 92 | 3 possible values: 93 | 94 | - `both`: show both editor and preview 95 | - there is a vertical gutter between editor and preview, you may drag the 96 | gutter to adjust sizes of them. 97 | - `editor`: show editor only 98 | - `preview`: show preview only 99 | - Use this mode if you don't need any editing feature. 100 | - in this mode this library is a markdown renderer. 101 | 102 | Default value: `both`. 103 | 104 | ## theme 105 | 106 | Overall theme: light, dark or auto: 107 | 108 | ```tsx 109 | ; 110 | ``` 111 | 112 | 3 possible values: 113 | 114 | - `light`: light theme 115 | - `dark`: dark theme 116 | - `auto`: same as system theme 117 | 118 | Default value: `auto`. 119 | 120 | ## toolbarItems 121 | 122 | You may configure the toolbar freely. 123 | 124 | ```tsx 125 | ; 126 | ``` 127 | 128 | A toolbar item could be either a string or a `ReactElement`. For toolbar items 129 | included with library, you may just specify a string. For your own custom 130 | toolbar items, please specify a `ReactElement`. 131 | 132 | ## Included toolbar Items 133 | 134 | - `'about'` 135 | - show a modal about MarkPlus 136 | - `'|'` 137 | - a vertical separator 138 | - `'bold'` 139 | - make text bold 140 | - `'italic'` 141 | - make text italic 142 | - `'strikethrough'` 143 | - make text strokethrough 144 | - `'underline'` 145 | - make text underlined 146 | - `'mark'` 147 | - make text marked 148 | - `'emoji'` 149 | - show a modal to insert emojis 150 | - `'fontawesome'` 151 | - show a modal to insert fontawesome icons 152 | - `'quote'` 153 | - quote text 154 | - `'unordered-list'` 155 | - create unordered list item 156 | - `'ordered-list'` 157 | - create ordered list item 158 | - `'unchecked-list'` 159 | - create unchecked task list item 160 | - `'checked-list'` 161 | - create checked task list item 162 | - `'link'` 163 | - insert a link 164 | - `'image'` 165 | - insert a image 166 | - `'code'` 167 | - insert a code snippet 168 | - `'table'` 169 | - insert a table 170 | - `'math'` 171 | - insert some math formulas 172 | - `flowchart` 173 | - insert some flowcharts 174 | 175 | ## Default toolbar items 176 | 177 | ```tsx 178 | import { defaultToolbarItems } from "react-markplus"; 179 | ``` 180 | 181 | Its value is: 182 | 183 | ```tsx 184 | [ 185 | "about", 186 | "|", 187 | "bold", 188 | "italic", 189 | "strikethrough", 190 | "underline", 191 | "mark", 192 | "|", 193 | "emoji", 194 | "fontawesome", 195 | "|", 196 | "quote", 197 | "unordered-list", 198 | "ordered-list", 199 | "unchecked-list", 200 | "checked-list", 201 | "|", 202 | "link", 203 | "image", 204 | "code", 205 | "table", 206 | "|", 207 | "math", 208 | "flowchart", 209 | ]; 210 | ``` 211 | 212 | You may add or remote items from it to customize your own toolbar. 213 | 214 | ## Custom toolbar item 215 | 216 | Here is a sample to create and insert a custom toolbar item: 217 | 218 | ```tsx 219 | { 228 | console.log("Todo: display a preferences modal"); 229 | }} 230 | > 231 | , 232 | ]} 233 | />; 234 | ``` 235 | 236 | ### Known issue 237 | 238 | Custom toolbar item will freeze in React 19 dev mode. 239 | 240 | It works in production mode though. It also works with React 18. 241 | -------------------------------------------------------------------------------- /compile.ts: -------------------------------------------------------------------------------- 1 | import { run } from "shell-commands"; 2 | 3 | // deno requires local import paths to have the .ts extension 4 | // tsc requires local import paths to have the .js extension 5 | const main = async () => { 6 | const changes = await run("git status --porcelain"); 7 | if (changes.trim().length > 0) { 8 | console.log("Please commit your changes before running this script"); 9 | return; 10 | } 11 | await run( 12 | `find src -type f \\( -name "*.ts" -o -name "*.tsx" \\) -exec sed -E -i '' 's/from "(\\.\\.?\\/.*)\\.ts(x)?";/from "\\1.js";/g' {} +`, 13 | ); 14 | await run("yarn tsc"); 15 | await run("git stash && git stash clear"); 16 | }; 17 | main(); 18 | -------------------------------------------------------------------------------- /demo/app.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from "antd"; 2 | import localforage from "localforage"; 3 | import { autoRun } from "manate"; 4 | import { auto } from "manate/react"; 5 | import React, { useEffect, useRef } from "react"; 6 | import waitFor from "wait-for-async"; 7 | 8 | import MarkPlus, { defaultToolbarItems, MarkPlusRef } from "../src/index.tsx"; 9 | import PreferencesModal from "./preferences-modal.tsx"; 10 | import { Store } from "./store.ts"; 11 | 12 | const App = (props: { store: Store }) => { 13 | const { store } = props; 14 | const { preferences } = store; 15 | 16 | // `markPlusRef` here is just for demo purpose, we don't need it 17 | const markPlusRef = useRef(null); 18 | 19 | // load sample markdown 20 | const [markdown, setMarkdown] = React.useState(""); 21 | useEffect(() => { 22 | const loadSampleData = async () => { 23 | const r = await fetch("sample.md"); 24 | const text = await r.text(); 25 | setMarkdown(text); 26 | const store = markPlusRef.current?.getStore(); 27 | console.log("store via ref:", store); 28 | }; 29 | loadSampleData(); 30 | }, []); 31 | 32 | // load/save preferences 33 | useEffect(() => { 34 | let preferencesSaver: ReturnType; 35 | const main = async () => { 36 | const savedPreferences = await localforage.getItem( 37 | "markplus-preferences", 38 | ); 39 | if (savedPreferences) { 40 | Object.assign(preferences, JSON.parse(savedPreferences)); 41 | } 42 | // must be after loading, otherwise it will save the default preferences 43 | preferencesSaver = autoRun(() => { 44 | localforage.setItem( 45 | "markplus-preferences", 46 | JSON.stringify(preferences), 47 | ); 48 | }); 49 | preferencesSaver.start(); 50 | }; 51 | main(); 52 | return () => { 53 | if (preferencesSaver) { 54 | preferencesSaver.stop(); 55 | } 56 | }; 57 | }, [preferences]); 58 | 59 | // scroll to hash 60 | useEffect(() => { 61 | const scrollToHash = async () => { 62 | if (location.hash.length === 0) { 63 | return; 64 | } 65 | const r = await waitFor({ 66 | interval: 100, 67 | times: 30, 68 | condition: () => document.querySelector(location.hash) !== null, 69 | }); 70 | if (!r) { 71 | return; 72 | } 73 | const linkElement = document.querySelector(location.hash)!; 74 | const rightPanel = document.querySelector(".right-panel")!; 75 | rightPanel.scrollTop = linkElement.offsetTop; 76 | }; 77 | scrollToHash(); 78 | }, []); 79 | 80 | // open preferences modal with cmd + , 81 | useEffect(() => { 82 | const keyUpListener = (event: KeyboardEvent) => { 83 | if (event.metaKey && event.key === ",") { 84 | event.preventDefault(); 85 | document.querySelector(".toolbar .fa-cog")?.click(); 86 | } 87 | }; 88 | addEventListener("keydown", keyUpListener); 89 | return () => { 90 | removeEventListener("keydown", keyUpListener); 91 | }; 92 | }, []); 93 | 94 | return ( 95 | <> 96 | (store.preferencesModalOpen = true)} 110 | // > 111 | // , 112 | ]} 113 | /> 114 | 121 | 122 | 123 | 124 | ); 125 | }; 126 | 127 | export default auto(App); 128 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MarkPlus 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/index.scss: -------------------------------------------------------------------------------- 1 | @use "katex/dist/katex.css"; 2 | @use "@fortawesome/fontawesome-free/css/all.css"; 3 | @use "../src/css/index.scss"; 4 | 5 | #root { 6 | position: fixed; 7 | left: 0; 8 | top: 0; 9 | width: 100vw; 10 | height: 100vh; 11 | } 12 | -------------------------------------------------------------------------------- /demo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "@ant-design/v5-patch-for-react-19"; 4 | 5 | import App from "./app.tsx"; 6 | import store from "./store.ts"; 7 | 8 | const root = createRoot(document.getElementById("root")!); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /demo/preferences-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Modal, Select } from "antd"; 2 | import { auto } from "manate/react"; 3 | import React from "react"; 4 | 5 | import { Store } from "./store.ts"; 6 | 7 | const PreferencesModal = (props: { store: Store }) => { 8 | const { store } = props; 9 | const { preferences } = store; 10 | return ( 11 | 15 | 22 | 23 | } 24 | onCancel={() => (store.preferencesModalOpen = false)} 25 | maskClosable={true} 26 | centered={true} 27 | > 28 |
29 |

30 | 31 |

32 |

MarkPlus Preferences

33 |
34 | 35 | (preferences.mode = value)} 54 | /> 55 | 56 | 57 | setEmojiValue(e.target.value)} 91 | placeholder="smile" 92 | onKeyUp={(e) => { 93 | if (e.key === "Enter") { 94 | handleEmojiOK(); 95 | } 96 | }} 97 | /> 98 |
99 | 100 |
101 | 102 | {/* font awesome modal */} 103 | modals.fontAwesome.close()} 106 | onOk={() => handleFaOK()} 107 | maskClosable={true} 108 | centered={true} 109 | afterOpenChange={(open) => { 110 | if (open) { 111 | faInput.current?.focus(); 112 | } 113 | }} 114 | > 115 |
116 |

117 | 121 |

122 |

Please enter a Font Awesome code:

123 |

124 | {`Examples: "cloud", "flag", "car", "truck", "heart", "dollar" ...`} 125 |

126 |

127 | For a complete list, please check{" "} 128 | 133 | Font Awesome Icons 134 | 135 | . 136 |

137 |
138 | setFaValue(e.target.value)} 143 | onKeyUp={(e) => { 144 | if (e.key === "Enter") { 145 | handleFaOK(); 146 | } 147 | }} 148 | /> 149 |
150 |
151 |
152 | 153 | ); 154 | }; 155 | 156 | export default auto(PromptModals); 157 | -------------------------------------------------------------------------------- /src/components/toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { auto } from "manate/react"; 2 | import React, { cloneElement, ReactElement } from "react"; 3 | 4 | import { Store } from "../store.ts"; 5 | 6 | const Toolbar = (props: { store: Store }) => { 7 | const { store } = props; 8 | const { modals } = store; 9 | const stylingClicked = (modifier: string): void => { 10 | const editor = store.editor; 11 | let mainSelection = editor.state.selection.main; 12 | if (mainSelection.empty) { 13 | const word = editor.state.wordAt(mainSelection.head); 14 | if (word) { 15 | editor.dispatch({ 16 | selection: { anchor: word.from, head: word.to }, 17 | }); 18 | } 19 | } 20 | mainSelection = editor.state.selection.main; // don't forget to update this variable 21 | editor.dispatch({ 22 | changes: { 23 | from: mainSelection.from, 24 | to: mainSelection.to, 25 | insert: `${modifier}${ 26 | editor.state.sliceDoc( 27 | mainSelection.from, 28 | mainSelection.to, 29 | ) 30 | }${modifier}`, 31 | }, 32 | }); 33 | }; 34 | const listClicked = (prefix: string): void => { 35 | const editor = store.editor; 36 | const startLine = editor.state.doc.lineAt(editor.state.selection.main.from); 37 | const endLine = editor.state.doc.lineAt(editor.state.selection.main.to); 38 | for (let i = startLine.number; i <= endLine.number; i++) { 39 | const line = editor.state.doc.line(i); 40 | editor.dispatch({ 41 | changes: { 42 | from: line.from, 43 | to: line.from, 44 | insert: prefix, 45 | }, 46 | }); 47 | } 48 | }; 49 | const insertFence = (type: string, sample: string): void => { 50 | const editor = store.editor; 51 | const mainSelection = editor.state.selection.main; 52 | const text = editor.state.sliceDoc(mainSelection.from, mainSelection.to) || 53 | sample; 54 | editor.dispatch({ 55 | changes: { 56 | from: mainSelection.from, 57 | to: mainSelection.to, 58 | insert: `\n\`\`\`${type}\n${text}\n\`\`\`\n`, 59 | }, 60 | }); 61 | }; 62 | return ( 63 |
64 | {store.preferences.toolbarItems.map((item, index) => { 65 | let reactElement: ReactElement; 66 | if (typeof item === "string") { 67 | switch (item) { 68 | case "about": { 69 | reactElement = ( 70 | modals.about.open()} 74 | title="About" 75 | /> 76 | ); 77 | break; 78 | } 79 | case "|": { 80 | reactElement = |; 81 | break; 82 | } 83 | case "bold": { 84 | reactElement = ( 85 | stylingClicked("**")} 89 | > 90 | 91 | ); 92 | break; 93 | } 94 | case "italic": { 95 | reactElement = ( 96 | stylingClicked("*")} 100 | > 101 | 102 | ); 103 | break; 104 | } 105 | case "strikethrough": { 106 | reactElement = ( 107 | stylingClicked("~~")} 111 | > 112 | 113 | ); 114 | break; 115 | } 116 | case "underline": { 117 | reactElement = ( 118 | stylingClicked("++")} 122 | > 123 | 124 | ); 125 | break; 126 | } 127 | case "mark": { 128 | reactElement = ( 129 | stylingClicked("==")} 133 | > 134 | 135 | ); 136 | break; 137 | } 138 | case "emoji": { 139 | reactElement = ( 140 | modals.emoji.open()} 144 | > 145 | 146 | ); 147 | break; 148 | } 149 | case "fontawesome": { 150 | reactElement = ( 151 | modals.fontAwesome.open()} 155 | > 156 | 157 | ); 158 | break; 159 | } 160 | case "quote": { 161 | reactElement = ( 162 | listClicked("> ")} 166 | > 167 | 168 | ); 169 | break; 170 | } 171 | case "unordered-list": { 172 | reactElement = ( 173 | listClicked("- ")} 177 | > 178 | 179 | ); 180 | break; 181 | } 182 | case "ordered-list": { 183 | reactElement = ( 184 | listClicked("1. ")} 188 | > 189 | 190 | ); 191 | break; 192 | } 193 | case "unchecked-list": { 194 | reactElement = ( 195 | listClicked("- [ ] ")} 199 | > 200 | 201 | ); 202 | break; 203 | } 204 | case "checked-list": { 205 | reactElement = ( 206 | listClicked("- [x] ")} 210 | > 211 | 212 | ); 213 | break; 214 | } 215 | case "link": { 216 | reactElement = ( 217 | { 221 | const editor = store.editor; 222 | const mainSelection = editor.state.selection.main; 223 | const text = editor.state.sliceDoc( 224 | mainSelection.from, 225 | mainSelection.to, 226 | ) || "link"; 227 | editor.dispatch({ 228 | changes: { 229 | from: mainSelection.from, 230 | to: mainSelection.to, 231 | insert: 232 | `[${text}](https://github.com/markpluslabs/react-markplus/)`, 233 | }, 234 | }); 235 | }} 236 | > 237 | 238 | ); 239 | break; 240 | } 241 | case "image": { 242 | reactElement = ( 243 | { 247 | const editor = store.editor; 248 | const mainSelection = editor.state.selection.main; 249 | const text = editor.state.sliceDoc( 250 | mainSelection.from, 251 | mainSelection.to, 252 | ) || "image"; 253 | editor.dispatch({ 254 | changes: { 255 | from: mainSelection.from, 256 | to: mainSelection.to, 257 | insert: 258 | `![${text}](https://markpluslabs.github.io/react-markplus/icon.svg)`, 259 | }, 260 | }); 261 | }} 262 | > 263 | 264 | ); 265 | break; 266 | } 267 | case "code": { 268 | reactElement = ( 269 | { 273 | const editor = store.editor; 274 | const mainSelection = editor.state.selection.main; 275 | const text = editor.state.sliceDoc( 276 | mainSelection.from, 277 | mainSelection.to, 278 | ) || "console.log('Hello, world!');"; 279 | editor.dispatch({ 280 | changes: { 281 | from: mainSelection.from, 282 | to: mainSelection.to, 283 | insert: `\n\`\`\`\n${text}\n\`\`\`\n`, 284 | }, 285 | }); 286 | }} 287 | > 288 | 289 | ); 290 | break; 291 | } 292 | case "table": { 293 | reactElement = ( 294 | { 298 | const sample = ` 299 | header 1 | header 2 300 | ---|--- 301 | row 1 col 1 | row 1 col 2 302 | row 2 col 1 | row 2 col 2`.trim(); 303 | const editor = store.editor; 304 | const cursorPos = editor.state.selection.main.head; 305 | const currentLine = editor.state.doc.lineAt(cursorPos); 306 | const isAtLineStart = cursorPos === currentLine.from; 307 | if (isAtLineStart) { 308 | editor.dispatch({ 309 | changes: { 310 | from: currentLine.from, 311 | to: currentLine.from, 312 | insert: `\n${sample}\n\n`, 313 | }, 314 | }); 315 | } else { 316 | editor.dispatch({ 317 | changes: { 318 | from: currentLine.to, 319 | to: currentLine.to, 320 | insert: `\n\n${sample}\n`, 321 | }, 322 | }); 323 | } 324 | }} 325 | > 326 | 327 | ); 328 | break; 329 | } 330 | case "math": { 331 | reactElement = ( 332 | { 336 | const editor = store.editor; 337 | const mainSelection = editor.state.selection.main; 338 | const text = editor.state.sliceDoc( 339 | mainSelection.from, 340 | mainSelection.to, 341 | ) || "E = mc^2"; 342 | editor.dispatch({ 343 | changes: { 344 | from: mainSelection.from, 345 | to: mainSelection.to, 346 | insert: `\n\`\`\`math\n${text}\n\`\`\`\n`, 347 | }, 348 | }); 349 | }} 350 | > 351 | 352 | ); 353 | break; 354 | } 355 | case "flowchart": { 356 | reactElement = ( 357 | insertFence("flowchart", "A -> B")} 361 | > 362 | 363 | ); 364 | break; 365 | } 366 | default: { 367 | throw new Error(`Unknown toolbar item: ${item}`); 368 | } 369 | } 370 | } else { 371 | reactElement = item as ReactElement; 372 | } 373 | return cloneElement(reactElement, { key: `item-${index}` }); 374 | })} 375 |
376 | ); 377 | }; 378 | 379 | export default auto(Toolbar); 380 | -------------------------------------------------------------------------------- /src/css/github-markdown.scss: -------------------------------------------------------------------------------- 1 | .markplus { 2 | &[data-theme="dark"] { 3 | .markdown-body { 4 | /* dark */ 5 | color-scheme: dark; 6 | --focus-outlineColor: #1f6feb; 7 | --fgColor-default: #f0f6fc; 8 | --fgColor-muted: #9198a1; 9 | --fgColor-accent: #4493f8; 10 | --fgColor-success: #3fb950; 11 | --fgColor-attention: #d29922; 12 | --fgColor-danger: #f85149; 13 | --fgColor-done: #ab7df8; 14 | --bgColor-default: #0d1117; 15 | --bgColor-muted: #151b23; 16 | --bgColor-neutral-muted: #656c7633; 17 | --bgColor-attention-muted: #bb800926; 18 | --borderColor-default: #3d444d; 19 | --borderColor-muted: #3d444db3; 20 | --borderColor-neutral-muted: #3d444db3; 21 | --borderColor-accent-emphasis: #1f6feb; 22 | --borderColor-success-emphasis: #238636; 23 | --borderColor-attention-emphasis: #9e6a03; 24 | --borderColor-danger-emphasis: #da3633; 25 | --borderColor-done-emphasis: #8957e5; 26 | --color-prettylights-syntax-comment: #9198a1; 27 | --color-prettylights-syntax-constant: #79c0ff; 28 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; 29 | --color-prettylights-syntax-entity: #d2a8ff; 30 | --color-prettylights-syntax-storage-modifier-import: #f0f6fc; 31 | --color-prettylights-syntax-entity-tag: #7ee787; 32 | --color-prettylights-syntax-keyword: #ff7b72; 33 | --color-prettylights-syntax-string: #a5d6ff; 34 | --color-prettylights-syntax-variable: #ffa657; 35 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; 36 | --color-prettylights-syntax-brackethighlighter-angle: #9198a1; 37 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; 38 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519; 39 | --color-prettylights-syntax-carriage-return-text: #f0f6fc; 40 | --color-prettylights-syntax-carriage-return-bg: #b62324; 41 | --color-prettylights-syntax-string-regexp: #7ee787; 42 | --color-prettylights-syntax-markup-list: #f2cc60; 43 | --color-prettylights-syntax-markup-heading: #1f6feb; 44 | --color-prettylights-syntax-markup-italic: #f0f6fc; 45 | --color-prettylights-syntax-markup-bold: #f0f6fc; 46 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7; 47 | --color-prettylights-syntax-markup-deleted-bg: #67060c; 48 | --color-prettylights-syntax-markup-inserted-text: #aff5b4; 49 | --color-prettylights-syntax-markup-inserted-bg: #033a16; 50 | --color-prettylights-syntax-markup-changed-text: #ffdfb6; 51 | --color-prettylights-syntax-markup-changed-bg: #5a1e02; 52 | --color-prettylights-syntax-markup-ignored-text: #f0f6fc; 53 | --color-prettylights-syntax-markup-ignored-bg: #1158c7; 54 | --color-prettylights-syntax-meta-diff-range: #d2a8ff; 55 | --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d; 56 | } 57 | } 58 | &[data-theme="light"] { 59 | .markdown-body { 60 | /* light */ 61 | color-scheme: light; 62 | --focus-outlineColor: #0969da; 63 | --fgColor-default: #1f2328; 64 | --fgColor-muted: #59636e; 65 | --fgColor-accent: #0969da; 66 | --fgColor-success: #1a7f37; 67 | --fgColor-attention: #9a6700; 68 | --fgColor-danger: #d1242f; 69 | --fgColor-done: #8250df; 70 | --bgColor-default: #ffffff; 71 | --bgColor-muted: #f6f8fa; 72 | --bgColor-neutral-muted: #818b981f; 73 | --bgColor-attention-muted: #fff8c5; 74 | --borderColor-default: #d1d9e0; 75 | --borderColor-muted: #d1d9e0b3; 76 | --borderColor-neutral-muted: #d1d9e0b3; 77 | --borderColor-accent-emphasis: #0969da; 78 | --borderColor-success-emphasis: #1a7f37; 79 | --borderColor-attention-emphasis: #9a6700; 80 | --borderColor-danger-emphasis: #cf222e; 81 | --borderColor-done-emphasis: #8250df; 82 | --color-prettylights-syntax-comment: #59636e; 83 | --color-prettylights-syntax-constant: #0550ae; 84 | --color-prettylights-syntax-constant-other-reference-link: #0a3069; 85 | --color-prettylights-syntax-entity: #6639ba; 86 | --color-prettylights-syntax-storage-modifier-import: #1f2328; 87 | --color-prettylights-syntax-entity-tag: #0550ae; 88 | --color-prettylights-syntax-keyword: #cf222e; 89 | --color-prettylights-syntax-string: #0a3069; 90 | --color-prettylights-syntax-variable: #953800; 91 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; 92 | --color-prettylights-syntax-brackethighlighter-angle: #59636e; 93 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; 94 | --color-prettylights-syntax-invalid-illegal-bg: #82071e; 95 | --color-prettylights-syntax-carriage-return-text: #f6f8fa; 96 | --color-prettylights-syntax-carriage-return-bg: #cf222e; 97 | --color-prettylights-syntax-string-regexp: #116329; 98 | --color-prettylights-syntax-markup-list: #3b2300; 99 | --color-prettylights-syntax-markup-heading: #0550ae; 100 | --color-prettylights-syntax-markup-italic: #1f2328; 101 | --color-prettylights-syntax-markup-bold: #1f2328; 102 | --color-prettylights-syntax-markup-deleted-text: #82071e; 103 | --color-prettylights-syntax-markup-deleted-bg: #ffebe9; 104 | --color-prettylights-syntax-markup-inserted-text: #116329; 105 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1; 106 | --color-prettylights-syntax-markup-changed-text: #953800; 107 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5; 108 | --color-prettylights-syntax-markup-ignored-text: #d1d9e0; 109 | --color-prettylights-syntax-markup-ignored-bg: #0550ae; 110 | --color-prettylights-syntax-meta-diff-range: #8250df; 111 | --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98; 112 | } 113 | } 114 | .markdown-body { 115 | --base-size-4: 0.25rem; 116 | --base-size-8: 0.5rem; 117 | --base-size-16: 1rem; 118 | --base-size-24: 1.5rem; 119 | --base-size-40: 2.5rem; 120 | --base-text-weight-normal: 400; 121 | --base-text-weight-medium: 500; 122 | --base-text-weight-semibold: 600; 123 | --fontStack-monospace: 124 | ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, 125 | monospace; 126 | --fgColor-accent: Highlight; 127 | } 128 | 129 | .markdown-body { 130 | -ms-text-size-adjust: 100%; 131 | -webkit-text-size-adjust: 100%; 132 | margin: 0; 133 | color: var(--fgColor-default); 134 | background-color: var(--bgColor-default); 135 | font-family: 136 | -apple-system, 137 | BlinkMacSystemFont, 138 | "Segoe UI", 139 | "Noto Sans", 140 | Helvetica, 141 | Arial, 142 | sans-serif, 143 | "Apple Color Emoji", 144 | "Segoe UI Emoji"; 145 | font-size: 16px; 146 | line-height: 1.5; 147 | word-wrap: break-word; 148 | scroll-behavior: auto !important; 149 | } 150 | 151 | .markdown-body .octicon { 152 | display: inline-block; 153 | fill: currentColor; 154 | vertical-align: text-bottom; 155 | } 156 | 157 | .markdown-body h1:hover .anchor .octicon-link:before, 158 | .markdown-body h2:hover .anchor .octicon-link:before, 159 | .markdown-body h3:hover .anchor .octicon-link:before, 160 | .markdown-body h4:hover .anchor .octicon-link:before, 161 | .markdown-body h5:hover .anchor .octicon-link:before, 162 | .markdown-body h6:hover .anchor .octicon-link:before { 163 | width: 16px; 164 | height: 16px; 165 | content: " "; 166 | display: inline-block; 167 | background-color: currentColor; 168 | -webkit-mask-image: url("data:image/svg+xml,"); 169 | mask-image: url("data:image/svg+xml,"); 170 | } 171 | 172 | .markdown-body details, 173 | .markdown-body figcaption, 174 | .markdown-body figure { 175 | display: block; 176 | } 177 | 178 | .markdown-body summary { 179 | display: list-item; 180 | } 181 | 182 | .markdown-body [hidden] { 183 | display: none !important; 184 | } 185 | 186 | .markdown-body a { 187 | background-color: transparent; 188 | color: var(--fgColor-accent); 189 | text-decoration: none; 190 | } 191 | 192 | .markdown-body abbr[title] { 193 | border-bottom: none; 194 | -webkit-text-decoration: underline dotted; 195 | text-decoration: underline dotted; 196 | } 197 | 198 | .markdown-body b, 199 | .markdown-body strong { 200 | font-weight: var(--base-text-weight-semibold, 600); 201 | } 202 | 203 | .markdown-body dfn { 204 | font-style: italic; 205 | } 206 | 207 | .markdown-body h1 { 208 | margin: 0.67em 0; 209 | font-weight: var(--base-text-weight-semibold, 600); 210 | padding-bottom: 0.3em; 211 | font-size: 2em; 212 | border-bottom: 1px solid var(--borderColor-muted); 213 | } 214 | 215 | .markdown-body mark { 216 | background-color: var(--bgColor-attention-muted); 217 | color: var(--fgColor-default); 218 | } 219 | 220 | .markdown-body small { 221 | font-size: 90%; 222 | } 223 | 224 | .markdown-body sub, 225 | .markdown-body sup { 226 | font-size: 75%; 227 | line-height: 0; 228 | position: relative; 229 | vertical-align: baseline; 230 | } 231 | 232 | .markdown-body sub { 233 | bottom: -0.25em; 234 | } 235 | 236 | .markdown-body sup { 237 | top: -0.5em; 238 | } 239 | 240 | .markdown-body img { 241 | border-style: none; 242 | max-width: 100%; 243 | box-sizing: content-box; 244 | } 245 | 246 | .markdown-body code, 247 | .markdown-body kbd, 248 | .markdown-body pre, 249 | .markdown-body samp { 250 | font-family: monospace; 251 | font-size: 1em; 252 | } 253 | 254 | .markdown-body figure { 255 | margin: 1em var(--base-size-40); 256 | } 257 | 258 | .markdown-body hr { 259 | box-sizing: content-box; 260 | overflow: hidden; 261 | background: transparent; 262 | border-bottom: 1px solid var(--borderColor-muted); 263 | height: 0.25em; 264 | padding: 0; 265 | margin: var(--base-size-24) 0; 266 | background-color: var(--borderColor-default); 267 | border: 0; 268 | } 269 | 270 | .markdown-body input { 271 | font: inherit; 272 | margin: 0; 273 | overflow: visible; 274 | font-family: inherit; 275 | font-size: inherit; 276 | line-height: inherit; 277 | } 278 | 279 | .markdown-body [type="button"], 280 | .markdown-body [type="reset"], 281 | .markdown-body [type="submit"] { 282 | -webkit-appearance: button; 283 | appearance: button; 284 | } 285 | 286 | .markdown-body [type="checkbox"], 287 | .markdown-body [type="radio"] { 288 | box-sizing: border-box; 289 | padding: 0; 290 | } 291 | 292 | .markdown-body [type="number"]::-webkit-inner-spin-button, 293 | .markdown-body [type="number"]::-webkit-outer-spin-button { 294 | height: auto; 295 | } 296 | 297 | .markdown-body [type="search"]::-webkit-search-cancel-button, 298 | .markdown-body [type="search"]::-webkit-search-decoration { 299 | -webkit-appearance: none; 300 | appearance: none; 301 | } 302 | 303 | .markdown-body ::-webkit-input-placeholder { 304 | color: inherit; 305 | opacity: 0.54; 306 | } 307 | 308 | .markdown-body ::-webkit-file-upload-button { 309 | -webkit-appearance: button; 310 | appearance: button; 311 | font: inherit; 312 | } 313 | 314 | .markdown-body a:hover { 315 | text-decoration: underline; 316 | } 317 | 318 | .markdown-body ::placeholder { 319 | color: var(--fgColor-muted); 320 | opacity: 1; 321 | } 322 | 323 | .markdown-body hr::before { 324 | display: table; 325 | content: ""; 326 | } 327 | 328 | .markdown-body hr::after { 329 | display: table; 330 | clear: both; 331 | content: ""; 332 | } 333 | 334 | .markdown-body table { 335 | border-spacing: 0; 336 | border-collapse: collapse; 337 | display: block; 338 | width: max-content; 339 | max-width: 100%; 340 | overflow: auto; 341 | } 342 | 343 | .markdown-body td, 344 | .markdown-body th { 345 | padding: 0; 346 | } 347 | 348 | .markdown-body details summary { 349 | cursor: pointer; 350 | } 351 | 352 | .markdown-body a:focus, 353 | .markdown-body [role="button"]:focus, 354 | .markdown-body input[type="radio"]:focus, 355 | .markdown-body input[type="checkbox"]:focus { 356 | outline: 2px solid var(--focus-outlineColor); 357 | outline-offset: -2px; 358 | box-shadow: none; 359 | } 360 | 361 | .markdown-body a:focus:not(:focus-visible), 362 | .markdown-body [role="button"]:focus:not(:focus-visible), 363 | .markdown-body input[type="radio"]:focus:not(:focus-visible), 364 | .markdown-body input[type="checkbox"]:focus:not(:focus-visible) { 365 | outline: solid 1px transparent; 366 | } 367 | 368 | .markdown-body a:focus-visible, 369 | .markdown-body [role="button"]:focus-visible, 370 | .markdown-body input[type="radio"]:focus-visible, 371 | .markdown-body input[type="checkbox"]:focus-visible { 372 | outline: 2px solid var(--focus-outlineColor); 373 | outline-offset: -2px; 374 | box-shadow: none; 375 | } 376 | 377 | .markdown-body a:not([class]):focus, 378 | .markdown-body a:not([class]):focus-visible, 379 | .markdown-body input[type="radio"]:focus, 380 | .markdown-body input[type="radio"]:focus-visible, 381 | .markdown-body input[type="checkbox"]:focus, 382 | .markdown-body input[type="checkbox"]:focus-visible { 383 | outline-offset: 0; 384 | } 385 | 386 | .markdown-body kbd { 387 | display: inline-block; 388 | padding: var(--base-size-4); 389 | font: 11px 390 | var( 391 | --fontStack-monospace, 392 | ui-monospace, 393 | SFMono-Regular, 394 | SF Mono, 395 | Menlo, 396 | Consolas, 397 | Liberation Mono, 398 | monospace 399 | ); 400 | line-height: 10px; 401 | color: var(--fgColor-default); 402 | vertical-align: middle; 403 | background-color: var(--bgColor-muted); 404 | border: solid 1px var(--borderColor-neutral-muted); 405 | border-bottom-color: var(--borderColor-neutral-muted); 406 | border-radius: 6px; 407 | box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted); 408 | } 409 | 410 | .markdown-body h1, 411 | .markdown-body h2, 412 | .markdown-body h3, 413 | .markdown-body h4, 414 | .markdown-body h5, 415 | .markdown-body h6 { 416 | margin-top: var(--base-size-24); 417 | margin-bottom: var(--base-size-16); 418 | font-weight: var(--base-text-weight-semibold, 600); 419 | line-height: 1.25; 420 | } 421 | 422 | .markdown-body h2 { 423 | font-weight: var(--base-text-weight-semibold, 600); 424 | padding-bottom: 0.3em; 425 | font-size: 1.5em; 426 | border-bottom: 1px solid var(--borderColor-muted); 427 | } 428 | 429 | .markdown-body h3 { 430 | font-weight: var(--base-text-weight-semibold, 600); 431 | font-size: 1.25em; 432 | } 433 | 434 | .markdown-body h4 { 435 | font-weight: var(--base-text-weight-semibold, 600); 436 | font-size: 1em; 437 | } 438 | 439 | .markdown-body h5 { 440 | font-weight: var(--base-text-weight-semibold, 600); 441 | font-size: 0.875em; 442 | } 443 | 444 | .markdown-body h6 { 445 | font-weight: var(--base-text-weight-semibold, 600); 446 | font-size: 0.85em; 447 | color: var(--fgColor-muted); 448 | } 449 | 450 | .markdown-body p { 451 | margin-top: 0; 452 | margin-bottom: 10px; 453 | } 454 | 455 | .markdown-body blockquote { 456 | margin: 0; 457 | padding: 0 1em; 458 | color: var(--fgColor-muted); 459 | border-left: 0.25em solid var(--borderColor-default); 460 | } 461 | 462 | .markdown-body ul, 463 | .markdown-body ol { 464 | margin-top: 0; 465 | margin-bottom: 0; 466 | padding-left: 2em; 467 | } 468 | 469 | .markdown-body ol ol, 470 | .markdown-body ul ol { 471 | list-style-type: lower-roman; 472 | } 473 | 474 | .markdown-body ul ul ol, 475 | .markdown-body ul ol ol, 476 | .markdown-body ol ul ol, 477 | .markdown-body ol ol ol { 478 | list-style-type: lower-alpha; 479 | } 480 | 481 | .markdown-body dd { 482 | margin-left: 0; 483 | } 484 | 485 | .markdown-body tt, 486 | .markdown-body code, 487 | .markdown-body samp { 488 | font-family: var( 489 | --fontStack-monospace, 490 | ui-monospace, 491 | SFMono-Regular, 492 | SF Mono, 493 | Menlo, 494 | Consolas, 495 | Liberation Mono, 496 | monospace 497 | ); 498 | font-size: 12px; 499 | } 500 | 501 | .markdown-body pre { 502 | margin-top: 0; 503 | margin-bottom: 0; 504 | font-family: var( 505 | --fontStack-monospace, 506 | ui-monospace, 507 | SFMono-Regular, 508 | SF Mono, 509 | Menlo, 510 | Consolas, 511 | Liberation Mono, 512 | monospace 513 | ); 514 | font-size: 12px; 515 | word-wrap: normal; 516 | } 517 | 518 | .markdown-body .octicon { 519 | display: inline-block; 520 | overflow: visible !important; 521 | vertical-align: text-bottom; 522 | fill: currentColor; 523 | } 524 | 525 | .markdown-body input::-webkit-outer-spin-button, 526 | .markdown-body input::-webkit-inner-spin-button { 527 | margin: 0; 528 | -webkit-appearance: none; 529 | appearance: none; 530 | } 531 | 532 | .markdown-body .mr-2 { 533 | margin-right: var(--base-size-8, 8px) !important; 534 | } 535 | 536 | .markdown-body::before { 537 | display: table; 538 | content: ""; 539 | } 540 | 541 | .markdown-body::after { 542 | display: table; 543 | clear: both; 544 | content: ""; 545 | } 546 | 547 | .markdown-body > *:first-child { 548 | margin-top: 0 !important; 549 | } 550 | 551 | .markdown-body > *:last-child { 552 | margin-bottom: 0 !important; 553 | } 554 | 555 | .markdown-body a:not([href]) { 556 | color: inherit; 557 | text-decoration: none; 558 | } 559 | 560 | .markdown-body .absent { 561 | color: var(--fgColor-danger); 562 | } 563 | 564 | .markdown-body .anchor { 565 | float: left; 566 | padding-right: var(--base-size-4); 567 | margin-left: -20px; 568 | line-height: 1; 569 | } 570 | 571 | .markdown-body .anchor:focus { 572 | outline: none; 573 | } 574 | 575 | .markdown-body p, 576 | .markdown-body blockquote, 577 | .markdown-body ul, 578 | .markdown-body ol, 579 | .markdown-body dl, 580 | .markdown-body table, 581 | .markdown-body pre, 582 | .markdown-body details { 583 | margin-top: 0; 584 | margin-bottom: var(--base-size-16); 585 | } 586 | 587 | .markdown-body blockquote > :first-child { 588 | margin-top: 0; 589 | } 590 | 591 | .markdown-body blockquote > :last-child { 592 | margin-bottom: 0; 593 | } 594 | 595 | .markdown-body h1 .octicon-link, 596 | .markdown-body h2 .octicon-link, 597 | .markdown-body h3 .octicon-link, 598 | .markdown-body h4 .octicon-link, 599 | .markdown-body h5 .octicon-link, 600 | .markdown-body h6 .octicon-link { 601 | color: var(--fgColor-default); 602 | vertical-align: middle; 603 | visibility: hidden; 604 | } 605 | 606 | .markdown-body h1:hover .anchor, 607 | .markdown-body h2:hover .anchor, 608 | .markdown-body h3:hover .anchor, 609 | .markdown-body h4:hover .anchor, 610 | .markdown-body h5:hover .anchor, 611 | .markdown-body h6:hover .anchor { 612 | text-decoration: none; 613 | } 614 | 615 | .markdown-body h1:hover .anchor .octicon-link, 616 | .markdown-body h2:hover .anchor .octicon-link, 617 | .markdown-body h3:hover .anchor .octicon-link, 618 | .markdown-body h4:hover .anchor .octicon-link, 619 | .markdown-body h5:hover .anchor .octicon-link, 620 | .markdown-body h6:hover .anchor .octicon-link { 621 | visibility: visible; 622 | } 623 | 624 | .markdown-body h1 tt, 625 | .markdown-body h1 code, 626 | .markdown-body h2 tt, 627 | .markdown-body h2 code, 628 | .markdown-body h3 tt, 629 | .markdown-body h3 code, 630 | .markdown-body h4 tt, 631 | .markdown-body h4 code, 632 | .markdown-body h5 tt, 633 | .markdown-body h5 code, 634 | .markdown-body h6 tt, 635 | .markdown-body h6 code { 636 | padding: 0 0.2em; 637 | font-size: inherit; 638 | } 639 | 640 | .markdown-body summary h1, 641 | .markdown-body summary h2, 642 | .markdown-body summary h3, 643 | .markdown-body summary h4, 644 | .markdown-body summary h5, 645 | .markdown-body summary h6 { 646 | display: inline-block; 647 | } 648 | 649 | .markdown-body summary h1 .anchor, 650 | .markdown-body summary h2 .anchor, 651 | .markdown-body summary h3 .anchor, 652 | .markdown-body summary h4 .anchor, 653 | .markdown-body summary h5 .anchor, 654 | .markdown-body summary h6 .anchor { 655 | margin-left: -40px; 656 | } 657 | 658 | .markdown-body summary h1, 659 | .markdown-body summary h2 { 660 | padding-bottom: 0; 661 | border-bottom: 0; 662 | } 663 | 664 | .markdown-body ul.no-list, 665 | .markdown-body ol.no-list { 666 | padding: 0; 667 | list-style-type: none; 668 | } 669 | 670 | .markdown-body ol[type="a s"] { 671 | list-style-type: lower-alpha; 672 | } 673 | 674 | .markdown-body ol[type="A s"] { 675 | list-style-type: upper-alpha; 676 | } 677 | 678 | .markdown-body ol[type="i s"] { 679 | list-style-type: lower-roman; 680 | } 681 | 682 | .markdown-body ol[type="I s"] { 683 | list-style-type: upper-roman; 684 | } 685 | 686 | .markdown-body ol[type="1"] { 687 | list-style-type: decimal; 688 | } 689 | 690 | .markdown-body div > ol:not([type]) { 691 | list-style-type: decimal; 692 | } 693 | 694 | .markdown-body ul ul, 695 | .markdown-body ul ol, 696 | .markdown-body ol ol, 697 | .markdown-body ol ul { 698 | margin-top: 0; 699 | margin-bottom: 0; 700 | } 701 | 702 | .markdown-body li > p { 703 | margin-top: var(--base-size-16); 704 | } 705 | 706 | .markdown-body li + li { 707 | margin-top: 0.25em; 708 | } 709 | 710 | .markdown-body dl { 711 | padding: 0; 712 | } 713 | 714 | .markdown-body dl dt { 715 | padding: 0; 716 | margin-top: var(--base-size-16); 717 | font-size: 1em; 718 | font-style: italic; 719 | font-weight: var(--base-text-weight-semibold, 600); 720 | } 721 | 722 | .markdown-body dl dd { 723 | padding: 0 var(--base-size-16); 724 | margin-bottom: var(--base-size-16); 725 | } 726 | 727 | .markdown-body table th { 728 | font-weight: var(--base-text-weight-semibold, 600); 729 | } 730 | 731 | .markdown-body table th, 732 | .markdown-body table td { 733 | padding: 6px 13px; 734 | border: 1px solid var(--borderColor-default); 735 | } 736 | 737 | .markdown-body table td > :last-child { 738 | margin-bottom: 0; 739 | } 740 | 741 | .markdown-body table tr { 742 | background-color: var(--bgColor-default); 743 | border-top: 1px solid var(--borderColor-muted); 744 | } 745 | 746 | .markdown-body table tr:nth-child(2n) { 747 | background-color: var(--bgColor-muted); 748 | } 749 | 750 | .markdown-body table img { 751 | background-color: transparent; 752 | } 753 | 754 | .markdown-body img[align="right"] { 755 | padding-left: 20px; 756 | } 757 | 758 | .markdown-body img[align="left"] { 759 | padding-right: 20px; 760 | } 761 | 762 | .markdown-body .emoji { 763 | max-width: none; 764 | vertical-align: text-top; 765 | background-color: transparent; 766 | } 767 | 768 | .markdown-body span.frame { 769 | display: block; 770 | overflow: hidden; 771 | } 772 | 773 | .markdown-body span.frame > span { 774 | display: block; 775 | float: left; 776 | width: auto; 777 | padding: 7px; 778 | margin: 13px 0 0; 779 | overflow: hidden; 780 | border: 1px solid var(--borderColor-default); 781 | } 782 | 783 | .markdown-body span.frame span img { 784 | display: block; 785 | float: left; 786 | } 787 | 788 | .markdown-body span.frame span span { 789 | display: block; 790 | padding: 5px 0 0; 791 | clear: both; 792 | color: var(--fgColor-default); 793 | } 794 | 795 | .markdown-body span.align-center { 796 | display: block; 797 | overflow: hidden; 798 | clear: both; 799 | } 800 | 801 | .markdown-body span.align-center > span { 802 | display: block; 803 | margin: 13px auto 0; 804 | overflow: hidden; 805 | text-align: center; 806 | } 807 | 808 | .markdown-body span.align-center span img { 809 | margin: 0 auto; 810 | text-align: center; 811 | } 812 | 813 | .markdown-body span.align-right { 814 | display: block; 815 | overflow: hidden; 816 | clear: both; 817 | } 818 | 819 | .markdown-body span.align-right > span { 820 | display: block; 821 | margin: 13px 0 0; 822 | overflow: hidden; 823 | text-align: right; 824 | } 825 | 826 | .markdown-body span.align-right span img { 827 | margin: 0; 828 | text-align: right; 829 | } 830 | 831 | .markdown-body span.float-left { 832 | display: block; 833 | float: left; 834 | margin-right: 13px; 835 | overflow: hidden; 836 | } 837 | 838 | .markdown-body span.float-left span { 839 | margin: 13px 0 0; 840 | } 841 | 842 | .markdown-body span.float-right { 843 | display: block; 844 | float: right; 845 | margin-left: 13px; 846 | overflow: hidden; 847 | } 848 | 849 | .markdown-body span.float-right > span { 850 | display: block; 851 | margin: 13px auto 0; 852 | overflow: hidden; 853 | text-align: right; 854 | } 855 | 856 | .markdown-body code, 857 | .markdown-body tt { 858 | padding: 0.2em 0.4em; 859 | margin: 0; 860 | font-size: 85%; 861 | white-space: break-spaces; 862 | background-color: var(--bgColor-neutral-muted); 863 | border-radius: 6px; 864 | } 865 | 866 | .markdown-body code br, 867 | .markdown-body tt br { 868 | display: none; 869 | } 870 | 871 | .markdown-body del code { 872 | text-decoration: inherit; 873 | } 874 | 875 | .markdown-body samp { 876 | font-size: 85%; 877 | } 878 | 879 | .markdown-body pre code { 880 | font-size: 100%; 881 | } 882 | 883 | .markdown-body pre > code { 884 | padding: 0; 885 | margin: 0; 886 | word-break: normal; 887 | white-space: pre; 888 | background: transparent; 889 | border: 0; 890 | } 891 | 892 | .markdown-body .highlight { 893 | margin-bottom: var(--base-size-16); 894 | } 895 | 896 | .markdown-body .highlight pre { 897 | margin-bottom: 0; 898 | word-break: normal; 899 | } 900 | 901 | .markdown-body .highlight pre, 902 | .markdown-body pre { 903 | padding: var(--base-size-16); 904 | overflow: auto; 905 | font-size: 85%; 906 | line-height: 1.45; 907 | color: var(--fgColor-default); 908 | background-color: var(--bgColor-muted); 909 | border-radius: 6px; 910 | } 911 | 912 | .markdown-body pre code, 913 | .markdown-body pre tt { 914 | display: inline; 915 | max-width: auto; 916 | padding: 0; 917 | margin: 0; 918 | overflow: visible; 919 | line-height: inherit; 920 | word-wrap: normal; 921 | background-color: transparent; 922 | border: 0; 923 | } 924 | 925 | .markdown-body .csv-data td, 926 | .markdown-body .csv-data th { 927 | padding: 5px; 928 | overflow: hidden; 929 | font-size: 12px; 930 | line-height: 1; 931 | text-align: left; 932 | white-space: nowrap; 933 | } 934 | 935 | .markdown-body .csv-data .blob-num { 936 | padding: 10px var(--base-size-8) 9px; 937 | text-align: right; 938 | background: var(--bgColor-default); 939 | border: 0; 940 | } 941 | 942 | .markdown-body .csv-data tr { 943 | border-top: 0; 944 | } 945 | 946 | .markdown-body .csv-data th { 947 | font-weight: var(--base-text-weight-semibold, 600); 948 | background: var(--bgColor-muted); 949 | border-top: 0; 950 | } 951 | 952 | .markdown-body [data-footnote-ref]::before { 953 | content: "["; 954 | } 955 | 956 | .markdown-body [data-footnote-ref]::after { 957 | content: "]"; 958 | } 959 | 960 | .markdown-body .footnotes { 961 | font-size: 12px; 962 | color: var(--fgColor-muted); 963 | border-top: 1px solid var(--borderColor-default); 964 | } 965 | 966 | .markdown-body .footnotes ol { 967 | padding-left: var(--base-size-16); 968 | } 969 | 970 | .markdown-body .footnotes ol ul { 971 | display: inline-block; 972 | padding-left: var(--base-size-16); 973 | margin-top: var(--base-size-16); 974 | } 975 | 976 | .markdown-body .footnotes li { 977 | position: relative; 978 | } 979 | 980 | .markdown-body .footnotes li:target::before { 981 | position: absolute; 982 | top: calc(var(--base-size-8) * -1); 983 | right: calc(var(--base-size-8) * -1); 984 | bottom: calc(var(--base-size-8) * -1); 985 | left: calc(var(--base-size-24) * -1); 986 | pointer-events: none; 987 | content: ""; 988 | border: 2px solid var(--borderColor-accent-emphasis); 989 | border-radius: 6px; 990 | } 991 | 992 | .markdown-body .footnotes li:target { 993 | color: var(--fgColor-default); 994 | } 995 | 996 | .markdown-body .footnotes .data-footnote-backref g-emoji { 997 | font-family: monospace; 998 | } 999 | 1000 | .markdown-body .pl-c { 1001 | color: var(--color-prettylights-syntax-comment); 1002 | } 1003 | 1004 | .markdown-body .pl-c1, 1005 | .markdown-body .pl-s .pl-v { 1006 | color: var(--color-prettylights-syntax-constant); 1007 | } 1008 | 1009 | .markdown-body .pl-e, 1010 | .markdown-body .pl-en { 1011 | color: var(--color-prettylights-syntax-entity); 1012 | } 1013 | 1014 | .markdown-body .pl-smi, 1015 | .markdown-body .pl-s .pl-s1 { 1016 | color: var(--color-prettylights-syntax-storage-modifier-import); 1017 | } 1018 | 1019 | .markdown-body .pl-ent { 1020 | color: var(--color-prettylights-syntax-entity-tag); 1021 | } 1022 | 1023 | .markdown-body .pl-k { 1024 | color: var(--color-prettylights-syntax-keyword); 1025 | } 1026 | 1027 | .markdown-body .pl-s, 1028 | .markdown-body .pl-pds, 1029 | .markdown-body .pl-s .pl-pse .pl-s1, 1030 | .markdown-body .pl-sr, 1031 | .markdown-body .pl-sr .pl-cce, 1032 | .markdown-body .pl-sr .pl-sre, 1033 | .markdown-body .pl-sr .pl-sra { 1034 | color: var(--color-prettylights-syntax-string); 1035 | } 1036 | 1037 | .markdown-body .pl-v, 1038 | .markdown-body .pl-smw { 1039 | color: var(--color-prettylights-syntax-variable); 1040 | } 1041 | 1042 | .markdown-body .pl-bu { 1043 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched); 1044 | } 1045 | 1046 | .markdown-body .pl-ii { 1047 | color: var(--color-prettylights-syntax-invalid-illegal-text); 1048 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg); 1049 | } 1050 | 1051 | .markdown-body .pl-c2 { 1052 | color: var(--color-prettylights-syntax-carriage-return-text); 1053 | background-color: var(--color-prettylights-syntax-carriage-return-bg); 1054 | } 1055 | 1056 | .markdown-body .pl-sr .pl-cce { 1057 | font-weight: bold; 1058 | color: var(--color-prettylights-syntax-string-regexp); 1059 | } 1060 | 1061 | .markdown-body .pl-ml { 1062 | color: var(--color-prettylights-syntax-markup-list); 1063 | } 1064 | 1065 | .markdown-body .pl-mh, 1066 | .markdown-body .pl-mh .pl-en, 1067 | .markdown-body .pl-ms { 1068 | font-weight: bold; 1069 | color: var(--color-prettylights-syntax-markup-heading); 1070 | } 1071 | 1072 | .markdown-body .pl-mi { 1073 | font-style: italic; 1074 | color: var(--color-prettylights-syntax-markup-italic); 1075 | } 1076 | 1077 | .markdown-body .pl-mb { 1078 | font-weight: bold; 1079 | color: var(--color-prettylights-syntax-markup-bold); 1080 | } 1081 | 1082 | .markdown-body .pl-md { 1083 | color: var(--color-prettylights-syntax-markup-deleted-text); 1084 | background-color: var(--color-prettylights-syntax-markup-deleted-bg); 1085 | } 1086 | 1087 | .markdown-body .pl-mi1 { 1088 | color: var(--color-prettylights-syntax-markup-inserted-text); 1089 | background-color: var(--color-prettylights-syntax-markup-inserted-bg); 1090 | } 1091 | 1092 | .markdown-body .pl-mc { 1093 | color: var(--color-prettylights-syntax-markup-changed-text); 1094 | background-color: var(--color-prettylights-syntax-markup-changed-bg); 1095 | } 1096 | 1097 | .markdown-body .pl-mi2 { 1098 | color: var(--color-prettylights-syntax-markup-ignored-text); 1099 | background-color: var(--color-prettylights-syntax-markup-ignored-bg); 1100 | } 1101 | 1102 | .markdown-body .pl-mdr { 1103 | font-weight: bold; 1104 | color: var(--color-prettylights-syntax-meta-diff-range); 1105 | } 1106 | 1107 | .markdown-body .pl-ba { 1108 | color: var(--color-prettylights-syntax-brackethighlighter-angle); 1109 | } 1110 | 1111 | .markdown-body .pl-sg { 1112 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); 1113 | } 1114 | 1115 | .markdown-body .pl-corl { 1116 | text-decoration: underline; 1117 | color: var(--color-prettylights-syntax-constant-other-reference-link); 1118 | } 1119 | 1120 | .markdown-body [role="button"]:focus:not(:focus-visible), 1121 | .markdown-body [role="tabpanel"][tabindex="0"]:focus:not(:focus-visible), 1122 | .markdown-body button:focus:not(:focus-visible), 1123 | .markdown-body summary:focus:not(:focus-visible), 1124 | .markdown-body a:focus:not(:focus-visible) { 1125 | outline: none; 1126 | box-shadow: none; 1127 | } 1128 | 1129 | .markdown-body [tabindex="0"]:focus:not(:focus-visible), 1130 | .markdown-body details-dialog:focus:not(:focus-visible) { 1131 | outline: none; 1132 | } 1133 | 1134 | .markdown-body g-emoji { 1135 | display: inline-block; 1136 | min-width: 1ch; 1137 | font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 1138 | font-size: 1em; 1139 | font-style: normal !important; 1140 | font-weight: var(--base-text-weight-normal, 400); 1141 | line-height: 1; 1142 | vertical-align: -0.075em; 1143 | } 1144 | 1145 | .markdown-body g-emoji img { 1146 | width: 1em; 1147 | height: 1em; 1148 | } 1149 | 1150 | .markdown-body .task-list-item { 1151 | list-style-type: none; 1152 | } 1153 | 1154 | .markdown-body .task-list-item label { 1155 | font-weight: var(--base-text-weight-normal, 400); 1156 | } 1157 | 1158 | .markdown-body .task-list-item.enabled label { 1159 | cursor: pointer; 1160 | } 1161 | 1162 | .markdown-body .task-list-item + .task-list-item { 1163 | margin-top: var(--base-size-4); 1164 | } 1165 | 1166 | .markdown-body .task-list-item .handle { 1167 | display: none; 1168 | } 1169 | 1170 | .markdown-body .task-list-item-checkbox { 1171 | margin: 0 0.2em 0.25em -1.4em; 1172 | vertical-align: middle; 1173 | } 1174 | 1175 | .markdown-body ul:dir(rtl) .task-list-item-checkbox { 1176 | margin: 0 -1.6em 0.25em 0.2em; 1177 | } 1178 | 1179 | .markdown-body ol:dir(rtl) .task-list-item-checkbox { 1180 | margin: 0 -1.6em 0.25em 0.2em; 1181 | } 1182 | 1183 | .markdown-body .contains-task-list:hover .task-list-item-convert-container, 1184 | .markdown-body 1185 | .contains-task-list:focus-within 1186 | .task-list-item-convert-container { 1187 | display: block; 1188 | width: auto; 1189 | height: 24px; 1190 | overflow: visible; 1191 | clip: auto; 1192 | } 1193 | 1194 | .markdown-body ::-webkit-calendar-picker-indicator { 1195 | filter: invert(50%); 1196 | } 1197 | 1198 | .markdown-body .markdown-alert { 1199 | padding: var(--base-size-8) var(--base-size-16); 1200 | margin-bottom: var(--base-size-16); 1201 | color: inherit; 1202 | border-left: 0.25em solid var(--borderColor-default); 1203 | } 1204 | 1205 | .markdown-body .markdown-alert > :first-child { 1206 | margin-top: 0; 1207 | } 1208 | 1209 | .markdown-body .markdown-alert > :last-child { 1210 | margin-bottom: 0; 1211 | } 1212 | 1213 | .markdown-body .markdown-alert .markdown-alert-title { 1214 | display: flex; 1215 | font-weight: var(--base-text-weight-medium, 500); 1216 | align-items: center; 1217 | line-height: 1; 1218 | } 1219 | 1220 | .markdown-body .markdown-alert.markdown-alert-note { 1221 | border-left-color: var(--borderColor-accent-emphasis); 1222 | } 1223 | 1224 | .markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { 1225 | color: var(--fgColor-accent); 1226 | } 1227 | 1228 | .markdown-body .markdown-alert.markdown-alert-important { 1229 | border-left-color: var(--borderColor-done-emphasis); 1230 | } 1231 | 1232 | .markdown-body 1233 | .markdown-alert.markdown-alert-important 1234 | .markdown-alert-title { 1235 | color: var(--fgColor-done); 1236 | } 1237 | 1238 | .markdown-body .markdown-alert.markdown-alert-warning { 1239 | border-left-color: var(--borderColor-attention-emphasis); 1240 | } 1241 | 1242 | .markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { 1243 | color: var(--fgColor-attention); 1244 | } 1245 | 1246 | .markdown-body .markdown-alert.markdown-alert-tip { 1247 | border-left-color: var(--borderColor-success-emphasis); 1248 | } 1249 | 1250 | .markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { 1251 | color: var(--fgColor-success); 1252 | } 1253 | 1254 | .markdown-body .markdown-alert.markdown-alert-caution { 1255 | border-left-color: var(--borderColor-danger-emphasis); 1256 | } 1257 | 1258 | .markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { 1259 | color: var(--fgColor-danger); 1260 | } 1261 | 1262 | .markdown-body > *:first-child > .heading-element:first-child { 1263 | margin-top: 0 !important; 1264 | } 1265 | } 1266 | -------------------------------------------------------------------------------- /src/css/highlightjs.scss: -------------------------------------------------------------------------------- 1 | .markplus { 2 | .markdown-body pre code.hljs { 3 | overflow-x: auto; 4 | padding: 0; 5 | display: block; 6 | } 7 | 8 | &[data-theme="dark"] { 9 | .markdown-body pre code.hljs { 10 | color: #c9d1d9; 11 | background: #151b23; 12 | 13 | .hljs-doctag, 14 | .hljs-keyword, 15 | .hljs-meta .hljs-keyword, 16 | .hljs-template-tag, 17 | .hljs-template-variable, 18 | .hljs-type, 19 | .hljs-variable.language_ { 20 | color: #ff7b72; 21 | } 22 | .hljs-title, 23 | .hljs-title.class_, 24 | .hljs-title.class_.inherited__, 25 | .hljs-title.function_ { 26 | color: #d2a8ff; 27 | } 28 | .hljs-attr, 29 | .hljs-attribute, 30 | .hljs-literal, 31 | .hljs-meta, 32 | .hljs-number, 33 | .hljs-operator, 34 | .hljs-variable, 35 | .hljs-selector-attr, 36 | .hljs-selector-class, 37 | .hljs-selector-id { 38 | color: #79c0ff; 39 | } 40 | .hljs-regexp, 41 | .hljs-string, 42 | .hljs-meta .hljs-string { 43 | color: #a5d6ff; 44 | } 45 | .hljs-built_in, 46 | .hljs-symbol { 47 | color: #ffa657; 48 | } 49 | .hljs-comment, 50 | .hljs-code, 51 | .hljs-formula { 52 | color: #8b949e; 53 | } 54 | .hljs-name, 55 | .hljs-quote, 56 | .hljs-selector-tag, 57 | .hljs-selector-pseudo { 58 | color: #7ee787; 59 | } 60 | .hljs-subst { 61 | color: #c9d1d9; 62 | } 63 | .hljs-section { 64 | color: #1f6feb; 65 | font-weight: bold; 66 | } 67 | .hljs-bullet { 68 | color: #f2cc60; 69 | } 70 | .hljs-emphasis { 71 | color: #c9d1d9; 72 | font-style: italic; 73 | } 74 | .hljs-strong { 75 | color: #c9d1d9; 76 | font-weight: bold; 77 | } 78 | .hljs-addition { 79 | color: #aff5b4; 80 | background-color: #033a16; 81 | } 82 | .hljs-deletion { 83 | color: #ffdcd7; 84 | background-color: #67060c; 85 | } 86 | } 87 | } 88 | 89 | &[data-theme="light"] { 90 | .markdown-body pre code.hljs { 91 | color: #24292e; 92 | background: #f6f8fa; 93 | 94 | .hljs-doctag, 95 | .hljs-keyword, 96 | .hljs-meta .hljs-keyword, 97 | .hljs-template-tag, 98 | .hljs-template-variable, 99 | .hljs-type, 100 | .hljs-variable.language_ { 101 | color: #d73a49; 102 | } 103 | .hljs-title, 104 | .hljs-title.class_, 105 | .hljs-title.class_.inherited__, 106 | .hljs-title.function_ { 107 | color: #6f42c1; 108 | } 109 | .hljs-attr, 110 | .hljs-attribute, 111 | .hljs-literal, 112 | .hljs-meta, 113 | .hljs-number, 114 | .hljs-operator, 115 | .hljs-variable, 116 | .hljs-selector-attr, 117 | .hljs-selector-class, 118 | .hljs-selector-id { 119 | color: #005cc5; 120 | } 121 | .hljs-regexp, 122 | .hljs-string, 123 | .hljs-meta .hljs-string { 124 | color: #032f62; 125 | } 126 | .hljs-built_in, 127 | .hljs-symbol { 128 | color: #e36209; 129 | } 130 | .hljs-comment, 131 | .hljs-code, 132 | .hljs-formula { 133 | color: #6a737d; 134 | } 135 | .hljs-name, 136 | .hljs-quote, 137 | .hljs-selector-tag, 138 | .hljs-selector-pseudo { 139 | color: #22863a; 140 | } 141 | .hljs-subst { 142 | color: #24292e; 143 | } 144 | .hljs-section { 145 | color: #005cc5; 146 | font-weight: bold; 147 | } 148 | .hljs-bullet { 149 | color: #735c0f; 150 | } 151 | .hljs-emphasis { 152 | color: #24292e; 153 | font-style: italic; 154 | } 155 | .hljs-strong { 156 | color: #24292e; 157 | font-weight: bold; 158 | } 159 | .hljs-addition { 160 | color: #22863a; 161 | background-color: #f0fff4; 162 | } 163 | .hljs-deletion { 164 | color: #b31d28; 165 | background-color: #ffeef0; 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/css/index.scss: -------------------------------------------------------------------------------- 1 | @use "./github-markdown.scss"; 2 | @use "./highlightjs.scss"; 3 | 4 | .markplus { 5 | /* 6 | ============================== 7 | start of light/dark theme 8 | ============================== 9 | */ 10 | &[data-theme="light"] { 11 | .cm-editor { 12 | background-color: #fff; 13 | color: #1f2328; 14 | .cm-content { 15 | caret-color: #1f2328; 16 | } 17 | } 18 | .toolbar { 19 | color: #666; 20 | background-color: white; 21 | } 22 | } 23 | &[data-theme="dark"] { 24 | .cm-editor { 25 | background-color: #0d1117; 26 | color: #f0f6fc; 27 | .cm-content { 28 | caret-color: #f0f6fc; 29 | } 30 | } 31 | .toolbar { 32 | color: #f0f6fc !important; 33 | background-color: #0d1117 !important; 34 | } 35 | ::-webkit-scrollbar { 36 | width: 12px; 37 | } 38 | ::-webkit-scrollbar-thumb { 39 | background-color: rgba(255, 255, 255, 0.6); 40 | border-radius: 10px; 41 | } 42 | ::-webkit-scrollbar-track { 43 | background-color: rgba(50, 50, 50, 0.8); 44 | } 45 | * { 46 | scrollbar-width: thin; 47 | scrollbar-color: rgba(255, 255, 255, 0.6) rgba(50, 50, 50, 0.8); 48 | } 49 | } 50 | /* 51 | ============================ 52 | end of light/dark theme 53 | ============================ 54 | */ 55 | 56 | /* 57 | ==================== 58 | start of layout 59 | ==================== 60 | */ 61 | height: 100%; 62 | .rows-grid { 63 | display: grid; 64 | grid-template-rows: 20px 6px 1fr; 65 | height: 100%; 66 | } 67 | 68 | .cols-grid { 69 | display: grid; 70 | grid-template-columns: 1fr 6px 1fr; 71 | height: 100%; 72 | overflow: hidden; 73 | } 74 | 75 | .left-panel { 76 | height: 100%; 77 | overflow: scroll; 78 | } 79 | 80 | .right-panel { 81 | height: 100%; 82 | overflow: scroll; 83 | position: relative; 84 | } 85 | 86 | .row-gutter { 87 | grid-column: 1/-1; 88 | grid-row: 2; 89 | cursor: pointer; 90 | } 91 | 92 | .col-gutter { 93 | grid-row: 1/-1; 94 | grid-column: 2; 95 | cursor: col-resize; 96 | } 97 | .editor, 98 | .cm-editor { 99 | height: 100%; 100 | } 101 | 102 | .markdown-body { 103 | box-sizing: border-box; 104 | min-width: 200px; 105 | max-width: 980px; 106 | margin: 0 auto; 107 | padding: 45px; 108 | min-height: 100%; 109 | } 110 | 111 | @media (max-width: 767px) { 112 | .markdown-body { 113 | padding: 15px; 114 | } 115 | } 116 | /* 117 | ================== 118 | end of layout 119 | ================== 120 | */ 121 | 122 | /* 123 | ==================== 124 | start of others 125 | ==================== 126 | */ 127 | .gutter { 128 | background-color: #dddddd; 129 | } 130 | .gutter:hover { 131 | background-color: #c4e1a4; 132 | } 133 | 134 | .toolbar { 135 | overflow: hidden; 136 | padding-left: 3px; 137 | 138 | /* toolbar cannot be selected */ 139 | -webkit-touch-callout: none; 140 | -webkit-user-select: none; 141 | -khtml-user-select: none; 142 | -moz-user-select: none; 143 | -ms-user-select: none; 144 | user-select: none; 145 | } 146 | .toolbar i { 147 | padding: 2px 5px; 148 | border: 1px solid transparent; 149 | } 150 | .about-icon { 151 | height: 20px; 152 | vertical-align: top; 153 | margin: 0 2px; 154 | } 155 | 156 | .toolbar i:hover, 157 | .about-icon:hover { 158 | background-color: #999; 159 | border-color: #ccc; 160 | cursor: pointer; 161 | } 162 | 163 | .dividor { 164 | font-style: normal; 165 | color: #999; 166 | margin: 0 8px; 167 | } 168 | 169 | /* fix clicking anchor scroll issue */ 170 | h1, 171 | h2, 172 | h3, 173 | h4, 174 | h5, 175 | h6 { 176 | scroll-margin-top: 1px; 177 | } 178 | 179 | /* custom container */ 180 | .success, 181 | .info, 182 | .warning, 183 | .danger { 184 | padding: 15px; 185 | margin-bottom: 20px; 186 | border: 1px solid transparent; 187 | border-radius: 4px; 188 | } 189 | .success > p:last-child, 190 | .info > p:last-child, 191 | .warning > p:last-child, 192 | .danger > p:last-child { 193 | margin-bottom: 0; 194 | } 195 | .success { 196 | color: #3c763d; 197 | background-color: #dff0d8; 198 | border-color: #d6e9c6; 199 | } 200 | .info { 201 | color: #31708f; 202 | background-color: #d9edf7; 203 | border-color: #bce8f1; 204 | } 205 | .warning { 206 | color: #8a6d3b; 207 | background-color: #fcf8e3; 208 | border-color: #faebcc; 209 | } 210 | .danger { 211 | color: #a94442; 212 | background-color: #f2dede; 213 | border-color: #ebccd1; 214 | } 215 | /* 216 | ================== 217 | end of others 218 | ================== 219 | */ 220 | } 221 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConfigProvider } from "antd"; 2 | import { manage } from "manate"; 3 | import React, { 4 | forwardRef, 5 | ReactElement, 6 | useEffect, 7 | useImperativeHandle, 8 | useMemo, 9 | } from "react"; 10 | 11 | import Layout from "./components/layout.tsx"; 12 | import { Store } from "./store.ts"; 13 | 14 | export const defaultToolbarItems = [ 15 | "about", 16 | "|", 17 | "bold", 18 | "italic", 19 | "strikethrough", 20 | "underline", 21 | "mark", 22 | "|", 23 | "emoji", 24 | "fontawesome", 25 | "|", 26 | "quote", 27 | "unordered-list", 28 | "ordered-list", 29 | "unchecked-list", 30 | "checked-list", 31 | "|", 32 | "link", 33 | "image", 34 | "code", 35 | "table", 36 | "|", 37 | "math", 38 | "flowchart", 39 | ]; 40 | 41 | export interface MarkPlusRef { 42 | getStore: () => Store; 43 | } 44 | 45 | const MarkPlus = forwardRef((props: { 46 | markdown?: string; 47 | mode?: "editor" | "preview" | "both"; 48 | toolbar?: "show" | "hide" | "none"; 49 | theme?: "light" | "dark" | "auto"; 50 | toolbarItems?: (string | ReactElement)[]; 51 | onChange?: (markdown: string) => void; 52 | }, ref) => { 53 | const { markdown, mode, toolbar, theme, toolbarItems } = props; 54 | const store = useMemo(() => { 55 | return manage(new Store()); 56 | }, []); 57 | useImperativeHandle(ref, () => ({ 58 | getStore: () => store, // Expose the `store` variable 59 | })); 60 | useEffect(() => { 61 | store.preferences.mode = mode ?? "both"; 62 | store.preferences.toolbar = toolbar ?? "show"; 63 | store.preferences.theme = theme ?? "auto"; 64 | store.preferences.toolbarItems = toolbarItems ?? defaultToolbarItems; 65 | store.onChange = props.onChange ?? (() => {}); 66 | }, [mode, toolbar, theme, toolbarItems, store, props.onChange]); 67 | useEffect(() => { 68 | if (store.editor) { 69 | const currentSelection = store.editor.state.selection; 70 | store.editor.dispatch({ 71 | changes: { 72 | from: 0, 73 | to: store.editor.state.doc.length, 74 | insert: markdown ?? "", 75 | }, 76 | selection: currentSelection, 77 | }); 78 | } 79 | }, [markdown, store.editor]); 80 | return ( 81 | 88 | 89 | 90 | ); 91 | }); 92 | 93 | export default MarkPlus; 94 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { EditorView } from "@codemirror/view"; 2 | import { ReactElement } from "react"; 3 | 4 | export class ModalState { 5 | isOpen = false; 6 | 7 | constructor(private store: Store) {} 8 | 9 | open() { 10 | this.isOpen = true; 11 | } 12 | 13 | close() { 14 | this.isOpen = false; 15 | this.store.editor?.focus(); 16 | } 17 | } 18 | 19 | class Preferences { 20 | mode: "editor" | "preview" | "both" = "both"; 21 | toolbar: "show" | "hide" | "none" = "show"; 22 | theme: "light" | "dark" | "auto" = "auto"; 23 | toolbarItems: (string | ReactElement)[] = []; 24 | } 25 | 26 | let counter = 0; 27 | export class Store { 28 | uid = `markplus-${counter++}`; 29 | editor!: EditorView; 30 | onChange: (markdown: string) => void = () => {}; 31 | 32 | modals = { 33 | about: new ModalState(this), 34 | emoji: new ModalState(this), 35 | fontAwesome: new ModalState(this), 36 | }; 37 | 38 | preferences = new Preferences(); 39 | } 40 | -------------------------------------------------------------------------------- /src/sync-scroll.ts: -------------------------------------------------------------------------------- 1 | import debounce from "debounce"; 2 | 3 | import { Store } from "./store.ts"; 4 | import { animate } from "./utils.ts"; 5 | 6 | type IScroll = { 7 | lastMarker: number; 8 | nextMarker: number; 9 | percentage: number; 10 | }; 11 | 12 | export const generateScrollMethods = (store: Store) => { 13 | let scrollingSide: string | null = null; 14 | let timeoutHandle: number | null = null; 15 | const scrollSide = ( 16 | side: "left" | "right", 17 | howToScroll: () => void, 18 | ): void => { 19 | if (scrollingSide !== null && scrollingSide !== side) { 20 | return; // the other side hasn't finished scrolling 21 | } 22 | scrollingSide = side; 23 | if (timeoutHandle) { 24 | clearTimeout(timeoutHandle); 25 | } 26 | timeoutHandle = setTimeout(() => { 27 | scrollingSide = null; 28 | }, 512) as unknown as number; 29 | howToScroll(); 30 | }; 31 | 32 | const scrollEditor = (targetLineNumber: number): void => { 33 | scrollSide("left", () => { 34 | animate( 35 | (lineNumber) => { 36 | const line = store.editor.state.doc.line(lineNumber); 37 | const dom = store.editor.scrollDOM; 38 | const lineCoords = store.editor.coordsAtPos(line.from); 39 | if (lineCoords) { 40 | dom.scrollTop += lineCoords.top - dom.getBoundingClientRect().top; 41 | } 42 | }, 43 | store.editor.state.doc.lineAt( 44 | store.editor.lineBlockAtHeight(store.editor.scrollDOM.scrollTop).from, 45 | ).number, 46 | targetLineNumber, 47 | 128, 48 | ); 49 | }); 50 | }; 51 | 52 | const scrollPreview = (scrollTop: number): void => { 53 | const rightPanel = document.querySelector(`#${store.uid} .right-panel`)!; 54 | scrollSide("right", () => { 55 | animate( 56 | (i) => (rightPanel.scrollTop = i), 57 | rightPanel.scrollTop, 58 | scrollTop, 59 | 128, 60 | ); 61 | }); 62 | }; 63 | 64 | const getEditorScroll = (): IScroll => { 65 | const lineMarkers = document.querySelectorAll( 66 | `#${store.uid} .right-panel .preview > [data-sl]`, 67 | ) as NodeListOf; 68 | const lines: number[] = []; 69 | lineMarkers.forEach((element: HTMLElement) => { 70 | lines.push(parseInt(element.dataset.sl!, 10)); 71 | }); 72 | const currentLine = store.editor.state.doc.lineAt( 73 | store.editor.lineBlockAtHeight(store.editor.scrollDOM.scrollTop).from, 74 | ).number; 75 | let lastMarker: number | null = null; 76 | let nextMarker: number | null = null; 77 | for (let i = 0; i < lines.length; i++) { 78 | if (lines[i] < currentLine) { 79 | lastMarker = lines[i]; 80 | } else { 81 | nextMarker = lines[i]; 82 | break; 83 | } 84 | } 85 | let percentage = 0; 86 | if (lastMarker && nextMarker && lastMarker !== nextMarker) { 87 | percentage = (currentLine - lastMarker) / (nextMarker - lastMarker); 88 | } 89 | // returns two neighboring markers' lines, and current scroll percentage between two markers 90 | const r = { lastMarker: lastMarker, nextMarker: nextMarker, percentage }; 91 | return r as IScroll; 92 | }; 93 | 94 | const setPreviewScroll = (editorScroll: IScroll): void => { 95 | let lastPosition = 0; 96 | let nextPosition = document.querySelector( 97 | `#${store.uid} .right-panel .preview`, 98 | )!.offsetHeight - 99 | document.querySelector(`#${store.uid} .right-panel`)! 100 | .offsetHeight; // maximum scroll 101 | 102 | if (editorScroll.lastMarker) { 103 | const lastMarkerElement = document.querySelector( 104 | `#${store.uid} .right-panel .preview > [data-sl="${editorScroll.lastMarker}"]`, 105 | ); 106 | if (lastMarkerElement) { 107 | lastPosition = lastMarkerElement.offsetTop; 108 | } 109 | } 110 | 111 | if (editorScroll.nextMarker) { 112 | const nextMarkerElement = document.querySelector( 113 | `#${store.uid} .right-panel .preview > [data-sl="${editorScroll.nextMarker}"]`, 114 | ); 115 | if (nextMarkerElement) { 116 | nextPosition = nextMarkerElement.offsetTop; 117 | } 118 | } 119 | const scrollTop = lastPosition + 120 | (nextPosition - lastPosition) * editorScroll.percentage; // right scroll according to left percentage 121 | scrollPreview(scrollTop); 122 | }; 123 | 124 | const getPreviewScroll = (): IScroll => { 125 | const rightPanel = document.querySelector( 126 | `#${store.uid} .right-panel`, 127 | )!; 128 | const preview = document.querySelector( 129 | `#${store.uid} .right-panel .preview`, 130 | )!; 131 | const scroll = rightPanel.scrollTop; 132 | let lastLine = 1; // editor line starts with 1 133 | let lastScroll = 0; 134 | let nextLine = store.editor.state.doc.toString().split("\n").length; // number of lines of markdown 135 | let nextScroll = preview.offsetHeight - rightPanel.offsetHeight; // maximum scroll 136 | const lineMarkers = document.querySelectorAll( 137 | `#${store.uid} .right-panel .preview > [data-sl]`, 138 | ); 139 | for (let i = 0; i < lineMarkers.length; i++) { 140 | const lineMarker = lineMarkers[i]; 141 | if (lineMarker.offsetTop < scroll) { 142 | lastLine = parseInt(lineMarker.dataset.sl!, 10); 143 | lastScroll = lineMarker.offsetTop; 144 | } else { 145 | nextLine = parseInt(lineMarker.dataset.sl!, 10); 146 | nextScroll = lineMarker.offsetTop; 147 | break; 148 | } 149 | } 150 | let percentage = 0; 151 | if (lastScroll !== nextScroll) { 152 | percentage = (scroll - lastScroll) / (nextScroll - lastScroll); 153 | } 154 | // returns two neighboring marker lines, and current scroll percentage between two markers 155 | const r = { 156 | lastMarker: lastLine, 157 | nextMarker: nextLine, 158 | percentage: percentage, 159 | }; 160 | return r; 161 | }; 162 | 163 | const setEditorScroll = (previewScroll: IScroll): void => { 164 | const targetLineNumber = 165 | (previewScroll.nextMarker - previewScroll.lastMarker) * 166 | previewScroll.percentage + 167 | previewScroll.lastMarker; 168 | scrollEditor(targetLineNumber); 169 | }; 170 | 171 | const syncPreview = debounce(() => { 172 | // sync right with left 173 | if (scrollingSide !== "left") { 174 | const editorScroll = getEditorScroll(); 175 | setPreviewScroll(editorScroll); 176 | } 177 | }, 128); 178 | 179 | const syncEditor = debounce(() => { 180 | // sync left with right 181 | if (scrollingSide !== "right") { 182 | const previewScroll = getPreviewScroll(); 183 | setEditorScroll(previewScroll); 184 | } 185 | }, 128); 186 | return { syncPreview, syncEditor }; 187 | }; 188 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const animate = ( 2 | move: (i: number) => void, 3 | start: number, 4 | end: number, 5 | duration: number, 6 | ): void => { 7 | const startTime = performance.now(); 8 | const animate = (currentTime: number) => { 9 | const timeElapsed = currentTime - startTime; 10 | if (timeElapsed < 0) { 11 | return requestAnimationFrame(animate); 12 | } 13 | // line number should be integer 14 | const target = Math.round( 15 | start + (end - start) * Math.min(timeElapsed / duration, 1), 16 | ); 17 | move(target); 18 | if (timeElapsed < duration) { 19 | requestAnimationFrame(animate); 20 | } 21 | }; 22 | requestAnimationFrame(animate); 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "jsx": "react", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "module": "nodenext", 8 | "moduleResolution": "nodenext", 9 | "skipLibCheck": true 10 | }, 11 | "files": ["src/index.tsx"] 12 | } 13 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from "@vitejs/plugin-react"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | root: "./demo", 6 | server: { 7 | port: 3000, 8 | }, 9 | plugins: [react()], 10 | base: "./", 11 | }); 12 | --------------------------------------------------------------------------------