inital commit

This commit is contained in:
Chris W 2023-10-06 17:08:13 -06:00
commit de6021a735
45 changed files with 5876 additions and 0 deletions

13
.eslintignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

30
.eslintrc.cjs Normal file
View File

@ -0,0 +1,30 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

13
.prettierignore Normal file
View File

@ -0,0 +1,13 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

9
.prettierrc Normal file
View File

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

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

3980
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "paste69-svelte",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@skeletonlabs/skeleton": "^2.2.0",
"@skeletonlabs/tw-plugin": "^0.2.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.8.2",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"tailwindcss": "^3.3.2",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.4.2"
},
"type": "module",
"dependencies": {
"@sentry/node": "^7.73.0",
"@ts-stack/markdown": "^1.5.0",
"highlight.js": "^11.8.0",
"mongodb": "^6.1.0",
"random-words": "^2.0.0",
"svelte-tabler": "^0.6.3"
}
}

13
postcss.config.cjs Normal file
View File

@ -0,0 +1,13 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;

12
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" class="w-full h-full py-[20px] px-[50px] dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body class="w-full h-full min-h-full" data-sveltekit-preload-data="hover" data-theme="skeleton">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

4
src/app.postcss Normal file
View File

@ -0,0 +1,4 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;

15
src/db/index.ts Normal file
View File

@ -0,0 +1,15 @@
import { MongoClient } from 'mongodb';
import { DB_URL } from '$env/static/private';
import type PasteSchema from "./paste-schema";
const client = new MongoClient(DB_URL);
await client.connect();
const db = client.db("paste69");
const pastes = db.collection<PasteSchema>("pastes");
export {
db,
pastes,
}

8
src/db/paste-schema.ts Normal file
View File

@ -0,0 +1,8 @@
export default interface PasteSchema {
id: string;
contents: string;
highlight: string;
encrypted: boolean;
burnAfterReading: boolean;
createdAt: Date;
}

20
src/hooks.server.ts Normal file
View File

@ -0,0 +1,20 @@
import { SENTRY_DSN } from '$env/static/private';
import * as Sentry from '@sentry/node';
import type { HandleServerError } from '@sveltejs/kit';
Sentry.init({
dsn: SENTRY_DSN,
});
interface ServerError extends Error {
code?: string;
}
export const handleError: HandleServerError = ({ error, event }) => {
Sentry.captureException(error, { extra: { event } });
return {
message: 'Uh oh! An unexpected error occurred.',
code: (error as ServerError)?.code ?? '500',
};
};

41
src/lib/assets/logo.svg Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns:serif="http://www.serif.com/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1010.9 930.9"
style="enable-background:new 0 0 1010.9 930.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#118979;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#149C8B;}
</style>
<g transform="matrix(1,0,0,1,-1339,0)">
<g transform="matrix(0.862194,0,0,1.01029,1279.19,36.8297)">
<rect id="No-Bg" x="29.7" y="-109.9" serif:id="No Bg" class="st0" width="1251.7" height="1068.3">
</rect>
<g id="No-Bg1" serif:id="No Bg">
<g transform="matrix(1.15983,0,0,0.989813,-103.977,-35.1696)">
<g transform="matrix(1,0,0,1,-569.977,-312.934)">
<path class="st1" d="M1730.3,724.8c0.8,148.9-23.4,272.5-72.7,370.6c-49.3,98.1-111.5,147.1-186.8,147.1
c-84.1,0-152.9-44.4-206.6-133.3c-42.4-27.2-63.7-54.1-63.7-80.5c0-6.4,1.2-12.8,3.6-19.2c10.4-28,44-42,100.9-42
c44,0,75.3,9.6,93.7,28.8c6.4,7.2,9.6,16.4,9.6,27.6c0,3.2-0.4,8.4-1.2,15.6c-0.8,7.2-1.2,12.4-1.2,15.6c0,11.2,4,20,12,26.4
c5.6,22.4,18.4,33.2,38.4,32.4c17.6-0.8,36.8-27.2,57.6-79.3c20.8-52.1,32.8-104.9,36-158.6c-24.8,32.8-58.5,49.2-100.9,49.2
c-66.5,0-121.3-26-164.6-78.1c-40-47.2-62.1-105.7-66.1-175.4c-3.2-56.1,14.8-122.9,54.1-200.6c44.8-90.5,96.5-135.7,155-135.7
c92.1,0,165.4,34.6,219.8,103.9C1701.9,508.8,1729.5,603.9,1730.3,724.8z M1528.5,646.8c4-19.2,6-40.8,6-64.9
c0-63.3-11.6-98.1-34.8-104.5c-21.6-5.6-43.6,8.8-66.1,43.2c-20,28.8-34,62.1-42,99.7c-5.6,24-8.4,48.4-8.4,73.3
c0,64.9,17.2,100.9,51.7,108.1c17.6,4,36.4-12.4,56.5-49.2C1508.1,720.4,1520.5,685.2,1528.5,646.8z"/>
</g>
<g transform="matrix(7.4705,0,0,7.4705,-3607.69,-3511.85)">
<path class="st2" d="M575.1,501.9c-1.4,3.8-6,5.8-13.8,5.8c-6,0-10.3-1.3-12.8-4c-0.9-1-1.3-2.2-1.3-3.6c0-0.5,0.1-1.3,0.2-2.2
c0.1-0.9,0.2-1.7,0.2-2.2c0-1.4-0.5-2.6-1.6-3.5c-0.8-3.2-2.5-4.7-5.3-4.4c-2.4,0.1-5.1,3.7-7.9,10.8
c-2.9,7.1-4.5,14.4-4.9,21.8c3.4-4.6,8-6.9,13.8-6.9c9.1,0,16.6,3.6,22.6,10.7c5.5,6.6,8.5,14.6,9.1,24
c0.4,7.7-2,16.9-7.4,27.5c-6.1,12.4-13.2,18.6-21.2,18.6c-12.6,0-22.7-4.7-30.1-14.2c-7.5-9.5-11.3-22.5-11.4-39
c-0.1-20.4,3.2-37.4,10-50.9c6.8-13.5,15.3-20.3,25.6-20.3c11.5,0,21,6.1,28.3,18.3c5.8,3.8,8.7,7.5,8.7,11
C575.6,500.1,575.4,501,575.1,501.9z M549.4,555.2c0.8-3.2,1.2-6.5,1.2-9.9c0-9-2.4-14-7.1-15c-2.4-0.4-5,1.8-7.7,6.8
c-2.3,4.4-4,9.3-5.1,14.7c-0.5,2.6-0.8,5.6-0.8,8.9c0,8.7,1.6,13.4,4.8,14.2c3,0.8,6-1.2,9.1-5.8
C546.4,565,548.3,560.4,549.4,555.2z"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLButtonAttributes } from "svelte/elements";
interface $$Props extends HTMLButtonAttributes {}
</script>
<button
{...$$restProps}
on:click
class="px-2 py-2 text-gray-100 border border-gray-300 bg-teal-500 hover:bg-teal-800 disabled:bg-gray-700 bg-opacity-30 transition-colors"
>
<slot />
</button>

