1
0
This commit is contained in:
Zef Hemel 2022-02-21 09:32:36 +01:00
parent 173da28a35
commit af59d394ae
39 changed files with 1937 additions and 798 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
/node_modules/
/dist/
/webapp/node_modules/
/webapp/dist/
/.parcel-cache/
/.idea

13
.vscode/noot.code-workspace vendored Normal file
View File

@ -0,0 +1,13 @@
{
"folders": [
{
"path": ".."
},
{
"path": "../server"
}
],
"settings": {
"editor.formatOnSave": true
}
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

3
notes/A Cool Note.md Normal file
View File

@ -0,0 +1,3 @@
## Lots of
Content #here

141
notes/start.md Normal file
View File

@ -0,0 +1,141 @@
# 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
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.
----
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.

1
notes/test.md Normal file
View File

@ -0,0 +1 @@
Sappie

4
server/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"deno.enable": true,
"deno.unstable": true
}

74
server/server.ts Normal file
View File

@ -0,0 +1,74 @@
import * as path from "https://deno.land/std@0.125.0/path/mod.ts";
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";
const fsPrefix = '/fs';
const notesPath = '../notes';
const fsRouter = new Router();
fsRouter.use(oakCors());
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));
}
}
context.response.body = JSON.stringify(fileNames);
});
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;
});
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}`);
})
fsRouter.put('/:note', async context => {
const noteName = context.params.note;
const localPath = `${notesPath}/${noteName}.md`;
let file;
try {
file = await Deno.create(localPath);
} catch (e) {
console.error("Error opening file for writing", localPath, e);
context.response.status = 500;
context.response.body = e.message;
return;
}
const result = context.request.body({ type: "reader" });
const text = await readAll(result.value);
file.write(text);
file.close();
console.log("Wrote to", localPath)
context.response.body = "OK";
});
const app = new Application();
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'
});
} catch {
next();
}
});
await app.listen({ port: 2222 });

View File

@ -1,319 +0,0 @@
import {markdown} from "./markdown";
import {commonmark, mkLang} from "./markdown/markdown";
import {
Decoration,
DecorationSet,
drawSelection,
dropCursor,
EditorView,
highlightSpecialChars,
keymap,
ViewPlugin,
ViewUpdate
} from '@codemirror/view';
import {history, historyKeymap} from '@codemirror/history';
import {foldKeymap} from '@codemirror/fold';
import {indentOnInput, syntaxTree} from '@codemirror/language';
import {indentWithTab, standardKeymap} from '@codemirror/commands';
import {bracketMatching} from '@codemirror/matchbrackets';
import {closeBrackets, closeBracketsKeymap} from '@codemirror/closebrackets';
import {searchKeymap} from '@codemirror/search';
import {autocompletion, completionKeymap} from '@codemirror/autocomplete';
import {rectangularSelection} from '@codemirror/rectangular-selection';
import {HighlightStyle, styleTags, Tag, tags as t} from '@codemirror/highlight';
import {lintKeymap} from '@codemirror/lint';
import {EditorSelection, EditorState, StateCommand, Transaction} from "@codemirror/state";
import {Text} from "@codemirror/text";
import {MarkdownConfig} from "@lezer/markdown";
import {commonmarkLanguage} from "@codemirror/lang-markdown";
const defaultMd = `# Custom Box Design
Some #time ago I (that's @zef.hemel) wrote [No More Boxes](https://zef.me/musing/no-more-boxes/). Let me finally follow up on that and share an approach that Ive been using opportunistically here and there, primarily for roles that hadnt been well defined yet.
Let me start out with a few [[principles]] are:
Our starting point is that everybody is **different**, and we should _benefit_ from this fact rather than _suppress_ it. The goal is therefore to uncover every persons [essence,](https://zef.me/musing/your-essence/) develop it and optimally integrate it into the larger organization.
And fenced
\`\`\`
Hello
\`\`\`
## Second lever
Steve Jobs famously said:
> It doesn't make sense to hire smart people and then tell them what to do. We hire smart people so they can tell _us_ what to do.
>
> Another thing
We can adapt this quote to personal development. Heres how we can formulate this analogy:
A list:
* We can adapt this quote to personal development. Heres how we can formulate this analogy. ANd some other non duplicate text. We can adapt this quote to personal development. Heres how we can formulate this analogy. We can adapt this quote to personal development. Heres **how** we can formulate this analogy.
* Another line
The role of management is to challenge and support people in this process, and provide them with relevant context (e.g. knowledge, experience, connections) they need.`
const WikiLinkTag = Tag.define();
const TagTag = Tag.define();
const MentionTag = Tag.define();
let mdStyle = 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: WikiLinkTag, class: "wiki-link"},
{tag: TagTag, class: "tag"},
{tag: 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"}
]);
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 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
};
}
interface WrapElement {
selector: string;
class: string;
}
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
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: number, to: number) {
console.log("Leaving", type.name);
}
});
}
console.log("All widgets", widgets)
return Decoration.set(widgets);
}
const lineWrapper = (wrapElements: WrapElement[]) => ViewPlugin.fromClass(class {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = wrapLines(view, wrapElements);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = wrapLines(update.view, wrapElements)
}
}
}, {
decorations: v => v.decorations,
});
const WikiLink: MarkdownConfig = {
defineNodes: ["WikiLink"],
parseInline: [{
name: "WikiLink",
parse(cx, next, pos) {
let match: RegExpMatchArray | null
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: MarkdownConfig = {
defineNodes: ["AtMention"],
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)))) {
return -1;
}
return cx.addElement(cx.elt("AtMention", pos, pos + 1 + match[0].length))
},
after: "Emphasis"
}]
}
const TagLink: MarkdownConfig = {
defineNodes: ["TagLink"],
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)))) {
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: WikiLinkTag,
AtMention: MentionTag,
TagLink: TagTag,
})
]
}])
/// Language support for [GFM](https://github.github.com/gfm/) plus
/// subscript, superscript, and emoji syntax.
export const myMarkdown = mkLang(WikiMarkdown)
let startState = EditorState.create({
doc: defaultMd,
extensions: [
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
// EditorState.allowMultipleSelections.of(true),
indentOnInput(),
// defaultHighlightStyle.fallback,
mdStyle,
bracketMatching(),
closeBrackets(),
autocompletion(),
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"},
]),
rectangularSelection(),
keymap.of([
...closeBracketsKeymap,
...standardKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap,
indentWithTab,
{
key: "Ctrl-b",
mac: "Cmd-b",
run: insertMarker('**')
},
{
key: "Ctrl-i",
mac: "Cmd-i",
run: insertMarker('_')
}
]),
EditorView.domEventHandlers({
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));
return false;
}
}
}),
markdown({
base: myMarkdown,
}),
EditorView.lineWrapping
]
})
let view = new EditorView({
state: startState,
parent: document.getElementById('editor')
});
view.focus();

