Preview & editor

This commit is contained in:
Simon Cambier 2023-11-24 18:15:30 +01:00
parent 2809dc6295
commit 0f9faa45f0
38 changed files with 259 additions and 172 deletions

View File

@ -3,8 +3,8 @@
<head> <head>
<title>Paste</title> <title>Paste</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" /> <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<link rel="stylesheet" type="text/css" href="%sveltekit.assets%/fonts.css" /> <link rel="preload" as="font" type="text/css" href="%sveltekit.assets%/hack-subset.css" />
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
@ -14,18 +14,10 @@ npm/codemirror@5.65.16/theme/nord.min.css"
/> />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
<script src="https://cdn.jsdelivr.net/combine/npm/lzma@2.3.2/src/lzma.min.js"></script>
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div class="dark:text-gray-300 dark:bg-gray-800 min-h-screen">%sveltekit.body%</div> <div class="dark:text-gray-300 dark:bg-gray-800 min-h-screen">%sveltekit.body%</div>
<script src="https://cdn.jsdelivr.net/combine/
npm/lzma@2.3.2/src/lzma.min.js,
npm/codemirror@5.65.16,
npm/codemirror@5.65.16/addon/mode/loadmode.min.js,
npm/codemirror@5.65.16/addon/mode/overlay.min.js,
npm/codemirror@5.65.16/addon/mode/multiplex.min.js,
npm/codemirror@5.65.16/addon/mode/simple.min.js,
npm/codemirror@5.65.16/addon/scroll/simplescrollbars.js,
npm/codemirror@5.65.16/mode/meta.min.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,122 @@
<script lang="ts">
import { onMount, tick } from 'svelte'
import { shorten, getLangFromUrl } from '$lib/utils'
import type { Editor } from 'codemirror'
import { shareUrl, selectedLang } from '../store'
import ComboBox from './ComboBox.svelte'
import Icon from '@iconify/svelte'
type Language = {
text: string
value: string
data: { mime: string; mode: string }
}
let cssClass = ''
export { cssClass as class }
export let editor: Editor | null = null
export let updateShareUrl: () => Promise<void>
let languages: Language[] = []
let selectedLanguage: Language | null = null
let textWrap = false
let urlInput: HTMLInputElement
let isUrlInputVisible = false
onMount(() => {
$selectedLang = getLangFromUrl()
selectedLanguage = languages.find((e) => e.value === $selectedLang)!
})
$: {
if (editor) {
languages = CodeMirror.modeInfo
.map((e: any) => ({
text: e.name,
value: shorten(e.name),
data: { mime: e.mime, mode: e.mode },
}))
.filter((l: any) => l.value !== 'gflm') // Remove github flavored markdown, redundant with markdown
selectedLanguage =
selectedLanguage ??
languages.find((e) => e.value === $selectedLang) ??
languages.find((e) => e.value === 'plt')!
const langData = selectedLanguage?.data ?? { mime: null, mode: null }
// @ts-ignore
editor.setOption('mode', langData.mime)
CodeMirror.autoLoadMode(editor, langData.mode)
$selectedLang = selectedLanguage?.value ?? 'plt'
// Line wrapping
editor.setOption('lineWrapping', textWrap)
}
}
async function showUrlInput() {
// Make sure the url is up to date
await updateShareUrl()
isUrlInputVisible = true
await tick()
urlInput.select()
}
async function copyUrl() {
urlInput.select()
await navigator.clipboard.writeText(urlInput.value)
setTimeout(closeUrlInput, 500)
}
function closeUrlInput() {
isUrlInputVisible = false
}
</script>
<div class={cssClass}>
{#if isUrlInputVisible}
<div class="flex gap-2 grow">
<input
bind:this={urlInput}
type="text"
class="border border-gray-300 bg-transparent p-1 grow"
value={$shareUrl}
/>
<button class="button" on:click={copyUrl}>Copy</button>
<button class="button" on:click={closeUrlInput}>Close</button>
</div>
{:else}
<div class="flex justify-end gap-2">
<div>
<ComboBox
items={languages}
bind:value={selectedLanguage}
class="bg-gray-700 border border-gray-300 p-1"
/>
</div>
<!-- Show link input-->
<button class="button" on:click={showUrlInput}>Get Link</button>
<!-- Toggle text wrap -->
<button class="button" title="Toggle text wrap" on:click={() => (textWrap = !textWrap)}>
{#if textWrap}
<Icon class="text-xl" icon="fluent:text-wrap-24-filled" />
{:else}
<Icon class="text-xl" icon="fluent:text-wrap-off-24-filled" />
{/if}
</button>
<!-- Switch to readonly view -->
<a class="button" href={$shareUrl}>
<Icon class="text-xl" icon="fluent:eye-12-regular" />
</a>
</div>
{/if}
</div>
<style lang="scss">
.button {
@apply text-sm border border-gray-300 p-1 hover:bg-gray-600/50;
}
</style>

View File

@ -1,135 +1,17 @@
<script lang="ts">
import { onMount, tick } from 'svelte'
import { shorten } from '$lib/utils'
import type { Editor } from 'codemirror'
import { shareUrl, selectedLang } from '../store'
import ComboBox from './ComboBox.svelte'
import Icon from '@iconify/svelte'
type Language = {
text: string
value: string
data: { mime: string; mode: string }
}
export let editor: Editor | null = null
export let updateShareUrl: () => Promise<void>
let languages: Language[] = []
let selectedLanguage: Language | null = null
let textWrap = false
let urlInput: HTMLInputElement
let isUrlInputVisible = false
$: {
if (editor) {
const language = selectedLanguage?.data ?? { mime: null, mode: null }
// @ts-ignore
editor.setOption('mode', language.mime)
CodeMirror.autoLoadMode(editor, language.mode)
$selectedLang = selectedLanguage?.value ?? null
// Line wrapping
editor.setOption('lineWrapping', textWrap)
}
}
onMount(() => {
languages = CodeMirror.modeInfo
.map((e: any) => ({
text: e.name,
value: shorten(e.name),
data: { mime: e.mime, mode: e.mode },
}))
.filter((l: any) => l.value !== 'gflm') // Remove github flavored markdown, redundant with markdown
console.log(languages)
})
export function setLanguage(lang: string) {
if (lang === 'mrwn' || lang === 'gflm') {
// back compatiblity with old links
lang = 'md'
}
const language = languages.find((e) => e.value === lang)!
selectedLanguage = language
// Automatic text wrap for plain text or markdown
if (lang === 'plt' || lang === 'mrwn' || !lang) {
textWrap = true
}
}
async function showUrlInput() {
// Make sure the url is up to date
await updateShareUrl()
isUrlInputVisible = true
await tick()
urlInput.select()
}
async function copyUrl() {
urlInput.select()
await navigator.clipboard.writeText(urlInput.value)
closeUrlInput()
}
function closeUrlInput() {
isUrlInputVisible = false
}
</script>
<div <div
class="flex flex-wrap justify-between items-center px-3 py-1 text-sm relative z-10 shadow-md gap-2" class="flex flex-wrap justify-between items-center px-3 py-1 text-sm relative z-10 shadow-md gap-2 font-mono bg-gray-700"
> >
<div class="flex items-center"> <div class="flex items-center">
<h1 class="text-xl">Paste</h1> <h1 class="text-xl">Paste</h1>
<span class="ml-8 text-xs"> <span class="ml-8 text-xs">
<a <a
href="https://paste.scambier.xyz/?l=mrwn#G14EICwPbMc0BJ69xNPoItLoErDRMwMj3+vxE5RxArxajeByk+b0b1Tm2h/5ugGgK6S83AqUCi09HeYmDs+vvzQc0QDZe7v7U2rHnFe6CTWSm2AArNua4tqtXVIEFVQuqG+is15etOm1OZOPPCH4xDX1iDNF2bQcN9ugum0Icnwj51n0nlvNacn/N3OOctSGrQbLoNzG8n5bSoJQXOx+PsxV+MeUl+rkKNPIAXT9c8LoOY/772FiFkLv/5w8rvtCBc38VEE5lzmGFcZ0QtqlzBhvWLV5WPhRkROqeoKoDCtH03/fQVI5Y8HfLkq2/Nuidd1GQTKgmPJQVWbZ4cAiqUdOJG6j6pvu48qWjqamYG3nFPHcdYLHYCRV877U2mBgSHTbLWv6eu/DEA88SE5XEIqiQFyDVS2HLN7jJSloLHQJ+jOE86zb9jrkQ9tXAwkAoovCcnlrW5T/EU4L7imRsDAkZdEySO/ri/NBuHPMpSFnqGItLlPd98/SZSmySbImOLfv4WGbdCxw9TYykGsk56aQaVSa/DB2t5HfD6x28lJVlMm3Hjnkwyx95PxNGH0teCAk6CqtbBYlC3LjhY0IEpJ0WWZWY6MlJB+jEW7lAN3DSJD0QFz2FZOOV/HwA9LbFBU=" href="/?l=md#G14EICwPbMc0BJ69xNPoItLoErDRMwMj3+vxE5RxArxajeByk+b0b1Tm2h/5ugGgK6S83AqUCi09HeYmDs+vvzQc0QDZe7v7U2rHnFe6CTWSm2AArNua4tqtXVIEFVQuqG+is15etOm1OZOPPCH4xDX1iDNF2bQcN9ugum0Icnwj51n0nlvNacn/N3OOctSGrQbLoNzG8n5bSoJQXOx+PsxV+MeUl+rkKNPIAXT9c8LoOY/772FiFkLv/5w8rvtCBc38VEE5lzmGFcZ0QtqlzBhvWLV5WPhRkROqeoKoDCtH03/fQVI5Y8HfLkq2/Nuidd1GQTKgmPJQVWbZ4cAiqUdOJG6j6pvu48qWjqamYG3nFPHcdYLHYCRV877U2mBgSHTbLWv6eu/DEA88SE5XEIqiQFyDVS2HLN7jJSloLHQJ+jOE86zb9jrkQ9tXAwkAoovCcnlrW5T/EU4L7imRsDAkZdEySO/ri/NBuHPMpSFnqGItLlPd98/SZSmySbImOLfv4WGbdCxw9TYykGsk56aQaVSa/DB2t5HfD6x28lJVlMm3Hjnkwyx95PxNGH0teCAk6CqtbBYlC3LjhY0IEpJ0WWZWY6MlJB+jEW7lAN3DSJD0QFz2FZOOV/HwA9LbFBU="
target="_blank" target="_blank"
> >
About</a About
> </a>
<a class="ml-4" href="https://git.scambier.xyz/scambier/paste" target="_blank">Source</a> <a class="ml-4" href="https://git.scambier.xyz/scambier/paste" target="_blank">Source</a>
</span> </span>
</div> </div>
<slot />
{#if isUrlInputVisible}
<div class="flex gap-2 grow">
<input
bind:this={urlInput}
type="text"
class="border border-gray-300 bg-transparent p-1 grow"
value={$shareUrl}
/>
<button on:click={copyUrl}>Copy</button>
<button on:click={closeUrlInput}>Close</button>
</div>
{:else}
<div class="flex justify-end gap-2">
<div>
<ComboBox
items={languages}
bind:value={selectedLanguage}
class="bg-gray-700 border border-gray-300 p-1"
/>
</div>
<button title="Toggle text wrap" on:click={() => (textWrap = !textWrap)}>
{#if textWrap}
<Icon class="text-xl" icon="fluent:text-wrap-24-filled" />
{:else}
<Icon class="text-xl" icon="fluent:text-wrap-off-24-filled" />
{/if}
</button>
<div>
<button on:click={showUrlInput}> Generate Link </button>
</div>
</div>
{/if}
</div> </div>
<style lang="scss">
button {
@apply text-sm border border-gray-300 p-1 hover:bg-gray-600/50;
}
</style>

View File

@ -28,4 +28,8 @@ export const shorten = (name: string) => {
return n.substr(0, 2) + n.substr(n.length - 2, 2) return n.substr(0, 2) + n.substr(n.length - 2, 2)
} }
export function getLangFromUrl() {
return new URLSearchParams(window.location.search).get('l') ?? 'plt'
}
export const byId = (id: string) => document.getElementById(id) export const byId = (id: string) => document.getElementById(id)

View File

@ -10,13 +10,16 @@
import rehypeStringify from 'rehype-stringify' import rehypeStringify from 'rehype-stringify'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import 'highlight.js/styles/nord.min.css' import 'highlight.js/styles/nord.min.css'
import TopBar from '../components/TopBar.svelte'
import Icon from '@iconify/svelte'
import { getLangFromUrl } from '$lib/utils'
let decompressed: string let decompressed: string
let isMarkdown = false let isMarkdown = false
let isPlainText = false let isPlainText = false
onMount(async () => { onMount(async () => {
let lang = new URLSearchParams(window.location.search).get('l') let lang = getLangFromUrl()
lang = lang === 'mrwn' || lang === 'gflm' ? 'md' : lang // back compatiblity with old links lang = lang === 'mrwn' || lang === 'gflm' ? 'md' : lang // back compatiblity with old links
// extract the part in the url after the hash // extract the part in the url after the hash
@ -30,7 +33,7 @@
} }
// Markdown // Markdown
if (lang === 'md' || lang === 'gflm') { if (lang === 'md') {
const html = await unified() const html = await unified()
.use(remarkParse) .use(remarkParse)
.use(remarkGfm) .use(remarkGfm)
@ -54,9 +57,18 @@
window.location.href = '/editor' window.location.href = '/editor'
} }
}) })
function getUrlDataPart(): string {
return window.location.search + window.location.hash
}
</script> </script>
<div> <div>
<TopBar>
<a href={'/editor' + getUrlDataPart()} class="p-1 hover:bg-gray-600/50" title="Edit a copy of this note">
<Icon class="text-xl" icon="fluent:document-edit-16-regular" />
</a>
</TopBar>
<div class="prose dark:prose-invert lg:py-12 p-[0.5em] md:max-w-3xl md:mx-auto lg:max-w-4xl"> <div class="prose dark:prose-invert lg:py-12 p-[0.5em] md:max-w-3xl md:mx-auto lg:max-w-4xl">
{#if isMarkdown} {#if isMarkdown}
{@html decompressed} {@html decompressed}

View File

@ -1,13 +1,12 @@
<script lang="ts"> <script lang="ts">
import type { Editor } from 'codemirror' import type { Editor } from 'codemirror'
import { onMount } from 'svelte'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import * as brotli from '$lib/brotli' import * as brotli from '$lib/brotli'
import * as lzma from '$lib/lzma' import * as lzma from '$lib/lzma'
import { byId } from '$lib/utils' import { byId } from '$lib/utils'
import TopBar from '../../components/TopBar.svelte' import TopBar from '../../components/TopBar.svelte'
import { selectedLang, shareUrl } from '../../store' import { selectedLang, shareUrl } from '../../store'
import EditForm from '../../components/EditForm.svelte'
let editor: Editor | null = null let editor: Editor | null = null
const readOnly = false const readOnly = false
@ -15,33 +14,6 @@
let charLen = 0 let charLen = 0
let compressed: string = '' let compressed: string = ''
let waiting = false let waiting = false
let setLanguage: (lang: string) => void
onMount(async () => {
initCodeEditor()
// extract the part in the url after the hash
const hash = window.location.hash.slice(1)
if (hash) {
// decompress the data
let decompressed: string
if (hash.startsWith('XQAAA')) {
decompressed = await lzma.decompress(hash)
} else {
decompressed = await brotli.decompress(hash)
}
// set the editor value
if (editor) {
editor.setValue(decompressed)
}
}
const lang = new URLSearchParams(window.location.search).get('l')
if (lang) {
setLanguage(lang)
} else {
setLanguage('plt')
}
})
async function updateShareUrl() { async function updateShareUrl() {
if (editor) { if (editor) {
@ -60,7 +32,7 @@
waiting = false waiting = false
}, 1000) }, 1000)
const initCodeEditor = () => { async function initCodeEditor() {
CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/%N/%N.js' CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/%N/%N.js'
editor = new CodeMirror(byId('editor'), { editor = new CodeMirror(byId('editor'), {
lineNumbers: true, lineNumbers: true,
@ -81,11 +53,45 @@
updateShareUrlDebounced() updateShareUrlDebounced()
} }
}) })
// extract the part in the url after the hash
const hash = window.location.hash.slice(1)
if (hash) {
// decompress the data
let decompressed: string
if (hash.startsWith('XQAAA')) {
decompressed = await lzma.decompress(hash)
} else {
decompressed = await brotli.decompress(hash)
}
// set the editor value
if (editor) {
editor.setValue(decompressed)
}
}
$selectedLang = new URLSearchParams(window.location.search).get('l') ?? ' plt'
} }
</script> </script>
<svelte:head>
<script
src="https://cdn.jsdelivr.net/combine/
npm/codemirror@5.65.16,
npm/codemirror@5.65.16/addon/mode/loadmode.min.js,
npm/codemirror@5.65.16/addon/mode/overlay.min.js,
npm/codemirror@5.65.16/addon/mode/multiplex.min.js,
npm/codemirror@5.65.16/addon/mode/simple.min.js,
npm/codemirror@5.65.16/addon/scroll/simplescrollbars.js,
npm/codemirror@5.65.16/mode/meta.min.js"
on:load={initCodeEditor}
></script>
</svelte:head>
<div class="flex flex-col font-mono h-screen bg-gray-700"> <div class="flex flex-col font-mono h-screen bg-gray-700">
<TopBar {editor} {updateShareUrl} bind:setLanguage /> <TopBar>
<EditForm {editor} {updateShareUrl} class="grow" />
</TopBar>
<div id="editor" class="grow overflow-hidden" /> <div id="editor" class="grow overflow-hidden" />
@ -98,13 +104,13 @@
? '?' ? '?'
: Math.round(($shareUrl.length / charLen) * 100)}% of original) : Math.round(($shareUrl.length / charLen) * 100)}% of original)
</div> </div>
</div> </div>
<style> <style>
:global(div.CodeMirror) { :global(div.CodeMirror) {
height: 100%; height: 100%;
font-size: 15px; font-size: 15px;
font-family: 'Hack', monospace;
} }
#footer { #footer {
--tw-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);

