Initial commit

pull/3/head
Zef Hemel 2022-02-16 10:57:28 +01:00
commit 277865e4e0
6 changed files with 2290 additions and 0 deletions

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "noot",
"version": "1.0.0",
"source": "src/index.html",
"license": "MIT",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "parcel",
"build": "parcel build"
},
"devDependencies": {
"parcel": "^2.3.1"
},
"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"
}
}

319
src/app.ts Normal file
View File

@ -0,0 +1,319 @@
import {markdown} from "./lang-markdown/index";
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 {commonmark, mkLang} from "./lang-markdown/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();

14
src/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Noot</title>
<link rel="stylesheet" href="styles.css" />
<script type="module" src="app.ts"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
<div id="editor"></div>
</body>
</html>

1
src/lang-markdown Submodule

@ -0,0 +1 @@
Subproject commit fa2b51164dbad56f1a7cc7fff6e228929ed44501

111
src/styles.css Normal file
View File

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

1824
yarn.lock Normal file

File diff suppressed because it is too large Load Diff