View File

@ -0,0 +1,22 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { HTMLTextareaAttributes } from 'svelte/elements';
export let contents = '';
let ref: HTMLTextAreaElement;
interface $$Props extends HTMLTextareaAttributes {
contents: string;
}
onMount(() => {
ref.focus();
});
</script>
<textarea
class="w-full h-full bg-transparent border-none resize-none outline-none"
bind:value={contents}
bind:this={ref}
{...$$restProps}
/>

View File

@ -0,0 +1,55 @@
<script lang="ts">
import { SlideToggle } from '@skeletonlabs/skeleton';
/** Exposes parent props to this component. */
export let parent: any;
import { getModalStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
// Form Data
const formData = {
encrypt: false,
password: undefined,
burnAfterReading: false,
};
// Custom submit function to pass the response and close the modal.
function onFormSubmit(): void {
if ($modalStore[0].response) $modalStore[0].response(formData);
modalStore.close();
}
// Base Classes
const cBase = 'card p-4 w-modal shadow-xl space-y-4';
const cHeader = 'text-2xl font-bold';
const cForm = 'border border-surface-500 p-4 space-y-4 rounded-container-token';
</script>
{#if $modalStore[0]}
<div class="modal-example-form {cBase}">
<header class={cHeader}>Options for Saving</header>
<article>Some extra options for saving. Encrypting your paste will make it require a password for anyone to open it. Burn after reading makes your paste viewable only once.</article>
<form class="modal-form {cForm}">
<div class="grid grid-cols-3">
<div class="col-span-1">Encrypt</div>
<SlideToggle name="slide" bind:checked={formData.encrypt} />
</div>
{#if formData.encrypt}
<div class="grid grid-cols-3">
<div class="col-span-1">Password</div>
<input class="input col-span-2 px-2 py-1" type="password" bind:value={formData.password} placeholder="Encryption password..." />
</div>
{/if}
<div class="grid grid-cols-3">
<div class="col-span-1">Burn After Reading</div>
<SlideToggle name="slide" bind:checked={formData.burnAfterReading} />
</div>
</form>
<!-- prettier-ignore -->
<footer class="modal-footer {parent.regionFooter}">
<button class="btn {parent.buttonNeutral}" on:click={parent.onClose}>{parent.buttonTextCancel}</button>
<button class="btn {parent.buttonPositive}" on:click={onFormSubmit}>Save Paste</button>
</footer>
</div>
{/if}

View File

@ -0,0 +1,69 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { Copy, DeviceFloppy, TextPlus, CaretDown } from 'svelte-tabler';
import { getModalStore, type ModalComponent, type ModalSettings } from '@skeletonlabs/skeleton';
import logo from '$lib/assets/logo.svg';
import Button from './Button.svelte';
import MoreOptionsForm from './MoreOptionsForm.svelte';
const modalStore = getModalStore();
const dispatch = createEventDispatcher();
let extraOptions = {
encrypt: false,
password: undefined as string | undefined,
burnAfterReading: false,
};
export let disableSave: boolean = false;
export let disableCopy: boolean = false;
export let disableMoreOptions: boolean = false;
const modalComponent: ModalComponent = {
ref: MoreOptionsForm,
}
const modalSettings: ModalSettings = {
type: 'component',
component: modalComponent,
response: (data: typeof extraOptions) => {
extraOptions = data;
onSave();
},
}
const onSave = () => dispatch('save', extraOptions);
const onNew = () => dispatch('new');
const onCopy = () => dispatch('copy');
</script>
<div class="flex flex-row max-w-full md:min-w-[595px] bg-slate-800">
<a
href="/about"
class="flex flex-col items-center justify-center py-2 px-8 bg-black bg-opacity-50"
>
<img class="w-8" src={logo} alt="Paste69 Logo" />
</a>
<div class="flex flex-col items-center justify-center pt-4 pb-2 px-12 w-full">
<div class="flex flex-row justify-between gap-2 w-full">
<Button title="Save" disabled={disableSave} on:click={onSave}>
<DeviceFloppy />
</Button>
<Button title="New" on:click={onNew}>
<TextPlus />
</Button>
<Button title="Copy" disabled={disableCopy} on:click={onCopy}>
<Copy />
</Button>
</div>
<!-- More options button with horizontal line through the word -->
<button
disabled={disableMoreOptions}
on:click={() => modalStore.trigger(modalSettings)}
class="w-full text-center text-sm text-gray-300 disabled:text-gray-400 border-b border-gray-500 leading-[0.1em] mt-4 mb-2"
>
<span class="bg-slate-800 px-2">More Options</span>
</button>
</div>
</div>

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

32
src/routes/+error.svelte Normal file
View File

@ -0,0 +1,32 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import ToolBox from '$lib/components/ToolBox.svelte';
import { ChevronRight } from 'svelte-tabler';
console.log($page);
</script>
<svelte:head>
<title>Paste69 - {$page.status} error</title>
</svelte:head>
<div class="absolute top-[23px] left-[5px]">
<ChevronRight />
</div>
<div class="flex flex-row items-center justify-center h-screen">
<div class="text-xl text-center">
<h1 class="h1 mb-2 p-0">{$page.status}</h1>
<p class="mb-2">{$page.error?.message}</p>
</div>
</div>
<div class="fixed bottom-0 right-0 w-full md:w-auto">
<ToolBox
disableSave={true}
disableCopy={true}
disableMoreOptions={true}
on:new={() => goto('/')}
/>
</div>

70
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,70 @@
<script>
import '../app.postcss';
import { Modal, Toast, initializeStores } from '@skeletonlabs/skeleton';
import {
PUBLIC_GOOGLE_ANALYTICS_SITE_ID,
PUBLIC_ACKEE_DOMAIN_ID,
PUBLIC_ACKEE_URL,
PUBLIC_MATOMO_SITE_ID,
PUBLIC_MATOMO_URL,
PUBLIC_PLAUSIBLE_DOMAIN,
PUBLIC_PLAUSIBLE_URL
} from '$env/static/public';
initializeStores();
</script>
<svelte:head>
{#if PUBLIC_GOOGLE_ANALYTICS_SITE_ID}
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${PUBLIC_GOOGLE_ANALYTICS_SITE_ID}`}
></script>
<script>
{@html `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${PUBLIC_GOOGLE_ANALYTICS_SITE_ID}');`}
</script>
{/if}
{#if PUBLIC_PLAUSIBLE_DOMAIN}
<script
defer
data-domain={PUBLIC_PLAUSIBLE_DOMAIN}
src={`${PUBLIC_PLAUSIBLE_URL}/js/script.js`}
></script>
{/if}
{#if PUBLIC_ACKEE_URL && PUBLIC_ACKEE_DOMAIN_ID}
<script
async
src={`${PUBLIC_ACKEE_URL}/tracker.js`}
data-ackee-server={PUBLIC_ACKEE_URL}
data-ackee-domain-id={PUBLIC_ACKEE_DOMAIN_ID}
></script>
{/if}
{#if PUBLIC_MATOMO_URL && PUBLIC_MATOMO_SITE_ID}
<script defer src={`${PUBLIC_MATOMO_URL}/matomo.js`}></script>
<script>
{@html `
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="${PUBLIC_MATOMO_URL}/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '${PUBLIC_MATOMO_SITE_ID}']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();`}
</script>
{/if}
</svelte:head>
<Modal />
<Toast />
<slot />

View File

@ -0,0 +1,15 @@
import { pastes } from "$db/index";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ url }) => {
// Check if the `copyFrom` query parameter is present
const copyFrom = url.searchParams.get("copy-from");
if (copyFrom) {
// If it is, we need to fetch the paste from the database
// and return it to the client.
const paste = await pastes.findOne({ id: copyFrom });
return {
paste: structuredClone(paste),
}
}
}

69
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,69 @@
<script lang="ts">
import type { PageData } from './$types';
import { ChevronRight } from 'svelte-tabler';
import Editor from '$lib/components/Editor.svelte';
import ToolBox from '$lib/components/ToolBox.svelte';
import { goto } from '$app/navigation';
import { getToastStore } from '@skeletonlabs/skeleton';
import { paste } from '../stores/app';
const toastStore = getToastStore();
export let data: PageData;
let contents = data.paste?.contents ?? '';
const newPaste = () => {
contents = '';
goto('/');
};
const savePaste = async (event: any) => {
const res = await fetch('/api/pastes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
contents,
...event.detail,
})
});
if (res.ok) {
const { id, highlight, burnAfterReading } = await res.json();
if (burnAfterReading) {
goto(`/${id}/created`);
} else {
goto(`/${id}.${highlight}`);
}
} else {
toastStore.trigger({
message: 'Failed to save paste.',
background: 'variant-filled-error',
autohide: false,
})
}
};
</script>
<svelte:head>
<title>Paste69 - Paste, if you dare</title>
</svelte:head>
<div class="absolute top-[23px] left-[5px]">
<ChevronRight />
</div>
<Editor
placeholder="Paste something, type something, do something."
bind:contents={contents}
/>
<div class="fixed bottom-0 right-0 w-full md:w-auto">
<ToolBox
disableSave={!contents}
disableCopy={true}
on:save={savePaste}
on:new={newPaste}
/>
</div>

View File

@ -0,0 +1,29 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { pastes } from '$db/index';
export const load: PageLoad = async ({ params }) => {
const [id, ext] = params.slug.split('.');
// Fetch the paste
const paste = await pastes.findOne({ id });
if (!paste) {
throw error(404, 'Paste not found');
}
// Build the response object
const response = {
id: paste.id,
contents: paste.contents,
encrypted: paste.encrypted,
highlight: ext || paste.highlight,
burnAfterReading: paste.burnAfterReading,
}
// If the paste is set as burnAfterReading, delete it
if (paste.burnAfterReading) {
await pastes.deleteOne({ id });
}
return response;
};

View File

@ -0,0 +1,108 @@
<script lang="ts">
import type { PageData } from './$types';
import ToolBox from '$lib/components/ToolBox.svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { highlight } from '$utils/hljs';
import { markdown } from '$utils/markdown';
import { getModalStore, getToastStore, type ModalSettings } from '@skeletonlabs/skeleton';
import { sleep } from '$utils/index';
let codeRef: HTMLPreElement;
const modalStore = getModalStore();
const toastStore = getToastStore();
export let data: PageData;
let decryptedData: string | undefined = undefined;
// Select all the text in the code block when it is double clicked.
const selectAll = () => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(codeRef);
selection?.removeAllRanges();
selection?.addRange(range);
};
// If the highlight is set to 'md' or 'markdown', and the
// query contains either 'render' or 'render=true'
// then we will render the markdown.
const renderMarkdown =
(data.highlight === 'md' ||
data.highlight === 'markdown') &&
(($page.url.searchParams.has('render') && !$page.url.searchParams.get('render')) ||
$page.url.searchParams.get('render') === 'true');
if (data.encrypted && !decryptedData) {
let modalOptions: ModalSettings;
const onResponse = async (password?: string) => {
if (!password || password.length === 0) {
await sleep(500);
return modalStore.trigger(modalOptions)
};
// Use the /api/pastes/[id]/decrypt endpoint to decrypt the paste
// and set the decryptedData variable to the result.
const result = await fetch(`/api/pastes/${data.id}/decrypt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
});
if (result.ok) {
const { contents: decrypted } = await result.json();
decryptedData = decrypted;
} else {
modalStore.trigger(modalOptions);
await sleep(500);
const id = toastStore.trigger({
message: 'Failed to decrypt paste.',
background: 'variant-filled-error',
});
}
};
modalOptions = {
type: 'prompt',
title: 'Encrypted Paste',
body: 'Enter the password to decrypt the paste.',
valueAttr: { type: 'password', required: 'true', placeholder: 'Password', class: 'modal-prompt-input input px-4 py-2' },
response: onResponse,
};
modalStore.trigger(modalOptions);
}
$: contents = renderMarkdown ? markdown(decryptedData ?? data.contents) : highlight(decryptedData ?? data.contents, data.highlight);
$: lineCount = contents.split('\n').length;
</script>
<svelte:head>
<title>Paste69 - Paste {data.id}</title>
</svelte:head>
{#if renderMarkdown}
<!-- prettier-ignore -->
<div class="markdown text-xl max-w-[90ch] pb-24">{@html contents}</div>
{:else}
<div class="absolute text-left text-gray-500 top-0 left-0 bottom-0 w-[45px] pt-[20px] pl-[5px]">
{#each Array.from({ length: lineCount }) as _, i}
<div>{i + 1}</div>
{/each}
</div>
<pre class="pb-24" bind:this={codeRef} on:dblclick={() => selectAll()} ><code>{@html contents}</code></pre>
{/if}
<div class="fixed bottom-0 right-0 w-full md:w-auto">
<ToolBox
disableSave={true}
disableMoreOptions={true}
disableCopy={data.encrypted}
on:new={() => goto('/')}
on:copy={() => goto(`/?copy-from=${data.id}`)}
/>
</div>

View File

@ -0,0 +1,18 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
import { pastes } from '$db/index';
import { SITE_URL } from '$env/static/private';
export const load: PageLoad = async ({ params }) => {
const paste = await pastes.findOne({ id: params.slug });
if (!paste) {
throw error(404, 'Paste not found');
}
const pasteUrl = `${SITE_URL}/${paste.id}.${paste.highlight}`;
return {
pasteUrl,
};
};

View File

@ -0,0 +1,31 @@
<script lang="ts">
import type { PageData } from './$types';
import { goto } from "$app/navigation";
import ToolBox from "$lib/components/ToolBox.svelte";
import { ChevronRight } from "svelte-tabler";
export let data: PageData;
</script>
<svelte:head>
<title>Paste69 - Burnable paste created</title>
</svelte:head>
<div class="absolute top-[18px] left-[5px]">
<ChevronRight />
</div>
<div class="fixed bottom-0 right-0">
<ToolBox
disableSave={true}
disableCopy={true}
disableMoreOptions={true}
on:new={() => goto('/')}
/>
</div>
<div class="flex flex-col w-full h-full justify-center items-center">
<div class="text-center text-xl">
<h1 class="h1 mb-6 text-center">Your burnable paste has been created.</h1>
<p class="mb-2">Share the following link with someone, and once they view it, it will be deleted forever.</p>
<a class="text-2xl underline" href={data.pasteUrl}>{data.pasteUrl}</a>
</div>
</div>

View File

@ -0,0 +1,118 @@
<script lang="ts">
import { Copy, DeviceFloppy, Moon, Sun, TextPlus } from 'svelte-tabler';
import { storeHighlightJs } from '@skeletonlabs/skeleton';
import { CodeBlock } from '@skeletonlabs/skeleton';
import ToolBox from '$lib/components/ToolBox.svelte';
import hljs from '$utils/hljs';
import { goto } from '$app/navigation';
storeHighlightJs.set(hljs);
</script>
<svelte:head>
<title>Paste69 - About</title>
</svelte:head>
<div class="fixed bottom-0 right-0 w-full md:w-auto">
<ToolBox disableSave={true} disableCopy={true} on:new={() => goto('/')} />
</div>
<div class="pl-8 max-w-[100ch] pb-24">
<h1 class="h1 mb-2">Paste69</h1>
<p class="mb-4 mt-4">
Paste69 is a pastebin service built with
<a class="underline hover:text-gray-300" href="https://kit.svelte.dev">SvelteKit</a>. It's a simple, fast, and easy to use pastebin
service based on HasteBin. Like HasteBin, it's also open source and can be found over on
<a class="underline hover:text-gray-300" href="https://github.com/watzon/paste69">GitHub</a>.
</p>
<p class="mb-4 mt-4">
Code highlighting is handled with the help of <a class="underline hover:text-gray-300" href="https://highlightjs.org/">highlight.js</a>.
So if you have any issues with language detection or missing languages, take it up with them.
</p>
<h2 id="usage" class="h2 mt-4 mb-2"><a class="underline hover:text-gray-300" href="#usage">Usage</a></h2>
<p class="mb-4 mt-4">
To create a paste, go <a class="underline hover:text-gray-300" href="/">home</a> or click the "New" button (<TextPlus
class="inline-block w-4 h-4"
/>) in the tool box in the bottom right corner of the page. Paste whatever text you want into
the editor, and click the "Save" button (<DeviceFloppy class="inline-block w-4 h-4" />) to
create the paste.
</p>
<p class="mb-4 mt-4">
To copy an existing paste, click the "Copy" button (<Copy class="inline-block w-4 h-4" />)
in the tool box in the bottom right corner of the page. This will start a new paste with the
contents of the existing paste.
</p>
<h2 id="cli-script" class="h2 mt-4 mb-2"><a class="underline hover:text-gray-300" href="#cli-script">CLI Script</a></h2>
<p class="mb-4 mt-4">
To make it easier to create pastes, a CLI script is available. The script can be found <a
href="/paste69.sh">here</a
>. To use the script:
</p>
<CodeBlock language="bash" code={`$ curl -O https://0x45.st/paste69.sh && chmod +x paste69.sh<br />
$ ./paste69.sh --help<br /><br />
Paste69 CLI script<br /><br />
Usage:<br />
paste69 &lt;file&gt; [options]<br />
cat &lt;file&gt; | paste69 [options]<br /><br />
Options:<br />
-h, --help Show this help text<br />
-r, --raw Return the raw JSON response<br />
-c, --copy Copy the paste URL to the clipboard<br />`}></CodeBlock>
<p class="mb-4 mt-4">To create a paste with the script, simply pipe the contents of a file to the script:</p>
<CodeBlock language="bash" code={`$ cat file.md | ./paste69.sh<br />
https://0x45.st/some-random-id.md`}></CodeBlock>
<h2 id="api" class="h2 mt-4 mb-2"><a class="underline hover:text-gray-300" href="#api">API</a></h2>
<p class="mb-4 mt-4">Paste69 has a simple API for creating and fetching pastes. The API is documented below.</p>
<h3 id="api-creating-a-paste" class="h3 mt-4 mb-2">
<a class="underline hover:text-gray-300" href="#api-creating-a-paste">Creating a Paste</a>
</h3>
<p class="mb-4 mt-4">
To create a paste, send a POST request to <code>/api/pastes</code>
{' '}
with the following JSON body:
</p>
<CodeBlock language="json" code={`{ "contents": "paste contents" }`}></CodeBlock>
<p class="mb-4 mt-4">If the paste was successfully created, the API will respond with the following JSON:</p>
<CodeBlock language="json" code={`{
"id": "paste id",
"contents": "paste contents",
"highlight": "txt",
"created_at": "2021-08-05T07:30:00.000Z",
}`}></CodeBlock>
<h3 id="api-fetching-a-paste" class="h3 mt-4 mb-2">
<a class="underline hover:text-gray-300" href="#api-fetching-a-paste">Fetching a Paste</a>
</h3>
<p class="mb-4 mt-4">
To fetch a paste, send a GET request to{' '}
<code>/api/pastes/:id</code>. If the paste exists, the API will respond with the following JSON:
</p>
<CodeBlock language="json" code={`{
"id": "paste id",
"contents": "paste contents",
"highlight": "txt",
"created_at": "2021-08-05T07:30:00.000Z",
}`}></CodeBlock>
</div>

View File

@ -0,0 +1,44 @@
import { detectLanguage } from "$utils/hljs";
import { encrypt as doEncrypt } from "$utils/crypto";
import { error, json, type RequestHandler } from "@sveltejs/kit";
import { generate } from 'random-words';
import { pastes } from "$db/index";
import { SITE_URL } from "$env/static/private";
export const POST: RequestHandler = async ({ request }) => {
const { contents, encrypt, password, burnAfterReading } = await request.json();
const id = generate({ exactly: 3, join: '-' });
if (!contents || contents.length === 0) {
throw error(400, 'No contents provided');
}
let pasteContents: string = contents;
const highlight = detectLanguage(contents) || 'txt';
if (encrypt && !password) {
throw error(400, 'No password provided for encryption.');
} else if (encrypt) {
pasteContents = await doEncrypt(contents, password);
}
const data = {
id,
url: `${SITE_URL}/${id}.${highlight}`,
highlight,
encrypted: !!encrypt,
contents: pasteContents,
burnAfterReading: !!burnAfterReading,
createdAt: new Date(),
};
const res = await pastes.insertOne(data);
if (!res.acknowledged) {
throw error(500, 'Failed to create paste');
}
return json(data, {
status: 201,
})
};

View File

@ -0,0 +1,31 @@
import { pastes } from "$db/index";
import { error, json, type RequestHandler } from "@sveltejs/kit";
// Fetch the paste with the given ID, returning it as a JSON object.
export const GET: RequestHandler = async ({ params }) => {
const { id } = params;
const paste = await pastes.findOne({ id });
if (!paste) {
throw error(404, 'Paste not found');
}
if (paste.encrypted) {
throw error(400, 'Paste is encrypted');
}
if (paste.burnAfterReading) {
await pastes.deleteOne({ id });
}
const data = {
id: paste.id,
highlight: paste.highlight,
contents: paste.contents,
burnAfterReading: paste.burnAfterReading,
createdAt: paste.createdAt,
}
return json(data);
};

View File

@ -0,0 +1,36 @@
import { error, json, type RequestHandler } from "@sveltejs/kit";
import { pastes } from "$db/index";
import { decrypt } from "$utils/crypto";
// Decrypt the paste with the given ID using the provided password.
export const POST: RequestHandler = async ({ params, request }) => {
const { id } = params;
const { password } = await request.json();
console.log(password);
const paste = await pastes.findOne({ id });
if (!paste) {
throw error(404, 'Paste not found');
}
if (!paste.encrypted) {
throw error(400, 'Paste is not encrypted');
}
if (!password) {
throw error(400, 'No password provided for decryption.');
}
try {
const contents = await decrypt(paste.contents, password);
return json({
...paste,
contents,
});
} catch {
throw error(400, 'Invalid password');
}
};

9
src/stores/app.ts Normal file
View File

@ -0,0 +1,9 @@
import { writable } from 'svelte/store';
import type PasteSchema from '$db/paste-schema';
export const paste = writable<PasteSchema>({
id: '',
contents: '',
highlight: '',
createdAt: new Date(),
});

89
src/utils/crypto.ts Normal file
View File

@ -0,0 +1,89 @@
import { subtle } from 'crypto';
// for large strings, use this from https://stackoverflow.com/a/49124600
const buff_to_base64 = (buff: Iterable<number>) =>
btoa(new Uint8Array(buff).reduce((data, byte) => data + String.fromCharCode(byte), ''));
const base64_to_buf = (b64: string) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const enc = new TextEncoder();
const dec = new TextDecoder();
export async function encrypt(data: string, password: string) {
const encryptedData = await encryptData(data, password);
return encryptedData;
}
export async function decrypt(data: string, password: string) {
const decryptedData = await decryptData(data, password);
if (!decryptedData) {
throw new Error('Invalid password');
}
return decryptedData;
}
const getPasswordKey = (password: string) =>
subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']);
const deriveKey = (passwordKey: CryptoKey, salt: Uint8Array, keyUsage: KeyUsage[]) =>
subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 250000,
hash: 'SHA-256'
},
passwordKey,
{ name: 'AES-GCM', length: 256 },
false,
keyUsage
);
async function encryptData(secretData: string, password: string) {
try {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const passwordKey = await getPasswordKey(password);
const aesKey = await deriveKey(passwordKey, salt, ['encrypt']);
const encryptedContent = await subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
aesKey,
enc.encode(secretData)
);
const encryptedContentArr = new Uint8Array(encryptedContent);
const buff = new Uint8Array(salt.byteLength + iv.byteLength + encryptedContentArr.byteLength);
buff.set(salt, 0);
buff.set(iv, salt.byteLength);
buff.set(encryptedContentArr, salt.byteLength + iv.byteLength);
const base64Buff = buff_to_base64(buff);
return base64Buff;
} catch (e) {
throw new Error('Invalid password');
}
}
async function decryptData(encryptedData: string, password: string) {
try {
const encryptedDataBuff = base64_to_buf(encryptedData);
const salt = encryptedDataBuff.slice(0, 16);
const iv = encryptedDataBuff.slice(16, 16 + 12);
const data = encryptedDataBuff.slice(16 + 12);
const passwordKey = await getPasswordKey(password);
const aesKey = await deriveKey(passwordKey, salt, ['decrypt']);
const decryptedContent = await subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
aesKey,
data
);
return dec.decode(decryptedContent);
} catch (e) {
throw new Error('Invalid password');
}
}

32
src/utils/hljs.ts Normal file
View File

@ -0,0 +1,32 @@
import hljs from 'highlight.js';
import { extensionMap, languages } from './languages';
// Theme
import 'highlight.js/styles/github-dark.css';
const autoLanguages = ['js', 'ts', 'css', 'html', 'json', 'md', 'php', 'py', 'rb', 'rs', 'shell'];
for (const [lang, exts] of Object.entries(extensionMap)) {
if (exts.length > 0) {
// Remove leading underscore, replace other underscores with dashes
const name = lang.replace(/^_/, '').replace(/_/g, '-');
// @ts-expect-error: this is valid
hljs.registerLanguage(name, languages[lang]!);
hljs.registerAliases(exts, { languageName: lang });
}
}
export const detectLanguage = (code: string) => {
return hljs.highlightAuto(code, autoLanguages).language;
};
export const highlight = (code: string, lang: string | undefined = undefined) => {
if (!lang) {
return hljs.highlightAuto(code, autoLanguages).value;
} else {
return hljs.highlight(code, { language: lang }).value;
}
};
// Re-export the `hljs` object
export default hljs;

2
src/utils/index.ts Normal file
View File

@ -0,0 +1,2 @@
// Async sleep
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

590
src/utils/languages.ts Normal file
View File

@ -0,0 +1,590 @@
import _1c from 'highlight.js/lib/languages/1c';
import abnf from 'highlight.js/lib/languages/abnf';
import accesslog from 'highlight.js/lib/languages/accesslog';
import actionscript from 'highlight.js/lib/languages/actionscript';
import ada from 'highlight.js/lib/languages/ada';
import angelscript from 'highlight.js/lib/languages/angelscript';
import apache from 'highlight.js/lib/languages/apache';
import applescript from 'highlight.js/lib/languages/applescript';
import arcade from 'highlight.js/lib/languages/arcade';
import arduino from 'highlight.js/lib/languages/arduino';
import armasm from 'highlight.js/lib/languages/armasm';
import asciidoc from 'highlight.js/lib/languages/asciidoc';
import aspectj from 'highlight.js/lib/languages/aspectj';
import autohotkey from 'highlight.js/lib/languages/autohotkey';
import autoit from 'highlight.js/lib/languages/autoit';
import avrasm from 'highlight.js/lib/languages/avrasm';
import awk from 'highlight.js/lib/languages/awk';
import axapta from 'highlight.js/lib/languages/axapta';
import bash from 'highlight.js/lib/languages/bash';
import basic from 'highlight.js/lib/languages/basic';
import bnf from 'highlight.js/lib/languages/bnf';
import brainfuck from 'highlight.js/lib/languages/brainfuck';
import _c from 'highlight.js/lib/languages/c';
import cal from 'highlight.js/lib/languages/cal';
import capnproto from 'highlight.js/lib/languages/capnproto';
import ceylon from 'highlight.js/lib/languages/ceylon';
import clean from 'highlight.js/lib/languages/clean';
import clojure_repl from 'highlight.js/lib/languages/clojure-repl';
import clojure from 'highlight.js/lib/languages/clojure';
import cmake from 'highlight.js/lib/languages/cmake';
import coffeescript from 'highlight.js/lib/languages/coffeescript';
import coq from 'highlight.js/lib/languages/coq';
import cos from 'highlight.js/lib/languages/cos';
import cpp from 'highlight.js/lib/languages/cpp';
import crmsh from 'highlight.js/lib/languages/crmsh';
import crystal from 'highlight.js/lib/languages/crystal';
import csharp from 'highlight.js/lib/languages/csharp';
import csp from 'highlight.js/lib/languages/csp';
import css from 'highlight.js/lib/languages/css';
import d from 'highlight.js/lib/languages/d';
import dart from 'highlight.js/lib/languages/dart';
import delphi from 'highlight.js/lib/languages/delphi';
import diff from 'highlight.js/lib/languages/diff';
import django from 'highlight.js/lib/languages/django';
import dns from 'highlight.js/lib/languages/dns';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
import dos from 'highlight.js/lib/languages/dos';
import dsconfig from 'highlight.js/lib/languages/dsconfig';
import dts from 'highlight.js/lib/languages/dts';
import dust from 'highlight.js/lib/languages/dust';
import ebnf from 'highlight.js/lib/languages/ebnf';
import elixir from 'highlight.js/lib/languages/elixir';
import elm from 'highlight.js/lib/languages/elm';
import erb from 'highlight.js/lib/languages/erb';
import erlang_repl from 'highlight.js/lib/languages/erlang-repl';
import erlang from 'highlight.js/lib/languages/erlang';
import excel from 'highlight.js/lib/languages/excel';
import fix from 'highlight.js/lib/languages/fix';
import flix from 'highlight.js/lib/languages/flix';
import fortran from 'highlight.js/lib/languages/fortran';
import fsharp from 'highlight.js/lib/languages/fsharp';
import gams from 'highlight.js/lib/languages/gams';
import gauss from 'highlight.js/lib/languages/gauss';
import gcode from 'highlight.js/lib/languages/gcode';
import gherkin from 'highlight.js/lib/languages/gherkin';
import glsl from 'highlight.js/lib/languages/glsl';
import gml from 'highlight.js/lib/languages/gml';
import go from 'highlight.js/lib/languages/go';
import golo from 'highlight.js/lib/languages/golo';
import gradle from 'highlight.js/lib/languages/gradle';
import graphql from 'highlight.js/lib/languages/graphql';
import groovy from 'highlight.js/lib/languages/groovy';
import haml from 'highlight.js/lib/languages/haml';
import handlebars from 'highlight.js/lib/languages/handlebars';
import haskell from 'highlight.js/lib/languages/haskell';
import haxe from 'highlight.js/lib/languages/haxe';
import hsp from 'highlight.js/lib/languages/hsp';
import http from 'highlight.js/lib/languages/http';
import hy from 'highlight.js/lib/languages/hy';
import inform7 from 'highlight.js/lib/languages/inform7';
import ini from 'highlight.js/lib/languages/ini';
import irpf90 from 'highlight.js/lib/languages/irpf90';
import isbl from 'highlight.js/lib/languages/isbl';
import java from 'highlight.js/lib/languages/java';
import javascript from 'highlight.js/lib/languages/javascript';
import json from 'highlight.js/lib/languages/json';
import julia_repl from 'highlight.js/lib/languages/julia-repl';
import julia from 'highlight.js/lib/languages/julia';
import kotlin from 'highlight.js/lib/languages/kotlin';
import lasso from 'highlight.js/lib/languages/lasso';
import latex from 'highlight.js/lib/languages/latex';
import ldif from 'highlight.js/lib/languages/ldif';
import leaf from 'highlight.js/lib/languages/leaf';
import less from 'highlight.js/lib/languages/less';
import lisp from 'highlight.js/lib/languages/lisp';
import livecodeserver from 'highlight.js/lib/languages/livecodeserver';
import livescript from 'highlight.js/lib/languages/livescript';
import llvm from 'highlight.js/lib/languages/llvm';
import lsl from 'highlight.js/lib/languages/lsl';
import lua from 'highlight.js/lib/languages/lua';
import makefile from 'highlight.js/lib/languages/makefile';
import markdown from 'highlight.js/lib/languages/markdown';
import mathematica from 'highlight.js/lib/languages/mathematica';
import matlab from 'highlight.js/lib/languages/matlab';
import maxima from 'highlight.js/lib/languages/maxima';
import mel from 'highlight.js/lib/languages/mel';
import mercury from 'highlight.js/lib/languages/mercury';
import mipsasm from 'highlight.js/lib/languages/mipsasm';
import mizar from 'highlight.js/lib/languages/mizar';
import mojolicious from 'highlight.js/lib/languages/mojolicious';
import monkey from 'highlight.js/lib/languages/monkey';
import moonscript from 'highlight.js/lib/languages/moonscript';
import n1ql from 'highlight.js/lib/languages/n1ql';
import nestedtext from 'highlight.js/lib/languages/nestedtext';
import nginx from 'highlight.js/lib/languages/nginx';
import nim from 'highlight.js/lib/languages/nim';
import nix from 'highlight.js/lib/languages/nix';
import node_repl from 'highlight.js/lib/languages/node-repl';
import nsis from 'highlight.js/lib/languages/nsis';
import objectivec from 'highlight.js/lib/languages/objectivec';
import ocaml from 'highlight.js/lib/languages/ocaml';
import openscad from 'highlight.js/lib/languages/openscad';
import oxygene from 'highlight.js/lib/languages/oxygene';
import parser3 from 'highlight.js/lib/languages/parser3';
import perl from 'highlight.js/lib/languages/perl';
import pf from 'highlight.js/lib/languages/pf';
import pgsql from 'highlight.js/lib/languages/pgsql';
import php from 'highlight.js/lib/languages/php';
import plaintext from 'highlight.js/lib/languages/plaintext';
import pony from 'highlight.js/lib/languages/pony';
import powershell from 'highlight.js/lib/languages/powershell';
import processing from 'highlight.js/lib/languages/processing';
import profile from 'highlight.js/lib/languages/profile';
import prolog from 'highlight.js/lib/languages/prolog';
import properties from 'highlight.js/lib/languages/properties';
import protobuf from 'highlight.js/lib/languages/protobuf';
import puppet from 'highlight.js/lib/languages/puppet';
import purebasic from 'highlight.js/lib/languages/purebasic';
import python_repl from 'highlight.js/lib/languages/python-repl';
import python from 'highlight.js/lib/languages/python';
import q from 'highlight.js/lib/languages/q';
import qml from 'highlight.js/lib/languages/qml';
import r from 'highlight.js/lib/languages/r';
import reasonml from 'highlight.js/lib/languages/reasonml';
import rib from 'highlight.js/lib/languages/rib';
import roboconf from 'highlight.js/lib/languages/roboconf';
import routeros from 'highlight.js/lib/languages/routeros';
import rsl from 'highlight.js/lib/languages/rsl';
import ruby from 'highlight.js/lib/languages/ruby';
import ruleslanguage from 'highlight.js/lib/languages/ruleslanguage';
import rust from 'highlight.js/lib/languages/rust';
import sas from 'highlight.js/lib/languages/sas';
import scala from 'highlight.js/lib/languages/scala';
import scheme from 'highlight.js/lib/languages/scheme';
import scilab from 'highlight.js/lib/languages/scilab';
import scss from 'highlight.js/lib/languages/scss';
import shell from 'highlight.js/lib/languages/shell';
import smali from 'highlight.js/lib/languages/smali';
import smalltalk from 'highlight.js/lib/languages/smalltalk';
import sml from 'highlight.js/lib/languages/sml';
import sqf from 'highlight.js/lib/languages/sqf';
import sql from 'highlight.js/lib/languages/sql';
import stan from 'highlight.js/lib/languages/stan';
import stata from 'highlight.js/lib/languages/stata';
import step21 from 'highlight.js/lib/languages/step21';
import stylus from 'highlight.js/lib/languages/stylus';
import subunit from 'highlight.js/lib/languages/subunit';
import swift from 'highlight.js/lib/languages/swift';
import taggerscript from 'highlight.js/lib/languages/taggerscript';
import tap from 'highlight.js/lib/languages/tap';
import tcl from 'highlight.js/lib/languages/tcl';
import thrift from 'highlight.js/lib/languages/thrift';
import tp from 'highlight.js/lib/languages/tp';
import twig from 'highlight.js/lib/languages/twig';
import typescript from 'highlight.js/lib/languages/typescript';
import vala from 'highlight.js/lib/languages/vala';
import vbnet from 'highlight.js/lib/languages/vbnet';
import vbscript from 'highlight.js/lib/languages/vbscript';
import verilog from 'highlight.js/lib/languages/verilog';
import vhdl from 'highlight.js/lib/languages/vhdl';
import vim from 'highlight.js/lib/languages/vim';
import wasm from 'highlight.js/lib/languages/wasm';
import wren from 'highlight.js/lib/languages/wren';
import x86asm from 'highlight.js/lib/languages/x86asm';
import xl from 'highlight.js/lib/languages/xl';
import xml from 'highlight.js/lib/languages/xml';
import xquery from 'highlight.js/lib/languages/xquery';
import yaml from 'highlight.js/lib/languages/yaml';
import zephir from 'highlight.js/lib/languages/zephir';
export const languages = {
'1c': _1c,
abnf,
accesslog,
actionscript,
ada,
angelscript,
apache,
applescript,
arcade,
arduino,
armasm,
asciidoc,
aspectj,
autohotkey,
autoit,
avrasm,
awk,
axapta,
bash,
basic,
bnf,
brainfuck,
c: _c,
cal,
capnproto,
ceylon,
clean,
clojure_repl,
clojure,
cmake,
coffeescript,
coq,
cos,
cpp,
crmsh,
crystal,
csharp,
csp,
css,
d,
dart,
delphi,
diff,
django,
dns,
dockerfile,
dos,
dsconfig,
dts,
dust,
ebnf,
elixir,
elm,
erb,
erlang_repl,
erlang,
excel,
fix,
flix,
fortran,
fsharp,
gams,
gauss,
gcode,
gherkin,
glsl,
gml,
go,
golo,
gradle,
graphql,
groovy,
haml,
handlebars,
haskell,
haxe,
hsp,
http,
hy,
inform7,
ini,
irpf90,
isbl,
java,
javascript,
json,
julia_repl,
julia,
kotlin,
lasso,
latex,
ldif,
leaf,
less,
lisp,
livecodeserver,
livescript,
llvm,
lsl,
lua,
makefile,
markdown,
mathematica,
matlab,
maxima,
mel,
mercury,
mipsasm,
mizar,
mojolicious,
monkey,
moonscript,
n1ql,
nestedtext,
nginx,
nim,
nix,
node_repl,
nsis,
objectivec,
ocaml,
openscad,
oxygene,
parser3,
perl,
pf,
pgsql,
php,
plaintext,
pony,
powershell,
processing,
profile,
prolog,
properties,
protobuf,
puppet,
purebasic,
python_repl,
python,
q,
qml,
r,
reasonml,
rib,
roboconf,
routeros,
rsl,
ruby,
ruleslanguage,
rust,
sas,
scala,
scheme,
scilab,
scss,
shell,
smali,
smalltalk,
sml,
sqf,
sql,
stan,
stata,
step21,
stylus,
subunit,
swift,
taggerscript,
tap,
tcl,
thrift,
tp,
twig,
typescript,
vala,
vbnet,
vbscript,
verilog,
vhdl,
vim,
wasm,
wren,
x86asm,
xl,
xml,
xquery,
yaml,
zephir
};
export const extensionMap: Record<string, string[]> = {
'1c': [],
abnf: [],
accesslog: [],
actionscript: ['as'],
ada: ['ada', 'adb', 'ads'],
angelscript: ['angelscript'],
apache: ['apache'],
applescript: ['app'],
arcade: ['arcade'],
arduino: ['ino'],
armasm: ['s'],
asciidoc: ['adoc'],
aspectj: ['aj'],
autohotkey: ['ahk'],
autoit: ['au3'],
avrasm: ['s'],
awk: ['awk'],
axapta: ['ax'],
bash: ['sh', 'bash'],
basic: ['bas'],
bnf: ['bnf'],
brainfuck: ['b', 'bf'],
c: ['c'],
cal: ['cal'],
capnproto: ['capnp'],
ceylon: ['ceylon'],
clean: ['icl', 'dcl'],
'clojure-repl': [],
clojure: ['clj', 'cljc', 'cljx', 'cljs'],
cmake: ['CMakeLists.txt', 'cmake'],
coffeescript: ['coffee'],
coq: ['v'],
cos: ['cls', 'int'],
cpp: ['cpp', 'cc', 'h', 'C', 'H', 'hpp'],
crmsh: ['crm'],
crystal: ['cr'],
csharp: ['cs'],
csp: ['csp'],
css: ['css'],
d: ['d'],
dart: ['dart'],
delphi: ['dpr', 'dpk', 'pas'],
diff: ['diff', 'patch'],
django: ['djhtml'],
dns: ['zone', 'bind'],
dockerfile: ['Dockerfile'],
dos: ['bat', 'cmd'],
dsconfig: ['dsconfig'],
dts: ['dts'],
dust: ['dust'],
ebnf: ['ebnf'],
elixir: ['ex', 'exs'],
elm: ['elm'],
erb: ['erb'],
'erlang-repl': [],
erlang: ['erl', 'hrl'],
excel: ['xls', 'xlsx'],
fix: ['fix'],
flix: ['flix'],
fortran: ['f', 'for', 'f90', 'f95'],
fsharp: ['fs', 'fsi'],
gams: ['gms'],
gauss: ['gss', 'gms'],
gcode: ['gcode'],
gherkin: ['feature'],
glsl: ['vert', 'frag', 'geom'],
gml: ['gml'],
go: ['go'],
golo: ['golo'],
gradle: ['gradle'],
graphql: ['graphql', 'gql'],
groovy: ['groovy'],
haml: ['haml'],
handlebars: ['hbs'],
haskell: ['hs'],
haxe: ['hx'],
hsp: ['hsp'],
http: ['http'],
hy: ['hy'],
inform7: ['ni', 'n'],
ini: ['ini'],
irpf90: ['f', 'f90'],
isbl: ['isbl'],
java: ['java'],
javascript: ['js'],
json: ['json', 'json5'],
'julia-repl': [],
julia: ['jl'],
kotlin: ['kt', 'kts', 'ktm'],
lasso: ['lasso', 'lassoapp'],
latex: ['tex'],
ldif: ['ldif'],
leaf: ['leaf'],
less: ['less'],
lisp: ['lisp'],
livecodeserver: ['lc', 'irev'],
livescript: ['ls'],
llvm: ['ll'],
lsl: ['lsl'],
lua: ['lua'],
makefile: ['Makefile'],
markdown: ['md', 'markdown'],
mathematica: ['nb'],
matlab: ['m'],
maxima: ['mac', 'mc'],
mel: ['mel'],
mercury: ['m', 'mh', 'pro'],
mipsasm: ['s'],
mizar: ['miz'],
mojolicious: ['ep'],
monkey: ['monkey'],
moonscript: ['moon'],
n1ql: ['n1ql'],
nestedtext: ['nt'],
nginx: ['nginx.conf'],
nim: ['nim'],
nix: ['nix'],
'node-repl': [],
nsis: ['nsi', 'nsh'],
objectivec: ['m', 'mm'],
ocaml: ['ml', 'mli'],
openscad: ['scad'],
oxygene: ['oxygene'],
parser3: ['parser3'],
perl: ['pl', 'pm'],
pf: ['pf'],
pgsql: ['pgsql'],
php: ['php', 'php5', 'php7', 'php8'],
plaintext: ['txt'],
pony: ['pony'],
powershell: ['ps1', 'psm1'],
processing: ['pde'],
profile: [],
prolog: ['pl', 'pro', 'P'],
properties: ['properties'],
protobuf: ['proto'],
puppet: ['pp'],
purebasic: ['pb', 'pbi'],
'python-repl': [],
python: ['py'],
q: ['q'],
qml: ['qml'],
r: ['R'],
reasonml: ['re', 'rei'],
rib: ['rib'],
roboconf: ['rbcf'],
routeros: ['rsc'],
rsl: ['rsl'],
ruby: ['rb'],
ruleslanguage: ['ruleslanguage'],
rust: ['rs'],
sas: ['sas'],
scala: ['scala'],
scheme: ['scm'],
scilab: ['sci'],
scss: ['scss'],
shell: ['sh', 'bash', 'dash', 'ksh', 'csh', 'tcsh', 'zsh', 'fish'],
smali: ['smali'],
smalltalk: ['st'],
sml: ['sml', 'sig', 'fun'],
sqf: ['sqf'],
sql: ['sql'],
stan: ['stan'],
stata: ['do', 'ado'],
step21: ['stp'],
stylus: ['styl'],
subunit: ['subunit'],
swift: ['swift'],
taggerscript: ['taggerscript'],
tap: ['tap'],
tcl: ['tcl'],
thrift: ['thrift'],
tp: ['tp'],
twig: ['twig'],
typescript: ['ts'],
vala: ['vala'],
vbnet: ['vb'],
vbscript: ['vbs'],
verilog: ['v'],
vhdl: ['vhdl'],
vim: ['vim'],
wasm: [],
wren: ['wren'],
x86asm: ['s'],
xl: ['xl', 'ilst'],
xml: ['xml', 'svg', 'dtd'],
xquery: ['xy', 'xquery'],
yaml: ['yaml', 'yml'],
zephir: ['zep']
};
// Take a language (full name or extension) as input, and
// return the name of the language as it should
// be displayed to the user.
export function getLanguageName(lang: string) {
if (lang in languages) {
return lang;
}
for (const [name, exts] of Object.entries(extensionMap)) {
if (exts.includes(lang)) {
return name;
}
}
return '';
}

