mirror of
https://github.com/dsec-hub/dsec-notebook.git
synced 2026-09-22 15:43:58 +00:00
added markdown editor
This commit is contained in:
parent
2bb6c09b5e
commit
efb364d7cc
12 changed files with 3640 additions and 3344 deletions
|
|
@ -21,9 +21,9 @@ if (!self.define) {
|
||||||
|
|
||||||
const singleRequire = (uri, parentUri) => {
|
const singleRequire = (uri, parentUri) => {
|
||||||
uri = new URL(uri + ".js", parentUri).href;
|
uri = new URL(uri + ".js", parentUri).href;
|
||||||
return (
|
return registry[uri] || (
|
||||||
registry[uri] ||
|
|
||||||
new Promise((resolve) => {
|
new Promise(resolve => {
|
||||||
if ("document" in self) {
|
if ("document" in self) {
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = uri;
|
script.src = uri;
|
||||||
|
|
@ -47,31 +47,27 @@ if (!self.define) {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.define = (depsNames, factory) => {
|
self.define = (depsNames, factory) => {
|
||||||
const uri =
|
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||||
nextDefineUri ||
|
|
||||||
("document" in self ? document.currentScript.src : "") ||
|
|
||||||
location.href;
|
|
||||||
if (registry[uri]) {
|
if (registry[uri]) {
|
||||||
// Module is already loading or loaded.
|
// Module is already loading or loaded.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let exports = {};
|
let exports = {};
|
||||||
const require = (depUri) => singleRequire(depUri, uri);
|
const require = depUri => singleRequire(depUri, uri);
|
||||||
const specialDeps = {
|
const specialDeps = {
|
||||||
module: { uri },
|
module: { uri },
|
||||||
exports,
|
exports,
|
||||||
require,
|
require
|
||||||
};
|
};
|
||||||
registry[uri] = Promise.all(
|
registry[uri] = Promise.all(depsNames.map(
|
||||||
depsNames.map((depName) => specialDeps[depName] || require(depName)),
|
depName => specialDeps[depName] || require(depName)
|
||||||
).then((deps) => {
|
)).then(deps => {
|
||||||
factory(...deps);
|
factory(...deps);
|
||||||
return exports;
|
return exports;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
define(["./workbox-7e5eb42b"], function (workbox) {
|
define(['./workbox-7e5eb42b'], (function (workbox) { 'use strict';
|
||||||
"use strict";
|
|
||||||
|
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
workbox.clientsClaim();
|
workbox.clientsClaim();
|
||||||
|
|
@ -80,19 +76,13 @@ define(["./workbox-7e5eb42b"], function (workbox) {
|
||||||
* requests for URLs in the manifest.
|
* requests for URLs in the manifest.
|
||||||
* See https://goo.gl/S9QRab
|
* See https://goo.gl/S9QRab
|
||||||
*/
|
*/
|
||||||
workbox.precacheAndRoute(
|
workbox.precacheAndRoute([{
|
||||||
[
|
"url": "/",
|
||||||
{
|
"revision": "0.7vtfun0cbi"
|
||||||
url: "/",
|
}], {});
|
||||||
revision: "0.or5ntbjovsg",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
|
||||||
new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
|
allowlist: [/^\/$/]
|
||||||
allowlist: [/^\/$/],
|
}));
|
||||||
}),
|
|
||||||
);
|
}));
|
||||||
});
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
96
package-lock.json
generated
96
package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "dsec-notebook",
|
"name": "dsec-notebook",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"markdown-it": "^15.0.1",
|
||||||
"resend": "^6.25.0"
|
"resend": "^6.25.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -4094,6 +4095,22 @@
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-nM4mHF/KM1v59ZNKX7zfusQz5wUAxR511YG8Vo6TyiV4aqhu++rbJW4v04xsWhpSsHFj66flT8P7znVpyO20xQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "PSF-2.0"
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||||
|
|
@ -4820,6 +4837,18 @@
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-abstract": {
|
"node_modules/es-abstract": {
|
||||||
"version": "1.24.2",
|
"version": "1.24.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
|
||||||
|
|
@ -6366,6 +6395,25 @@
|
||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-character": {
|
"node_modules/locate-character": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
|
|
@ -6400,6 +6448,33 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "15.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.1.tgz",
|
||||||
|
"integrity": "sha512-9/7gE95FNPkfUWrjJIoHZza2iLmuJlPD0UNMxPi7bxUrbCR525YZY0r+zyfes0dZI5ZZ/uNIXUJca0pJvtw41g==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^3.0.0",
|
||||||
|
"entities": "^8.0.0",
|
||||||
|
"linkify-it": "^6.0.0",
|
||||||
|
"mdurl": "^2.1.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
|
@ -6410,6 +6485,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.2.6",
|
"version": "10.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||||
|
|
@ -6799,6 +6880,15 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz",
|
||||||
|
|
@ -7928,6 +8018,12 @@
|
||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"fmt:check": "oxfmt --check"
|
"fmt:check": "oxfmt --check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"markdown-it": "^15.0.1",
|
||||||
"resend": "^6.25.0"
|
"resend": "^6.25.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
7
src/lib/components/Markdown.svelte
Normal file
7
src/lib/components/Markdown.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { renderMarkdown } from "$lib/markdown";
|
||||||
|
|
||||||
|
let { content }: { content: string } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="markdown">{@html renderMarkdown(content)}</div>
|
||||||
208
src/lib/components/MarkdownEditor.svelte
Normal file
208
src/lib/components/MarkdownEditor.svelte
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { tick } from "svelte";
|
||||||
|
import Markdown from "./Markdown.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
content = $bindable(""),
|
||||||
|
label = "Content",
|
||||||
|
placeholder = "",
|
||||||
|
rows = 10,
|
||||||
|
}: {
|
||||||
|
content?: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let textarea: HTMLTextAreaElement | undefined = $state();
|
||||||
|
let mode = $state<"write" | "preview">("write");
|
||||||
|
|
||||||
|
function selection(): { start: number; end: number; text: string } {
|
||||||
|
if (!textarea) return { start: content.length, end: content.length, text: "" };
|
||||||
|
const start = textarea.selectionStart ?? content.length;
|
||||||
|
const end = textarea.selectionEnd ?? content.length;
|
||||||
|
return { start, end, text: content.slice(start, end) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyEdit(
|
||||||
|
build: (
|
||||||
|
value: string,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
) => {
|
||||||
|
value: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const { start, end } = selection();
|
||||||
|
const result = build(content, start, end);
|
||||||
|
content = result.value;
|
||||||
|
void tick().then(() => {
|
||||||
|
if (!textarea) return;
|
||||||
|
textarea.focus();
|
||||||
|
textarea.setSelectionRange(result.start, result.end);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrap(before: string, after = before) {
|
||||||
|
const { start, end, text } = selection();
|
||||||
|
applyEdit((value) => {
|
||||||
|
const next = value.slice(0, start) + before + text + after + value.slice(end);
|
||||||
|
return {
|
||||||
|
value: next,
|
||||||
|
start: start + before.length,
|
||||||
|
end: start + before.length + text.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefixLines(prefix: string) {
|
||||||
|
const { start, end } = selection();
|
||||||
|
applyEdit((value) => {
|
||||||
|
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
||||||
|
let lineEnd = end;
|
||||||
|
if (start === end) {
|
||||||
|
const nextNewline = value.indexOf("\n", end);
|
||||||
|
lineEnd = nextNewline === -1 ? value.length : nextNewline;
|
||||||
|
}
|
||||||
|
const block = value.slice(lineStart, lineEnd);
|
||||||
|
const prefixed = block
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => prefix + line)
|
||||||
|
.join("\n");
|
||||||
|
return {
|
||||||
|
value: value.slice(0, lineStart) + prefixed + value.slice(lineEnd),
|
||||||
|
start: lineStart,
|
||||||
|
end: lineStart + prefixed.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertLink() {
|
||||||
|
const { start, end, text } = selection();
|
||||||
|
const label = text || "link text";
|
||||||
|
const insertion = `[${label}](url)`;
|
||||||
|
applyEdit((value) => {
|
||||||
|
const next = value.slice(0, start) + insertion + value.slice(end);
|
||||||
|
const selectStart = start + insertion.length - 4;
|
||||||
|
const selectEnd = start + insertion.length - 1;
|
||||||
|
return { value: next, start: selectStart, end: selectEnd };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertCodeBlock() {
|
||||||
|
const { start, end, text } = selection();
|
||||||
|
applyEdit((value) => {
|
||||||
|
const next = value.slice(0, start) + "```\n" + text + "\n```" + value.slice(end);
|
||||||
|
return {
|
||||||
|
value: next,
|
||||||
|
start: start + 4,
|
||||||
|
end: start + 4 + text.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if label}
|
||||||
|
<label for="markdown-editor" class="kicker mb-2 block">{label}</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if mode === "write"}
|
||||||
|
<div
|
||||||
|
class="border-rule bg-surface mb-2 flex flex-wrap items-center gap-1 rounded-sm border p-1"
|
||||||
|
>
|
||||||
|
<button type="button" class="editor-tool" title="Bold" onclick={() => wrap("**")}>
|
||||||
|
<strong>B</strong>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="editor-tool" title="Italic" onclick={() => wrap("_")}>
|
||||||
|
<em>I</em>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="editor-tool"
|
||||||
|
title="Strikethrough"
|
||||||
|
onclick={() => wrap("~~")}
|
||||||
|
>
|
||||||
|
<s>S</s>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="editor-tool" title="Inline code" onclick={() => wrap("`")}>
|
||||||
|
</>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="editor-tool"
|
||||||
|
title="Heading"
|
||||||
|
onclick={() => prefixLines("## ")}
|
||||||
|
>
|
||||||
|
H
|
||||||
|
</button>
|
||||||
|
<button type="button" class="editor-tool" title="Link" onclick={insertLink}>🔗</button>
|
||||||
|
<button type="button" class="editor-tool" title="Code block" onclick={insertCodeBlock}>
|
||||||
|
{`{ }`}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="editor-tool"
|
||||||
|
title="Bullet list"
|
||||||
|
onclick={() => prefixLines("- ")}
|
||||||
|
>
|
||||||
|
• List
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="editor-tool"
|
||||||
|
title="Blockquote"
|
||||||
|
onclick={() => prefixLines("> ")}
|
||||||
|
>
|
||||||
|
❝
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="border-rule mb-2 flex items-center gap-2 border-b">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="kicker {mode === 'write'
|
||||||
|
? 'text-ink'
|
||||||
|
: 'hover:text-primary'} border-b-2 border-transparent pb-2"
|
||||||
|
onclick={() => (mode = "write")}
|
||||||
|
>
|
||||||
|
Write
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="kicker {mode === 'preview'
|
||||||
|
? 'text-ink'
|
||||||
|
: 'hover:text-primary'} border-b-2 border-transparent pb-2"
|
||||||
|
onclick={() => (mode = "preview")}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if mode === "write"}
|
||||||
|
<textarea
|
||||||
|
id="markdown-editor"
|
||||||
|
bind:this={textarea}
|
||||||
|
bind:value={content}
|
||||||
|
{rows}
|
||||||
|
{placeholder}
|
||||||
|
class="field resize-y"></textarea>
|
||||||
|
<p class="text-faint mt-2 text-xs">
|
||||||
|
Markdown supported: **bold**, _italic_, `code`, [links](url), lists, quotes and code
|
||||||
|
blocks.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="border-rule bg-surface text-ink min-h-40 rounded-sm border px-3 py-2.5 text-sm leading-relaxed"
|
||||||
|
>
|
||||||
|
{#if content.trim()}
|
||||||
|
<Markdown {content} />
|
||||||
|
{:else}
|
||||||
|
<p class="text-faint">Nothing to preview yet.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
11
src/lib/markdown.ts
Normal file
11
src/lib/markdown.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
|
||||||
|
const md = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
linkify: true,
|
||||||
|
breaks: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function renderMarkdown(source: string): string {
|
||||||
|
return md.render(source ?? "");
|
||||||
|
}
|
||||||
|
|
@ -119,3 +119,140 @@ button {
|
||||||
.nav-link-active {
|
.nav-link-active {
|
||||||
@apply text-ink;
|
@apply text-ink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-tool {
|
||||||
|
@apply text-ink hover:border-primary hover:text-primary bg-surface inline-flex h-8 min-w-8 items-center justify-center rounded-sm border border-transparent px-1.5 font-sans text-xs font-medium transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown > :last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown p {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h1,
|
||||||
|
.markdown h2,
|
||||||
|
.markdown h3,
|
||||||
|
.markdown h4 {
|
||||||
|
font-family: var(--font-serif);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin: 1.25em 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h2 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h3 {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h4 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown ul,
|
||||||
|
.markdown ol {
|
||||||
|
margin: 0.75em 0;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown ul {
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown ol {
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown li {
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown a {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown a:hover {
|
||||||
|
color: var(--color-secondary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown code {
|
||||||
|
background: var(--color-rule);
|
||||||
|
color: var(--color-ink);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.15em 0.35em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown pre {
|
||||||
|
background: var(--color-ink);
|
||||||
|
color: var(--color-surface);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown pre code {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown blockquote {
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
color: var(--color-muted);
|
||||||
|
margin: 0.75em 0;
|
||||||
|
padding: 0.25em 0 0.25em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--color-rule);
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.75em 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown th,
|
||||||
|
.markdown td {
|
||||||
|
border: 1px solid var(--color-rule);
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown th {
|
||||||
|
background: var(--color-rule);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import VoteStack from "$lib/components/VoteStack.svelte";
|
import VoteStack from "$lib/components/VoteStack.svelte";
|
||||||
|
import Markdown from "$lib/components/Markdown.svelte";
|
||||||
import { timeAgo } from "$lib/time";
|
import { timeAgo } from "$lib/time";
|
||||||
import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
|
import type { NoteDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
|
||||||
|
|
||||||
|
|
@ -115,10 +116,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed">
|
||||||
class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed whitespace-pre-wrap"
|
<Markdown content={note.content} />
|
||||||
>
|
|
||||||
{note.content}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="border-rule mt-12 border-t pt-8">
|
<section class="border-rule mt-12 border-t pt-8">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||||
import type { TopicDoc, UnitDoc } from "$lib/types";
|
import type { TopicDoc, UnitDoc } from "$lib/types";
|
||||||
|
|
||||||
let topics: TopicDoc[] = $state([]);
|
let topics: TopicDoc[] = $state([]);
|
||||||
|
|
@ -121,14 +122,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="content" class="kicker mb-2 block">Content</label>
|
<MarkdownEditor
|
||||||
<textarea
|
bind:content
|
||||||
id="content"
|
label="Content"
|
||||||
bind:value={content}
|
|
||||||
rows={10}
|
|
||||||
placeholder="Write your notes here..."
|
placeholder="Write your notes here..."
|
||||||
class="field resize-y"
|
rows={10}
|
||||||
required></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
|
||||||
import type { TopicDoc, UnitDoc } from "$lib/types";
|
import type { TopicDoc, UnitDoc } from "$lib/types";
|
||||||
|
|
||||||
let topics: TopicDoc[] = $state([]);
|
let topics: TopicDoc[] = $state([]);
|
||||||
|
|
@ -121,14 +122,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="content" class="kicker mb-2 block">Details</label>
|
<MarkdownEditor
|
||||||
<textarea
|
bind:content
|
||||||
id="content"
|
label="Details"
|
||||||
bind:value={content}
|
|
||||||
rows={10}
|
|
||||||
placeholder="Describe your question in detail..."
|
placeholder="Describe your question in detail..."
|
||||||
class="field resize-y"
|
rows={10}
|
||||||
required></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import VoteStack from "$lib/components/VoteStack.svelte";
|
import VoteStack from "$lib/components/VoteStack.svelte";
|
||||||
|
import Markdown from "$lib/components/Markdown.svelte";
|
||||||
import { timeAgo } from "$lib/time";
|
import { timeAgo } from "$lib/time";
|
||||||
import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
|
import type { QuestionDoc, CommentDoc, TopicDoc, UnitDoc } from "$lib/types";
|
||||||
|
|
||||||
|
|
@ -113,10 +114,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed">
|
||||||
class="border-rule text-ink mt-8 border-t pt-8 text-[15px] leading-relaxed whitespace-pre-wrap"
|
<Markdown content={question.content} />
|
||||||
>
|
|
||||||
{question.content}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="border-rule mt-12 border-t pt-8">
|
<section class="border-rule mt-12 border-t pt-8">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue