1
0

Cleanup, functional

This commit is contained in:
Zef Hemel 2022-02-21 13:25:41 +01:00
parent cbe0677f93
commit c9f4266d34
21 changed files with 237 additions and 451 deletions

1
notes/Another note.md Normal file
View File

@ -0,0 +1 @@
Sup yo

View File

@ -0,0 +1 @@
Yo!!

1
notes/Zef is cool.md Normal file
View File

@ -0,0 +1 @@
I know he is! **Bold**

View File

@ -9,6 +9,7 @@ For the rest of you, if you are not a parent, have no plan to be, or have absolu
Thank you for sharing that perspective. However, in your case specifically, I have to insist you keep reading. Its kids with your independent mindset that were trying to raise here. Although, perhaps you already know how to do that. Get in touch.
Hello there
-----

View File

@ -1 +1,3 @@
# Sappie
# Sappie
Sup

5
notes/test3.md Normal file
View File

@ -0,0 +1,5 @@
# Hello
## Second level header
bla

View File

@ -27,14 +27,24 @@ fsRouter.get('/', async context => {
fsRouter.get('/:note', async context => {
const noteName = context.params.note;
const localPath = `${notesPath}/${noteName}.md`;
const text = await Deno.readTextFile(localPath);
context.response.body = text;
try {
const text = await Deno.readTextFile(localPath);
context.response.body = text;
} catch (e) {
context.response.status = 404;
context.response.body = "";
}
});
fsRouter.options('/:note', async context => {
const localPath = `${notesPath}/${context.params.note}.md`;
const stat = await Deno.stat(localPath);
context.response.headers.set('Content-length', `${stat.size}`);
try {
const stat = await Deno.stat(localPath);
context.response.headers.set('Content-length', `${stat.size}`);
} catch (e) {
context.response.status = 200;
context.response.body = "";
}
})
fsRouter.put('/:note', async context => {

View File

@ -1,130 +0,0 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
import { indentWithTab, standardKeymap } from "@codemirror/commands";
import { history, historyKeymap } from "@codemirror/history";
import { indentOnInput } from "@codemirror/language";
import { bracketMatching } from "@codemirror/matchbrackets";
import { searchKeymap } from "@codemirror/search";
import { EditorState, StateField } from "@codemirror/state";
import { drawSelection, dropCursor, EditorView, highlightSpecialChars, keymap, } from "@codemirror/view";
import * as commands from "./commands";
import { markdown } from "./markdown";
import { lineWrapper } from "./lineWrapper";
import customMarkDown from "./parser";
import customMarkdownStyle from "./style";
import { HttpFileSystem } from "./fs";
import ReactDOM from "react-dom";
import { useEffect, useRef } from "react";
const fs = new HttpFileSystem("http://localhost:2222/fs");
class Editor {
constructor(parent, currentNote, text) {
this.view = new EditorView({
state: this.createEditorState(text),
parent: parent,
});
this.currentNote = currentNote;
}
load(name, text) {
this.currentNote = name;
this.view.setState(this.createEditorState(text));
}
async save() {
await fs.writeNote(this.currentNote, this.view.state.sliceDoc());
}
focus() {
this.view.focus();
}
createEditorState(text) {
return EditorState.create({
doc: text,
extensions: [
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
indentOnInput(),
customMarkdownStyle,
bracketMatching(),
closeBrackets(),
autocompletion(),
EditorView.lineWrapping,
lineWrapper([
{ selector: "ATXHeading1", class: "line-h1" },
{ selector: "ATXHeading2", class: "line-h2" },
{ selector: "ListItem", class: "line-li" },
{ selector: "Blockquote", class: "line-blockquote" },
{ selector: "CodeBlock", class: "line-code" },
{ selector: "FencedCode", class: "line-fenced-code" },
]),
keymap.of([
...closeBracketsKeymap,
...standardKeymap,
...searchKeymap,
...historyKeymap,
...completionKeymap,
indentWithTab,
{
key: "Ctrl-b",
mac: "Cmd-b",
run: commands.insertMarker("**"),
},
{
key: "Ctrl-i",
mac: "Cmd-i",
run: commands.insertMarker("_"),
},
{
key: "Ctrl-s",
mac: "Cmd-s",
run: (target) => {
Promise.resolve()
.then(async () => {
console.log("Saving");
await this.save();
})
.catch((e) => console.error(e));
return true;
},
},
]),
EditorView.domEventHandlers({
click: (event, view) => {
if (event.metaKey || event.ctrlKey) {
console.log("Navigate click");
let coords = view.posAtCoords(event);
console.log("Coords", view.state.doc.sliceString(coords, coords + 1));
return false;
}
},
}),
markdown({
base: customMarkDown,
}),
StateField.define({
create: () => null,
update: (value, transaction) => {
if (transaction.docChanged) {
console.log("Something changed");
}
return null;
},
}),
],
});
}
}
export const App = () => {
const editorRef = useRef();
useEffect(() => {
let editor = new Editor(editorRef.current, "", "");
editor.focus();
// @ts-ignore
window.editor = editor;
fs.readNote("start").then((text) => {
editor.load("start", text);
});
}, []);
return (_jsxs(_Fragment, { children: [_jsx("div", { id: "top", children: "Hello" }, void 0), _jsx("div", { id: "editor", ref: editorRef }, void 0), _jsx("div", { id: "bottom", children: "Bottom" }, void 0)] }, void 0));
};
ReactDOM.render(_jsx(App, {}, void 0), document.body);

View File

@ -24,19 +24,11 @@ import customMarkdownStyle from "./style";
import { FilterList } from "./components/filter";
import { NoteMeta, AppViewState, Action } from "./types";
import reducer from "./reducer";
const fs = new HttpFileSystem("http://localhost:2222/fs");
type NoteMeta = {
name: string;
};
type AppViewState = {
currentNote: string;
isSaved: boolean;
isFiltering: boolean;
allNotes: NoteMeta[];
};
const initialViewState = {
currentNote: "",
isSaved: false,
@ -44,50 +36,6 @@ const initialViewState = {
allNotes: [],
};
type Action =
| { type: "loaded"; name: string }
| { type: "saved" }
| { type: "start-navigate" }
| { type: "stop-navigate" }
| { type: "updated" }
| { type: "notes-list"; notes: NoteMeta[] };
function reducer(state: AppViewState, action: Action): AppViewState {
switch (action.type) {
case "loaded":
return {
...state,
currentNote: action.name,
isSaved: true,
};
case "saved":
return {
...state,
isSaved: true,
};
case "updated":
return {
...state,
isSaved: false,
};
case "start-navigate":
return {
...state,
isFiltering: true,
};
case "stop-navigate":
return {
...state,
isFiltering: false,
};
case "notes-list":
return {
...state,
allNotes: action.notes,
};
}
}
class Editor {
view: EditorView;
currentNote: string;
@ -184,7 +132,6 @@ class Editor {
update(value: null, transaction: Transaction): null {
if (transaction.docChanged) {
console.log("Something changed");
this.dispatch({
type: "updated",
});
@ -215,6 +162,10 @@ class Editor {
focus() {
this.view.focus();
}
navigate(name: string) {
location.hash = encodeURIComponent(name);
}
}
function TopBar({
@ -223,28 +174,32 @@ function TopBar({
isFiltering,
allNotes,
onNavigate,
onClick,
}: {
currentNote: string;
isSaved: boolean;
isFiltering: boolean;
allNotes: NoteMeta[];
onNavigate: (note: string) => void;
onNavigate: (note: string | undefined) => void;
onClick: () => void;
}) {
return (
<div id="top">
<span className="current-note">{currentNote}</span>
{isSaved ? "" : "*"}
<div className="current-note" onClick={onClick}>
» {currentNote}
{isSaved ? "" : "*"}
</div>
{isFiltering ? (
{isFiltering && (
<FilterList
initialText=""
options={allNotes}
onSelect={(opt) => {
console.log("Selected", opt);
onNavigate(opt.name);
onNavigate(opt?.name);
}}
></FilterList>
) : null}
)}
</div>
);
}
@ -260,13 +215,9 @@ function AppView() {
editor.focus();
// @ts-ignore
window.editor = editor;
fs.readNote("start").then((text) => {
editor!.load("start", text);
dispatch({
type: "loaded",
name: "start",
});
});
if (!location.hash) {
editor.navigate("start");
}
}, []);
useEffect(() => {
@ -280,6 +231,30 @@ function AppView() {
.catch((e) => console.error(e));
}, []);
useEffect(() => {
function hashChange() {
const noteName = decodeURIComponent(location.hash.substring(1));
console.log("Now navigating to", noteName);
fs.readNote(noteName)
.then((text) => {
editor!.load(noteName, text);
dispatch({
type: "loaded",
name: noteName,
});
})
.catch((e) => {
console.error("Error loading note", e);
});
}
hashChange();
window.addEventListener("hashchange", hashChange);
return () => {
window.removeEventListener("hashchange", hashChange);
};
}, []);
return (
<>
<TopBar
@ -287,16 +262,15 @@ function AppView() {
isSaved={appState.isSaved}
isFiltering={appState.isFiltering}
allNotes={appState.allNotes}
onClick={() => {
dispatch({ type: "start-navigate" });
}}
onNavigate={(note) => {
dispatch({ type: "stop-navigate" });
editor!.focus();
fs.readNote(note).then((text) => {
editor!.load(note, text);
dispatch({
type: "loaded",
name: note,
});
});
if (note) {
editor!.navigate(note);
}
}}
/>
<div id="editor" ref={editorRef}></div>
@ -305,4 +279,4 @@ function AppView() {
);
}
ReactDOM.render(<AppView />, document.body);
ReactDOM.render(<AppView />, document.getElementById("root"));

View File

@ -1,38 +0,0 @@
import { EditorSelection, Transaction } from "@codemirror/state";
import { Text } from "@codemirror/text";
export function insertMarker(marker) {
return ({ state, dispatch }) => {
const changes = state.changeByRange((range) => {
const isBoldBefore = state.sliceDoc(range.from - marker.length, range.from) === marker;
const isBoldAfter = state.sliceDoc(range.to, range.to + marker.length) === marker;
const changes = [];
changes.push(isBoldBefore ? {
from: range.from - marker.length,
to: range.from,
insert: Text.of([''])
} : {
from: range.from,
insert: Text.of([marker]),
});
changes.push(isBoldAfter ? {
from: range.to,
to: range.to + marker.length,
insert: Text.of([''])
} : {
from: range.to,
insert: Text.of([marker]),
});
const extendBefore = isBoldBefore ? -marker.length : marker.length;
const extendAfter = isBoldAfter ? -marker.length : marker.length;
return {
changes,
range: EditorSelection.range(range.from + extendBefore, range.to + extendAfter),
};
});
dispatch(state.update(changes, {
scrollIntoView: true,
annotations: Transaction.userEvent.of('input'),
}));
return true;
};
}

View File

@ -2,16 +2,19 @@ import React, { useEffect, useRef, useState } from "react";
type Option = {
name: string;
hint?: string;
};
export function FilterList({
initialText,
options,
onSelect,
allowNew = false,
}: {
initialText: string;
options: Option[];
onSelect: (option: Option) => void;
onSelect: (option: Option | undefined) => void;
allowNew?: boolean;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const [text, setText] = useState(initialText);
@ -19,18 +22,23 @@ export function FilterList({
const [selectedOption, setSelectionOption] = useState(0);
const filter = (e: React.ChangeEvent<HTMLInputElement>) => {
const keyword = e.target.value.toLowerCase();
const originalPhrase = e.target.value;
const searchPhrase = originalPhrase.toLowerCase();
if (keyword) {
const results = options.filter((option) => {
return option.name.toLowerCase().indexOf(keyword) !== -1;
if (searchPhrase) {
let results = options.filter((option) => {
return option.name.toLowerCase().indexOf(searchPhrase) !== -1;
});
results.splice(0, 0, {
name: originalPhrase,
hint: "Create new",
});
setMatchingOptions(results);
} else {
setMatchingOptions(options);
}
setText(keyword);
setText(originalPhrase);
setSelectionOption(0);
};
@ -38,10 +46,22 @@ export function FilterList({
searchBoxRef.current!.focus();
}, []);
useEffect(() => {
function closer() {
onSelect(undefined);
}
document.addEventListener("click", closer);
return () => {
console.log("Unsubscribing");
document.removeEventListener("click", closer);
};
}, []);
return (
<div className="filter-container">
<input
type="search"
type="text"
value={text}
ref={searchBoxRef}
onChange={filter}
@ -60,32 +80,36 @@ export function FilterList({
onSelect(matchingOptions[selectedOption]);
e.preventDefault();
break;
case "Escape":
onSelect(undefined);
break;
}
}}
className="input"
placeholder="Filter"
placeholder=""
/>
<div className="result-list">
{matchingOptions && matchingOptions.length > 0 ? (
matchingOptions.map((option, idx) => (
<li
key={"" + idx}
className={selectedOption === idx ? "selected-option" : "option"}
onMouseOver={(e) => {
setSelectionOption(idx);
}}
onClick={(e) => {
onSelect(option);
e.preventDefault();
}}
>
<span className="user-name">{option.name}</span>
</li>
))
) : (
<h1>No results found!</h1>
)}
{matchingOptions && matchingOptions.length > 0
? matchingOptions.map((option, idx) => (
<div
key={"" + idx}
className={
selectedOption === idx ? "selected-option" : "option"
}
onMouseOver={(e) => {
setSelectionOption(idx);
}}
onClick={(e) => {
e.preventDefault();
onSelect(option);
}}
>
<span className="user-name">{option.name}</span>
{option.hint && <span className="hint">{option.hint}</span>}
</div>
))
: null}
</div>
</div>
);

View File

@ -1,4 +0,0 @@
import { Tag } from '@codemirror/highlight';
export const WikiLinkTag = Tag.define();
export const TagTag = Tag.define();
export const MentionTag = Tag.define();

View File

@ -1,24 +0,0 @@
export class HttpFileSystem {
constructor(url) {
this.url = url;
}
async listNotes() {
let req = await fetch(this.url, {
method: 'GET'
});
return (await req.json()).map((name) => ({ name }));
}
async readNote(name) {
let req = await fetch(`${this.url}/${name}`, {
method: 'GET'
});
return await req.text();
}
async writeNote(name, text) {
let req = await fetch(`${this.url}/${name}`, {
method: 'PUT',
body: text
});
await req.text();
}
}

View File

@ -1,7 +1,6 @@
export interface NoteMeta {
name: string;
}
import { NoteMeta } from "./types";
export interface FileSystem {
listNotes(): Promise<NoteMeta[]>;

View File

@ -8,5 +8,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body></body>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1,45 +0,0 @@
import { syntaxTree } from '@codemirror/language';
import { Decoration, ViewPlugin } from '@codemirror/view';
function wrapLines(view, wrapElements) {
let widgets = [];
for (let { from, to } of view.visibleRanges) {
const doc = view.state.doc;
syntaxTree(view.state).iterate({
from, to,
enter: (type, from, to) => {
const bodyText = doc.sliceString(from, to);
// console.log("Enter", type.name, bodyText);
for (let wrapElement of wrapElements) {
if (type.name == wrapElement.selector) {
const bodyText = doc.sliceString(from, to);
// console.log("Found", type.name, "with: ", bodyText);
let idx = from;
for (let line of bodyText.split("\n")) {
widgets.push(Decoration.line({
class: wrapElement.class,
}).range(doc.lineAt(idx).from));
idx += line.length + 1;
}
}
}
},
leave(type, from, to) {
// console.log("Leaving", type.name);
}
});
}
// console.log("All widgets", widgets);
return Decoration.set(widgets);
}
export const lineWrapper = (wrapElements) => ViewPlugin.fromClass(class {
constructor(view) {
this.decorations = wrapLines(view, wrapElements);
}
update(update) {
if (update.docChanged || update.viewportChanged) {
this.decorations = wrapLines(update.view, wrapElements);
}
}
}, {
decorations: v => v.decorations,
});

View File

@ -1,57 +0,0 @@
import { styleTags } from '@codemirror/highlight';
import { commonmark, mkLang } from "./markdown/markdown";
import * as ct from './customtags';
const WikiLink = {
defineNodes: ["WikiLink"],
parseInline: [{
name: "WikiLink",
parse(cx, next, pos) {
let match;
if (next != 91 /* '[' */ || !(match = /^\[[^\]]+\]\]/.exec(cx.slice(pos + 1, cx.end)))) {
return -1;
}
return cx.addElement(cx.elt("WikiLink", pos, pos + 1 + match[0].length));
},
after: "Emphasis"
}]
};
const AtMention = {
defineNodes: ["AtMention"],
parseInline: [{
name: "AtMention",
parse(cx, next, pos) {
let match;
if (next != 64 /* '@' */ || !(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))) {
return -1;
}
return cx.addElement(cx.elt("AtMention", pos, pos + 1 + match[0].length));
},
after: "Emphasis"
}]
};
const TagLink = {
defineNodes: ["TagLink"],
parseInline: [{
name: "TagLink",
parse(cx, next, pos) {
let match;
if (next != 35 /* '#' */ || !(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))) {
return -1;
}
return cx.addElement(cx.elt("TagLink", pos, pos + 1 + match[0].length));
},
after: "Emphasis"
}]
};
const WikiMarkdown = commonmark.configure([WikiLink, AtMention, TagLink, {
props: [
styleTags({
WikiLink: ct.WikiLinkTag,
AtMention: ct.MentionTag,
TagLink: ct.TagTag,
})
]
}]);
/// Language support for [GFM](https://github.github.com/gfm/) plus
/// subscript, superscript, and emoji syntax.
export default mkLang(WikiMarkdown);

38
webapp/src/reducer.ts Normal file
View File

@ -0,0 +1,38 @@
import { Action, AppViewState } from "./types";
export default function reducer(state: AppViewState, action: Action): AppViewState {
console.log("Got action", action)
switch (action.type) {
case "loaded":
return {
...state,
currentNote: action.name,
isSaved: true,
};
case "saved":
return {
...state,
isSaved: true,
};
case "updated":
return {
...state,
isSaved: false,
};
case "start-navigate":
return {
...state,
isFiltering: true,
};
case "stop-navigate":
return {
...state,
isFiltering: false,
};
case "notes-list":
return {
...state,
allNotes: action.notes,
};
}
}

View File

@ -1,31 +0,0 @@
import { HighlightStyle, tags as t } from '@codemirror/highlight';
import * as ct from './customtags';
export default HighlightStyle.define([
{ tag: t.heading1, class: "h1" },
{ tag: t.heading2, class: "h2" },
{ tag: t.link, class: "link" },
{ tag: t.meta, class: "meta" },
{ tag: t.quote, class: "quote" },
{ tag: t.monospace, class: "code" },
{ tag: t.url, class: "url" },
{ tag: ct.WikiLinkTag, class: "wiki-link" },
{ tag: ct.TagTag, class: "tag" },
{ tag: ct.MentionTag, class: "mention" },
{ tag: t.emphasis, class: "emphasis" },
{ tag: t.strong, class: "strong" },
{ tag: t.atom, class: "atom" },
{ tag: t.bool, class: "bool" },
{ tag: t.url, class: "url" },
{ tag: t.inserted, class: "inserted" },
{ tag: t.deleted, class: "deleted" },
{ tag: t.literal, class: "literal" },
{ tag: t.list, class: "list" },
{ tag: t.definition, class: "li" },
{ tag: t.string, class: "string" },
{ tag: t.number, class: "number" },
{ tag: [t.regexp, t.escape, t.special(t.string)], class: "string2" },
{ tag: t.variableName, class: "variableName" },
{ tag: t.comment, class: "comment" },
{ tag: t.invalid, class: "invalid" },
{ tag: t.punctuation, class: "punctuation" }
]);

View File

@ -5,25 +5,31 @@ body {
padding: 0;
}
body {
display: flex;
flex-direction: column;
}
#top {
height: 40px;
background-color: #eee;
position: absolute;
top: 0;
left: 0;
right: 0;
}
#bottom {
height: 40px;
background-color: #eee;
margin: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
}
#editor {
flex-grow: 1;
width: 100%;
position: absolute;
top: 40px;
bottom: 40px;
left: 0;
right: 0;
overflow-y: hidden;
}
@ -39,7 +45,7 @@ body {
.cm-editor .cm-content {
font-family: "Menlo";
margin: 25px;
margin: 5px;
}
.cm-editor .cm-selectionBackground {
@ -142,15 +148,46 @@ reach-portal > div > div {
padding: 5px;
}
.current-note {
font-family: "Menlo";
margin-left: 10px;
margin-top: 10px;
font-weight: bold;
}
.filter-container {
font-family: "Menlo";
background-color: white;
display: block;
position: absolute;
left: 10px;
top: 10px;
right: 10px;
z-index: 1000;
border: #333 1px solid;
}
.filter-container input {
font-family: "Menlo";
width: 100%;
/* border: 1px #333 solid; */
border: 0;
border-bottom: 1px #333 dotted;
padding: 3px;
}
.filter-container .option,
.filter-container .selected-option {
padding: 3px 3px 3px 3px;
cursor: pointer;
}
.filter-container .selected-option {
background-color: #520130;
background-color: #b1b1b1;
}
.filter-container .hint {
float: right;
margin-right: 10px;
color: #333;
}

20
webapp/src/types.ts Normal file
View File

@ -0,0 +1,20 @@
export type NoteMeta = {
name: string;
};
export type AppViewState = {
currentNote: string;
isSaved: boolean;
isFiltering: boolean;
allNotes: NoteMeta[];
};
export type Action =
| { type: "loaded"; name: string }
| { type: "saved" }
| { type: "start-navigate" }
| { type: "stop-navigate" }
| { type: "updated" }
| { type: "notes-list"; notes: NoteMeta[] };