Compare commits

...

3 Commits

Author SHA1 Message Date
b620536eb8 Removed LZMA compression 2023-11-24 19:37:08 +01:00
ad4b21daf1 Async import of brotli 2023-11-24 19:00:49 +01:00
0f9faa45f0 Preview & editor 2023-11-24 18:15:30 +01:00
41 changed files with 274 additions and 233 deletions

View File

@ -2,14 +2,14 @@
> A no-database, no-backend pastebin-like service. All the data is stored in the url.
This is a rewrite of https://nopaste.boris.sh/, which was a rewrite of https://topaz.github.io/paste/. The biggest improvement of this version is that it uses brotli compression, while staying compatible with the original lzma compressed urls.
This is a rewrite of https://nopaste.boris.sh/, which is a rewrite of https://topaz.github.io/paste/
[More information on this self-hosted "about" page](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=)
[More information on this self-hosted "about" page](https://paste.scambier.xyz/?l=md#G0oEIKySx5UfTkdu1XoEsNFTBka+1+MnKOMEeLUaweWLc/oYlbttJz93QOgIkhdODeUmDs/+u3T5RANkM7+ktGLRxbTN0WIKyb2AWJbsnIoNDvzMtbcgQpsQYPWXPwUeRkuMWOXIEYIfzvnjqY85WNtT9Rjq3Y4/qHB1+mjzCQsTvr9UIfLbq28+i4joe5i/F2SQCTO9G/SlgnojZOKr9BSbIq0EPVBnVgg9T8+zls2Ezgg67tZTjX8pVB70RIghiwfFeqn/ZMQv9VGcbEWDIHnUlL52CL4FeCQ6DMJ4OOtVWk9jWI3i1CRvzxbmHudPlk1iA4pD9wHgOOILFPKQUAntulosi6VRhJ9eeSmViKpbloonQX9cDQVUkg8rzdkoBllTTfnVcYwDRsMS3UjTv/I+befAQeNMiRc7ESFegwUd6yEFPyVJG8RCXcgvw1QfdBe93uXQ9qGBL6BhLpjlpqxjmf9fwkKHFYClXmn7yaZrc/HrW6YH5F6Ei9MGKMvY5be++/noh6TJ5MeGgDzeJgNr0rjvcltZrDRS8wumbWpseMjTY1bz/rp/fJVQxwy/Rd+kXjlGrMJLZsVVwED5b0Jr26wUAm85shUyLX50kklq6ZIRfnzNX4LWrnl72vWR1YaTr/3n+Cp+6IBEliYA)
## TODO
- [x] Use brotli
- [x] Stay compatible with original lzma-compressed urls
- [x] ~~Stay compatible with original lzma-compressed urls~~
- [x] Have a read-only view with nice colors for code blocks
- [ ] Use CodeMirror 6
- [ ] 100% self-contained, no dependance on jsdeliver

View File

@ -3,8 +3,8 @@
<head>
<title>Paste</title>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<link rel="stylesheet" type="text/css" href="%sveltekit.assets%/fonts.css" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<link rel="preload" as="font" type="text/css" href="%sveltekit.assets%/hack-subset.css" />
<link
rel="stylesheet"
type="text/css"
@ -17,15 +17,5 @@ npm/codemirror@5.65.16/theme/nord.min.css"
</head>
<body data-sveltekit-preload-data="hover">
<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>
</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
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">
<h1 class="text-xl">Paste</h1>
<span class="ml-8 text-xs">
<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#G0oEIKySx5UfTkdu1XoEsNFTBka+1+MnKOMEeLUaweWLc/oYlbttJz93QOgIkhdODeUmDs/+u3T5RANkM7+ktGLRxbTN0WIKyb2AWJbsnIoNDvzMtbcgQpsQYPWXPwUeRkuMWOXIEYIfzvnjqY85WNtT9Rjq3Y4/qHB1+mjzCQsTvr9UIfLbq28+i4joe5i/F2SQCTO9G/SlgnojZOKr9BSbIq0EPVBnVgg9T8+zls2Ezgg67tZTjX8pVB70RIghiwfFeqn/ZMQv9VGcbEWDIHnUlL52CL4FeCQ6DMJ4OOtVWk9jWI3i1CRvzxbmHudPlk1iA4pD9wHgOOILFPKQUAntulosi6VRhJ9eeSmViKpbloonQX9cDQVUkg8rzdkoBllTTfnVcYwDRsMS3UjTv/I+befAQeNMiRc7ESFegwUd6yEFPyVJG8RCXcgvw1QfdBe93uXQ9qGBL6BhLpjlpqxjmf9fwkKHFYClXmn7yaZrc/HrW6YH5F6Ei9MGKMvY5be++/noh6TJ5MeGgDzeJgNr0rjvcltZrDRS8wumbWpseMjTY1bz/rp/fJVQxwy/Rd+kXjlGrMJLZsVVwED5b0Jr26wUAm85shUyLX50kklq6ZIRfnzNX4LWrnl72vWR1YaTr/3n+Cp+6IBEliYA"
target="_blank"
>
About</a
>
About
</a>
<a class="ml-4" href="https://git.scambier.xyz/scambier/paste" target="_blank">Source</a>
</span>
</div>
{#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}
<slot />
</div>
<style lang="scss">
button {
@apply text-sm border border-gray-300 p-1 hover:bg-gray-600/50;
}
</style>

6
src/globals.d.ts vendored
View File

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

View File

@ -1,33 +0,0 @@
// 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(base64: string): Promise<string> {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest()
req.open('GET', 'data:application/octet;base64,' + base64)
req.responseType = 'arraybuffer'
req.onload = (e) => {
const bytes = new Uint8Array(req.response)
lzma.decompress(bytes, (result, err) => {
if (err) reject(err)
else resolve(result)
})
}
req.send()
})
}
export { compress, decompress }