View File

@ -1,111 +0,0 @@
#editor {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
:root {
--ident: 18px;
}
.cm-editor {
width: 100%;
height: 100%;
font-size: var(--ident);
}
.cm-editor .cm-content {
font-family: "Menlo";
margin: 25px;
}
.cm-editor .cm-selectionBackground {
background-color: #d7e1f6 !important;
}
.cm-editor .h1 {
font-size: 1.5em;
color: #fff;
font-weight: bold;
}
.cm-editor .cm-line.line-h1 {
display: block;
background-color: rgba(0, 15, 52, 0.6);
}
.cm-editor .h1.meta {
color: orange;
}
.cm-editor .h2 {
font-size: 1.2em;
color: #fff;
font-weight: bold;
}
.cm-editor .cm-line.line-h2 {
display: block;
background-color: rgba(0, 15, 52, 0.6);
}
.cm-editor .h2.meta {
color: orange;
}
.cm-editor .line-code {
background-color: #efefef;
margin-left: 30px;
}
.cm-editor .line-fenced-code {
background-color: #efefef;
}
.cm-editor .meta {
color: #520130;
}
.cm-editor .line-blockquote {
background-color: #eee;
color: #676767;
text-indent: calc(-1 * (var(--ident) + 3px));
padding-left: var(--ident);
}
.cm-editor .emphasis {
font-style: italic;
}
.cm-editor .strong {
font-weight: 900;
}
.cm-editor .link:not(.meta,.url) {
color: #0330cb;
text-decoration: underline;
}
.cm-editor .link.url {
color: #7e7d7d;
}
.cm-editor .wiki-link {
color: #0330cb;
/*text-decoration: underline;*/
}
.cm-editor .mention {
color: gray;
}
.cm-editor .tag {
color: #8d8d8d;
}
.cm-editor .line-li {
text-indent: calc(-1 * var(--ident) - 3px);
margin-left: var(--ident);
}