50
src/utils/markdown.ts Normal file
View File

@ -0,0 +1,50 @@
import { CodeBlock } from '@skeletonlabs/skeleton';
import { Marked, Renderer } from '@ts-stack/markdown';
import { highlight } from './hljs';
import { getLanguageName } from './languages';
class PasteRenderer extends Renderer {
override heading(text: string, level: number) {
const margin = level === 1 ? 8 : 2;
return `<h${level} class="h${level} mb-${margin} font-bold">${text}</h${level}>`;
}
override paragraph(text: string): string {
return `<p class="mt-0 mb-5">${text}</p>`;
}
override link(href: string, title: string, text: string): string {
return `<a href="${href}" title="${title}" target="_blank" rel="noopener noreferrer" class="underline">${text}</a>`;
}
override list(body: string, ordered?: boolean | undefined): string {
const tag = ordered ? 'ol' : 'ul';
return `<${tag} class="list-disc mt-5 mb-5 pl-7">${body}</${tag}>`;
}
override code(code: string, lang?: string | undefined): string {
const highlighted = highlight(code, lang);
const languageName = lang ? getLanguageName(lang) : '';
return `<div class="codeblock overflow-hidden shadow bg-neutral-900/90 text-sm text-white rounded-container-token" data-testid="codeblock">
<header class="codeblock-header text-xs text-white/50 uppercase flex justify-between items-center p-2 pl-4">
<span class="codeblock-language">${languageName}</span>
</header>
<pre class="codeblock-pre whitespace-pre-wrap break-all p-4 pt-1"><code class="codeblock-code language-bash lineNumbers">${highlighted}</code></pre>
</div>`
}
}
Marked.setOptions({
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false,
renderer: new PasteRenderer(),
});
export function markdown(text: string) {
return Marked.parse(text);
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

23
svelte.config.js Normal file
View File

@ -0,0 +1,23 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: [vitePreprocess({})],
kit: {
// 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.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
alias: {
'$db/*': './src/db/*',
'$utils/*': './src/utils/*',
}
}
};
export default config;

31
tailwind.config.ts Normal file
View File

@ -0,0 +1,31 @@
import { join } from 'path';
import type { Config } from 'tailwindcss';
import { skeleton } from '@skeletonlabs/tw-plugin';
const config = {
darkMode: 'class',
content: [
'./src/**/*.{html,js,svelte,ts}',
join(require.resolve(
'@skeletonlabs/skeleton'),
'../**/*.{html,js,svelte,ts}'
)
],
theme: {
extend: {},
},
plugins: [
skeleton({
themes: {
preset: ['skeleton'],
},
}),
],
safelist: [
'mb-8',
]
} satisfies Config;
export default config;

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6
vite.config.ts Normal file
View File

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