diff --git a/.gitignore b/.gitignore index 3f707c6..8bbead3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,3 @@ tmp/ # build output dist/ packaged/ - -# Generated prisma client -# Configured in schema.prisma -prisma/ \ No newline at end of file diff --git a/build.sh b/build.sh index 4ca5cec..43aaa05 100755 --- a/build.sh +++ b/build.sh @@ -42,13 +42,15 @@ cp package.json dist/ cp yarn.lock dist/ # see package.js -# Could embed here but it is already setup as a postinstall script cp ./rebuild-better-sqlite3.sh dist/ # todo: This is installing dev dependencies which, because of webpack, should not be needed. # When I use install --production, the final build complains it cannot find electron. Sigh. cd dist/ yarn +# todo: this only handles sqlite3... electron-rebuild (once fixed) would address all +# modules w/ native dependencies +yarn rebuild cd ../ diff --git a/package.json b/package.json index 8fe1ccd..da74649 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "start": "yarn dev:webpack & yarn dev:electron", "dev:webpack": "webpack-dev-server --config webpack.js --hot --inline --colors", "dev:electron": "electron src/electron/index.js", - "test": "mocha -r ts-node/register -r esm src/**/*.test.ts", + "test": "mocha -r esm -r ts-node/register src/**/*.test.ts", "test:one": "mocha -r ts-node/register -r esm", - "postinstall": "./rebuild-better-sqlite3.sh" + "rebuild": "./rebuild-better-sqlite3.sh" }, "dependencies": { "ajv": "^8.6.2", @@ -17,20 +17,13 @@ "better-sqlite3": "^7.4.3", "cuid": "^2.1.8", "electron-store": "^8.0.1", - "get-port": "^5.1.1", "klaw": "^3.0.0", - "ky": "^0.20.0", - "ky-universal": "^0.8.1", + "ky": "^0.28.6", + "ky-universal": "^0.9.1", "lodash": "^4.17.20", "luxon": "^1.24.1", "mime": "^2.5.2", - "mkdirp": "^1.0.4", - "remark": "^12.0.0", - "remark-gfm": "^2.0.0", - "remark-parse": "^8.0.3", - "remark-stringify": "^8.1.1", - "ts-mdast": "^1.0.0", - "unified": "^9.1.0" + "mkdirp": "^1.0.4" }, "devDependencies": { "@types/better-sqlite3": "^5.4.0", @@ -39,10 +32,10 @@ "@types/luxon": "^1.24.3", "@types/mkdirp": "^1.0.1", "@types/mocha": "^7.0.2", - "@types/prismjs": "^1.16.1", "@types/react": "^17.0.27", "@types/react-dom": "^17.0.9", "@types/rehype-react": "^4.0.0", + "@udecode/plate": "^5.3.0", "chai": "^4.2.0", "chai-json-schema-ajv": "^5.2.4", "css-loader": "^3.6.0", @@ -56,23 +49,31 @@ "html-webpack-plugin": "^4.3.0", "mobx": "^5.15.4", "mobx-react-lite": "^2.0.7", - "mocha": "^8.0.1", - "prismjs": "^1.21.0", + "mocha": "^9.1.3", "react": "^17.0.2", "react-day-picker": "^7.4.10", "react-dom": "^17.0.2", - "rehype-react": "^6.1.0", - "remark-rehype": "^7.0.0", + "rehype-react": "^7.0.3", + "remark": "^14.0.1", + "remark-gfm": "^3.0.0", + "remark-parse": "^8.0.3", + "remark-rehype": "^10.0.0", "remark-slate-transformer": "^0.4.1", - "slate": "^0.65.3", - "slate-history": "^0.65.3", - "slate-react": "^0.65.3", + "remark-stringify": "^8.1.1", + "remark-unwrap-images": "^3.0.0", + "slate": "^0.66.5", + "slate-history": "^0.66.0", + "slate-hyperscript": "^0.66.0", + "slate-react": "^0.66.7", "source-map-loader": "^1.0.0", "spectre.css": "^0.5.8", "style-loader": "^1.2.1", + "styled-components": "^5.3.1", "ts-loader": "^7.0.5", - "ts-node": "^8.10.2", + "ts-mdast": "^1.0.0", + "ts-node": "^10.3.0", "typescript": "^4.3.5", + "unified": "^10.1.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" diff --git a/src/electron/index.js b/src/electron/index.js index 7a255bb..8dae170 100644 --- a/src/electron/index.js +++ b/src/electron/index.js @@ -33,7 +33,6 @@ let dbfile = settings.get(DATABASE_URL); function setDatabaseUrl(url) { if (!url) throw new Error('setDatabaseUrl called with null or empty string'); - // todo: validate it can be loaded (or created) by Prisma client settings.set(DATABASE_URL, url); } diff --git a/src/error.tsx b/src/error.tsx new file mode 100644 index 0000000..e6a21bd --- /dev/null +++ b/src/error.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { Pane, Alert } from "evergreen-ui"; + +interface State { + hasError: boolean; + error: any; +} + +export default class ErrorBoundary extends React.Component { + constructor(props: any) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: any) { + // Update state so the next render will show the fallback UI. + return { hasError: true, error }; + } + + componentDidCatch(error: any, errorInfo: any) { + // You can also log the error to an error reporting service + console.error("top level error boundary reached:", error, errorInfo); + } + + renderError() { + let errStr: string = "Unable to parse error for display. Check console? :|"; + let stack: string = ""; + try { + if (this.state.error instanceof Error) { + errStr = this.state.error.message; + stack = this.state.error.stack || ""; + } else if (typeof this.state.error === "string") { + errStr = this.state.error; + } else { + errStr = JSON.stringify(this.state.error, null, 2); + } + errStr = JSON.stringify(this.state.error, null, 2); + } catch (err) { + console.error( + "Error parsing error to string in top-level Error boundary" + ); + } + + return ( + + +

There was an unhandled error that crashed the app