BIN
webapp/.DS_Store vendored Normal file

Binary file not shown.

6
webapp/.parcelrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "@parcel/config-default",
"validators": {
"*.{ts,tsx}": ["@parcel/validator-typescript"]
}
}

View File

@ -9,13 +9,19 @@
"build": "parcel build"
},
"devDependencies": {
"parcel": "^2.3.1"
"@parcel/validator-typescript": "^2.3.2",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"parcel": "^2.3.2",
"typescript": ">=3.0.0"
},
"dependencies": {
"@codemirror/basic-setup": "^0.19.1",
"@codemirror/commands": "^0.19.8",
"@codemirror/lang-markdown": "^0.19.6",
"@codemirror/state": "^0.19.7",
"@codemirror/view": "^0.19.42"
"@codemirror/view": "^0.19.42",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

BIN
webapp/src/.DS_Store vendored Normal file

Binary file not shown.

130
webapp/src/app.js Normal file
View File

@ -0,0 +1,130 @@
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);

173
webapp/src/app.tsx Normal file
View File

@ -0,0 +1,173 @@
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 { MutableRefObject, useEffect, useRef, useState } from "react";
const fs = new HttpFileSystem("http://localhost:2222/fs");
type AppState = {
currentNote: string;
};
class Editor {
view: EditorView;
currentNote: string;
constructor(parent: Element, currentNote: string, text: string) {
this.view = new EditorView({
state: this.createEditorState(text),
parent: parent,
});
this.currentNote = currentNote;
}
load(name: string, text: string) {
this.currentNote = name;
this.view.setState(this.createEditorState(text));
}
async save() {
await fs.writeNote(this.currentNote, this.view.state.sliceDoc());
}
focus() {
this.view.focus();
}
private createEditorState(text: string): EditorState {
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: EditorView): boolean => {
Promise.resolve()
.then(async () => {
console.log("Saving");
await this.save();
})
.catch((e) => console.error(e));
return true;
},
},
]),
EditorView.domEventHandlers({
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)
);
return false;
}
},
}),
markdown({
base: customMarkDown,
}),
StateField.define({
create: () => null,
update: (value, transaction) => {
if (transaction.docChanged) {
console.log("Something changed");
}
return null;
},
}),
],
});
}
}
function TopBar({ editor }: { editor: Editor | null }) {
return (
<div id="top">
This is the top bar, do something cool: {editor?.currentNote}
</div>
);
}
function App() {
const editorRef = useRef<HTMLDivElement>(null);
const [editor, setEditor] = useState<Editor | null>(null);
useEffect(() => {
let editor = new Editor(editorRef.current!, "", "");
editor.focus();
// @ts-ignore
window.editor = editor;
fs.readNote("start").then((text) => {
editor.load("start", text);
});
setEditor(editor);
}, []);
return (
<>
<TopBar editor={editor} />
<div id="editor" ref={editorRef}></div>
<div id="bottom">Bottom</div>
</>
);
}
ReactDOM.render(<App />, document.body);

38
webapp/src/commands.js Normal file
View File

