This commit is contained in:
Simon Cambier 2023-10-29 17:25:22 +01:00
parent 906ead2d9a
commit 36bc353ff5
35 changed files with 1056 additions and 145 deletions

View File

@ -1,9 +1,10 @@
{ {
"useTabs": true, "useTabs": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "es5",
"printWidth": 100, "printWidth": 100,
"plugins": ["prettier-plugin-svelte"], "plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."], "pluginSearchDirs": ["."],
"semi": false,
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }

View File

@ -14,18 +14,32 @@
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4", "@sveltejs/kit": "^1.20.4",
"@types/codemirror": "^5.60.12",
"@types/lodash-es": "^4.17.10",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"autoprefixer": "^10.4.16",
"eslint": "^8.28.0", "eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0", "eslint-plugin-svelte": "^2.30.0",
"postcss": "^8.4.31",
"prettier": "^2.8.0", "prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5", "svelte": "^4.0.5",
"svelte-check": "^3.4.3", "svelte-check": "^3.4.3",
"tailwindcss": "^3.3.5",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vite": "^4.4.2" "vite": "^4.4.2"
}, },
"type": "module" "type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.3",
"brotli-compress": "^1.3.3",
"lodash-es": "^4.17.21",
"sass": "^1.69.5",
"slim-select": "^2.6.0",
"svelte-select": "^5.7.0",
"tw-elements": "^1.0.0"
}
} }

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -3,10 +3,33 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" type="text/css" href="%sveltekit.assets%/fonts.css" />
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/combine/
npm/bootstrap@4.6.1/dist/css/bootstrap-grid.min.css,
npm/codemirror@5.65.5/lib/codemirror.min.css,
npm/codemirror@5.65.5/addon/scroll/simplescrollbars.css,
npm/codemirror@5.65.5/theme/dracula.min.css,
npm/microtip@0.2.2/microtip.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%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents" class="h-screen text-gray-300">%sveltekit.body%</div>
<script src="https://cdn.jsdelivr.net/combine/
npm/lzma@2.3.2/src/lzma.min.js,
npm/clipboard@2.0.11/dist/clipboard.min.js,
npm/micromodal@0.4.10/dist/micromodal.min.js,
npm/codemirror@5.65.5,
npm/codemirror@5.65.5/addon/mode/loadmode.min.js,
npm/codemirror@5.65.5/addon/mode/overlay.min.js,
npm/codemirror@5.65.5/addon/mode/multiplex.min.js,
npm/codemirror@5.65.5/addon/mode/simple.min.js,
npm/codemirror@5.65.5/addon/scroll/simplescrollbars.js,
npm/codemirror@5.65.5/mode/meta.min.js"></script>
</body> </body>
</html> </html>

5
src/app.postcss Normal file
View File

@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
/* App layout */

View File