+
{errStr}
+
{stack}
+
+
+ ); + } + + render() { + if (this.state.hasError) { + return this.renderError(); + } + + return this.props.children; + } +} diff --git a/src/hooks/loadutils.ts b/src/hooks/loadutils.ts index 4ab13b0..b8d8e6e 100644 --- a/src/hooks/loadutils.ts +++ b/src/hooks/loadutils.ts @@ -5,7 +5,7 @@ import { useState, useCallback } from "react"; import { toaster } from "evergreen-ui"; // For errror handling. Also doesn't really need to be here -import ky from "ky-universal"; +import ky, { HTTPError } from "ky-universal"; // https://stackoverflow.com/questions/53215285/how-can-i-force-component-to-re-render-with-hooks-in-react function useForceUpdate() { @@ -106,7 +106,7 @@ export function withLoading( // Extract the API error message, if any. // todo: A convention for error structure (like title/details) // would probably support moving this logic into the client library - if (err instanceof ky.HTTPError) { + if (err instanceof HTTPError) { try { // Basically if the error comes from my backend, pull out the // error title then propagate. Client library should handle this. diff --git a/src/layout.tsx b/src/layout.tsx index 9436628..eccab72 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -1,7 +1,8 @@ import React, { PropsWithChildren } from "react"; import { Pane, Tablist, Tab } from "evergreen-ui"; +import ErrorBoundary from "./error"; -type View = 'journals' | 'documents' | 'preferences'; +type View = "journals" | "documents" | "preferences"; interface Props { selected?: View; @@ -13,33 +14,36 @@ const monoStyle = { }; export default function Layout(props: PropsWithChildren) { - // todo: optimize - const tabs = (['journals', 'documents', 'preferences'] as View[]).map((tab) => { - return ( - props.setSelected(tab)} - isSelected={props.selected === tab} - aria-controls={`panel-${tab}`} - > - {tab} - - ); - }); + const tabs = (["journals", "documents", "preferences"] as View[]).map( + (tab) => { + return ( + props.setSelected(tab)} + isSelected={props.selected === tab} + aria-controls={`panel-${tab}`} + > + {tab} + + ); + } + ); return ( - - - - # - chronicles + + + + + # + chronicles + + + {tabs} + - - {tabs} - + {props.children} - {props.children} - + ); } diff --git a/src/markdown.ts b/src/markdown.ts deleted file mode 100644 index 59d2862..0000000 --- a/src/markdown.ts +++ /dev/null @@ -1,100 +0,0 @@ -import unified from "unified"; -import remarkParse, { RemarkParseOptions } from "remark-parse"; -import remarkStringify, { RemarkStringifyOptions } from "remark-stringify"; -import { Root } from "ts-mdast"; -// https://github.com/inokawa/remark-slate-transformer/ -import { - remarkToSlate, - slateToRemark, - mdastToSlate, -} from "remark-slate-transformer"; -import { Node as SNode } from "slate"; - -export * from "ts-mdast"; - -// On remark versions > 12 I think they moved to micromark, which lost the -// gfm option and instead requires this package -import remarkGfm from "remark-gfm"; - -/** - * The types from remark-parse say it accepts no arguments for parse. - * Unfied says it accepts a VFile, of which string is one type. - * - * Here I'm simplifying to say it accepts a string, and returns an MDAST Root. - * - * I do not fully understand the unified ecosystem but these captures the subset I - * am currently interested in. - * - * TODO: Clean this all up. Figure out if I should care about the more sophisticated - * parser behaviors and whether I can fork and group all of this up into a Deno package. - */ -interface Parser { - parse(contents: string): Root; -} - -// Wrap parser creation so I can signify it takes Partial -function makeParser(opts: Partial) { - return unified().use(remarkParse, opts); -} - -function makeStringCompiler(opts: Partial): any { - return ( - unified() - // yeah idk if adding the parser is needed here - // or if adding the options, I think they are defaults - // ¯\_(ツ)_/¯ - .use(remarkParse, { commonmark: true, gfm: true }) - .use(remarkStringify, opts) - ); -} - -export const parser = makeParser({ - commonmark: true, - gfm: true, -}); - -/** - * NOTE: When I consolidated this file, this was only used in legacy contexts. - * Once a new indexing strategy is devised revisit this - * - * I added this so I could stringify Nodes after parsing, for indexing - * specific node text which might otherwise have child nodes. - * - * Its not the best idea, because it may not serialize back with the - * same exact text that came in. So, this is useful for POC but longer term... - * I'll need to do something a little smarter perhaps. - */ -export const stringifier = makeStringCompiler({ - commonmark: true, - gfm: true, -}); - -const slateToStringProcessor = unified() - .use(slateToRemark) - .use(remarkStringify); - -// Intermediate markdown parser, exported here so I could store the intermediate -// mdast state prior to parsing to Slate DOM for debugging purposes -const stringToMdast = unified().use(remarkParse); -const stringToSlateProcessor = stringToMdast.use(remarkToSlate); - -export function stringToSlate(text: string) { - // remarkToSlate must use a newer version, where file.result exists - // file.contents also exists here... maybe the compilers are overlapping since - // they modify in place? shrug - // https://github.com/unifiedjs/unified/releases/tag/9.0.0 - const output = stringToSlateProcessor.processSync(text); - return (output as any).result; -} - -export function slateToString(nodes: SNode[]) { - // per documentation https://github.com/inokawa/remark-slate-transformer/ - // slate value must be wrapped. Remark's parse expects a string while `run` - // operates on ASTs - const ast = slateToStringProcessor.runSync({ - type: "root", - children: nodes, - }); - - return slateToStringProcessor.stringify(ast); -} diff --git a/src/markdown/index.test.ts b/src/markdown/index.test.ts new file mode 100644 index 0000000..13e6274 --- /dev/null +++ b/src/markdown/index.test.ts @@ -0,0 +1,170 @@ +import { describe, it } from "mocha"; +import { expect } from "chai"; +import { stringToSlate } from "./"; + +// NOTE: I Never actually ran this because I can't get mocha to work... +// TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /Users/cloverich/code/pragma/src/markdown/index.test.ts +// ...although I have ts-node/register, esm, etc. Need to research... +// ...this ecosystem is quickly killing me +describe("Markdown to Slate conversion", function () { + it("unnests image tags", function () { + const input = ` +I have a few ideas to record: + +- My first idea +- My second idea +- My third idea is \_nested\_ + + - It seems to work! + + + +Now lets add an image: + +![](ckure3z1b00003u65tfr1m2ki..png) + +This works! _Seriously_, no **complaints**. + +`; + + const slateDOM = [ + { + type: "p", + children: [ + { + text: "I have a few ideas to record:", + }, + ], + }, + { + type: "ul", + children: [ + { + type: "li", + children: [ + { + type: "lic", + children: [ + { + text: "My first idea ", + }, + ], + }, + ], + checked: null, + spread: false, + }, + { + type: "li", + children: [ + { + type: "lic", + children: [ + { + text: "My second idea", + }, + ], + }, + ], + checked: null, + spread: false, + }, + { + type: "li", + children: [ + { + type: "lic", + children: [ + { + text: "My third idea is ", + }, + { + text: "_", + }, + { + text: "nested", + }, + { + text: "_", + }, + ], + }, + { + type: "ul", + children: [ + { + type: "li", + children: [ + { + type: "lic", + children: [ + { + text: "It seems to work!", + }, + ], + }, + ], + checked: null, + spread: false, + }, + ], + ordered: false, + start: null, + spread: false, + }, + ], + checked: null, + spread: true, + }, + ], + ordered: false, + start: null, + spread: false, + }, + { + type: "p", + children: [ + { + text: "Now lets add an image: ", + }, + ], + }, + { + type: "img", + url: "chronicles://ckure3z1b00003u65tfr1m2ki..png", + title: null, + alt: null, + children: [ + { + text: "", + }, + ], + }, + { + type: "p", + children: [ + { + text: "This works! ", + }, + { + italic: true, + text: "Seriously", + }, + { + text: ", no ", + }, + { + bold: true, + text: "complaints", + }, + { + text: ".", + }, + ], + }, + ]; + + const parsed = stringToSlate(input); + expect(parsed).to.deep.equal(slateDOM); + }); +}); diff --git a/src/markdown/index.ts b/src/markdown/index.ts new file mode 100644 index 0000000..8cc57c6 --- /dev/null +++ b/src/markdown/index.ts @@ -0,0 +1,57 @@ +import { unified } from "unified"; +import remarkParse from "remark-parse"; +import remarkGfm from "remark-gfm"; +import remarkStringify from "remark-stringify"; +import remarkUnwrapImages from "remark-unwrap-images"; +import { remarkToSlate, slateToRemark } from "./remark-slate-transformer"; +import { Node as SNode } from "slate"; + +export * from "ts-mdast"; + +const stringifier = unified().use(remarkStringify); +const parser = unified().use(remarkParse).use(remarkGfm); + +const slateToStringProcessor = unified() + .use(slateToRemark) + .use(remarkStringify); + +const stringToSlateProcessor = parser + .use(remarkUnwrapImages) + .use(remarkToSlate); + +export function mdastToString(mdast: any): string { + // todo: types + return stringifier.stringify(mdast) as any; +} + +export function stringToMdast(text: string) { + return parser.parse(text); +} + +export function stringToSlate(text: string) { + // remarkToSlate must use a newer version, where file.result exists + // file.contents also exists here... maybe the compilers are overlapping since + // they modify in place? shrug + // https://github.com/unifiedjs/unified/releases/tag/9.0.0 + // const output = stringToSlateProcessor.processSync(text); + const mdast = stringToSlateProcessor.parse(text); + // console.log("mdast", JSON.stringify(mdast, null, 2)); + const transformed = stringToSlateProcessor.runSync(mdast); + // console.log("transformed", JSON.stringify(transformed, null, 2)); + const output = stringToSlateProcessor.stringify(transformed); + // console.log("output", JSON.stringify(output, null, 2)); + // return (output as any).result; + return output as any; +} + +export function slateToString(nodes: SNode[]): string { + // per documentation https://github.com/inokawa/remark-slate-transformer/ + // slate value must be wrapped. Remark's parse expects a string while `run` + // operates on ASTs + const ast = slateToStringProcessor.runSync({ + type: "root", + children: nodes, + }); + + return slateToStringProcessor.stringify(ast) as any; +} diff --git a/src/markdown/remark-slate-transformer/README.md b/src/markdown/remark-slate-transformer/README.md new file mode 100644 index 0000000..7554350 --- /dev/null +++ b/src/markdown/remark-slate-transformer/README.md @@ -0,0 +1,9 @@ +This is a partial fork of https://github.com/inokawa/remark-slate-transformer + +I did this to quickly modify some of the output node types when converting mdast to slate. + +If it works, consider formally forking, or opening a PR to the original repo and in particular: + +https://github.com/inokawa/remark-slate-transformer/issues/37 +https://github.com/inokawa/remark-slate-transformer/issues/31 +https://github.com/inokawa/remark-slate-transformer/issues/67 \ No newline at end of file diff --git a/src/markdown/remark-slate-transformer/index.ts b/src/markdown/remark-slate-transformer/index.ts new file mode 100644 index 0000000..f1b4005 --- /dev/null +++ b/src/markdown/remark-slate-transformer/index.ts @@ -0,0 +1,6 @@ +export { default as remarkToSlate } from "./plugins/remark-to-slate"; +export { default as slateToRemark } from "./plugins/slate-to-remark"; +// export { default as slateToRemarkLegacy } from "./plugins/slate0.47-to-remark"; +// export { default as remarkToSlateLegacy } from "./plugins/remark-to-slate0.47"; +// export { mdastToSlate } from "./transformers/mdast-to-slate"; +export { slateToMdast } from "./transformers/slate-to-mdast"; diff --git a/src/markdown/remark-slate-transformer/models/mdast.ts b/src/markdown/remark-slate-transformer/models/mdast.ts new file mode 100644 index 0000000..795cdaf --- /dev/null +++ b/src/markdown/remark-slate-transformer/models/mdast.ts @@ -0,0 +1,224 @@ +// ref: https://github.com/syntax-tree/mdast + +export interface Parent { + children: Content[]; +} + +export interface Literal { + value: string; +} + +export interface Root extends Parent { + type: "root"; +} + +export interface Paragraph extends Parent { + type: "paragraph"; + children: PhrasingContent[]; +} + +export interface Heading extends Parent { + type: "heading"; + depth: 1 | 2 | 3 | 4 | 5 | 6; + children: PhrasingContent[]; +} + +export interface ThematicBreak { + type: "thematicBreak"; +} + +export interface Blockquote extends Parent { + type: "blockquote"; + children: BlockContent[]; +} + +export interface List extends Parent { + type: "list"; + ordered?: boolean; + start?: number; + spread?: boolean; + children: ListContent[]; +} + +export interface ListItem extends Parent { + type: "listItem"; + checked?: boolean; + spread?: boolean; + children: BlockContent[]; +} + +export interface Table extends Parent { + type: "table"; + align?: AlignType[]; + children: TableContent[]; +} + +export interface TableRow extends Parent { + type: "tableRow"; + children: RowContent[]; +} + +export interface TableCell extends Parent { + type: "tableCell"; + children: PhrasingContent[]; +} + +export interface HTML extends Literal { + type: "html"; +} + +export interface Code extends Literal { + type: "code"; + lang?: string; + meta?: string; +} + +export interface YAML extends Literal { + type: "yaml"; +} + +export interface TOML extends Literal { + type: "toml"; +} + +export interface Definition extends Association, Resource { + type: "definition"; +} + +export interface FootnoteDefinition extends Parent, Association { + type: "footnoteDefinition"; + children: BlockContent[]; +} + +export interface Text extends Literal { + type: "text"; +} + +export interface Emphasis extends Parent { + type: "emphasis"; + children: PhrasingContent[]; +} + +export interface Strong extends Parent { + type: "strong"; + children: PhrasingContent[]; +} + +export interface Delete extends Parent { + type: "delete"; + children: PhrasingContent[]; +} + +export interface InlineCode extends Literal { + type: "inlineCode"; +} + +export interface Break { + type: "break"; +} + +export interface Link extends Parent, Resource { + type: "link"; + children: StaticPhrasingContent[]; +} + +export interface Image extends Resource, Alternative { + type: "image"; +} + +export interface LinkReference extends Parent, Reference { + type: "linkReference"; + children: StaticPhrasingContent[]; +} + +export interface ImageReference extends Reference, Alternative { + type: "imageReference"; +} + +export interface Footnote extends Parent { + type: "footnote"; + children: PhrasingContent[]; +} + +export interface FootnoteReference extends Association { + type: "footnoteReference"; +} + +export interface Math extends Literal { + type: "math"; +} + +export interface InlineMath extends Literal { + type: "inlineMath"; +} + +export interface Resource { + url: string; + title?: string; +} + +export interface Association { + identifier: string; + label?: string; +} + +export interface Reference extends Association { + referenceType: ReferenceType; +} + +export interface Alternative { + alt?: string; +} + +export type Content = + | TopLevelContent + | ListContent + | TableContent + | RowContent + | PhrasingContent; + +export type TopLevelContent = + | BlockContent + | FrontmatterContent + | DefinitionContent; + +export type BlockContent = + | Paragraph + | Heading + | ThematicBreak + | Blockquote + | List + | Table + | HTML + | Code + | Math; + +export type FrontmatterContent = YAML | TOML; + +export type DefinitionContent = Definition | FootnoteDefinition; + +export type ListContent = ListItem; + +export type TableContent = TableRow; + +export type RowContent = TableCell; + +export type PhrasingContent = StaticPhrasingContent | Link | LinkReference; + +export type StaticPhrasingContent = + | Text + | Emphasis + | Strong + | Delete + | HTML + | InlineCode + | Break + | Image + | ImageReference + | Footnote + | FootnoteReference + | InlineMath; + +export type AlignType = "left" | "right" | "center" | null; + +export type ReferenceType = "shortcut" | "collapsed" | "full"; diff --git a/src/markdown/remark-slate-transformer/models/slate.ts b/src/markdown/remark-slate-transformer/models/slate.ts new file mode 100644 index 0000000..5fa9d22 --- /dev/null +++ b/src/markdown/remark-slate-transformer/models/slate.ts @@ -0,0 +1,8 @@ +// ref: https://docs.slatejs.org/concepts/11-typescript + +import * as slate from "slate"; + +export type Node = Editor | Element | Text; +export type Editor = slate.Editor; +export type Element = slate.Element & { type: string }; +export type Text = slate.Text; diff --git a/src/markdown/remark-slate-transformer/plugins/remark-to-slate.ts b/src/markdown/remark-slate-transformer/plugins/remark-to-slate.ts new file mode 100644 index 0000000..de9a79b --- /dev/null +++ b/src/markdown/remark-slate-transformer/plugins/remark-to-slate.ts @@ -0,0 +1,8 @@ +import { mdastToSlate } from "../transformers/mdast-to-slate"; + +export default function plugin() { + // @ts-ignore + this.Compiler = function (node: any) { + return mdastToSlate(node); + }; +} diff --git a/src/markdown/remark-slate-transformer/plugins/slate-to-remark.ts b/src/markdown/remark-slate-transformer/plugins/slate-to-remark.ts new file mode 100644 index 0000000..ffd4c1f --- /dev/null +++ b/src/markdown/remark-slate-transformer/plugins/slate-to-remark.ts @@ -0,0 +1,12 @@ +import type { Plugin } from "unified"; +import { slateToMdast } from "../transformers/slate-to-mdast"; + +type Settings = {}; + +const plugin: Plugin<[Settings?]> = function (settings?: Settings) { + // @ts-ignore + return function (node: any) { + return slateToMdast(node); + }; +}; +export default plugin; diff --git a/src/markdown/remark-slate-transformer/transformers/mdast-to-slate.ts b/src/markdown/remark-slate-transformer/transformers/mdast-to-slate.ts new file mode 100644 index 0000000..7c2f7c0 --- /dev/null +++ b/src/markdown/remark-slate-transformer/transformers/mdast-to-slate.ts @@ -0,0 +1,454 @@ +import * as slate from "../models/slate"; +import * as mdast from "../models/mdast"; + +// NOTE: added +import { prefixUrl } from "../util"; + +// NOTE: added, and a good example of what changes I would want to make to this library! +import { + ELEMENT_LI, + ELEMENT_LIC, + ELEMENT_OL, + ELEMENT_TODO_LI, + ELEMENT_UL, + ELEMENT_CODE_BLOCK, +} from "@udecode/plate"; // todo: sub-package which has only elements? + +export type Decoration = { + [key in ( + | mdast.Emphasis + | mdast.Strong + | mdast.Delete + | mdast.InlineCode + )["type"]]?: true; +}; + +export function mdastToSlate(node: mdast.Root): slate.Node[] { + return createSlateRoot(node); +} + +function createSlateRoot(root: mdast.Root): slate.Node[] { + return convertNodes(root.children, {}); +} + +function convertNodes(nodes: mdast.Content[], deco: Decoration): slate.Node[] { + if (nodes.length === 0) { + return [{ text: "" }]; + } + + return nodes.reduce((acc, node) => { + acc.push(...createSlateNode(node, deco)); + return acc; + }, []); +} + +// NOTE: Added +const DECORATION_MAPPING = { + emphasis: "italic", + strong: "bold", + delete: "strikethrough", + inlineCode: "code", +}; + +function createSlateNode(node: mdast.Content, deco: Decoration): SlateNode[] { + switch (node.type) { + case "paragraph": + return [createParagraph(node, deco)]; + case "heading": + return [createHeading(node, deco)]; + case "thematicBreak": + return [createThematicBreak(node)]; + case "blockquote": + return [createBlockquote(node, deco)]; + case "list": + return [createList(node, deco)]; + case "listItem": + return [createListItem(node, deco)]; + case ELEMENT_LIC as any: + return [createListItemChild(node, deco)]; + case "table": + return [createTable(node, deco)]; + case "tableRow": + return [createTableRow(node, deco)]; + case "tableCell": + return [createTableCell(node, deco)]; + case "html": + return [createHtml(node)]; + case "code": + return [createCode(node)]; + case "yaml": + return [createYaml(node)]; + case "toml": + return [createToml(node)]; + case "definition": + return [createDefinition(node)]; + case "footnoteDefinition": + return [createFootnoteDefinition(node, deco)]; + case "text": + return [createText(node.value, deco)]; + case "emphasis": + case "strong": + case "delete": { + const { type, children } = node; + return children.reduce((acc, n) => { + acc.push( + ...createSlateNode(n, { ...deco, [DECORATION_MAPPING[type]]: true }) + ); + return acc; + }, []); + } + case "inlineCode": { + const { type, value } = node; + return [createText(value, { ...deco, [DECORATION_MAPPING[type]]: true })]; + } + case "break": + return [createBreak(node)]; + case "link": + return [createLink(node, deco)]; + case "image": + return [createImage(node)]; + case "linkReference": + return [createLinkReference(node, deco)]; + case "imageReference": + return [createImageReference(node)]; + case "footnote": + return [createFootnote(node, deco)]; + case "footnoteReference": + return [createFootnoteReference(node)]; + case "math": + return [createMath(node)]; + case "inlineMath": + return [createInlineMath(node)]; + default: + const _: never = node; + break; + } + return []; +} + +export type Paragraph = ReturnType; + +function createParagraph(node: mdast.Paragraph, deco: Decoration) { + const { type, children } = node; + return { + type: "p", // NOTE: plate's DOM expects `p`, not `paragraph` + children: convertNodes(children, deco), + }; +} + +export type Heading = ReturnType; + +function createHeading(node: mdast.Heading, deco: Decoration) { + const { type, children, depth } = node; + return { + type, + depth, + children: convertNodes(children, deco), + }; +} + +export type ThematicBreak = ReturnType; + +function createThematicBreak(node: mdast.ThematicBreak) { + return { + type: node.type, + children: [{ text: "" }], + }; +} + +export type Blockquote = ReturnType; + +function createBlockquote(node: mdast.Blockquote, deco: Decoration) { + return { + type: node.type, + children: convertNodes(node.children, deco), + }; +} + +export type List = ReturnType; + +function createList(node: mdast.List, deco: Decoration) { + const { type, children, ordered, start, spread } = node; + return { + type: ordered ? ELEMENT_OL : ELEMENT_UL, // todo: support check list items? No, support those via different function + children: convertNodes(children, deco), + ordered, + start, + spread, + }; +} + +export type ListItem = ReturnType; + +function createListItem(node: mdast.ListItem, deco: Decoration) { + const { type, children, checked, spread } = node; + + // NOTE: Added + // Plate li children must have an lic type unless they are another list, + // otherwise the plugin does really wierd stuff + children.forEach((child) => { + child.type = child.type === "paragraph" ? ELEMENT_LIC : (child.type as any); + }); + + return { + type: ELEMENT_LI, + children: convertNodes(children, deco), + checked, + spread, + }; +} + +// NOTE: Added this custom to create ELEMENT_LIC according to plates custom list item handling... +function createListItemChild(node: any, deco: Decoration) { + // NOTE: shrug see notes in createListItem + const { type, children } = node; + + return { + type: ELEMENT_LIC, + children: convertNodes(children, deco), + }; +} + +export type Table = ReturnType; + +function createTable(node: mdast.Table, deco: Decoration) { + const { type, children, align } = node; + return { + type, + children: convertNodes(children, deco), + align, + }; +} + +export type TableRow = ReturnType; + +function createTableRow(node: mdast.TableRow, deco: Decoration) { + const { type, children } = node; + return { + type, + children: convertNodes(children, deco), + }; +} + +export type TableCell = ReturnType; + +function createTableCell(node: mdast.TableCell, deco: Decoration) { + const { type, children } = node; + return { + type, + children: convertNodes(children, deco), + }; +} + +export type Html = ReturnType; + +function createHtml(node: mdast.HTML) { + const { type, value } = node; + return { + type, + children: [{ text: value }], + }; +} + +export type Code = ReturnType; + +function createCode(node: mdast.Code) { + const { type, value, lang, meta } = node; + return { + type: ELEMENT_CODE_BLOCK, + lang, + meta, + children: [{ text: value }], + }; +} + +export type Yaml = ReturnType; + +function createYaml(node: mdast.YAML) { + const { type, value } = node; + return { + type, + children: [{ text: value }], + }; +} + +export type Toml = ReturnType; + +function createToml(node: mdast.TOML) { + const { type, value } = node; + return { + type, + children: [{ text: value }], + }; +} + +export type Math = ReturnType; + +function createMath(node: mdast.Math) { + const { type, value } = node; + return { + type, + children: [{ text: value }], + }; +} + +export type InlineMath = ReturnType; + +function createInlineMath(node: mdast.InlineMath) { + const { type, value } = node; + return { + type, + children: [{ text: value }], + }; +} + +export type Definition = ReturnType; + +function createDefinition(node: mdast.Definition) { + const { type, identifier, label, url, title } = node; + return { + type, + identifier, + label, + url, + title, + children: [{ text: "" }], + }; +} + +export type FootnoteDefinition = ReturnType; + +function createFootnoteDefinition( + node: mdast.FootnoteDefinition, + deco: Decoration +) { + const { type, children, identifier, label } = node; + return { + type, + children: convertNodes(children, deco), + identifier, + label, + }; +} + +export type Text = ReturnType; + +function createText(text: string, deco: Decoration) { + return { + ...deco, + text, + }; +} + +export type Break = ReturnType; + +function createBreak(node: mdast.Break) { + return { + type: node.type, + children: [{ text: "" }], + }; +} + +export type Link = ReturnType; + +function createLink(node: mdast.Link, deco: Decoration) { + const { type, children, url, title } = node; + return { + type: "a", // NOTE: Default plate link component uses "a" + children: convertNodes(children, deco), + url, + title, + }; +} + +export type Image = ReturnType; + +function createImage(node: mdast.Image) { + const { type, url, title, alt } = node; + return { + // NOTE: I changed this from simply type, which forwarded the incoming "image" type, + // to "img", which plate expects + type: "img", + // NOTE: I modify url's here which is a bit silly but i'm in hack-it-in mode so :| + url: prefixUrl(url), + title, + alt, + children: [{ text: "" }], + }; +} + +export type LinkReference = ReturnType; + +function createLinkReference(node: mdast.LinkReference, deco: Decoration) { + const { type, children, referenceType, identifier, label } = node; + return { + type, + children: convertNodes(children, deco), + referenceType, + identifier, + label, + }; +} + +export type ImageReference = ReturnType; + +function createImageReference(node: mdast.ImageReference) { + const { type, alt, referenceType, identifier, label } = node; + return { + type, + alt, + referenceType, + identifier, + label, + children: [{ text: "" }], + }; +} + +export type Footnote = ReturnType; + +function createFootnote(node: mdast.Footnote, deco: Decoration) { + const { type, children } = node; + return { + type, + children: convertNodes(children, deco), + }; +} + +export type FootnoteReference = ReturnType; + +function createFootnoteReference(node: mdast.FootnoteReference) { + const { type, identifier, label } = node; + return { + type, + identifier, + label, + children: [{ text: "" }], + }; +} + +export type SlateNode = + | Paragraph + | Heading + | ThematicBreak + | Blockquote + | List + | ListItem + | Table + | TableRow + | TableCell + | Html + | Code + | Yaml + | Toml + | Definition + | FootnoteDefinition + | Text + | Break + | Link + | Image + | LinkReference + | ImageReference + | Footnote + | FootnoteReference + | Math + | InlineMath; diff --git a/src/markdown/remark-slate-transformer/transformers/slate-to-mdast.ts b/src/markdown/remark-slate-transformer/transformers/slate-to-mdast.ts new file mode 100644 index 0000000..b744b6e --- /dev/null +++ b/src/markdown/remark-slate-transformer/transformers/slate-to-mdast.ts @@ -0,0 +1,508 @@ +import * as unistLib from "unist"; +import * as slate from "../models/slate"; +import * as mdast from "../models/mdast"; +import * as slateInternal from "./mdast-to-slate"; + +import { Node as SNode } from "slate"; + +// NOTE: added +import { unPrefixUrl } from "../util"; + +// NOTE: added, and a good example of what changes I would want to make to this library! +import { + ELEMENT_LI, + ELEMENT_LIC, + ELEMENT_OL, + ELEMENT_TODO_LI, + ELEMENT_UL, + ELEMENT_CODE_BLOCK, +} from "@udecode/plate"; // todo: sub-package which has only elements? + +// NOTE: Changed these, they were just mirroring mdasts' before +// which doesn't make sense +type DecorationType = keyof Decoration; + +type Decoration = { + italic: true | undefined; + bold: true | undefined; + strikethrough: true | undefined; + code: true | undefined; +}; + +const DecorationMapping = { + italic: "emphasis", + bold: "strong", + strikethrough: "delete", + code: "inlineCode", +}; + +type TextOrDecoration = + | mdast.Text + | mdast.Emphasis + | mdast.Strong + | mdast.Delete + | mdast.InlineCode; + +export function slateToMdast(node: slate.Node): unistLib.Node { + return createMdastRoot(node); +} + +function createMdastRoot(node: slate.Node): unistLib.Node { + const root: mdast.Root = { + type: "root", + children: convertNodes((node as any).children) as mdast.Root["children"], + }; + return root as any as unistLib.Node; +} + +function convertNodes(nodes: slate.Node[]): unistLib.Node[] { + const mdastNodes: unistLib.Node[] = []; + let textQueue: slateInternal.Text[] = []; + for (let i = 0; i <= nodes.length; i++) { + const n = nodes[i] as slateInternal.SlateNode; + if (n && isText(n)) { + textQueue.push(n); + } else { + const mdastTexts: TextOrDecoration[] = []; + const starts: DecorationType[] = []; + let textTemp: string = ""; + for (let j = 0; j < textQueue.length; j++) { + const cur: any = textQueue[j]; + textTemp += cur.text; + + const prevStartsStr = starts.toString(); + + const prev: any = textQueue[j - 1]; + const next: any = textQueue[j + 1]; + const ends: DecorationType[] = []; + Object.keys(DecorationMapping).forEach((k: any) => { + if (cur[k]) { + if (!prev || !prev[k]) { + starts.push(k); + } + if (!next || !next[k]) { + ends.push(k); + } + } + }); + + const endsToRemove = starts.reduce< + { key: DecorationType; index: number }[] + >((acc, k, kIndex) => { + if (ends.includes(k)) { + acc.push({ key: k, index: kIndex }); + } + return acc; + }, []); + + if (starts.length > 0) { + let bef = ""; + let aft = ""; + if ( + endsToRemove.length === 1 && + prevStartsStr !== starts.toString() && + starts.length - endsToRemove.length === 0 + ) { + while (textTemp.startsWith(" ")) { + bef += " "; + textTemp = textTemp.slice(1); + } + while (textTemp.endsWith(" ")) { + aft += " "; + textTemp = textTemp.slice(0, -1); + } + } + let res: TextOrDecoration = { + type: "text", + value: textTemp, + }; + textTemp = ""; + const startsReversed = starts.slice().reverse(); + startsReversed.forEach((k) => { + switch (k) { + case "code": + res = { + type: "inlineCode", + value: (res as any).value, + }; + break; + case "bold": + case "italic": + case "strikethrough": + res = { + type: DecorationMapping[k] as any, + children: [res], + }; + break; + default: + const _: never = k; + break; + } + }); + const arr: TextOrDecoration[] = []; + if (bef.length > 0) { + arr.push({ type: "text", value: bef }); + } + arr.push(res); + if (aft.length > 0) { + arr.push({ type: "text", value: aft }); + } + mdastTexts.push(...arr); + } + + if (endsToRemove.length > 0) { + endsToRemove.reverse().forEach((e) => { + starts.splice(e.index, 1); + }); + } else { + mdastTexts.push({ type: "text", value: textTemp }); + textTemp = ""; + } + } + if (textTemp) { + mdastTexts.push({ type: "text", value: textTemp }); + textTemp = ""; + } + + mdastNodes.push(...(mergeTexts(mdastTexts) as any as unistLib.Node[])); + textQueue = []; + if (!n) continue; + const node = createMdastNode(n); + if (node) { + mdastNodes.push(node as unistLib.Node); + } + } + } + + return mdastNodes; +} + +function createMdastNode( + node: any //Exclude --> as any because the switch thinks node.type is a string +): Exclude | null { + switch (node.type) { + case ELEMENT_LIC: // NOTE: added. + case "paragraph": + case "p": + return createParagraph(node); + case "heading": + return createHeading(node); + case "thematicBreak": + return createThematicBreak(node); + case "blockquote": + return createBlockquote(node); + case "list": + case ELEMENT_UL: // NOTE: added + case ELEMENT_OL: // NOTE: added + return createList(node); + case "listItem": + case ELEMENT_LI: // NOTE: added + return createListItem(node); + case "table": + return createTable(node); + case "tableRow": + return createTableRow(node); + case "tableCell": + return createTableCell(node); + case "html": + return createHtml(node); + case "code": // NOTE: don't think this is used by plate + case ELEMENT_CODE_BLOCK: + return createCode(node); + case "yaml": + return createYaml(node); + case "toml": + return createToml(node); + case "definition": + return createDefinition(node); + case "footnoteDefinition": + return createFootnoteDefinition(node); + case "break": + return createBreak(node); + case "link": + case "a" as any: // NOTE: added "a" here + return createLink(node); + case "image": + // NOTE: I MODIFIED next line to also catch img as image + case "img": + return createImage(node); + case "linkReference": + return createLinkReference(node); + case "imageReference": + return createImageReference(node); + case "footnote": + return createFootnote(node); + case "footnoteReference": + return creatFootnoteReference(node); + case "math": + return createMath(node); + case "inlineMath": + return createInlineMath(node); + default: + console.warn("slateToMdast encountered unknown node type", node); + // @ts-ignore + const _: never = node; + break; + } + return null; +} + +function isText(node: slateInternal.SlateNode): node is slateInternal.Text { + return "text" in node; +} + +function mergeTexts(nodes: TextOrDecoration[]): TextOrDecoration[] { + const res: TextOrDecoration[] = []; + for (const cur of nodes) { + const last = res[res.length - 1]; + if (last && last.type === cur.type) { + if (last.type === "text") { + last.value += (cur as typeof last).value; + } else if (last.type === "inlineCode") { + last.value += (cur as typeof last).value; + } else { + last.children = mergeTexts( + last.children.concat( + (cur as typeof last).children + ) as TextOrDecoration[] + ); + } + } else { + if (cur.type === "text" && cur.value === "") continue; + res.push(cur); + } + } + return res; +} + +function createParagraph(node: slateInternal.Paragraph): mdast.Paragraph { + const { type, children } = node; + return { + type: "paragraph", + children: convertNodes(children) as any as mdast.Paragraph["children"], + }; +} + +function createHeading(node: slateInternal.Heading): mdast.Heading { + const { type, depth, children } = node; + return { + type, + depth, + children: convertNodes(children) as any as mdast.Heading["children"], + }; +} + +function createThematicBreak( + node: slateInternal.ThematicBreak +): mdast.ThematicBreak { + const { type } = node; + return { + type, + }; +} + +function createBlockquote(node: slateInternal.Blockquote): mdast.Blockquote { + const { type, children } = node; + return { + type, + children: convertNodes(children) as any as mdast.Blockquote["children"], + }; +} + +function createList(node: slateInternal.List): mdast.List { + const { type, ordered, start, spread, children } = node; + return { + type: "list", + ordered, + start, + spread, + children: convertNodes(children) as any as mdast.List["children"], + }; +} + +function createListItem(node: slateInternal.ListItem): mdast.ListItem { + const { type, checked, spread, children } = node; + return { + type: "listItem", + checked, + spread, + children: convertNodes(children) as any as mdast.ListItem["children"], + }; +} + +function createTable(node: slateInternal.Table): mdast.Table { + const { type, align, children } = node; + return { + type, + align, + children: convertNodes(children) as any as mdast.Table["children"], + }; +} + +function createTableRow(node: slateInternal.TableRow): mdast.TableRow { + const { type, children } = node; + return { + type, + children: convertNodes(children) as any as mdast.TableRow["children"], + }; +} + +function createTableCell(node: slateInternal.TableCell): mdast.TableCell { + const { type, children } = node; + return { + type, + children: convertNodes(children) as any as mdast.TableCell["children"], + }; +} + +function createHtml(node: slateInternal.Html): mdast.HTML { + const { type, children } = node; + return { + type, + value: children[0].text, + }; +} + +function createCode(node: slateInternal.Code): mdast.Code { + const { lang, meta } = node; + return { + type: "code", + lang, + meta, + // NOTE: Added this .. the code as existed doesn't make sense? + // code is handled as marks in other parts of the codebase. Hmm. + value: SNode.string(node), //[0].text, + }; +} + +function createYaml(node: slateInternal.Yaml): mdast.YAML { + const { type, children } = node; + return { + type, + value: children[0].text, + }; +} + +function createToml(node: slateInternal.Toml): mdast.TOML { + const { type, children } = node; + return { + type, + value: children[0].text, + }; +} + +function createDefinition(node: slateInternal.Definition): mdast.Definition { + const { type, identifier, label, url, title } = node; + return { + type, + identifier, + label, + url, + title, + }; +} + +function createFootnoteDefinition( + node: slateInternal.FootnoteDefinition +): mdast.FootnoteDefinition { + const { type, identifier, label, children } = node; + return { + type, + identifier, + label, + children: convertNodes( + children + ) as any as mdast.FootnoteDefinition["children"], + }; +} + +function createBreak(node: slateInternal.Break): mdast.Break { + const { type } = node; + return { + type, + }; +} + +function createLink(node: slateInternal.Link): mdast.Link { + const { type, url, title, children } = node; + return { + type: "link", // note: changes from type to type: "link" so it can accept "a", see the switch statement + url, // note: converted, "as any" added because mdast.Link thinks its url and not link? + title, + children: convertNodes(children) as any as mdast.Link["children"], + } as any; +} + +function createImage(node: slateInternal.Image): mdast.Image { + const { type, url, title, alt } = node; + return { + // NOTE: added this @ts-ignore line + // todo: replace "image" with a constant, like mdast.Image.type + // @ts-ignore + type: "image", // NOTE: added here because createImage may be called with type: 'img" -- convert to something mdast understands + url: unPrefixUrl(url), + title, + alt, + }; +} + +function createLinkReference( + node: slateInternal.LinkReference +): mdast.LinkReference { + const { type, identifier, label, referenceType, children } = node; + return { + type, + identifier, + label, + referenceType, + children: convertNodes(children) as any as mdast.LinkReference["children"], + }; +} + +function createImageReference( + node: slateInternal.ImageReference +): mdast.ImageReference { + const { type, identifier, label, alt, referenceType } = node; + return { + type, + identifier, + label, + alt, + referenceType, + }; +} + +function createFootnote(node: slateInternal.Footnote): mdast.Footnote { + const { type, children } = node; + return { + type, + children: convertNodes(children) as any as mdast.Footnote["children"], + }; +} + +function creatFootnoteReference( + node: slateInternal.FootnoteReference +): mdast.FootnoteReference { + const { type, identifier, label } = node; + return { + type, + identifier, + label, + }; +} + +function createMath(node: slateInternal.Math): mdast.Math { + const { type, children } = node; + return { + type, + value: children[0].text, + }; +} + +function createInlineMath(node: slateInternal.InlineMath): mdast.InlineMath { + const { type, children } = node; + return { + type, + value: children[0].text, + }; +} diff --git a/src/markdown/remark-slate-transformer/util.ts b/src/markdown/remark-slate-transformer/util.ts new file mode 100644 index 0000000..4b83c35 --- /dev/null +++ b/src/markdown/remark-slate-transformer/util.ts @@ -0,0 +1,32 @@ +/** + * NOTE: COPIED FROM editor/blocks/images.tsx + * + * For absolute image urls, prefix them with chronicles:// which will trigger + * the protocol handler in the main process, which as of now merely serves + * the file + * + * When implementing drag and drop and accounting for other legacy journals, + * many image files were absolute filepaths to various places on the filesystem + * + * todo: Upload and host all image files from a single directory + * + * @param url + * @returns + */ +export function prefixUrl(url: string) { + const isLocalPath = !url.startsWith("http"); + + if (isLocalPath) { + return "chronicles://" + url; + } else { + return url; + } +} + +export function unPrefixUrl(url: string) { + if (url.startsWith("chronicles://")) { + return url.slice(13); + } else { + return url; + } +} diff --git a/src/preload/importer/importChronicles.ts b/src/preload/importer/importChronicles.ts index 3618ffc..4d520d2 100644 --- a/src/preload/importer/importChronicles.ts +++ b/src/preload/importer/importChronicles.ts @@ -1,5 +1,5 @@ import { Files } from "../files"; -import { parser } from "../../markdown"; +import { stringToMdast } from "../../markdown"; import { shouldIndexDay } from "./indexer"; import fs from "fs"; import path from "path"; @@ -8,7 +8,6 @@ import { DateTime } from "luxon"; import { configure } from "../client"; const client = configure("/who/cares.com"); -// Hmmm... maybe this is built in to Prisma client somehow async function findOrCreate(name: string) { const journals = await client.journals.list(); // if (journals.includes) @@ -82,7 +81,7 @@ async function loadDocument(filepath: string) { const contents = await Files.read(filepath); return { contents: contents, - mdast: parser.parse(contents), + mdast: stringToMdast(contents), }; } diff --git a/src/preload/importer/indexer.ts b/src/preload/importer/indexer.ts index 660bc3f..48de119 100644 --- a/src/preload/importer/indexer.ts +++ b/src/preload/importer/indexer.ts @@ -1,6 +1,5 @@ import path from "path"; -import { parser, stringifier } from "../../markdown"; -import { Root } from "../../markdown"; +import { stringToMdast, mdastToString, Root } from "../../markdown"; import { Database } from "better-sqlite3"; import { Files, PathStatsFile } from "../files"; import { DateTime } from "luxon"; @@ -58,7 +57,7 @@ class Indexer { let contents: string; try { - contents = stringifier.stringify(node); + contents = mdastToString(node); } catch (err: any) { throw new IndexParsingError(err); } @@ -94,7 +93,7 @@ class Indexer { * @param contents */ update = async (journal: string, date: string, contents: string) => { - const parsed = parser.parse(contents); + const parsed = stringToMdast(contents); const stmt = this.db.prepare( "DELETE FROM nodes where journal = :journal and date = :date" ); @@ -158,7 +157,7 @@ class Indexer { // todo: track parsing errors so you understand why your content // isn't showing up in your journal view (failed to index). try { - const parsed = parser.parse(contents); + const parsed = stringToMdast(contents); // BUG ALERT: I was passing `entry.path` as second argument, when it wanted the // filename, because it wants an ISODate: 2020-05-01, which is how we name files. diff --git a/src/views/edit/editor/blocks/images.tsx b/src/views/edit/editor/blocks/images.tsx deleted file mode 100644 index e157cf5..0000000 --- a/src/views/edit/editor/blocks/images.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import React from 'react'; -import { ReactEditor, RenderElementProps } from "slate-react"; -import { Transforms } from "slate"; -import { ImageElement, VideoElement } from '../../util'; -import { css } from 'emotion'; - -/** - * Create an image node and insert it at the current selection - */ -function insertImage(editor: ReactEditor, filepath: string) { - const image = { type: 'image', url: filepath, children: [{ text: '' }] } - Transforms.insertNodes(editor, image) - padDocument(editor); -} - -function insertVideo(editor: ReactEditor, filepath: string) { - const image = { type: 'video', url: filepath, children: [{ text: '' }] } - Transforms.insertNodes(editor, image) - padDocument(editor); - -} - -/** - * Add an extra paragraph node to protect against video or image elements being - * the last element in the document, which currently prevents user from entering - * more text. Ideally a listener at a higher level could observe for blocking elements - * and if the lsat element is not editable text or something, auto-insert a paragraph block. - */ -function padDocument(editor: ReactEditor) { - Transforms.insertNodes(editor, { type: 'paragraph', children: [{text: ''}]} as any); -} - -export function insertFile(editor: ReactEditor, filepath: string) { - const extension = filepath.split('.').pop(); - if (!extension) { - console.error('insertFile called but filepath did not contain an extension:', filepath); - return; - } - - if (imageExtensions.includes(extension)) { - return insertImage(editor, filepath) - } - - if (videoExtensions.includes(extension)) { - return insertVideo(editor, filepath); - } - - console.error('Unable to insertFile into Slate Editor of unknown extension: ', filepath); -} - - -/** - * For absolute image urls, prefix them with chronicles:// which will trigger - * the protocol handler in the main process, which as of now merely serves - * the file - * - * When implementing drag and drop and accounting for other legacy journals, - * many image files were absolute filepaths to various places on the filesystem - * - * todo: Upload and host all image files from a single directory - * - * @param url - * @returns - */ -function prefixUrl(url: string) { - const isLocalPath = !url.startsWith('http') - - if (isLocalPath) { - return 'chronicles://' + url; - } else { - return url; - } -} - - -interface ImageElementProps extends RenderElementProps { - element: ImageElement; -} - -interface VideoElementProps extends RenderElementProps { - element: VideoElement; -} - -export const Image = ({ attributes, children, element }: ImageElementProps) => { - return ( -
-
- -
- {children} -
- ) -} - -export const Video = ({ attributes, children, element }: VideoElementProps) => { - return ( -
-
-
- {children} -
- ) -} - -/** - * Does the URL end with a known image extension? - */ -export function isImageUrl (filepath: string) { - const extension = filepath.split('.').pop(); - if (!extension) return false; - return imageExtensions.includes(extension.toLowerCase()); -} - - -// Copied from this repo: https://github.com/arthurvr/image-extensions -// Which is an npm package that is just a json file -const imageExtensions = [ - "ase", - "art", - "bmp", - "blp", - "cd5", - "cit", - "cpt", - "cr2", - "cut", - "dds", - "dib", - "djvu", - "egt", - "exif", - "gif", - "gpl", - "grf", - "icns", - "ico", - "iff", - "jng", - "jpeg", - "jpg", - "jfif", - "jp2", - "jps", - "lbm", - "max", - "miff", - "mng", - "msp", - "nitf", - "ota", - "pbm", - "pc1", - "pc2", - "pc3", - "pcf", - "pcx", - "pdn", - "pgm", - "PI1", - "PI2", - "PI3", - "pict", - "pct", - "pnm", - "pns", - "ppm", - "psb", - "psd", - "pdd", - "psp", - "px", - "pxm", - "pxr", - "qfx", - "raw", - "rle", - "sct", - "sgi", - "rgb", - "int", - "bw", - "tga", - "tiff", - "tif", - "vtf", - "xbm", - "xcf", - "xpm", - "3dv", - "amf", - "ai", - "awg", - "cgm", - "cdr", - "cmx", - "dxf", - "e2d", - "egt", - "eps", - "fs", - "gbr", - "odg", - "svg", - "stl", - "vrml", - "x3d", - "sxd", - "v2d", - "vnd", - "wmf", - "emf", - "art", - "xar", - "png", - "webp", - "jxr", - "hdp", - "wdp", - "cur", - "ecw", - "iff", - "lbm", - "liff", - "nrrd", - "pam", - "pcx", - "pgf", - "sgi", - "rgb", - "rgba", - "bw", - "int", - "inta", - "sid", - "ras", - "sun", - "tga" -] - -// https://github.com/sindresorhus/video-extensions/blob/main/video-extensions.json -const videoExtensions = [ - "3g2", - "3gp", - "aaf", - "asf", - "avchd", - "avi", - "drc", - "flv", - "m2v", - "m3u8", - "m4p", - "m4v", - "mkv", - "mng", - "mov", - "mp2", - "mp4", - "mpe", - "mpeg", - "mpg", - "mpv", - "mxf", - "nsv", - "ogg", - "ogv", - "qt", - "rm", - "rmvb", - "roq", - "svi", - "vob", - "webm", - "wmv", - "yuv" -]; \ No newline at end of file diff --git a/src/views/edit/editor/blocks/markdown.tsx b/src/views/edit/editor/blocks/markdown.tsx deleted file mode 100644 index c5cb934..0000000 --- a/src/views/edit/editor/blocks/markdown.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import Prism from "prismjs"; -import React, { useCallback } from "react"; -import { Text } from "slate"; -import { css } from "emotion"; - -Prism.languages.markdown = Prism.languages.extend("markup", {}); -(Prism.languages as any).insertBefore("markdown", "prolog", { - blockquote: { pattern: /^>(?:[\t ]*>)*/m, alias: "punctuation" }, - code: [ - { pattern: /^(?: {4}|\t).+/m, alias: "keyword" }, - { pattern: /``.+?``|`[^`\n]+`/, alias: "keyword" }, - ], - title: [ - { - pattern: /\w+.*(?:\r?\n|\r)(?:==+|--+)/, - alias: "important", - inside: { punctuation: /==+$|--+$/ }, - }, - { - pattern: /(^\s*)#+.+/m, - lookbehind: !0, - alias: "important", - inside: { punctuation: /^#+|#+$/ }, - }, - ], - hr: { - pattern: /(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m, - lookbehind: !0, - alias: "punctuation", - }, - list: { - pattern: /(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m, - lookbehind: !0, - alias: "punctuation", - }, - "url-reference": { - pattern: - /!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/, - inside: { - variable: { pattern: /^(!?\[)[^\]]+/, lookbehind: !0 }, - string: /(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/, - punctuation: /^[\[\]!:]|[<>]/, - }, - alias: "url", - }, - bold: { - pattern: /(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, - lookbehind: !0, - inside: { punctuation: /^\*\*|^__|\*\*$|__$/ }, - }, - italic: { - pattern: /(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, - lookbehind: !0, - inside: { punctuation: /^[*_]|[*_]$/ }, - }, - url: { - pattern: - /!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/, - inside: { - variable: { pattern: /(!?\[)[^\]]+(?=\]$)/, lookbehind: !0 }, - string: { pattern: /"(?:\\.|[^"\\])*"(?=\)$)/ }, - }, - }, -}); -(Prism.languages.markdown.bold as any).inside.url = Prism.util.clone( - Prism.languages.markdown.url, -); -(Prism.languages.markdown.italic as any).inside.url = Prism.util.clone( - Prism.languages.markdown.url, -); -(Prism.languages.markdown.bold as any).inside.italic = Prism.util.clone( - Prism.languages.markdown.italic, -); -(Prism.languages.markdown.italic as any).inside.bold = Prism.util.clone( - Prism.languages.markdown.bold, -); - - -export const useDecorateMarkdown = () => { - return useCallback(([node, path]) => { - const ranges: any = []; - - if (!Text.isText(node)) { - return ranges; - } - - const getLength = (token: any) => { - if (typeof token === "string") { - return token.length; - } else if (typeof token.content === "string") { - return token.content.length; - } else { - return token.content.reduce((l: any, t: any) => l + getLength(t), 0); - } - }; - - const tokens = Prism.tokenize(node.text, Prism.languages.markdown); - let start = 0; - - for (const token of tokens) { - const length = getLength(token); - const end = start + length; - - if (typeof token !== "string") { - ranges.push({ - [token.type]: true, - anchor: { path, offset: start }, - focus: { path, offset: end }, - }); - } - - start = end; - } - - return ranges; - }, []); -} - -export const MarkdownLeaf = ({ attributes, children, leaf }: any) => { - return ( - - {children} - - ); -}; \ No newline at end of file diff --git a/src/views/edit/editor/blocks/menu.tsx b/src/views/edit/editor/blocks/menu.tsx deleted file mode 100644 index a6cb876..0000000 --- a/src/views/edit/editor/blocks/menu.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { PropsWithChildren, Ref } from 'react'; -import ReactDOM from 'react-dom'; -import { cx, css } from 'emotion'; -import { useRef, useEffect } from 'react'; -import { useSlate, ReactEditor } from 'slate-react'; -import { Editor, Range } from 'slate'; - -/** - * Contents started from: - * https://github.com/ianstormtaylor/slate/blob/main/site/components.tsx - * - * Partially used while developing another feature before rolling my own. - * Will revisit using these for the proper hovering toolbar. - */ - -interface BaseProps { - className: string - [key: string]: unknown -} - -type OrNull = T | null -type OrUndef = T | undefined; - -export const Menu = React.forwardRef( - ( - { className, ...props }: PropsWithChildren, - ref: Ref - ) => ( -
* { - display: inline-block; - } - & > * + * { - margin-left: 15px; - } - ` - )} - /> - ) -) - -export const Portal = ({ children }: PropsWithChildren) => { - return typeof document === 'object' - ? ReactDOM.createPortal(children, document.body) - : null -} - - - -export const HoveringToolbar = () => { - const ref = useRef() - const editor = useSlate() as ReactEditor; - - - - useEffect(() => { - const toolbar = ref.current - const { selection } = editor - - if (!toolbar) { - return - } - - if ( - !selection || - !ReactEditor.isFocused(editor as ReactEditor) || - Range.isCollapsed(selection) || - Editor.string(editor, selection) === '' - ) { - toolbar.removeAttribute('style') - return - } - - // todo: review whether doing this through Slate (like i do for links) is preferable - const domSelection = window.getSelection() - - // todo: handle null ref - const domRange = domSelection!.getRangeAt(0) - const rect = domRange.getBoundingClientRect() - toolbar.style.opacity = '1' - toolbar.style.top = `${rect.top + window.pageYOffset - toolbar.offsetHeight}px` - toolbar.style.left = `${rect.left + - window.pageXOffset - - toolbar.offsetWidth / 2 + - rect.width / 2}px` - }) - - return ( - - - {/* - - */} - - - ) -} \ No newline at end of file diff --git a/src/views/edit/editor/editor.tsx b/src/views/edit/editor/editor.tsx deleted file mode 100644 index 1734497..0000000 --- a/src/views/edit/editor/editor.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { Slate, Editable, withReact, ReactEditor, RenderElementProps } from "slate-react"; -import { createEditor, Node } from "slate"; -import { withHistory } from "slate-history"; -import { css } from "emotion"; -import { withHelpers } from './withHelpers'; -import { isImageElement, isLinkElement, isVideoElement } from '../util'; -import { EditLinkMenus } from './blocks/links'; -import { useDecorateMarkdown, MarkdownLeaf } from './blocks/markdown'; -import { Image, Video } from './blocks/images'; - -export interface Props { - saving: boolean; - value: Node[]; - setValue: (n: Node[]) => any; -} - - -const renderElement = (props: RenderElementProps) => { - const { attributes, children, element } = props - - // NOTE: This is being called constantly as text is selected, eww - // todo: I could use !isTypedElement, return early, then use a switch with - // type discrimination here to avoid the need for these type checking - if (isImageElement(element)) { - return - } else if (isVideoElement(element)) { - return