From ec375f14f7fd7649e7e5b8e23e22cd25fff02526 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Sat, 25 May 2024 11:13:07 -0500 Subject: [PATCH] markdown --- app/package.json | 8 +- app/src/components/NodeDisplay.tsx | 6 +- .../components/nodes/JournalPage.module.scss | 19 +++ app/src/components/nodes/JournalPage.tsx | 120 +++++++++++++++++- app/src/components/nodes/convertToJsx.tsx | 9 ++ crates/panorama-daemon/src/error.rs | 2 +- crates/panorama-daemon/src/journal.rs | 18 +-- crates/panorama-daemon/src/migrations.rs | 6 +- crates/panorama-daemon/src/node.rs | 9 +- pnpm-lock.yaml | 35 +++++ 10 files changed, 206 insertions(+), 26 deletions(-) create mode 100644 app/src/components/nodes/convertToJsx.tsx diff --git a/app/package.json b/app/package.json index 4176401..3572b85 100644 --- a/app/package.json +++ b/app/package.json @@ -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", diff --git a/app/src/components/NodeDisplay.tsx b/app/src/components/NodeDisplay.tsx index f4da92d..981ea5a 100644 --- a/app/src/components/NodeDisplay.tsx +++ b/app/src/components/NodeDisplay.tsx @@ -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) { )}
- {data.title ?? (untitled)} + {data?.title ?? (untitled)}
{isSuccess ? ( diff --git a/app/src/components/nodes/JournalPage.module.scss b/app/src/components/nodes/JournalPage.module.scss index 3622f1f..ae3a855 100644 --- a/app/src/components/nodes/JournalPage.module.scss +++ b/app/src/components/nodes/JournalPage.module.scss @@ -6,4 +6,23 @@ .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; + } } \ No newline at end of file diff --git a/app/src/components/nodes/JournalPage.tsx b/app/src/components/nodes/JournalPage.tsx index 84eef03..424251f 100644 --- a/app/src/components/nodes/JournalPage.tsx +++ b/app/src/components/nodes/JournalPage.tsx @@ -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: +const MDContext = createContext(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( + () => 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 ( <>
@@ -23,7 +61,79 @@ export default function JournalPage({ id, data }) {
{JSON.stringify(data, null, 2)}
-
+
+ + {jsxContent} + + +
{JSON.stringify(content, null, 2)}
+
); } + +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 ; + + 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 ( +
+
+
+
+
+ ); +} diff --git a/app/src/components/nodes/convertToJsx.tsx b/app/src/components/nodes/convertToJsx.tsx new file mode 100644 index 0000000..575d07e --- /dev/null +++ b/app/src/components/nodes/convertToJsx.tsx @@ -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) { + } +} diff --git a/crates/panorama-daemon/src/error.rs b/crates/panorama-daemon/src/error.rs index 96ab4a3..3227b3f 100644 --- a/crates/panorama-daemon/src/error.rs +++ b/crates/panorama-daemon/src/error.rs @@ -3,7 +3,6 @@ use axum::{ response::{IntoResponse, Response}, }; - pub type AppResult = std::result::Result; // 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), diff --git a/crates/panorama-daemon/src/journal.rs b/crates/panorama-daemon/src/journal.rs index 57675d7..0a1ee0f 100644 --- a/crates/panorama-daemon/src/journal.rs +++ b/crates/panorama-daemon/src/journal.rs @@ -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, }))) } diff --git a/crates/panorama-daemon/src/migrations.rs b/crates/panorama-daemon/src/migrations.rs index 3c799fc..fa4fd86 100644 --- a/crates/panorama-daemon/src/migrations.rs +++ b/crates/panorama-daemon/src/migrations.rs @@ -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')], diff --git a/crates/panorama-daemon/src/node.rs b/crates/panorama-daemon/src/node.rs index c1f4664..559f21d 100644 --- a/crates/panorama-daemon/src/node.rs +++ b/crates/panorama-daemon/src/node.rs @@ -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(), })), )) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2b0c4a..dbc21e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: