├── public ├── favicon.ico ├── robots.txt ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── writespace-opengraph.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── site.webmanifest ├── manifest.json ├── index.html ├── safari-pinned-tab.svg └── userguide.html ├── src ├── setupTests.js ├── App.test.js ├── index.js ├── index.css ├── reportWebVitals.js ├── App.js ├── components │ ├── CodeBlock.js │ ├── Markdown.js │ ├── TextCounter.js │ ├── ToolTray.js │ ├── TypeBox.js │ ├── Download.js │ ├── Print.js │ ├── FirstTime.js │ ├── NewDoc.js │ ├── Export.js │ ├── Layout.js │ ├── Copy.js │ ├── Files.js │ └── Settings │ │ └── Settings.js ├── App.css └── theme.js ├── .gitignore ├── README.md ├── LICENSE.md └── package.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/writespace-opengraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/writespace-opengraph.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeHawk314/WriteSpace/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #12a4ce 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import ReactGA from "react-ga"; 6 | 7 | ReactGA.initialize("UA-189044863-1"); 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .eslintcache -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import "./App.css"; 3 | 4 | // Theme 5 | import { MuiThemeProvider } from "@material-ui/core"; 6 | import theme from "./theme"; 7 | 8 | // Analytics 9 | import ReactGA from "react-ga"; 10 | 11 | import Layout from "./components/Layout"; 12 | 13 | function App() { 14 | useEffect(() => { 15 | ReactGA.pageview(window.location.pathname); 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WriteSpace", 3 | "name": "WriteSpace - Notepad with Markdown & KaTeX", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "/android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#12a4ce", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SyntaxHighlighter from "react-syntax-highlighter"; 3 | import { github } from "react-syntax-highlighter/dist/esm/styles/hljs"; 4 | 5 | const CodeBlock = ({ language, value }) => { 6 | return ( 7 | 23 | ); 24 | }; 25 | 26 | export default CodeBlock; 27 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | width: 100%; 9 | margin: 0; 10 | } 11 | 12 | img { 13 | max-width: 100%; 14 | max-height: 100vh; 15 | page-break-inside: avoid; 16 | display: block; 17 | } 18 | 19 | #hiddenOutput { 20 | position: fixed; 21 | right: -60rem; 22 | width: auto; 23 | max-width: 40rem; 24 | min-width: 0px; 25 | } 26 | 27 | #hiddenOutput * .katex .op-symbol.large-op { 28 | top: -0.65em !important; 29 | } 30 | 31 | @media print { 32 | .hidePrint { 33 | display: none !important; 34 | } 35 | 36 | .showPrint * { 37 | -webkit-print-color-adjust: exact !important; 38 | } 39 | .showPrint { 40 | margin: 0 !important; 41 | display: block !important; 42 | max-width: none; 43 | } 44 | 45 | .leftAlign { 46 | position: absolute; 47 | left: 0; 48 | margin: 10px; 49 | padding: 10px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Markdown.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import RemarkMathPlugin from "remark-math"; 4 | import { BlockMath, InlineMath } from "react-katex"; 5 | import CodeBlock from "./CodeBlock"; 6 | import "katex/dist/katex.min.css"; 7 | 8 | const _mapProps = ({ settings, ...props }) => ({ 9 | ...props, 10 | escapeHtml: false, 11 | plugins: [RemarkMathPlugin], 12 | renderers: { 13 | ...props.renderers, 14 | math: ({ value }) => 15 | settings.katexBlock ? {value} : value, 16 | inlineMath: ({ value }) => 17 | settings.katexInline ? {value} : value, 18 | code: ({ value }) => , 19 | }, 20 | }); 21 | 22 | const Markdown = ({ settings, ...props }) => ( 23 | 24 | ); 25 | 26 | export default Markdown; 27 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from "@material-ui/core/styles"; 2 | 3 | export default createMuiTheme({ 4 | breakpoints: { 5 | values: { 6 | xs: 0, 7 | sm: 600, 8 | md: 960, 9 | lg: 1280, 10 | xl: 1920, 11 | }, 12 | }, 13 | palette: { 14 | primary: { 15 | main: "hsl(193, 84%, 44%)", 16 | light: "hsl(193, 64%, 74%)", 17 | dark: "hsl(193, 94%, 28%)", 18 | }, 19 | common: { 20 | white: "white", 21 | gray: "gray", 22 | lightGray: "hsl(0, 0%, 91%)", 23 | lightestGray: "hsl(0, 0%, 96%)", 24 | red: "hsl(355, 78%, 46%)", 25 | green: "hsl(127, 43%, 59%)", 26 | black: "black", 27 | on: "hsl(165, 90%, 60%)", 28 | off: "hsl(0, 90%, 65%)", 29 | alerts: "hsla(0,0%,100%, 0.3)", 30 | }, 31 | }, 32 | typography: { 33 | useNextVariants: true, 34 | fontFamily: "Roboto", 35 | }, 36 | spacing: 8, 37 | singleSpacing: 1, 38 | }); 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WriteSpace 2 | 3 | A minimalist web notepad with Markdown and KaTeX support. Use it at [https://writespace.app](https://www.writespace.app). 4 | 5 | Supports Markdown. [Markdown syntax](https://www.markdownguide.org/cheat-sheet/). 6 | 7 | Supports math expressions via KaTex between $ delimiters. [KaTeX syntax](https://katex.org/docs/supported.html) Example: 8 | 9 | ``` 10 | Euler's identity: $e^{i \pi} + 1 = 0$ 11 | ``` 12 | 13 | Supports code blocks with syntax highlighting between triple backquotes. 14 | 15 | ## Features 16 | 17 | - Markdown 18 | - KaTeX 19 | - Live output rendering 20 | - Code block syntax highlighting 21 | - Print rendered output or plain text 22 | - Download/copy as txt, image or html 23 | 24 | ## Future feature ideas 25 | 26 | - Save to Google Drive, Box, Dropbox 27 | 28 | ## How to run locally 29 | 30 | 1. Clone repo 31 | 2. cd to directory 32 | 3. npm i _(installs npm packages)_ 33 | 4. npm start _(starts development server)_ 34 | -------------------------------------------------------------------------------- /src/components/TextCounter.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | container: { 6 | position: "fixed", 7 | bottom: 30, 8 | left: 30, 9 | padding: 10, 10 | borderRadius: 5, 11 | backgroundColor: theme.palette.common.lightestGray, 12 | boxShadow: "0px 0px 5px lightgrey", 13 | fontFamily: "Roboto", 14 | fontSize: "1rem", 15 | }, 16 | })); 17 | 18 | function TextCounter({ writing, open }) { 19 | const classes = useStyles(); 20 | const [selected, setSelected] = useState(); 21 | 22 | document.onselectionchange = function () { 23 | setSelected(document.getSelection().toString()); 24 | }; 25 | 26 | return ( 27 | <> 28 | {open && ( 29 |
30 | {selected && "Selected: "} 31 | {(selected || writing).match(/(?:\w|['-]+\w)+/g)?.length || 0} words |{" "} 32 | {(selected || writing).length} chars 33 |
34 | )} 35 | 36 | ); 37 | } 38 | 39 | export default TextCounter; 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 CodeHawk314 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 | -------------------------------------------------------------------------------- /src/components/ToolTray.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import Settings from "./Settings/Settings"; 4 | import Download from "./Download"; 5 | import Copy from "./Copy"; 6 | import NewDoc from "./NewDoc"; 7 | import Files from "./Files"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | tray: { 11 | position: "absolute", 12 | top: 30, 13 | right: 30, 14 | padding: 3, 15 | borderRadius: "9999px", 16 | // backgroundColor: theme.palette.common.lightGray, 17 | }, 18 | })); 19 | 20 | function ToolTray({ writing, setWriting, settings, setSettings, ...props }) { 21 | const classes = useStyles(); 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | ); 32 | } 33 | 34 | export default ToolTray; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "writespace", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.3", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/styles": "^4.11.3", 9 | "html-to-pdfmake": "^2.1.4", 10 | "html2canvas": "^1.0.0-rc.7", 11 | "juice": "^7.0.0", 12 | "pdfmake": "^0.1.70", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-ga": "^3.3.0", 16 | "react-katex": "^2.0.2", 17 | "react-markdown": "^5.0.3", 18 | "react-scripts": "4.0.1", 19 | "react-syntax-highlighter": "^15.4.3", 20 | "remark-math": "^4.0.0" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "GENERATE_SOURCEMAP=false react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@testing-library/jest-dom": "^5.11.4", 48 | "@testing-library/react": "^11.1.0", 49 | "@testing-library/user-event": "^12.1.10", 50 | "babel-plugin-import": "^1.13.3", 51 | "customize-cra": "^1.0.0", 52 | "react-app-rewired": "^2.1.8", 53 | "web-vitals": "^0.2.4" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/TypeBox.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { TextField } from "@material-ui/core"; 3 | 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | input: { 8 | width: "100%", 9 | minHeight: "calc(70vh - 120px)", 10 | alignItems: "start", 11 | fontFamily: "inherit", 12 | fontSize: "inherit", 13 | borderStyle: "none", 14 | }, 15 | })); 16 | 17 | function TypeBox({ writing, setWriting, ...props }) { 18 | const classes = useStyles(); 19 | const [saveTimeout, setSaveTimeout] = useState(false); 20 | 21 | const onChange = (event) => { 22 | setWriting(event.target.value); 23 | 24 | // Save current writing to localstorage every 3 seconds when typing 25 | if (!saveTimeout) { 26 | setSaveTimeout(true); 27 | setTimeout(() => { 28 | localStorage.setItem("currentPad", event.target.value); 29 | setSaveTimeout(false); 30 | }, 1000); 31 | } 32 | }; 33 | 34 | return ( 35 | <> 36 | 49 |
54 | 55 | ); 56 | } 57 | 58 | export default TypeBox; 59 | -------------------------------------------------------------------------------- /src/components/Download.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import ReactGA from "react-ga"; 4 | import GetAppOutlinedIcon from "@material-ui/icons/GetAppOutlined"; 5 | import { IconButton, Tooltip } from "@material-ui/core"; 6 | 7 | import { getExported } from "./Export"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | button: { 11 | backgroundColor: theme.palette.common.lightestGray, 12 | margin: 3, 13 | }, 14 | })); 15 | 16 | function Download({ writing, settings }) { 17 | const classes = useStyles(); 18 | 19 | const download = (toDownload, ext) => { 20 | let element = document.createElement("a"); 21 | element.download = "download." + ext; 22 | element.href = 23 | ext === "png" 24 | ? toDownload?.toDataURL() 25 | : "data:text/plain;charset=utf-8," + encodeURIComponent(toDownload); 26 | 27 | element.style.display = "none"; 28 | element.click(); 29 | }; 30 | 31 | const onDownloadButtonClick = () => { 32 | if (settings.dlFormat === "pdf") { 33 | window.print(); 34 | return; 35 | } 36 | getExported(settings.dlFormat, writing, settings).then((r) => { 37 | console.log(typeof r); 38 | download(r, settings.dlFormat); 39 | }); 40 | 41 | ReactGA.event({ 42 | category: "Export", 43 | action: "Download doc", 44 | label: settings.dlFormat, 45 | }); 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | export default Download; 58 | -------------------------------------------------------------------------------- /src/components/Print.js: -------------------------------------------------------------------------------- 1 | function Print({ settings, setSettings }) { 2 | const beforePrint = () => { 3 | if (window.aaa == null) { 4 | window.aaa = settings.showOutput; 5 | window.aaa === false && setSettings({ ...settings, showOutput: true }); 6 | 7 | document.getElementsByClassName("typebox")[0]?.classList.add("hidePrint"); 8 | 9 | document.getElementById( 10 | "textFieldCopy" 11 | ).innerHTML = settings.printRendered 12 | ? null 13 | : document 14 | .getElementById("textField") 15 | .innerHTML.replace(/\n/g, "
"); 16 | 17 | !settings.showOutput && 18 | document.getElementById("markdownOutput")?.classList.add("hide"); 19 | 20 | document 21 | .getElementById("markdownOutput") 22 | ?.classList.add(settings.printRendered ? "showPrint" : "hidePrint"); 23 | } 24 | }; 25 | 26 | const afterPrint = () => { 27 | document 28 | .getElementsByClassName("typebox")[0] 29 | ?.classList.remove("hidePrint"); 30 | 31 | document 32 | .getElementById("markdownOutput") 33 | ?.classList.remove("showPrint", "hidePrint", "hide"); 34 | 35 | document.getElementById("textFieldCopy").innerHTML = null; 36 | 37 | window.aaa === false && setSettings({ ...settings, showOutput: false }); 38 | delete window.aaa; 39 | }; 40 | 41 | if (window.matchMedia) { 42 | var mediaQueryList = window.matchMedia("print"); 43 | mediaQueryList.addListener(function (mql) { 44 | if (mql.matches) { 45 | beforePrint(); 46 | } else { 47 | afterPrint(); 48 | } 49 | }); 50 | } 51 | 52 | window.onbeforeprint = beforePrint; 53 | window.onafterprint = afterPrint; 54 | 55 | return null; 56 | } 57 | 58 | export default Print; 59 | -------------------------------------------------------------------------------- /src/components/FirstTime.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { IconButton, Link } from "@material-ui/core"; 4 | import CloseIcon from "@material-ui/icons/Close"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | container: { 8 | position: "absolute", 9 | top: 95, 10 | right: 36, 11 | padding: 10, 12 | borderRadius: 5, 13 | backgroundColor: theme.palette.common.lightestGray, 14 | fontFamily: "Roboto", 15 | fontSize: "11pt", 16 | [theme.breakpoints.down("xs")]: { 17 | top: 90, 18 | padding: "4px 10px", 19 | fontSize: "10pt", 20 | }, 21 | }, 22 | close: { 23 | padding: 3, 24 | marginRight: 5, 25 | }, 26 | closeIcon: { 27 | fontSize: 13, 28 | }, 29 | })); 30 | 31 | function FirstTime() { 32 | const classes = useStyles(); 33 | const [firstTime, setFirstTime] = useState(); 34 | 35 | const close = () => { 36 | setFirstTime(false); 37 | localStorage.setItem("notFirstTime", 1); 38 | }; 39 | 40 | useEffect(() => { 41 | setFirstTime(!localStorage.getItem("notFirstTime")); 42 | setTimeout(() => { 43 | setFirstTime(false); 44 | localStorage.setItem("notFirstTime", 1); 45 | }, 30000); 46 | }, []); 47 | 48 | return ( 49 | <> 50 | {firstTime && ( 51 |
52 | 53 | 54 | 55 | First time? View the{" "} 56 | 57 | user guide 58 | 59 | . 60 |
61 | )} 62 | 63 | ); 64 | } 65 | 66 | export default FirstTime; 67 | -------------------------------------------------------------------------------- /src/components/NewDoc.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import ReactGA from "react-ga"; 4 | import AddIcon from "@material-ui/icons/Add"; 5 | import { IconButton, Tooltip } from "@material-ui/core"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | button: { 9 | backgroundColor: theme.palette.common.lightestGray, 10 | margin: 3, 11 | }, 12 | })); 13 | 14 | function NewDoc({ writing, setWriting }) { 15 | const classes = useStyles(); 16 | 17 | const onNewDocButtonClick = () => { 18 | if (!writing) { 19 | // Already a fresh slate 20 | return; 21 | } 22 | 23 | const currentPadCreatedOn = parseInt( 24 | localStorage.getItem("currentPadCreatedOn") 25 | ); 26 | let files = JSON.parse(localStorage.getItem("files") || "[]") || []; 27 | const currentFileIndex = files.findIndex( 28 | (elem) => elem.createdOn === currentPadCreatedOn 29 | ); 30 | 31 | if (currentFileIndex !== -1) { 32 | if (writing !== files[currentFileIndex].data) { 33 | files.push({ 34 | ...files.splice(currentFileIndex, 1)[0], 35 | data: writing, 36 | }); 37 | } 38 | } else { 39 | files.push({ 40 | data: writing, 41 | createdOn: currentPadCreatedOn || Date.now() - 100, 42 | }); 43 | } 44 | 45 | localStorage.setItem("files", JSON.stringify(files)); 46 | localStorage.setItem("currentPad", ""); 47 | localStorage.setItem("currentPadCreatedOn", Date.now()); 48 | setWriting(""); 49 | 50 | ReactGA.event({ 51 | category: "Files", 52 | action: "New doc", 53 | }); 54 | }; 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default NewDoc; 66 | -------------------------------------------------------------------------------- /src/components/Export.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import Markdown from "./Markdown"; 3 | import { renderToString } from "react-dom/server"; 4 | import Juice from "juice"; 5 | import html2canvas from "html2canvas"; 6 | 7 | const getDocCss = () => { 8 | let css = []; 9 | for (let sheeti = 0; sheeti < document.styleSheets.length; sheeti++) { 10 | let sheet = document.styleSheets[sheeti]; 11 | let rules = null; 12 | try { 13 | rules = "cssRules" in sheet ? sheet.cssRules : sheet.rules; 14 | } catch {} 15 | if (rules) { 16 | for (let rulei = 0; rulei < rules.length; rulei++) { 17 | let rule = rules[rulei]; 18 | if ("cssText" in rule) css.push(rule.cssText); 19 | else 20 | css.push(rule.selectorText + " {\n" + rule.style.cssText + "\n}\n"); 21 | } 22 | } 23 | } 24 | return css.join("\n"); 25 | }; 26 | 27 | const exportPng = async (rendered) => { 28 | return new Promise((resolve, reject) => { 29 | let elem = document.getElementById("hiddenOutput"); 30 | ReactDOM.render(rendered, elem, () => { 31 | setTimeout(() => { 32 | html2canvas(elem, { 33 | useCORS: true, 34 | allowTaint: true, 35 | 36 | // Fix output image shifted right bug 37 | scrollX: -window.scrollX, 38 | scrollY: -window.scrollY, 39 | windowWidth: document.documentElement.offsetWidth, 40 | windowHeight: document.documentElement.offsetHeight, 41 | }).then((canvas) => { 42 | resolve(canvas); 43 | }); 44 | ReactDOM.render(null, elem); 45 | }, 300); 46 | }); 47 | }); 48 | }; 49 | 50 | const exportHtml = (rendered) => { 51 | const html = "" + renderToString(rendered) + ""; 52 | return Juice.inlineContent(html, getDocCss()); 53 | }; 54 | 55 | const getExported = async (format, writing, settings) => { 56 | return new Promise((resolve, reject) => { 57 | switch (format) { 58 | case "txt": 59 | resolve(writing); 60 | break; 61 | case "png": 62 | resolve(exportPng({writing})); 63 | break; 64 | case "html": 65 | resolve(exportHtml({writing})); 66 | break; 67 | default: 68 | resolve(writing); 69 | } 70 | }); 71 | }; 72 | 73 | export { getExported }; 74 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 17 | 23 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 62 | 63 | WriteSpace 64 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import TypeBox from "./TypeBox"; 4 | import ToolTray from "./ToolTray"; 5 | import TextCounter from "./TextCounter"; 6 | 7 | import Markdown from "./Markdown"; 8 | import Print from "./Print"; 9 | import FirstTime from "./FirstTime"; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | container: { 13 | display: "flex", 14 | minHeight: "100vh", 15 | width: "100%", 16 | maxWidth: "100vw", 17 | alignItems: "start", 18 | justifyContent: "center", 19 | }, 20 | center: { 21 | position: "relative", 22 | display: "flex", 23 | flexDirection: "row", 24 | width: "100%", 25 | maxWidth: "80rem", 26 | boxSizing: "border-box", 27 | alignItems: "start", 28 | justifyContent: "center", 29 | }, 30 | typeBox: { 31 | margin: 10, 32 | marginTop: 100, 33 | marginBottom: "30vh", 34 | padding: 10, 35 | minHeight: "calc(70vh - 120px)", 36 | width: "100%", 37 | minWidth: "20vh", 38 | maxWidth: "60rem", 39 | }, 40 | markdownDiv: { 41 | margin: 10, 42 | marginTop: 90, 43 | marginBottom: 100, 44 | padding: 10, 45 | width: "100%", 46 | minWidth: "20vh", 47 | display: "block", 48 | }, 49 | divider: { 50 | position: "absolute", 51 | top: "5vh", 52 | bottom: "5vh", 53 | margin: "auto", 54 | width: 2, 55 | borderRadius: 1, 56 | backgroundColor: theme.palette.common.lightGray, 57 | }, 58 | hide: { 59 | display: "none", 60 | }, 61 | })); 62 | 63 | function Layout() { 64 | const classes = useStyles(); 65 | const [writing, setWriting] = useState(""); 66 | 67 | const defaultSettings = { 68 | showOutput: true, 69 | showTextCount: false, 70 | printRendered: true, 71 | dlFormat: "txt", 72 | copyFormat: "png", 73 | katexInline: false, 74 | katexBlock: true, 75 | fontSize: 12, 76 | fontFamily: "Helvetica, sans-serif", 77 | }; 78 | const [settings, setSettings] = useState( 79 | JSON.parse(localStorage.getItem("settings")) || defaultSettings 80 | ); 81 | 82 | // Load data from last session if exists 83 | useEffect(() => { 84 | const data = localStorage.getItem("currentPad"); 85 | data && setWriting(data); 86 | 87 | if (!localStorage.getItem("currentPadCreatedOn") || !data) { 88 | localStorage.setItem("currentPadCreatedOn", Date.now()); 89 | } 90 | }, []); 91 | 92 | return ( 93 | <> 94 |
101 |
102 | 107 | {settings.showOutput && ( 108 | <> 109 |
110 |
111 | {writing} 112 |
113 | 114 | )} 115 | 119 |
120 | 126 | 127 | 128 |
132 |
133 | 134 | ); 135 | } 136 | 137 | export default Layout; 138 | -------------------------------------------------------------------------------- /src/components/Copy.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import ReactGA from "react-ga"; 4 | import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; 5 | import CloseIcon from "@material-ui/icons/Close"; 6 | import { Snackbar, IconButton, Tooltip } from "@material-ui/core"; 7 | 8 | import { getExported } from "./Export"; 9 | 10 | const useStyles = makeStyles((theme) => ({ 11 | button: { 12 | backgroundColor: theme.palette.common.lightestGray, 13 | margin: 3, 14 | }, 15 | snackbarSuccess: { 16 | backgroundColor: theme.palette.common.green, 17 | }, 18 | snackbarError: { 19 | backgroundColor: theme.palette.common.red, 20 | }, 21 | })); 22 | 23 | function Copy({ writing, settings }) { 24 | const classes = useStyles(); 25 | const [snackbar, setSnackbar] = useState({ 26 | open: false, 27 | success: true, 28 | msg: "", 29 | }); 30 | 31 | const copyToClipboard = async (toCopy, format) => { 32 | switch (format) { 33 | case "png": 34 | toCopy.toBlob(async (blob) => { 35 | try { 36 | // eslint-disable-next-line no-undef 37 | const data = [new ClipboardItem({ [blob.type]: blob })]; 38 | await navigator.clipboard.write(data); 39 | setSnackbar({ 40 | open: true, 41 | success: true, 42 | msg: "Copied output picture to the clipboard!", 43 | }); 44 | } catch (e) { 45 | setSnackbar({ 46 | open: true, 47 | success: false, 48 | msg: 49 | "Sorry, copying images to the clipboard is not supported in your browser.", 50 | }); 51 | console.error(e); 52 | return; 53 | } 54 | }); 55 | break; 56 | default: 57 | try { 58 | await navigator.clipboard.writeText(toCopy); 59 | } catch (e) { 60 | setSnackbar({ 61 | open: true, 62 | success: false, 63 | msg: 64 | "Sorry, copying to the clipboard is not supported in your browser.", 65 | }); 66 | console.error(e); 67 | return; 68 | } 69 | let msg; 70 | switch (format) { 71 | case "txt": 72 | msg = "Copied text to the clipboard!"; 73 | break; 74 | case "html": 75 | msg = "Copied output html to the clipboard!"; 76 | break; 77 | default: 78 | msg = "Copied to the clipboard!"; 79 | } 80 | setSnackbar({ open: true, success: true, msg: msg }); 81 | } 82 | }; 83 | 84 | const onCopyButtonClick = () => { 85 | getExported(settings.copyFormat, writing, settings).then((toCopy) => { 86 | copyToClipboard(toCopy, settings.copyFormat); 87 | }); 88 | ReactGA.event({ 89 | category: "Export", 90 | action: "Copy", 91 | label: settings.copyFormat, 92 | }); 93 | }; 94 | 95 | const handleSnackbarClose = () => { 96 | setSnackbar({ ...snackbar, open: false }); 97 | }; 98 | 99 | return ( 100 | <> 101 | 102 | 103 | 104 | 105 | 106 | 126 | 127 | 128 | } 129 | /> 130 | 131 | ); 132 | } 133 | 134 | export default Copy; 135 | -------------------------------------------------------------------------------- /src/components/Files.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import ReactGA from "react-ga"; 4 | import FolderOutlinedIcon from "@material-ui/icons/FolderOutlined"; 5 | import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; 6 | import SearchIcon from "@material-ui/icons/Search"; 7 | import { 8 | Dialog, 9 | DialogTitle, 10 | DialogContent, 11 | DialogActions, 12 | List, 13 | ListItem, 14 | ListItemSecondaryAction, 15 | Divider, 16 | Button, 17 | IconButton, 18 | Typography, 19 | Tooltip, 20 | TextField, 21 | InputAdornment, 22 | } from "@material-ui/core"; 23 | 24 | const useStyles = makeStyles((theme) => ({ 25 | list: { 26 | width: "30rem", 27 | position: "relative", 28 | overflow: "auto", 29 | maxHeight: 401, 30 | padding: 0, 31 | }, 32 | listItem: { 33 | display: "flex", 34 | flexDirection: "column", 35 | alignItems: "start", 36 | justifyContent: "center", 37 | width: "100%", 38 | height: "calc(5em - 1px)", 39 | paddingRight: 69, 40 | overflow: "hidden", 41 | textOverflow: "ellipsis", 42 | "&$selected": { 43 | // backgroundColor: theme.palette.primary.light, 44 | }, 45 | }, 46 | listItemHeading: { 47 | fontWeight: 500, 48 | width: "100%", 49 | }, 50 | listItemText: { 51 | width: "100%", 52 | }, 53 | selected: {}, 54 | button: { 55 | backgroundColor: theme.palette.common.lightestGray, 56 | margin: 3, 57 | }, 58 | deleteButton: { 59 | color: theme.palette.common.red, 60 | }, 61 | search: { 62 | width: "100%", 63 | marginBottom: "1rem", 64 | }, 65 | searchIcon: { 66 | color: theme.palette.common.gray, 67 | }, 68 | searchInput: { 69 | padding: 8, 70 | }, 71 | })); 72 | 73 | function Files({ writing, setWriting }) { 74 | const classes = useStyles(); 75 | const [filesDialogOpen, setFilesDialogOpen] = useState(false); 76 | const [deleteConfirmCreatedOn, setDeleteConfirmCreatedOn] = useState(null); 77 | const [currentPadCreatedOn, setCurrentPadCreatedOn] = useState(); 78 | const [searchTerm, setSearchTerm] = useState(""); 79 | const [files, setFiles] = useState([]); 80 | 81 | const onDialogClose = () => { 82 | setFilesDialogOpen(false); 83 | }; 84 | 85 | const onFilesButtonClick = () => { 86 | const files = JSON.parse(localStorage.getItem("files") || "[]") || []; 87 | setFiles(files); 88 | setSearchTerm(""); 89 | const currentPadCreatedOn = parseInt( 90 | localStorage.getItem("currentPadCreatedOn") 91 | ); 92 | setCurrentPadCreatedOn(currentPadCreatedOn); 93 | saveCurrentfile(currentPadCreatedOn, files); 94 | setFilesDialogOpen(true); 95 | 96 | ReactGA.event({ 97 | category: "Files", 98 | action: "Files dialog opened", 99 | }); 100 | }; 101 | 102 | const saveCurrentfile = (currentPadCreatedOn, files) => { 103 | let filesTemp = files; 104 | 105 | const currentFileIndex = filesTemp.findIndex( 106 | (elem) => elem.createdOn === currentPadCreatedOn 107 | ); 108 | 109 | if (writing) { 110 | if (currentFileIndex !== -1) { 111 | if (writing !== filesTemp[currentFileIndex].data) { 112 | filesTemp.push({ 113 | ...filesTemp.splice(currentFileIndex, 1)[0], 114 | data: writing, 115 | }); 116 | } 117 | } else { 118 | filesTemp.push({ 119 | data: writing, 120 | createdOn: currentPadCreatedOn || Date.now() - 100, 121 | }); 122 | } 123 | } 124 | localStorage.setItem("files", JSON.stringify(filesTemp)); 125 | setFiles(filesTemp); 126 | }; 127 | 128 | const onFileClick = (file) => { 129 | setFilesDialogOpen(false); 130 | 131 | setWriting(file.data); 132 | localStorage.setItem("currentPadCreatedOn", file.createdOn); 133 | localStorage.setItem("currentPad", file.data); 134 | }; 135 | 136 | const deleteFile = () => { 137 | const createdOn = deleteConfirmCreatedOn; 138 | setDeleteConfirmCreatedOn(null); 139 | 140 | let filesTemp = files; 141 | const toDelIndex = filesTemp.findIndex( 142 | (elem) => elem.createdOn === createdOn 143 | ); 144 | 145 | filesTemp.splice(toDelIndex, 1); 146 | localStorage.setItem("files", JSON.stringify(filesTemp)); 147 | 148 | setFiles([...filesTemp]); 149 | 150 | if (createdOn === currentPadCreatedOn) { 151 | localStorage.setItem("files", JSON.stringify(files)); 152 | localStorage.setItem("currentPad", ""); 153 | localStorage.setItem("currentPadCreatedOn", Date.now()); 154 | setWriting(""); 155 | } 156 | }; 157 | 158 | const renderDeleteConfirm = () => { 159 | return ( 160 | 161 | Delete File? 162 | 163 |

This action cannot be undone

164 |
165 | 166 | 169 | 172 | 173 |
174 | ); 175 | }; 176 | 177 | const renderListItem = (file) => { 178 | // const regEx = /(?:#{0,6} )?(.+?(?:\n|$))\n*([\s\S]{0,100})[\s\S]*/g; // group 1: first line without heading hashes, group 2: all lines below 179 | const regEx = /(?:#{0,6} )?(.+?(?:\n|$))\n*(.*(?:\n|$))[\s\S]*/g; // group 1: first line without heading hashes, group 2: second line 180 | const lines = regEx.exec(file.data); 181 | 182 | return ( 183 | 193 | 194 | {lines ? lines[1] : ""} 195 | 196 | 197 | {lines 198 | ? searchTerm.length > 0 199 | ? lines[0].substr( 200 | lines[0].toLowerCase().indexOf(searchTerm), 201 | lines[0].length 202 | ) 203 | : lines[2] 204 | : ""} 205 | 206 | 207 | 210 | 211 | 212 | 213 | 214 | ); 215 | }; 216 | 217 | const renderFileList = () => { 218 | let filteredFiles = files.filter((file) => 219 | file?.data?.toLowerCase().includes(searchTerm) 220 | ); 221 | return ( 222 | 223 | {files?.length > 0 ? ( 224 | filteredFiles?.length > 0 ? ( 225 | filteredFiles 226 | .slice(0) 227 | .reverse() 228 | .map((file) => { 229 | return ( 230 | 231 | 232 | {renderListItem(file)} 233 | 234 | ); 235 | }) 236 | ) : ( 237 |

No results

238 | ) 239 | ) : ( 240 |

You don't have any other saved files

241 | )} 242 | 243 |
244 | ); 245 | }; 246 | 247 | const renderSearchBar = () => { 248 | return ( 249 | setSearchTerm(e.target.value.toLowerCase())} 252 | className={classes.search} 253 | variant="outlined" 254 | placeholder="Search" 255 | InputProps={{ 256 | startAdornment: ( 257 | 258 | 259 | 260 | ), 261 | }} 262 | inputProps={{ 263 | className: classes.searchInput, 264 | }} 265 | /> 266 | ); 267 | }; 268 | 269 | return ( 270 | <> 271 | 272 | Files 273 | 274 | {files?.length > 0 && renderSearchBar()} 275 | {renderFileList()} 276 | 277 | 278 | 279 | 280 | 281 | {renderDeleteConfirm()} 282 | 283 | 284 | 285 | 286 | 287 | 288 | ); 289 | } 290 | 291 | export default Files; 292 | -------------------------------------------------------------------------------- /src/components/Settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import ReactGA from "react-ga"; 4 | import SettingsOutlinedIcon from "@material-ui/icons/SettingsOutlined"; 5 | import { 6 | Dialog, 7 | DialogTitle, 8 | DialogContent, 9 | DialogActions, 10 | Button, 11 | IconButton, 12 | FormControlLabel, 13 | Checkbox, 14 | TextField, 15 | Select, 16 | MenuItem, 17 | Tooltip, 18 | Link, 19 | } from "@material-ui/core"; 20 | 21 | const useStyles = makeStyles((theme) => ({ 22 | column: { 23 | display: "flex", 24 | flexFlow: "column nowrap", 25 | }, 26 | dialogContent: { 27 | width: "18rem", 28 | }, 29 | dialogActions: { 30 | justifyContent: "space-between", 31 | padding: "8px 16px", 32 | }, 33 | button: { 34 | backgroundColor: theme.palette.common.lightestGray, 35 | margin: 3, 36 | }, 37 | formLabel: { 38 | justifyContent: "flex-end", 39 | marginLeft: 0, 40 | }, 41 | select: { 42 | margin: 5, 43 | marginLeft: 9, 44 | }, 45 | numberField: { 46 | width: "2.8rem", 47 | margin: 5, 48 | marginLeft: 9, 49 | }, 50 | link: { 51 | padding: 8, 52 | }, 53 | })); 54 | 55 | function Settings({ settings, setSettings }) { 56 | const classes = useStyles(); 57 | const [settingsOpen, setSettingsOpen] = useState(false); 58 | 59 | const onClose = () => { 60 | setSettingsOpen(false); 61 | }; 62 | 63 | const onSettingsButtonClick = () => { 64 | setSettingsOpen(true); 65 | 66 | ReactGA.event({ 67 | category: "Settings", 68 | action: "Settings opened", 69 | }); 70 | }; 71 | 72 | const onSettingsFontSizeChange = (e) => { 73 | const val = parseFloat(e.target.value); 74 | setSettings({ ...settings, fontSize: val }); 75 | }; 76 | 77 | const onSettingsFontFamilyChange = (e) => { 78 | setSettings({ ...settings, fontFamily: e.target.value }); 79 | }; 80 | 81 | const onSettingsShowOutputChange = (e) => { 82 | setSettings({ ...settings, showOutput: e.target.checked }); 83 | }; 84 | 85 | const onSettingsShowTextCountChange = (e) => { 86 | setSettings({ ...settings, showTextCount: e.target.checked }); 87 | }; 88 | 89 | const onSettingsKatexInlineChange = (e) => { 90 | setSettings({ ...settings, katexInline: e.target.checked }); 91 | }; 92 | 93 | const onSettingsKatexBlockChange = (e) => { 94 | setSettings({ ...settings, katexBlock: e.target.checked }); 95 | }; 96 | 97 | const onSettingsPrintRenderedChange = (e) => { 98 | setSettings({ ...settings, printRendered: e.target.value }); 99 | }; 100 | 101 | const onSettingsDlFormatChange = (e) => { 102 | setSettings({ ...settings, dlFormat: e.target.value }); 103 | }; 104 | 105 | const onSettingsCopyFormatChange = (e) => { 106 | setSettings({ ...settings, copyFormat: e.target.value }); 107 | }; 108 | 109 | const onUserGuideOpened = (e) => { 110 | ReactGA.event({ 111 | category: "Settings", 112 | action: "User guide opened", 113 | }); 114 | }; 115 | 116 | // Save all settings changes to localstorage 117 | useEffect(() => { 118 | localStorage.setItem("settings", JSON.stringify(settings)); 119 | }, [settings]); 120 | 121 | const fontsList = [ 122 | ["Helvetica", "Helvetica, sans-serif"], 123 | ["Arial", "Arial, sans-serif"], 124 | ["Verdana", "Verdana, sans-serif"], 125 | ["Tahoma", "Tahoma, sans-serif"], 126 | ["Trebuchet MS", "'Trebuchet MS', sans-serif"], 127 | ["Times New Roman", "'Times New Roman', serif"], 128 | ["Georgia", "Georgia, serif"], 129 | ["Garamond", "Garamond, serif"], 130 | ["Courier New", "'Courier New', monospace"], 131 | ["Brush Script MT", "'Brush Script MT', cursive"], 132 | ]; 133 | 134 | return ( 135 | <> 136 | 137 | 138 | 139 | 140 | 141 | 142 | Settings 143 | 144 |
145 | 154 | } 155 | className={classes.formLabel} 156 | label="Font size" 157 | labelPlacement="start" 158 | /> 159 | 166 | {fontsList.map((font) => { 167 | return ( 168 | 173 | {font[0]} 174 | 175 | ); 176 | })} 177 | 178 | } 179 | className={classes.formLabel} 180 | label="Font" 181 | labelPlacement="start" 182 | /> 183 | 191 | } 192 | className={classes.formLabel} 193 | label="Show live rendered output" 194 | labelPlacement="start" 195 | /> 196 | 204 | } 205 | className={classes.formLabel} 206 | label="Show word and char count" 207 | labelPlacement="start" 208 | /> 209 | 217 | } 218 | className={classes.formLabel} 219 | label="Inline KaTeX ($)" 220 | labelPlacement="start" 221 | /> 222 | 230 | } 231 | className={classes.formLabel} 232 | label="Block KaTeX ($$)" 233 | labelPlacement="start" 234 | /> 235 | 242 | Rendered Markdown 243 | Raw Text 244 | 245 | } 246 | className={classes.formLabel} 247 | label="Print" 248 | labelPlacement="start" 249 | /> 250 | 257 | Plain text 258 | Image 259 | HTML 260 | PDF (via print) 261 | 262 | } 263 | className={classes.formLabel} 264 | label="Download format" 265 | labelPlacement="start" 266 | /> 267 | 274 | Plain text 275 | Image 276 | HTML 277 | 278 | } 279 | className={classes.formLabel} 280 | label="Copy to clipboard format" 281 | labelPlacement="start" 282 | /> 283 |
284 |
285 | 286 | 292 | User Guide 293 | 294 | 295 | 296 |
297 | 298 | ); 299 | } 300 | 301 | export default Settings; 302 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 119 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /public/userguide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | User Guide 5 | 10 | 11 | 12 |
13 | WriteSpace logo 14 |

