1
0

Getting there

This commit is contained in:
Zef Hemel 2022-02-22 14:18:37 +01:00
parent c9f4266d34
commit 84fcd0aeed
23 changed files with 651 additions and 533 deletions

BIN
.DS_Store vendored

Binary file not shown.

2
.gitignore vendored
View File

@ -2,4 +2,4 @@
/webapp/dist/
/.parcel-cache/
/.idea
/notes

View File

@ -8,6 +8,12 @@
}
],
"settings": {
"editor.formatOnSave": true
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"javascript.format.enable": false,
"typescript.format.enable": false,
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
}

View File

@ -1 +1,2 @@
Yo!!

View File

@ -1,144 +1,3 @@
# Great Parenting!
> “Zef, you keep talking about all these amazing management concepts. You are an inspiration to all. However, what I really want to know is how you apply your enlightened ideas to being a parent!” — Nobody, Ever
Home page
Dear mister or miss “Ever” — if this is in fact your real name. Im so glad you asked!
For the rest of you, if you are not a parent, have no plan to be, or have absolutely zero interest in my philosophy of parenting — the latter seems unlikely to me, but I want to call out anyway — feel free to stop reading.
“So, do I need your permission to stop reading now? I will decide that myself, thank you very much!”
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
-----
Before we start, why would you even listen to me? Lets [[be honest]] here, I have no relevant credentials beyond being a dad myself. However, has that ever stopped anybody from doing anything? As any conspiracy theorist would say: you dont need to take my word for it, do your own research — Im just asking questions!
That said, its worth knowing I have three kids. All boys. One eight-year old, and five-year-old twins.
I know what youre thinking.
Im a former academic, Im not blind to it. My familys twin boys situation makes for a great **A/B testing opportunity**. So lets address that right off the bat.
With two boys of the same age (although one will gladly point out that he was pulled out 2 minutes earlier, so hes the older one), we could apply one type of experimental parenting technique to one of the twins, and another to the second one.
Sadly, there are a few reasons that make this a no go:
1. **Moral:** we have fairly strong sense how we want to raise our kids, and Im afraid that the argument “sorry, you were the control group” is not going to fly thirty years down the road — as the “B” kid inevitably work through his dysfunctions with his psychologist.
2. **Variance:** even without A/B test our twins are shockingly different. We try to apply a similar approach to parenting with all our kids, still the twins have extremely different behavior. It is unlikely we would be able to tell, thirty years down the line, which parts of their life are screwed up due to our parenting experiment, and which ones are due to how they already were when they popped out. Although, obviously, we will claim full credit for everything if they turn out successful.
3. **Statistical significance:** two boys is a tiny sample size. To get anywhere, we would need a larger sample. Maybe a hundred kids, ideally twined up. My wife has vetoed this idea.
So no A/B testing with our kids. It is what it is.
Instead, we will just have to go with our gut here, or make an educated guess. And with that, let me put in a disclaimer: if you decide to take any of this seriously, its at your own risk. Dont come to me in thirty years saying “wow man, we really screwed up my kid with your advice.” Likely Ill be in the same boat.
Do your own research.
---- -
However, if youre going to severely limit your own research and just have budgeted one book to read, I would recommend that book to be [Unconditional Parenting](https://amzn.to/3rWwLT4) by *Alfie Kohn*.
You may remember that I brought up Kohns work before. Hes also the guy who wrote [Punished By Rewards](https://amzn.to/3HcBeWd) (which I spoke about at length in [No More Rewards](https://zef.plus/no-more-rewards/)) and [No Contest](https://amzn.to/3LKlgFU) (which I still have to write up). Hes one of my heroes in many ways. He has contrarian ideas, backed up with so much data and research that you can only conclude: oh wow, how could we have been doing this wrong for so long?
If you have [No More Rewards](https://zef.plus/no-more-rewards/) fresh in your head (as you should), you will actually not be surprised by the parenting approach proposed by Kohn at all. This is to be expected, because, spoiler alert: kids are people too.
I believe that a lot of challenges people face later in life can be traced back to how they were raised. Everything can be explained by trauma. Read [The Laws of Human Nature](https://amzn.to/3uZIdPz), and you will learn that for a huge amount of “dysfunctions” in people can be traced back to what their context looked like in their early years. Which, incidentally, is another reason to put at least some effort into this whole parenting topic. At least finish this post, is that too much to ask?
Enough prelude.
----
So, what does Kohn suggest? The way I would frame it (although Kohn does not) is to *work backwards.* Indeed, very Amazonian. Think about what we like our kids to be like when they grow up, and then create a context for them today in which is it is likely they get there.
What should our kids be like when theyre teenagers, in their twenties and beyond? For many of us, things that will jump to mind will include that they should be **independent**, **creative**, and **confident**. And rich, of course, so they pay us back all that money we invested in them. Do you have any idea what I cost per hour? With interest and taking into account inflation please.
Great. So lets see what we can do *now* to help them get there.
Lets start with a case study.
![](IMG_0087.jpeg)
Meet Leo (picture from a few years back). Leo is the 2-minute-younger twin. Cute, I know.
What is he doing? Hes showing us a drawing he made.
How do we respond to this masterpiece?
What most of us intuitively would do is is say something along the lines of “Thats beautiful!” “Great job!” “Lets put that on the fridge!”
We had mentioned that we want our kids to grow up **confident** so this strategy seems to make sense. We help Leo to feel confident by praising his work. Youre awesome, because you drew an awesome bear (I think thats a bear, at least).
However, lets take a step back. Why do you think Leo is showing us this picture? Is he seeking praise? Approval? Does he need *us* (his parents) to feel confident about himself?
Leo is comfortable. He (rightfully) believes he is loved, and whatever he does wont change that. Hes a kid, motivated to explore the world and to master his skills. As he progresses, he enjoys sharing this progress with the most important people in his life: us (for now). He can judge himself that hes making progress (the bear from a month ago looked more like an elephant), he is confident, he has no reason not to be.
But then, we say “good job, thats a beautiful drawing!” We offer to put the drawing on the fridge. He may get an ice cream, because we love the drawing that much.
Were now changing the game.
Whereas Leo was purely *intrinsically motivated* before (and **self**-confident as a result), weve now (accidentally) taken a step to *extrinsically motivate* him. We have taken a (small) step to make his confidence dependent on external factors: youre awesome, **because** you drew a bear that was qualified by your parents as “awesome.” He now starts to learn that the world is judging him, and that he should care what people think (its coming from his parents after all). And… hes liking it, because he gets smiles, cheers and ice cream.
----
Lets recap this whole [intrinsic vs. extrinsic motivation](https://zef.plus/no-more-rewards/) topic we spoke about in the past.
[Self-determination theory](https://en.wikipedia.org/wiki/Self-determination_theory) says there are two types of motivation with very different properties:
1. **Intrinsic motivation** is the natural, inherent drive to seek out challenges and new possibilities. Intrinsic motivation is present naturally. It does not need constant reinforcement, it persists unless actively pushed out.
2. **Extrinsic motivation** comes from external sources, for kids this can come in the shape of praise, stickers, ice cream. Compared to intrinsic motivation, extrinsic motivation is easier to control from the outside (hence extrinsic), but it is also short lived — it requires constant *reinforcement*.
Let this be one of few cases where I express clear judgment: *intrinsic* motivation is the good kind, thats what we want. In our kids; in the people we work with. Extrinsic motivation is *convenient* (we can easily use it to get people to do what we want), but its not long-term sustainable.
Intrinsic motivation comes from three basic desires that are just as applicable to kids as they are to adults:
1. **Autonomy** — our ability to have control our own acts and life.
2. **Competence** — our need to control outcomes and experience mastery.
3. **Relatedness** our will to interact with, be connected to others (a sense *belonging* and *purpose*).
Growing up is all about these things: gaining *autonomy* and *competence* by being able to do more and more things yourself (eating, walking, drawing, writing, reading), all the while being *connected* with the people most important to us (our parents).
----
By praising Leo we are not only at risk replacing intrinsic motivation with extrinsic motivation, we are also reducing his autonomy. Leos sense of self worth and confidence becomes a bit more **dependent** on what we (his parents) think. For now he still enjoys drawing, but he starts to be worried whether his parents will like *this* drawing just as much or more than the *previous* one. He liked getting that praise, and seeing all his drawings on the fridge feels good. He wants more of that. Its kind of addictive. To ensure another reward, hes likely to play it safe and draw something similar (bye bye **creativity** — which is all about feeling the space to take risk without repercussions).
Leo makes another drawing. A bit quicker this time, with less effort. The quality is worse (a [known issue with extrinsically motivated work](https://zef.plus/no-more-rewards/)). We dont respond as enthusiastically as before. Did he do something wrong? Do his parents still love him just as much?
This doesnt seem like a great strategy working towards independent, creative, confident children.
Lets try a different strategy. A strategy not dependent on rewards.
----
“Oh, you drew a bear and used multiple colors this time! Show me how you did that!”
We sit with Leo as he shows off his box of crayons, and offer suggestions on how to use the colors to make the next bear look even less like an elephant.
Absolutely, it is higher effort and time consuming than clamoring “great drawing, heres an ice cream.” However, Leo still feels important because we spend time with him (boosting his confidence). Also, we dont make him dependent on our judgment, nor block his creativity. Great!
Some other scenarios where praise would be an obvious response, but we approach things differently:
When our older son proudly shared he finished his first *Harry Potter* book (he has read it both in Polish and Dutch, not to brag or anything), we didnt praise his reading skills. We didnt reward him for it. Kohn, in his *Punished By Rewards* books cites a piece of research where they tried to “incentivize” reading by giving school kids money rewards for every book they read. The result: they read a lot more… thin books with lots of pictures, with very low recall of what they were about. The kids stopped reading the second the rewards stopped. So it worked, kinda, I suppose? But not really.
So we dont do that. When my son fishes a book, we say that we can tell he really enjoys reading, and is getting better at it (mastery), why this matters (purpose), and then buy him the next *Harry Potter* book. Not as a reward, but as a means to support his mastery and joy of reading.
Its a struggle. It often feels weird. But we feel its right.
----
Lets put this all in perspective. Does this mean that if you say “good job!” you will turn your kids into compliant zombies that will no longer do anything unless they get a sticker? While Kohn describes one case in his book where kids were “nudged” with external motivators to such a level that they lost all sense of self, this is very extreme.
Chances are high that you, like most of us, have received your fair share of praise and sticker equivalents growing up. And you turned out alright. For some definition of alright, anyway.
However, if I were to go out on a limb, I could [ascribe events like the 2008 financial crisis](https://zef.plus/no-more-rewards/) to enough parents and teachers using their gold stars to motivate their kids, training them to be driven by extrinsic motivators like *bonuses* later in life. Could that be a big contributing factor? Did gold stars in school, and parents cheering “nice drawing” result in a financial crisis? I cant prove that. Im just asking questions.
However, what is becoming very visible is that our world is getting more challenging in this sense. For “kids these days” so [much of their self worth](https://www.netflix.com/title/81254224) is determined by how many *likes* they get on facebook. Kids are hammered by social platforms with external motivators. They post a selfie, and stressfully monitor how many hearts they get compared to their friends, and how many comments. How many followers do they have?
If we can do anything to equip our kids to be resistant to this type of stuff in the future, Im eager to do it.
Im not so naive to believe that Ive now completely convinced you ought to completely change your approach to parenting tomorrow. Next week is fine too. After that your kids will be doomed though. Just saying.
Somewhat more seriously, go do your own research… by reading that [Unconditional Parenting](https://amzn.to/3rWwLT4) book.
No more rewards. Even for our kids.
[[Great Parenting]]

View File

@ -4,27 +4,32 @@ import FileInfo = Deno.FileInfo;
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
import { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts";
import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts";
import { exists } from "https://deno.land/std@0.126.0/fs/mod.ts";
const fsPrefix = '/fs';
const notesPath = '../notes';
const fsPrefix = "/fs";
const notesPath = "../notes";
const fsRouter = new Router();
fsRouter.use(oakCors());
fsRouter.get('/', async context => {
fsRouter.get("/", async (context) => {
const localPath = notesPath;
let fileNames: string[] = [];
for await (const dirEntry of Deno.readDir(localPath)) {
if (dirEntry.isFile) {
fileNames.push(dirEntry.name.substring(0, dirEntry.name.length - path.extname(dirEntry.name).length));
fileNames.push(
dirEntry.name.substring(
0,
dirEntry.name.length - path.extname(dirEntry.name).length
)
);
}
}
context.response.body = JSON.stringify(fileNames);
});
fsRouter.get('/:note', async context => {
fsRouter.get("/:note", async (context) => {
const noteName = context.params.note;
const localPath = `${notesPath}/${noteName}.md`;
try {
@ -36,21 +41,21 @@ fsRouter.get('/:note', async context => {
}
});
fsRouter.options('/:note', async context => {
fsRouter.options("/:note", async (context) => {
const localPath = `${notesPath}/${context.params.note}.md`;
try {
const stat = await Deno.stat(localPath);
context.response.headers.set('Content-length', `${stat.size}`);
context.response.headers.set("Content-length", `${stat.size}`);
} catch (e) {
context.response.status = 200;
context.response.body = "";
}
})
});
fsRouter.put('/:note', async context => {
fsRouter.put("/:note", async (context) => {
const noteName = context.params.note;
const localPath = `${notesPath}/${noteName}.md`;
const existingNote = await exists(localPath);
let file;
try {
file = await Deno.create(localPath);
@ -64,17 +69,22 @@ fsRouter.put('/:note', async context => {
const text = await readAll(result.value);
file.write(text);
file.close();
console.log("Wrote to", localPath)
console.log("Wrote to", localPath);
context.response.status = existingNote ? 200 : 201;
context.response.body = "OK";
});
const app = new Application();
app.use(new Router().use(fsPrefix, fsRouter.routes(), fsRouter.allowedMethods()).routes());
app.use(
new Router()
.use(fsPrefix, fsRouter.routes(), fsRouter.allowedMethods())
.routes()
);
app.use(async (context, next) => {
try {
await context.send({
root: '../webapp/dist',
index: 'index.html'
root: "../webapp/dist",
index: "index.html",
});
} catch {
next();

2
server/test.ts Normal file
View File

@ -0,0 +1,2 @@
import { parser } from "https://unpkg.com/@lezer/markdown?module";
console.log(parser);

View File

@ -6,7 +6,8 @@
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "parcel",
"build": "parcel build"
"build": "parcel build",
"check-watch": "tsc --noEmit --watch"
},
"devDependencies": {
"@parcel/validator-typescript": "^2.3.2",
@ -21,7 +22,6 @@
"@codemirror/lang-markdown": "^0.19.6",
"@codemirror/state": "^0.19.7",
"@codemirror/view": "^0.19.42",
"kbar": "^0.1.0-beta.27",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}

View File

@ -16,23 +16,26 @@ import {
import React, { useEffect, useReducer, useRef } from "react";
import ReactDOM from "react-dom";
import * as commands from "./commands";
import { CommandPalette } from "./components/commandpalette";
import { NoteNavigator } from "./components/notenavigator";
import { HttpFileSystem } from "./fs";
import { lineWrapper } from "./lineWrapper";
import { markdown } from "./markdown";
import customMarkDown from "./parser";
import customMarkdownStyle from "./style";
import { FilterList } from "./components/filter";
import { NoteMeta, AppViewState, Action } from "./types";
import reducer from "./reducer";
import customMarkdownStyle from "./style";
import { Action, AppViewState } from "./types";
import { syntaxTree } from "@codemirror/language";
import * as util from "./util";
const fs = new HttpFileSystem("http://localhost:2222/fs");
const initialViewState = {
const initialViewState: AppViewState = {
currentNote: "",
isSaved: false,
isFiltering: false,
showNoteNavigator: false,
showCommandPalette: false,
allNotes: [],
};
@ -56,6 +59,7 @@ class Editor {
}
createEditorState(text: string): EditorState {
const editor = this;
return EditorState.create({
doc: text,
extensions: [
@ -107,6 +111,25 @@ class Editor {
return true;
},
},
{
key: "Ctrl-Enter",
mac: "Cmd-Enter",
run: (target): boolean => {
// TODO: Factor this and click handler into one action
let selection = target.state.selection.main;
if (selection.empty) {
let node = syntaxTree(target.state).resolveInner(
selection.from
);
if (node && node.name === "WikiLinkPage") {
let noteName = target.state.sliceDoc(node.from, node.to);
this.navigate(noteName);
return true;
}
}
return false;
},
},
{
key: "Ctrl-p",
mac: "Cmd-p",
@ -115,6 +138,14 @@ class Editor {
return true;
},
},
{
key: "Ctrl-.",
mac: "Cmd-.",
run: (target): boolean => {
this.dispatch({ type: "show-palette" });
return true;
},
},
]),
EditorView.domEventHandlers({
click: this.click.bind(this),
@ -133,7 +164,7 @@ class Editor {
update(value: null, transaction: Transaction): null {
if (transaction.docChanged) {
this.dispatch({
type: "updated",
type: "note-updated",
});
}
@ -147,16 +178,34 @@ class Editor {
click(event: MouseEvent, view: EditorView) {
if (event.metaKey || event.ctrlKey) {
console.log("Navigate click");
let coords = view.posAtCoords(event);
console.log("Coords", view.state.doc.sliceString(coords!, coords! + 1));
let coords = view.posAtCoords(event)!;
let node = syntaxTree(view.state).resolveInner(coords);
if (node && node.name === "WikiLinkPage") {
let noteName = view.state.sliceDoc(node.from, node.to);
this.navigate(noteName);
}
return false;
}
}
async save() {
await fs.writeNote(this.currentNote, this.view.state.sliceDoc());
this.dispatch({ type: "saved" });
const created = await fs.writeNote(
this.currentNote,
this.view.state.sliceDoc()
);
this.dispatch({ type: "note-saved" });
// If a new note was created, let's refresh the note list
if (created) {
await this.loadNoteList();
}
}
async loadNoteList() {
let notesMeta = await fs.listNotes();
this.dispatch({
type: "notes-listed",
notes: notesMeta,
});
}
focus() {
@ -168,43 +217,38 @@ class Editor {
}
}
function TopBar({
let editor: Editor | null;
function NavigationBar({
currentNote,
isSaved,
isFiltering,
allNotes,
onNavigate,
onClick,
}: {
currentNote: string;
isSaved: boolean;
isFiltering: boolean;
allNotes: NoteMeta[];
onNavigate: (note: string | undefined) => void;
onClick: () => void;
}) {
return (
<div id="top">
<div className="current-note" onClick={onClick}>
» {currentNote}
{isSaved ? "" : "*"}
</div>
{isFiltering && (
<FilterList
initialText=""
options={allNotes}
onSelect={(opt) => {
console.log("Selected", opt);
onNavigate(opt?.name);
}}
></FilterList>
)}
</div>
);
}
let editor: Editor | null;
function StatusBar({ isSaved }: { isSaved: boolean }) {
let wordCount = 0,
readingTime = 0;
if (editor) {
let text = editor.view.state.sliceDoc();
wordCount = util.countWords(text);
readingTime = util.readingTime(wordCount);
}
return (
<div id="bottom">
{wordCount} words | {readingTime} min | {isSaved ? "Saved" : "Edited"}
</div>
);
}
function AppView() {
const editorRef = useRef<HTMLDivElement>(null);
@ -221,16 +265,21 @@ function AppView() {
}, []);
useEffect(() => {
fs.listNotes()
.then((notes) => {
dispatch({
type: "notes-list",
notes: notes,
});
})
.catch((e) => console.error(e));
editor?.loadNoteList();
}, []);
// Auto save
useEffect(() => {
const id = setTimeout(() => {
if (!appState.isSaved) {
editor?.save();
}
}, 2000);
return () => {
clearTimeout(id);
};
}, [appState.isSaved]);
useEffect(() => {
function hashChange() {
const noteName = decodeURIComponent(location.hash.substring(1));
@ -240,7 +289,7 @@ function AppView() {
.then((text) => {
editor!.load(noteName, text);
dispatch({
type: "loaded",
type: "note-loaded",
name: noteName,
});
})
@ -257,24 +306,45 @@ function AppView() {
return (
<>
<TopBar
currentNote={appState.currentNote}
isSaved={appState.isSaved}
isFiltering={appState.isFiltering}
{appState.showNoteNavigator && (
<NoteNavigator
allNotes={appState.allNotes}
onClick={() => {
dispatch({ type: "start-navigate" });
}}
onNavigate={(note) => {
dispatch({ type: "stop-navigate" });
editor!.focus();
if (note) {
editor
?.save()
.then(() => {
editor!.navigate(note);
})
.catch((e) => {
alert("Could not save note, not switching");
});
}
}}
/>
)}
{appState.showCommandPalette && (
<CommandPalette
onTrigger={(cmd) => {
dispatch({ type: "hide-palette" });
editor!.focus();
if (cmd) {
console.log("Run", cmd);
}
}}
commands={[{ name: "My command", run: () => {} }]}
/>
)}
<NavigationBar
currentNote={appState.currentNote}
onClick={() => {
dispatch({ type: "start-navigate" });
}}
/>
<div id="editor" ref={editorRef}></div>
<div id="bottom">Bottom</div>
<StatusBar isSaved={appState.isSaved} />
</>
);
}

View File

@ -1,47 +1,60 @@
import { EditorSelection, EditorState, StateCommand, Transaction } from "@codemirror/state";
import { EditorSelection, StateCommand, Transaction } from "@codemirror/state";
import { Text } from "@codemirror/text";
export function insertMarker(marker: string): StateCommand {
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 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 ? {
changes.push(
isBoldBefore
? {
from: range.from - marker.length,
to: range.from,
insert: Text.of([''])
} : {
insert: Text.of([""]),
}
: {
from: range.from,
insert: Text.of([marker]),
})
}
);
changes.push(isBoldAfter ? {
changes.push(
isBoldAfter
? {
from: range.to,
to: range.to + marker.length,
insert: Text.of([''])
} : {
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),
}
})
range: EditorSelection.range(
range.from + extendBefore,
range.to + extendAfter
),
};
});
dispatch(
state.update(changes, {
scrollIntoView: true,
annotations: Transaction.userEvent.of('input'),
annotations: Transaction.userEvent.of("input"),
})
)
);
return true
return true;
};
}

View File

@ -0,0 +1,21 @@
import { AppCommand } from "../types";
import { FilterList } from "./filter";
export function CommandPalette({
commands,
onTrigger,
}: {
commands: AppCommand[];
onTrigger: (command: AppCommand) => void;
}) {
return (
<FilterList
placeholder="Enter command to run"
options={commands}
allowNew={false}
onSelect={(opt) => {
onTrigger(opt as AppCommand);
}}
/>
);
}

View File

@ -1,41 +1,63 @@
import React, { useEffect, useRef, useState } from "react";
type Option = {
export interface Option {
name: string;
hint?: string;
};
}
function magicSorter(a: Option, b: Option): number {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
} else {
return 1;
}
}
export function FilterList({
initialText,
placeholder,
options,
onSelect,
allowNew = false,
newHint,
}: {
initialText: string;
placeholder: string;
options: Option[];
onSelect: (option: Option | undefined) => void;
allowNew?: boolean;
newHint?: string;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const [text, setText] = useState(initialText);
const [matchingOptions, setMatchingOptions] = useState(options);
const [text, setText] = useState("");
const [matchingOptions, setMatchingOptions] = useState(
options.sort(magicSorter)
);
const [selectedOption, setSelectionOption] = useState(0);
let selectedElementRef = useRef<HTMLDivElement>(null);
const filter = (e: React.ChangeEvent<HTMLInputElement>) => {
const originalPhrase = e.target.value;
const searchPhrase = originalPhrase.toLowerCase();
if (searchPhrase) {
let foundExactMatch = false;
let results = options.filter((option) => {
if (option.name.toLowerCase() === searchPhrase) {
foundExactMatch = true;
}
return option.name.toLowerCase().indexOf(searchPhrase) !== -1;
});
results.splice(0, 0, {
results = results.sort(magicSorter);
if (allowNew && !foundExactMatch) {
results.push({
name: originalPhrase,
hint: "Create new",
hint: newHint,
});
}
setMatchingOptions(results);
} else {
setMatchingOptions(options);
let results = options.sort(magicSorter);
setMatchingOptions(results);
}
setText(originalPhrase);
@ -58,11 +80,12 @@ export function FilterList({
};
}, []);
return (
const returEl = (
<div className="filter-container">
<input
type="text"
value={text}
placeholder={placeholder}
ref={searchBoxRef}
onChange={filter}
onKeyDown={(e: React.KeyboardEvent) => {
@ -86,7 +109,6 @@ export function FilterList({
}
}}
className="input"
placeholder=""
/>
<div className="result-list">
@ -94,6 +116,7 @@ export function FilterList({
? matchingOptions.map((option, idx) => (
<div
key={"" + idx}
ref={selectedOption === idx ? selectedElementRef : undefined}
className={
selectedOption === idx ? "selected-option" : "option"
}
@ -113,4 +136,12 @@ export function FilterList({
</div>
</div>
);
useEffect(() => {
selectedElementRef.current?.scrollIntoView({
block: "nearest",
});
});
return returEl;
}

View File

@ -0,0 +1,22 @@
import { NoteMeta } from "../types";
import { FilterList } from "./filter";
export function NoteNavigator({
allNotes,
onNavigate,
}: {
allNotes: NoteMeta[];
onNavigate: (note: string | undefined) => void;
}) {
return (
<FilterList
placeholder=""
options={allNotes}
allowNew={true}
newHint="Create note"
onSelect={(opt) => {
onNavigate(opt?.name);
}}
/>
);
}

View File

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

View File

@ -1,37 +1,36 @@
import { NoteMeta } from "./types";
export interface FileSystem {
listNotes(): Promise<NoteMeta[]>;
readNote(name: string): Promise<string>;
writeNote(name: string, text: string): Promise<void>;
// @return whether a new note was created for this
writeNote(name: string, text: string): Promise<boolean>;
}
export class HttpFileSystem implements FileSystem {
url: string;
constructor(url: string) {
this.url = url;
}
async listNotes(): Promise<NoteMeta[]> {
let req = await fetch(this.url, {
method: 'GET'
method: "GET",
});
return (await req.json()).map((name: string) => ({ name }));
}
async readNote(name: string): Promise<string> {
let req = await fetch(`${this.url}/${name}`, {
method: 'GET'
method: "GET",
});
return await req.text();
}
async writeNote(name: string, text: string): Promise<void> {
async writeNote(name: string, text: string): Promise<boolean> {
let req = await fetch(`${this.url}/${name}`, {
method: 'PUT',
body: text
method: "PUT",
body: text,
});
await req.text();
// 201 (Created) means a new note was created
return req.status === 201;
}
}

View File

@ -1,22 +1,26 @@
import { syntaxTree } from '@codemirror/language';
import { syntaxTree } from "@codemirror/language";
import {
Decoration,
DecorationSet, EditorView, ViewPlugin,
ViewUpdate
} from '@codemirror/view';
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
} from "@codemirror/view";
import { Range } from '@codemirror/rangeset';
import { Range } from "@codemirror/rangeset";
interface WrapElement {
selector: string;
class: string;
}
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
let widgets: Range<Decoration>[] = [];
for (let { from, to } of view.visibleRanges) {
const doc = view.state.doc;
syntaxTree(view.state).iterate({
from, to,
from,
to,
enter: (type, from, to) => {
const bodyText = doc.sliceString(from, to);
for (let wrapElement of wrapElements) {
@ -24,22 +28,28 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
const bodyText = doc.sliceString(from, to);
let idx = from;
for (let line of bodyText.split("\n")) {
widgets.push(Decoration.line({
widgets.push(
Decoration.line({
class: wrapElement.class,
}).range(doc.lineAt(idx).from));
}).range(doc.lineAt(idx).from)
);
idx += line.length + 1;
}
}
}
},
leave(type, from: number, to: number) {
}
leave(type, from: number, to: number) {},
});
}
// console.log("All widgets", widgets);
// Widgets have to be sorted by `from` in ascending order
widgets = widgets.sort((a, b) => {
return a.from < b.from ? -1 : 1;
});
return Decoration.set(widgets);
}
export const lineWrapper = (wrapElements: WrapElement[]) => ViewPlugin.fromClass(class {
export const lineWrapper = (wrapElements: WrapElement[]) =>
ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
@ -51,6 +61,8 @@ export const lineWrapper = (wrapElements: WrapElement[]) => ViewPlugin.fromClass
this.decorations = wrapLines(update.view, wrapElements);
}
}
}, {
decorations: v => v.decorations,
});
},
{
decorations: (v) => v.decorations,
}
);

View File

@ -1,60 +1,86 @@
import { styleTags } from '@codemirror/highlight';
import { styleTags } from "@codemirror/highlight";
import { MarkdownConfig } from "@lezer/markdown";
import { commonmark, mkLang } from "./markdown/markdown";
import * as ct from './customtags';
import * as ct from "./customtags";
const WikiLink: MarkdownConfig = {
defineNodes: ["WikiLink"],
parseInline: [{
defineNodes: ["WikiLink", "WikiLinkPage"],
parseInline: [
{
name: "WikiLink",
parse(cx, next, pos) {
let match: RegExpMatchArray | null;
if (next != 91 /* '[' */ || !(match = /^\[[^\]]+\]\]/.exec(cx.slice(pos + 1, cx.end)))) {
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));
return cx.addElement(
cx.elt("WikiLink", pos, pos + match[0].length + 1, [
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 1),
])
);
},
after: "Emphasis"
}]
after: "Emphasis",
},
],
};
const AtMention: MarkdownConfig = {
defineNodes: ["AtMention"],
parseInline: [{
parseInline: [
{
name: "AtMention",
parse(cx, next, pos) {
let match: RegExpMatchArray | null;
if (next != 64 /* '@' */ || !(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))) {
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));
return cx.addElement(
cx.elt("AtMention", pos, pos + 1 + match[0].length)
);
},
after: "Emphasis"
}]
after: "Emphasis",
},
],
};
const TagLink: MarkdownConfig = {
defineNodes: ["TagLink"],
parseInline: [{
parseInline: [
{
name: "TagLink",
parse(cx, next, pos) {
let match: RegExpMatchArray | null;
if (next != 35 /* '#' */ || !(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))) {
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"
}]
after: "Emphasis",
},
],
};
const WikiMarkdown = commonmark.configure([WikiLink, AtMention, TagLink, {
const WikiMarkdown = commonmark.configure([
WikiLink,
AtMention,
TagLink,
{
props: [
styleTags({
WikiLink: ct.WikiLinkTag,
WikiLinkPage: ct.WikiLinkPageTag,
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);

View File

@ -1,20 +1,27 @@
import { Action, AppViewState } from "./types";
export default function reducer(state: AppViewState, action: Action): AppViewState {
console.log("Got action", action)
export default function reducer(
state: AppViewState,
action: Action
): AppViewState {
console.log("Got action", action);
switch (action.type) {
case "loaded":
case "note-loaded":
return {
...state,
currentNote: action.name,
isSaved: true,
};
case "saved":
case "note-saved":
return {
...state,
isSaved: true,
};
case "updated":
case "note-updated":
// Minor rerender optimization, this is triggered a lot
if (!state.isSaved) {
return state;
}
return {
...state,
isSaved: false,
@ -22,17 +29,28 @@ export default function reducer(state: AppViewState, action: Action): AppViewSta
case "start-navigate":
return {
...state,
isFiltering: true,
showNoteNavigator: true,
};
case "stop-navigate":
return {
...state,
isFiltering: false,
showNoteNavigator: false,
};
case "notes-list":
case "notes-listed":
return {
...state,
allNotes: action.notes,
};
case "show-palette":
return {
...state,
showCommandPalette: true,
};
case "hide-palette":
return {
...state,
showCommandPalette: false,
};
}
return state;
}

View File

@ -1,5 +1,5 @@
import { HighlightStyle, tags as t } from '@codemirror/highlight';
import * as ct from './customtags';
import { HighlightStyle, tags as t } from "@codemirror/highlight";
import * as ct from "./customtags";
export default HighlightStyle.define([
{ tag: t.heading1, class: "h1" },
@ -10,6 +10,7 @@ export default HighlightStyle.define([
{ tag: t.monospace, class: "code" },
{ tag: t.url, class: "url" },
{ tag: ct.WikiLinkTag, class: "wiki-link" },
{ tag: ct.WikiLinkPageTag, class: "wiki-link-page" },
{ tag: ct.TagTag, class: "tag" },
{ tag: ct.MentionTag, class: "mention" },
{ tag: t.emphasis, class: "emphasis" },
@ -28,5 +29,5 @@ export default HighlightStyle.define([
{ tag: t.variableName, class: "variableName" },
{ tag: t.comment, class: "comment" },
{ tag: t.invalid, class: "invalid" },
{ tag: t.punctuation, class: "punctuation" }
{ tag: t.punctuation, class: "punctuation" },
]);

View File

@ -1,3 +1,10 @@
:root {
--ident: 18px;
--editor-font: "Avenir";
--top-bar-bg: rgb(41, 41, 41);
/* --editor-font: "Menlo"; */
}
html,
body {
height: 100%;
@ -7,21 +14,26 @@ body {
#top {
height: 40px;
background-color: #eee;
background-color: var(--top-bar-bg);
position: absolute;
top: 0;
left: 0;
right: 0;
color: #eee;
}
#bottom {
height: 40px;
background-color: #eee;
margin: 0;
position: absolute;
bottom: 0;
left: 0;
right: 0;
position: absolute;
height: 25px;
background-color: var(--top-bar-bg);
color: #eee;
margin: 0;
padding: 5px;
font-family: var(--editor-font);
text-align: right;
}
#editor {
@ -33,10 +45,6 @@ body {
overflow-y: hidden;
}
:root {
--ident: 18px;
}
.cm-editor {
width: 100%;
height: 100%;
@ -44,7 +52,7 @@ body {
}
.cm-editor .cm-content {
font-family: "Menlo";
font-family: var(--editor-font);
margin: 5px;
}
@ -92,7 +100,7 @@ body {
}
.cm-editor .meta {
color: #520130;
color: #650007;
}
.cm-editor .line-blockquote {
@ -119,9 +127,12 @@ body {
color: #7e7d7d;
}
.cm-editor .wiki-link {
.cm-editor .wiki-link-page {
color: #0330cb;
/*text-decoration: underline;*/
text-decoration: underline;
}
.cm-editor .wiki-link {
color: #808080;
}
.cm-editor .mention {
@ -137,43 +148,40 @@ body {
margin-left: var(--ident);
}
reach-portal input {
background-color: #fff;
border: 0;
}
reach-portal > div > div {
background-color: #fff;
border: #000 1px solid;
padding: 5px;
}
.current-note {
font-family: "Menlo";
font-family: var(--editor-font);
margin-left: 10px;
margin-top: 10px;
font-weight: bold;
}
.filter-container {
font-family: "Menlo";
background-color: white;
font-family: var(--editor-font);
display: block;
position: absolute;
left: 10px;
top: 10px;
right: 10px;
z-index: 1000;
border: #333 1px solid;
z-index: 1000;
position: absolute;
left: 8px;
top: 8px;
right: 10px;
}
.filter-container .result-list {
max-height: 200px;
overflow-y: scroll;
background-color: white;
}
.filter-container input {
font-family: "Menlo";
font-family: var(--editor-font);
width: 100%;
/* border: 1px #333 solid; */
background-color: var(--top-bar-bg);
color: #eee;
border: 0;
border-bottom: 1px #333 dotted;
padding: 3px;
outline: 0;
font-weight: bold;
}
.filter-container .option,

View File

@ -3,18 +3,27 @@ export type NoteMeta = {
name: string;
};
export type AppCommand = {
name: string;
run: () => void;
}
export type AppViewState = {
currentNote: string;
isSaved: boolean;
isFiltering: boolean;
showNoteNavigator: boolean;
showCommandPalette: boolean;
allNotes: NoteMeta[];
};
export type Action =
| { type: "loaded"; name: string }
| { type: "saved" }
| { type: "note-loaded"; name: string }
| { type: "note-saved" }
| { type: "note-updated" }
| { type: "notes-listed"; notes: NoteMeta[] }
| { type: "start-navigate" }
| { type: "stop-navigate" }
| { type: "updated" }
| { type: "notes-list"; notes: NoteMeta[] };
| { type: "show-palette" }
| { type: "hide-palette" }
;

9
webapp/src/util.ts Normal file
View File

@ -0,0 +1,9 @@
export function countWords(str: string): number {
var matches = str.match(/[\w\d\'\'-]+/gi);
return matches ? matches.length : 0;
}
export function readingTime(wordCount: number): number {
// 225 is average word reading speed for adults
return Math.ceil(wordCount / 225);
}