@ -0,0 +1,38 @@
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;
};
}

47
webapp/src/commands.ts Normal file
View File

@ -0,0 +1,47 @@
import { EditorSelection, EditorState, 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 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
};
}

4
webapp/src/customtags.js Normal file
View File

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

5
webapp/src/customtags.ts Normal file
View File

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

24
webapp/src/fs.js Normal file
View File

@ -0,0 +1,24 @@
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();
}
}

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

@ -0,0 +1,38 @@
export interface NoteMeta {
name: string;
}
export interface FileSystem {
listNotes(): Promise<NoteMeta>;
readNote(name: string): Promise<string>;
writeNote(name: string, text: string): Promise<void>;
}
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'
});
return (await req.json()).map((name: string) => ({ name }));
}
async readNote(name: string): Promise<string> {
let req = await fetch(`${this.url}/${name}`, {
method: 'GET'
});
return await req.text();
}
async writeNote(name: string, text: string): Promise<void> {
let req = await fetch(`${this.url}/${name}`, {
method: 'PUT',
body: text
});
await req.text();
}
}

View File

@ -4,11 +4,9 @@
<meta charset="utf-8" />
<title>Noot</title>
<link rel="stylesheet" href="styles.css" />
<script type="module" src="app.ts"></script>
<script type="module" src="app.tsx"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
<div id="editor"></div>
</body>
<body></body>
</html>

45
webapp/src/lineWrapper.js Normal file
View File

@ -0,0 +1,45 @@
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,
});

56
webapp/src/lineWrapper.ts Normal file
View File

@ -0,0 +1,56 @@
import { syntaxTree } from '@codemirror/language';
import {
Decoration,
DecorationSet, EditorView, ViewPlugin,
ViewUpdate
} from '@codemirror/view';
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,
enter: (type, from, to) => {
const bodyText = doc.sliceString(from, to);
for (let wrapElement of wrapElements) {
if (type.name == wrapElement.selector) {
const bodyText = doc.sliceString(from, to);
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: number, to: number) {
}
});
}
// console.log("All widgets", widgets);
return Decoration.set(widgets);
}
export const lineWrapper = (wrapElements: WrapElement[]) => ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = wrapLines(view, wrapElements);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = wrapLines(update.view, wrapElements);
}
}
}, {
decorations: v => v.decorations,
});

View File