WriteSpace

15 |
16 |

17 | WriteSpace is a minimalist web notepad with Markdown and KaTeX 18 | support. It includes easy options to print the rendered output or export 19 | your notes as raw text, image, or html. 20 |

21 |

GitHub

22 |

Markdown

23 |

24 | WriteSpace supports Markdown to easily create headings, bullets, insert 25 | images, and more in your notes. 26 | Markdown syntax 27 |

28 |

29 | Check "Show live rendered output" in settings to view the rendered markdown output. 30 |

31 |

KaTeX

32 |

33 | WriteSpace supports KaTeX to allow you to easily write math expressions. 34 | KaTeX syntax 35 |

36 |

37 | To use KaTeX, first make sure it is enabled in settings. Inline KaTeX 38 | expressions can be written between 39 | $ 46 | delimiters. Block KaTeX expressions can be written between 47 | $$ 54 | delimiters. 55 |

56 |

57 | Inline KaTeX example: $x^2$ -> 58 | x2x^2 162 |

163 |

Block KaTeX example:

164 |

$$
e^{i\pi}+1=0
$$

165 |
166 | eiπ+1=0e^{i\pi}+1=0 340 |
341 |

Code blocks

342 |

343 | WriteSpace supports code blocks with automatic syntax highlighting. Code 344 | blocks can be written between 345 | ``` 352 | delimiters. Example: 353 |

354 |

```
print('hello world!')
```

355 |
print('hello world!')
366 |

Word/character counter

367 |

368 | To view word and character count, enable "Show word and char 369 | count" in settings. 370 |

371 |

Export options

372 |

373 | WriteSpace lets you easily export as raw text, a PNG image, or html. You 374 | can export and download via the "download" button in the top 375 | right, or export and copy to the clipboard via the "copy" button 376 | in the top right. You can also export raw text or the rendered output as a 377 | PDF via your browser's print dialog. In settings, you can choose what 378 | format you would like to export in for printing, downloading, and copying. 379 |

380 | 381 | 382 | --------------------------------------------------------------------------------