Update to use the Monaco editor (#7)
4
.github/banner.svg
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
46
README.md
@@ -1,33 +1,47 @@
|
||||
<h1 align="center">📋 paste</h1>
|
||||
<p align="center">
|
||||
<img src=".github/banner.svg">
|
||||
</p>
|
||||
|
||||
**paste is a simple web app for writing & sharing code.** It's my own take on conventional pastebin sites like _pastebin.com_ or _hastebin_.
|
||||
|
||||
The frontend _(this repository)_ is written using the React framework. The backend data storage is handled by a separate web service called [bytebin](https://github.com/lucko/bytebin).
|
||||
|
||||
The user-interface is quite simple; it supports syntax highlighting, automatic indentation, many supported languages, themes, zooming in/out, linking to specific lines or sections, and more!
|
||||
The user-interface is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/), the engine behind the popular Visual Studio Code text editor. It's quite simple; it supports syntax highlighting, automatic indentation, many supported languages, themes, zooming in/out, linking to specific lines or sections, and more!
|
||||
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/03rBijj.gif">
|
||||
</p>
|
||||
## pastes.dev
|
||||
|
||||
## Usage
|
||||
I host a public instance at [pastes.dev](https://pastes.dev). Please feel free to use it to share code/configs/whatever!
|
||||
|
||||
I host a public instance of paste at [paste.lucko.me](https://paste.lucko.me). Please feel free to use it to share code/configs/whatever!
|
||||
Please note that the following (very-non-legally worded) [terms of service](https://github.com/lucko/bytebin#public-instances) apply.
|
||||
If you come across any content which is illegal or infringes on copyright, please [get in touch](https://lucko.me/contact) and let me know so I can remove it.
|
||||
|
||||
However please note that the (very-non-legally worded) [terms of service](https://github.com/lucko/bytebin#public-instances) for my public bytebin instance apply here too. If you come across any content which is illegal or infringes on copyright, please [get in touch](https://lucko.me/contact) and let me know so I can remove it.
|
||||
Uploaded content is retained for 90 days then deleted.
|
||||
|
||||
Uploaded content is retained for 30 days then deleted.
|
||||
### pastes.dev API
|
||||
|
||||
### Host your own
|
||||
* To **read** content, send a HTTP `GET` request to `https://api.pastes.dev/<key>`.
|
||||
* Replace `<key>` with the id of the paste.
|
||||
* The content is returned in the response body.
|
||||
* The `Content-Type` header is `text/<language>`, where language is the id of the language the paste was saved with.
|
||||
* To **upload** content, send a HTTP `POST` request to `https://api.pastes.dev/post`.
|
||||
* Include the content in the request body.
|
||||
* Specify the language with the `Content-Type: text/<language>` header, and please provide a `User-Agent` header too.
|
||||
* The paste "key" is returned in the `Location` header, or in the response body as a JSON object in the format `{"key": "<key>"}`.
|
||||
|
||||
If you want to host your own paste, first you need to compile it:
|
||||
The API is powered by the [bytebin](https://github.com/lucko/bytebin) service, so more information about how it works can be found there.
|
||||
## Host your own
|
||||
|
||||
It's quite simple to host your own version.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/lucko/paste
|
||||
cd paste
|
||||
yarn install
|
||||
|
||||
# Outputs html/css/js files to /build
|
||||
yarn build
|
||||
|
||||
# or run the following instead of 'yarn build' to start a webserver for testing/development
|
||||
# Start a webserver for testing/development
|
||||
yarn start
|
||||
```
|
||||
|
||||
@@ -41,11 +55,3 @@ docker compose up -d
|
||||
```
|
||||
|
||||
You should then (hopefully!) be able to access the application at `http://localhost:8080/`.
|
||||
|
||||
## API
|
||||
|
||||
paste uses [bytebin](https://github.com/lucko/bytebin) for data storage.
|
||||
|
||||
As a result, you can use the [bytebin API](https://github.com/lucko/bytebin#api-usage) to submit/read content programatically.
|
||||
|
||||
To set the language of a paste, use the `Content-Type` header with value `text/<language>` (e.g. `Content-Type: text/yaml` for a _.yml_ file).
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
@@ -11,11 +12,9 @@
|
||||
"history": "^5.0.0",
|
||||
"local-storage": "^2.0.0",
|
||||
"pako": "^2.0.3",
|
||||
"prismjs": "^1.23.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-simple-code-editor": "^0.11.0",
|
||||
"react-scripts": "5.0.0",
|
||||
"styled-components": "^5.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
public/assets/logo256.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -2,27 +2,26 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>paste</title>
|
||||
<title>pastes</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#aaddff" />
|
||||
<meta name="description" content="lucko's simple pastebin." />
|
||||
<meta name="theme-color" content="#d2a8ff" />
|
||||
<meta name="description" content="a simple pastebin." />
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="paste" />
|
||||
<meta name="twitter:description" content="lucko's simple pastebin." />
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/assets/logo180.png" />
|
||||
<meta name="twitter:title" content="pastes" />
|
||||
<meta name="twitter:description" content="a simple pastebin." />
|
||||
<meta name="twitter:image" content="%PUBLIC_URL%/assets/logo256.png" />
|
||||
|
||||
<meta property="og:title" content="paste" />
|
||||
<meta property="og:description" content="lucko's simple pastebin." />
|
||||
<meta property="og:title" content="pastes" />
|
||||
<meta property="og:description" content="a simple pastebin." />
|
||||
<meta property="og:type" content="product" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/assets/logo180.png" />
|
||||
<meta property="og:url" content="https://paste.lucko.me/" />
|
||||
<meta property="og:site_name" content="paste" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/assets/logo256.png" />
|
||||
<meta property="og:url" content="%PUBLIC_URL%" />
|
||||
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link href="%PUBLIC_URL%/assets/logo512.png" rel="shortcut icon" sizes="512x512" type="image/png">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/logo256.png" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
20
src/App.js
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Editor from './components/Editor';
|
||||
import parseContentType from 'content-type-parser';
|
||||
import { languageIds } from './util/highlighting';
|
||||
@@ -22,7 +22,7 @@ async function loadFromBytebin(id) {
|
||||
resp.headers.get('content-type')
|
||||
);
|
||||
|
||||
document.title = 'paste | ' + id;
|
||||
document.title = 'pastes | ' + id;
|
||||
return { ok: true, content, type };
|
||||
} else {
|
||||
return { ok: false };
|
||||
@@ -49,13 +49,19 @@ const LOADED = Symbol();
|
||||
export default function App() {
|
||||
const [pasteId] = useState(getPasteIdFromUrl);
|
||||
const [state, setState] = useState(INITIAL);
|
||||
const [content, setContent] = useState('');
|
||||
const [forcedContent, setForcedContent] = useState('');
|
||||
const [actualContent, setActualContent] = useState('');
|
||||
const [contentType, setContentType] = useState();
|
||||
|
||||
const setContent = useCallback((content) => {
|
||||
setActualContent(content);
|
||||
setForcedContent(content);
|
||||
}, [setActualContent, setForcedContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pasteId && state === INITIAL) {
|
||||
setState(LOADING);
|
||||
setContent('Loading...');
|
||||
setForcedContent('Loading...');
|
||||
loadFromBytebin(pasteId).then(({ ok, content, type }) => {
|
||||
if (ok) {
|
||||
setContent(content);
|
||||
@@ -72,8 +78,10 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<Editor
|
||||
content={content}
|
||||
setContent={setContent}
|
||||
forcedContent={forcedContent}
|
||||
setForcedContent={setContent}
|
||||
actualContent={actualContent}
|
||||
setActualContent={setActualContent}
|
||||
contentType={contentType}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,15 @@ import EditorControls from './EditorControls';
|
||||
import EditorTextArea from './EditorTextArea';
|
||||
import themes from '../style/themes';
|
||||
|
||||
export default function Editor({ content, setContent, contentType }) {
|
||||
export default function Editor({
|
||||
forcedContent,
|
||||
setForcedContent,
|
||||
actualContent,
|
||||
setActualContent,
|
||||
contentType,
|
||||
}) {
|
||||
const [language, setLanguage] = useState('plain');
|
||||
|
||||
const [theme, setTheme] = usePreference(
|
||||
'theme',
|
||||
'dark',
|
||||
@@ -37,8 +44,8 @@ export default function Editor({ content, setContent, contentType }) {
|
||||
<ThemeProvider theme={themes[theme]}>
|
||||
<EditorGlobalStyle />
|
||||
<EditorControls
|
||||
code={content}
|
||||
setCode={setContent}
|
||||
actualContent={actualContent}
|
||||
setForcedContent={setForcedContent}
|
||||
language={language}
|
||||
setLanguage={setLanguage}
|
||||
theme={theme}
|
||||
@@ -46,8 +53,9 @@ export default function Editor({ content, setContent, contentType }) {
|
||||
zoom={zoom}
|
||||
/>
|
||||
<EditorTextArea
|
||||
code={content}
|
||||
setCode={setContent}
|
||||
forcedContent={forcedContent}
|
||||
setActualContent={setActualContent}
|
||||
theme={themes[theme]}
|
||||
language={language}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
|
||||
@@ -10,8 +10,8 @@ import themes from '../style/themes';
|
||||
import { postUrl } from '../util/constants';
|
||||
|
||||
export default function EditorControls({
|
||||
code,
|
||||
setCode,
|
||||
actualContent,
|
||||
setForcedContent,
|
||||
language,
|
||||
setLanguage,
|
||||
theme,
|
||||
@@ -23,23 +23,25 @@ export default function EditorControls({
|
||||
|
||||
useEffect(() => {
|
||||
setRecentlySaved(false);
|
||||
}, [code, language]);
|
||||
}, [actualContent, language]);
|
||||
|
||||
const save = useCallback(() => {
|
||||
if (!code || recentlySaved) {
|
||||
if (!actualContent || recentlySaved) {
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
saveToBytebin(code, language).then(pasteId => {
|
||||
saveToBytebin(actualContent, language).then(pasteId => {
|
||||
setSaving(false);
|
||||
setRecentlySaved(true);
|
||||
history.replace({
|
||||
pathname: pasteId,
|
||||
});
|
||||
copy(window.location.href);
|
||||
document.title = 'paste | ' + pasteId;
|
||||
if (pasteId) {
|
||||
history.replace({
|
||||
pathname: pasteId,
|
||||
});
|
||||
copy(window.location.href);
|
||||
document.title = 'paste | ' + pasteId;
|
||||
}
|
||||
});
|
||||
}, [code, language, recentlySaved]);
|
||||
}, [actualContent, language, recentlySaved]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = e => {
|
||||
@@ -61,7 +63,7 @@ export default function EditorControls({
|
||||
}, [save, zoom]);
|
||||
|
||||
function reset() {
|
||||
setCode('');
|
||||
setForcedContent('');
|
||||
setLanguage('plain');
|
||||
history.replace({
|
||||
pathname: '/',
|
||||
@@ -109,6 +111,7 @@ export default function EditorControls({
|
||||
|
||||
const Header = styled.header`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default function EditorPrismStyle({ children }) {
|
||||
return <Main>{children}</Main>;
|
||||
}
|
||||
|
||||
const Main = styled.main`
|
||||
padding-top: 2em;
|
||||
color: ${props => props.theme.editor.primary};
|
||||
background: ${props => props.theme.editor.background};
|
||||
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-size: 1em;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*='language-']::-moz-selection,
|
||||
pre[class*='language-'] ::-moz-selection,
|
||||
code[class*='language-']::-moz-selection,
|
||||
code[class*='language-'] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: ${props => props.theme.editor.selection};
|
||||
}
|
||||
|
||||
pre[class*='language-']::selection,
|
||||
pre[class*='language-'] ::selection,
|
||||
code[class*='language-']::selection,
|
||||
code[class*='language-'] ::selection {
|
||||
text-shadow: none;
|
||||
background: ${props => props.theme.editor.selection};
|
||||
}
|
||||
|
||||
@media print {
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*='language-'] {
|
||||
padding: 1em;
|
||||
margin: 0.5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: ${props => props.theme.editor.comment};
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: ${props => props.theme.editor.punctuation};
|
||||
}
|
||||
|
||||
.token.annotation {
|
||||
color: ${props => props.theme.editor.annotation};
|
||||
}
|
||||
|
||||
.token.namespace {
|
||||
color: ${props => props.theme.editor.namespace};
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag {
|
||||
color: ${props => props.theme.editor.property};
|
||||
}
|
||||
|
||||
.token.tag .punctuation {
|
||||
color: ${props => props.theme.editor.primary};
|
||||
}
|
||||
|
||||
.token.script > .token.punctuation {
|
||||
color: ${props => props.theme.editor.punctuation};
|
||||
}
|
||||
|
||||
.token.tag .attr-value {
|
||||
color: ${props => props.theme.editor.selector};
|
||||
}
|
||||
|
||||
.token.tag .script {
|
||||
color: ${props => props.theme.editor.primary};
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: ${props => props.theme.editor.keyword};
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
color: ${props => props.theme.editor.constant};
|
||||
}
|
||||
|
||||
.token.number,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: ${props => props.theme.editor.number};
|
||||
}
|
||||
|
||||
.token.attr-name {
|
||||
color: ${props => props.theme.editor.function};
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: ${props => props.theme.editor.selector};
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: ${props => props.theme.editor.operator};
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: ${props => props.theme.editor.keyword};
|
||||
}
|
||||
|
||||
.token.comment .token.keyword {
|
||||
color: ${props => props.theme.editor.primary};
|
||||
}
|
||||
|
||||
.token.comment .token.tag,
|
||||
.token.comment .token.tag > .token.punctuation {
|
||||
color: ${props => props.theme.editor.commentTag};
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: ${props => props.theme.editor.function};
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: ${props => props.theme.editor.className};
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: ${props => props.theme.editor.variable};
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
`;
|
||||
@@ -1,114 +1,127 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ReactEditor from 'react-simple-code-editor';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import history from 'history/browser';
|
||||
import EditorPrismStyle from './EditorPrismStyle';
|
||||
import { getHighlighter } from '../util/highlighting';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import styled from 'styled-components';
|
||||
import themes, { makeMonacoTheme } from '../style/themes';
|
||||
|
||||
export default function EditorTextArea({ code, setCode, language, fontSize }) {
|
||||
const [isSelected, isSelectionMiddle, toggleSelected] = useSelectedLine();
|
||||
const highlight = getHighlighter(language);
|
||||
export default function EditorTextArea({
|
||||
forcedContent,
|
||||
setActualContent,
|
||||
theme,
|
||||
language,
|
||||
fontSize,
|
||||
}) {
|
||||
const [editor, setEditor] = useState();
|
||||
const [monaco, setMonaco] = useState();
|
||||
const [selected, toggleSelected] = useSelectedLine();
|
||||
const editorAreaRef = useRef();
|
||||
useLineNumberMagic(editorAreaRef, selected, toggleSelected, editor, monaco);
|
||||
|
||||
function highlightWithLineNumbers(input, grammar) {
|
||||
return highlight(input, grammar)
|
||||
.split(/\r?\n/)
|
||||
.map((line, i) => (
|
||||
<span key={i}>
|
||||
<LineNumber
|
||||
lineNo={i + 1}
|
||||
selected={isSelected(i + 1)}
|
||||
shouldScroll={isSelectionMiddle(i + 1)}
|
||||
toggleSelected={toggleSelected}
|
||||
/>
|
||||
<span dangerouslySetInnerHTML={{ __html: line }} />
|
||||
</span>
|
||||
))
|
||||
.reduce((acc, curr, idx) => {
|
||||
if (idx !== 0) {
|
||||
acc.push('\n');
|
||||
}
|
||||
acc.push(curr);
|
||||
return acc;
|
||||
}, []);
|
||||
function beforeMount(monaco) {
|
||||
for (const [id, theme] of Object.entries(themes)) {
|
||||
monaco.editor.defineTheme(id, makeMonacoTheme(theme));
|
||||
}
|
||||
}
|
||||
|
||||
const autoBracketState = useState(null);
|
||||
const editorRef = useRef();
|
||||
function keydown(e) {
|
||||
handleKeydown(e, editorRef.current, autoBracketState);
|
||||
function onMount(editor, monaco) {
|
||||
setEditor(editor);
|
||||
setMonaco(monaco);
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
const onChange = useCallback(
|
||||
value => {
|
||||
setActualContent(value);
|
||||
},
|
||||
[setActualContent]
|
||||
);
|
||||
|
||||
return (
|
||||
<EditorPrismStyle>
|
||||
<StyledReactEditor
|
||||
ref={editorRef}
|
||||
value={code}
|
||||
onValueChange={setCode}
|
||||
highlight={highlightWithLineNumbers}
|
||||
placeholder={'Paste (or type) some code...'}
|
||||
padding={10}
|
||||
size={fontSize}
|
||||
textareaId="code-area"
|
||||
autoFocus={true}
|
||||
onKeyDown={keydown}
|
||||
<EditorArea ref={editorAreaRef}>
|
||||
<Editor
|
||||
theme={theme.id}
|
||||
language={language === 'plain' ? 'plaintext' : language}
|
||||
options={{
|
||||
fontFamily: 'JetBrains Mono',
|
||||
fontSize: fontSize,
|
||||
fontLigatures: true,
|
||||
wordWrap: true,
|
||||
renderLineHighlight: 'none',
|
||||
detectIndentation: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
beforeMount={beforeMount}
|
||||
onMount={onMount}
|
||||
onChange={onChange}
|
||||
value={forcedContent}
|
||||
/>
|
||||
</EditorPrismStyle>
|
||||
</EditorArea>
|
||||
);
|
||||
}
|
||||
|
||||
const LineNumber = ({ lineNo, selected, shouldScroll, toggleSelected }) => {
|
||||
const autoScroll = useAutoScroll(shouldScroll);
|
||||
const EditorArea = styled.main`
|
||||
margin-top: 2.5em;
|
||||
height: 100%;
|
||||
|
||||
function click(e) {
|
||||
toggleSelected(lineNo, e.shiftKey);
|
||||
.line-numbers {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
return selected ? (
|
||||
<HighlightedLineNumber ref={autoScroll} onClick={click}>
|
||||
{lineNo}
|
||||
</HighlightedLineNumber>
|
||||
) : (
|
||||
<PlainLineNumber ref={autoScroll} onClick={click}>
|
||||
{lineNo}
|
||||
</PlainLineNumber>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledReactEditor = styled(ReactEditor)`
|
||||
counter-reset: line;
|
||||
font-size: ${props => props.size}px;
|
||||
outline: 0;
|
||||
min-height: calc(100vh - 2rem);
|
||||
|
||||
#code-area {
|
||||
outline: none;
|
||||
padding-left: 60px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding-left: 60px !important;
|
||||
.highlighted-line + div {
|
||||
color: ${props => props.theme.editor.lineNumberHl};
|
||||
background-color: ${props => props.theme.editor.lineNumberHlBackground};
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
const PlainLineNumber = styled.span`
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
color: ${props => props.theme.editor.lineNumber};
|
||||
text-align: right;
|
||||
width: 40px;
|
||||
font-weight: 100;
|
||||
user-select: none;
|
||||
function useLineNumberMagic(
|
||||
editorAreaRef,
|
||||
selected,
|
||||
toggleSelected,
|
||||
editor,
|
||||
monaco
|
||||
) {
|
||||
// add an event listener for clicking on line numbers
|
||||
useEffect(() => {
|
||||
const node = editorAreaRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
// override parent <pre>
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
`;
|
||||
const handler = click => {
|
||||
const target = click?.target;
|
||||
if (target && target.classList.contains('line-numbers')) {
|
||||
toggleSelected(parseInt(target.textContent), click);
|
||||
}
|
||||
};
|
||||
|
||||
const HighlightedLineNumber = styled(PlainLineNumber)`
|
||||
color: ${props => props.theme.editor.lineNumberHl};
|
||||
background-color: ${props => props.theme.editor.lineNumberHlBackground};
|
||||
font-weight: bold;
|
||||
`;
|
||||
node.addEventListener('click', handler);
|
||||
return () => node.removeEventListener('click', handler);
|
||||
}, [editorAreaRef, toggleSelected]);
|
||||
|
||||
// apply a 'highlighed' decoration to the selected lines
|
||||
useEffect(() => {
|
||||
if (!editor || !monaco) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = [];
|
||||
if (selected[0] !== -1) {
|
||||
range.push({
|
||||
range: new monaco.Range(selected[0], 1, selected[1], 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: 'highlighted-line',
|
||||
},
|
||||
});
|
||||
}
|
||||
const decorations = editor.deltaDecorations([], range);
|
||||
|
||||
return () => {
|
||||
editor.deltaDecorations(decorations, []);
|
||||
};
|
||||
}, [editor, monaco, selected]);
|
||||
}
|
||||
|
||||
function useSelectedLine() {
|
||||
// extract highlighted lines from window hash
|
||||
@@ -116,7 +129,7 @@ function useSelectedLine() {
|
||||
const hash = window.location.hash;
|
||||
if (/^#L\d+(-\d+)?$/.test(hash)) {
|
||||
const [start, end] = hash.substring(2).split('-').map(Number);
|
||||
return [start, isNaN(end) ? -1 : end];
|
||||
return [start, isNaN(end) ? start : end];
|
||||
} else {
|
||||
return [-1, -1];
|
||||
}
|
||||
@@ -127,7 +140,7 @@ function useSelectedLine() {
|
||||
let hash = '';
|
||||
|
||||
if (selected[0] !== -1) {
|
||||
if (selected[1] !== -1) {
|
||||
if (selected[1] !== selected[0]) {
|
||||
const start = Math.min(...selected);
|
||||
const end = Math.max(...selected);
|
||||
hash = `#L${start}-${end}`;
|
||||
@@ -140,171 +153,16 @@ function useSelectedLine() {
|
||||
}, [selected]);
|
||||
|
||||
// toggle the highlighting for a given line
|
||||
function toggleSelected(lineNo, shift) {
|
||||
if (selected[0] === lineNo && selected[1] === -1) {
|
||||
function toggleSelected(lineNo, e) {
|
||||
const shift = e.shiftKey;
|
||||
if (selected[0] === lineNo && selected[1] === lineNo) {
|
||||
setSelected([-1, -1]);
|
||||
} else if (selected[0] === -1 || !shift) {
|
||||
setSelected([lineNo, -1]);
|
||||
setSelected([lineNo, lineNo]);
|
||||
} else {
|
||||
setSelected([selected[0], lineNo]);
|
||||
}
|
||||
}
|
||||
|
||||
// should a line be highlighted in the viewer?
|
||||
function isSelected(lineNo) {
|
||||
if (selected[0] === -1) {
|
||||
return false;
|
||||
}
|
||||
if (selected[1] === -1) {
|
||||
return selected[0] === lineNo;
|
||||
}
|
||||
|
||||
return lineNo >= Math.min(...selected) && lineNo <= Math.max(...selected);
|
||||
}
|
||||
|
||||
// is a line in the middle of the selection
|
||||
function isSelectionMiddle(lineNo) {
|
||||
if (selected[0] === -1) {
|
||||
return false;
|
||||
}
|
||||
if (selected[1] === -1) {
|
||||
return selected[0] === lineNo;
|
||||
}
|
||||
|
||||
return (
|
||||
lineNo === Math.floor((Math.min(...selected) + Math.max(...selected)) / 2)
|
||||
);
|
||||
}
|
||||
|
||||
return [isSelected, isSelectionMiddle, toggleSelected];
|
||||
}
|
||||
|
||||
function useAutoScroll(shouldScroll) {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
// only attempt to autoscroll if this is the first render.
|
||||
if (!firstRender) {
|
||||
return;
|
||||
}
|
||||
setFirstRender(false);
|
||||
|
||||
if (shouldScroll) {
|
||||
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}, [shouldScroll, firstRender]);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
const KEYCODE_ENTER = 13;
|
||||
const KEYCODE_PARENS = 57;
|
||||
const KEYCODE_PARENS_CLOSE = 48;
|
||||
const KEYCODE_BRACKETS = 219;
|
||||
const KEYCODE_BRACKETS_CLOSE = 221;
|
||||
const KEYCODE_QUOTE = 222;
|
||||
const KEYCODE_BACK_QUOTE = 192;
|
||||
|
||||
function getPair({ keyCode, shiftKey }) {
|
||||
if (keyCode === KEYCODE_PARENS && shiftKey) {
|
||||
return ['(', ')'];
|
||||
} else if (keyCode === KEYCODE_BRACKETS) {
|
||||
if (shiftKey) {
|
||||
return ['{', '}'];
|
||||
} else {
|
||||
return ['[', ']'];
|
||||
}
|
||||
} else if (keyCode === KEYCODE_QUOTE) {
|
||||
if (shiftKey) {
|
||||
return ['"', '"'];
|
||||
} else {
|
||||
return ["'", "'"];
|
||||
}
|
||||
} else if (keyCode === KEYCODE_BACK_QUOTE && !shiftKey) {
|
||||
return ['`', '`'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleKeydown(e, editor, [autoBracket, setAutoBracket]) {
|
||||
const { value, selectionStart, selectionEnd } = e.target;
|
||||
if (selectionStart !== selectionEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user types a closing bracket explictly, just jump to after the automatically added one
|
||||
if (
|
||||
selectionStart !== 0 &&
|
||||
autoBracket === e.key &&
|
||||
(e.keyCode === KEYCODE_BRACKETS_CLOSE || e.keyCode === KEYCODE_PARENS_CLOSE)
|
||||
) {
|
||||
e.preventDefault();
|
||||
editor._applyEdits({
|
||||
value: value,
|
||||
selectionStart: selectionStart + 1,
|
||||
selectionEnd: selectionStart + 1,
|
||||
});
|
||||
setAutoBracket(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// reset auto brackets
|
||||
setAutoBracket(null);
|
||||
|
||||
// When entering an open bracket/quote, add the closing one
|
||||
const pair = getPair(e);
|
||||
if (pair) {
|
||||
// don't add double apostrophes if it looks like a sentence
|
||||
if (
|
||||
e.keyCode === KEYCODE_QUOTE &&
|
||||
!e.shiftKey &&
|
||||
selectionStart !== 0 &&
|
||||
/[a-zA-Z]/.test(value.charAt(selectionStart - 1))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
editor._applyEdits({
|
||||
value:
|
||||
value.substring(0, selectionStart) +
|
||||
pair[0] +
|
||||
pair[1] +
|
||||
value.substring(selectionEnd),
|
||||
selectionStart: selectionStart + 1,
|
||||
selectionEnd: selectionStart + 1,
|
||||
});
|
||||
setAutoBracket(pair[1]);
|
||||
}
|
||||
|
||||
// When pressing enter immediately after an open bracket, automatically add a newline plus extra indent
|
||||
if (
|
||||
e.keyCode === KEYCODE_ENTER &&
|
||||
selectionEnd !== 0 &&
|
||||
value[selectionEnd - 1] === '{'
|
||||
) {
|
||||
const line = editor._getLines(value, selectionStart).pop();
|
||||
const matches = line.match(/^\s+/);
|
||||
const existingIndent = matches ? matches[0] : '';
|
||||
|
||||
const indent = ' ';
|
||||
const updatedValue =
|
||||
value.substring(0, selectionStart) +
|
||||
'\n' +
|
||||
existingIndent +
|
||||
indent +
|
||||
'\n' +
|
||||
existingIndent +
|
||||
value.substring(selectionEnd);
|
||||
const updatedSelection =
|
||||
selectionStart + 1 /* newline */ + existingIndent.length + indent.length;
|
||||
|
||||
e.preventDefault();
|
||||
editor._applyEdits({
|
||||
value: updatedValue,
|
||||
selectionStart: updatedSelection,
|
||||
selectionEnd: updatedSelection,
|
||||
});
|
||||
}
|
||||
return [selected, toggleSelected];
|
||||
}
|
||||
|
||||
@@ -5,8 +5,20 @@ body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
const themes = {
|
||||
light: {
|
||||
id: 'light',
|
||||
primary: '#aaddff',
|
||||
secondary: '#022550',
|
||||
highlight: '#36368c',
|
||||
lightOrDark: 'light',
|
||||
|
||||
editor: {
|
||||
background: 'none',
|
||||
background: '#ffffff',
|
||||
lineNumber: '#cccccc',
|
||||
lineNumberHl: 'black',
|
||||
lineNumberHl: '#000000',
|
||||
lineNumberHlBackground: '#e0f6ff',
|
||||
primary: 'black',
|
||||
primary: '#000000',
|
||||
selection: '#b3d4fc',
|
||||
comment: 'slategray',
|
||||
comment: '#708090',
|
||||
commentTag: '#A082BD',
|
||||
punctuation: '#999',
|
||||
annotation: '#999',
|
||||
namespace: 'slategray',
|
||||
property: '#e90',
|
||||
constant: '#905',
|
||||
number: '#905',
|
||||
selector: '#690',
|
||||
punctuation: '#999999',
|
||||
annotation: '#999999',
|
||||
namespace: '#708090',
|
||||
property: '#ee9900',
|
||||
constant: '#990055',
|
||||
number: '#990055',
|
||||
selector: '#669900',
|
||||
operator: '#9a6e3a',
|
||||
keyword: '#07a',
|
||||
keyword: '#0077aa',
|
||||
function: '#DD4A68',
|
||||
className: '#DD4A68',
|
||||
variable: '#e90',
|
||||
variable: '#ee9900',
|
||||
},
|
||||
},
|
||||
blue: {
|
||||
id: 'blue',
|
||||
primary: '#022550',
|
||||
secondary: '#aaddff',
|
||||
highlight: '#77c8f9',
|
||||
@@ -37,7 +39,7 @@ const themes = {
|
||||
editor: {
|
||||
background: '#041f29',
|
||||
lineNumber: '#81969A',
|
||||
lineNumberHl: '#fff',
|
||||
lineNumberHl: '#ffffff',
|
||||
lineNumberHlBackground: '#0e303e',
|
||||
primary: '#E0E2E4',
|
||||
selection: '#E0E2E4',
|
||||
@@ -58,6 +60,7 @@ const themes = {
|
||||
},
|
||||
},
|
||||
dark: {
|
||||
id: 'dark',
|
||||
primary: '#c9d1d9', // fg.default
|
||||
secondary: '#010409', // canvas.inset
|
||||
highlight: '#161b22', // canvas.overlay
|
||||
@@ -70,7 +73,6 @@ const themes = {
|
||||
lineNumberHlBackground: '#161b22', // canvas.overlay
|
||||
primary: '#c9d1d9', // fg.default
|
||||
selection: '#c9d1d9', // fg.default
|
||||
|
||||
comment: '#8b949e',
|
||||
commentTag: '#79c0ff',
|
||||
punctuation: '#d2a8ff',
|
||||
@@ -90,3 +92,89 @@ const themes = {
|
||||
};
|
||||
|
||||
export default themes;
|
||||
|
||||
export function makeMonacoTheme(theme) {
|
||||
return {
|
||||
base: theme.lightOrDark === 'light' ? 'vs' : 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{
|
||||
token: '', // minimap
|
||||
foreground: theme.editor.primary.substring(1),
|
||||
background: theme.editor.background.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'string',
|
||||
foreground: theme.editor.selector.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'keyword',
|
||||
foreground: theme.editor.keyword.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'constant',
|
||||
foreground: theme.editor.constant.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'number',
|
||||
foreground: theme.editor.number.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'annotation',
|
||||
foreground: theme.editor.annotation.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'variable',
|
||||
foreground: theme.editor.variable.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'operator',
|
||||
foreground: theme.editor.operator.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'operators',
|
||||
foreground: theme.editor.operator.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'punctuation',
|
||||
foreground: theme.editor.operator.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'delimiter',
|
||||
foreground: theme.editor.punctuation.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'delimiter.square',
|
||||
foreground: theme.editor.punctuation.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'delimiter.bracket',
|
||||
foreground: theme.editor.punctuation.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'delimiter.parenthesis',
|
||||
foreground: theme.editor.punctuation.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'identifier',
|
||||
foreground: theme.editor.function.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'type',
|
||||
foreground: theme.editor.className.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'comment',
|
||||
foreground: theme.editor.comment.substring(1),
|
||||
},
|
||||
{
|
||||
token: 'identifier.java',
|
||||
foreground: theme.editor.primary.substring(1),
|
||||
},
|
||||
],
|
||||
colors: {
|
||||
'editor.background': theme.editor.background,
|
||||
'editor.foreground': theme.editor.primary,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,66 +1,24 @@
|
||||
import {
|
||||
highlight,
|
||||
languages as prismLanguages,
|
||||
} from 'prismjs/components/prism-core';
|
||||
|
||||
import 'prismjs/components/prism-markup';
|
||||
import 'prismjs/components/prism-css';
|
||||
import 'prismjs/components/prism-clike';
|
||||
import 'prismjs/components/prism-javascript';
|
||||
import 'prismjs/components/prism-bash';
|
||||
import 'prismjs/components/prism-diff';
|
||||
import 'prismjs/components/prism-docker';
|
||||
import 'prismjs/components/prism-go';
|
||||
import 'prismjs/components/prism-groovy';
|
||||
import 'prismjs/components/prism-haskell';
|
||||
import 'prismjs/components/prism-java';
|
||||
import 'prismjs/components/prism-javadoclike';
|
||||
import 'prismjs/components/prism-javadoc';
|
||||
import 'prismjs/components/prism-javastacktrace';
|
||||
import 'prismjs/components/prism-javascript';
|
||||
import 'prismjs/components/prism-json';
|
||||
import 'prismjs/components/prism-kotlin';
|
||||
import 'prismjs/components/prism-log';
|
||||
import 'prismjs/components/prism-markdown';
|
||||
import 'prismjs/components/prism-php';
|
||||
import 'prismjs/components/prism-markup-templating';
|
||||
import 'prismjs/components/prism-properties';
|
||||
import 'prismjs/components/prism-protobuf';
|
||||
import 'prismjs/components/prism-python';
|
||||
import 'prismjs/components/prism-jsx';
|
||||
import 'prismjs/components/prism-typescript';
|
||||
import 'prismjs/components/prism-tsx';
|
||||
import 'prismjs/components/prism-ruby';
|
||||
import 'prismjs/components/prism-rust';
|
||||
import 'prismjs/components/prism-sql';
|
||||
import 'prismjs/components/prism-toml';
|
||||
import 'prismjs/components/prism-yaml';
|
||||
|
||||
export const languages = {
|
||||
config: ['yaml', 'json', 'toml', 'properties'],
|
||||
logs: ['log', 'javastacktrace'],
|
||||
config: ['yaml', 'json', 'xml', 'ini'],
|
||||
code: [
|
||||
'java',
|
||||
'javascript',
|
||||
'typescript',
|
||||
'python',
|
||||
'kotlin',
|
||||
'clike',
|
||||
'bash',
|
||||
'cpp',
|
||||
'csharp',
|
||||
'shell',
|
||||
'ruby',
|
||||
'rust',
|
||||
'sql',
|
||||
'go',
|
||||
'groovy',
|
||||
'haskell',
|
||||
],
|
||||
web: ['markup', 'css', 'php', 'jsx', 'tsx'],
|
||||
misc: ['plain', 'docker', 'diff', 'markdown', 'protobuf'],
|
||||
web: ['html', 'css', 'php'],
|
||||
misc: ['plain', 'dockerfile', 'markdown'],
|
||||
};
|
||||
|
||||
export const languageIds = Object.values(languages).flat(1);
|
||||
// missing following the rewrite: toml, properties, log, javastacktrace, groovy, haskell, protobuf
|
||||
// would be good to add these back with custom language definitions
|
||||
|
||||
export function getHighlighter(language) {
|
||||
const grammar = prismLanguages[language] || {};
|
||||
return input => highlight(input, grammar);
|
||||
}
|
||||
export const languageIds = Object.values(languages).flat(1);
|
||||
|
||||