View File

@ -28,4 +28,8 @@ export const shorten = (name: string) => {
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)

View File

@ -2,4 +2,4 @@
import '../app.postcss'
</script>
<slot />
<slot />

View File

@ -1,7 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte'
import * as brotli from '$lib/brotli'
import * as lzma from '$lib/lzma'
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
@ -10,27 +8,27 @@
import rehypeStringify from 'rehype-stringify'
import hljs from 'highlight.js'
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 isMarkdown = false
let isPlainText = false
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
// extract the part in the url after the hash
const hash = window.location.hash.slice(1)
if (hash) {
// decompress the data
if (hash.startsWith('XQAAA')) {
decompressed = await lzma.decompress(hash)
} else {
decompressed = await brotli.decompress(hash)
}
const { decompress } = await import('$lib/brotli')
decompressed = await decompress(hash)
// Markdown
if (lang === 'md' || lang === 'gflm') {
if (lang === 'md') {
const html = await unified()
.use(remarkParse)
.use(remarkGfm)
@ -54,20 +52,35 @@
window.location.href = '/editor'
}
})
function getUrlDataPart(): string {
return window.location.search + window.location.hash
}
</script>
<div>
<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}
{@html decompressed}
{:else if isPlainText}
<div class="whitespace-pre-line">
{decompressed}
</div>
{:else}
<pre><code>{@html decompressed}</code></pre>
{/if}
</div>
{#if decompressed}
<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">
{#if isMarkdown}
{@html decompressed}
{:else if isPlainText}
<div class="whitespace-pre-line">
{decompressed}
</div>
{:else}
<pre><code>{@html decompressed}</code></pre>
{/if}
</div>
{/if}
</div>
<style lang="scss">

View File

@ -1,13 +1,11 @@
<script lang="ts">
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'
import { selectedLang, shareUrl } from '../../store'
import EditForm from '../../components/EditForm.svelte'
let editor: Editor | null = null
const readOnly = false
@ -15,33 +13,6 @@
let charLen = 0
let compressed: string = ''
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() {
if (editor) {
@ -60,7 +31,7 @@
waiting = false
}, 1000)
const initCodeEditor = () => {
async function initCodeEditor() {
CodeMirror.modeURL = 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/%N/%N.js'
editor = new CodeMirror(byId('editor'), {
lineNumbers: true,
@ -81,11 +52,40 @@
updateShareUrlDebounced()
}
})
// extract the part in the url after the hash
const hash = window.location.hash.slice(1)
if (hash) {
// decompress the data
const decompressed = await brotli.decompress(hash)
// set the editor value
if (editor) {
editor.setValue(decompressed)
}
}
$selectedLang = new URLSearchParams(window.location.search).get('l') ?? ' plt'
}
</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">
<TopBar {editor} {updateShareUrl} bind:setLanguage />
<TopBar>
<EditForm {editor} {updateShareUrl} class="grow" />
</TopBar>
<div id="editor" class="grow overflow-hidden" />
@ -98,13 +98,13 @@
? '?'
: Math.round(($shareUrl.length / charLen) * 100)}% of original)
</div>
</div>
<style>
:global(div.CodeMirror) {
height: 100%;
font-size: 15px;
font-family: 'Hack', monospace;
}
#footer {
--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'
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: {
extend: {
fontFamily: {
mono: ['"JetBrainsMono"', ...defaultTheme.fontFamily.mono],
mono: ['"Hack"', ...defaultTheme.fontFamily.mono],
},
typography: {
DEFAULT: {