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
+126 -56
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
{appState.showNoteNavigator && (
<NoteNavigator
allNotes={appState.allNotes}
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}
isSaved={appState.isSaved}
isFiltering={appState.isFiltering}
allNotes={appState.allNotes}
onClick={() => {
dispatch({ type: "start-navigate" });
}}
onNavigate={(note) => {
dispatch({ type: "stop-navigate" });
editor!.focus();
if (note) {
editor!.navigate(note);
}
}}
/>
<div id="editor" ref={editorRef}></div>
<div id="bottom">Bottom</div>
<StatusBar isSaved={appState.isSaved} />
</>
);
}
+53 -40
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 changes = [];
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),
changes.push(
isBoldBefore
? {
from: range.from - marker.length,
to: range.from,
insert: Text.of([""]),
}
})
: {
from: range.from,
insert: Text.of([marker]),
}
);
dispatch(
state.update(changes, {
scrollIntoView: true,
annotations: Transaction.userEvent.of('input'),
})
)
changes.push(
isBoldAfter
? {
from: range.to,
to: range.to + marker.length,
insert: Text.of([""]),
}
: {
from: range.to,
insert: Text.of([marker]),
}
);
return true
};
}
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;
};
}
+21
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);
}}
/>
);
}
+44 -13
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, {
name: originalPhrase,
hint: "Create new",
});
results = results.sort(magicSorter);
if (allowNew && !foundExactMatch) {
results.push({
name: originalPhrase,
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;
}
+22
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);
}}
/>
);
}
+2 -1
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();
+29 -30
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>;
listNotes(): Promise<NoteMeta[]>;
readNote(name: string): Promise<string>;
// @return whether a new note was created for this
writeNote(name: string, text: string): Promise<boolean>;
}
export class HttpFileSystem implements FileSystem {
url: string;
url: string;
constructor(url: string) {
this.url = url;
}
async listNotes(): Promise<NoteMeta[]> {
let req = await fetch(this.url, {
method: "GET",
});
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();
}
}
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<boolean> {
let req = await fetch(`${this.url}/${name}`, {
method: "PUT",
body: text,
});
// 201 (Created) means a new note was created
return req.status === 201;
}
}
+59 -47
View File
@@ -1,56 +1,68 @@
import { syntaxTree } from '@codemirror/language';
import { syntaxTree } from "@codemirror/language";
import {
Decoration,
DecorationSet, EditorView, ViewPlugin,
ViewUpdate
} from '@codemirror/view';
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
} from "@codemirror/view";
import { Range } from '@codemirror/rangeset';
import { Range } from "@codemirror/rangeset";
interface WrapElement {
selector: string;
class: string;
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) {
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;
}
});
}
// 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);
}
}
},
leave(type, from: number, to: number) {},
});
}
// 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 {
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,
}
}, {
decorations: v => v.decorations,
});
);
+74 -48
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: [{
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"
}]
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)))
) {
return -1;
}
return cx.addElement(
cx.elt("WikiLink", pos, pos + match[0].length + 1, [
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 1),
])
);
},
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"
}]
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"
}]
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, {
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.
styleTags({
WikiLink: ct.WikiLinkTag,
WikiLinkPage: ct.WikiLinkPageTag,
AtMention: ct.MentionTag,
TagLink: ct.TagTag,
}),
],
},
]);
export default mkLang(WikiMarkdown);
+27 -9
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;
}
+30 -29
View File
@@ -1,32 +1,33 @@
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" },
{ 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" }
{ 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.WikiLinkPageTag, class: "wiki-link-page" },
{ 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" },
]);
+42 -34
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,
+14 -5
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
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);
}