markdown
This commit is contained in:
parent
5bb6ae6aa3
commit
ec375f14f7
10 changed files with 206 additions and 26 deletions
|
@ -18,6 +18,8 @@
|
|||
"@tanstack/react-query": "^5.37.1",
|
||||
"@tauri-apps/api": "^1",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"classnames": "^2.5.1",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
"hast-util-to-mdast": "^10.1.0",
|
||||
"javascript-time-ago": "^2.5.10",
|
||||
"jotai": "^2.8.1",
|
||||
|
@ -26,10 +28,14 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-time-ago": "^7.3.3"
|
||||
"react-time-ago": "^7.3.3",
|
||||
"remark": "^15.0.1",
|
||||
"remark-rehype": "^11.1.0",
|
||||
"vfile": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import styles from "./NodeDisplay.module.scss";
|
||||
import ReactTimeAgo from "react-time-ago";
|
||||
import Markdown from "react-markdown";
|
||||
import MDEditor, { commands } from "@uiw/react-md-editor";
|
||||
import { useState } from "react";
|
||||
import JournalPage from "./nodes/JournalPage";
|
||||
|
||||
export interface NodeDisplayProps {
|
||||
|
@ -19,6 +16,7 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
|||
return json;
|
||||
},
|
||||
});
|
||||
|
||||
const { isSuccess, status, data } = query;
|
||||
|
||||
return (
|
||||
|
@ -31,7 +29,7 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
|
|||
)}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{data.title ?? <span className={styles.untitled}>(untitled)</span>}
|
||||
{data?.title ?? <span className={styles.untitled}>(untitled)</span>}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{isSuccess ? (
|
||||
|
|
|
@ -7,3 +7,22 @@
|
|||
.mdEditor {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.block {
|
||||
padding: 12px;
|
||||
|
||||
&:not(.isEditing) {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
&.isEditing {
|
||||
background-color: rgb(235, 243, 246);
|
||||
outline: 2px solid skyblue;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(235, 243, 246);
|
||||
cursor: text;
|
||||
}
|
||||
}
|
|
@ -1,21 +1,59 @@
|
|||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
||||
import styles from "./JournalPage.module.scss";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import Markdown from "react-markdown";
|
||||
import { toMdast } from "hast-util-to-mdast";
|
||||
import { Node as MdastNode } from "mdast";
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import { toMarkdown } from "mdast-util-to-markdown";
|
||||
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { VFile } from "vfile";
|
||||
import { common } from "@mui/material/colors";
|
||||
import classNames from "classnames";
|
||||
|
||||
const MDContext = createContext(null);
|
||||
interface MDContextValue {
|
||||
isEditing: boolean;
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
||||
const MDContext = createContext<MDContextValue>(null!);
|
||||
|
||||
const emptyContent = { type: "root", children: [] };
|
||||
|
||||
export default function JournalPage({ id, data }) {
|
||||
const [content, setContent] = useState(() => data.content);
|
||||
const [isEditing, setIsEditing] = useState(() => false);
|
||||
const [currentlyFocused, setCurrentlyFocused] = useState<string | undefined>(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
const tree = fromMarkdown(data.content);
|
||||
console.log("tree", tree);
|
||||
useEffect(() => {
|
||||
if (content === null) {
|
||||
setContent(() => ({
|
||||
type: "root",
|
||||
children: [
|
||||
{ type: "paragraph", children: [{ type: "text", value: "" }] },
|
||||
],
|
||||
}));
|
||||
setCurrentlyFocused(".children[0]");
|
||||
setIsEditing(true);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const contextValue = { content, setContent, isEditing, setIsEditing };
|
||||
|
||||
const jsxContent = convertToJsx(content, { currentlyFocused });
|
||||
|
||||
return (
|
||||
<>
|
||||
<details>
|
||||
|
@ -23,7 +61,79 @@ export default function JournalPage({ id, data }) {
|
|||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</details>
|
||||
|
||||
<div className={styles.mdContent} data-color-mode="light"></div>
|
||||
<div className={styles.mdContent} data-color-mode="light">
|
||||
<MDContext.Provider value={contextValue}>
|
||||
{jsxContent}
|
||||
</MDContext.Provider>
|
||||
|
||||
<pre>{JSON.stringify(content, null, 2)}</pre>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConvertToJsxOpts {
|
||||
currentlyFocused?: string | undefined;
|
||||
parent?: MdastNode | undefined;
|
||||
}
|
||||
|
||||
function convertToJsx(
|
||||
tree: MdastNode,
|
||||
opts?: ConvertToJsxOpts | undefined,
|
||||
): ReactNode {
|
||||
console.log("tree", tree);
|
||||
|
||||
if (tree === null) return;
|
||||
|
||||
const commonProps = {
|
||||
node: tree,
|
||||
parent: opts?.parent,
|
||||
};
|
||||
|
||||
switch (tree.type) {
|
||||
case "root":
|
||||
return tree.children.map((child) =>
|
||||
convertToJsx(child, { parent: tree }),
|
||||
);
|
||||
|
||||
case "paragraph":
|
||||
return <Paragraph {...commonProps} />;
|
||||
|
||||
default:
|
||||
throw new Error(`unhandled ${tree.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function Paragraph({ ...args }) {
|
||||
// const { isEditing } = useContext(MDContext);
|
||||
const [isEditing, setIsEditing] = useState(() => false);
|
||||
const [localValue, setLocalValue] = useState(null);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
if (!isEditing) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const save = useCallback(() => {
|
||||
console.log("saving!", localValue);
|
||||
});
|
||||
|
||||
const onPaste = useCallback((evt) => {
|
||||
console.log("pasted");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={classNames(styles.block, isEditing && styles.isEditing)}
|
||||
contentEditable={isEditing}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onPaste={onPaste}
|
||||
onBlur={save}
|
||||
>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
9
app/src/components/nodes/convertToJsx.tsx
Normal file
9
app/src/components/nodes/convertToJsx.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { ReactNode } from "react";
|
||||
import { Nodes as MdastNodes } from "mdast";
|
||||
|
||||
export function convertToJsx(tree: MdastNodes): ReactNode {
|
||||
console.log("tree", tree);
|
||||
|
||||
switch (tree.type) {
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ use axum::{
|
|||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
|
||||
pub type AppResult<T, E = AppError> = std::result::Result<T, E>;
|
||||
|
||||
// Make our own error that wraps `anyhow::Error`.
|
||||
|
@ -13,6 +12,7 @@ pub struct AppError(miette::Report);
|
|||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
eprintln!("Encountered error: {}", self.0);
|
||||
eprintln!("{:?}", self.0);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Something went wrong: {}", self.0),
|
||||
|
|
|
@ -14,7 +14,7 @@ pub async fn get_todays_journal_id(
|
|||
|
||||
let result = state.db.run_script(
|
||||
"
|
||||
?[node_id] := *journal_days[day, node_id], day = $day
|
||||
?[node_id] := *journal_day[day, node_id], day = $day
|
||||
",
|
||||
btmap! {
|
||||
"day".to_owned() => today.clone().into(),
|
||||
|
@ -29,27 +29,28 @@ pub async fn get_todays_journal_id(
|
|||
let uuid = Uuid::now_v7();
|
||||
let node_id = uuid.to_string();
|
||||
|
||||
let _result = state.db.run_script_fold_err(
|
||||
state.db.run_script(
|
||||
"
|
||||
{
|
||||
?[id, type] <- [[$node_id, 'panorama/journal/page']]
|
||||
:put node { id, type }
|
||||
?[id, title, type] <- [[$node_id, $title, 'panorama/journal/page']]
|
||||
:put node { id, title, type }
|
||||
}
|
||||
{
|
||||
?[node_id, content] <- [[$node_id, 'Default **content**']]
|
||||
?[node_id, content] <- [[$node_id, {}]]
|
||||
:put journal { node_id => content }
|
||||
}
|
||||
{
|
||||
?[day, node_id] <- [[$day, $node_id]]
|
||||
:put journal_days { day => node_id }
|
||||
:put journal_day { day => node_id }
|
||||
}
|
||||
",
|
||||
btmap! {
|
||||
"node_id".to_owned() => node_id.clone().into(),
|
||||
"day".to_owned() => today.clone().into(),
|
||||
"title".to_owned() => today.clone().into(),
|
||||
},
|
||||
ScriptMutability::Mutable,
|
||||
);
|
||||
)?;
|
||||
|
||||
return Ok(Json(json!({
|
||||
"node_id": node_id
|
||||
|
@ -58,7 +59,8 @@ pub async fn get_todays_journal_id(
|
|||
|
||||
let node_id = result.rows[0][0].get_str().unwrap();
|
||||
Ok(Json(json!({
|
||||
"node_id": node_id
|
||||
"node_id": node_id,
|
||||
"day": today,
|
||||
})))
|
||||
}
|
||||
|
||||
|
|
|
@ -119,11 +119,11 @@ fn migration_01(db: &DbInstance) -> Result<()> {
|
|||
{ :create node_refers_to { node_id: String => other_node_id: String } }
|
||||
|
||||
# Create journal type
|
||||
{ :create journal { node_id: String => content: String } }
|
||||
{ :create journal_days { day: String => node_id: String } }
|
||||
{ :create journal { node_id: String => content: Json } }
|
||||
{ :create journal_day { day: String => node_id: String } }
|
||||
{
|
||||
::fts create journal:text_index {
|
||||
extractor: content,
|
||||
extractor: dump_json(content),
|
||||
extract_filter: !is_null(content),
|
||||
tokenizer: Simple,
|
||||
filters: [Lowercase, Stemmer('english'), Stopwords('en')],
|
||||
|
|
|
@ -19,12 +19,12 @@ pub async fn get_node(
|
|||
j[content] := *journal{ node_id, content }, node_id = $node_id
|
||||
j[content] := not *journal{ node_id }, node_id = $node_id, content = null
|
||||
|
||||
jd[day] := *journal_days{ node_id, day }, node_id = $node_id
|
||||
jd[day] := not *journal_days{ node_id }, node_id = $node_id, day = null
|
||||
jd[day] := *journal_day{ node_id, day }, node_id = $node_id
|
||||
jd[day] := not *journal_day{ node_id }, node_id = $node_id, day = null
|
||||
|
||||
?[
|
||||
extra_data, content, day, created_at, updated_at, type
|
||||
] := *node{ id, type, created_at, updated_at, extra_data },
|
||||
extra_data, content, day, created_at, updated_at, type, title
|
||||
] := *node{ id, type, title, created_at, updated_at, extra_data },
|
||||
j[content],
|
||||
jd[day],
|
||||
id = $node_id
|
||||
|
@ -52,6 +52,7 @@ pub async fn get_node(
|
|||
"created_at": row[3].get_float(),
|
||||
"updated_at": row[4].get_float(),
|
||||
"type": row[5].get_str(),
|
||||
"title": row[6].get_str(),
|
||||
})),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -35,6 +35,12 @@ importers:
|
|||
'@uiw/react-md-editor':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
hast-util-to-jsx-runtime:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
hast-util-to-mdast:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
|
@ -62,10 +68,22 @@ importers:
|
|||
react-time-ago:
|
||||
specifier: ^7.3.3
|
||||
version: 7.3.3(javascript-time-ago@2.5.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
remark:
|
||||
specifier: ^15.0.1
|
||||
version: 15.0.1
|
||||
remark-rehype:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
vfile:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
devDependencies:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^1
|
||||
version: 1.5.14
|
||||
'@types/mdast':
|
||||
specifier: ^4.0.4
|
||||
version: 4.0.4
|
||||
'@types/react':
|
||||
specifier: ^18.2.15
|
||||
version: 18.3.3
|
||||
|
@ -836,6 +854,9 @@ packages:
|
|||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -1440,6 +1461,9 @@ packages:
|
|||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
remark@15.0.1:
|
||||
resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -2316,6 +2340,8 @@ snapshots:
|
|||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
color-convert@1.9.3:
|
||||
|
@ -3298,6 +3324,15 @@ snapshots:
|
|||
mdast-util-to-markdown: 2.1.0
|
||||
unified: 11.0.4
|
||||
|
||||
remark@15.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
remark-parse: 11.0.0
|
||||
remark-stringify: 11.0.0
|
||||
unified: 11.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve@1.22.8:
|
||||
|
|
Loading…
Reference in a new issue