View File

@ -1,4 +1,4 @@
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
export const shareUrl = writable('') export const shareUrl = writable('')
export const selectedLang = writable<string | null>(null) export const selectedLang = writable<string>('plt')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

1
static/favicon.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><g fill="none"><path fill="#E19747" d="M5 4.5A1.5 1.5 0 0 1 6.5 3h19A1.5 1.5 0 0 1 27 4.5v24a1.5 1.5 0 0 1-1.5 1.5h-19A1.5 1.5 0 0 1 5 28.5v-24Z"/><path fill="#F3EEF8" d="M25 6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v21a1 1 0 0 0 1 1h10.586c.147 0 .29-.032.421-.093l.321-.783l.632-4.086l4.457-.783l.49-.247a1 1 0 0 0 .093-.422V6Z"/><path fill="#CDC4D6" d="M24.91 22H20a1 1 0 0 0-1 1v4.91a1 1 0 0 0 .293-.203l5.414-5.414A1 1 0 0 0 24.91 22Z"/><path fill="#9B9B9B" d="M18 4a2 2 0 1 0-4 0h-1a2 2 0 0 0-2 2v1.5a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5V6a2 2 0 0 0-2-2h-1Zm-1 0a1 1 0 1 1-2 0a1 1 0 0 1 2 0Zm-8 8.5a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5Zm0 3a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 0 1h-13a.5.5 0 0 1-.5-.5Zm.5 2.5a.5.5 0 0 0 0 1h13a.5.5 0 0 0 0-1h-13ZM9 21.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5Z"/></g></svg>

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