@ -0,0 +1,234 @@
import { EditorSelection } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { markdownLanguage } from "./markdown";
function nodeStart(node, doc) {
return doc.sliceString(node.from, node.from + 50);
}
class Context {
constructor(node, from, to, spaceBefore, spaceAfter, type, item) {
this.node = node;
this.from = from;
this.to = to;
this.spaceBefore = spaceBefore;
this.spaceAfter = spaceAfter;
this.type = type;
this.item = item;
}
blank(trailing = true) {
let result = this.spaceBefore;
if (this.node.name == "Blockquote")
result += ">";
else
for (let i = this.to - this.from - result.length - this.spaceAfter.length; i > 0; i--)
result += " ";
return result + (trailing ? this.spaceAfter : "");
}
marker(doc, add) {
let number = this.node.name == "OrderedList" ? String((+itemNumber(this.item, doc)[2] + add)) : "";
return this.spaceBefore + number + this.type + this.spaceAfter;
}
}
function getContext(node, line, doc) {
let nodes = [];
for (let cur = node; cur && cur.name != "Document"; cur = cur.parent) {
if (cur.name == "ListItem" || cur.name == "Blockquote")
nodes.push(cur);
}
let context = [], pos = 0;
for (let i = nodes.length - 1; i >= 0; i--) {
let node = nodes[i], match, start = pos;
if (node.name == "Blockquote" && (match = /^[ \t]*>( ?)/.exec(line.slice(pos)))) {
pos += match[0].length;
context.push(new Context(node, start, pos, "", match[1], ">", null));
}
else if (node.name == "ListItem" && node.parent.name == "OrderedList" &&
(match = /^([ \t]*)\d+([.)])([ \t]*)/.exec(nodeStart(node, doc)))) {
let after = match[3], len = match[0].length;
if (after.length >= 4) {
after = after.slice(0, after.length - 4);
len -= 4;
}
pos += len;
context.push(new Context(node.parent, start, pos, match[1], after, match[2], node));
}
else if (node.name == "ListItem" && node.parent.name == "BulletList" &&
(match = /^([ \t]*)([-+*])([ \t]+)/.exec(nodeStart(node, doc)))) {
let after = match[3], len = match[0].length;
if (after.length > 4) {
after = after.slice(0, after.length - 4);
len -= 4;
}
pos += len;
context.push(new Context(node.parent, start, pos, match[1], after, match[2], node));
}
}
return context;
}
function itemNumber(item, doc) {
return /^(\s*)(\d+)(?=[.)])/.exec(doc.sliceString(item.from, item.from + 10));
}
function renumberList(after, doc, changes, offset = 0) {
for (let prev = -1, node = after;;) {
if (node.name == "ListItem") {
let m = itemNumber(node, doc);
let number = +m[2];
if (prev >= 0) {
if (number != prev + 1)
return;
changes.push({ from: node.from + m[1].length, to: node.from + m[0].length, insert: String(prev + 2 + offset) });
}
prev = number;
}
let next = node.nextSibling;
if (!next)
break;
node = next;
}
}
/// This command, when invoked in Markdown context with cursor
/// selection(s), will create a new line with the markup for
/// blockquotes and lists that were active on the old line. If the
/// cursor was directly after the end of the markup for the old line,
/// trailing whitespace and list markers are removed from that line.
///
/// The command does nothing in non-Markdown context, so it should
/// not be used as the only binding for Enter (even in a Markdown
/// document, HTML and code regions might use a different language).
export const insertNewlineContinueMarkup = ({ state, dispatch }) => {
let tree = syntaxTree(state), { doc } = state;
let dont = null, changes = state.changeByRange(range => {
if (!range.empty || !markdownLanguage.isActiveAt(state, range.from))
return dont = { range };
let pos = range.from, line = doc.lineAt(pos);
let context = getContext(tree.resolveInner(pos, -1), line.text, doc);
while (context.length && context[context.length - 1].from > pos - line.from)
context.pop();
if (!context.length)
return dont = { range };
let inner = context[context.length - 1];
if (inner.to - inner.spaceAfter.length > pos - line.from)
return dont = { range };
let emptyLine = pos >= (inner.to - inner.spaceAfter.length) && !/\S/.test(line.text.slice(inner.to));
// Empty line in list
if (inner.item && emptyLine) {
// First list item or blank line before: delete a level of markup
if (inner.node.firstChild.to >= pos ||
line.from > 0 && !/[^\s>]/.test(doc.lineAt(line.from - 1).text)) {
let next = context.length > 1 ? context[context.length - 2] : null;
let delTo, insert = "";
if (next && next.item) { // Re-add marker for the list at the next level
delTo = line.from + next.from;
insert = next.marker(doc, 1);
}
else {
delTo = line.from + (next ? next.to : 0);
}
let changes = [{ from: delTo, to: pos, insert }];
if (inner.node.name == "OrderedList")
renumberList(inner.item, doc, changes, -2);
if (next && next.node.name == "OrderedList")
renumberList(next.item, doc, changes);
return { range: EditorSelection.cursor(delTo + insert.length), changes };
}
else { // Move this line down
let insert = "";
for (let i = 0, e = context.length - 2; i <= e; i++)
insert += context[i].blank(i < e);
insert += state.lineBreak;
return { range: EditorSelection.cursor(pos + insert.length), changes: { from: line.from, insert } };
}
}
if (inner.node.name == "Blockquote" && emptyLine && line.from) {
let prevLine = doc.lineAt(line.from - 1), quoted = />\s*$/.exec(prevLine.text);
// Two aligned empty quoted lines in a row
if (quoted && quoted.index == inner.from) {
let changes = state.changes([{ from: prevLine.from + quoted.index, to: prevLine.to },
{ from: line.from + inner.from, to: line.to }]);
return { range: range.map(changes), changes };
}
}
let changes = [];
if (inner.node.name == "OrderedList")
renumberList(inner.item, doc, changes);
let insert = state.lineBreak;
let continued = inner.item && inner.item.from < line.from;
// If not dedented
if (!continued || /^[\s\d.)\-+*>]*/.exec(line.text)[0].length >= inner.to) {
for (let i = 0, e = context.length - 1; i <= e; i++)
insert += i == e && !continued ? context[i].marker(doc, 1) : context[i].blank();
}
let from = pos;
while (from > line.from && /\s/.test(line.text.charAt(from - line.from - 1)))
from--;
changes.push({ from, to: pos, insert });
return { range: EditorSelection.cursor(from + insert.length), changes };
});
if (dont)
return false;
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "input" }));
return true;
};
function isMark(node) {
return node.name == "QuoteMark" || node.name == "ListMark";
}
function contextNodeForDelete(tree, pos) {
let node = tree.resolveInner(pos, -1), scan = pos;
if (isMark(node)) {
scan = node.from;
node = node.parent;
}
for (let prev; prev = node.childBefore(scan);) {
if (isMark(prev)) {
scan = prev.from;
}
else if (prev.name == "OrderedList" || prev.name == "BulletList") {
node = prev.lastChild;
scan = node.to;
}
else {
break;
}
}
return node;
}
/// This command will, when invoked in a Markdown context with the
/// cursor directly after list or blockquote markup, delete one level
/// of markup. When the markup is for a list, it will be replaced by
/// spaces on the first invocation (a further invocation will delete
/// the spaces), to make it easy to continue a list.
///
/// When not after Markdown block markup, this command will return
/// false, so it is intended to be bound alongside other deletion
/// commands, with a higher precedence than the more generic commands.
export const deleteMarkupBackward = ({ state, dispatch }) => {
let tree = syntaxTree(state);
let dont = null, changes = state.changeByRange(range => {
let pos = range.from, { doc } = state;
if (range.empty && markdownLanguage.isActiveAt(state, range.from)) {
let line = doc.lineAt(pos);
let context = getContext(contextNodeForDelete(tree, pos), line.text, doc);
if (context.length) {
let inner = context[context.length - 1];
let spaceEnd = inner.to - inner.spaceAfter.length + (inner.spaceAfter ? 1 : 0);
// Delete extra trailing space after markup
if (pos - line.from > spaceEnd && !/\S/.test(line.text.slice(spaceEnd, pos - line.from)))
return { range: EditorSelection.cursor(line.from + spaceEnd),
changes: { from: line.from + spaceEnd, to: pos } };
if (pos - line.from == spaceEnd) {
let start = line.from + inner.from;
// Replace a list item marker with blank space
if (inner.item && inner.node.from < inner.item.from && /\S/.test(line.text.slice(inner.from, inner.to)))
return { range, changes: { from: start, to: line.from + inner.to, insert: inner.blank() } };
// Delete one level of indentation
if (start < pos)
return { range: EditorSelection.cursor(start), changes: { from: start, to: pos } };
}
}
}
return dont = { range };
});
if (dont)
return false;
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "delete" }));
return true;
};