@ -0,0 +1,20 @@
<script lang="ts">
type Item = { text: string }
export let value: Item | null = null
export let items: Item[]
</script>
<select bind:value={value} class={ ($$restProps.class ?? '') }>
{#each items as item}
<option value={item}>
{item.text}
</option>
{/each}
</select>
<style lang="scss">
select {
appearance: none;
}
</style>

View File

@ -0,0 +1,121 @@
<script lang="ts">
import { onMount, tick } from 'svelte'
import { shorten } from '$lib/utils'
import Select from './Select.svelte'
import type { Editor } from 'codemirror'
type Language = {
text: string
value: string
data: { mime: string; mode: string }
}
export let editor: Editor
let languages: Language[] = []
let selectedLanguage: Language | null = null
let urlInput: HTMLInputElement
let isUrlInputVisible = false
$: setDefaultLanguage(languages)
$: {
const language = selectedLanguage?.data ?? { mime: null, mode: null }
// @ts-ignore
editor.setOption('mode', language.mime)
CodeMirror.autoLoadMode(editor, language.mode)
}
// const initLangSelector = () => {
// select = new SlimSelect({
// select: '#language',
// data: CodeMirror.modeInfo.map((e) => ({
// text: e.name,
// value: shorten(e.name),
// data: { mime: e.mime, mode: e.mode },
// })),
// settings: {
// openPosition: 'down',
// },
// events: {
// afterChange: (e) => {
// console.log(e)
// // const language = e.data || { mime: null, mode: null }
// // editor.setOption('mode', language.mime)
// // CodeMirror.autoLoadMode(editor, language.mode)
// // document.title =
// // e.text && e.text !== 'Plain Text' ? `NoPaste - ${e.text} code snippet` : 'NoPaste'
// },
// },
// })
// // Set lang selector
// const l = new URLSearchParams(window.location.search).get('l')
// }
onMount(() => {
languages = CodeMirror.modeInfo.map((e) => ({
text: e.name,
value: shorten(e.name),
data: { mime: e.mime, mode: e.mode },
}))
// initLangSelector()
})
function setDefaultLanguage(languages: Language[]) {
const lang = languages.find((e) => e.text.toLowerCase() === 'plain text')!
selectedLanguage = lang
}
async function showUrlInput() {
isUrlInputVisible = true
await tick()
urlInput.select()
}
async function copyUrl() {
urlInput.select()
await navigator.clipboard.writeText(urlInput.value)
closeUrlInput()
}
function closeUrlInput() {
isUrlInputVisible = false
}
</script>
<div class="flex justify-between p-2 text-sm relative">
<div>NoPaste</div>
<div class="flex justify-end gap-2">
<div>
<Select
items={languages}
bind:value={selectedLanguage}
class="bg-gray-700 border border-gray-300 p-1"
/>
</div>
<div>
<button on:click={showUrlInput}> Generate Link </button>
</div>
</div>
{#if isUrlInputVisible}
<div class="absolute w-full h-full bg-gray-700 top-0 left-0 p-2">
<div class="flex gap-2">
<input
bind:this={urlInput}
type="text"
class="border border-gray-300 bg-transparent p-1 grow"
value="url goes here"
/>
<button on:click={copyUrl}> Copy </button>
<button on:click={closeUrlInput}> Close </button>
</div>
</div>
{/if}
</div>
<style lang="scss">
button {
@apply text-sm border border-gray-300 p-1 hover:bg-gray-600/50;
}
</style>

7
src/globals.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare class LZMA {
constructor(any): {}
compress: (value: string, level: number, callback: (result: number[]) => void) => void
decompress: (b: string, callback: (result: any) => void) => void
}
declare const CodeMirror: any

20
src/lib/brotli.ts Normal file
View File

@ -0,0 +1,20 @@
import { compress as brotliCompress, decompress as brotliDecompress } from 'brotli-compress'
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
async function compress(value: string): Promise<string> {
const data = textEncoder.encode(value)
const compressed = await brotliCompress(data)
return btoa(String.fromCharCode(...compressed))
}
async function decompress(b: string): Promise<string> {
const data = atob(b)
const bytes = Uint8Array.from(data, (c) => c.charCodeAt(0))
const decompressed = await brotliDecompress(bytes)
const decoded = textDecoder.decode(decompressed)
return decoded
}
export { compress, decompress }

23
src/lib/lzma.ts Normal file
View File

@ -0,0 +1,23 @@
// LZMA imported from <script> in index.html
const blob = new Blob(['importScripts("https://cdn.jsdelivr.net/npm/lzma@2.3.2/src/lzma_worker.min.js");']);
const lzma = new LZMA(window.URL.createObjectURL(blob));
async function compress(value: string): Promise<string> {
return new Promise((resolve, reject) => {
lzma.compress(value, 1, (numbers: number[]) => {
const bytes = new Uint8Array(numbers);
const b64 = btoa(String.fromCharCode(...bytes))
resolve(b64)
})
})
}
async function decompress(b: string): Promise<string> {
return new Promise((resolve, reject) => {
lzma.decompress(b, (result: any) => {
resolve(result)
})
})
}
export { compress, decompress }

31
src/lib/utils.ts Normal file
View File

@ -0,0 +1,31 @@
export const slugify = (str: string) =>
str
.trim()
.toString()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/\+/g, '-p')
.replace(/#/g, '-sharp')
.replace(/[^\w\-]+/g, '')
export const shorten = (name: string) => {
let n = slugify(name).replace('script', '-s').replace('python', 'py')
const nov = (s: string) => s[0] + s.substr(1).replace(/[aeiouy-]/g, '')
if (n.replace(/-/g, '').length <= 4) {
return n.replace(/-/g, '')
}
if (n.split('-').length >= 2) {
return n
.split('-')
.map((x) => nov(x.substr(0, 2)))
.join('')
.substr(0, 4)
}
n = nov(n)
if (n.length <= 4) {
return n
}
return n.substr(0, 2) + n.substr(n.length - 2, 2)
}
export const byId = (id: string) => document.getElementById(id)

View File

@ -0,0 +1,5 @@
<script lang="ts">
import '../app.postcss'
</script>
<slot />

2
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,2 @@
export const prerender = true
export const ssr = false

View File

@ -1,2 +1,82 @@
<h1>Welcome to SvelteKit</h1> <script lang="ts">
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p> import type { Editor } from 'codemirror'
import { onMount } from 'svelte'
import { debounce } from 'lodash-es'
import * as brotli from '$lib/brotli'
import * as lzma from '$lib/lzma'
import { byId } from '$lib/utils'
import TopBar from '../components/TopBar.svelte'
const algorithm: {
compress: (data: string) => Promise<string>
decompress: (data: string) => Promise<string>
} = brotli
const host = window.location.protocol + '//' + window.location.host + '/'
let editor: Editor | null = null
const readOnly = false
let charLen = 0
let compressed: string = ''
let waiting = false
$: shareUrl = host + compressed
const updateCompressed = debounce(async () => {
if (editor) {
compressed = await algorithm.compress(editor.getValue())
console.log(compressed)
}
waiting = false
}, 1000)
const initCodeEditor = () => {
CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.5/mode/%N/%N.js'
editor = new CodeMirror(byId('editor'), {
lineNumbers: true,
theme: 'dracula',
readOnly: readOnly,
lineWrapping: false,
scrollbarStyle: 'native',
}) as Editor
console.log(editor)
if (readOnly) {
document.body.classList.add('readonly')
}
editor.on('change', async () => {
if (editor) {
waiting = true
charLen = editor.getValue().length
updateCompressed()
}
})
}
onMount(async () => {
initCodeEditor()
})
</script>
<div class="flex flex-col font-mono h-screen bg-gray-700">
{#if !!editor}
<TopBar {editor} />
{/if}
<div id="editor" class="grow overflow-hidden" />
{editor?.getValue()}
<div class="p-2 text-sm">
Data length: {charLen}
| Link length: {waiting ? '?' : shareUrl.length}
({waiting || !compressed.length || !charLen
? '?'
: Math.round((shareUrl.length / charLen) * 100)}% of original)
</div>
</div>
<style>
:global(div.CodeMirror) {
height: 100%;
}
</style>

116
static/fonts.css Normal file
View File

@ -0,0 +1,116 @@
/* 0. extract fonts into `JetBrainsMono` folder */
/* 1. check fonts `urls` with your directory structure */
/* normal fonts */
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 100;
src: url("/fonts/JetBrainsMono-Thin.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 200;
src: url("/fonts/JetBrainsMono-ExtraLight.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 300;
src: url("/fonts/JetBrainsMono-Light.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 400;
src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 500;
src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 600;
src: url("/fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 700;
src: url("/fonts/JetBrainsMono-Bold.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: normal;
font-weight: 800;
src: url("/fonts/JetBrainsMono-ExtraBold.woff2")
format("woff2");
}
/* italic fonts */
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 100;
src: url("/fonts/JetBrainsMono-ThinItalic.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 200;
format("truetype");
src: url("/fonts/JetBrainsMono-ExtraLightItalic.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 300;
src: url("/fonts/JetBrainsMono-LightItalic.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 400;
src: url("/fonts/JetBrainsMono-Italic.woff2") format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 500;
src: url("/fonts/JetBrainsMono-MediumItalic.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 600;
format("truetype");
src: url("/fonts/JetBrainsMono-SemiBoldItalic.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 700;
src: url("/fonts/JetBrainsMono-BoldItalic.woff2")
format("woff2");
}
@font-face {
font-family: JetBrainsMono;
font-style: italic;
font-weight: 800;
format("truetype");
src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2")
format("woff2");
}

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.

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.

View File

@ -1,5 +1,6 @@
import adapter from '@sveltejs/adapter-auto'; // import adapter from '@sveltejs/adapter-auto'
import { vitePreprocess } from '@sveltejs/kit/vite'; import { vitePreprocess } from '@sveltejs/kit/vite'
import adapter from '@sveltejs/adapter-static'
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@ -11,8 +12,8 @@ const config = {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter. // If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter() adapter: adapter(),
} },
}; }
export default config; export default config

14
tailwind.config.js Normal file
View File

@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require('tailwindcss/defaultTheme')
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {
fontFamily: {
mono: ['"JetBrainsMono"', ...defaultTheme.fontFamily.mono],
},
},
},
plugins: [],
}

View File

@ -1,6 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'; import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [sveltekit()],
}); })