idiot protection UwU
This commit is contained in:
12
package.json
12
package.json
@@ -9,19 +9,21 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"next": "15.5.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"next": "15.5.2"
|
"react-merge-refs": "^3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"tailwindcss": "^4",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.2",
|
"eslint-config-next": "15.5.2",
|
||||||
"@eslint/eslintrc": "^3"
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
next:
|
next:
|
||||||
specifier: 15.5.2
|
specifier: 15.5.2
|
||||||
version: 15.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 15.5.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -17,6 +20,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 19.1.0
|
specifier: 19.1.0
|
||||||
version: 19.1.0(react@19.1.0)
|
version: 19.1.0(react@19.1.0)
|
||||||
|
react-merge-refs:
|
||||||
|
specifier: ^3.0.2
|
||||||
|
version: 3.0.2(react@19.1.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3
|
specifier: ^3
|
||||||
@@ -726,6 +732,10 @@ packages:
|
|||||||
client-only@0.0.1:
|
client-only@0.0.1:
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||||
|
|
||||||
|
clsx@2.1.1:
|
||||||
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -1530,6 +1540,14 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
react-merge-refs@3.0.2:
|
||||||
|
resolution: {integrity: sha512-MSZAfwFfdbEvwkKWP5EI5chuLYnNUxNS7vyS0i1Jp+wtd8J4Ga2ddzhaE68aMol2Z4vCnRM/oGOo1a3V75UPlw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@19.1.0:
|
react@19.1.0:
|
||||||
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2434,6 +2452,8 @@ snapshots:
|
|||||||
|
|
||||||
client-only@0.0.1: {}
|
client-only@0.0.1: {}
|
||||||
|
|
||||||
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
@@ -3376,6 +3396,10 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
|
react-merge-refs@3.0.2(react@19.1.0):
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
|
||||||
react@19.1.0: {}
|
react@19.1.0: {}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #101010;
|
||||||
--foreground: #171717;
|
--foreground: #ededed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-inter);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-jetbrains-mono);
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import {
|
||||||
|
Geist,
|
||||||
|
Inter,
|
||||||
|
JetBrains_Mono as JetbrainsMono,
|
||||||
|
} from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const inter = Inter({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-inter",
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const jetbrainsMono = JetbrainsMono({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-jetbrains-mono",
|
||||||
subsets: ["latin"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: {
|
||||||
description: "Generated by create next app",
|
default: "Quill",
|
||||||
|
template: "%s | Quill",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${inter.variable} ${jetbrainsMono.variable} antialiased min-h-screen min-w-screen relative flex h-0 w-0`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
137
src/app/page.tsx
137
src/app/page.tsx
@@ -1,103 +1,42 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
|
import Editor from "@/lib/editor";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const State = {
|
||||||
|
Write: 0,
|
||||||
|
View: 1,
|
||||||
|
} as const;
|
||||||
|
type State = (typeof State)[keyof typeof State];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
const [state, setState] = useState<State>(State.Write);
|
||||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
const [buffer, setBuffer] = useState("");
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
const [focus, setFocus] = useState(false);
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
return (
|
||||||
<a
|
<main className="editor font-mono flex relative w-full h-full flex-col">
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
<div className="info-line text-white/50 m-4 mb-0">
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<span>[untitled document]</span>
|
||||||
target="_blank"
|
</div>
|
||||||
rel="noopener noreferrer"
|
<div className="content-area grow relative flex h-0 overflow-clip">
|
||||||
>
|
<Editor
|
||||||
<Image
|
className="px-4 overflow-y-auto w-full"
|
||||||
className="dark:invert"
|
onFocusChange={setFocus}
|
||||||
src="/vercel.svg"
|
/>
|
||||||
alt="Vercel logomark"
|
</div>
|
||||||
width={20}
|
<div className="status-line m-4 mt-0 border-t border-white/40 py-4 pb-0 flex flex-row gap-3 *:pl-3 *:border-l *:first:border-0 text-white/50 *:border-white/30">
|
||||||
height={20}
|
<span>* unsaved</span>
|
||||||
/>
|
<span>542 words</span>
|
||||||
Deploy now
|
<span>writing</span>
|
||||||
</a>
|
<span
|
||||||
<a
|
className="text-red-400 data-[focus=true]:collapse"
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
data-focus={focus}
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
>
|
||||||
target="_blank"
|
unfocused
|
||||||
rel="noopener noreferrer"
|
</span>
|
||||||
>
|
</div>
|
||||||
Read our docs
|
</main>
|
||||||
</a>
|
);
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/lib/editor.tsx
Normal file
104
src/lib/editor.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import assert from "assert";
|
||||||
|
import { get } from "http";
|
||||||
|
import { KeyboardEvent, Ref, RefObject, useEffect, useRef } from "react";
|
||||||
|
import { mergeRefs } from "react-merge-refs";
|
||||||
|
import { Markdown } from "./markdown";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
ref?: Ref<HTMLDivElement>;
|
||||||
|
|
||||||
|
onFocusChange?: (focused: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Editor({
|
||||||
|
className = "",
|
||||||
|
ref: extRef,
|
||||||
|
onFocusChange: extFocusChange = () => void {},
|
||||||
|
}: Props) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
||||||
|
const _state = useRef({
|
||||||
|
markdown: new Markdown(),
|
||||||
|
caret: [0, 0],
|
||||||
|
focused: false,
|
||||||
|
});
|
||||||
|
const state = () => _state.current;
|
||||||
|
|
||||||
|
function onSelectionChange(ev: Event) {
|
||||||
|
// console.log(caretPos());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(ev: KeyboardEvent<HTMLDivElement>) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (ev.key.length !== 1) return;
|
||||||
|
|
||||||
|
state().markdown.textAt(0).node.textContent = ev.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
document.addEventListener(
|
||||||
|
"selectionchange",
|
||||||
|
(ev) => state().focused && onSelectionChange(ev)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFocusChange = (focused: boolean) => {
|
||||||
|
state().focused = focused;
|
||||||
|
extFocusChange(focused);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
contentEditable
|
||||||
|
suppressContentEditableWarning
|
||||||
|
ref={mergeRefs([ref, extRef])}
|
||||||
|
className={`focus:outline-0 whitespace-pre inline ${className}`}
|
||||||
|
onFocus={() => onFocusChange(true)}
|
||||||
|
onBlur={() => onFocusChange(false)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// function caretPos(): (
|
||||||
|
// | (number & { range: false })
|
||||||
|
// | ([number, number] & { range: true })
|
||||||
|
// ) & { [Symbol.iterator](): Generator<number> } {
|
||||||
|
// let {
|
||||||
|
// anchorOffset: base,
|
||||||
|
// anchorNode: baseNode,
|
||||||
|
// focusOffset: focus,
|
||||||
|
// focusNode,
|
||||||
|
// } = document.getSelection()!;
|
||||||
|
|
||||||
|
// base +=
|
||||||
|
// Number.parseInt(
|
||||||
|
// baseNode?.parentElement?.getAttribute("data-range-start") ?? "0"
|
||||||
|
// ) ?? 0;
|
||||||
|
// focus +=
|
||||||
|
// Number.parseInt(
|
||||||
|
// focusNode?.parentElement?.getAttribute("data-range-start") ?? "0"
|
||||||
|
// ) ?? 0;
|
||||||
|
|
||||||
|
// const scalar = base === focus;
|
||||||
|
// const position = scalar
|
||||||
|
// ? base
|
||||||
|
// : [Math.min(base, focus), Math.max(base, focus)];
|
||||||
|
|
||||||
|
// return Object.assign(position, {
|
||||||
|
// range: !scalar,
|
||||||
|
// [Symbol.iterator]: function* () {
|
||||||
|
// if (Array.isArray(position)) {
|
||||||
|
// yield position[0];
|
||||||
|
// yield position[1];
|
||||||
|
// } else {
|
||||||
|
// yield position;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// }) as ReturnType<typeof caretPos>;
|
||||||
|
// }
|
||||||
165
src/lib/markdown.tsx
Normal file
165
src/lib/markdown.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { RefObject } from "react";
|
||||||
|
|
||||||
|
type Event<T extends unknown[]> = ((...args: T) => void) & {
|
||||||
|
on: (callback: (...args: T) => void) => void;
|
||||||
|
once: (callback: (...args: T) => void) => void;
|
||||||
|
off: (callback: (...args: T) => void) => void;
|
||||||
|
};
|
||||||
|
function Event<T extends unknown[]>(): Event<T> {
|
||||||
|
let listeners: ((...args: T) => void)[] = [];
|
||||||
|
const callable = ((...args: T) => {
|
||||||
|
for (const listener of listeners) listener(...args);
|
||||||
|
}) as Event<T>;
|
||||||
|
|
||||||
|
callable.on = (callback) => {
|
||||||
|
listeners.push(callback);
|
||||||
|
};
|
||||||
|
callable.off = (callback) => {
|
||||||
|
const index = listeners.findIndex((other) => other === callback);
|
||||||
|
listeners = [
|
||||||
|
...listeners.slice(0, index),
|
||||||
|
...listeners.slice(index + 1),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
callable.once = (callback) => {
|
||||||
|
const closure = (...args: T) => {
|
||||||
|
callback(...args);
|
||||||
|
callable.off(closure);
|
||||||
|
};
|
||||||
|
callable.on(closure);
|
||||||
|
};
|
||||||
|
|
||||||
|
return callable;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Node {
|
||||||
|
private _start: number;
|
||||||
|
public get start() {
|
||||||
|
return this._start;
|
||||||
|
}
|
||||||
|
protected set start(value: number) {
|
||||||
|
this._start = value;
|
||||||
|
|
||||||
|
this.BoundChange(this._start, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _end: number;
|
||||||
|
public get end() {
|
||||||
|
return this._end;
|
||||||
|
}
|
||||||
|
protected set end(value: number) {
|
||||||
|
this._end = value;
|
||||||
|
|
||||||
|
this.BoundChange(undefined, this._end);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly BoundChange: Event<
|
||||||
|
[number, undefined] | [undefined, number]
|
||||||
|
> = Event();
|
||||||
|
|
||||||
|
public NodeCreated: Event<[Node]> = Event();
|
||||||
|
|
||||||
|
public node: globalThis.Node;
|
||||||
|
public constructor(offset: number) {
|
||||||
|
this._start = offset;
|
||||||
|
this._end = offset;
|
||||||
|
|
||||||
|
this.node = this.createElement();
|
||||||
|
console.log(this.node);
|
||||||
|
|
||||||
|
this.NodeCreated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract _textAt(position: number): Text;
|
||||||
|
public textAt(position: number): Text {
|
||||||
|
console.assert(position >= this._start && position <= this._end);
|
||||||
|
return this._textAt(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract createElement(): globalThis.Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class NodeCollection<T extends Node> extends Node {
|
||||||
|
collection: (T | Text)[] = [];
|
||||||
|
|
||||||
|
protected _textAt(position: number): Text {
|
||||||
|
if (this.collection.length === 0) {
|
||||||
|
const text = new Text(this.start);
|
||||||
|
|
||||||
|
this.NodeCreated(text);
|
||||||
|
this.collection.push(text);
|
||||||
|
|
||||||
|
this.node.appendChild(text.node);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < this.collection.length; i++) {
|
||||||
|
const node = this.collection[i];
|
||||||
|
if (node.start > position) break;
|
||||||
|
if (node.end <= position) continue;
|
||||||
|
return node.textAt(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = this.collection[i - 1];
|
||||||
|
console.assert(prev);
|
||||||
|
|
||||||
|
if (prev.end === position) {
|
||||||
|
const text = new Text(position);
|
||||||
|
|
||||||
|
this.NodeCreated(text);
|
||||||
|
this.collection.push(text);
|
||||||
|
|
||||||
|
this.node.insertBefore(text.node, prev.node.nextSibling);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev.textAt(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Text extends Node {
|
||||||
|
private text: string = "";
|
||||||
|
public TextChange: Event<[string]> = Event();
|
||||||
|
|
||||||
|
public constructor(offset: number) {
|
||||||
|
super(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
_textAt(_: number): Text {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createElement(): globalThis.Node {
|
||||||
|
const node = document.createTextNode("");
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Paragraph extends NodeCollection<Node> {
|
||||||
|
public createElement(): globalThis.Node {
|
||||||
|
const node = document.createElement("p");
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Markdown extends NodeCollection<Paragraph> {
|
||||||
|
public lookup: Map<globalThis.Node, Node> = new Map();
|
||||||
|
|
||||||
|
public constructor(element: HTMLDivElement) {
|
||||||
|
super(0);
|
||||||
|
this.NodeCreated.on((node) => {
|
||||||
|
console.log("created node:", node);
|
||||||
|
|
||||||
|
this.lookup.set(node.node, node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public createElement(): globalThis.Node {
|
||||||
|
return 0 as never;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/lib/theme.tsx
Normal file
21
src/lib/theme.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
const DefaultTheme = {
|
||||||
|
background: "#ededed",
|
||||||
|
contrast: "#f1f1f1",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
export interface Theme extends Record<keyof typeof DefaultTheme, string> {}
|
||||||
|
export class Theme {
|
||||||
|
private constructor() {
|
||||||
|
for (const key in DefaultTheme) {
|
||||||
|
this[key as keyof Theme] =
|
||||||
|
DefaultTheme[key as keyof typeof DefaultTheme];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static default() {
|
||||||
|
return new Theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static parse() {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user