View File

@ -0,0 +1,37 @@
import { Prec } from "@codemirror/state";
import { keymap } from "@codemirror/view";
import { LanguageSupport } from "@codemirror/language";
import { MarkdownParser, parseCode } from "@lezer/markdown";
import { html } from "@codemirror/lang-html";
import { commonmarkLanguage, markdownLanguage, mkLang, getCodeParser } from "./markdown";
import { insertNewlineContinueMarkup, deleteMarkupBackward } from "./commands";
export { commonmarkLanguage, markdownLanguage, insertNewlineContinueMarkup, deleteMarkupBackward };
/// A small keymap with Markdown-specific bindings. Binds Enter to
/// [`insertNewlineContinueMarkup`](#lang-markdown.insertNewlineContinueMarkup)
/// and Backspace to
/// [`deleteMarkupBackward`](#lang-markdown.deleteMarkupBackward).
export const markdownKeymap = [
{ key: "Enter", run: insertNewlineContinueMarkup },
{ key: "Backspace", run: deleteMarkupBackward }
];
const htmlNoMatch = html({ matchClosingTags: false });
/// Markdown language support.
export function markdown(config = {}) {
let { codeLanguages, defaultCodeLanguage, addKeymap = true, base: { parser } = commonmarkLanguage } = config;
if (!(parser instanceof MarkdownParser))
throw new RangeError("Base parser provided to `markdown` should be a Markdown parser");
let extensions = config.extensions ? [config.extensions] : [];
let support = [htmlNoMatch.support], defaultCode;
if (defaultCodeLanguage instanceof LanguageSupport) {
support.push(defaultCodeLanguage.support);
defaultCode = defaultCodeLanguage.language;
}
else if (defaultCodeLanguage) {
defaultCode = defaultCodeLanguage;
}
let codeParser = codeLanguages || defaultCode ? getCodeParser(codeLanguages || [], defaultCode) : undefined;
extensions.push(parseCode({ codeParser, htmlParser: htmlNoMatch.language.parser }));
if (addKeymap)
support.push(Prec.high(keymap.of(markdownKeymap)));
return new LanguageSupport(mkLang(parser.configure(extensions)), support);
}