34
static/hack-subset.css Normal file
View File

@ -0,0 +1,34 @@
/*!
* Hack typeface https://github.com/source-foundry/Hack
* License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
*/
/* FONT PATHS
* -------------------------- */
@font-face {
font-family: 'Hack';
src: url('fonts/hack-regular-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular-subset.woff?sha=3114f1256') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-bold-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold-subset.woff?sha=3114f1256') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-italic-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic-webfont.woff?sha=3114f1256') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-bolditalic-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic-subset.woff?sha=3114f1256') format('woff');
font-weight: 700;
font-style: italic;
}

34
static/hack.css Normal file
View File

@ -0,0 +1,34 @@
/*!
* Hack typeface https://github.com/source-foundry/Hack
* License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md
*/
/* FONT PATHS
* -------------------------- */
@font-face {
font-family: 'Hack';
src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Hack';
src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff');
font-weight: 700;
font-style: italic;
}

View File

@ -6,7 +6,7 @@ export default {
theme: { theme: {
extend: { extend: {
fontFamily: { fontFamily: {
mono: ['"JetBrainsMono"', ...defaultTheme.fontFamily.mono], mono: ['"Hack"', ...defaultTheme.fontFamily.mono],
}, },
typography: { typography: {
DEFAULT: { DEFAULT: {