├── 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 | You need to enable JavaScript to run this app.
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 |
167 | Cancel
168 |
169 |
170 | Delete
171 |
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 | Close
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 | Close
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 |
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 | x 2 x^2 x 2
162 |
163 | Block KaTeX example:
164 | $$ e^{i\pi}+1=0$$
165 |
166 |
e i π + 1 = 0 e^{i\pi}+1=0 e i π + 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 |
--------------------------------------------------------------------------------