View File

@ -0,0 +1,75 @@
import { Language, defineLanguageFacet, languageDataProp, foldNodeProp, indentNodeProp, LanguageDescription, ParseContext } from "@codemirror/language";
import { styleTags, tags as t } from "@codemirror/highlight";
import { parser as baseParser, GFM, Subscript, Superscript, Emoji } from "@lezer/markdown";
const data = defineLanguageFacet({ block: { open: "<!--", close: "-->" } });
export const commonmark = baseParser.configure({
props: [
styleTags({
"Blockquote/...": t.quote,
HorizontalRule: t.contentSeparator,
"ATXHeading1/... SetextHeading1/...": t.heading1,
"ATXHeading2/... SetextHeading2/...": t.heading2,
"ATXHeading3/...": t.heading3,
"ATXHeading4/...": t.heading4,
"ATXHeading5/...": t.heading5,
"ATXHeading6/...": t.heading6,
"Comment CommentBlock": t.comment,
Escape: t.escape,
Entity: t.character,
"Emphasis/...": t.emphasis,
"StrongEmphasis/...": t.strong,
"Link/... Image/...": t.link,
"OrderedList/... BulletList/...": t.list,
// "CodeBlock/... FencedCode/...": t.blockComment,
"InlineCode CodeText": t.monospace,
URL: t.url,
"HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark": t.processingInstruction,
"CodeInfo LinkLabel": t.labelName,
LinkTitle: t.string,
Paragraph: t.content
}),
foldNodeProp.add(type => {
if (!type.is("Block") || type.is("Document"))
return undefined;
return (tree, state) => ({ from: state.doc.lineAt(tree.from).to, to: tree.to });
}),
indentNodeProp.add({
Document: () => null
}),
languageDataProp.add({
Document: data
})
]
});
export function mkLang(parser) {
return new Language(data, parser, parser.nodeSet.types.find(t => t.name == "Document"));
}
/// Language support for strict CommonMark.
export const commonmarkLanguage = mkLang(commonmark);
const extended = commonmark.configure([GFM, Subscript, Superscript, Emoji, {
props: [
styleTags({
"TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark": t.processingInstruction,
"TableHeader/...": t.heading,
"Strikethrough/...": t.strikethrough,
TaskMarker: t.atom,
Task: t.list,
Emoji: t.character,
"Subscript Superscript": t.special(t.content),
TableCell: t.content
})
]
}]);
/// Language support for [GFM](https://github.github.com/gfm/) plus
/// subscript, superscript, and emoji syntax.
export const markdownLanguage = mkLang(extended);
export function getCodeParser(languages, defaultLanguage) {
return (info) => {
let found = info && LanguageDescription.matchLanguageName(languages, info, true);
if (!found)
return defaultLanguage ? defaultLanguage.parser : null;
if (found.support)
return found.support.language.parser;
return ParseContext.getSkippingParser(found.load());
};
}

57
webapp/src/parser.js Normal file
View File

@ -0,0 +1,57 @@
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);

60
webapp/src/parser.ts Normal file
View File

@ -0,0 +1,60 @@
import { styleTags } from '@codemirror/highlight';
import { MarkdownConfig } from "@lezer/markdown";
import { commonmark, mkLang } from "./markdown/markdown";
import * as ct from './customtags';
const WikiLink: MarkdownConfig = {
defineNodes: ["WikiLink"],
parseInline: [{
name: "WikiLink",
parse(cx, next, pos) {
let match: RegExpMatchArray | null;
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: MarkdownConfig = {
defineNodes: ["AtMention"],
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)))) {
return -1;
}
return cx.addElement(cx.elt("AtMention", pos, pos + 1 + match[0].length));
},
after: "Emphasis"
}]
};
const TagLink: MarkdownConfig = {
defineNodes: ["TagLink"],
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)))) {
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);

31
webapp/src/style.js Normal file
View File

@ -0,0 +1,31 @@
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" }
]);

32
webapp/src/style.ts Normal file
View File

@ -0,0 +1,32 @@
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" }
]);

132
webapp/src/styles.css Normal file
View File

@ -0,0 +1,132 @@
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
}
#top {
height: 40px;
background-color: #eee;
}
#bottom {
height: 40px;
background-color: #eee;
margin: 0;
}
#editor {
flex-grow: 1;
width: 100%;
overflow-y: hidden;
}
:root {
--ident: 18px;
}
.cm-editor {
width: 100%;
height: 100%;
font-size: var(--ident);
}
.cm-editor .cm-content {
font-family: "Menlo";
margin: 25px;
}
.cm-editor .cm-selectionBackground {
background-color: #d7e1f6 !important;
}
.cm-editor .h1 {
font-size: 1.5em;
color: #fff;
font-weight: bold;
}
.cm-editor .cm-line.line-h1 {
display: block;
background-color: rgba(0, 15, 52, 0.6);
}
.cm-editor .h1.meta {
color: orange;
}
.cm-editor .h2 {
font-size: 1.2em;
color: #fff;
font-weight: bold;
}
.cm-editor .cm-line.line-h2 {
display: block;
background-color: rgba(0, 15, 52, 0.6);
}
.cm-editor .h2.meta {
color: orange;
}
.cm-editor .line-code {
background-color: #efefef;
margin-left: 30px;
}
.cm-editor .line-fenced-code {
background-color: #efefef;
}
.cm-editor .meta {
color: #520130;
}
.cm-editor .line-blockquote {
background-color: #eee;
color: #676767;
text-indent: calc(-1 * (var(--ident) + 3px));
padding-left: var(--ident);
}
.cm-editor .emphasis {
font-style: italic;
}
.cm-editor .strong {
font-weight: 900;
}
.cm-editor .link:not(.meta, .url) {
color: #0330cb;
text-decoration: underline;
}
.cm-editor .link.url {
color: #7e7d7d;
}
.cm-editor .wiki-link {
color: #0330cb;
/*text-decoration: underline;*/
}
.cm-editor .mention {
color: gray;
}
.cm-editor .tag {
color: #8d8d8d;
}
.cm-editor .line-li {
text-indent: calc(-1 * var(--ident) - 3px);
margin-left: var(--ident);
}

10
webapp/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"include": ["src/**/*"],
"compilerOptions": {
"target": "es2021",
"strict": true ,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx"
}
}

File diff suppressed because it is too large Load Diff